---
milestone: v1.0
audited: 2026-02-23T18:30:00Z
status: tech_debt
scores:
  requirements: 37/37
  phases: 3/3
  integration: 37/37
  flows: 3/4
gaps:
  requirements: []
  integration:
    - id: CLI-02-partial
      severity: high
      description: "cli.py line 203 uses result['_relevance_score'] direct key access (no .get() fallback)"
      detail: "MCP server and Python API both use row.get('_relevance_score', 0.0) safely. CLI will raise KeyError if LanceDB returns a result row without this key."
      fix: "Change to result.get('_relevance_score', 0.0) in cli.py:203"
    - id: INGEST-07-silent
      severity: medium
      description: "Silent exception swallowing in CorpusIndex._delete_stale_chunks() and _get_existing_files()"
      detail: "indexer.py lines 299 and 354 have bare except Exception: pass/return {}. INGEST-07 stale chunk deletion can fail completely without any log or signal."
      fix: "Add logging.warning() in exception handlers at minimum"
    - id: MCP-02-silent
      severity: medium
      description: "MCP full_content silently returns empty string on file read failure"
      detail: "server.py:87-88 catches OSError silently. MCP client receives full_content='' with no error signal when a file is deleted/moved after indexing."
      fix: "Include a content_error field or log the OSError"
  flows:
    - id: CLI-search-flow
      status: partial
      break_point: "cli.py:203 - _relevance_score KeyError risk"
      affected: "corpus search command only"
      not_affected: "MCP server, Python API"
tech_debt:
  - phase: 01-foundation
    items:
      - "needs_reindex() in ingest/scanner.py is exported but never called — indexer implements its own inline equivalent. Dead code."
  - phase: 02-search-core
    items:
      - "FTS index rebuilt redundantly: once per source in index_source(), and again on CorpusSearch construction. Harmless but wasteful."
      - "use_llm_classification parameter on index_source() is not exposed to CLI, API, or config — asymmetric with summarize flag (SUMM-03)"
  - phase: 03-agent-interfaces
    items:
      - "DATA_DIR defined independently in cli.py and api/public.py; mcp/server.py imports from cli.py. Dual source-of-truth that could drift."
  - all-phases:
    items:
      - "No VERIFICATION.md created for any of the 3 phases — verification was done manually inline in SUMMARY files rather than as separate verification pass"
verification_gaps:
  - phase: "01-foundation"
    status: "missing VERIFICATION.md"
    evidence: "Requirements confirmed via SUMMARY.md frontmatter: CONF-01..05, INGEST-01..07 all listed"
  - phase: "02-search-core"
    status: "missing VERIFICATION.md"
    evidence: "Requirements confirmed inline in 02-05 SUMMARY: CLI-01..05, SEARCH-01..05, CLASS-01..03, SUMM-01..03"
  - phase: "03-agent-interfaces"
    status: "missing VERIFICATION.md"
    evidence: "Requirements confirmed in 03-03 SUMMARY: MCP-01..06, API-01..03. UAT complete (6/6 tests passed)"
---

# Milestone v1.0 Audit Report

**Audited:** 2026-02-23
**Status:** ⚡ Tech Debt (no blockers, accumulated deferred items)
**Score:** 37/37 requirements satisfied

---

## Requirements Coverage

All 37 v1 requirements verified across 3 independent sources (SUMMARY frontmatter, integration analysis, and inline phase verification).

