---
phase: 29-per-paper-view
plan: 01
type: execute
wave: 1
depends_on: [28-02, 27-02]
files_modified:
  - paperforge/plugin/main.js
  - paperforge/plugin/styles.css
autonomous: true
requirements: [PAPER-01, PAPER-02, PAPER-03, PAPER-04]

must_haves:
  truths:
    - "User opens a paper note with zotero_key — dashboard switches to paper mode with metadata (title, authors, year) at the top"
    - "User sees a lifecycle stepper showing the paper's current stage (imported/indexed/pdf_ready/fulltext_ready/deep_read/ai_ready) with completed/current/pending state classes"
    - "User sees a 2x2 health matrix with PDF/OCR/Note/Asset cells, color-coded ok/warn/fail with hover tooltips"
    - "User sees a maturity gauge (1-6 segments) with level number and blocking checks listed when level < 6"
    - "User sees a next-step recommendation card with human-readable action and a clickable trigger button that runs the recommended CLI command or copies the zotero_key for /pf-deep"
    - "User sees contextual action buttons above the components: Copy Context (copies JSON to clipboard) and Open Fulltext (opens the fulltext.md file)"
  artifacts:
    - path: "paperforge/plugin/styles.css"
      provides: "Per-paper view layout CSS (Section 15) + next-step card CSS (Section 16)"
      contains: ".paperforge-paper-view, .paperforge-paper-header, .paperforge-paper-actions, .paperforge-contextual-btn, .paperforge-next-step-card"
    - path: "paperforge/plugin/main.js"
      provides: "Replaces _renderPaperMode() placeholder with full per-paper view rendering"
      contains: "_renderPaperMode(), _openFulltext()"
  key_links:
    - from: "_detectAndSwitch()"
      to: "_renderPaperMode()"
      via: "_switchMode('paper')"
      pattern: "_renderPaperMode"
    - from: "_renderPaperMode()"
      to: "_renderLifecycleStepper()"
      via: "this._renderLifecycleStepper(contentContainer, entry, entry.lifecycle)"
      pattern: "_renderLifecycleStepper"
    - from: "_renderPaperMode()"
      to: "_renderHealthMatrix()"
      via: "this._renderHealthMatrix(contentContainer, entry.health)"
      pattern: "_renderHealthMatrix"
    - from: "_renderPaperMode()"
      to: "_renderMaturityGauge()"
      via: "this._renderMaturityGauge(contentContainer, entry.maturity.level, entry.maturity.blocking)"
      pattern: "_renderMaturityGauge"
    - from: "_renderPaperMode()"
      to: "_runAction()"
      via: "next-step action button calls this._runAction()"
      pattern: "_runAction"
---

<objective>
Wire up the per-paper dashboard view using Phase 27 component render methods, triggered by Phase 28 context detection. Replace the `_renderPaperMode()` placeholder with a full rendering pipeline: metadata header, lifecycle stepper, health matrix, maturity gauge, next-step recommendation card with action trigger, and contextual action buttons (Copy Context, Open Fulltext).

Purpose: User can see the full lifecycle, health, maturity, and next-step guidance for any individual paper by opening its note and viewing the PaperForge dashboard.
Output: Modified `paperforge/plugin/main.js` + `paperforge/plugin/styles.css`
</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/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md

@.planning/phases/27-component-library/27-02-SUMMARY.md
@.planning/phases/28-dashboard-shell-context-detection/28-02-SUMMARY.md

@paperforge/plugin/main.js
@paperforge/plugin/styles.css
@paperforge/worker/asset_state.py
@paperforge/worker/asset_index.py

<interfaces>
<!-- Key types and contracts needed by the executor -->

Entry shape from formal-library.json (canonical index entries built in Phase 23 and enriched in Phase 24):
```
entry = {
  "zotero_key": "ABCDEFG",
  "title": "Paper Title",
  "authors": ["Author1", "Author2"],  // array of strings
  "year": "2024",
  "domain": "骨科",
  "lifecycle": "fulltext_ready",       // string from compute_lifecycle(): imported|indexed|pdf_ready|fulltext_ready|deep_read_done|ai_context_ready
  "health": {
    "pdf_health": "ok",                // status string or detailed check message
    "ocr_health": "done",
    "note_health": "healthy",
    "asset_health": "healthy"
  },
  "maturity": {
    "level": 4,                        // number 1-6
    "blocking": ["Fulltext needed"]    // array of strings, empty when level=6
  },
  "next_step": "ocr",                  // string: sync|ocr|repair|/pf-deep|rebuild index|ready
  "fulltext_path": "Literature/domain/key - Title/fulltext.md",
  "note_path": "Literature/domain/key - Title.md",
}
```

