# Entity Find: Compact Output Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Replace entity find's 1.6MB all-docs dump with a compact profile: metadata + facts + document_count + breadcrumbs.

**Architecture:** Rewrite `_build_entity_result()` in `cli.py` to query the `facts` table and `COUNT(*)` instead of fetching all linked documents. Mirror the change in `mcp_server.py`. Update tests and usage text.

**Tech Stack:** Python, SQLite, Click CLI, pytest

**Issue:** [#3](https://github.com/tenfourty/kbx/issues/3)

---

### Task 1: Update existing test to assert new output shape

**Files:**
- Modify: `tests/test_cli.py:384-391`

**Step 1: Update `test_person_find_json` to assert the new shape**

Replace the existing test at line 384:

```python
def test_person_find_json(self, runner, cli_db):
    """kb person find 'Eve' --json returns compact profile."""
    _db, db_path = cli_db
    result = invoke_cli(runner, ["person", "find", "Eve", "--json"], db_path)
    assert result.exit_code == 0
    data = json.loads(result.output)
    assert data["name"] == "Eve Perrin"
    # New compact shape
    assert "facts" in data
    assert isinstance(data["facts"], list)
    assert "document_count" in data
    assert isinstance(data["document_count"], int)
    assert "source_path" in data
    assert "breadcrumbs" in data
    # Old shape is gone
    assert "documents" not in data
```

**Step 2: Run test to verify it fails**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run pytest tests/test_cli.py::TestPersonCommand::test_person_find_json -xvs`

Expected: FAIL — `"facts" not in data`, `"documents" in data`

---

### Task 2: Rewrite `_build_entity_result()` in cli.py

**Files:**
- Modify: `src/kb/cli.py:575-601`

**Step 1: Write the new `_build_entity_result` function**

Replace lines 575-601 with:

```python
def _build_entity_result(conn: sqlite3.Connection, entity_row: sqlite3.Row) -> dict[str, Any]:
    """Build a compact entity result with facts, doc count, and breadcrumbs."""
    entity_id = entity_row["id"]
    entity_name = str(entity_row["name"])
    entity_type = str(entity_row["entity_type"])
    source_path = str(entity_row["source_path"]) if entity_row["source_path"] else None

    # Facts from the facts table
    fact_rows = conn.execute(
        "SELECT fact_text, fact_date FROM facts WHERE entity_id = ? ORDER BY fact_date DESC, id DESC",
        (entity_id,),
    ).fetchall()
    facts = [{"text": str(r["fact_text"]), "date": r["fact_date"]} for r in fact_rows]

    # Document count (not the full list)
    doc_count_row = conn.execute(
        "SELECT COUNT(DISTINCT document_id) as cnt FROM entity_mentions WHERE entity_id = ?",
        (entity_id,),
    ).fetchone()
    document_count = int(doc_count_row["cnt"])

    # Breadcrumbs — commands for deeper exploration
    breadcrumbs: dict[str, str] = {}
    if entity_type in ("person", "project"):
        breadcrumbs["timeline"] = f'kbx {entity_type} timeline "{entity_name}" --limit 20'
        # 30 days ago for "recent"
        from datetime import date, timedelta

        thirty_ago = (date.today() - timedelta(days=30)).isoformat()
        breadcrumbs["recent"] = (
            f'kbx {entity_type} timeline "{entity_name}" --from {thirty_ago}'
        )
    else:
        breadcrumbs["search"] = f'kbx search "{entity_name}" --limit 20'
    if source_path:
        breadcrumbs["profile"] = f"kbx view {source_path}"

    return {
        "id": entity_id,
        "name": entity_name,
        "entity_type": entity_type,
        "aliases": json.loads(entity_row["aliases"]) if entity_row["aliases"] else [],
        "metadata": json.loads(entity_row["metadata"]) if entity_row["metadata"] else {},
        "facts": facts,
        "source_path": source_path,
        "document_count": document_count,
        "breadcrumbs": breadcrumbs,
    }
```

**Step 2: Run the test to verify it passes**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run pytest tests/test_cli.py::TestPersonCommand::test_person_find_json -xvs`

Expected: PASS

**Step 3: Run all tests to check for breakage**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run pytest tests/test_cli.py -x -q`

Expected: Some failures in tests that assert `"documents" in data` — these are fixed in Task 3.

**Step 4: Commit**

```bash
git add src/kb/cli.py tests/test_cli.py
git commit -m "fix(entity-find): replace doc dump with compact profile (#3)

_build_entity_result now returns facts, document_count, source_path,
and breadcrumbs instead of all linked documents."
```

---

### Task 3: Fix remaining test assertions

**Files:**
- Modify: `tests/test_cli.py`

Tests that reference the old `documents` field need updating. These are the known ones:

**Step 1: Update `TestPersonAmbiguous::test_person_find_ambiguous_returns_multiple`**

At line 673, the test checks multiple results. Each result now has the new shape. Verify the test still passes — it checks for `data["results"]` structure, not `documents`. Read the test to confirm.

**Step 2: Run the full CLI test suite**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run pytest tests/test_cli.py -x -q`

Expected: All green. Fix any remaining assertions that reference `"documents"`.

**Step 3: Run mypy**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run mypy src/kb/cli.py`

Expected: Clean

**Step 4: Commit**

```bash
git add tests/test_cli.py
git commit -m "test: update entity find assertions for compact output (#3)"
```

---

### Task 4: Add test for facts in entity find output

**Files:**
- Modify: `tests/test_cli.py` (add new test in `TestPersonCommand` class)

**Step 1: Write the test**

Add after `test_person_find_json` (around line 391):

```python
def test_person_find_includes_facts(self, runner, cli_db):
    """kb person find includes facts from the facts table."""
    db, db_path = cli_db
    conn = db.get_sqlite_conn()
    # Insert a fact for Eve (entity_id=1)
    conn.execute(
        "INSERT INTO facts (entity_id, fact_text, fact_date) VALUES (1, 'Leads platform team', '2026-02-23')"
    )
    conn.commit()
    result = invoke_cli(runner, ["person", "find", "Eve", "--json"], db_path)
    assert result.exit_code == 0
    data = json.loads(result.output)
    assert len(data["facts"]) == 1
    assert data["facts"][0]["text"] == "Leads platform team"
    assert data["facts"][0]["date"] == "2026-02-23"

def test_person_find_breadcrumbs(self, runner, cli_db):
    """kb person find includes breadcrumbs for deeper exploration."""
    _db, db_path = cli_db
    result = invoke_cli(runner, ["person", "find", "Eve", "--json"], db_path)
    assert result.exit_code == 0
    data = json.loads(result.output)
    bc = data["breadcrumbs"]
    assert "timeline" in bc
    assert "Eve Perrin" in bc["timeline"]
    assert "recent" in bc
    assert "profile" in bc
    assert bc["profile"] == "kbx view memory/people/eve.md"

def test_person_find_document_count(self, runner, cli_db):
    """kb person find shows document_count instead of full doc list."""
    _db, db_path = cli_db
    result = invoke_cli(runner, ["person", "find", "Eve", "--json"], db_path)
    assert result.exit_code == 0
    data = json.loads(result.output)
    # Eve has 1 mention in cli_db fixture (entity_id=1 -> doc_id=2)
    assert data["document_count"] == 1
    assert "documents" not in data

def test_project_find_compact(self, runner, cli_db):
    """kb project find also uses compact output."""
    _db, db_path = cli_db
    result = invoke_cli(runner, ["project", "find", "Cloud Migration", "--json"], db_path)
    assert result.exit_code == 0
    data = json.loads(result.output)
    assert "facts" in data
    assert "document_count" in data
    assert "breadcrumbs" in data
    assert "documents" not in data
    assert "timeline" in data["breadcrumbs"]
```

**Step 2: Run the new tests**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run pytest tests/test_cli.py::TestPersonCommand::test_person_find_includes_facts tests/test_cli.py::TestPersonCommand::test_person_find_breadcrumbs tests/test_cli.py::TestPersonCommand::test_person_find_document_count -xvs`

Expected: PASS

**Step 3: Commit**

```bash
git add tests/test_cli.py
git commit -m "test: add facts, breadcrumbs, and document_count tests (#3)"
```

---

### Task 5: Update MCP server `handle_kb_person_find`

**Files:**
- Modify: `src/kb/mcp_server.py:57-95`

**Step 1: Rewrite `handle_kb_person_find` to use the same compact output**

Replace lines 57-95 with:

```python
def handle_kb_person_find(db: Database, name: str) -> str:
    """Look up a person profile. Returns JSON string."""
    try:
        conn = db.get_sqlite_conn()
        entity_row = find_entity(conn, name)
        if entity_row is None:
            return json.dumps({"error": f"Entity not found: {name}"})

        entity_id = entity_row["id"]
        entity_name = str(entity_row["name"])
        entity_type = str(entity_row["entity_type"])
        source_path = str(entity_row["source_path"]) if entity_row["source_path"] else None

        # Facts
        fact_rows = conn.execute(
            "SELECT fact_text, fact_date FROM facts WHERE entity_id = ? ORDER BY fact_date DESC, id DESC",
            (entity_id,),
        ).fetchall()
        facts = [{"text": str(r["fact_text"]), "date": r["fact_date"]} for r in fact_rows]

        # Document count
        doc_count_row = conn.execute(
            "SELECT COUNT(DISTINCT document_id) as cnt FROM entity_mentions WHERE entity_id = ?",
            (entity_id,),
        ).fetchone()
        document_count = int(doc_count_row["cnt"])

        # Breadcrumbs
        from datetime import date, timedelta

        thirty_ago = (date.today() - timedelta(days=30)).isoformat()
        breadcrumbs: dict[str, str] = {}
        if entity_type in ("person", "project"):
            breadcrumbs["timeline"] = f'kbx {entity_type} timeline "{entity_name}" --limit 20'
            breadcrumbs["recent"] = (
                f'kbx {entity_type} timeline "{entity_name}" --from {thirty_ago}'
            )
        else:
            breadcrumbs["search"] = f'kbx search "{entity_name}" --limit 20'
        if source_path:
            breadcrumbs["profile"] = f"kbx view {source_path}"

        result = {
            "id": entity_id,
            "name": entity_name,
            "entity_type": entity_type,
            "aliases": json.loads(entity_row["aliases"]) if entity_row["aliases"] else [],
            "metadata": json.loads(entity_row["metadata"]) if entity_row["metadata"] else {},
            "facts": facts,
            "source_path": source_path,
            "document_count": document_count,
            "breadcrumbs": breadcrumbs,
        }
        return json.dumps(result, default=str, ensure_ascii=False)
    except Exception as e:
        print(f"kb_person_find error: {e}", file=sys.stderr)
        traceback.print_exc(file=sys.stderr)
        return json.dumps({"error": str(e)})
```

**Step 2: Run mypy on mcp_server**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run mypy src/kb/mcp_server.py`

Expected: Clean

**Step 3: Commit**

```bash
git add src/kb/mcp_server.py
git commit -m "fix(mcp): update person find to use compact output (#3)"
```

---

### Task 6: Update usage text

**Files:**
- Modify: `src/kb/cli.py:1343-1344`

**Step 1: Update the usage text**

Change line 1344 from:
```
  kb person find "Name" --json       # profile + linked documents
```
to:
```
  kb person find "Name" --json       # compact profile (facts, metadata, breadcrumbs)
```

Also update line 1349:
```
  kb project find "Name" --json      # project profile
```
to:
```
  kb project find "Name" --json      # compact profile (facts, metadata, breadcrumbs)
```

**Step 2: Run usage to verify**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run kb usage 2>/dev/null | grep "find""`

Expected: Updated descriptions visible

**Step 3: Commit**

```bash
git add src/kb/cli.py
git commit -m "docs: update usage text for compact entity find (#3)"
```

---

### Task 7: Full verification

**Step 1: Run full test suite**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run pytest -x -q --cov`

Expected: All green, coverage >= 90%

**Step 2: Run mypy**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run mypy src/`

Expected: Clean

**Step 3: Run ruff**

Run: `cd "/Users/jeremy.brown/PERSONAL Files/Documents/20-29 Personal/Control Tower/kbx" && uv run ruff check src/ && uv run ruff format --check src/`

Expected: Clean

**Step 4: Manual smoke test**

Run: `kbx person find "Jeremy Brown" --json 2>/dev/null | python3 -c "import json,sys; d=json.load(sys.stdin); print(f'Size: {len(json.dumps(d))} bytes'); print(json.dumps(d, indent=2)[:500])"`

Expected: ~500 bytes instead of 1.6MB, with facts and breadcrumbs visible.
