---
phase: 25-surface-convergence-doctor-repair
plan: 03
type: execute
wave: 1
depends_on: []
files_modified:
  - paperforge/worker/base_views.py
  - paperforge/worker/repair.py
  - tests/test_base_views.py
  - tests/test_repair.py
autonomous: true
requirements:
  - SURF-01
  - SURF-02
  - SURF-04
  - MIG-04

must_haves:
  truths:
    - "Base views show lifecycle, maturity_level, and next_step columns instead of has_pdf, do_ocr, analyze, ocr_status"
    - "Base views sort by lifecycle by default, showing auto-computed readiness"
    - "Base view filters use lifecycle states instead of raw status combinations"
    - "User customization via PAPERFORGE_VIEW_PREFIX merge logic is preserved"
    - "Repair calls build_index() after fixing source artifacts, ensuring derived state is rebuilt"
    - "Repair flow is source-first: fix artifacts, then rebuild canonical index, never editing index directly"
    - "User has documented reversible rebuild path after migration failure"
  artifacts:
    - path: paperforge/worker/base_views.py
      provides: "Updated build_base_views() with lifecycle columns and filter changes"
    - path: paperforge/worker/repair.py
      provides: "run_repair() calls build_index() after fix operations"
    - path: tests/test_base_views.py
      provides: "Tests for new Base view column structure"
    - path: tests/test_repair.py
      provides: "Tests verifying build_index called after repair"
  key_links:
    - from: paperforge/worker/base_views.py:build_base_views
      to: canonical index lifecycle/maturity/next_step fields
      via: "Column names in view definitions match index entry derived fields"
      pattern: "lifecycle|maturity_level|next_step"
    - from: paperforge/worker/repair.py:run_repair
      to: paperforge/worker/asset_index.build_index
      via: "After fixing source artifacts, call build_index() to regenerate canonical index"
      pattern: "build_index"
---

<objective>
Update Base views to use lifecycle/maturity/next_step columns from the canonical index, replacing the old raw status fields. Strengthen repair to call `build_index()` after fixing source artifacts, ensuring derived state is rebuilt from clean sources. Document the reversible rebuild path for migration recovery.

Purpose: Base views must consume the same lifecycle/health/maturity semantics from the canonical index as CLI status and the plugin dashboard (SURF-01, SURF-04). Repair follows source-first + rebuild pattern (SURF-02). Users have a documented recovery path if migration fails (MIG-04).

Output:
- Updated `paperforge/worker/base_views.py` — lifecycle-based columns and filters
- Updated `paperforge/worker/repair.py` — build_index() call after fix operations
- Tests for both changes
</objective>

<execution_context>
@/C/Users/Lin/.opencode/get-shit-done/workflows/execute-plan.md
@/C/Users/Lin/.opencode/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/STATE.md
@.planning/ROADMAP.md
@.planning/phases/25-surface-convergence-doctor-repair/25-CONTEXT.md
@paperforge/worker/base_views.py
@paperforge/worker/repair.py
@paperforge/worker/asset_index.py
@paperforge/worker/asset_state.py

<interfaces>
From paperforge/worker/base_views.py:
```python
PAPERFORGE_VIEW_PREFIX = "# PAPERFORGE_VIEW: "

def build_base_views(domain: str) -> list[dict]:
    """Build 8 views with fixed name/filter. Each view dict has: name (str), order (list), filter (str|None)."""

def merge_base_views(existing_content: str | None, new_views: list[dict]) -> str:
    """Incremental merge: replace PAPERFORGE_VIEW_PREFIX-marked views, preserve user views."""

def ensure_base_views(vault: Path, paths: dict[str, Path], config: dict, force: bool = False) -> None:
    """Generate/refresh Domain Base files."""
```

From paperforge/worker/asset_index.py:
```python
def build_index(vault: Path, verbose: bool = False) -> int:
    """Full rebuild of canonical asset index for vault."""
```

From paperforge/worker/repair.py:
```python
def run_repair(vault: Path, paths: dict, verbose: bool = False, fix: bool = False, fix_paths: bool = False) -> dict:
    """Scan for three-way state divergence and optionally repair."""

def repair_pdf_paths(vault, paths, error_records, verbose=False) -> int:
    """Re-resolve PDF paths for items with path_error. Calls refresh_index_entry per item."""
```

