---
phase: 056-stop-the-bleeding
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - scripts/check_version_sync.py
  - paperforge/worker/status.py
autonomous: true
requirements:
  - BLEED-01
  - BLEED-02

must_haves:
  truths:
    - "Running `python scripts/check_version_sync.py` exits 0 and reports all 6+ version declarations consistent with paperforge.__init__.__version__"
    - "Running `python scripts/check_version_sync.py` exits 1 with clear diagnostics when a version declaration is deliberately mismatched"
    - "`paperforge doctor` reports 'fail' (not 'warn') if installed PyYAML version is < 6.0, consistent with pyproject.toml hard dependency"
    - "`paperforge doctor` already reports 'fail' if yaml module is not importable at all (unchanged behavior)"
  artifacts:
    - path: "scripts/check_version_sync.py"
      provides: "CI gate that validates all version declarations are in sync"
      min_lines: 80
    - path: "paperforge/worker/status.py"
      provides: "PyYAML version check elevated from 'warn' to 'fail' for < 6.0"
      contains: "add_check.*fail.*PyYAML"
  key_links:
    - from: "scripts/check_version_sync.py"
      to: "paperforge/__init__.py"
      via: "import paperforge; paperforge.__version__"
      pattern: "__version__|import paperforge"
    - from: "scripts/check_version_sync.py"
      to: "paperforge/plugin/manifest.json"
      via: "json.load -> compare version field"
      pattern: "manifest\\.json"
    - from: "scripts/check_version_sync.py"
      to: "paperforge/plugin/versions.json"
      via: "json.load -> verify current version has entry, stale entries not modified"
      pattern: "versions\\.json"
    - from: "paperforge/worker/status.py"
      to: "pyproject.toml"
      via: "dependency declared as pyyaml>=6.0; doctor fail level matches"
      pattern: "pyyaml>=6\\.0"
---

<objective>
Internal consistency gates — version sync checker and PyYAML doctor hardening.

Purpose: Create the CI gate that proves all version declarations are in sync (BLEED-01), and eliminate PyYAML dependency ambiguity in doctor (BLEED-02). These are foundation checks that all subsequent v2.1 phases rely on to trust the declared version number.

Output:
- `scripts/check_version_sync.py` — executable CI script, exit 0 on sync, exit 1 on mismatch
- `paperforge/worker/status.py` — PyYAML < 6.0 check elevated from "warn" to "fail"
</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/056-stop-the-bleeding/056-CONTEXT.md

@paperforge/__init__.py
@paperforge/plugin/manifest.json
@paperforge/plugin/versions.json
@pyproject.toml
@paperforge/worker/status.py

<interfaces>
Source of truth version declarations:

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

From paperforge/plugin/manifest.json:
```json
{
  "version": "1.4.17rc3",
  "minAppVersion": "1.9.0"
}
```

From paperforge/plugin/versions.json:
```json
{
  "1.4.3": "1.9.0",
  "1.4.17rc3": "1.9.0"
}
```

From pyproject.toml (dynamic):
```toml
dynamic = ["version"]
[tool.setuptools.dynamic]
version = {attr = "paperforge.__version__"}
```

From paperforge/worker/status.py `_MODULE_MANIFEST`:
```python
{"import": "yaml", "pip": "pyyaml>=6.0", "label": "PyYAML"},
```

