---
phase: 57-contract-layer
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - paperforge/core/__init__.py
  - paperforge/core/errors.py
  - paperforge/core/result.py
  - tests/cli/test_contract_roundtrip.py
autonomous: true
requirements:
  - CTRT-01
  - CTRT-02

must_haves:
  truths:
    - "ErrorCode enum exists with all 8 required members (PYTHON_NOT_FOUND, VERSION_MISMATCH, BBT_EXPORT_NOT_FOUND, OCR_TOKEN_MISSING, SYNC_FAILED, VALIDATION_ERROR, INTERNAL_ERROR, UNKNOWN)"
    - "PFResult dataclass serializes to JSON with {ok, command, version, data, error} shape"
    - "PFError dataclass serializes to JSON with {code, message, details} shape"
    - "JSON round-trip: serialize PFResult -> JSON string -> deserialize -> equal to original"
  artifacts:
    - path: "paperforge/core/__init__.py"
      provides: "Core package init"
      min_lines: 1
    - path: "paperforge/core/errors.py"
      provides: "ErrorCode enum with all error codes"
      exports: ["ErrorCode"]
    - path: "paperforge/core/result.py"
      provides: "PFResult and PFError dataclasses with to_dict/to_json/from_json"
      exports: ["PFResult", "PFError"]
    - path: "tests/cli/test_contract_roundtrip.py"
      provides: "Round-trip unit tests for PFResult/PFError serialization"
      min_lines: 40
  key_links:
    - from: "paperforge/core/result.py"
      to: "paperforge/core/errors.py"
      via: "import ErrorCode"
      pattern: "from paperforge.core.errors import ErrorCode"
    - from: "tests/cli/test_contract_roundtrip.py"
      to: "paperforge/core/result.py"
      via: "import PFResult"
      pattern: "from paperforge.core.result import PFResult"
---

<objective>
Create the core contract types (PFResult, PFError dataclasses and ErrorCode enum) that all subsequent CLI --json wrapping and the dashboard command depend on. This is the foundational layer for the contract-driven architecture.

Purpose: Define the machine-readable API surface that every CLI command will produce and the plugin will consume.
Output: paperforge/core/errors.py (ErrorCode enum), paperforge/core/result.py (PFResult/PFError), round-trip unit tests
</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
@.planning/STATE.md
@.planning/phases/057-contract-layer/057-CONTEXT.md

<interfaces>
From paperforge/__init__.py:
```python
__version__ = "1.4.17rc3"
```

From existing tests/cli/test_contract_helpers.py (assert_json_shape, normalize_snapshot, assert_valid_json available for reuse):
```python
def assert_json_shape(data: dict, required_keys: set[str], optional_keys: set[str] | None = None)
def assert_valid_json(output: str) -> dict
def normalize_snapshot(output: str, vault_path: str | None = None) -> str
```
</interfaces>
</context>

<tasks>

<task type="auto">
  <name>Task 1: Create ErrorCode enum in paperforge/core/errors.py</name>
  <files>
    paperforge/core/__init__.py
    paperforge/core/errors.py
  </files>
  <action>
    Create the paperforge/core/ directory with paperforge/core/__init__.py containing a minimal package docstring.

    Create paperforge/core/errors.py with:

    ErrorCode(str, Enum) -- string enum using SCREAMING_SNAKE_CASE naming with values equal to names. The 8 members per user decision (D-01, D-02, D-03):

    - PYTHON_NOT_FOUND: Python interpreter not found by the plugin
    - VERSION_MISMATCH: Version mismatch between plugin manifest and installed package
    - BBT_EXPORT_NOT_FOUND: Better BibTeX JSON export file missing or invalid
    - OCR_TOKEN_MISSING: PaddleOCR API token not configured in .env
    - SYNC_FAILED: Sync operation failed (selection-sync or index-refresh)
    - VALIDATION_ERROR: Validation error -- use details.field for field-level granularity
    - INTERNAL_ERROR: Unexpected internal error -- use details for trace/context info
    - UNKNOWN: Catch-all fallback for unmapped/unexpected errors

    Each member MUST have a docstring describing when it is used.

    Add helper: @classmethod def from_exception(cls, exc: Exception) -> ErrorCode that maps common exception types (FileNotFoundError -> BBT_EXPORT_NOT_FOUND, ValueError -> VALIDATION_ERROR, etc.) to appropriate codes. Unknown exceptions map to UNKNOWN.

    Per user decision (CONTEXT.md): ErrorCode enum ONLY -- no JSON schema generation, no auto-generation from field_registry.yaml (deferred to Phase 59).
  </action>
  <verify>
    <automated>python -c "from paperforge.core.errors import ErrorCode; e = ErrorCode.UNKNOWN; assert isinstance(e, ErrorCode); assert e.value == 'UNKNOWN'; print('OK enum:', e.value)"</automated>
  </verify>
  <done>ErrorCode enum exists in paperforge/core/errors.py with 8 members, all SCREAMING_SNAKE_CASE, each with docstring, plus from_exception helper.</done>
</task>

