# Memo Insights Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Build a read-only `memo insights` command with JSON overview, tags, review, and links views.

**Architecture:** Add one focused command module at `memo/commands/insights.py` and register it in the existing CLI command loader. Tests exercise behavior through `memo.cli:main`, matching the current project pattern. User docs stay compact in `README.md` and `skills/memo/SKILL.md`.

**Tech Stack:** Python 3.11+, argparse, sqlite3, JSON, pytest, existing memo helper functions.

---

### Task 1: CLI Behavior Tests

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

- [x] **Step 1: Add failing test coverage for insights output**

Append tests that create memos, links, and review timestamps, then assert JSON output:

```python
def test_insights_overview_tags_review_and_links(data_dir, capsys):
    run_cli(["add", "fresh ai note", "#work/ai"], capsys)
    run_cli(["add", "old solo note", "#life"], capsys)
    run_cli(["add", "linked old note", "#work"], capsys)
    run_cli(["link", "1", "3", "--type", "supports"], capsys)

    now = time.time()
    old = now - 200 * 86400
    recent = now - 5 * 86400
    with sqlite3.connect(db_path(data_dir)) as conn:
        conn.execute(
            "UPDATE memos SET created_at=?, last_review_at=NULL WHERE id=2",
            (old,),
        )
        conn.execute(
            "UPDATE memos SET created_at=?, last_review_at=?, review_count=2 WHERE id=3",
            (old, recent),
        )

    code, out, err = run_cli(["insights"], capsys)
    overview = parse_json_output(out)
    assert code == 0
    assert err == ""
    assert overview["view"] == "overview"
    assert overview["window"]["days"] == 30
    assert overview["totals"] == {"memos": 3, "tags": 3, "links": 1}
    assert overview["activity"]["created"] == 1
    assert overview["activity"]["reviewed"] == 1
    assert overview["top_tags"][0] == {"tag": "#work/ai", "count": 1}
    assert overview["review"]["due"] == 1
    assert overview["review"]["stale"] == 1
    assert overview["links"]["linked_memos"] == 2
    assert overview["links"]["unlinked_memos"] == 1

    code, out, err = run_cli(["insights", "--days", "0", "--view", "tags"], capsys)
    tags = parse_json_output(out)
    assert code == 0
    assert err == ""
    assert tags["window"]["since"] is None
    assert tags["unique_tags"] == 3
    assert {"tag": "#work", "count": 1} in tags["global_top_tags"]

    code, out, err = run_cli(["insights", "--view", "review"], capsys)
    review = parse_json_output(out)
    assert code == 0
    assert err == ""
    assert review["due"] == 1
    assert review["never_reviewed"] == 2
    assert review["most_reviewed"][0]["id"] == 3

    code, out, err = run_cli(["insights", "--view", "links"], capsys)
    links = parse_json_output(out)
    assert code == 0
    assert err == ""
    assert links["total_links"] == 1
    assert links["relation_types"] == [{"relation_type": "supports", "count": 1}]
```

- [x] **Step 2: Add empty database and invalid argument tests**

```python
def test_insights_empty_database_and_invalid_args(data_dir, capsys):
    code, out, err = run_cli(["insights"], capsys)
    payload = parse_json_output(out)
    assert code == 0
    assert err == ""
    assert payload["totals"] == {"memos": 0, "tags": 0, "links": 0}
    assert payload["top_tags"] == []
    assert payload["review"]["due"] == 0
    assert payload["links"]["unlinked_memos"] == 0

    with pytest.raises(SystemExit) as invalid_view_exit:
        cli.main(["insights", "--view", "trends"])
    assert invalid_view_exit.value.code == 2

    with pytest.raises(SystemExit) as invalid_days_exit:
        cli.main(["insights", "--days", "-1"])
    assert invalid_days_exit.value.code == 2

    with pytest.raises(SystemExit) as invalid_limit_exit:
        cli.main(["insights", "--limit", "-1"])
    assert invalid_limit_exit.value.code == 2
```