| Requirement | Phase | Integration Status | Evidence |
|-------------|-------|-------------------|----------|
| CONF-01 | 1 | WIRED | config/schema.py SourceConfig → load_config() → walk_source() |
| CONF-02 | 1 | WIRED | EmbeddingConfig.model → OllamaEmbedder |
| CONF-03 | 1 | WIRED | SourceConfig.include/exclude → walk_source() |
| CONF-04 | 1 | WIRED | SourceConfig.name → source_name on ChunkRecord |
| CONF-05 | 1 | WIRED | add_command() → save_config() |
| INGEST-01 | 1 | WIRED | index_command() → CorpusIndex.open() → index_source() |
| INGEST-02 | 1 | WIRED | chunk_file() dispatches .md/.py/.ts/.js/.json/.yaml |
| INGEST-03 | 1 | WIRED | chunk_markdown/chunk_python/chunk_lines dispatch |
| INGEST-04 | 1 | WIRED | make_chunk_id() sha256 called in indexer.py:224 |
| INGEST-05 | 1 | WIRED | Hash comparison at indexer.py:171 |
| INGEST-06 | 1 | WIRED | All ChunkRecord fields populated in indexer.py:223-239 |
| INGEST-07 | 1 | WIRED* | _delete_stale_chunks() — silent failure risk |
| SEARCH-01 | 2 | WIRED | CorpusSearch.hybrid_search() → RRF-ranked results |
| SEARCH-02 | 2 | WIRED | query_type="hybrid" + RRFReranker() |
| SEARCH-03 | 2 | WIRED | hybrid_search(source=...) → where filter |
| SEARCH-04 | 2 | WIRED | hybrid_search(file_type=...) → where filter |
| SEARCH-05 | 2 | WIRED | CLI:snippet, MCP:full_content, API:SearchResult |
| CLASS-01 | 2 | WIRED | classify_file() → construct_type per file |
| CLASS-02 | 2 | WIRED | construct_type stored on ChunkRecord |
| CLASS-03 | 2 | WIRED | hybrid_search(construct_type=...) → where filter |
| SUMM-01 | 2 | WIRED | generate_summary() called per changed file |
| SUMM-02 | 2 | WIRED | Summary prepended to first chunk text |
| SUMM-03 | 2 | WIRED | SourceConfig.summarize → should_summarize() |
| CLI-01 | 2 | WIRED | corpus index → incremental via hash comparison |
| CLI-02 | 2 | PARTIAL | corpus search — _relevance_score KeyError risk |
| CLI-03 | 2 | WIRED | --source/--type/--construct/--limit flags |
| CLI-04 | 2 | WIRED | corpus status → CorpusSearch.status() |
| CLI-05 | 2 | WIRED | corpus add → add_command() |
| MCP-01 | 3 | WIRED | FastMCP("corpus") + corpus mcp serve |
| MCP-02 | 3 | WIRED* | Path(file_path).read_text() — silent OSError |
| MCP-03 | 3 | WIRED | corpus_search(query, source, type, construct, top_k) |
| MCP-04 | 3 | WIRED | logging.basicConfig(stream=sys.stderr), no print() |
| MCP-05 | 3 | WIRED | Optional[str] not str\|None union syntax |
| MCP-06 | 3 | WIRED | embedder.embed_batch(["warmup"]) in corpus_lifespan |
| API-01 | 3 | WIRED | SearchResult: path, file_type, construct_type, summary, score, snippet |
| API-02 | 3 | WIRED | index() → CorpusIndex.index_source() per source |
| API-03 | 3 | WIRED | All 3 interfaces call CorpusSearch.hybrid_search() |

*WIRED* = functionally wired but with a known silent failure path

---

## E2E Flows

| Flow | Status | Break Point |
|------|--------|-------------|
| `corpus add` → `corpus index` → data in LanceDB | ✓ Complete | — |
| `corpus search` → ranked results with snippets | ⚠ Partial | cli.py:203 _relevance_score KeyError risk |
| `corpus mcp serve` → Claude Code calls corpus_search → full file content | ✓ Complete | MCP-02 silent empty on deleted files |
| `from corpus import search` → SearchResult list | ✓ Complete | — |

---

## Cross-Phase Integration

**18 exports properly connected across phases. 1 orphaned export (needs_reindex). 1 design gap (use_llm_classification not user-configurable).**

The MCP server (`mcp/server.py`) imports DATA_DIR from `cli.py` rather than computing it independently — this is a correct coupling that avoids divergence, but the Python API (`api/public.py`) computes it independently using the same `platformdirs` call.

---

## Tech Debt Summary

| Phase | Item | Severity |
|-------|------|----------|
| All | No VERIFICATION.md created for any phase | Low |
| All | CLI-02 _relevance_score direct key access | High |
| All | Silent exception swallowing in indexer stale cleanup | Medium |
| All | Silent OSError on MCP full_content read | Medium |
| 01 | needs_reindex() dead code | Low |
| 02 | FTS index rebuilt redundantly | Low |
| 02 | use_llm_classification not user-configurable | Low |
| 03 | DATA_DIR dual source-of-truth | Low |

---

## Verdict

All 37 v1 requirements are implemented and wired end-to-end. No requirements are orphaned or unsatisfied.

The CLI-02 KeyError risk (`result['_relevance_score']`) is the one high-severity item — it is a potential runtime crash in `corpus search` that should be fixed before marking v1.0 shipped. It is a 1-line fix.

Everything else is tech debt or design asymmetry with no user-facing impact.

---
*Audit generated: 2026-02-23*