Current build_base_views() column lists (base_views.py:53-134):
- "控制面板": title, year, first_author, journal, impact_factor, has_pdf, do_ocr, analyze, ocr_status, deep_reading_status, pdf_path, fulltext_md_path
- "推荐分析": year, title, first_author, journal, impact_factor, has_pdf, do_ocr, analyze, ocr_status, deep_reading_status, pdf_path, fulltext_md_path
- "待 OCR": year, first_author, title, has_pdf, do_ocr, ocr_status, pdf_path
- "OCR 完成": year, first_author, title, has_pdf, do_ocr, ocr_status, pdf_path
- "待深度阅读": year, first_author, title, has_pdf, do_ocr, analyze, ocr_status, deep_reading_status, pdf_path
- "深度阅读完成": year, first_author, title, has_pdf, do_ocr, analyze, ocr_status, deep_reading_status, pdf_path
- "正式卡片": title, year, first_author, journal, impact_factor, has_pdf, deep_reading_status, pdf_path
- "全记录": title, year, first_author, journal, impact_factor, has_pdf, do_ocr, analyze, ocr_status, deep_reading_status, pdf_path, fulltext_md_path
</interfaces>
</context>

<tasks>

<task type="auto" tdd="false">
<name>Task 1: Update Base view columns and filters to use lifecycle</name>
<files>
  paperforge/worker/base_views.py
  tests/test_base_views.py
</files>

<read_first>
  paperforge/worker/base_views.py:44-134 (build_base_views)
  paperforge/worker/base_views.py:329-375 (_build_base_yaml)
  paperforge/worker/base_views.py:196-226 (PROPERTIES_YAML in merge_base_views)
</read_first>

<action>
Update `build_base_views()`, `_build_base_yaml()`, and the `PROPERTIES_YAML` block in `merge_base_views()` per decisions D-13 through D-16.

**D-13:** Base `build_base_views()` adds `lifecycle`, `maturity_level`, and `next_step` columns.
**D-14:** Old `has_pdf`, `do_ocr`, `analyze`, `ocr_status` columns removed from Base views (superseded by lifecycle).
**D-15:** Sort order: lifecycle ascending (show auto-computed readiness, not raw fields).
**D-16:** Base filters updated to use lifecycle instead of raw status combinations.

**Implementation:**

1. Update `build_base_views()` — replace each view's column list per D-13 and D-14:

   New column structure (replace `has_pdf`, `do_ocr`, `analyze`, `ocr_status` with `lifecycle`, `maturity_level`, `next_step`):

   View "控制面板" (Dashboard):
   ```python
   "order": [
       "file.name",
       "title",
       "year",
       "first_author",
       "journal",
       "impact_factor",
       "lifecycle",
       "maturity_level",
       "next_step",
       "deep_reading_status",
       "pdf_path",
       "fulltext_md_path",
   ],
   "filter": None,
   ```

   View "推荐分析" (Recommended):
   ```python
   "order": [
       "year",
       "title",
       "first_author",
       "journal",
       "impact_factor",
       "lifecycle",
       "maturity_level",
       "next_step",
       "deep_reading_status",
       "pdf_path",
       "fulltext_md_path",
   ],
   "filter": "lifecycle != 'indexed'",  # papers with PDF, not just metadata
   ```

   View "待 OCR" (Pending OCR) — filter uses lifecycle:
   ```python
   "order": ["year", "first_author", "title", "lifecycle", "next_step", "pdf_path"],
   "filter": "lifecycle = 'pdf_ready'",
   ```

   View "OCR 完成" (OCR Complete):
   ```python
   "order": ["year", "first_author", "title", "lifecycle", "maturity_level", "next_step", "pdf_path"],
   "filter": "lifecycle = 'fulltext_ready' OR lifecycle = 'deep_read_done' OR lifecycle = 'ai_context_ready'",
   ```

   View "待深度阅读" (Pending deep read):
   ```python
   "order": ["year", "first_author", "title", "lifecycle", "maturity_level", "next_step", "pdf_path"],
   "filter": "lifecycle = 'fulltext_ready'",
   ```

   View "深度阅读完成" (Deep read done):
   ```python
   "order": ["year", "first_author", "title", "lifecycle", "maturity_level", "next_step", "pdf_path"],
   "filter": "lifecycle = 'deep_read_done' OR lifecycle = 'ai_context_ready'",
   ```

   View "正式卡片" (Formal cards):
   ```python
   "order": ["title", "year", "first_author", "journal", "impact_factor", "lifecycle", "maturity_level", "next_step", "pdf_path"],
   "filter": "lifecycle = 'deep_read_done' OR lifecycle = 'ai_context_ready'",
   ```

   View "全记录" (All records):
   ```python
   "order": [
       "title",
       "year",
       "first_author",
       "journal",
       "impact_factor",
       "lifecycle",
       "maturity_level",
       "next_step",
       "deep_reading_status",
       "pdf_path",
       "fulltext_md_path",
   ],
   "filter": None,
   ```

