---
phase: 46-index-path-resolution
plan: 001
type: execute
wave: 1
depends_on: []
files_modified:
  - paperforge/worker/asset_index.py
  - paperforge/config.py
  - tests/test_config.py
autonomous: true
requirements:
  - PATH-01
  - PATH-02
  - PATH-03
  - PATH-04

must_haves:
  truths:
    - "Canonical index entry `paper_root` uses the user's configured literature_dir (not hardcoded 'Literature/')"
    - "Canonical index entries `main_note_path`, `fulltext_path`, `deep_reading_path`, `ai_path` all use config-resolved literature_dir"
    - "Environment variable PAPERFORGE_LITERATURE_DIR correctly overrides literature_dir (no truncation to PAPERFORGERATURE_DIR)"
    - "paperforge_paths() library_records key returns <control>/library-records matching its docstring"
    - "Legacy paperforge.json top-level skill_dir and command_dir migrate into vault_config on first sync"
  artifacts:
    - path: "paperforge/worker/asset_index.py"
      provides: "5 config-resolved workspace-path fields in canonical index entries"
      min_lines: 5
      contains: "paper_root.*relative_to"
    - path: "paperforge/config.py"
      provides: "Corrected env var name, library_records path, and CONFIG_PATH_KEYS"
      contains: "PAPERFORGE_LITERATURE_DIR"
    - path: "tests/test_config.py"
      provides: "Updated env var name in test assertion"
      contains: "PAPERFORGE_LITERATURE_DIR"
  key_links:
    - from: "paperforge/worker/asset_index.py:334-338"
      to: "paperforge/config.py load_vault_config()"
      via: "paths dict from paperforge_paths()"
      pattern: "workspace_dir.*relative_to"
    - from: "paperforge/config.py:65"
      to: "paperforge/config.py:226-230 env var loading loop"
      via: "ENV_KEYS dict lookup"
      pattern: "PAPERFORGE_LITERATURE_DIR"
---

<objective>
Replace 5 hardcoded "Literature/" path strings in the canonical index builder with config-resolved paths, fix the env var typo, fix the library_records path to match its docstring, and add missing migration keys.

**Purpose:** Eliminate root cause #1 of the v1.9 ripple (index hardcodes "Literature/" path) and 3 config correctness defects that cause silent path misresolution in custom vault layouts.

**Output:** 
- `asset_index.py` — 5 workspace-path fields dynamically resolved via `pathlib.relative_to(vault)`
- `config.py` — env var `PAPERFORGE_LITERATURE_DIR` works correctly; `library_records` key returns `<control>/library-records`; `CONFIG_PATH_KEYS` includes `skill_dir` and `command_dir` for full migration coverage
- `test_config.py` — env var name assertion updated
</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/phases/46-index-path-resolution/46-CONTEXT.md
@.planning/REQUIREMENTS.md

**Key codebase surfaces:**
- `paperforge/worker/asset_index.py:228-365` — `_build_entry()` constructs the canonical index entry dict. Lines 334-338 have 5 hardcoded `"Literature/"` prefix strings.
- `paperforge/config.py:60-70` — `ENV_KEYS` dict maps config keys to env var names. Line 65 has the typo.
- `paperforge/config.py:255-336` — `paperforge_paths()` builds the path inventory. Line 331 returns just `control` instead of `control / "library-records"`.
- `paperforge/config.py:358-364` — `CONFIG_PATH_KEYS` tuple drives migration from legacy top-level keys to `vault_config`. Missing `skill_dir` and `command_dir`.
- `tests/test_config.py:167-182` — `test_env_keys_has_all_required_overrides` checks all env var names. Line 175 has the truncated name.

**Established patterns:**
- Path construction in `_build_entry()` already uses `paths["literature"]` (line 282) for `workspace_dir` and `main_note_path` Path objects. These are correctly config-resolved.
- All other vault-relative paths in the index entry (lines 325-332) already use `path.relative_to(vault)`.
- `CONFIG_PATH_KEYS` drives the `migrate_paperforge_json()` function (line 367+).
</context>

<tasks>

