"""Data structures shared across the sorter.""" from __future__ import annotations from dataclasses import dataclass, field from typing import Any # --------------------------------------------------------------------------- # The taxonomy, simplified 2026-09-24. # # Sam's model: the TYPE of a note mirrors the FOLDER it lives in. Nothing is # guessed from content — the destination decides the type. # # being worked on -> 200 projects/ -> project # ongoing interest + docs-> 300 areas/ -> area # family / non-work -> 400 Personal Family -> family # # Everything else that used to exist (subject, tool, container, machine, # backup-system, website, ...) has been RETIRED. Do not reintroduce them. # --------------------------------------------------------------------------- # Type mirrors folder. These are the only valid types. VALID_TYPES = { "project", # 200 projects/ "area", # 300 areas/ "family", # 400 Personal Family/ "index", # 100 Table of Contents/ "daily", # 000 daily/ "archive", # 500 archive/ "clipping", # Clippings/ (browser extension drops) "note", # unsorted placeholder, only ever in the inbox } # The three real filing destinations. BUCKETS = ("project", "area", "family") # Folder prefix -> type. Order matters: longest prefix wins. FOLDER_BUCKETS: list[tuple[str, str]] = [ ("200 projects", "project"), ("300 areas", "area"), ("400 Personal Family", "family"), ("100 Table of Contents", "index"), ("000 daily", "daily"), ("500 archive", "archive"), ("Clippings", "clipping"), ] # The generic-template marker. Only valid while a note sits in the inbox. PLACEHOLDER_TYPE = "note" def type_from_folder(folder: str) -> str | None: """Return the type implied by a vault folder, or None if unknown.""" if not folder: return None for prefix, note_type in FOLDER_BUCKETS: if folder == prefix or folder.startswith(prefix + "/"): return note_type return None @dataclass class Note: """A note discovered in the vault.""" path: str # absolute path rel_path: str # path relative to vault root filename: str # e.g. "Borg Backup.md" stem: str # e.g. "Borg Backup" title: str | None # frontmatter title, if present note_type: str | None # frontmatter type status: str | None # frontmatter status tags: list[str] = field(default_factory=list) areas: list[str] = field(default_factory=list) projects: list[str] = field(default_factory=list) fm: dict[str, Any] = field(default_factory=dict) has_frontmatter: bool = False body: str = "" @property def display_name(self) -> str: return self.title or self.stem @property def folder(self) -> str: """Vault-relative folder, '' for root.""" parent = self.rel_path.rsplit("/", 1) return parent[0] if len(parent) == 2 else "" @property def expected_type(self) -> str | None: """The type this note's folder says it should have.""" return type_from_folder(self.folder) @property def needs_classification(self) -> bool: """ True when the note has no trustworthy type. A note is only 'sorted' when its type matches its folder. """ current = (self.note_type or "").strip().lower() expected = self.expected_type if expected is None: return True # not in a known bucket (e.g. inbox) return current != expected @dataclass class Proposal: """A classification recommendation — a plan, not an action.""" source_rel: str note_type: str target_folder: str toc_section: str template: str confidence: float method: str # "deterministic" | "jev" | "none" | "trusted" reasons: list[str] = field(default_factory=list) is_new_area: bool = False warnings: list[str] = field(default_factory=list) alternatives: list[tuple[str, float]] = field(default_factory=list) proposed_tags: list[str] = field(default_factory=list) def as_dict(self) -> dict[str, Any]: return { "source": self.source_rel, "type": self.note_type, "target_folder": self.target_folder, "toc_section": self.toc_section, "template": self.template, "confidence": round(self.confidence, 3), "method": self.method, "reasons": self.reasons, "is_new_area": self.is_new_area, "warnings": self.warnings, "alternatives": self.alternatives, "proposed_tags": self.proposed_tags, }