2. **D-15:** Add a `sort` field to each view dict. The sort order should prioritize `lifecycle` ascending. Add after `filter` in each view:
   ```python
   "sort": [{"field": "lifecycle", "direction": "asc"}],
   ```
   
   Update `_render_views_section()` and `_build_base_yaml()` to emit the `sort` field in the YAML output. Add rendering for sort after filter:
   ```python
   if v.get("sort"):
       sort_yaml = yaml.dump(v["sort"], default_flow_style=False).strip()
       lines.append(f"    sort:")
       for sort_item in v["sort"]:
           lines.append(f"      - field: {sort_item['field']}")
           lines.append(f"        direction: {sort_item['direction']}")
   ```
   This requires adding `import yaml` (or render manually). Since this is a .base file (Obsidian format), render as:
   ```
   sort:
     - field: lifecycle
       direction: asc
   ```

3. Update `PROPERTIES_YAML` block in `merge_base_views()` (line 196-226):
   - Add lifecycle, maturity_level, next_step display names
   - Remove has_pdf, do_ocr, analyze, ocr_status display names
   
   New properties:
   ```python
   PROPERTIES_YAML = """properties:
     zotero_key:
       displayName: "Zotero Key"
     title:
       displayName: "Title"
     year:
       displayName: "Year"
     first_author:
       displayName: "First Author"
     journal:
       displayName: "Journal"
     impact_factor:
       displayName: "IF"
     lifecycle:
       displayName: "Lifecycle"        # or Chinese: 生命周期
     maturity_level:
       displayName: "Maturity"         # or Chinese: 就绪级别
     next_step:
       displayName: "Next Step"        # or Chinese: 下一步
     deep_reading_status:
       displayName: "Deep Reading"
     pdf_path:
       displayName: "PDF Path"
     fulltext_md_path:
       displayName: "Fulltext"
   """
   ```

4. Update `_build_base_yaml()` (line 329) to match — remove has_pdf, do_ocr, analyze, ocr_status; add lifecycle, maturity_level, next_step.

Write tests in `tests/test_base_views.py`:
- `test_build_base_views_includes_lifecycle_columns`: Verify every view dict's `order` list contains `lifecycle`, `maturity_level`, and `next_step`.
- `test_build_base_views_removes_old_columns`: Verify no view's `order` list contains `has_pdf`, `do_ocr`, `analyze`, or `ocr_status`.
- `test_build_base_views_filters_use_lifecycle`: Verify filter strings use `lifecycle =` instead of raw field names.
- `test_build_base_views_has_sort`: Verify every view dict has a `sort` list containing `{"field": "lifecycle", "direction": "asc"}`.
- `test_properties_yaml_updated`: Verify `PROPERTIES_YAML` contains `lifecycle`, `maturity_level`, `next_step` and does NOT contain `has_pdf`, `do_ocr`, `analyze`, `ocr_status`.

**Per the agent's Discretion on column names:** Use English column names for displayName (Lifecycle, Maturity, Next Step). The Chinese label decision (生命周期/就绪状态) is noted as discretion — keep English for now as the base is a technical view.
</action>

<acceptance_criteria>
- Every view in `build_base_views()` has `lifecycle`, `maturity_level`, `next_step` in its `order` list
- No view contains `has_pdf`, `do_ocr`, `analyze`, or `ocr_status` in its `order` list
- Filter strings use `lifecycle =` lifecycle-specific values
- Every view has `sort: [{"field": "lifecycle", "direction": "asc"}]`
- `_render_views_section()` emits `sort:` YAML correctly
- `PROPERTIES_YAML` in `merge_base_views()` includes lifecycle/maturity_level/next_step
- `PROPERTIES_YAML` does NOT include has_pdf/do_ocr/analyze/ocr_status
- `_build_base_yaml()` properties updated consistently
- All existing Base view merge tests continue to pass (user customization preserved)
</acceptance_criteria>

<verify>
  <automated>pytest tests/test_base_views.py -x -q --tb=short</automated>
</verify>

<done>
Base views now show lifecycle/maturity/next_step columns. Filters use lifecycle states. Sort by lifecycle ascending. Old raw status columns removed.
</done>
</task>

<task type="auto" tdd="false">
<name>Task 2: Strengthen repair source-first rebuild pattern</name>
<files>
  paperforge/worker/repair.py
  tests/test_repair.py
</files>

<read_first>
  paperforge/worker/repair.py:182-359 (run_repair function)
  paperforge/worker/asset_index.py:322-380 (build_index function)
</read_first>

