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.
This commit is contained in:
@@ -34,12 +34,12 @@ from .vault import Vault
|
|||||||
from .writer import FIELD_ORDER, Writer
|
from .writer import FIELD_ORDER, Writer
|
||||||
|
|
||||||
# Fields the watcher owns. Never present on a filed note.
|
# Fields the watcher owns. Never present on a filed note.
|
||||||
ADMIN_FIELDS = ("proposed_folder", "proposed_folder_auto", "proposed_toc",
|
ADMIN_FIELDS = ("proposed_folder", "proposed_toc", "proposed_tags",
|
||||||
"proposed_tags", "confidence", "approved", "proposed_for")
|
"confidence", "approved", "proposed_for")
|
||||||
|
|
||||||
PROPOSAL_ORDER = (
|
PROPOSAL_ORDER = (
|
||||||
"title", "type", "status",
|
"title", "type", "status",
|
||||||
"proposed_folder", "proposed_folder_auto", "proposed_toc", "proposed_tags",
|
"proposed_folder", "proposed_toc", "proposed_tags",
|
||||||
"confidence", "approved", "proposed_for",
|
"confidence", "approved", "proposed_for",
|
||||||
"created", "updated", "tags", "aliases", "id",
|
"created", "updated", "tags", "aliases", "id",
|
||||||
)
|
)
|
||||||
@@ -79,19 +79,16 @@ def proposal_is_stale(note: Note) -> bool:
|
|||||||
return recorded != content_hash(note)
|
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
|
Once set, this is never changed by the watcher. The owner can edit it, or
|
||||||
difference between the two means a human edited it. Their choice wins —
|
delete the field to ask for a fresh suggestion.
|
||||||
the watcher must never overwrite it.
|
|
||||||
"""
|
"""
|
||||||
chosen = str(note.fm.get("proposed_folder") or "").strip()
|
chosen = str(note.fm.get("proposed_folder") or "").strip()
|
||||||
auto = str(note.fm.get("proposed_folder_auto") or "").strip()
|
return chosen or None
|
||||||
if chosen and auto and chosen != auto:
|
|
||||||
return chosen
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class Watcher:
|
class Watcher:
|
||||||
@@ -133,12 +130,13 @@ class Watcher:
|
|||||||
# ----------------------------------------------------------------- proposing
|
# ----------------------------------------------------------------- proposing
|
||||||
|
|
||||||
def _write_proposal(self, note: Note, proposal: Proposal,
|
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."""
|
"""Add the proposal fields to the note in place. Returns log lines."""
|
||||||
fm = dict(note.fm)
|
fm = dict(note.fm)
|
||||||
|
|
||||||
# The owner's hand-picked folder always wins over a fresh suggestion.
|
# A folder that is already recorded is never re-decided. The owner's
|
||||||
effective = preserve_folder or proposal.target_folder
|
# 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.
|
# Resolve the real index + section so the proposal reads clearly.
|
||||||
toc_rel, toc_section, _ = self.writer.resolve_toc(
|
toc_rel, toc_section, _ = self.writer.resolve_toc(
|
||||||
@@ -152,7 +150,6 @@ class Watcher:
|
|||||||
fm["proposed_toc"] = "(not indexed)"
|
fm["proposed_toc"] = "(not indexed)"
|
||||||
|
|
||||||
fm["proposed_folder"] = effective
|
fm["proposed_folder"] = effective
|
||||||
fm["proposed_folder_auto"] = proposal.target_folder
|
|
||||||
if proposal.proposed_tags:
|
if proposal.proposed_tags:
|
||||||
fm["proposed_tags"] = proposal.proposed_tags
|
fm["proposed_tags"] = proposal.proposed_tags
|
||||||
fm["confidence"] = round(proposal.confidence, 2)
|
fm["confidence"] = round(proposal.confidence, 2)
|
||||||
@@ -164,8 +161,8 @@ class Watcher:
|
|||||||
self.writer._atomic_write(Path(note.path), text)
|
self.writer._atomic_write(Path(note.path), text)
|
||||||
|
|
||||||
note_msg = f"**proposed** `{note.filename}` → `{effective}` "
|
note_msg = f"**proposed** `{note.filename}` → `{effective}` "
|
||||||
if preserve_folder:
|
if keep_folder:
|
||||||
note_msg += "(your folder kept) "
|
note_msg += "(folder already set — kept) "
|
||||||
note_msg += f"(confidence {proposal.confidence:.2f})"
|
note_msg += f"(confidence {proposal.confidence:.2f})"
|
||||||
return [note_msg] + [f" - {w}" for w in proposal.warnings]
|
return [note_msg] + [f" - {w}" for w in proposal.warnings]
|
||||||
|
|
||||||
@@ -263,12 +260,12 @@ class Watcher:
|
|||||||
f"**held** `{note.filename}` — "
|
f"**held** `{note.filename}` — "
|
||||||
+ "; ".join(proposal.warnings)
|
+ "; ".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")
|
actions.append(f"**held** `{note.filename}` — no destination determined")
|
||||||
else:
|
else:
|
||||||
actions += self._write_proposal(
|
actions += self._write_proposal(
|
||||||
note, 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
|
except Exception as e: # never let one note kill the loop
|
||||||
actions.append(f"**error** `{note.filename}`: {type(e).__name__}: {e}")
|
actions.append(f"**error** `{note.filename}`: {type(e).__name__}: {e}")
|
||||||
|
|||||||
@@ -326,27 +326,27 @@ def main() -> int:
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ---- watcher: the owner's hand-picked folder must survive ----------
|
# ---- watcher: a recorded folder is never re-decided ----------------
|
||||||
from sorter.watch import user_selected_folder
|
from sorter.watch import existing_folder
|
||||||
from sorter.models import Note as _Note
|
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",
|
return _Note(path="x", rel_path="010 inbox/x.md", filename="x.md",
|
||||||
stem="x", title="x", note_type="note", status=None,
|
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":
|
if existing_folder(_n("300 areas/A")) == "300 areas/A":
|
||||||
print("PASS guard: a hand-edited folder is treated as the owner's choice")
|
print("PASS guard: a recorded folder is treated as already decided")
|
||||||
passed += 1
|
passed += 1
|
||||||
else:
|
else:
|
||||||
details.append("FAIL guard: hand-edited folder not detected")
|
details.append("FAIL guard: recorded folder not detected")
|
||||||
failed += 1
|
failed += 1
|
||||||
|
|
||||||
if user_selected_folder(_n("200 projects/B", "200 projects/B")) is None:
|
if existing_folder(_n("")) is None:
|
||||||
print("PASS guard: an untouched proposal is not treated as customised")
|
print("PASS guard: no recorded folder means the watcher may suggest one")
|
||||||
passed += 1
|
passed += 1
|
||||||
else:
|
else:
|
||||||
details.append("FAIL guard: untouched proposal wrongly seen as customised")
|
details.append("FAIL guard: empty folder wrongly treated as set")
|
||||||
failed += 1
|
failed += 1
|
||||||
|
|
||||||
# ---- inbox cleared --------------------------------------------------
|
# ---- inbox cleared --------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user