From paperforge/worker/status.py version check (lines ~448-458):
```python
# PyYAML specific: warn if version < 6.0
if mod_name == "yaml" and ver:
    ver_parts = str(ver).split(".")
    if int(ver_parts[0]) < 6:
        add_check("Python 环境", "warn", ...)
```
</interfaces>
</context>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Create version sync CI gate script</name>
  <files>
    scripts/check_version_sync.py
  </files>
  <behavior>
    - Test 1: Given all version declarations aligned at the same version, script exits 0 and prints "ALL VERSIONS SYNCED"
    - Test 2: Given a deliberate mismatch in manifest.json, script exits 1 and reports which file is out of sync
    - Test 3: Given a stale fallback '1.4.17rc2' string in main.js (when current is '1.4.17rc3'), script warns or reports it
    - Test 4: Given a stale version entry in versions.json that doesn't match current, script reports the inconsistency
    - Test 5: Given AGENTS.md docs version line references a different version than __version__, script reports mismatch
  </behavior>
  <action>
    Create `scripts/check_version_sync.py` — a Python CI gate that:

    1. Single source of truth: Read `__version__` from `paperforge/__init__.py` via `import paperforge` or regex fallback
    2. Check `paperforge/plugin/manifest.json` — the `"version"` field MUST match __version__
    3. Check `paperforge/plugin/versions.json` — a key matching __version__ MUST exist. Old/stale entries (e.g. "1.4.3") that do NOT match current version are flagged as "STALE ENTRY" (WARNING only — they're historical, per Obsidian plugin update mechanism requirements). Additionally verify old entries have NOT had their minAppVersion retroactively changed — this is a data integrity check.
    4. Check `pyproject.toml` — verify `version = {attr = "paperforge.__version__"}` pattern is present (dynamic sync, but verify the attr reference is correct)
    5. Check `paperforge/plugin/main.js` — search for hardcoded fallback version strings LIKE `'1.4.17rc2'` that don't match current __version__; flag as WARNING since these are version-drift hazards
    6. Check `AGENTS.md` — find the `Docs version` line (approx line 3) and verify the version there matches __version__
    7. Verify `build/lib/paperforge/plugin/manifest.json` version matches (build artifact)
    8. Print a per-file report, then "ALL VERSIONS SYNCED" and exit 0 if all pass, or "VERSION MISMATCH" with file list and exit 1 on any failure.

    Follow the `scripts/consistency_audit.py` pattern:
    - `#!/usr/bin/env python3` shebang
    - `from __future__ import annotations`
    - Use `pathlib.Path` for file operations
    - `REPO_ROOT = Path(__file__).parent.parent.resolve()`
    - Exit code 0 = all pass, 1 = any fail
    - Print clear, actionable messages for each failure

    Use `scripts/consistency_audit.py` as a design reference for structure and style — but the logic here is version-specific.

    Use `paperforge/consistency_audit.py` if it exists, otherwise create from scratch following the existing `scripts/` convention.
  </action>
  <verify>
    <automated>cd "$REPO_ROOT" && python -m pytest tests/ -k "test_version_sync or test_check_version" --no-header -q 2>&1 || python scripts/check_version_sync.py --help</automated>
  </verify>
  <done>
    - `scripts/check_version_sync.py` exists and is executable
    - Running it with all versions consistent exits 0
    - Temporarily editing manifest.json version to a different value causes exit 1 with clear diagnostic
    - The script covers 6+ version declaration locations
    - Consistent with existing `scripts/consistency_audit.py` patterns
  </done>
</task>

<task type="auto">
  <name>Task 2: Elevate PyYAML version check from warn to fail in doctor</name>
  <files>
    paperforge/worker/status.py
  </files>
  <action>
    In `paperforge/worker/status.py`, modify the PyYAML-specific version check (approximately lines 448-458):

    The current code has:
    ```python
    # PyYAML specific: warn if version < 6.0
    if mod_name == "yaml" and ver:
        try:
            ver_parts = str(ver).split(".")
            if int(ver_parts[0]) < 6:
                add_check("Python 环境", "warn",
                    f"{mod_info['label']} {ver} (需要 >=6.0)",
                    f"运行: {interp} -m pip install {mod_info['pip']}")
                continue
        except (ValueError, IndexError):
            pass
    ```

    Change `"warn"` to `"fail"` — since `pyyaml>=6.0` is declared as a hard dependency in `pyproject.toml` (in `[project]dependencies`, NOT in `[project.optional-dependencies]`), a version below 6.0 should be a hard failure, not a warning.

    The "fail" severity is already used on line 440 when the module is completely missing. This change makes the version check consistent: both missing PyYAML and PyYAML < 6.0 are hard failures.

    Rationale documented in comment: align with pyproject.toml hard dependency declaration — single source of truth, no "optional dependency" ambiguity (addresses BLEED-02).
  </action>
  <verify>
    <automated>python -c "import ast; tree = ast.parse(open('paperforge/worker/status.py', 'rb').read()); print('parsed OK')"</automated>
  </verify>
  <done>
    - `"warn"` changed to `"fail"` in the PyYAML version < 6.0 check
    - Comment added explaining why this is a hard-fail (per pyproject.toml)
    - Syntax-valid Python, linter-clean
  </done>
</task>

</tasks>

<verification>
1. Run `python scripts/check_version_sync.py` — expect ALL VERSIONS SYNCED exit 0
2. Run `python -c "import ast; ast.parse(open('paperforge/worker/status.py').read())"` — no syntax errors
3. Verify `"fail"` appears (not `"warn"`) in the PyYAML version check in status.py
4. Run `ruff check paperforge/worker/status.py` — no new lint errors
</verification>

<success_criteria>
- `scripts/check_version_sync.py` detects mismatches between __version__, manifest.json, versions.json, pyproject.toml, AGENTS.md docs version, and main.js hardcoded fallbacks
- `paperforge doctor` hard-fails (not warns) when PyYAML version is < 6.0
- No syntax or lint regressions in modified files
</success_criteria>

<output>
After completion, create `.planning/phases/056-stop-the-bleeding/056-01-SUMMARY.md`
</output>
