# OVP Active Backlog

**Updated:** 2026-05-03
**Status:** Active implementation backlog source

This file is the single current backlog entry point for implementation sequencing. Completed items are archived in [docs/backlog-archive.md](docs/backlog-archive.md).

It is not the only evidence source. It is the maintained merge view over:

- repo milestone history and shipped phase docs
- `docs/plans/2026-04-22-vision-and-roadmap-trusted-reuse-compiler.md`
- recent KSR task extraction in `/Users/chris/Documents/ovp-vault/30-Projects/Active/OVP-Knowledge-State-Runtime.md`
- `docs/plans/2026-04-29-reader-product-shape-and-backlog-reconciliation.md`
- kg-eval quality assessment and `OVP_FIX_PLAN.md` (2026-05-01)

Rule: historical plans and vault research notes feed this file; they do not override it silently. When a PR lands or a KSR task changes state, update this file first, then mirror or annotate secondary docs as needed.

## Current Milestones

| Milestone | Status | Meaning |
| --- | --- | --- |
| M0–M3 | Done | Foundation, operator workbench, roadmap consolidation, reader-first atlas (see [archive](docs/backlog-archive.md)) |
| M4 KSR Safety And Hot-Path Hardening | Done | projection labels, hot-path audit, wiring evals, article routing preview, evidence spans, candidate risk, JSONL streaming, projection lifecycle hardening, runtime-state API fixes (PR #98, #99, #100) |
| M5 Context Pack And Operational Runtime | Done | session snapshots, context budget, runtime state in `/ops` and doctor, provider-facing runtime-state API, action queue health |
| M5a Quality And Dedup Hardening | Done | concept dedup pipeline integration, promote semantic guard, historical data cleanup (PR #101) |
| M5b Slug-Level Dedup Apply | Done | concept_dedup apply at threshold 0.75 (non-default override; code default=0.82): 297 clusters, 334 duplicates archived to 70-Archive/dedup-merged/, vault reduced 7020→6686 Evergreens |
| M6 Policy, Permission, And Knowledge Evolution | Later | permission layer, claim lifecycle, conflict detection, policy promotion |
| M7 Semantic Extraction And Query Feedback Loop | Later | relation extractor, query feedback, routines, notebook/raw-source mode |
| M8 Type Unification And Extraction Quality | Active | unified object kind taxonomy, Canonical-State `entity_type` frontmatter, body-size-aware extraction, quote-grounding, single-pass LLM refactor |
| M9 Pack As Domain Ontology | Next | pack-defined object kind specs, typed relation constraints, schema registry, domain-specific extraction profiles |
| M10 Operational Knowledge Layer | Later | action types on objects, permission + contract, cross-entity aggregation, decision memory |
| M11 Source Authority And Cross-Source Identity | Done | typed source-authority providers, entity layer, runtime resolver, refresh wrapper, db backup (PRs #112–#124) |
| M12 Extraction-Time Entity Prime And Auto-Wikilink | Done | entity_aliases view, LLM extractor primed with known entities, auto-wikilink CLI (BL-038/039/040, PRs #126–#128) |
| M13 Synthesis Layer (Crystal) | Done | Louvain communities + LLM-synthesized community crystals + contradiction crystals + append-only versioning + sampling-aware renderer (BL-041/042/043/044, PRs #130-#136) |
| M14 Curated Atlas (Crystal Read Model) | Done | `crystal_scores` Projection (#142) + Curated Atlas markdown export (#143) + page_fts indexing of crystal bodies (#144) + big-community splitter inside `_detect_communities` (#145) + reuse-feedback wired into BL-045 scoring + `/atlas/curated` HTTP route + `/api/atlas/curated` JSON endpoint (BL-045/046/047/048/049). Tag + entity facets deferred to a small follow-up. See `docs/plans/2026-05-04-m14-curated-atlas.md` |
| M15 Architecture Language Cleanup | Done | Six-term architecture contract (Source / Candidate / Canonical State / Projection / Access Surface / Governance), ARCHITECTURE.md rewrite, doc split into RUNTIME / PACKS / PRODUCT_SURFACES / GLOSSARY, doc lint with phased rollout (PR #139) |
| M16 Reader / Maintainer Surface Split | Active | Hard-split the UI shell into a Reader product (`/`, `/search`, `/topics`, `/object`, `/note`, `/topic`, `/map`, `/explore`) and a Maintainer product (`/ops/...`).  URL prefix decides shell; old top-level maintainer paths 301 to `/ops/<same>`.  BL-050 split the routes; BL-051 unified Reader vocabulary (Topic / Featured Topics / open question), folded the redundant Curated Atlas card into Top Topics, and capped the visual map.  BL-052 (audit-only, no code) covers Maintainer-side vocabulary chaos. |
| M17 Intake Hardening (BL-058) | Done (v0.13.0) | URL preservation through deep-dive (C1), deprecate legacy 13-section LLM rewrite as default (C2 — `skip_deep_dive=True`), `ovp-dedup-cleanup` CLI for existing dups, **global URL dedup across the active staging chain** (Clippings + 02-Pinboard + 01-Raw + 02-Processing + 03-Processed) wired at every intake site with downstream-first priority; `source_dedup_skipped` audit events carry `stage`; `ovp-fidelity-sample` + `ovp-prompt-ab` measurement CLIs for the absorb v2 review (PRs #170, #171, #172). |
| M18 Trust-Aware Compiler Hardening | Next | Lock the fidelity foundations before scaling Interpretation features. Three locks: (a) **single-writer invariant** for canonical fields — every `objects` / `provenance` / `claims` / `relations` column has exactly one owner module (BL-060); (b) **prose-level revision history** for evergreens so any LLM rewrite is reviewable + rollback-able (BL-061); (c) **two-pass absorb routing** — replace the single big LLM call with a cheap router (decides update-or-create against existing evergreens) + per-target focused extractors, moving dedup from after-the-fact to write-time (BL-062). Depends on no other milestone. Origin: 2026-05-09 strategy review against Arkon + Rowboat. |
| M19 Live Concept Interpretation Surface | Later | Give the **Interpretation ledger** — the missing 4th of the four-ledger model **Artifact / Claim / Interpretation / Commitment** (saved in operator memory, distinct from M15's six-term architecture contract) — a first-class surface. User-declared `30-Projects/Tracking/<topic>.md` files with `type: live-concept` frontmatter — agent maintains the synthesis sections from scope_evergreens, user owns `## My take`, contradictions detected against the user's stated view (not globally). Depends on M18 landing first (single-writer + revisions enable agent-only patches; two-pass routing improves scope-evergreen quality). Origin: 2026-05-09 strategy review. |
| M20 Cognitive Surface — vault talks back + programmable tasks | Done | OVP today computes synthesis (crystals, scores, contradictions, open_questions) but doesn't push.  M20 closes the loop with three additions: (a) **USER.md + OVP_RULES.md** (BL-075) — user identity and autonomous-action contract become LLM context for every relevant call site; (b) **QUEUE → GENERATED task dispatcher** (BL-076) — drop `RESEARCH-*.md` / `SYNTHESIZE-*.md` into `50-Inbox/02-Tasks/`, handler runs LLM, output lands in `40-Resources/Generated/YYYY-MM/`; (c) **`/digest` daily synthesis** (BL-077) — a `DIGEST-daily` cron task aggregates last-24h crystals + top contradictions + USER focus into a ~200-word morning brief, surfaced as a Reader-home banner card.  Independent of M18/M19 — runs in parallel.  Plan: `docs/plans/2026-05-11-m20-cognitive-surface.md`. |
| M23 Digest Redesign — Crystal-only → Daily Knowledge Feedback | Next | Three weeks of M20 daily-digest use proved the design wrong at the **input layer**, not the prompt: the digest reads from `community_crystals` + `contradiction_crystals` + `crystal_scores` only, but crystal synthesis runs ~weekly while ingestion runs daily, so a 5–9 day lag makes every morning's digest read the same stale row.  Operator drops 10 articles → digest can't see them until synthesis runs.  M23 rebuilds the digest as **four-layer daily knowledge feedback**: Layer 0 today's intake from `audit_events`, Layer 1 evergreen delta from `evergreen_revisions` (the spine), Layer 2 connection to existing crystals/contradictions, Layer 3 pipeline-state backpressure (incl. stale-crystal flag — a cluster with a 7-day-old crystal + 8 new evergreens counts as *unsynthesized*).  Window is operator-local (`[last_successful_digest_at, as_of]`), not UTC midnight; tz config via new `<vault>/.ovp/digest.yaml` (follows `llm_profiles.yaml` precedent — no unified `settings.yaml` yet).  Input-hash idempotency gate kills the 5/12-style "5 redundant LLM passes" thrash.  Stage 2 includes a **data-readiness preflight** (Codex P2): `evergreen_revisions` table existence, recent rows, change_note quality sample, graph_clusters non-empty — handler renders degraded sections instead of crashing.  Stage 4 link-only in v1 (no POST mutations from Reader-side digest); mid-day "Regenerate digest now" button explicit (not cron-only).  Success metrics measurable: ≥ 50% click-through, 100% intake-reflection on days with ≥ 3 articles, ≥ 20% question follow-through.  Sequenced strictly after M22 merges.  Plan: `docs/plans/2026-05-13-m23-digest-redesign.md`. |
| M26 Daily Observability And Explainable Zeros | Done | Real use of M25 `/ops/today` showed the remaining maintainer UX gap: the page faithfully renders raw `audit_events`, but users read the numbers as item-level workflow movement.  A `0` currently means only "no evidence row in this bucket"; it cannot distinguish "stage did not run", "ran with no input", "ran with no output", "failed", "telemetry missing", or "projection stale".  M26 defines one top-line health verdict plus four daily views — Activity (event time), New Intake (intake time), Workflow Progress (state transition time), and Current Backlog (current projection) — then adds item-count semantics, intake cohorts, staleness checks, run-level stage telemetry, and zero reasons.  **Shipped 2026-05-16: all of BL-100…109 merged (PRs #251, #252, #253, #254, #255, #256, #258, #259, #260); issue #250 closed by #259.  Dogfooded on the operator vault; the three date-axes reconcile.**  Plan: `docs/plans/2026-05-16-m26-daily-observability.md`. |
| M27 Maintainer Architecture Health | Next | Post-M26 architecture-hygiene debt, NOT a product gap and NOT an M26 blocker — M26's semantic fixes shipped; this is the maintainability tail surfaced by `tests/test_architecture_fitness.py` (2 failed: file-size limits + sqlite boundary).  M26 added significant code to the already-oversized `view_models.py` / `_ui_renderers.py`, making continued feature work progressively harder to review.  Three slices, priority BL-110 > BL-111 > BL-112: split the maintainer-UI mega-files by product surface (BL-110), converge direct `sqlite3` access behind a data-access facade/allowlist so schema/projection edits stop being project-wide blast radius (BL-111), split `unified_pipeline_enhanced.py` into stage orchestration / CLI / artifact IO / step impls (BL-112).  No user-visible change; reduces future bug-injection risk and review cost. |
| M22 Anchored inquiry as a sidebar + Digest history | Next | M21b shipped "Ask about this" as a page-navigation, which on first real use surfaced four user-story failures: jumping breaks reading context, profile picker shows no real choice today, visibility ask happens before the answer exists, composer textarea is too small.  M22 reshapes the surface: inline **right-side drawer** opens on the same Reader page so the artifact stays in view (BL-090); composer is ephemeral-first — Save / Absorb / Discard happen *after* the operator sees the answer (BL-089 splits `run_turn` into `run_turn_in_memory` + `persist_turn`; BL-091 wires the three affordances); profile picker hides when all profiles share a model + visibility radio moves out of pre-submit + textarea autosizes (BL-092).  Side fix: daily digests get a `/digests` list view + prev/next nav on each digest (BL-093) — filesystem scan over `40-Resources/Generated/digests/`, no projection.  Backend primitives (BL-082/083/084) unchanged.  `/chat?id=…` page kept as direct-URL + no-JS fallback.  SSE streaming still deferred.  Plan: `docs/plans/2026-05-13-m22-anchored-inquiry-drawer.md`. |
| M21 Anchored Inquiry Surface — *Ask the vault about what you are reading* | Done | OVP's third Reader-side push/do surface (after Top Topics / Digest), scoped down by Codex review from "chat app" to "anchored inquiry primitive".  Sub-milestones: **M21a Anchored Inquiry MVP** — provider profiles (BL-081), inquiry markdown schema + fileops (BL-082), two-layer context binder anchor+retrieval (BL-083), headless handler + `ovp-ask` CLI with three-tier audit-ledger cost guardrail and **explicit write-back hook** that emits `ABSORB-chat-<id>-turn-<n>.md` into the existing absorb queue (BL-084).  **M21b Reader UI** — synchronous `/chat` page + Fast/Balanced/Deep profile dropdown only (no raw provider strings in UI) (BL-086) + "Ask about this" buttons on `/note` / `/object` / `/topic` / digest (BL-087); SSE streaming + interrupt recovery deferred to a follow-up.  **M21c History library** — `knowledge.db.chats` projection with `pages_index` + `page_fts` shadow rows for `visibility: indexed` sessions only (BL-085) + Reader `/chats` list grouped by status (BL-088).  Depends on M20 (`context_loader`, USER.md/OVP_RULES.md, QUEUE pattern) and reuses `ovp-query`'s retrieval helpers — does NOT reimplement RAG.  Invariant: **inquiry transcripts are canonical artifacts, not canonical knowledge** — anything entering `objects` / `claims` / atlas must go through the existing absorb / promote / review pipeline via BL-084's write-back hook.  Visibility field `indexed` or `unindexed`; "private" never appears in UI copy.  v0.21.0.  Origin: 2026-05-12 Codex reviews (two passes).  Plan: `docs/plans/2026-05-12-m21-chat-surface.md`. |

## Recently Shipped (PRs #98–#139)

| PR | What shipped |
| --- | --- |
| #98 | P0+P1 bug fixes from review: JSONL streaming, advisory file lock, runtime-state API JSON 400, reader UI resilience |
| #99 | Scoped incremental quality checks |
| #100 | Four-phase architecture refactor: JSONL read/write streaming, truth_api module boundary cleanup, ui_server route hardening (CSP/CSRF), projection lifecycle markers |
| #101 | P0-P2 from OVP_FIX_PLAN: concept_dedup pipeline integration with scope_slugs, promote semantic guard (trigram-Jaccard), historical Evergreen data cleanup (71→61), find_similar_slugs utility |
| #109 | Typed StepResult contracts — eliminate silent step-to-step fallbacks |
| #110 | Missing llm_client.py + dedup backfill against extraction log |
| #111 | Four pipeline guardrails — schema audit, kill silent imports, metrics, E2E |
| #112 | Liberate evergreen extractor prompt from 3-5 cap + null-escape (PR-A) |
| #113 | Source-authority subsystem PR-D1 + D2: domain rules, author whitelist, GitHub, arXiv |
| #114 | Source-authority discovery + LLM-judge + yaml overrides (PR-D3) — `ovp-source-coverage`, `ovp-score-domain`, `domain_overrides.yaml`, `author_overrides.yaml` |
| #115 | Twitter author entity layer + twitterapi.io backfill (PR-E1) — 521 entities, ~$0.10 |
| #117 | `ovp-backup-db` — point-in-time snapshots of knowledge.db via SQLite online backup API |
| #119 | GitHub project + user entity backfill (PR-E2) — 922 entities, $0; cross-platform `twitter_username` linkage harvested |
| #120 | Source-authority scorer reads entity table + identity merge (PR-E3) — 54 self-reported person merges auto-applied |
| #121 | GitHubSignalProvider entity-table fast path (PR-E4) — ingest-time scoring shortcuts to entity table, zero HTTP for backfilled repos |
| #122 | `ovp-refresh-source-authority` chained entity refresh + launchd plist (PR-E5) |
| #123 | `person` → `person + organization` split (PR-F1) — 54 → 37 person + 17 organization on real vault |
| #124 | 12 entity-layer review fixes: read-side write side effects, identity merge backlinks, lock-steal race, append-only history, excluded-host signal, GitHub bare profile URLs, source_coverage entity-aware unknowns, score_sources --domains-only honors overrides |
| #126 | `entity_aliases` view + `ovp-entity-aliases` CLI (BL-038, M12): unified read surface across authors.jsonl, author_overrides.yaml, entities table, github_user back-link |
| #127 | Extraction-time entity prime in `auto_evergreen_extractor` (BL-039, M12): top-N canonicals injected into LLM user prompt so name variants collapse to one handle |
| #128 | `ovp-link-entities` auto-wikilink CLI (BL-040, M12): scans evergreen prose, inserts `[[canonical_handle]]`, generates `10-Knowledge/Entity/<handle>.md` stubs; PreparedMatcher for batch reuse |
| #129 | M12 review-pass: path-traversal slug allowlist on canonical_handle; CJK alias boundary fix (ASCII-only boundary for non-ASCII aliases); collision-logging fires regardless of winner; M12 docs sync |
| #130 | Louvain community detection (BL-041, M13): replaces connected-component clustering in `truth_projection.py`; `cluster_kind="louvain_community"`; deterministic seed |
| #131 | Community Crystal MVP (BL-042, M13): `ovp-synthesize-community-crystals` + `community_crystals` table; MiniMax-M2.7-highspeed default; append-only PK |
| #132 | Contradiction crystals (BL-043, M13): `ovp-synthesize-contradiction-crystals` + `contradiction_crystals` table; deliberately preserves tension as "open question" crystal; resolved contradictions skipped |
| #133 | Crystal append-only versioning (BL-044, M13): `superseded_by_synthesized_at` on both crystal tables; archive helper moves prior live markdown to `70-Archive/Crystals/<safe-id>/<ts>.md`; `ovp-list-crystals` surfaces version chains |

## Active Backlog

| ID | Priority | Status | Work item | Source links |
| --- | --- | --- | --- | --- |
| BL-025 | P0 | Done (Phase 1) | Unified Object Kind Taxonomy.  The "three type systems" (concept_registry.kind / truth_store.object_kind / view_models labels) were already collapsed into `object_kinds.py` pre-BL-025; this PR fixes the remaining gap — `auto_evergreen_extractor` was binary-collapsing v2's 10 unit-type values into `method` or `concept` for `entity_type`, producing 89% `entity_type: concept` on the live vault.  Added `V2_UNIT_TYPES` frozenset + labels + identity mapping in extractor and promote.  Reader filters and pack `object_kind_specs()` (M9) can now actually discriminate. | M8 |
| BL-026 | P0 | Done (Phase 1) | Extraction pipeline entity_type output.  v2 absorb already produced `unit_type`; the conflated `entity_type` now matches it (each of fact / method / procedure / tradeoff / failure_mode / counterexample / case_detail / learning / decision / quote is a valid `entity_type`).  Promote (`write_evergreen_file`) carries the mapping forward.  Out of scope: historical backfill (BL-030) and Reader type-facet UI (separate UX). | M8 |
| BL-027 | P1 | Active | Body-size-aware extraction (P3): auto_evergreen_extractor prompt includes article body length and bounded target count | M8, OVP_FIX_PLAN P3 |
| BL-028 | P1 | Active | Quote-grounding requirement (P4): promote rejects candidates without source-grounded quotes | M8, OVP_FIX_PLAN P4 |
| BL-029 | P2 | Done + UX-cleanup landed (PR #182, PR #183) | Deep-dive layer deletion — `auto_article_processor` is now intake-only (image download + frontmatter parse + lifecycle archive).  Removed: `class ArticleProcessor` (6-dim LLM rewrite + classify_article + create_embedded_evergreens), `LiteLLMClient` (article copy), `init_llm`, `_resolve_article_links`, `_write_resolution_sidecar`, `_upsert_candidates`, `_augment_frontmatter`, `_prepare_interpretation_source`, `_clean_body_text`, `_has_substantive_content`, `_looks_like_paper_source`, `_extract_html_text`, `_fetch_url_text`, `_fetch_docs_fallback_text`, `--keep-deep-dive` CLI flag, `skip_deep_dive` constructor param.  ~600 lines removed.  PR #182 swept the remaining UX vestiges across truth_api / view_models / renderers / packs (119 references in 31 files) and added pagination + D3 cluster viz; PR #183 rewrote the `/object` provenance card around the post-BL-029 chain (Source URL → Source File → Pipeline Stages → Evergreen).  Page IA documented in `docs/page-ia-post-bl029.md`. | M8 |
| BL-030 | P1 | Done (Phase 1) | Historical Evergreen entity_type backfill.  Two-phase design — Phase 1 deterministic ``unit_type → entity_type`` rewrite for v2 evergreens whose ``entity_type`` was set by the pre-BL-025 collapse (zero LLM cost), Phase 2 LLM classification for v1 evergreens with no ``unit_type``.  Live run rewrote 1841 v2 evergreens in 0.7s; ``entity_type: concept`` share dropped from 89% → 72.5%.  Phase 2 (re-classify the remaining 6848 v1 ``concept``-typed evergreens via LLM) deferred to a separate run since it costs ~$30-50 + several hours. | M8 |
| BL-031 | P1 | Next | Pack object_kind_specs() API: BaseDomainPack defines object kinds with properties and reader layouts | M9 |
| BL-032 | P2 | Next | Pack typed relation constraints: relation types carry source/target kind constraints | M9 |
| BL-033 | P2 | Next | Schema registry and lint: `ovp-schema list`, `ovp-lint --schema` validates entity_type against pack schema | M9 |
| BL-034 | P2 | Next | Domain-specific extraction profiles: different packs produce different entity_type distributions | M9 |
| BL-015 | P1 | Later | Permission layer and claim lifecycle fields | M6, KSR-005, KSR-006 |
| BL-016 | P2 | Later | Conflict detection regularization, high-risk profile gate, candidate compaction with restore | M6, KSR-007, KSR-008, KSR-024 |
| BL-017 | P2 | Later | Schema-on-demand guard for new claim/candidate profiles | M6, KSR-028 |
| BL-018 | P2 | Later | Reviewed semantic relation extractor and query feedback loop | M7, April 22 roadmap |
| BL-019 | P2 | Later | Skill/routine extraction profile, notebook/raw-source mode, ingest ROI, hybrid retrieval, multimodal caption-first ingest, follow-up object model | M7, KSR-010, KSR-011, KSR-012, KSR-016, KSR-019, KSR-029 |
| BL-022 | P1 | Later | Decision context memory: first-class rationale, rejected alternatives, dissent, owner, participants, and validity windows for high-value decisions | M6/M7, KSR-030 |
| BL-023 | P1 | Later | Agent workspace substrate and information-health loop | M6/M7, KSR-031, KSR-032 |
| BL-024 | P1 | Later | Machine-facing memory substrate: typed scored memory records with freshness, evidence count, supersession, last-used telemetry, and token-budgeted selective injection | M6/M7, KSR-033 |
| BL-035 | P1 | Later | Action types on objects: typed operations (review, decide, verify) with preconditions, postconditions, audit trail | M10 |
| BL-036 | P2 | Later | Cross-entity aggregation: typed-mention index, property aggregation by kind | M10 |
| BL-037 | P1 | Next | Body-level semantic dedup: use page_embeddings cosine similarity to detect paraphrastic clones (same concept, different slug names); slug-trigram-Jaccard cannot reach these | M5b, OVP_FIX_PLAN |
| BL-038 | P0 | Done | entity_aliases view + `ovp-entity-aliases` CLI (PR #126). Single read surface for BL-039/040. | M12 |
| BL-039 | P0 | Done | Extraction-time entity prime in `auto_evergreen_extractor` (PR #127). Top-N entity_aliases injected into the LLM user prompt so name variants resolve to one canonical handle. | M12 |
| BL-040 | P1 | Done | `ovp-link-entities` auto-wikilink CLI (PR #128). Walks `10-Knowledge/Evergreen/`, replaces alias hits with `[[canonical_handle]]`, generates `10-Knowledge/Entity/<handle>.md` stubs. | M12 |
| BL-041 | P1 | Done | Louvain community detection replaces the connected-component clustering in `graph_clusters`.  Communities are now the default product output of the truth projection; `cluster_kind="louvain_community"` (internal label).  Wired into `_build_graph_seeds`, no separate CLI — runs on every `rebuild_knowledge_index`. | M13 |
| BL-042 | P0 | Done | Community Crystal MVP — `ovp-synthesize-community-crystals` reads Louvain communities from `graph_clusters`, sends top-K member evergreens (deterministic order; authority weighting deferred to v2) to MiniMax-M2.7-highspeed, writes `40-Resources/Crystals/<sha>.md` and persists lineage in the new `community_crystals` table.  Append-only schema (PK includes `synthesized_at`) so BL-044 versioning lands without migration. | M13 |
| BL-043 | P1 | Done | `ovp-synthesize-contradiction-crystals` reads `status='open'` rows from the `contradictions` table, sends each side's claim_text + source evergreen body to the LLM, and writes `40-Resources/Crystals/contradiction-<sha>.md` plus a `contradiction_crystals` lineage row.  System prompt is deliberately framed as "open question" — does not try to resolve, only to expose the tension.  Resolved contradictions are skipped. | M13 |
| BL-044 | P1 | Done | Crystal append-only versioning — both `community_crystals` and `contradiction_crystals` carry `superseded_by_synthesized_at`; supersede helper flips the prior current row's pointer + archives its markdown to `70-Archive/Crystals/<safe-id>/<timestamp>.md` before the new row lands.  `ovp-list-crystals` surfaces the version chain. | M13 |
| BL-045 | P0 | Done | `crystal_scores` Projection — per-crystal weighted score (size_norm + credibility_norm + contradiction_norm + reuse_recency_norm + evergreen_recency_norm) persisted alongside its component signals.  Auto-rebuilt by `ovp-knowledge-index`; standalone `ovp-rescore-crystals` for ad-hoc re-scoring.  Schema-version 5 → 6.  reuse_recency_norm is a forward-compat placeholder (always 0 until BL-049 lands). | M14 |
| BL-046 | P0 | Done | Curated Atlas Access Surface — markdown export at `40-Resources/CuratedAtlas.md` (PR #143) plus the `/atlas/curated` HTTP route + `/api/atlas/curated` JSON twin.  Top-N (default 30, max 100) crystals ranked by `crystal_scores`; each entry carries label, teaser, score breakdown, and a click-through to the underlying crystal markdown.  `/atlas` carries a back-link to the curated view.  Pure read against the DB — no LLM cost, no vault writes. | M14 |
| BL-047 | P1 | Done (FTS slice) | `page_fts` extended to index every current crystal body so `/search` returns crystals alongside evergreen pages.  Synthetic slugs `crystal:<safe-id>` / `contradiction:<safe-id>` distinguish the kinds.  Tag + entity facets deferred to a small follow-up — current crystals share uniform tags so the tag-facet has no signal yet, and the entity facet is ~3× the implementation cost. | M14 |
| BL-048 | P1 | Done | Big-community splitter integrated into `_detect_communities` itself.  Communities above `_SPLIT_THRESHOLD` (50 members) trigger a second Louvain pass on the sub-graph induced by their members; each sub-community becomes its own `graph_clusters` row + downstream community crystal.  Best-effort: communities with no internal edges or that re-merge into a single sub-community are kept whole.  Naturally idempotent on every projection rebuild. | M14 |
| BL-049 | P2 | Done | Reuse-feedback loop wired: BL-045's `reuse_recency_norm` now reads `reuse_events` rows tagged `object_kind IN ('community_crystal','contradiction_crystal')` over a rolling 30-day window, normalized by per-pack max.  Cold start (no events) keeps the signal at 0 — same as the BL-045 v0 placeholder.  As surfaces emit crystal-tagged events, the signal lifts naturally; no further code change required to scale. | M14 |
| BL-050 | P0 | Done | Reader / Maintainer hard split — URL prefix decides shell, Reader home rewritten around `crystal_scores` + Curated Atlas + Recent Crystals, 27 maintainer routes relocated under `/ops/*` with 301 back-compat, `?mode=operator` toggle removed, single cross-link in each shell footer.  See `docs/plans/2026-05-04-bl-050-reader-maintainer-split.md` (PR #150) | M16 |
| BL-051 | P0 | Done | Reader vocabulary unification + map cleanup — `/atlas/curated` → `/topics` (301 back-compat), page title `Curated Atlas` → `Featured Topics`, FTS prefix `[crystal]` → `[topic]` and `[contradiction]` → `[open question]`, Reader nav `Atlas` link → `Topics`, home drops the redundant Curated Atlas card and adds `See all N featured topics →` link inside Top Topics, `Recent Crystals` → `Recent Topics`, `/map` capped at 3 members per cluster with `?show_all=1` escape hatch + hover-only labels.  See `docs/plans/2026-05-04-bl-051-vocabulary-and-map-cleanup.md` (PR #151) | M16 |
| BL-052 | P1 | Done (audit) | Maintainer Vocabulary Audit — matrix doc enumerates every `/ops/*` surface (URL → renderer → view-model → backing entity → overlap class R/C/N/K → action).  Surfaces: 1 R-class duplicate (`Audit` nav label vs `/ops/events`), 4 C-class concept overlaps (Candidates/Evolution Candidates, Contradictions/Open Questions, Production/Evolution), 1 N-class mis-naming, 12 K-class.  See `docs/plans/2026-05-04-bl-052-maintainer-vocab-matrix.md`.  Implementation backlog → BL-053. | M16 |
| BL-053 | P1 | Done (Phase 2) | Ops 工作台 IA 重构 + 词汇清理.  **Phase 1**: by-time pivots ahead of the toolbox — new `/ops/today` (5-card daily digest), new `/ops/runs` index + `/ops/runs/<txn_id>`, nav rebuild (`Audit`→`Events`, `Candidates`→`Concept Candidates`).  **Phase 2 (PR #179)**: drop `/ops/deep-dives` (BL-029 producer gone); pulse navless fix; `/ops/objects` pagination + alpha/most-linked sort; `/ops/clusters` total + per-page + Show all; `/ops/events` date filter; `/ops/today` prev/next pivots + per-card "See all"; `/ops/timeline` day-card drill-down; `/ops/runs` by-day grouping + Idle markers + window pivot; queue consolidation under `/ops/queue/*` with 301s for legacy paths; `/ops` foyer (today/queue/last-run); page-help banners on every `/ops/*` surface; `docs/maintainer-workflow.md`. | M16 |
| BL-054 | P0 | Done | Crystal scoring substrate fix — `source_authority` schema wired into rebuild + JSONL replay; `objects.source_url` column populated from frontmatter; `ovp-backfill-provenance` CLI achieves 99.9% coverage on the live vault; `auto_evergreen_extractor` writes source provenance forward; `_credibility_sum` dedupes by source URL; new `source_diversity_norm` ratio signal; weight rebalance.  See `docs/plans/2026-05-04-bl-054-crystal-scoring-substrate-fix.md` (PR #152) | M14 |
| BL-055 | P0 | Done | Provenance spine — first-class `provenance` table with append-only PK `(pack, object_id, stage, derived_at)`; rebuild populates `stage='ingest'` rows from frontmatter; preserved across rebuilds; `ovp-doctor` reports provenance health.  See `docs/plans/2026-05-04-bl-055-provenance-spine.md` (PR #152) | M14 |
| BL-056 | P0 | Done | Stage emit hooks — every stage that creates / modifies a Canonical-State object writes a provenance row beyond the rebuild's ingest baseline.  Wired: `synthesize_community_crystal`, `synthesize_contradiction_crystal`, `promote` (concept review), and now `extract` (backdated to the candidate's ``absorbed_at`` so chain timestamps reflect when ``auto_evergreen_extractor`` produced the candidate, not when the human reviewed it).  Shared helper at `src/ovp_pipeline/provenance.py`.  See `docs/plans/2026-05-04-bl-056-stage-emit-hooks.md` | M14 |
| BL-057 | P2 | Next | Provenance lint enforcement — extend `ovp-doctor` to fail (not warn) when orphan_objects grows unexpectedly; `ovp-lint --provenance` rule that any newly-promoted evergreen without a `stage='promote'` row is an error.  Depends on BL-056. | M14 |
| BL-046b | P2 | Next | Crystal **tag facet** for `/search` — group results by crystal `tags` frontmatter.  Deferred from BL-047. | M14 |
| BL-047b | P2 | Next | Crystal **entity facet** for `/search` — join through `entity_aliases` to filter crystals by mentioned entity.  Deferred from BL-047. | M14 |
| BL-049b | P2 | Next | Surface-side `reuse_events` emission — Reader clicks on `/atlas/curated`, `/note?path=…/Crystals/...`, and FTS crystal hits write a `reuse_events` row tagged `object_kind='community_crystal'` so `reuse_recency_norm` lifts above zero.  Deferred from BL-049. | M14 |
| BL-060 | P0 | Next | **Single-writer invariant for canonical fields** — every column in `objects` / `provenance` / `claims` / `relations` gets exactly one **owner module** that's allowed to write it; other modules call the owner's helper instead of writing directly.  Origin: PR #185 root cause was three modules (frontmatter rebuild, `ovp-backfill-provenance`, `ovp-backfill-objects-source-url`) writing `objects.source_url` independently → reconciliation race.  Scope: (a) audit current write sites for each canonical column; (b) define owner-module map in `docs/canonical-write-ownership.md`; (c) refactor violations to call owners; (d) add `tests/test_architecture_fitness.py` rule that prevents new direct writes.  Out of scope: locking inside owners (already covered by `withFileLock` / `knowledge_db_write_lock`).  Sized: 2 PRs, ~1500–2000 LOC. | M18 |
| BL-061 | P1 | Next | **Prose-level evergreen revision history** — new `evergreen_revisions(pack, object_id, version, content_md, change_type, changed_by, derived_at, change_note)` table, immutable append-only.  Write hooks at each owner module from BL-060 covering stages: `extract`, `promote`, `editor_edit`, `llm_rewrite`, `rollback`.  Mirrors Arkon's `WikiPageRevision` model.  Enables: "what did this evergreen say before absorb v2 rewrote it" + per-revision diff in `/object?id=…&tab=history` + `ovp-rollback-evergreen <slug> <version>` CLI.  This is the load-bearing table for fidelity audit work.  Depends on BL-060 (so each owner module owns its own revision write).  Sized: 1 PR, ~600–1000 LOC. | M18 |
| BL-062 | P0 | Next | **Two-pass absorb routing** — decompose `auto_evergreen_extractor`'s single big LLM call into:  (Pass 1) cheap structured-output router that reads source + a pre-built **evergreen index** (slug + summary + key claims, compact) and emits `{updates: [{slug, evidence_segments}], creates: [{title, rationale}]}`;  (Pass 2) per-target focused extractor that update-or-create against the routed target with full context of which other evergreens are in scope (so wikilinks resolve to real slugs, not fabricated ones).  Primary win is **quality, not cost**: write-time dedup replaces after-the-fact `concept_dedup` → all source_anchors accumulate on the same evergreen instead of being lost in dedup-merge → cross-source consistency for repeated concepts → wikilinks resolve to existing slugs.  Measurable success: source_anchor count per evergreen up; new-evergreen-with-similar-existing rate down; concept_dedup archive volume down.  Depends on BL-060 (writes go through cleaner owner module).  Retire `concept_dedup` as primary path post-shipping (keep as belt-and-suspenders).  Sized: 1 PR, ~1000–1500 LOC. | M18 |
| BL-063 | P1 | Next | **Live Concept primitive (精选派 / curated-selective subset)** — declarative `30-Projects/Tracking/<slug>.md` files with `type: live-concept` frontmatter carrying `objective`, `scope_evergreens` (list of evergreen slugs this concept tracks), and `triggers` (`on_ingest_match` / `on_contradiction_against_view` / `weekly_resynthesis`).  Body has user-owned sections (`## My take`) and agent-owned sections (`## Current synthesis` / `## Recent evidence` / `## Tensions`); section ownership enforced via HTML-comment markers.  Agent uses Rowboat-style **patch-style edits** restricted to agent-owned sections — adopts the **same single-writer principle as BL-060** but applied at markdown-section granularity rather than DB-column granularity (BL-060 itself is DB-only).  Gives the **Interpretation ledger** (the missing 4th of the Artifact / Claim / Interpretation / Commitment four-ledger model) its first-class surface.  *Excludes:* narrow firehose (BL-059), automatic scope expansion, multi-objective per concept.  Depends on BL-060/061/062 landing first.  Sized: 3 PRs (data model + scheduler; 3 trigger implementations; agent prompt + section-aware edits), ~2500 LOC total. | M19 |
| BL-059 | P3 | Later | **Narrow firehose for Live Concepts** — declarative RSS / Twitter-list / keyword pull *bound to a specific Live Concept*, populating only that concept's `## Inbox` region (NOT the main truth store).  User does promote-or-discard decision per item; only promoted items run through absorb v2 → evergreen.  Keeps human attention curation as the canonical signal while solving the精选派 (curated-selective) blind-spot ("you don't know what you don't know" within a declared topic).  Defer until BL-063 ships and we have actual Live Concept usage data — premature without that.  See 2026-05-09 strategy review for the精选派 (curated-selective) vs 广撒网 (broad-net / firehose) framing. | M19+ |
| BL-067 | P1 | Done | **CLI auto-promote revision hook** — `record_promote_audit_pair` helper extracted from `truth_api._emit_promote_provenance`; the BL-067 hook in `auto_evergreen_extractor`'s auto-promote loop now writes a `change_type='promote'` revision row with `changed_by='cli:auto_promote'` whenever the CLI promotes via `--auto-promote`.  Pre-fix only the UI/MCP review path wrote revisions, so CLI auto-promotes left no audit trail.  PR #200. | M18 |
| BL-068 | P1 | Done | **Pluggable router LLM + index size cap** — `OVP_ROUTER_{MODEL,API_BASE,API_KEY,API_TYPE}` env vars let operators point the BL-062 Pass 1 router at a different provider than the main extractor (motivation: MiniMax M2.7-highspeed's 2K-token input cap can't host the router prompt; DeepSeek V4 Flash 1M via SenseNova OpenAI-compat unblocks the live vault).  Plus `OVP_ROUTER_MAX_INDEX_ENTRIES` (default 1000) bounds the rendered index at ~300KB so the router prompt fits 1M-context providers.  Granular audit statuses (`prompt_registry_error` / `request_error` / `empty_response` / `parse_error`) replace the single overloaded `parse_error` for triage.  PR #200. | M18 |
| BL-069 | P0 | Done | **Embedding-based top-K pre-filter for absorb router** — replaces BL-068's alphabetical truncation with cosine-similarity ranking.  Embed source via `embedding.embed_text` (Qwen3-Embedding-0.6B / hash fallback), top-K against `page_embeddings` joined to `objects` for pack scoping, return top-K slugs as `priority_slugs` to `build_evergreen_index`.  Strategy is recorded in `audit_events.payload_json` as `index_strategy` (`embedding_topk` / `alphabetical_fallback` / `alphabetical` / `explicit`).  Defensive: zero-norm source vectors (CJK + hash backend) fall back to alphabetical; dimensionality mismatch (hash 128-d vs Qwen 1024-d) falls back to alphabetical.  Aggregated chunk vectors L2-renormalised so cosine = dot-product holds for multi-chunk evergreens.  Live result: same article that routed `0 updates / 4 creates` under alphabetical now routes `1 update / 3 creates` — surfaces `ai-maturity-framework-l0-to-l4` as the UPDATE target that the first-1000 alphabetical slice missed.  PR #201. | M18 |
| BL-070 | P2 | Done | **`ovp-knowledge-index --audit-sync-only`** — re-ingest `audit_events` from JSONL logs WITHOUT a full projection rebuild.  1.2s on the live vault vs 20+ min for full rebuild.  Bypasses `_ensure_knowledge_db` to avoid triggering an implicit rebuild on schema mismatch (codex P2: would defeat the "fast no-rebuild" promise).  Plus fixes: `evergreen_revisions` added to `INDEPENDENT_CANONICAL_TABLE_COLUMNS` preserve list so it survives rebuilds (was being wiped every time); `recent_audit_events` accepts `event_type` + `since` SQL filters so BL-063 Live Concept scheduler isn't truncated by noisy logs.  PR #202. | M18 |
| BL-071 | P0 | Done | **03-Processed sources are absorb-eligible** — closes the post-BL-058 chain end-to-end.  `_reject_intake_source_target` narrowed to block only WIP intake stages (Clippings / 01-Raw / 02-Processing); 03-Processed is the durable absorb-input layer by design.  New `_is_intake_only_source_markdown` detector accepts clippings / pinboard / hand-curated raws (has frontmatter + source URL + body ≥ 200 chars + not deep-dive + not `extraction_status: skipped`).  Same detection applied across all four absorb entry points: `_collect_absorb_targets(file_path=)`, `_collect_absorb_targets(directory=)`, `AutoEvergreenExtractor.process_directory`, `_collect_processed_github_sources` (recent scan).  Live verified: first-ever `ovp-absorb --file 03-Processed/X.md` on a raw clipping (no deep-dive step) produced 4 evergreens.  PR #203. | M18 |
| BL-072 | P2 | Later | **Router-as-primary** — flip BL-062 shadow router from audit-only to actually driving extraction.  Pre-req: enough shadow data to validate parse rate (≥90%), update/create decision alignment with `concept_dedup`, and observed source_anchor accumulation.  Currently shadow data is too small (~10 OK runs) for statistical confidence; defer until D batch shows the router is reliable on the live vault. | M18+ |
| BL-073 | P2 | Later | **`ovp-pipeline --incremental` absorb step** — auto-run BL-071 absorb on 03-Processed sources at end of `--incremental`.  Trade-off: makes pipeline LLM-cost-heavy by default vs current "operator chooses when to absorb" decoupling.  Defer until ergonomics + cost are characterised. | M18+ |
| BL-075 | P0 | Active | **USER.md + OVP_RULES.md + LLM context loader** (M20 stage 0).  New `00-Polaris/USER.md` template (identity, current focus, voice, what to ignore) + new `OVP_RULES.md` (autonomous-action contract: never delete without confirmation, always date-stamp, when uncertain deposit to GENERATED and flag, log every write).  New `context_loader.py` module reads both files and concatenates as a system-prompt prefix.  Wired into `auto_evergreen_extractor`, `community_crystals`, `contradiction_crystals`, and the BL-076 task handlers.  Graceful degradation when files missing (returns `""`).  Zero behavioural change in existing flows when files don't exist.  Plan: `docs/plans/2026-05-11-m20-cognitive-surface.md`. | M20 |
| BL-076 | P0 | Active | **QUEUE → GENERATED task dispatcher** (M20 stage 1).  New `50-Inbox/02-Tasks/` watcher with filename-prefix → handler map: `RESEARCH-*` → research handler, `SYNTHESIZE-*` → synthesis handler, `CONTRADICT-*` → contradiction sweep, `DIGEST-*` → digest handler (filled in by BL-077).  Handlers run LLM with BL-075 context prefix; output to `40-Resources/Generated/YYYY-MM/<filename>.md`; original task archives to `70-Archive/tasks/`.  Rate-limit: 12 tasks / 24h / pack (configurable).  `task_dispatched` audit events to `pipeline.jsonl`.  Reuses AutoPilot's existing inotify loop (`autopilot/daemon.py`).  CLI: `ovp-task --file <path>` for synchronous one-shot.  Plan: see M20 plan doc. | M20 |
| BL-077 | P0 | Active | **`/digest` daily synthesis** (M20 stage 3).  AutoPilot cron at 06:00 enqueues `DIGEST-daily.md` into `50-Inbox/02-Tasks/`; BL-076 handler runs digest skill.  Digest aggregates (last 24h `community_crystals`, top-N `crystal_scores` contradictions, open `community_crystals` with tension, USER.md current focus, last 7d query_log) into a ~200-word brief with three sections: Tensions / Themes / Unanswered.  Output: `40-Resources/Generated/digests/YYYY-MM-DD.md`.  Reader Knowledge Library home (`/`) gets a banner card "Today's digest →" above Top Topics.  Plan: see M20 plan doc.  Success metric: ≥50% open rate over 4 weeks. | M20 |
| BL-081 | P0 | Done | **`.ovp/llm_profiles.yaml` + `llm_profiles.py` loader** (M21a step 1).  Four built-in profiles: Fast (MiniMax M2.7-highspeed), Balanced (Sonnet 4.6), Deep (Opus 4.7).  Per-use-case defaults (`default_for.chat / extraction / digest / router`) and per-pack limits (`chat_input_tokens_per_request`, `chat_output_tokens_per_request`, `chat_daily_tokens_per_pack`).  Loader exposes `resolve_profile(name)` + `profile_for_use_case(name)`.  Falls back to existing `AUTO_VAULT_*` env vars when the file is missing — legacy vaults see no change.  Companion `00-Polaris/MODELS.md` is hand-authored documentation only, not parsed.  Reader UI only shows the profile name (Fast/Balanced/Deep), never the raw provider/model string.  Plan: `docs/plans/2026-05-12-m21-chat-surface.md`. | M21a |
| BL-082 | P0 | Done | **`40-Resources/Chats/YYYY-MM/<topic-slug>-<short-hash>.md` schema + `chat_fileops.py`** (M21a step 2).  Canonical markdown with frontmatter (`type / status / visibility / anchor / profile / model / started_at / last_message_at / turn_count`) — token spend NOT in frontmatter, it lives in the audit ledger.  Body: alternating `## User · <ISO>` / `## Assistant · <ISO> · turn-N` H2 sections.  Each assistant turn carries an inline `<!-- context-manifest ... -->` HTML comment as a **read-only audit snapshot** (manifest is never re-read by the binder).  Stream-safe: assistant turns buffer to memory, append atomically on commit, interrupted streams write `## Assistant · ... · turn-N · interrupted` with `status: interrupted`.  `read_chat / append_turn / mark_interrupted / pending_lock` mirror `live_concept_fileops` so the heading-matcher helpers are shared.  Schema fixed before any handler writes. | M21a |
| BL-083 | P0 | Done | **Two-layer `context_binder.py` — anchor + per-turn retrieval** (M21a step 3).  **Anchor layer** (fixed per session): `note` / `object` / `crystal` / `standalone` — body + linked evergreens + cluster neighbours.  **Retrieval layer** (rebuilt per turn from the user's message): wraps `ovp-query`'s existing FTS, semantic search, `crystal_scores`, and open-contradiction helpers — does NOT reimplement RAG.  Anchor budget ≈ 60% of remaining, retrieval gets the rest; over-budget drops cluster neighbours → distant evergreens → low-score retrieval hits, anchor body always preserved.  `omitted_*` recorded with reason.  Manifest written into the transcript is an audit snapshot only — the next turn always rebuilds context fresh from current vault state. | M21a |
| BL-084 | P0 | Done | **Headless `chat_handler.py` + `ovp-ask` CLI** (M21a step 4).  CLI: `ovp-ask new --anchor <kind:ref> --profile <name> --message ...`, `reply --id`, `list [--status]`, `show --id`.  Non-streaming path first.  Three-tier cost guardrail enforced before the LLM call: (a) per-request input cap, (b) per-response output cap, (c) per-pack-per-UTC-day soft cap computed by summing `audit_events` (chat_turn_completed + chat_turn_failed payload counts — failures still count; unindexed sessions still count).  **Does NOT depend on BL-085**: audit-events ledger is the source of truth, not the `chats` projection.  Audit emits `chat_turn_completed / chat_turn_failed / chat_cap_hit / chat_summary_rebuilt / chat_writeback_handoff`; unindexed payloads record counts only, never bodies.  **Bundled BL-084b — write-back hook**: `ovp-ask absorb --id <chat> --turn <n>` writes `50-Inbox/02-Tasks/ABSORB-chat-<id>-turn-<n>.md`; nothing in M21 writes directly to `objects` / `claims` / atlas.  Default off — operator must invoke explicitly. | M21a |
| BL-085 | P0 | Done | **`knowledge.db.chats` projection + visibility-aware FTS** (M21c step 1).  Schema: `chat_id PK, pack, file_path, status, visibility, anchor_kind, anchor_ref, profile, model, started_at, last_message_at, turn_count, input_tokens, output_tokens`.  Indexed on `(pack, last_message_at DESC)`.  Visibility field is `indexed` or `unindexed` — "private" never appears in code or UI copy.  Search integration: `indexed` sessions get **both** a `pages_index` shadow row (synthetic slug `chat:<chat_id>`) **and** a `page_fts` row, since `/search` is `page_fts JOIN pages_index`; `unindexed` sessions get the `chats` row only and never reach search or the context binder's retrieval layer.  `ovp-knowledge-index` rebuilds all three tables from the markdown corpus — no mutable DB state. | M21c |
| BL-086 | P0 | Done | **Reader `/chat` page** (M21b step 1).  Routes: `GET /chat?id=<id>`, `POST /chat/message`, `GET /chats`.  Composer with profile dropdown showing **Fast / Balanced / Deep only** (no raw provider strings, no Custom entry — Custom belongs in CLI / yaml) and a "Don't index or reuse" toggle whose copy is honest: "OVP won't include this session in search, the inquiry list, or future context-binder retrieval.  The selected LLM provider still receives the current request."  Collapsible "Context anchored to X" card shows the BL-083 two-layer manifest including the omitted-items count.  Synchronous v1 — full page refresh per turn.  SSE streaming + interrupt recovery deferred to a follow-up. | M21b |
| BL-087 | P0 | Done | **"Ask about this" entry-point buttons** (M21b step 2).  Added to `_render_note_page` (thin + full shell), `_render_object_page`, `_render_topic_page`, and the digest preamble card.  Button links to `/chat?anchor=<kind>:<ref>` so the inquiry opens with the BL-083 anchor pre-bound; first user message triggers the retrieval layer.  Reader-side only — no Maintainer mirror.  Tracked by the success metric "anchor-bound inquiry ratio ≥ 60%". | M21b |
| BL-088 | P1 | Done | **Reader `/chats` list view** (M21c step 2).  Grouped by status (Pinned / Active / Archived collapsed by default).  Row: anchor pill + last-message timestamp + profile pill + turn count.  `unindexed` sessions hidden from this view by design — operator can still reach them via `ovp-ask show --id` or the file path.  No `/ops/chats` mirror — inquiry is a Reader surface, not a Maintainer one. | M21c |
| BL-089 | P0 | Next | **Ephemeral-first `run_turn` split** (M22 step 1).  Refactor `chat_handler.run_turn` into `run_turn_in_memory(...)` (LLM call + audit-events; no transcript file) and `persist_turn(session, kind=save\|absorb)` (writes the markdown + optionally the BL-084b write-back task).  The existing `run_turn` becomes a thin wrapper that does `run_turn_in_memory + persist_turn(kind="save")` so M21a CLI is unchanged.  Drawer's "Save / Absorb / Discard" affordance (BL-091) hangs off this split. | M22 |
| BL-090 | P0 | Next | **Inquiry drawer + entry-button rewire** (M22 step 2).  New `_chat_drawer.py` renderer + static JS asset (~150 LOC).  Drawer: right-side, default 380px, drag-resize persisted via `localStorage`, Esc closes.  "Ask about this" buttons (BL-087) change from `<a href="/chat?anchor=…">` to `<button data-anchor>` + click handler.  No-JS fallback wraps the button in `<a href>` to the page route.  New `POST /chat/drawer/message` endpoint streams the response (SSE later — v1 returns full POST result). | M22 |
| BL-091 | P0 | Next | **Save / Absorb / Discard affordance** (M22 step 3).  Three buttons render under each assistant turn in the drawer.  Save → `persist_turn(kind="save")` (writes transcript, lands in `/chats`).  Absorb → Save + BL-084b `writeback_to_absorb_queue`.  Discard → close drawer, nothing written beyond the already-emitted audit-event token spend.  Post-save banner: "Saved as [[chat-id]] · open in history".  This is the M21b user-story fix #3 — visibility decided *after* the answer. | M22 |
| BL-092 | P0 | Next | **Composer cleanup** (M22 step 4).  Textarea: 5-row min, `field-sizing: content` + `oninput` autosize fallback to 60vh max, full drawer width.  Profile picker: rendered under `<details><summary>⚙ Advanced</summary>` ONLY when `len({p.litellm_model for p in book.profiles.values()}) > 1` (i.e., when there's an actual choice to make).  Current state has all three profiles pointing at MiniMax-M2.7-highspeed → no disclosure at all.  Pre-submit visibility radio: **removed** from the composer — that decision moves to BL-091's affordance.  Applies to both drawer and the surviving `/chat?id=…` standalone page. | M22 |
| BL-093 | P1 | Next | **Digest history library** (M22 step 5).  Independent of the drawer work.  New `/digests` route + `_digests_list_page.py` — scans `40-Resources/Generated/digests/*.md` newest-first, one row per file (date + first-paragraph summary + link to `/note?path=…`).  Filesystem scan only — no `knowledge.db` projection (3600 files/decade isn't worth it).  Reader nav gains `Digests` entry next to `Chats`.  Each digest's preamble card gains "← yesterday" / "tomorrow →" links based on sibling files (disabled when sibling doesn't exist). | M22 |
| BL-094 | P0 | Next | **Digest data collector + preflight + `digest.yaml` config** (M23 step 2).  New `src/ovp_pipeline/digest_inputs.py` with `collect_digest_inputs(vault_dir, pack, *, as_of) -> DigestInputs` frozen dataclass (Layer 0/1/2/3 deterministic SQL+audit collectors).  Operator-local window `[last_successful_digest_at, as_of]` resolved via `tzlocal` with `<vault>/.ovp/digest.yaml` override.  Data-readiness preflight (6 checks: evergreen_revisions table + recent rows, audit_events Layer 0 events, change_note quality sample, graph_clusters non-empty, community_crystals non-empty) emits `digest_preflight` audit event and degrades gracefully — handler never raises.  Layer 3's "unsynthesized" includes stale-crystal flag (`MAX(evergreen_revisions.derived_at WHERE cluster=C) > community_crystals.synthesized_at`).  Tests with fixture vault covering each layer + each preflight failure path.  Pure data layer, no LLM call. | M23 |
| BL-095 | P0 | Next | **Digest prompt v2 + input-hash idempotency gate** (M23 step 3).  New `_DIGEST_SYSTEM_PROMPT_V2` — four-question structure (intake / new thinking / connection / worth-doing-next), drops "no call to action" rule, demands article-awareness without article-summary.  Handler emits new body shape with `schema_version: 2` + operator-local timestamps + per-section preflight degradation markers.  `input_hash = sha256(canonical_json({window_start, window_end, sorted ids per layer}))` — stable identifiers only, no wall-clock prose drift.  Gate skips LLM call on same-day re-runs with matching hash and emits `digest_skipped_no_change`.  Honest no-data path: when Layer 0+1+2 empty and Layer 3 unchanged, render 2-line acknowledgment, not fabricated brief. | M23 |
| BL-096 | P1 | Next | **Reader integration + mid-day regenerate button** (M23 step 4).  v1 **link-only** (no POST mutations from Reader surface — defers to BL-099).  Home banner teaser pulls from new `## Window's intake` section.  `/digests` list shows intake counts inline.  Digest body affordances: "Resolve contradiction" → `/ops/queue/contradictions?status=open`, "Run synthesis on cluster X" → `/ops/cluster?id=<cluster_id>`, "Read source" → `/note?path=…`.  Each click records `digest_clicked_through` audit event carrying action shape for Stage 5 metrics.  Maintainer "Regenerate digest now" POST button at `/ops/digest/regenerate` (config flag `mid_day_regenerate_button: true`) — input-hash gate makes redundant runs cheap. | M23 |
| BL-097 | P2 | Next | **Digest instrumentation + `/ops/digest-health`** (M23 step 5).  New audit events: `digest_clicked_through` (action_shape, target_slug), `digest_question_acted_on` (action_shape).  `/ops/digest-health` page or section: 14-day rolling charts of click-through-rate (target ≥ 50%), intake-reflection-rate (target 100% on days with ≥ 3 articles), question-follow-through-rate (target ≥ 20%).  If click-through < 30% at end of week 2, design failed → iterate before extending to BL-099. | M23 |
| BL-098 | P2 | Later | **Claim/relation/entity_mentions `created_at` ADDITIVE migration + Layer 1 v2** (M23.1).  Adds timestamp column to three tables under the M22 three-bucket policy (ADDITIVE — no rebuild required).  Extends `digest_inputs.py` Layer 1 to surface new claims and relations alongside evergreen deltas.  Single backfill: `created_at = COALESCE(extracted_at, NOW())` for existing rows.  Deferred from M23 v1 because schema migration is non-trivial and Layer 1 v1 ships value with just `evergreen_revisions`. | M23.1 |
| BL-099 | P2 | Later | **POST-driven digest action buttons** (M23.1).  v1 of M23 (BL-096) is link-only — operator clicks through to existing maintainer pages.  v2 adds in-place mutations from the digest itself: `POST /digest/synthesize-cluster?id=X`, `POST /digest/resolve-contradiction?id=Y&decision=…`.  Deferred from M23 v1 per codex review: adding mutation surface on a Reader-side feature is its own scope, not a digest concern.  Pre-requisite: BL-097's instrumentation shows ≥ 20% link click-through, proving the affordances are wanted. | M23.1 |
| BL-100 | P0 | Done | **Daily vocabulary, hierarchy, and view contract** (#251).  Lock `/ops/today` as one top-line health verdict plus four separate daily zones: Activity (event time), New Intake (intake time), Workflow Progress (transition time), Current Backlog (right-now).  Copy says which timestamp each number uses; explains why Activity / Received and New Intake can differ. | M26 |
| BL-101 | P0 | Done | **Activity item-count semantics** (#251).  Replaced raw event-count Activity cards with distinct item counts per state (source slug for Received/Extracted/NeedsAction, object id for Accepted, cluster id for Synthesized); drilldown still shows raw rows; regression tests assert card == drilldown distinct count. | M26 |
| BL-102 | P0 | Done | **Timestamp normalization + pack scoping for daily cards** (#251).  Replaced SQLite `date(timestamp)` over mixed formats with shared `audit_time` operator-local-day bucketing + consistent pack scoping (match included, different excluded, legacy pack-less only under default). | M26 |
| BL-103a | P0 | Done | **Staleness zero reasons** (#252).  Telemetry-free `audit_sync_stale` / `projection_stale` from JSONL-tail vs DB max-ts vs `ops_state.refreshed_at`; rendered as the top-line freshness verdict. | M26 |
| BL-103b | P0 | Done | **Stage run ledger and full zero reasons** (#254).  DAG-boundary `stage_*` telemetry (best-effort, never breaks the DAG); read-time stage-run rollup (no schema migration); full taxonomy `not_run`/`ran_no_input`/`ran_no_output`/`failed`/`telemetry_missing`/`healthy` wired into `/ops/today`. | M26 |
| BL-104 | P1 | Done | **Workflow Progress projection** (#255).  Distinct items that ENTERED a state on the day (earliest qualifying evidence == day); distinct from Activity (event rows) and Current Backlog (right-now). | M26 |
| BL-105 | P0 | Done | **Intake cohort view** (#253).  "Flow by intake day": sources whose first intake is day D, with current ops_state distribution + age + stalled; reuses BL-101 identity + current `ops_state`. | M26 |
| BL-106 | P1 | Done | **Digest uses intake cohorts, not only event-day activity** (#256).  `IntakeLayer.intake_cohort_sources` + honest-no-data gate + Layer 0 prompt so a day's digest acknowledges same-day saves even when absorb runs later. | M26/M23 |
| BL-107 | P2 | Done | **`ovp-refresh-ops` heavy-rebuild watermark** (#259, closed issue #250).  Canonical-evidence detection gated on the last successful full rebuild (`truth_projections.built_at`, pack-scoped, read-only — verified not advanced by ops_state.rebuild or audit-sync).  Idempotent + strictly more correct than the old window. | M26 |
| BL-108 | P2 | Done | **Streaming audit ingest/read path** (#260).  `_collect_audit_rows` streams via `iter_for_index` instead of `read_text().splitlines()` (36568 rows on the operator vault); bonus: skips a corrupt line instead of aborting the rebuild.  Memory `audit-rows-streaming-debt` → RESOLVED. | M26 |
| BL-109 | P1 | Done | **Defensive timestamp normalization for digest windows** (#258; BL-102 analog, cross-surface consistency).  Digest Layer 0 + preflight now use shared `audit_time.parse_audit_ts` + in-Python window compare instead of a lexicographic SQL string compare.  Honest scope: the 0 seen dogfooding BL-106 was a backdated-probe + watermark artifact, NOT this compare; no visible change on the current vault — correctness hardening. | M26 |
| BL-110 | P1 | Done | **Maintainer UI module split** (M27; #262 view_models, #263 _ui_renderers).  Each ~8000-line file → a package: `_constants` leaf + topologically-layered `_layer*` + per-surface modules + `__init__` re-export with a `ModuleType.__setattr__` monkeypatch shim (zero call-site/test churn, behaviour-preserving).  Every file < the 3000 default; both stale 5000 ratchet entries removed (tightened, not relocated).  `test_file_size_limits` cleared for both files; zero regressions proven by pristine-vs-branch full-suite comparison. | M27 |
| BL-111 | P1 | Done | **SQLite boundary policy** (M27; policy codification — facade deferred to BL-111b).  Made `test_no_direct_sqlite_in_non_data_modules` green by recording the rule's TRUE boundary, NOT relaxing it: (a) prefix-aware allowlist matcher so BL-110's package splits keep their pre-existing grant (path-shape change only); (b) the 15 genuine pre-existing violators grouped into named, rationalised sets (`ui_projection_payload_builders` / `ops_projection_readers` / `projection_writers` / `thin_projection_clis`) — each documents it touches the DERIVED knowledge.db projection, never canonical Authority, same category already baselined. | M27 |
| BL-111b | P2 | Next | **Data-access facade migration** (M27; the real refactor BL-111 deferred per "NOT a big migration first").  Introduce `ops_state_store`/`audit_events_store`/`digest_store`/`chat_store`; migrate the BL-111 allowlisted groups onto them, shrinking the allowlist as each lands (ratchet: entries only removed).  Start from duplicated SQL + hot paths (`absorb_router` 1131L, `auto_evergreen_extractor` 2081L, `ops_lifecycle` 713L).  Behaviour-preserving + full-suite gated per migration. | M27 |
| BL-112 | P2 | Done | **`unified_pipeline_enhanced.py` leaf-extraction** (M27; #265).  3806→3192: standalone constants/helpers/classes moved to flat `_pipeline_{constants,env,plan,runtime,utils}.py` siblings (same dir ⇒ `__file__` path constants byte-identical), re-exported so entrypoints + 19 importers unchanged.  `EnhancedPipeline` + `main` byte-identical (hot path untouched).  Cleared the last `test_file_size_limits` red — **architecture-fitness fully green (9/9)**.  Full suite 3036 passed / 0 failed. | M27 |
| BL-112b | P3 | Later | **`EnhancedPipeline` god-class decomposition** (M27; the deep refactor BL-112 deferred).  2865L / 59-method class — `step_absorb`/`step_quality`/`_run_pipeline_locked`/DAG runner.  Mixin or delegation split of the hot orchestration path; behaviour-risky, much larger, zero file-size urgency now (file is already <3500).  Behaviour-preserving + full-suite gated per step; only when a real maintenance need justifies the risk. | M27 |
| BL-113 | P2 | Next | **Lightweight embedding-identity API** (resource-safety follow-up; surfaced reviewing PR3 #271).  Today the active model id / dim is only readable via `embedding.get_model_name()` / `get_dimensions()`, both of which call `_load_mlx_model()`.  So even a 100%-cache-hit rebuild (PR3 reuse) still loads the MLX backend via `_assert_embedding_consistent` at rebuild start + the per-chunk `get_model_name()` — PR3 saves embedding *inference* compute/wall-time, NOT model-load peak RSS.  Split "report the configured/active model id+dim" into a lightweight path that does not trigger a model load (e.g. resolve from the configured backend choice + a stored sidecar, load only when an actual `embed_text` is needed); then a full-cache-hit rebuild never loads the model and peak RSS drops.  This is the genuine "machine can't be OOM-killed by a rebuild" win that PR3 explicitly does not deliver. | resource-safety |

## KSR Task Coverage

| KSR ID | Backlog mapping | Status |
| --- | --- | --- |
| KSR-001 Evidence span 化 | BL-006 | Done |
| KSR-002 Projection 标注 | BL-002 | Done |
| KSR-003 Candidate 风险分层 | BL-007 | Done |
| KSR-004 Session snapshot/context pack | BL-013 | Done |
| KSR-005 Permission layer 分離 | BL-015 | Later |
| KSR-006 Claim lifecycle 字段 | BL-015 | Later |
| KSR-007 Conflict detection 常规化 | BL-016 | Later |
| KSR-008 High-risk user profile gate | BL-016 | Later |
| KSR-009 Cost-aware workflow routing | BL-003/BL-005 | Partial |
| KSR-010 Skill/routine extraction profile | BL-019 | Later |
| KSR-011 Notebook/raw-source mode | BL-019 | Later |
| KSR-012 Ingest ROI metrics | BL-019 | Later |
| KSR-013 Source lifecycle idempotency | M0/M4 | Done |
| KSR-014 Article routing preview | BL-005 | Done |
| KSR-015 Dashboard/search hot-path audit | BL-003 | Done |
| KSR-016 Hybrid retrieval experiment | BL-019 | Later |
| KSR-017 Explicit context budget | BL-013 | Done |
| KSR-018 Markdown-aware evidence chunking | BL-006 | Done |
| KSR-019 Multimodal caption-first ingest | BL-019 | Later |
| KSR-020 Operational runtime graph | BL-014 | Done |
| KSR-021 Claim lease for workflow items | BL-014 | Done |
| KSR-022 OVP prime/context pack | BL-013 | Done |
| KSR-023 Pipeline observability metrics | BL-014 | Done |
| KSR-024 Candidate compaction with restore | BL-016 | Later |
| KSR-025 Context provider facade | BL-014 | Done |
| KSR-026 Workflow wiring eval suite | BL-004 | Done |
| KSR-027 Live-source routing policy | BL-005/BL-014 | Later |
| KSR-028 Schema-on-demand guard | BL-017 | Later |
| KSR-029 Follow-up object model | BL-019 | Later |
| KSR-030 Decision context memory | BL-022 | Later |
| KSR-031 Agent workspace substrate | BL-023 | Later |
| KSR-032 Information health loop | BL-023 | Later |
| KSR-033 Machine-facing memory substrate | BL-024 | Later |

## Evolution Strategy

OVP is evolving from a personal Zettelkasten into a typed knowledge platform capable of serving as a company second brain.

### Stage 1: Type Unification + Quality (M8)

Unify the three existing type systems (`concept_registry.kind`, `truth_store.object_kind`, `view_models._OBJECT_KIND_LABELS`) into one canonical taxonomy. Add `entity_type` to Canonical State frontmatter so vault markdown remains the trust boundary for typing. Complete P3 (body-size-aware extraction), P4 (quote-grounding), and P5 (single-pass LLM refactor).

Key insight: OVP already has typed objects in Projections (truth_store), typed relations (research_tech semantic_relations), and kind-specific UI (reader profiles). The gap is Canonical State frontmatter and extraction pipeline output.

### Stage 2: Pack As Domain Ontology (M9)

Make packs the carrier of domain ontology. Each pack defines its own object kinds with properties, typed relation constraints, extraction profiles, and schema validation. This turns OVP from "one knowledge base" into "a platform that hosts domain-specific knowledge bases."

### Stage 3: Operational Knowledge Layer (M10, evaluate ROI after M9)

Add Palantir-style typed actions on objects, permission contracts, cross-entity aggregation, and decision memory. This is the "company second brain" operational layer. Evaluate need after Stage 2 ships.

### Stage 1.5: Source Authority + Cross-Source Identity (M11, Done)

Shipped through PRs #112–#124 in May 2026.  Three layers added on the read side without disturbing the existing pipeline:

* **Source authority providers (PR-D1/D2/D3)** — typed `SignalProvider` Protocol; deterministic `domain_rules` + `author_rules` whitelist, `github_stars` + `arxiv` + `twitter` (stub) live signals, soft never-gating combination rule, yaml-overrides + LLM-judge for the long tail.
* **Entity layer (PR-E1/E2/E3/E4/F1)** — `entities` + `entity_signals_history` SQLite tables holding `twitter_author`, `github_project`, `github_user`, `person`, `organization` rows.  twitterapi.io + GitHub REST backfills, identity merge with self-reported / exact-handle / fuzzy strategies, `person`/`organization` split driven by GitHub's `user.type`.  521 + 922 + 54 entities on the OVP vault, ~$0.10 one-shot.
* **Operational glue (PRs #117/#122)** — `ovp-backup-db` (SQLite online backup), `ovp-refresh-source-authority` (chained refresh wrapper, lock-protected, status JSON, launchd plist).

This is the foundation that lets M12 (extraction prime + auto-wikilink) close the loop from "we know who Karpathy is" to "the next ingest run uses that knowledge".

### Stage 4: Synthesis Layer / Crystal (M13)

OVP's L3 gap relative to NM 0.8.  NM has 240 LLM-synthesized crystals + 41 community summaries; OVP has only the mechanical 312-component `graph_clusters` and a single Atlas index page.  The plan:

1. Replace mechanical components with Louvain community detection on the relations graph (BL-041).
2. For each community, pick top-K evergreens by authority and LLM-synthesize a single markdown crystal into `40-Resources/Crystals/`; persist lineage in a `crystals` SQLite table (BL-042).
3. Add contradiction crystals using the existing `contradictions` table as seed (BL-043).
4. Append-only versioning so when source evergreens change, the new crystal version archives the old one (BL-044).

Cost target: ~¥0.001/crystal × 80-150 crystals = a few cents per full re-synthesis.  MiniMax-M2.7-highspeed.

## Next Decision

Two parallel execution lanes:

1. **M8 lane**: Execute BL-025 through BL-030.  Then evaluate M9 scope based on real pack adoption.
2. **M12 → M13 lane**: Execute BL-038 (entity_aliases) → BL-039 (extraction prime) → BL-040 (auto-wikilink), then BL-041 (Louvain) → BL-042 (Crystal MVP) → BL-043 (contradiction crystals) → BL-044 (versioning).

These two lanes don't conflict — M8 lives in the extraction prompt + frontmatter schema, M12/M13 live in the entity layer + synthesis surface.  Schedule M12 work whenever an M8 LLM call would benefit from primed entity context.