- [x] **Step 3: Run tests to verify failure**

Run:

```bash
uv run --extra dev pytest tests/test_cli.py -k insights -q
```

Expected: fail because the `insights` command is unknown.

### Task 2: Insights Command

**Files:**
- Create: `memo/commands/insights.py`
- Modify: `memo/cli.py`

- [x] **Step 1: Register the command**

Add `"insights"` to `COMMANDS` in `memo/cli.py` and add this help row:

```python
print("    insights       show memo insights")
```

- [x] **Step 2: Implement `memo/commands/insights.py`**

Create a read-only command module with:

```python
"""insights 子命令"""

import argparse
import json
import time
from collections import Counter

from .. import fmt_time, warn

VIEWS = ("overview", "tags", "review", "links")
SECONDS_PER_DAY = 86400


def add_parser(sub):
    p = sub.add_parser("insights", help="查看 memo 洞察")
    p.add_argument("--view", choices=VIEWS, default="overview")
    p.add_argument("--days", type=positive_int, default=30)
    p.add_argument("--limit", type=positive_int, default=10)
    p.set_defaults(func=cmd_insights)
    return p


def positive_int(value):
    try:
        parsed = int(value)
    except ValueError as exc:
        raise argparse.ArgumentTypeError("must be a non-negative integer") from exc
    if parsed < 0:
        raise argparse.ArgumentTypeError("must be >= 0")
    return parsed
```

Then implement:

- `parse_days_window(days)`: returns `{"days": days, "since": formatted or None, "until": formatted, "_since_ts": raw, "_until_ts": raw}`
- `load_memo_rows(conn)`: reads `id, content, tags, created_at, updated_at, review_count, last_review_at`
- `tag_counts(rows)`: parses tag JSON with warnings on malformed values
- `build_overview(conn, days, limit)`
- `build_tag_insights(conn, days, limit)`
- `build_review_insights(conn, days, limit)`
- `build_link_insights(conn, days, limit)`

Keep `_since_ts` and `_until_ts` internal by removing them before printing.

- [x] **Step 3: Run focused tests**

Run:

```bash
uv run --extra dev pytest tests/test_cli.py -k insights -q
```

Expected: pass.

### Task 3: Documentation

**Files:**
- Modify: `README.md`
- Modify: `skills/memo/SKILL.md`

- [x] **Step 1: Update README usage examples**

Add `memo insights` to the main usage block and more commands block if needed:

```bash
memo insights
memo insights --view tags
```

- [x] **Step 2: Update memo skill command guidance**

Add `insights` to safe read-oriented commands:

```bash
memo insights
memo insights --view tags
memo insights --view review
memo insights --view links
```

Add a short common task note that insights is read-only and returns JSON.

- [x] **Step 3: Run formatting and focused tests**

Run:

```bash
uv tool run ruff format --check .
uv run --extra dev pytest tests/test_cli.py -k insights -q
```

Expected: both pass, unless existing unrelated files block ruff.

### Task 4: Final Verification

**Files:**
- All changed files

- [x] **Step 1: Run project checks**

Run:

```bash
uv tool run ruff check memo tests
uv tool run ruff format --check memo tests
uv run --extra dev pytest
```

Expected: pass.

- [x] **Step 2: Run manual check with temp data**

Run:

```bash
tmpdir="$(mktemp -d)"
MEMO_DATA_DIR="$tmpdir" uv run memo add "manual insight #test"
MEMO_DATA_DIR="$tmpdir" uv run memo insights
```

Expected: JSON with `view: "overview"`, one memo, one tag, zero links.

- [x] **Step 3: Review git diff**

Run:

```bash
git diff -- memo/commands/insights.py memo/cli.py tests/test_cli.py README.md skills/memo/SKILL.md
```

Expected: scoped changes for insights only.