<task type="auto">
  <name>Task 1: Replace 5 hardcoded "Literature/" paths in asset_index.py with config-resolved relative paths (PATH-01)</name>
  <files>paperforge/worker/asset_index.py</files>
  <action>
    Replace lines 334-338 in `_build_entry()`. The 5 workspace-path fields currently use hardcoded `f"Literature/{domain}/{key} - {title_slug}/..."` strings.

    The function already computes `workspace_dir` (line 282) and `main_note_path` (line 283) as Path objects using `paths["literature"]` (which resolves to the user's configured `literature_dir`). Use `pathlib.Path.relative_to(vault)` on these existing Path objects to derive each vault-relative string, with forward-slash normalization via `.replace("\\", "/")`.

    Replace:
    ```python
        # Workspace path fields (Phase 22 paper workspace structure, per D-12)
        "paper_root": f"Literature/{domain}/{key} - {title_slug}/",
        "main_note_path": f"Literature/{domain}/{key} - {title_slug}/{key} - {title_slug}.md",
        "fulltext_path": f"Literature/{domain}/{key} - {title_slug}/fulltext.md",
        "deep_reading_path": f"Literature/{domain}/{key} - {title_slug}/deep-reading.md",
        "ai_path": f"Literature/{domain}/{key} - {title_slug}/ai/",
    ```

    With:
    ```python
        # Workspace path fields — config-resolved via paths["literature"] (Phase 46: PATH-01)
        "paper_root": str(workspace_dir.relative_to(vault)).replace("\\", "/") + "/",
        "main_note_path": str(main_note_path.relative_to(vault)).replace("\\", "/"),
        "fulltext_path": str((workspace_dir / "fulltext.md").relative_to(vault)).replace("\\", "/"),
        "deep_reading_path": str((workspace_dir / "deep-reading.md").relative_to(vault)).replace("\\", "/"),
        "ai_path": str((workspace_dir / "ai").relative_to(vault)).replace("\\", "/") + "/",
    ```

    Key points:
    - `workspace_dir` (line 282) = `paths["literature"] / domain / f"{key} - {title_slug}"` — already config-resolved
    - `main_note_path` (line 283) = `workspace_dir / f"{key} - {title_slug}.md"` — already config-resolved
    - Use `.replace("\\", "/")` for Windows portability (consistent with existing patterns at lines 325-332)
    - Do NOT touch the `note_path` at line 325 — it's already dynamically resolved
  </action>
  <verify>
    <automated>
      # Verify the 5 fields are no longer hardcoded "Literature/" strings
      $file = "paperforge/worker/asset_index.py"
      $content = Get-Content $file -Raw
      # Should NOT find old pattern
      if ($content -match '"paper_root": f"Literature/') { throw "FAIL: paper_root still uses hardcoded Literature/" }
      if ($content -match '"main_note_path": f"Literature/') { throw "FAIL: main_note_path still uses hardcoded Literature/" }
      if ($content -match '"fulltext_path": f"Literature/') { throw "FAIL: fulltext_path still uses hardcoded Literature/" }
      if ($content -match '"deep_reading_path": f"Literature/') { throw "FAIL: deep_reading_path still uses hardcoded Literature/" }
      if ($content -match '"ai_path": f"Literature/') { throw "FAIL: ai_path still uses hardcoded Literature/" }
      # Should find new patterns with relative_to
      if ($content -notmatch 'relative_to\(vault\)') { throw "FAIL: No relative_to call found in the 5 path fields" }
      Write-Output "[OK] All 5 workspace-path fields use config-resolved paths"
    </automated>
  </verify>
  <done>
    - `paper_root`, `main_note_path`, `fulltext_path`, `deep_reading_path`, `ai_path` all resolve via `workspace_dir.relative_to(vault)` and `main_note_path.relative_to(vault)` instead of hardcoded `f"Literature/..."`
    - A vault configured with `literature_dir: "Papers"` will produce paths starting with `Papers/` instead of `Literature/`
    - Python syntax check passes (`python -c "import ast; ast.parse(open('paperforge/worker/asset_index.py').read())"`)
  </done>
</task>

<task type="auto">
  <name>Task 2: Fix config.py env var typo, library_records path, CONFIG_PATH_KEYS, and test_config.py (PATH-02, PATH-03, PATH-04)</name>
  <files>paperforge/config.py, tests/test_config.py</files>
  <action>
    Apply three corrections to `paperforge/config.py` and one to `tests/test_config.py`:

    **Fix 1 — PATH-03: Env var name typo (config.py line 65)**
    Change:
    ```python
        "literature_dir": "paperforgeRATURE_DIR",
    ```
    To:
    ```python
        "literature_dir": "PAPERFORGE_LITERATURE_DIR",
    ```
    Rationale: The string was formed by concatenating `PAPERFORGE` with `LITERATURE_DIR` — but the `LI` was dropped, making it `paperforgeRATURE_DIR` (with lowercase 'p' inconsistency too). The corrected name `PAPERFORGE_LITERATURE_DIR` follows the established naming convention of `PAPERFORGE_<CONFIG_KEY>` in uppercase.

    **Fix 2 — PATH-02: library_records path matches docstring (config.py line 331)**
    Change:
    ```python
        "library_records": control,
    ```
    To:
    ```python
        "library_records": control / "library-records",
    ```
    Rationale: The docstring at line 278 documents the key as returning `<control>/library-records`, but the code returned just `<control>` (effectively `<control>` itself). This caused `sync.py:722` to construct `paths["library_records"] / domain / key.md` which resolved to `<control>/<domain>/key.md` instead of `<control>/library-records/<domain>/key.md`. Fix the code to match the documented behavior. The phantom `<control>/library-records/` subdirectory is acceptable — `migrate_paperforge_json()` may need to create it during migration if it doesn't already exist, but the Path object itself is just a path reference, not a directory existence requirement. Phase 47 (LEGACY-02) will remove the dead consumer in `sync.py:722-723`.

    **Fix 3 — PATH-04: Add missing keys to CONFIG_PATH_KEYS (config.py lines 358-364)**
    Change `CONFIG_PATH_KEYS` from:
    ```python
    CONFIG_PATH_KEYS: tuple[str, ...] = (
        "system_dir",
        "resources_dir",
        "literature_dir",
        "control_dir",
        "base_dir",
    )
    ```
    To:
    ```python
    CONFIG_PATH_KEYS: tuple[str, ...] = (
        "system_dir",
        "resources_dir",
        "literature_dir",
        "control_dir",
        "base_dir",
        "skill_dir",
        "command_dir",
    )
    ```
    Rationale: `migrate_paperforge_json()` (line 367) iterates over `CONFIG_PATH_KEYS` to detect legacy top-level keys and merge them into `vault_config`. Since `skill_dir` and `command_dir` are defined in `DEFAULT_CONFIG` and managed by `load_vault_config()`, a user who configured them as top-level keys in `paperforge.json` would have them silently orphaned after migration. Adding them ensures all path-relevant keys are migrated. This does NOT change the writing behavior — the migration function already handles arbitrary keys in this tuple.

    **Fix 4 — PATH-03: Update test assertion (test_config.py line 175)**
    Change:
    ```python
        "paperforgeRATURE_DIR",
    ```
    To:
    ```python
        "PAPERFORGE_LITERATURE_DIR",
    ```
    This test at `tests/test_config.py:167-182` (`test_env_keys_has_all_required_overrides`) validates that all expected env var names are registered in `ENV_KEYS`. The test must reflect the corrected name.
  </action>
  <verify>
    <automated>
      $errors = @()
      # PATH-03: Check env var name in config.py
      $cfgContent = Get-Content "paperforge/config.py" -Raw
      if ($cfgContent -match '"literature_dir":\s*"(?!PAPERFORGE_LITERATURE_DIR)') {
        $errors += "FAIL: literature_dir env var is not PAPERFORGE_LITERATURE_DIR"
      } elseif ($cfgContent -match "paperforgeRATURE_DIR") {
        $errors += "FAIL: Old typo paperforgeRATURE_DIR still present in config.py"
      } else {
        Write-Output "[OK] PATH-03: config.py env var corrected to PAPERFORGE_LITERATURE_DIR"
      }

      # PATH-02: Check library_records path
      # Look for: "library_records": control / "library-records"
      if ($cfgContent -match '"library_records":\s*control\s*/\s*"library-records"') {
        Write-Output "[OK] PATH-02: library_records returns control / 'library-records'"
      } else {
        $errors += "FAIL: library_records path does not resolve to control / 'library-records'"
      }

      # PATH-04: Check CONFIG_PATH_KEYS includes skill_dir and command_dir
      $cfgLines = Get-Content "paperforge/config.py"
      $inKeys = $false
      $foundSkill = $false
      $foundCommand = $false
      foreach ($line in $cfgLines) {
        if ($line -match 'CONFIG_PATH_KEYS') { $inKeys = $true }
        if ($inKeys -and $line -match '"skill_dir"') { $foundSkill = $true }
        if ($inKeys -and $line -match '"command_dir"') { $foundCommand = $true }
        if ($inKeys -and $line -match '^\)') { break }
      }
      if ($foundSkill) { Write-Output "[OK] PATH-04: CONFIG_PATH_KEYS includes skill_dir" }
      else { $errors += "FAIL: CONFIG_PATH_KEYS missing skill_dir" }
      if ($foundCommand) { Write-Output "[OK] PATH-04: CONFIG_PATH_KEYS includes command_dir" }
      else { $errors += "FAIL: CONFIG_PATH_KEYS missing command_dir" }

      # PATH-03: Check test_config.py
      $testContent = Get-Content "tests/test_config.py" -Raw
      if ($testContent -match "paperforgeRATURE_DIR") {
        $errors += "FAIL: Old typo still present in test_config.py"
      } elseif ($testContent -match "PAPERFORGE_LITERATURE_DIR") {
        Write-Output "[OK] PATH-03: test_config.py env var name updated"
      } else {
        $errors += "FAIL: PAPERFORGE_LITERATURE_DIR not found in test_config.py"
      }

      if ($errors.Count -gt 0) { throw ($errors -join "`n") }
      Write-Output "[OK] All 4 fixes verified"
    </automated>
  </verify>
  <done>
    - `config.py:65` — env var name is `PAPERFORGE_LITERATURE_DIR` (not `paperforgeRATURE_DIR`)
    - `config.py:331` — `library_records` returns `control / "library-records"` matching the docstring
    - `config.py:358-364` — `CONFIG_PATH_KEYS` includes `skill_dir` and `command_dir`
    - `tests/test_config.py:175` — test assertion uses `PAPERFORGE_LITERATURE_DIR`
    - Python syntax check passes for both files (`python -c "import ast; ast.parse(open('paperforge/config.py').read())"`)
  </done>
</task>

</tasks>

<verification>
**Overall verification for this plan:**

1. Python import check — all modified modules import without errors:
   ```bash
   python -c "from paperforge.config import ENV_KEYS, CONFIG_PATH_KEYS, paperforge_paths; print('config.py OK')"
   python -c "from paperforge.worker.asset_index import _build_entry; print('asset_index.py OK')"
   ```

2. Unit test run for affected tests:
   ```bash
   python -m pytest tests/test_config.py::test_env_keys_has_all_required_overrides -x -q --tb=short
   ```

3. Path resolution correctness — smoke test verifying config-resolved paths work:
   ```bash
   python -c "
   from paperforge.config import paperforge_paths, load_vault_config
   from pathlib import Path
   import tempfile, os
   with tempfile.TemporaryDirectory() as td:
       vault = Path(td)
       (vault / 'System' / 'PaperForge' / 'exports').mkdir(parents=True)
       (vault / 'System' / 'PaperForge' / 'ocr').mkdir(parents=True)
       (vault / 'MyPapers').mkdir()  # custom literature_dir
       (vault / 'LitControl').mkdir()
       (vault / 'Bases').mkdir()
       (vault / '.opencode' / 'skills').mkdir(parents=True)
       (vault / '.opencode' / 'command').mkdir(parents=True)
       cfg = load_vault_config(vault, overrides={'literature_dir': 'MyPapers', 'control_dir': 'LitControl'})
       paths = paperforge_paths(vault, cfg)
       assert str(paths['literature']).endswith('MyPapers'), f'Expected MyPapers, got {paths[\"literature\"]}'
       assert str(paths['library_records']).endswith('LitControl/library-records'), f'Expected LitControl/library-records, got {paths[\"library_records\"]}'
       print('[OK] Config-resolved paths verified')
   "
   ```
</verification>

<success_criteria>
1. All 5 workspace-path fields in canonical index entries use config-resolved `literature_dir` — verified by running `paperforge sync` and inspecting index entries (NOTE: full verification requires a vault with Zotero exports; automated smoke test above confirms path resolution logic).
2. `PAPERFORGE_LITERATURE_DIR` env var correctly overrides `literature_dir` — the env var name change is syntactically verified (no more `paperforgeRATURE_DIR` anywhere).
3. `paperforge_paths()["library_records"]` returns `<control>/library-records` matching its docstring.
4. `CONFIG_PATH_KEYS` includes `skill_dir` and `command_dir` — verified by static analysis.
5. All modified files pass Python syntax and import validation.
</success_criteria>

<output>
After completion, create `.planning/phases/46-index-path-resolution/46-001-SUMMARY.md`
</output>
