fix(sync): freeze workspace slug + frontmatter-only update to prevent deep-reading data loss

- Slug freeze: reuse existing workspace dir when title slug changes in Zotero
- Frontmatter-only: for existing notes, replace only YAML block, never touch body
- extract_preserved_deep_reading: match both ##精读 and ##🔍精读 + skip placeholder-only sections
This commit is contained in:
Research Assistant 2026-05-10 22:31:16 +08:00
parent f8c05959ed
commit a5dd71bb13
2 changed files with 46 additions and 7 deletions

View file

@ -226,19 +226,35 @@ def generate_review(candidates: list[dict]) -> str:
def extract_preserved_deep_reading(text: str) -> str:
if not text:
return ""
match = re.search("^## 🔍 精读\\s*$", text, re.MULTILINE)
# Match "## 🔍 精读" or "## 精读" (emoji is optional)
match = re.search(r"^##\s*🔍?\s*精读\s*$", text, re.MULTILINE)
if not match:
return ""
start = match.start()
preserved = text[start:].strip()
# Skip if section body is empty or only contains placeholder text
body = re.sub(r"^##\s*🔍?\s*精读\s*$", "", preserved, flags=re.MULTILINE).strip()
if not body or _is_placeholder_only(body):
return ""
return preserved
_PLACEHOLDER_PATTERN = re.compile(r"(待补充)|\(待补充\)|\(TODO\)|\(TBD\)")
def _is_placeholder_only(body: str) -> bool:
"""Check if the deep reading body is only placeholder text (no real content)."""
cleaned = _PLACEHOLDER_PATTERN.sub("", body).strip()
cleaned = re.sub(r"^[-*]\s*$", "", cleaned, flags=re.MULTILINE).strip()
return not cleaned
def has_deep_reading_content(text: str) -> bool:
preserved = extract_preserved_deep_reading(text)
if not preserved:
return False
body = preserved.replace(DEEP_READING_HEADER, "").strip()
# Strip both "## 🔍 精读" and "## 精读" header variants
body = re.sub(r"^##\s*🔍?\s*精读\s*$", "", preserved, flags=re.MULTILINE).strip()
if not body:
return False

View file

@ -262,13 +262,22 @@ def _build_entry(item: dict, vault: Path, paths: dict, domain: str, zotero_dir:
write_json(meta_path, meta)
title_slug = slugify_filename(item["title"])
note_path = paths["literature"] / domain / f"{key} - {title_slug}.md"
# --- Freeze slug: reuse existing workspace if it exists under a different slug ---
workspace_dir = paths["literature"] / domain / f"{key} - {title_slug}"
if not workspace_dir.exists():
for candidate in (paths["literature"] / domain).glob(f"{key} - *"):
if candidate.is_dir():
workspace_dir = candidate
title_slug = workspace_dir.name.split(" - ", 1)[1] if " - " in workspace_dir.name else title_slug
break
if note_path.parent.exists():
for stale_note in note_path.parent.glob(f"{key} - *.md"):
if stale_note != note_path:
stale_note.unlink()
# Workspace paths (Phase 26: flat-to-workspace migration)
workspace_dir = paths["literature"] / domain / f"{key} - {title_slug}"
main_note_path = workspace_dir / f"{key} - {title_slug}.md"
deep_reading_file = workspace_dir / "deep-reading.md"
target_fulltext = workspace_dir / "fulltext.md"
@ -363,10 +372,24 @@ def _build_entry(item: dict, vault: Path, paths: dict, domain: str, zotero_dir:
entry["maturity"] = compute_maturity(entry)
entry["next_step"] = compute_next_step(entry)
# --- Workspace: always create and write to workspace path (Phase 38: no flat fallback) ---
existing_text = main_note_path.read_text(encoding="utf-8") if main_note_path.exists() else ""
main_note_path.write_text(frontmatter_note(entry, existing_text), encoding="utf-8")
# Slug already frozen above — for existing notes, update frontmatter only (preserve body)
if main_note_path.exists():
text = main_note_path.read_text(encoding="utf-8")
fm_close = text.find("---\n", 4) # closing --- after opening ---
if fm_close != -1:
body = text[fm_close + 4:] # everything after frontmatter
new_full = frontmatter_note(entry, "")
new_fm_close = new_full.find("---\n", 4)
if new_fm_close != -1:
new_fm = new_full[: new_fm_close + 4] # new frontmatter block with closing ---\n
main_note_path.write_text(new_fm + body, encoding="utf-8")
else:
main_note_path.write_text(new_full, encoding="utf-8")
else:
main_note_path.write_text(frontmatter_note(entry, text), encoding="utf-8")
else:
existing_text = note_path.read_text(encoding="utf-8") if note_path.exists() else ""
main_note_path.write_text(frontmatter_note(entry, existing_text), encoding="utf-8")
# Write per-workspace paper-meta.json (Phase 37: internal state outside frontmatter)
write_paper_meta(workspace_dir, entry, paperforge_version=PAPERFORGE_VERSION)