<action>
Strengthen the repair source-first + rebuild pattern per decisions D-11 and D-12.

**D-11:** Repair keeps source-first + rebuild pattern: fix source artifacts, then rebuild canonical index. Never edits index directly.
**D-12:** After repair, `repair.py` calls `rebuild_index()` to regenerate the canonical index.

**Current state:** `run_repair()` already edits source artifacts (library record markdown, formal note markdown, meta.json) and calls `refresh_index_entry()` per-paper. However, it does NOT call `build_index()` (full rebuild) after all fixes are done. The per-paper `refresh_index_entry()` calls are correct for individual fixes, but after a batch repair completes, a full rebuild ensures consistency.

**Implementation:**

1. At the end of `run_repair()`, after the path error detection block (line 345-358) and before the return (line 359), add:
   ```python
   # Phase 25: Full rebuild after repair (D-12)
   # After fixing source artifacts, rebuild the canonical index so all derived
   # state fields (lifecycle, health, maturity, next_step) reflect the repaired state.
   if fix or fix_paths:
       try:
           from paperforge.worker.asset_index import build_index
           rebuilt_count = build_index(vault, verbose)
           print(f"[repair] Rebuilt canonical index: {rebuilt_count} entries")
           result["rebuilt"] = rebuilt_count
       except Exception as e:
           logger.error("Failed to rebuild index after repair: %s", e)
           print(f"[repair] WARNING: Index rebuild failed: {e}")
           result["rebuilt"] = -1
   else:
       # dry-run mode: no rebuild needed
       pass
   ```

2. Use lazy import inside the block to avoid circular dependency risks — even though `repair.py` doesn't import from `asset_index.py` at module level, keeping the import inside the conditional block follows the established pattern.

3. Add `"rebuilt"` key to `result` dict initialization at line 193:
   ```python
   result = {"scanned": 0, "divergent": [], "fixed": 0, "errors": [], "rebuilt": 0}
   ```

4. **MIG-04:** Add an explicit user-facing message for the rebuild path. After the index rebuild, add:
   ```python
   print()
   print(f"[repair] All source artifacts repaired. Canonical index rebuilt with {rebuilt_count} entries.")
   print("[repair] Run `paperforge status` or open the plugin dashboard to verify.")
   if rebuilt_count > 0:
       print("[repair] If the result looks incomplete, run `paperforge sync --rebuild-index`")
       print("[repair] to regenerate from scratch (existing .bak file available for recovery).")
   ```

Write tests in `tests/test_repair.py`:
- `test_repair_calls_build_index_after_fix`: Mock `build_index` and verify it's called when `fix=True`.
- `test_repair_does_not_call_build_index_dry_run`: Verify `build_index` is NOT called when `fix=False` (dry-run).
- `test_repair_rebuilt_in_result`: Verify the result dict contains `rebuilt` key with an integer value after fix.
- `test_repair_rebuild_fallback_on_error`: Verify repair completes without crashing even if `build_index` raises an exception.
</action>

<acceptance_criteria>
- `run_repair()` calls `build_index(vault, verbose)` after fix operations when `fix=True`
- `build_index` is NOT called during dry-run mode (`fix=False`)
- Result dict includes `rebuilt` key (int: entry count, or -1 on failure)
- User-friendly messages printed about repair completion and recovery path
- All existing repair tests continue to pass
</acceptance_criteria>

<verify>
  <automated>pytest tests/test_repair.py -x -q --tb=short</automated>
</verify>

<done>
Repair calls build_index() after fixing source artifacts. MIG-04 rebuild path documented in output messages. User can recover via `paperforge sync --rebuild-index` with .bak fallback.
</done>
</task>

</tasks>

<verification>
1. `pytest tests/test_base_views.py -x -q --tb=short` — all column/filter/property tests pass
2. `pytest tests/test_repair.py -x -q --tb=short` — all rebuild/call tests pass
3. `pytest tests/ -x -q --tb=short` — no regressions in any test suite
4. No test file exists yet for test_base_views.py — it must be created in Wave 0
</verification>

<success_criteria>
- SURF-01: Base views show lifecycle/maturity/next_step columns — all surfaces (status, plugin, Base) consume same derived data from canonical index
- SURF-02: Repair rebuilds canonical index after fixing source artifacts — derived state reflects repaired sources
- SURF-04: Base views are derived from canonical index lifecycle rather than duplicated filtering logic
- MIG-04: Rebuild path is documented and recoverable (--rebuild-index flag, .bak backup, repair output messages)
</success_criteria>

<output>
After completion, create `.planning/phases/25-surface-convergence-doctor-repair/25-03-SUMMARY.md`
</output>
