---
phase: 54-dashboard-workflow-closure
plan: 001
type: execute
wave: 1
depends_on: []
files_modified:
  - docs/ux-contract.md
  - tests/journey/__init__.py
  - tests/journey/conftest.py
  - tests/journey/test_onboarding.py
  - tests/journey/test_daily_workflow.py
autonomous: true
requirements: [JNY-01, JNY-02, JNY-03]

must_haves:
  truths:
    - "docs/ux-contract.md defines concrete, verifiable step sequences for install, sync, OCR, and dashboard"
    - "Each step in the contract has a single measurable outcome (specific file exists, specific state value, specific command exit code)"
    - "New user onboarding journey test completes in a temp vault: install -> sync -> OCR -> analyze -> deep-read"
    - "Daily workflow journey test completes in a pre-configured temp vault with existing papers"
    - "Both journey tests use journey_vault fixture (tmp_path-based) with isolation guard"
    - "Both journey tests are marked @pytest.mark.journey and excluded from default CI gate"
  artifacts:
    - path: "docs/ux-contract.md"
      provides: "Verifiable UX contracts for all 4 core workflows"
      min_lines: 80
    - path: "tests/journey/conftest.py"
      provides: "Journey fixture pack — fresh_vault and established_vault at different onboarding stages"
      contains: "journey_vault"
    - path: "tests/journey/test_onboarding.py"
      provides: "New user onboarding journey test (JNY-02)"
      contains: "test_new_user_onboarding"
    - path: "tests/journey/test_daily_workflow.py"
      provides: "Daily workflow journey test (JNY-03)"
      contains: "test_existing_user_adds_paper"
  key_links:
    - from: "tests/journey/conftest.py"
      to: "fixtures/vault_builder.py::VaultBuilder"
      via: "Reuses VaultBuilder.build() to construct temp vaults at specific journey levels"
      pattern: "from fixtures.vault_builder import VaultBuilder"
    - from: "tests/journey/test_onboarding.py"
      to: "tests/e2e/conftest.py::e2e_cli_invoker"
      via: "Reuses CLI invoker pattern with journey-specific vault paths"
      pattern: "subprocess.run"
    - from: "tests/journey/test_daily_workflow.py"
      to: "tests/journey/conftest.py::established_vault"
      via: "Consumes established_vault fixture which builds a vault with pre-existing formal notes"
      pattern: "established_vault"
---

<objective>
**Objective:** Define the UX contract document and implement Level 5 user journey tests for the two critical user workflows — new user onboarding and daily paper workflow.

**Purpose:** The UX contract (`docs/ux-contract.md`) gives test authors and developers a shared, verifiable specification of what "correct behavior" means for each workflow. The journey tests then validate that these contracts hold in disposable temp vaults, ensuring that real users experience the documented workflow sequences without surprises.

**Output:** `docs/ux-contract.md` with concrete step sequences; `tests/journey/` with conftest, onboarding test, and daily workflow test.
</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/REQUIREMENTS.md
@.planning/ROADMAP.md
@.planning/phases/54-dashboard-workflow-closure/54-CONTEXT.md

<interfaces>
Reusable infrastructure from Phase 52/53 — DO NOT re-implement:

From tests/e2e/conftest.py:
```python
# e2e_cli_invoker fixture — returns (invoke_fn, vault_path)
#   invoke_fn(["sync"]) -> subprocess.CompletedProcess with .stdout, .stderr, .returncode
# vault is tmp_path-based, auto-cleaned

# vault_builder fixture — session-scoped VaultBuilder instance
# vault_builder.build("minimal") | "standard" | "full" -> Path
```

From tests/cli/conftest.py:
```python
# cli_invoker fixture — returns a function
#   cli_invoker(["paths", "--json"], vault_level="minimal") -> CompletedProcess
#   cli_invoker(["ocr", "--diagnose"], vault_level="standard") -> CompletedProcess

# mock_ocr_backend fixture — context-managed
#   with mock_ocr_backend() as rsps:
#       result = cli_invoker(["ocr"])
```

