---
phase: 24-derived-lifecycle-health-maturity
plan: 02
type: execute
wave: 2
depends_on:
  - 24-01
files_modified:
  - paperforge/worker/asset_index.py
  - tests/test_asset_index_integration.py
autonomous: true
requirements:
  - STATE-01
  - STATE-02
  - STATE-03
  - STATE-04
  - AIC-01

must_haves:
  truths:
    - "Index entries produced by build_index() contain lifecycle, health, maturity, and next_step fields"
    - "Index entries produced by refresh_index_entry() contain lifecycle, health, maturity, and next_step fields"
    - "All four fields are derived from source artifacts (the entry dict), not hand-edited"
    - "Lifecycle field is a string matching one of the six states (indexed, pdf_ready, fulltext_ready, deep_read_done, ai_context_ready)"
    - "Health field is a dict with four string values (pdf_health, ocr_health, note_health, asset_health)"
    - "Maturity field is a dict with level (int 1-6), level_name (str), checks (dict of 6 bools), blocking (str or None)"
    - "Next_step field is a string matching one of: sync, ocr, repair, /pf-deep, rebuild index, ready"
    - "Existing index fields unchanged — only additive change"
  artifacts:
    - path: "paperforge/worker/asset_index.py"
      provides: "Canonical index entries with embedded lifecycle/health/maturity/next_step"
      contains: "from paperforge.worker.asset_state import"
    - path: "tests/test_asset_index_integration.py"
      provides: "Integration tests verifying derived state fields in index entries"
      contains: "class TestDerivedStateFields"
  key_links:
    - from: "asset_index._build_entry()"
      to: "asset_state.compute_lifecycle / compute_health / compute_maturity / compute_next_step"
      via: "direct import and call after entry dict construction"
      pattern: "compute_lifecycle|compute_health|compute_maturity|compute_next_step"
    - from: "asset_index.build_index()"
      to: "asset_index._build_entry()"
      via: "existing call chain (unchanged)"
      pattern: "_build_entry\\(item"
    - from: "asset_index.refresh_index_entry()"
      to: "asset_index._build_entry()"
      via: "existing call chain (unchanged)"
      pattern: "_build_entry\\(found_item"
---

<objective>
Wire the four pure derivation functions from `asset_state.py` (Plan 24-01) into `asset_index.py` so that every canonical index entry produced by `build_index()` and `refresh_index_entry()` includes machine-derived `lifecycle`, `health`, `maturity`, and `next_step` fields.

Purpose: Make lifecycle/health/maturity/next-step available in the canonical index JSON for all consumers (Phase 25 status, plugin dashboard, Phase 26 context packs). Per D-11: fields are embedded during index construction, derived from source artifacts, not hand-edited.
Output: Updated `_build_entry()` with 4 new fields + integration tests verifying the fields appear in both full-build and incremental-refresh paths.
</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/ROADMAP.md §Phase 24 — Goal: "source-derived readiness, health findings, maturity, next steps"
@.planning/REQUIREMENTS.md — STATE-01..04, AIC-01
@.planning/phases/24-derived-lifecycle-health-maturity/24-CONTEXT.md — D-10 (module export names), D-11 (embed in _build_entry/refresh_index_entry)
@paperforge/worker/asset_index.py — _build_entry() lines 254-303 (entry dict construction point), build_index() lines 310-368, refresh_index_entry() lines 376-456
@paperforge/worker/asset_state.py — compute_lifecycle, compute_health, compute_maturity, compute_next_step (created by Plan 24-01)
@tests/test_asset_index_integration.py — Existing integration test patterns (360 lines, _setup_incremental_vault, _write_index, _make_export_item helpers)
@tests/test_asset_state.py — Unit tests for all four functions (26 tests, from Plan 24-01)

<interfaces>
<!-- Contract: asset_state exports consumed by this plan -->

From paperforge/worker/asset_state.py (created by Plan 24-01):
```python
def compute_lifecycle(entry: dict) -> str:
    """Return lifecycle state: indexed | pdf_ready | fulltext_ready | deep_read_done | ai_context_ready."""

def compute_health(entry: dict) -> dict[str, str]:
    """Return {pdf_health: str, ocr_health: str, note_health: str, asset_health: str} with fix paths."""

def compute_maturity(entry: dict) -> dict:
    """Return {level: int 1-6, level_name: str, checks: dict[str,bool], blocking: str|None}."""

def compute_next_step(entry: dict) -> str:
    """Return sync | ocr | repair | /pf-deep | rebuild index | ready."""
```

