From 4f92d3782b3986ae066ff49ed946850942f77be3 Mon Sep 17 00:00:00 2001 From: sam Date: Sat, 26 Sep 2026 08:19:00 +1000 Subject: [PATCH] fix(watch): drop proposed_folder_auto; a recorded folder is never re-decided The extra bookkeeping field showed up in Obsidian's Properties panel as a second 'proposed folder' row, which was confusing. Simpler rule replaces it: once proposed_folder is set it is never changed. The owner edits it if they disagree; deleting the field asks for a fresh suggestion. --- sorter/watch.py | 37 +++++++++++++++++-------------------- tests/run_tests.py | 20 ++++++++++---------- 2 files changed, 27 insertions(+), 30 deletions(-) diff --git a/sorter/watch.py b/sorter/watch.py index 2e9be5a..d5cc394 100644 --- a/sorter/watch.py +++ b/sorter/watch.py @@ -34,12 +34,12 @@ 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_folder_auto", "proposed_toc", - "proposed_tags", "confidence", "approved", "proposed_for") +ADMIN_FIELDS = ("proposed_folder", "proposed_toc", "proposed_tags", + "confidence", "approved", "proposed_for") PROPOSAL_ORDER = ( "title", "type", "status", - "proposed_folder", "proposed_folder_auto", "proposed_toc", "proposed_tags", + "proposed_folder", "proposed_toc", "proposed_tags", "confidence", "approved", "proposed_for", "created", "updated", "tags", "aliases", "id", ) @@ -79,19 +79,16 @@ def proposal_is_stale(note: Note) -> bool: return recorded != content_hash(note) -def user_selected_folder(note: Note) -> str | None: +def existing_folder(note: Note) -> str | None: """ - The folder the owner chose by hand, if they changed it. + The folder already recorded on a note, if any — chosen by the owner or by + an earlier proposal. - 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. + Once set, this is never changed by the watcher. The owner can edit it, or + delete the field to ask for a fresh suggestion. """ 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 + return chosen or None class Watcher: @@ -133,12 +130,13 @@ class Watcher: # ----------------------------------------------------------------- proposing def _write_proposal(self, note: Note, proposal: Proposal, - preserve_folder: str | None = None) -> list[str]: + keep_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 + # A folder that is already recorded is never re-decided. The owner's + # choice wins, and so does an earlier proposal. + effective = keep_folder or proposal.target_folder # Resolve the real index + section so the proposal reads clearly. toc_rel, toc_section, _ = self.writer.resolve_toc( @@ -152,7 +150,6 @@ class Watcher: fm["proposed_toc"] = "(not indexed)" 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) @@ -164,8 +161,8 @@ class Watcher: self.writer._atomic_write(Path(note.path), text) note_msg = f"**proposed** `{note.filename}` → `{effective}` " - if preserve_folder: - note_msg += "(your folder kept) " + if keep_folder: + note_msg += "(folder already set — kept) " note_msg += f"(confidence {proposal.confidence:.2f})" return [note_msg] + [f" - {w}" for w in proposal.warnings] @@ -263,12 +260,12 @@ class Watcher: f"**held** `{note.filename}` — " + "; ".join(proposal.warnings) ) - elif not proposal.target_folder and not user_selected_folder(note): + elif not proposal.target_folder and not existing_folder(note): actions.append(f"**held** `{note.filename}` — no destination determined") else: actions += self._write_proposal( note, proposal, - preserve_folder=user_selected_folder(note), + keep_folder=existing_folder(note), ) except Exception as e: # never let one note kill the loop actions.append(f"**error** `{note.filename}`: {type(e).__name__}: {e}") diff --git a/tests/run_tests.py b/tests/run_tests.py index 26b478c..c266b67 100644 --- a/tests/run_tests.py +++ b/tests/run_tests.py @@ -326,27 +326,27 @@ def main() -> int: except OSError: pass - # ---- watcher: the owner's hand-picked folder must survive ---------- - from sorter.watch import user_selected_folder + # ---- watcher: a recorded folder is never re-decided ---------------- + from sorter.watch import existing_folder from sorter.models import Note as _Note - def _n(chosen, auto): + def _n(chosen): return _Note(path="x", rel_path="010 inbox/x.md", filename="x.md", stem="x", title="x", note_type="note", status=None, - fm={"proposed_folder": chosen, "proposed_folder_auto": auto}) + fm={"proposed_folder": chosen}) - if user_selected_folder(_n("300 areas/A", "200 projects/B")) == "300 areas/A": - print("PASS guard: a hand-edited folder is treated as the owner's choice") + if existing_folder(_n("300 areas/A")) == "300 areas/A": + print("PASS guard: a recorded folder is treated as already decided") passed += 1 else: - details.append("FAIL guard: hand-edited folder not detected") + details.append("FAIL guard: recorded folder not detected") failed += 1 - if user_selected_folder(_n("200 projects/B", "200 projects/B")) is None: - print("PASS guard: an untouched proposal is not treated as customised") + if existing_folder(_n("")) is None: + print("PASS guard: no recorded folder means the watcher may suggest one") passed += 1 else: - details.append("FAIL guard: untouched proposal wrongly seen as customised") + details.append("FAIL guard: empty folder wrongly treated as set") failed += 1 # ---- inbox cleared --------------------------------------------------