From fixtures/vault_builder.py:
```python
# VaultBuilder.build(level) -> Path
# "minimal": config + dirs + .env
# "standard": + BBT exports + mock PDFs + Zotero storage
# "full": + OCR fixtures + formal notes + canonical index

# Fixtures root: fixtures/zotero/ (JSON variants), fixtures/pdf/ (PDF samples),
#                fixtures/ocr/ (mock responses + outputs)
```

From fixtures/ocr/mock_ocr_backend.py:
```python
# mock_ocr_success() -> responses.RequestsMock  (context manager)
# mock_ocr_error(status=401) -> responses.RequestsMock
# mock_ocr_timeout() -> responses.RequestsMock  (polls return 'queued' forever)
# mock_ocr_pending() -> responses.RequestsMock  (poll returns 'processing')
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="false">
  <name>Task 1: Write UX Contract Document</name>
  <files>
    - docs/ux-contract.md
  </files>
  <action>
    Create `docs/ux-contract.md` — a concrete, verifiable specification of every user-facing workflow. This is NOT a user guide or tutorial; it is a testable contract that journey tests validate against.

    Structure:

    # UX Contract

    ## Workflow 1: First-Time Installation
    Document 6 sequential steps. Each entry has:
    - **Step**: Short action name (e.g. "Create vault root")
    - **Trigger**: What the user does (e.g. "Run `paperforge setup --headless --vault /path`")
    - **Measurable Outcome**: Single verifiable fact. Use exact file paths, exit codes, and state values. Examples:
      - "`{vault}/paperforge.json` exists with valid JSON containing `system_dir`, `resources_dir`, `literature_dir`"
      - "`{vault}/System/PaperForge/.env` exists and contains `PADDLEOCR_API_TOKEN`"
      - "`paperforge sync` exits code 0 and creates `{vault}/Resources/Literature/{domain}/{key} - {title}.md` with valid frontmatter"
    - **Error Contract**: What happens on failure (exit code, stderr message pattern)

    Steps:
    1. Vault selection / directory creation
    2. Config file written (paperforge.json)
    3. .env created with API token
    4. Directory structure created (System/, Resources/Literature/, Bases/)
    5. First sync runs successfully
    6. First formal note appears in Resources/Literature/

    ## Workflow 2: Daily Sync
    4 steps:
    1. User adds paper in Zotero
    2. Better BibTeX auto-exports JSON
    3. User runs `paperforge sync`
    4. Formal note appears with correct frontmatter state

    ## Workflow 3: OCR Pipeline
    5 steps:
    1. User sets `do_ocr: true` in formal note frontmatter
    2. User runs `paperforge ocr`
    3. OCR job submitted to PaddleOCR API
    4. Job completes, fulltext.md + images extracted
    5. `meta.json` has `ocr_status: done`; formal note `ocr_status` updated to `done`

    ## Workflow 4: Dashboard View (Obsidian Plugin)
    3 steps:
    1. User opens Obsidian, navigates to Bases/
    2. Base view renders with workflow-gate columns (has_pdf, do_ocr, analyze, ocr_status)
    3. Per-paper dashboard shows lifecycle stepper, health matrix, next-step recommendations

    For each step, define:
    - Step ID (W1-S1, W1-S2, etc.)
    - Trigger
    - Action description
    - **Verification** (single shell command or file existence check that proves the step is done)
    - Error contract (exit code and stderr pattern on failure)

    Use markdown tables for the step sequences. Reference a standard install path `{vault}` throughout.

    Do NOT include prose discussions, anecdotal walkthroughs, or tutorial-style explanations. Every line must be verifiable.

    Target: ~100 lines.
  </action>
  <verify>
    <automated>python -c "p=__import__('pathlib').Path('docs/ux-contract.md'); assert p.exists() and len(p.read_text('utf-8')) > 2000, 'UX contract too short or missing'"</automated>
  </verify>
  <done>
    docs/ux-contract.md exists with step sequences for install, sync, OCR, and dashboard. Each step has Trigger, Measurable Outcome, and Error Contract. Total > 2000 chars.
  </done>
</task>

<task type="auto" tdd="false">
  <name>Task 2: Create Journey Test Infrastructure</name>
  <files>
    - tests/journey/__init__.py
    - tests/journey/conftest.py
  </files>
  <action>
    Create journey test infrastructure that reuses Phase 52/53 vault builder and CLI invoker patterns.

    **`tests/journey/__init__.py`** — empty file.

    **`tests/journey/conftest.py`**:

    ```python
    """Journey test fixtures — temp vaults at specific onboarding stages with mock data."""
    ```

    1. Import `VaultBuilder` from `fixtures.vault_builder`, `mock_ocr_success` from `fixtures.ocr.mock_ocr_backend`.

    2. **`journey_fresh_vault` fixture** (function-scoped, tmp_path):
       - Builds vault at "standard" level (has BBT exports, PDFs, Zotero storage but NO formal notes yet)
       - Returns `(vault_path, vault_builder)` tuple so tests can add more data
       - Auto-cleanup on teardown (reuse `_force_rmtree` pattern or VaultBuilder's tmp_path)
       - **Isolation guard**: `assert "tmp" in str(vault)` — must never point at a real vault

    3. **`journey_established_vault` fixture** (function-scoped, tmp_path):
       - Builds vault at "full" level (has formal notes, canonical index, OCR fixtures)
       - Calls `vault_builder.build("full")` then extends with:
         - A second domain (e.g. "sports_medicine") with an additional formal note
         - Updated canonical index reflecting both domains
       - Returns `(vault_path, vault_builder)` tuple
       - **Isolation guard**: `assert "tmp" in str(vault)` — must never point at a real vault

    4. **`journey_cli_invoker` fixture**:
       - Same pattern as `e2e_cli_invoker` from `tests/e2e/conftest.py`
       - Takes a vault path parameter, returns `invoke_fn`
       - `invoke_fn(["sync"])` runs `python -m paperforge --vault {vault} sync` via subprocess

    Fixture guards: Every vault fixture MUST include `assert "tmp" in str(vault)` or `assert "pytest" in str(vault)` to prevent accidental use on real vaults. This is a hard safety requirement for Phase 54 per CHAOS-03 pattern.

    Do NOT duplicate VaultBuilder — reuse it from fixtures/. Do NOT duplicate mock_ocr_backend — import from fixtures/ocr/mock_ocr_backend.py.
  </action>
  <verify>
    <automated>python -c "p=__import__('pathlib').Path('tests/journey/conftest.py'); assert p.exists(); content=p.read_text('utf-8'); assert 'VaultBuilder' in content; assert 'journey_fresh_vault' in content; assert 'journey_established_vault' in content; assert 'journey_cli_invoker' in content; assert 'tmp' in content or 'pytest' in content"</automated>
  </verify>
  <done>
    tests/journey/__init__.py and conftest.py created. conftest provides journey_fresh_vault, journey_established_vault, and journey_cli_invoker fixtures. All vault fixtures have tmp_path isolation guards.
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 3: Implement New User Onboarding & Daily Workflow Journey Tests</name>
  <files>
    - tests/journey/test_onboarding.py
    - tests/journey/test_daily_workflow.py
  </files>
  <behavior>
    # JNY-02: New user onboarding
    Test 1: test_new_user_onboarding
    - Uses journey_fresh_vault fixture
    - Step 1: Run `paperforge sync` — exit code 0, formal notes created
    - Step 2: Read formal note frontmatter — verify zotero_key, domain, has_pdf all populated
    - Step 3: Set `do_ocr: true` in formal note frontmatter
    - Step 4: Run OCR with mock_ocr_success backend — examine output, no crash
    - Step 5: Set `analyze: true` in formal note frontmatter
    - Step 6: Verify deep_reading_status is "pending" (not "done" — agent hasn't run)

    # JNY-03: Daily workflow
    Test 2: test_existing_user_adds_paper
    - Uses journey_established_vault fixture (has existing papers already synced)
    - Step 1: Count existing formal notes
    - Step 2: Add a new BBT JSON export to the vault exports/ directory
    - Step 3: Run `paperforge sync` — exit code 0
    - Step 4: Verify count of formal notes increased by 1
    - Step 5: Verify new note has correct frontmatter, existing notes unchanged
  </behavior>
  <action>
    Write both journey test files.

    **`tests/journey/test_onboarding.py`**:

    ```python
    """JNY-02: New user onboarding journey test.

    Simulates a brand-new user: fresh vault -> sync -> OCR -> analyze -> deep-read ready.
    """
    import pytest

    pytestmark = pytest.mark.journey
    ```

    - `test_new_user_onboarding(journey_fresh_vault, journey_cli_invoker)`:
      - Unpack vault and invoker from fixtures
      - **Step 1: Sync**: `invoker(["sync"])` — assert returncode==0
      - **Step 2: Verify formal notes**: Check `{vault}/Resources/Literature/` for .md files. Read frontmatter, verify `has_pdf: true`, `zotero_key` is non-empty, `domain` is set.
      - **Step 3: Enable OCR**: Edit the formal note frontmatter, set `do_ocr: true`. Use helper to do a clean frontmatter replacement.
      - **Step 4: Run OCR** (with mock): Use `mock_ocr_success()` context manager, call `invoker(["ocr"])` — assert returncode==0, verify stderr/stdout show success
      - **Step 5: Enable analyze**: Edit formal note, set `analyze: true`
      - **Step 6: Verify deep-read queue**: Run `invoker(["deep-reading"])` — assert the output contains the paper key or title, showing it's in the queue

    **`tests/journey/test_daily_workflow.py`**:

    ```python
    """JNY-03: Daily workflow journey test.

    Simulates an existing user: established vault with papers -> add new paper -> sync -> OCR -> read.
    """
    import pytest

    pytestmark = pytest.mark.journey
    ```

    - `test_existing_user_adds_paper(journey_established_vault, journey_cli_invoker)`:
      - Unpack vault and invoker
      - **Step 1: Count existing notes**: Count .md files in `Resources/Literature/*/`
      - **Step 2: Add new export**: Copy an additional BBT JSON fixture (e.g. from `fixtures/zotero/`) into `System/PaperForge/exports/` with a unique name
      - **Step 3: Run sync**: `invoker(["sync"])` — assert returncode==0
      - **Step 4: Verify count increased**: New count == old count + 1
      - **Step 5: Verify new note**: Find the newly created .md file, verify frontmatter correctness. Verify existing notes are unchanged (same content hash for old notes).

    Use mocks from `fixtures.ocr.mock_ocr_backend` for any OCR steps. Use clean frontmatter helper functions (read frontmatter as dict, modify, write back). Do NOT hardcode fixture keys — derive them from the actual vault contents. All assertions must be specific (check actual values, not just existence).
  </action>
  <verify>
    <automated>python -m pytest tests/journey/ -m journey -v --tb=short --timeout=120 2>&1 | findstr /C:"passed" /C:"failed"</automated>
  </verify>
  <done>
    Both journey tests pass. Onboarding test covers fresh vault -> sync -> OCR -> analyze -> deep-read queue. Daily workflow test covers existing vault -> add paper -> sync -> OCR -> verify.
  </done>
</task>

</tasks>

<verification>
1. `docs/ux-contract.md` exists with 4 documented workflows, each step has measurable outcome and error contract
2. `pytest tests/journey/ -m journey --tb=short` passes with both tests green
3. Journey test vaults are tmp_path-based (never touch real vaults)
4. All tests marked `@pytest.mark.journey` (excluded from default CI gate)
</verification>

<success_criteria>
- [ ] docs/ux-contract.md defines step sequences for install, sync, OCR, dashboard — each with single measurable outcome
- [ ] `test_new_user_onboarding` completes: formal notes created on sync, OCR mock succeeds, analyze flag set, deep-reading queue sees the paper
- [ ] `test_existing_user_adds_paper` completes: existing notes preserved, new paper appears after sync
- [ ] All journey tests use journey_vault fixtures with `assert "tmp"` isolation guards
</success_criteria>

<output>
After completion, create `.planning/phases/54-dashboard-workflow-closure/54-001-SUMMARY.md`
</output>
