2828 decode_index_body as _decode_index_body ,
2929 encode_index_body as _encode_index_body ,
3030)
31+ from app .services .vault_sweep_plan import (
32+ is_in_topic_dir ,
33+ plan_duplicate_trash ,
34+ plan_noise_trash ,
35+ plan_topic_move ,
36+ propose_topic_move ,
37+ )
3138from app .time_utils import _iso_utc , _today_str
3239from sentinel_shared .embedding_codec import decode_embedding , encode_embedding
3340from sentinel_shared .similarity import cosine_similarity , find_dup_clusters
5057 "_encode_index_body" ,
5158 "cosine_similarity" ,
5259 "find_dup_clusters" ,
60+ "is_in_topic_dir" ,
5361 "split_frontmatter" ,
5462 "join_frontmatter" ,
63+ "propose_topic_move" ,
5564]
5665
5766logger = logging .getLogger (__name__ )
@@ -178,44 +187,8 @@ async def walk_vault(client, root: str = "") -> AsyncIterator[str]:
178187# stays here; I/O goes through the injected ``vault``.
179188
180189
181- # --- Topic-folder move (misplaced note → correct topic dir) ---
182-
183-
184- def is_in_topic_dir (path : str , topic_dir : str ) -> bool :
185- """True when ``path`` is already within ``topic_dir``.
186-
187- Handles the journal nested-date case: ``journal/2026-04-27/foo.md`` is
188- considered in-dir for any ``journal/...`` topic_dir, not just exact
189- same-day match. The sweeper does not relocate journal entries between
190- days — only flags a wrong-topic placement.
191- """
192- if not topic_dir :
193- return False
194- # Same dir or any subdirectory of topic_dir's root family.
195- # For journal/2026-04-27, the family root is "journal/"; so journal/.../
196- # is considered "in topic_dir family".
197- family_root = topic_dir .split ("/" , 1 )[0 ] + "/"
198- return path .startswith (family_root )
199-
200-
201- def propose_topic_move (
202- src_path : str , topic : str , * , today : str | None = None
203- ) -> str | None :
204- """Return the destination path a topic-move WOULD use, or None if no
205- move is needed (already in topic family) or topic has no canonical dir.
206-
207- Used by ``run_sweep(dry_run=True)`` to populate ``proposed_moves``
208- without touching the vault.
209- """
210- from app .services .note_classifier import topic_dir_for
211-
212- topic_dir = topic_dir_for (topic , today = today )
213- if not topic_dir :
214- return None
215- if is_in_topic_dir (src_path , topic_dir ):
216- return None
217- filename = src_path .rsplit ("/" , 1 )[- 1 ]
218- return f"{ topic_dir } /{ filename } "
190+ # Topic-folder move planning lives in app.services.vault_sweep_plan and is
191+ # imported/re-exported here for backwards compatibility with existing callers.
219192
220193
221194# --- Lockfile (migrated to ObsidianVault.acquire_sweep_lock / release_sweep_lock) ---
@@ -481,12 +454,9 @@ async def _is_safe() -> bool:
481454 if getattr (result , "topic" , None ) == "noise" :
482455 if dry_run :
483456 today = _today_str ()
484- report .proposed_moves .append ({
485- "kind" : "trash" ,
486- "src" : path ,
487- "dst" : f"_trash/{ today } /{ path .rsplit ('/' , 1 )[- 1 ]} " ,
488- "reason" : "cheap-filter:noise" ,
489- })
457+ report .proposed_moves .append (
458+ plan_noise_trash (path , today = today ).asdict ()
459+ )
490460 report .noise_moved += 1
491461 else :
492462 # MANDATORY per-move safety check (re-evaluated here, not once per run)
@@ -509,17 +479,19 @@ async def _is_safe() -> bool:
509479 # directory and the current path isn't already in that family,
510480 # move (or propose to move) the note to {topic_dir}/{filename}.
511481 topic = getattr (result , "topic" , None )
512- proposed_dst = (
513- propose_topic_move (path , topic ) if topic else None
482+ topic_plan = (
483+ plan_topic_move (
484+ path ,
485+ topic ,
486+ confidence = float (result .confidence ),
487+ )
488+ if topic
489+ else None
514490 )
491+ proposed_dst = topic_plan .dst if topic_plan is not None else None
515492 if proposed_dst is not None :
516493 if dry_run :
517- report .proposed_moves .append ({
518- "kind" : "topic" ,
519- "src" : path ,
520- "dst" : proposed_dst ,
521- "reason" : f"topic={ topic } (confidence={ result .confidence :.2f} )" ,
522- })
494+ report .proposed_moves .append (topic_plan .asdict ())
523495 # 260427-cza: parity with the live `else` branch below
524496 # which increments topic_moves. Without this, dry-run
525497 # reports `topic_moves: 0` while listing N proposals.
@@ -664,13 +636,14 @@ async def _is_safe() -> bool:
664636 keeper_path = survivors [keeper_idx ][0 ]
665637 if dry_run :
666638 today = _today_str ()
667- proposed = f"_trash/{ today } /{ src .rsplit ('/' , 1 )[- 1 ]} "
668- report .proposed_moves .append ({
669- "kind" : "trash" ,
670- "src" : src ,
671- "dst" : proposed ,
672- "reason" : f"duplicate of { keeper_path } (cosine≥0.92, conf={ conf :.1f} )" ,
673- })
639+ report .proposed_moves .append (
640+ plan_duplicate_trash (
641+ src ,
642+ keeper_path ,
643+ confidence = conf ,
644+ today = today ,
645+ ).asdict ()
646+ )
674647 report .duplicates_moved += 1
675648 continue
676649 # MANDATORY per-move safety check (re-evaluated before each dedup-trash)
0 commit comments