feat: slim base views to 4, add ocr redo view

This commit is contained in:
Research Assistant 2026-06-01 11:09:38 +08:00
parent c1c5b9b8ba
commit d10c29bd96
3 changed files with 38 additions and 130 deletions

View file

@ -39,7 +39,7 @@ PAPERFORGE_VIEW_PREFIX = "# PAPERFORGE_VIEW: "
def build_base_views(domain: str) -> list[dict]:
"""Build the 8-view list for a domain Base file.
"""Build the 4-view list for a domain Base file.
Uses workflow gate columns (has_pdf, do_ocr, analyze, ocr_status)
matching the master version's Base views. See REQUIREMENTS.md
@ -49,7 +49,7 @@ def build_base_views(domain: str) -> list[dict]:
domain: The domain name (e.g., "骨科").
Returns:
List of 8 view dicts, each with keys: name, order, filter, sort.
List of 4 view dicts, each with keys: name, order, filter, sort.
"""
return [
{
@ -71,34 +71,11 @@ def build_base_views(domain: str) -> list[dict]:
],
"filter": None,
},
{
"name": "推荐分析",
"order": [
"year",
"title",
"first_author",
"journal",
"impact_factor",
"has_pdf",
"do_ocr",
"analyze",
"ocr_status",
"deep_reading_status",
"pdf_path",
"fulltext_md_path",
],
"filter": "analyze = true AND has_pdf = true",
},
{
"name": "待 OCR",
"order": ["year", "first_author", "title", "has_pdf", "do_ocr", "ocr_status", "pdf_path"],
"filter": "do_ocr = true AND ocr_status = 'pending'",
},
{
"name": "OCR 完成",
"order": ["year", "first_author", "title", "has_pdf", "do_ocr", "ocr_status", "pdf_path"],
"filter": "ocr_status = 'done'",
},
{
"name": "待深度阅读",
"order": [
@ -115,51 +92,9 @@ def build_base_views(domain: str) -> list[dict]:
"filter": "analyze = true AND ocr_status = 'done' AND deep_reading_status = 'pending'",
},
{
"name": "深度阅读完成",
"order": [
"year",
"first_author",
"title",
"has_pdf",
"do_ocr",
"analyze",
"ocr_status",
"deep_reading_status",
"pdf_path",
],
"filter": "deep_reading_status = 'done'",
},
{
"name": "正式卡片",
"order": [
"title",
"year",
"first_author",
"journal",
"impact_factor",
"has_pdf",
"deep_reading_status",
"pdf_path",
],
"filter": "deep_reading_status = 'done'",
},
{
"name": "全记录",
"order": [
"title",
"year",
"first_author",
"journal",
"impact_factor",
"has_pdf",
"do_ocr",
"analyze",
"ocr_status",
"deep_reading_status",
"pdf_path",
"fulltext_md_path",
],
"filter": None,
"name": "重做OCR",
"order": ["year", "first_author", "title", "ocr_redo", "ocr_status"],
"filter": "ocr_status = 'done'",
},
]
@ -267,7 +202,7 @@ def merge_base_views(existing_content: str | None, new_views: list[dict], folder
"""Incrementally merge standard PaperForge views into an existing .base file.
Strategy:
- PaperForge generates exactly 8 views with known names (STANDARD_VIEW_NAMES).
- PaperForge generates exactly 4 views with known names.
- Any OTHER views in the existing file are user-defined and MUST be preserved.
- Each PaperForge view is preceded by a PAPERFORGE_VIEW_PREFIX comment marker.
- On refresh: replace ALL PaperForge views (identified by prefix) with fresh ones.
@ -276,7 +211,7 @@ def merge_base_views(existing_content: str | None, new_views: list[dict], folder
Args:
existing_content: Raw text of existing .base file (or None/empty for fresh generation).
new_views: List of 8 view dicts from build_base_views().
new_views: List of 4 view dicts from build_base_views().
folder_filter: Vault-relative folder path for file.inFolder() (used for fresh generation).
Returns:

View file

@ -43,7 +43,7 @@ class TestIncrementalMerge:
ensure_base_views(self.vault, self.paths, self.config, force=False)
content = domain_base.read_text(encoding="utf-8")
assert content.count("type: table") == 8
assert content.count("type: table") == 4
user_custom = (
content
@ -61,9 +61,9 @@ class TestIncrementalMerge:
refreshed = domain_base.read_text(encoding="utf-8")
assert "My Custom Dashboard" in refreshed, "User custom view was lost on incremental refresh"
assert refreshed.count("type: table") == 9
assert refreshed.count("type: table") == 5
assert "控制面板" in refreshed
assert "OCR 完成" in refreshed
assert "重做OCR" in refreshed
def test_standard_paperforge_views_are_updated_on_refresh(self):
"""Standard views are replaced with fresh content on each refresh."""
@ -71,12 +71,12 @@ class TestIncrementalMerge:
ensure_base_views(self.vault, self.paths, self.config, force=False)
content1 = domain_base.read_text(encoding="utf-8")
assert content1.count(PAPERFORGE_VIEW_PREFIX) == 8
assert content1.count(PAPERFORGE_VIEW_PREFIX) == 4
ensure_base_views(self.vault, self.paths, self.config, force=False)
content2 = domain_base.read_text(encoding="utf-8")
assert content2.count("type: table") == 8
assert content2.count("type: table") == 4
assert "${LIBRARY_RECORDS}" not in content2
def test_force_flag_does_full_regeneration(self):
@ -101,8 +101,8 @@ class TestIncrementalMerge:
refreshed = domain_base.read_text(encoding="utf-8")
assert "My Custom View" not in refreshed, "force=True should have replaced all views"
assert refreshed.count("type: table") == 8
assert refreshed.count(PAPERFORGE_VIEW_PREFIX) == 8
assert refreshed.count("type: table") == 4
assert refreshed.count(PAPERFORGE_VIEW_PREFIX) == 4
def test_user_modified_filter_on_standard_view_is_preserved(self):
"""User changes filter on a standard PF view — on refresh it is PRESERVED.
@ -140,7 +140,7 @@ class TestIncrementalMerge:
assert domain_base.exists()
content = domain_base.read_text(encoding="utf-8")
assert content.count("type: table") == 8
assert content.count("type: table") == 4
assert PAPERFORGE_VIEW_PREFIX in content
@ -174,7 +174,7 @@ class TestLiteratureHubBase:
hub_base = bases / "Literature Hub.base"
assert hub_base.exists(), "Literature Hub.base not created"
content = hub_base.read_text(encoding="utf-8")
assert content.count("type: table") == 8, f"Expected 8 views, got {content.count('type: table')}"
assert content.count("type: table") == 4, f"Expected 4 views, got {content.count('type: table')}"
assert PAPERFORGE_VIEW_PREFIX in content
assert "${LIBRARY_RECORDS}" not in content, "Placeholder should be substituted"
@ -237,8 +237,7 @@ views:
result = merge_base_views(existing, views, folder_filter="Resources/Literature/骨科")
assert "My Custom View" in result, "User view was lost"
assert "推荐分析" in result
assert result.count("type: table") >= 8
assert result.count("type: table") >= 6
def test_merge_base_views_first_run_generates_fresh(self):
"""merge_base_views with no existing content generates fresh YAML."""
@ -285,9 +284,9 @@ views:
- file.name
- title
- year
# PAPERFORGE_VIEW: OCR 完成
# PAPERFORGE_VIEW: 重做OCR
- type: table
name: "OCR 完成"
name: "重做OCR"
widths:
year: 55
title: 380

View file

@ -1,4 +1,4 @@
"""Tests for the 8-view Base generation system (Phase 39: workflow-gate views)."""
"""Tests for the 4-view Base generation system (Phase 44: slim base views)."""
from paperforge.worker.base_views import (
build_base_views,
@ -7,15 +7,22 @@ from paperforge.worker.base_views import (
class TestBuildBaseViews:
def test_returns_exactly_8_views(self):
def test_build_base_views_has_4_standard_views(self):
views = build_base_views("骨科")
assert len(views) == 8
names = {v["name"] for v in views}
def test_all_view_names_present(self):
assert names == {"控制面板", "待 OCR", "待深度阅读", "重做OCR"}
def test_ocr_redo_view_has_correct_columns(self):
views = build_base_views("骨科")
names = [v["name"] for v in views]
expected = ["控制面板", "推荐分析", "待 OCR", "OCR 完成", "待深度阅读", "深度阅读完成", "正式卡片", "全记录"]
assert names == expected
redo = next(v for v in views if v["name"] == "重做OCR")
assert redo["order"] == ["year", "first_author", "title", "ocr_redo", "ocr_status"]
assert redo["filter"] == "ocr_status = 'done'"
def test_returns_exactly_4_views(self):
views = build_base_views("骨科")
assert len(views) == 4
def test_each_view_has_required_keys(self):
views = build_base_views("骨科")
@ -37,11 +44,6 @@ class TestBuildBaseViews:
assert "do_ocr = true" in pending["filter"]
assert "ocr_status = 'pending'" in pending["filter"]
def test_ocr_done_filter(self):
views = build_base_views("骨科")
done = next(v for v in views if v["name"] == "OCR 完成")
assert "ocr_status = 'done'" in done["filter"]
def test_deep_reading_pending_filter(self):
views = build_base_views("骨科")
pending = next(v for v in views if v["name"] == "待深度阅读")
@ -49,44 +51,16 @@ class TestBuildBaseViews:
assert "ocr_status = 'done'" in pending["filter"]
assert "deep_reading_status = 'pending'" in pending["filter"]
def test_build_base_views_includes_workflow_flags(self):
def test_removed_views_not_present(self):
removed = {"推荐分析", "OCR 完成", "深度阅读完成", "正式卡片", "全记录"}
views = build_base_views("骨科")
for v in views:
order_set = set(v["order"])
assert "has_pdf" in order_set, f"View '{v['name']}' missing has_pdf"
if v["name"] not in ("正式卡片",):
assert "do_ocr" in order_set, f"View '{v['name']}' missing do_ocr"
if v["name"] not in ("正式卡片", "待 OCR", "OCR 完成"):
assert "analyze" in order_set, f"View '{v['name']}' missing analyze"
def test_build_base_views_removes_ghost_columns(self):
views = build_base_views("骨科")
ghost_fields = {"lifecycle", "maturity_level", "next_step"}
for v in views:
order_set = set(v["order"])
assert ghost_fields.isdisjoint(order_set), f"View '{v['name']}' still contains ghost lifecycle columns"
def test_build_base_views_filters_use_workflow_gates(self):
views = build_base_views("骨科")
for v in views:
if v["filter"] is not None:
assert "lifecycle" not in v["filter"], f"View '{v['name']}' filter uses lifecycle instead of workflow gates"
names = {v["name"] for v in views}
assert removed.isdisjoint(names), f"Removed views still present: {removed & names}"
def test_build_base_views_has_no_sort(self):
views = build_base_views("骨科")
for v in views:
assert "sort" not in v, f"View '{v['name']}' should not have sort key (lifecycle removed from frontmatter)"
def test_properties_yaml_updated(self):
from paperforge.worker.base_views import merge_base_views
fresh = merge_base_views(None, build_base_views("骨科"), folder_filter="Resources/Literature/骨科")
assert "lifecycle:" not in fresh
assert "maturity_level:" not in fresh
assert "next_step:" not in fresh
assert "has_pdf:" in fresh
assert "do_ocr:" in fresh
assert "analyze:" in fresh
assert "ocr_status:" in fresh
assert "sort" not in v, f"View '{v['name']}' should not have sort key"
class TestSubstituteConfigPlaceholders: