fix(watch): the owner's folder choice must never be overwritten
Two bugs found while filing the Obsidian Sorter note itself: 1. proposed_toc went stale when proposed_folder was edited by hand. The watcher now re-resolves the index line from the folder on every pass. 2. Re-proposing after a body edit clobbered a hand-edited proposed_folder. The watcher's own suggestion is now kept separately in proposed_folder_auto, so a difference marks a human choice, which wins. Adds two guard tests. 17/17 passing.
This commit is contained in:
@@ -34,13 +34,13 @@ from .vault import Vault
|
||||
from .writer import FIELD_ORDER, Writer
|
||||
|
||||
# Fields the watcher owns. Never present on a filed note.
|
||||
ADMIN_FIELDS = ("proposed_folder", "proposed_toc", "proposed_tags",
|
||||
"confidence", "approved", "proposed_for")
|
||||
ADMIN_FIELDS = ("proposed_folder", "proposed_folder_auto", "proposed_toc",
|
||||
"proposed_tags", "confidence", "approved", "proposed_for")
|
||||
|
||||
PROPOSAL_ORDER = (
|
||||
"title", "type", "status",
|
||||
"proposed_folder", "proposed_toc", "proposed_tags", "confidence",
|
||||
"approved", "proposed_for",
|
||||
"proposed_folder", "proposed_folder_auto", "proposed_toc", "proposed_tags",
|
||||
"confidence", "approved", "proposed_for",
|
||||
"created", "updated", "tags", "aliases", "id",
|
||||
)
|
||||
|
||||
@@ -79,6 +79,21 @@ def proposal_is_stale(note: Note) -> bool:
|
||||
return recorded != content_hash(note)
|
||||
|
||||
|
||||
def user_selected_folder(note: Note) -> str | None:
|
||||
"""
|
||||
The folder the owner chose by hand, if they changed it.
|
||||
|
||||
We keep the watcher's own suggestion in `proposed_folder_auto`, so a
|
||||
difference between the two means a human edited it. Their choice wins —
|
||||
the watcher must never overwrite it.
|
||||
"""
|
||||
chosen = str(note.fm.get("proposed_folder") or "").strip()
|
||||
auto = str(note.fm.get("proposed_folder_auto") or "").strip()
|
||||
if chosen and auto and chosen != auto:
|
||||
return chosen
|
||||
return None
|
||||
|
||||
|
||||
class Watcher:
|
||||
"""Polls the inbox and maintains proposals."""
|
||||
|
||||
@@ -117,13 +132,17 @@ class Watcher:
|
||||
|
||||
# ----------------------------------------------------------------- proposing
|
||||
|
||||
def _write_proposal(self, note: Note, proposal: Proposal) -> list[str]:
|
||||
def _write_proposal(self, note: Note, proposal: Proposal,
|
||||
preserve_folder: str | None = None) -> list[str]:
|
||||
"""Add the proposal fields to the note in place. Returns log lines."""
|
||||
fm = dict(note.fm)
|
||||
|
||||
# The owner's hand-picked folder always wins over a fresh suggestion.
|
||||
effective = preserve_folder or proposal.target_folder
|
||||
|
||||
# Resolve the real index + section so the proposal reads clearly.
|
||||
toc_rel, toc_section, _ = self.writer.resolve_toc(
|
||||
proposal.note_type, proposal.target_folder
|
||||
type_from_folder(effective) or proposal.note_type, effective
|
||||
)
|
||||
if toc_rel and toc_section:
|
||||
fm["proposed_toc"] = f"{toc_rel} → {toc_section}"
|
||||
@@ -132,7 +151,8 @@ class Watcher:
|
||||
else:
|
||||
fm["proposed_toc"] = "(not indexed)"
|
||||
|
||||
fm["proposed_folder"] = proposal.target_folder
|
||||
fm["proposed_folder"] = effective
|
||||
fm["proposed_folder_auto"] = proposal.target_folder
|
||||
if proposal.proposed_tags:
|
||||
fm["proposed_tags"] = proposal.proposed_tags
|
||||
fm["confidence"] = round(proposal.confidence, 2)
|
||||
@@ -143,10 +163,11 @@ class Watcher:
|
||||
if not self.dry_run:
|
||||
self.writer._atomic_write(Path(note.path), text)
|
||||
|
||||
return [
|
||||
f"**proposed** `{note.filename}` → `{proposal.target_folder}` "
|
||||
f"(confidence {proposal.confidence:.2f})",
|
||||
] + [f" - {w}" for w in proposal.warnings]
|
||||
note_msg = f"**proposed** `{note.filename}` → `{effective}` "
|
||||
if preserve_folder:
|
||||
note_msg += "(your folder kept) "
|
||||
note_msg += f"(confidence {proposal.confidence:.2f})"
|
||||
return [note_msg] + [f" - {w}" for w in proposal.warnings]
|
||||
|
||||
def _expected_toc(self, folder: str) -> str:
|
||||
"""The index + section a folder should be listed under."""
|
||||
@@ -242,10 +263,13 @@ class Watcher:
|
||||
f"**held** `{note.filename}` — "
|
||||
+ "; ".join(proposal.warnings)
|
||||
)
|
||||
elif not proposal.target_folder:
|
||||
elif not proposal.target_folder and not user_selected_folder(note):
|
||||
actions.append(f"**held** `{note.filename}` — no destination determined")
|
||||
else:
|
||||
actions += self._write_proposal(note, proposal)
|
||||
actions += self._write_proposal(
|
||||
note, proposal,
|
||||
preserve_folder=user_selected_folder(note),
|
||||
)
|
||||
except Exception as e: # never let one note kill the loop
|
||||
actions.append(f"**error** `{note.filename}`: {type(e).__name__}: {e}")
|
||||
return actions
|
||||
|
||||
Reference in New Issue
Block a user