<task type="auto">
  <name>Task 2: Create PFResult and PFError dataclasses in paperforge/core/result.py</name>
  <files>
    paperforge/core/result.py
  </files>
  <action>
    Create paperforge/core/result.py with two dataclasses using `from __future__ import annotations`.

    PFError dataclass:
    - code: str -- Error code string (e.g., "SYNC_FAILED", "VALIDATION_ERROR")
    - message: str -- Human-readable error description
    - details: dict = field(default_factory=dict) -- Optional structured payload (e.g., {"field": "pdf_path", "reason": "not found"})
    - to_dict() -> dict: Returns {"code": code, "message": message, "details": details}
    - WARNING: details may contain non-JSON-serializable types -- coerce with str() on serialization.

    PFResult dataclass:
    - ok: bool -- True when operation succeeded (even with partial results + warnings in data); False only when error is set
    - command: str -- CLI command name (e.g., "status", "sync")
    - version: str -- Version string from __version__. Use lazy import inside methods (not at module level) to avoid circular imports.
    - data: Any = None -- Payload (dict for commands with structured output, list for collections, None for errors)
    - error: PFError | None = None -- Set only when ok is False

    __post_init__ validation: if ok is True and error is not None -> raise ValueError("ok=True requires error=None"). if ok is False and error is None -> raise ValueError("ok=False requires an error").

    Methods:
    - to_dict() -> dict: Returns {"ok": ok, "command": command, "version": version, "data": data, "error": error.to_dict() if error else None}
    - to_json(indent: int = 2) -> str: JSON-dumps to_dict() with ensure_ascii=False, indent=indent, default=str for non-serializable values
    - @staticmethod from_json(json_str: str) -> PFResult: Parse JSON string back to PFResult. If the "error" field is not None, construct PFError from the dict.

    Module-level __all__ = ["PFResult", "PFError"].

    Per user decisions:
    - Top-level fields MUST be {ok, command, version, data, error} following Google JSON Style Guide
    - Version: semver string from __version__ (e.g., "1.4.17rc3")
    - ok=false only when error is set; ok=true even with partial results (add warnings sub-field in data)
    - Error shape: {"code":"ERR_CODE","message":"Human text","details":{}}
  </action>
  <verify>
    <automated>python -c "
from paperforge.core.result import PFResult, PFError
# Test OK result
r = PFResult(ok=True, command='status', version='1.4.17rc3', data={'papers': 5})
d = r.to_dict()
assert d['ok'] == True
assert d['command'] == 'status'
assert d['error'] is None
# Test error result
er = PFResult(ok=False, command='sync', version='1.4.17rc3', error=PFError(code='SYNC_FAILED', message='Sync failed', details={'key': 'val'}))
d2 = er.to_dict()
assert d2['ok'] == False
assert d2['error']['code'] == 'SYNC_FAILED'
# Test round-trip
j = r.to_json()
r2 = PFResult.from_json(j)
assert r2.ok == r.ok
assert r2.command == r.command
# Test validation
try:
    PFResult(ok=True, command='test', version='1.0', error=PFError(code='X', message='y'))
    assert False, 'Should have raised'
except ValueError:
    pass
print('OK: round-trip + validation valid')
"</automated>
  </verify>
  <done>PFResult and PFError dataclasses in paperforge/core/result.py with to_dict(), to_json(), from_json(), and __post_init__ validation. Round-trip (serialize->deserialize->equal) verified.</done>
</task>

<task type="auto">
  <name>Task 3: Write round-trip unit tests in tests/cli/test_contract_roundtrip.py</name>
  <files>
    tests/cli/test_contract_roundtrip.py
  </files>
  <action>
    Create tests/cli/test_contract_roundtrip.py with pytest tests covering:

    1. TestPFResultShape: Verify PFResult.to_dict() has exactly {ok, command, version, data, error} keys. Test both success and error states.

    2. TestPFErrorShape: Verify PFError.to_dict() has exactly {code, message, details} keys. Test with empty dict, with populated details.

    3. TestPFResultRoundTrip: Full round-trip tests:
       - Serialize PFResult (ok=True, with data dict) -> JSON string -> deserialize -> assert equal
       - Serialize PFResult (ok=False, with PFError) -> JSON string -> deserialize -> assert equal
       - Serialize PFResult with list data -> JSON -> deserialize -> assert equal

    4. TestPFResultValidation: Verify __post_init__ raises ValueError for:
       - ok=True with error set
       - ok=False without error

    Use existing fixtures/conftest patterns (from tests/cli/conftest.py). No vault needed for these pure unit tests -- they test the dataclasses directly.

    Keep tests compact: 4 test classes, no more than 5 assertions each.
  </action>
  <verify>
    <automated>python -m pytest tests/cli/test_contract_roundtrip.py -v --tb=short</automated>
  </verify>
  <done>All round-trip tests pass: shape verification, serialization round-trip, validation error cases.</done>
</task>

</tasks>

<verification>
- python -c "from paperforge.core.errors import ErrorCode; print(list(ErrorCode))" shows all 8 members
- python -c "from paperforge.core.result import PFResult, PFError; print(PFResult(ok=True, command='status', version='1.0', data={}).to_json())" produces valid JSON
- pytest tests/cli/test_contract_roundtrip.py -v passes all tests
</verification>

<success_criteria>
- [ ] paperforge/core/errors.py exists with ErrorCode enum (8 members, SCREAMING_SNAKE_CASE, docstrings, from_exception)
- [ ] paperforge/core/result.py exists with PFResult and PFError dataclasses (to_dict, to_json, from_json, __post_init__ validation)
- [ ] Round-trip unit tests pass without requiring a vault
- [ ] All downstream phases (57-02, 57-03, 57-04) can import from paperforge.core
</success_criteria>

<output>
After completion, create .planning/phases/057-contract-layer/057-01-SUMMARY.md
</output>