Available render methods (from Phase 27):
```javascript
_renderLifecycleStepper(container, lifecycle, currentStage)
  // lifecycle: passed through for truthiness check, currentStage: string matching stages[i].key
  // Valid stage keys: imported, indexed, pdf_ready, fulltext_ready, deep_read, ai_ready

_renderHealthMatrix(container, health)
  // health object with keys: pdf_health, ocr_health, note_health, asset_health
  // status values: ok/healthy → .ok class, warn/warning → .warn class, anything else → .fail class

_renderMaturityGauge(container, maturityLevel, blockingChecks)
  // maturityLevel: number 1-6, blockingChecks: array of strings

_renderSkeleton(container)    // adds .paperforge-loading class for shimmer
_renderEmptyState(container, message)  // shows muted "No data" / custom message
```

State references (from Phase 28):
```javascript
this._currentPaperEntry  // full entry dict or null (set by _detectAndSwitch → _findEntry)
this._currentPaperKey    // zotero_key string or null
this._currentMode        // 'global' | 'paper' | 'collection'
this._contentEl          // .paperforge-content-area container
```

Existing ACTIONS array members relevant to per-paper mode:
- paperforge-copy-context: { id:'paperforge-copy-context', cmd:'context', needsKey:true, ... }
- paperforge-sync: { id:'paperforge-sync', cmd:'sync', ... }
- paperforge-ocr: { id:'paperforge-ocr', cmd:'ocr', ... }
- paperforge-repair: { id:'paperforge-repair', cmd:'repair', ... }