<!-- Contract: _build_entry() entry dict shape — where new fields are added -->

From paperforge/worker/asset_index.py `_build_entry()` (lines 254-295):
```python
# After workspace path fields (line 295), the entry dict has these fields:
entry = {
    "zotero_key": key, "domain": domain, "title": ..., "authors": ..., ...
    "has_pdf": bool, "pdf_path": str, "ocr_status": str,
    "deep_reading_status": str, "note_path": str,
    "paper_root": str, "main_note_path": str, "fulltext_path": str,
    "deep_reading_path": str, "ai_path": str,
}
# NEW fields to add after line 295:
# entry["lifecycle"] = compute_lifecycle(entry)
# entry["health"] = compute_health(entry)
# entry["maturity"] = compute_maturity(entry)
# entry["next_step"] = compute_next_step(entry)
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add derived state fields to _build_entry()</name>
  <files>paperforge/worker/asset_index.py</files>
  <read_first>
    - paperforge/worker/asset_index.py lines 254-303 (_build_entry function — the exact insertion point)
    - paperforge/worker/asset_state.py (all four function signatures, created by Plan 24-01)
    - .planning/phases/24-derived-lifecycle-health-maturity/24-CONTEXT.md §D-10, D-11 (locked decisions on module and integration)
  </read_first>
  <action>
    Modify `_build_entry()` in `paperforge/worker/asset_index.py` to import and call the four derivation functions.

    **Step 1 — Add import** (inside _build_entry, alongside existing lazy imports at lines 223-231):

    After the existing lazy imports block (line 223-231) inside `_build_entry()`, add:
    ```python
    from paperforge.worker.asset_state import (
        compute_lifecycle,
        compute_health,
        compute_maturity,
        compute_next_step,
    )
    ```

    **Step 2 — Add derived field computation** (after the entry dict is fully built, before formal note write):

    After the entry dict closing `}` on line 295 and before the `# Write / update the formal note` comment (line 297), insert:
    ```python
    # ---- derived state (Phase 24: lifecycle, health, maturity, next-step) ----
    entry["lifecycle"] = compute_lifecycle(entry)
    entry["health"] = compute_health(entry)
    entry["maturity"] = compute_maturity(entry)
    entry["next_step"] = compute_next_step(entry)
    ```

    The call order is: lifecycle first (needed by maturity), then health, then maturity (calls lifecycle internally), then next_step. This avoids any issues if maturity were to modify the entry dict (it doesn't — pure function).

    **Step 3 — Verify no other changes needed:**

    - `build_index()` calls `_build_entry()` per item (line 360) — new fields flow automatically.
    - `refresh_index_entry()` calls `_build_entry()` for the matched item (line 440) — new fields flow automatically.
    - The entry dict returned by `_build_entry()` now has 4 additional keys: `lifecycle` (str), `health` (dict), `maturity` (dict), `next_step` (str).
    - All new values are JSON-serializable — no changes needed to `atomic_write_index()` or `build_envelope()`.
    - Existing code that reads the entry dict uses `.get()` or direct key access for known fields — new fields are additive, no breakage.

    **Commit:** `feat(24-02): add lifecycle, health, maturity, next_step to canonical index entries`
  </action>
  <verify>
    <automated>python -c "from paperforge.worker.asset_state import compute_lifecycle, compute_health, compute_maturity, compute_next_step; print('asset_state imports OK')" &amp;&amp; python -c "from paperforge.worker.asset_index import build_envelope, get_index_path, _build_entry; print('asset_index imports OK')"</automated>
  </verify>
  <acceptance_criteria>
    - paperforge/worker/asset_index.py contains `from paperforge.worker.asset_state import` inside `_build_entry()`
    - The import block includes all four function names: compute_lifecycle, compute_health, compute_maturity, compute_next_step
    - `entry["lifecycle"] = compute_lifecycle(entry)` is present after the entry dict construction (after the `ai_path` line and before `note_path.parent.mkdir`)
    - `entry["health"] = compute_health(entry)` is present
    - `entry["maturity"] = compute_maturity(entry)` is present
    - `entry["next_step"] = compute_next_step(entry)` is present
    - All four assignment lines appear in the correct section (after entry dict fields, before formal note write)
    - Module imports without error: `from paperforge.worker.asset_index import build_index` succeeds
    - Existing tests still pass: `pytest tests/test_asset_index.py tests/test_asset_index_integration.py -q` exits 0
  </acceptance_criteria>
  <done>_build_entry() now embeds lifecycle, health, maturity, and next_step in every canonical index entry. Both build_index() and refresh_index_entry() produce entries with the four new fields.</done>
</task>

<task type="auto">
  <name>Task 2: Add integration tests for derived state fields in index entries</name>
  <files>tests/test_asset_index_integration.py</files>
  <read_first>
    - tests/test_asset_index_integration.py (existing helpers: _setup_incremental_vault, _write_index, _make_export_item)
    - paperforge/worker/asset_index.py _build_entry() (updated in Task 1 — the fields being tested)
    - paperforge/worker/asset_state.py (the functions producing the values being tested)
  </read_first>
  <action>
    Add a new test class `TestDerivedStateFields` to `tests/test_asset_index_integration.py` at the end of the file (after class `TestWorkspacePaths` at line 360).

    Use the existing `_setup_incremental_vault`, `_write_index`, `_make_export_item` helpers (already imported/defined in the file).

    **Six test methods:**

    ```python
    class TestDerivedStateFields:
        """Lifecycle, health, maturity, next_step fields in index entries."""

        def test_all_four_fields_present_after_full_build(self, tmp_path: Path) -> None:
            """build_index() produces entries with lifecycle, health, maturity, next_step."""
            from paperforge.worker.asset_index import build_index, read_index

            items = [_make_export_item("AAA", "Paper A")]
            vault = _setup_incremental_vault(tmp_path, items)
            build_index(vault)

            result = read_index(vault)
            entry = result["items"][0]

            assert "lifecycle" in entry, "entry must have lifecycle field"
            assert "health" in entry, "entry must have health field"
            assert "maturity" in entry, "entry must have maturity field"
            assert "next_step" in entry, "entry must have next_step field"

        def test_all_four_fields_present_after_incremental_refresh(self, tmp_path: Path) -> None:
            """refresh_index_entry() produces entries with lifecycle, health, maturity, next_step."""
            from paperforge.worker.asset_index import read_index, refresh_index_entry

            items = [_make_export_item("AAA", "Paper A")]
            vault = _setup_incremental_vault(tmp_path, items)
            _write_index(vault, [{"zotero_key": "AAA", "title": "Paper A"}])
            refresh_index_entry(vault, "AAA")

            result = read_index(vault)
            entry = result["items"][0]

            assert "lifecycle" in entry
            assert "health" in entry
            assert "maturity" in entry
            assert "next_step" in entry

        def test_lifecycle_is_valid_state(self, tmp_path: Path) -> None:
            """lifecycle field is one of the six valid state strings."""
            from paperforge.worker.asset_index import build_index, read_index

            items = [_make_export_item("AAA", "Paper A")]
            vault = _setup_incremental_vault(tmp_path, items)
            build_index(vault)

            entry = read_index(vault)["items"][0]
            valid = {"indexed", "pdf_ready", "fulltext_ready", "deep_read_done", "ai_context_ready"}
            assert entry["lifecycle"] in valid, f"got {entry['lifecycle']}"

        def test_health_has_four_dimensions(self, tmp_path: Path) -> None:
            """health field is a dict with pdf_health, ocr_health, note_health, asset_health."""
            from paperforge.worker.asset_index import build_index, read_index

            items = [_make_export_item("AAA", "Paper A")]
            vault = _setup_incremental_vault(tmp_path, items)
            build_index(vault)

            health = read_index(vault)["items"][0]["health"]
            assert isinstance(health, dict), f"health should be dict, got {type(health)}"
            assert "pdf_health" in health
            assert "ocr_health" in health
            assert "note_health" in health
            assert "asset_health" in health
            for v in health.values():
                assert isinstance(v, str) and len(v) > 0, f"health value should be non-empty string"

        def test_maturity_structure(self, tmp_path: Path) -> None:
            """maturity field is a dict with level (int 1-6), level_name (str), checks (6 bools), blocking."""
            from paperforge.worker.asset_index import build_index, read_index

            items = [_make_export_item("AAA", "Paper A")]
            vault = _setup_incremental_vault(tmp_path, items)
            build_index(vault)

            mat = read_index(vault)["items"][0]["maturity"]
            assert isinstance(mat, dict)
            assert isinstance(mat["level"], int) and 1 <= mat["level"] <= 6
            assert isinstance(mat["level_name"], str) and len(mat["level_name"]) > 0
            assert isinstance(mat["checks"], dict)
            for check_name in ("metadata", "pdf", "fulltext", "figure", "ai", "review"):
                assert check_name in mat["checks"], f"missing check: {check_name}"
                assert isinstance(mat["checks"][check_name], bool)
            # blocking is a string or None
            assert mat["blocking"] is None or isinstance(mat["blocking"], str)

        def test_next_step_is_valid_action(self, tmp_path: Path) -> None:
            """next_step is one of the six valid action strings."""
            from paperforge.worker.asset_index import build_index, read_index

            items = [_make_export_item("AAA", "Paper A")]
            vault = _setup_incremental_vault(tmp_path, items)
            build_index(vault)

            next_step = read_index(vault)["items"][0]["next_step"]
            valid = {"sync", "ocr", "repair", "/pf-deep", "rebuild index", "ready"}
            assert next_step in valid, f"got {next_step}"
    ```

    No new imports needed — all imports used are already imported in the file (Path, json, pytest, asset_index functions). The helpers are already defined.

    Run the new tests: `pytest tests/test_asset_index_integration.py::TestDerivedStateFields -v`

    **Commit:** `test(24-02): add integration tests for derived state fields in index entries`
  </action>
  <verify>
    <automated>pytest tests/test_asset_index_integration.py::TestDerivedStateFields -v</automated>
  </verify>
  <acceptance_criteria>
    - tests/test_asset_index_integration.py contains class TestDerivedStateFields with 6 test methods
    - test_all_four_fields_present_after_full_build — verifies lifecycle/health/maturity/next_step keys exist after build_index()
    - test_all_four_fields_present_after_incremental_refresh — same verification after refresh_index_entry()
    - test_lifecycle_is_valid_state — lifecycle value is in {indexed, pdf_ready, fulltext_ready, deep_read_done, ai_context_ready}
    - test_health_has_four_dimensions — health dict has exactly the 4 dimension keys with non-empty string values
    - test_maturity_structure — maturity dict has level (1-6), level_name (str), checks (6 bool keys), blocking
    - test_next_step_is_valid_action — next_step is in {sync, ocr, repair, /pf-deep, rebuild index, ready}
    - All 6 tests pass: `pytest tests/test_asset_index_integration.py::TestDerivedStateFields -v` exits 0
    - All existing tests still pass: `pytest tests/ -q` exits 0
  </acceptance_criteria>
  <done>Six integration tests verify that both build_index() and refresh_index_entry() produce entries with correct lifecycle, health, maturity, and next_step fields.</done>
</task>

<task type="auto">
  <name>Task 3: Verify full test suite and field serialization correctness</name>
  <files>tests/test_asset_index_integration.py</files>
  <read_first>
    - tests/test_asset_index.py (existing unit tests — ensure they still pass)
    - tests/test_asset_state.py (unit tests from Plan 24-01 — ensure they still pass)
    - tests/test_asset_index_integration.py (updated in Task 2)
  </read_first>
  <action>
    Run the complete test suite to verify no regressions and that the new fields serialize correctly in the index JSON:

    1. **Full test suite:**
       ```bash
       pytest tests/ -q --tb=short
       ```
       All existing tests must pass. Pay special attention to:
       - `test_asset_index.py` — Tests that check entry structure (e.g., `TestIncrementalRefreshBehavior`) may need updating if they assert exact entry contents
       - `test_asset_index_integration.py` — The `test_incremental_refresh_preserves_unrelated_entries` test asserts BBB and CCC are `{"zotero_key": "BBB", "title": "Paper B"}` — these entries were written with only those two fields and NOT refreshed, so they won't have the new fields (that's correct behavior — only refreshed entries get the new fields)

    2. **Check test_incremental_refresh_preserves_unrelated_entries:** 
       This test at line 158-192 writes a pre-built index with only `zotero_key` and `title` for BBB/CCC, then refreshes AAA. BBB and CCC are NOT refreshed — they keep their pre-built data. This is correct behavior. Verify the test still passes.

    3. **Verify index JSON serialization:**
       ```bash
       python -c "
       from paperforge.worker.asset_index import build_envelope
       import json
       entry = {'zotero_key': 'TEST', 'has_pdf': True, 'ocr_status': 'pending', 
                'deep_reading_status': 'pending', 'note_path': '', 'pdf_path': '',
                'ai_path': '', 'fulltext_path': '', 'deep_reading_path': '', 'main_note_path': '',
                'ocr_json_path': '', 'title': 'Test', 'authors': []}
       from paperforge.worker.asset_state import compute_lifecycle, compute_health, compute_maturity, compute_next_step
       entry['lifecycle'] = compute_lifecycle(entry)
       entry['health'] = compute_health(entry)
       entry['maturity'] = compute_maturity(entry)
       entry['next_step'] = compute_next_step(entry)
       envelope = build_envelope([entry])
       serialized = json.dumps(envelope, ensure_ascii=False, indent=2)
       assert 'lifecycle' in serialized
       assert 'health' in serialized
       assert 'maturity' in serialized
       assert 'next_step' in serialized
       # Verify re-parse yields same structure
       reparsed = json.loads(serialized)
       assert isinstance(reparsed['items'][0]['maturity']['checks'], dict)
       print('JSON serialization OK — all four fields round-trip correctly')
       "
       ```

    4. **Fix any test failures:**
       - If `test_incremental_refresh_preserves_unrelated_entries` asserts BBB/CCC exactly, it should still pass since those entries were pre-built with only two fields and never refreshed.
       - If any other test breaks due to the new fields, update the test to accommodate the new fields (additive change, no semantic breakage).

    **Commit:** `test(24-02): verify full test suite and serialization correctness`
  </action>
  <verify>
    <automated>pytest tests/ -q --tb=short</automated>
  </verify>
  <acceptance_criteria>
    - Full test suite passes: `pytest tests/ -q` exits 0
    - test_asset_index.py: all existing tests pass (additive fields, no semantic breakage)
    - test_asset_state.py: all 26 tests pass (unchanged from Plan 24-01)
    - test_asset_index_integration.py: all existing + 6 new tests pass (15 total in file)
    - JSON serialization round-trip: lifecycle/health/maturity/next_step survive json.dumps → json.loads
    - health dict values are non-empty strings in serialized JSON
    - maturity.checks has all 6 bool keys in serialized JSON
    - maturity.level is an integer 1-6 in serialized JSON
  </acceptance_criteria>
  <done>All tests pass. Derived state fields are correctly serialized in the canonical index JSON. Ready for Phase 25 surface convergence.</done>
</task>

</tasks>

<verification>
All three integration tasks complete:
1. _build_entry() imports and calls all four compute functions — fields added to entry dict
2. Six integration tests verify fields in both build_index() and refresh_index_entry() paths
3. Full test suite passes — no regressions, correct JSON serialization

Both entry points (full build and incremental refresh) now produce entries with lifecycle, health, maturity, and next_step fields derived from source artifacts per D-11.
</verification>

<success_criteria>
- [x] _build_entry() calls compute_lifecycle, compute_health, compute_maturity, compute_next_step
- [x] Entry dict contains lifecycle (str), health (dict), maturity (dict), next_step (str) fields
- [x] build_index() produces entries with all four fields
- [x] refresh_index_entry() produces entries with all four fields
- [x] Integration tests verify field presence, types, and value validity
- [x] Full test suite passes (including Phase 23 tests)
- [x] Index JSON serialization round-trips all four new fields correctly
- [x] No schema version bump needed (additive, forward-compatible change)
</success_criteria>

<output>
After completion, create `.planning/phases/24-derived-lifecycle-health-maturity/24-02-SUMMARY.md`
</output>