Existing _runAction(a, card) — a is an action object from ACTIONS, card is the DOM element for status.
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Add per-paper view CSS layout and next-step card styles</name>
  <files>paperforge/plugin/styles.css</files>
  <read_first>
    - paperforge/plugin/styles.css (read the last ~80 lines to see Section 13 and 14 patterns and avoid conflicts)
  </read_first>
  <action>
    Append two new CSS sections to `paperforge/plugin/styles.css` AFTER the last Section 14 closing `===` line. Use the exact same section header format as existing sections (e.g., `/* ========================================================================== SECTION N — Name ========================================================================== */`).

    **Section 15 — Per-Paper View:**

    ```css
    /* ==========================================================================
       SECTION 15 — Per-Paper View Layout
       ========================================================================== */
    .paperforge-paper-view {
        display: flex;
        flex-direction: column;
        gap: 20px;
    }

    .paperforge-paper-header {
        padding-bottom: 8px;
        border-bottom: 1px solid var(--background-modifier-border);
    }

    .paperforge-paper-title {
        font-size: var(--font-ui-large);
        font-weight: var(--font-semibold);
        color: var(--text-normal);
        line-height: 1.4;
        margin: 0 0 6px 0;
    }

    .paperforge-paper-meta {
        font-size: var(--font-ui-small);
        color: var(--text-muted);
        line-height: 1.5;
    }

    .paperforge-paper-authors {
        display: inline;
    }

    .paperforge-paper-year {
        display: inline;
    }

    .paperforge-paper-year::before {
        content: " \u00B7  ";
    }

    .paperforge-paper-actions {
        display: flex;
        flex-direction: row;
        gap: 8px;
        flex-wrap: wrap;
    }

    .paperforge-contextual-btn {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        padding: 6px 14px;
        font-size: var(--font-ui-small);
        font-weight: var(--font-semibold);
        color: var(--text-normal);
        background: var(--background-secondary-alt);
        border: 1px solid var(--background-modifier-border);
        border-radius: var(--radius-m);
        cursor: pointer;
        transition: background 0.15s, color 0.15s;
        user-select: none;
    }

    .paperforge-contextual-btn:hover {
        background: var(--background-modifier-hover);
        color: var(--text-accent);
    }

    .paperforge-contextual-btn:active {
        transform: scale(0.97);
    }

    .paperforge-contextual-btn-icon {
        font-size: 14px;
        line-height: 1;
    }
    ```

    **Section 16 — Next-Step Card:**

    ```css
    /* ==========================================================================
       SECTION 16 — Next-Step Recommendation Card
       ========================================================================== */
    .paperforge-next-step-card {
        background: var(--background-secondary);
        border: 1px solid var(--background-modifier-border);
        border-radius: var(--radius-m);
        padding: 16px;
        display: flex;
        flex-direction: column;
        gap: 10px;
    }

    .paperforge-next-step-card.ready {
        border-color: var(--color-green);
        background: color-mix(in srgb, var(--color-green) 8%, var(--background-secondary));
    }

    .paperforge-next-step-label {
        font-size: var(--font-ui-smaller);
        font-weight: var(--font-semibold);
        text-transform: uppercase;
        letter-spacing: 0.5px;
        color: var(--text-muted);
    }

    .paperforge-next-step-text {
        font-size: var(--font-ui-small);
        color: var(--text-normal);
        line-height: 1.5;
    }

    .paperforge-next-step-trigger {
        display: inline-flex;
        align-items: center;
        gap: 6px;
        padding: 6px 14px;
        font-size: var(--font-ui-small);
        font-weight: var(--font-semibold);
        color: var(--text-on-accent);
        background: var(--interactive-accent);
        border: none;
        border-radius: var(--radius-m);
        cursor: pointer;
        transition: opacity 0.15s;
        align-self: flex-start;
        user-select: none;
    }

    .paperforge-next-step-trigger:hover {
        opacity: 0.85;
    }

    .paperforge-next-step-trigger:active {
        transform: scale(0.97);
    }

    .paperforge-next-step-trigger.running {
        opacity: 0.6;
        pointer-events: none;
    }
    ```

    Insert the above two sections at the end of the file (after the last Section 14 closing fence). Ensure the section numbering is correct — these are Sections 15 and 16, continuing from Phase 28's Section 14.
  </action>
  <verify>
    <automated>rg -n "SECTION 15" paperforge/plugin/styles.css && rg -n "SECTION 16" paperforge/plugin/styles.css</automated>
  </verify>
  <acceptance_criteria>
    - `styles.css` contains `/* SECTION 15 -- Per-Paper View Layout */`
    - `styles.css` contains `/* SECTION 16 -- Next-Step Recommendation Card */`
    - Class `.paperforge-paper-view` defined in CSS
    - Class `.paperforge-next-step-card` defined in CSS
    - Class `.paperforge-contextual-btn` defined in CSS
    - No syntax errors: `node -e "require('fs').readFileSync('paperforge/plugin/styles.css','utf-8')"` succeeds
  </acceptance_criteria>
  <done>CSS Sections 15 and 16 added. Per-paper view has layout container, header, meta, actions row, contextual button, and next-step card styling.</done>
</task>

<task type="auto">
  <name>Task 2: Replace _renderPaperMode() placeholder with full per-paper view — metadata, components, contextual actions, and next-step card</name>
  <files>paperforge/plugin/main.js</files>
  <read_first>
    - paperforge/plugin/main.js lines 813-834 (existing _renderPaperMode placeholder)
    - paperforge/plugin/main.js lines 880-992 (existing _runAction method, for reuse pattern)
    - paperforge/plugin/main.js lines 557-660 (existing _renderLifecycleStepper, _renderHealthMatrix, _renderMaturityGauge signatures)
    - paperforge/plugin/main.js lines 157-208 (ACTIONS array, for matching action objects by cmd)
  </read_first>
  <action>
    Replace the existing `_renderPaperMode()` method (lines 813-834) with the full implementation below. Also add a new helper method `_openFulltext(path)`.

    **Step 1: Replace `_renderPaperMode()`**

    Remove the existing placeholder (lines 813-834). Insert the new implementation:

    ```javascript
    /* ── Per-Paper Mode Render (D-01 through D-09, Phase 29) ── */
    _renderPaperMode() {
        const entry = this._currentPaperEntry;
        const key = this._currentPaperKey;

        // --- Handle loading / empty / not-found states (D-03) ---
        if (!key) {
            // No key at all (defensive fallback)
            this._renderEmptyState(this._contentEl, 'No paper data available.');
            return;
        }

        if (!entry) {
            // Key exists but entry not found in index (D-18)
            this._contentEl.createEl('div', {
                cls: 'paperforge-content-placeholder',
                text: 'Paper "' + key + '" not found in canonical index. Sync first.',
            });
            return;
        }

        // --- Create per-paper view container (D-04 through D-09) ---
        const view = this._contentEl.createEl('div', { cls: 'paperforge-paper-view' });

        // ============ Contextual Action Buttons Row (D-10, D-11, D-12, D-13) ============
        const actionsRow = view.createEl('div', { cls: 'paperforge-paper-actions' });

        // "Copy Context" button (D-11) — reuse existing paperforge-copy-context action
        const ctxBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn' });
        ctxBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\u2139' });
        ctxBtn.createEl('span', { text: 'Copy Context' });
        ctxBtn.addEventListener('click', () => {
            const action = ACTIONS.find(a => a.id === 'paperforge-copy-context');
            if (action) this._runAction(action, ctxBtn);
        });

        // "Open Fulltext" button (D-12) — open fulltext.md in Obsidian
        if (entry.fulltext_path) {
            const ftBtn = actionsRow.createEl('button', { cls: 'paperforge-contextual-btn' });
            ftBtn.createEl('span', { cls: 'paperforge-contextual-btn-icon', text: '\uD83D\uDCC4' });
            ftBtn.createEl('span', { text: 'Open Fulltext' });
            ftBtn.addEventListener('click', () => this._openFulltext(entry.fulltext_path));
        }

        // ============ Paper Metadata Header (D-04) ============
        const header = view.createEl('div', { cls: 'paperforge-paper-header' });
        const titleText = entry.title || 'Untitled';
        header.createEl('div', { cls: 'paperforge-paper-title', text: titleText });

        const meta = header.createEl('div', { cls: 'paperforge-paper-meta' });
        if (entry.authors && entry.authors.length > 0) {
            const authorsStr = entry.authors.join(', ');
            meta.createEl('span', { cls: 'paperforge-paper-authors', text: authorsStr });
        }
        if (entry.year) {
            meta.createEl('span', { cls: 'paperforge-paper-year', text: String(entry.year) });
        }

        // ============ Lifecycle Stepper (D-05) ============
        this._renderLifecycleStepper(view, entry, entry.lifecycle);

        // ============ Health Matrix (D-06) ============
        this._renderHealthMatrix(view, entry.health);

        // ============ Maturity Gauge (D-07) ============
        const maturity = entry.maturity || {};
        this._renderMaturityGauge(view, maturity.level, maturity.blocking);

        // ============ Next-Step Recommendation Card (D-08, D-09) ============
        this._renderNextStepCard(view, entry, key);
    }
    ```

    **Step 2: Add `_renderNextStepCard(container, entry, key)` helper**

    Insert this NEW method AFTER `_renderPaperMode()` (after the closing brace of _renderPaperMode):

    ```javascript
    /* ── Next-Step Recommendation Card (D-08, D-09) ── */
    _renderNextStepCard(container, entry, key) {
        const nextStep = entry.next_step || 'ready';

        // Map next_step value to human-readable info
        const stepInfo = {
            'sync':         { label: 'Sync Needed',    text: 'This paper needs to be synced from Zotero. Click to run sync.',             cmd: 'sync',     icon: '\u21BB' },
            'ocr':          { label: 'OCR Needed',     text: 'Fulltext is missing but PDF is present. Click to run OCR.',                  cmd: 'ocr',      icon: '\u229E' },
            'repair':       { label: 'Repair Needed',  text: 'State divergence or path errors detected. Click to repair.',                 cmd: 'repair',   icon: '\u21BA' },
            'rebuild index':{ label: 'Rebuild Needed', text: 'Index may be stale. Click to run sync to rebuild.',                          cmd: 'sync',     icon: '\u21BB' },
            '/pf-deep':     { label: 'Ready for Deep Reading', text: 'Fulltext is ready. Copy key to use /pf-deep in OpenCode.',          cmd: null,       icon: '\uD83D\uDD0D' },
            'ready':        { label: 'All Set',        text: 'This paper is fully processed and ready for use.',                           cmd: 'ready',    icon: '\u2713' },
        };

        const info = stepInfo[nextStep] || stepInfo['ready'];

        // Build the card
        const card = container.createEl('div', { cls: 'paperforge-next-step-card' });
        if (nextStep === 'ready') card.addClass('ready');

        card.createEl('div', { cls: 'paperforge-next-step-label', text: 'Recommended Next Step' });
        card.createEl('div', { cls: 'paperforge-next-step-text', text: info.text });

        if (info.cmd && info.cmd !== 'ready') {
            // Action button for sync/ocr/repair/rebuild index — reuse existing _runAction
            const trigger = card.createEl('button', { cls: 'paperforge-next-step-trigger' });
            trigger.createEl('span', { text: info.icon + '  ' + info.label });
            trigger.addEventListener('click', () => {
                const action = ACTIONS.find(a => a.cmd === info.cmd);
                if (action) this._runAction(action, trigger);
            });
        } else if (nextStep === '/pf-deep') {
            // Copy zotero_key to clipboard for /pf-deep (D-09)
            const trigger = card.createEl('button', { cls: 'paperforge-next-step-trigger' });
            trigger.createEl('span', { text: '\uD83D\uDCCB  Copy Key for /pf-deep' });
            trigger.addEventListener('click', () => {
                navigator.clipboard.writeText(key).then(() => {
                    trigger.setText('\u2713  Copied!');
                    new Notice('Zotero key copied: ' + key);
                }).catch(() => {
                    new Notice('[!!] Clipboard write failed', 6000);
                });
            });
        } else if (nextStep === 'ready') {
            // Show "Copy Context" shortcut for ready state (D-09)
            const trigger = card.createEl('button', { cls: 'paperforge-next-step-trigger' });
            trigger.createEl('span', { text: '\u2139  Copy Context' });
            trigger.addEventListener('click', () => {
                const action = ACTIONS.find(a => a.id === 'paperforge-copy-context');
                if (action) this._runAction(action, trigger);
            });
        }
    }
    ```

    **Step 3: Add `_openFulltext(fulltextPath)` helper**

    Insert this NEW method after `_renderNextStepCard`:

    ```javascript
    /* ── Open Fulltext File in Obsidian (D-12) ── */
    _openFulltext(fulltextPath) {
        if (!fulltextPath) {
            new Notice('[!!] No fulltext path available for this paper', 6000);
            return;
        }
        // fulltext_path is relative to vault root (e.g., "Literature/domain/key - Title/fulltext.md")
        // Use Obsidian API to open the file
        const file = this.app.vault.getAbstractFileByPath(fulltextPath);
        if (file) {
            this.app.workspace.openLinkText(file.path, '');
        } else {
            new Notice('[!!] Fulltext file not found: ' + fulltextPath, 6000);
        }
    }
    ```

    **IMPORTANT IMPLEMENTATION RULES:**
    - Insert `_renderNextStepCard` and `_openFulltext` AFTER `_renderPaperMode`, BEFORE `_renderCollectionMode`. Keep all _renderPaperMode-related methods together.
    - The `_runAction` method (lines 880-992) expects `ACTIONS.find(a => a.cmd === info.cmd)` to return a valid action object. For `cmd: 'sync'`, it matches ACTIONS[0] (paperforge-sync). For `cmd: 'ocr'`, it matches ACTIONS[1] (paperforge-ocr). For `cmd: 'repair'`, it matches ACTIONS[3] (paperforge-repair). These all exist and need no modification.
    - The paperforge-copy-context action with id `'paperforge-copy-context'` already exists in ACTIONS (line 191). It uses `needsKey: true` which reads `zotero_key` from the active file's frontmatter — this works correctly because in per-paper mode the active file IS the paper's `.md` file.
    - For `/pf-deep`, there's no CLI command — just copy the key to clipboard. The unicode escape `\uD83D\uDD0D` renders as a magnifying glass icon.
    - For the "Open Fulltext" button icon `\uD83D\uDCC4` renders as a document icon. 
    - All render method calls (lifecycle stepper, health matrix, maturity gauge) pass `view` as the container — they render into the paper view flow.
    - Do NOT modify any existing method signatures (especially _runAction, _detectAndSwitch, _switchMode) — these are already working correctly.
    - Do NOT modify the existing _renderCollectionMode method.
    - `entry.authors` is an array of strings per `_build_entry()` in asset_index.py line 268: `item.get("authors", [])`.
    - `entry.year` is a string per line 271: `item.get("year", "")`.
  </action>
  <verify>
    <automated>node -e "const c=require('fs').readFileSync('paperforge/plugin/main.js','utf-8'); const hasPaperMode=c.includes('_renderPaperMode'); const hasNextStep=c.includes('_renderNextStepCard'); const hasFulltext=c.includes('_openFulltext'); console.log('renderPaperMode:',hasPaperMode,'nextStepCard:',hasNextStep,'openFulltext:',hasFulltext); process.exit(hasPaperMode&&hasNextStep&&hasFulltext?0:1)"</automated>
  </verify>
  <acceptance_criteria>
    - `main.js` contains `_renderPaperMode()` method replaced with new implementation
    - `main.js` contains `_renderNextStepCard(container, entry, key)` method
    - `main.js` contains `_openFulltext(fulltextPath)` method
    - `main.js` parses without syntax errors: `node -c paperforge/plugin/main.js` succeeds
    - No reference to the old placeholder text "Phase 29 will add" remains in _renderPaperMode
    - Next-step mapping covers all 6 states: sync, ocr, repair, rebuild index, /pf-deep, ready
    - Action buttons in next-step card for sync/ocr/repair call `_runAction` with correct action from ACTIONS
    - Action button for /pf-deep copies `key` to clipboard via `navigator.clipboard.writeText(key)`
    - Action button for "ready" state shows "Copy Context" and calls _runAction with paperforge-copy-context
    - Contextual action buttons row is created with "Copy Context" and "Open Fulltext" (if entry.fulltext_path exists)
    - Paper metadata header renders title, authors (joined by comma), year
    - Lifecycle stepper called with `(view, entry, entry.lifecycle)`
    - Health matrix called with `(view, entry.health)`
    - Maturity gauge called with `(view, maturity.level, maturity.blocking)` where maturity = entry.maturity || {}
  </acceptance_criteria>
  <done>_renderPaperMode() renders: contextual action buttons (Copy Context, Open Fulltext), paper metadata header (title, authors, year), lifecycle stepper, health matrix, maturity gauge with blocking checks, and next-step recommendation card with appropriate action trigger for each state (sync/ocr/repair/pf-deep/ready). Placeholder text fully replaced.</done>
</task>

</tasks>

<verification>
1. Open a paper note (`.md` with `zotero_key` in frontmatter) in Obsidian → PaperForge dashboard shows per-paper view with metadata, components, and actions.
2. Lifecycle stepper highlights current stage with `.current` class, earlier stages show `.completed`, later stages show `.pending`.
3. Health matrix renders 4 color-coded cells (PDF, OCR, Note, Asset) with hover tooltips.
4. Maturity gauge shows 6 segments with filled count matching the paper's level.
5. Next-step card shows the recommended action with explanatory text and a clickable trigger button.
6. "Copy Context" button copies the paper's canonical index entry JSON to clipboard.
7. "Open Fulltext" button (if fulltext_path exists) opens the fulltext.md file in Obsidian.
8. File parses: `node -c paperforge/plugin/main.js` exits 0.
</verification>

<success_criteria>
- Phase 28 `_switchMode('paper')` → `_renderPaperMode()` renders a complete per-paper dashboard
- All 4 Phase 27 component render methods display with correct data from the canonical index entry
- Next-step recommendation drives user action (CLI command or key copy)
- Contextual actions (Copy Context, Open Fulltext) operate correctly
- No new components created — pure wiring of existing Phase 27 and Phase 28 methods
</success_criteria>

<output>
After completion, create `.planning/phases/29-per-paper-view/29-01-SUMMARY.md`
</output>
