"""Knowtation Harmony merge policies — the six-row default policy table. A *Harmony policy* is a named, semantically-motivated merge rule that maps a (path_glob, dimension, strategy) triple onto a priority. Six default policies cover the common Knowtation vault layouts without requiring the user to author custom ``.museattributes`` rules from scratch. Policy table (in evaluation order, highest priority first) ---------------------------------------------------------- +---------+----------------------+-------------+------------------+----------+ | Name | Path glob | Dimension | Strategy | Priority | +=========+======================+=============+==================+==========+ | archive | ``archive/**`` | ``*`` | ``ours`` | 50 | +---------+----------------------+-------------+------------------+----------+ | inbox | ``inbox/**`` | ``*`` | ``prefer-newer- | 40 | | | | | date`` | | +---------+----------------------+-------------+------------------+----------+ | capture | ``captures/**`` | ``*`` | ``prefer-newer- | 35 | | | | | date`` | | +---------+----------------------+-------------+------------------+----------+ | tags | ``**/*.md`` | ``frontmat- | ``union-sorted`` | 30 | | | | ter`` | | | +---------+----------------------+-------------+------------------+----------+ | project | ``projects/**`` | ``*`` | ``knowtation- | 20 | | | | | 3way`` | | +---------+----------------------+-------------+------------------+----------+ | default | ``*`` | ``*`` | ``auto`` | 0 | +---------+----------------------+-------------+------------------+----------+ Rationale --------- ``archive`` (priority 50) Archived notes are immutable. ``ours`` prevents any remote change from re-opening resolved content. ``inbox`` (priority 40) Captured notes are time-ordered. ``prefer-newer-date`` resolves conflicts by retaining the most recent capture without losing earlier timestamps. ``capture`` (priority 35) Import-pipeline output in ``captures/`` follows the same logic as inbox. Lower priority than ``inbox`` so path-specific inbox rules take precedence. ``tags`` (priority 30) Frontmatter tag/entity/attachment sets are independent across branches. ``union-sorted`` accumulates additions deterministically. Applies to the ``frontmatter`` dimension only; body/section conflicts escalate to ``knowtation-3way`` via the ``project`` rule. ``project`` (priority 20) Project notes are collaborative. ``knowtation-3way`` applies the full four-layer structured merge; falls back to ``auto`` until Phase 2.4 lands. ``default`` (priority 0) Catch-all. Defers all unmatched paths to the engine. Public API ---------- - :data:`HARMONY_POLICIES` — ordered list of :class:`HarmonyPolicy` objects. - :func:`to_attribute_rules` — convert all policies to sorted ``AttributeRule`` list. - :func:`write_harmony_museattributes` — write the policy table to ``.museattributes``. - :func:`resolve_harmony_strategy` — look up the strategy for a (path, dimension). """ from __future__ import annotations import pathlib from dataclasses import dataclass, field from muse.core.attributes import AttributeRule, resolve_strategy # --------------------------------------------------------------------------- # HarmonyPolicy dataclass # --------------------------------------------------------------------------- @dataclass(frozen=True) class HarmonyPolicy: """A single Harmony merge policy. Attributes: name: Short identifier used in documentation and audit logs. path_pattern: ``fnmatch`` glob matched against vault-relative paths. dimension: Knowtation dimension name (e.g. ``"frontmatter"``) or ``"*"`` to match any dimension. strategy: Resolution strategy string (must be in :data:`muse.core.attributes.VALID_STRATEGIES`). priority: Evaluation order weight; higher-priority policies are tested before lower-priority ones. comment: Human-readable rationale written to ``.museattributes``. """ name: str path_pattern: str dimension: str strategy: str priority: int comment: str = field(default="") # --------------------------------------------------------------------------- # The six canonical Harmony policies # --------------------------------------------------------------------------- HARMONY_POLICIES: tuple[HarmonyPolicy, ...] = ( HarmonyPolicy( name="archive", path_pattern="archive/**", dimension="*", strategy="ours", priority=50, comment=( "Archived notes are immutable. " "Prefer the local version and discard any remote change." ), ), HarmonyPolicy( name="inbox", path_pattern="inbox/**", dimension="*", strategy="prefer-newer-date", priority=40, comment=( "Inbox captures are time-ordered. " "Keep the note with the later 'date' frontmatter field." ), ), HarmonyPolicy( name="capture", path_pattern="captures/**", dimension="*", strategy="prefer-newer-date", priority=35, comment=( "Import-pipeline output in captures/ follows the same rule as inbox: " "the note with the later date wins." ), ), HarmonyPolicy( name="tags", path_pattern="**/*.md", dimension="frontmatter", strategy="union-sorted", priority=30, comment=( "Frontmatter tag, entity, and attachment sets are independent across " "branches. Accumulate additions and sort alphabetically." ), ), HarmonyPolicy( name="project", path_pattern="projects/**", dimension="*", strategy="knowtation-3way", priority=20, comment=( "Project notes are collaborative. " "Apply the full Knowtation four-layer structured merge." ), ), HarmonyPolicy( name="default", path_pattern="*", dimension="*", strategy="auto", priority=0, comment="Catch-all: defer unmatched paths to the engine's three-way algorithm.", ), ) # --------------------------------------------------------------------------- # Conversion helpers # --------------------------------------------------------------------------- def to_attribute_rules() -> list[AttributeRule]: """Convert all Harmony policies to a priority-sorted ``AttributeRule`` list. The returned list is sorted by priority (descending), then by declaration order (ascending), matching the contract of :func:`~muse.core.attributes.load_attributes_full`. Returns: List of :class:`~muse.core.attributes.AttributeRule` objects. """ rules: list[AttributeRule] = [] for idx, policy in enumerate(HARMONY_POLICIES): rules.append( AttributeRule( path_pattern=policy.path_pattern, dimension=policy.dimension, strategy=policy.strategy, comment=policy.comment, priority=policy.priority, source_index=idx, ) ) rules.sort(key=lambda r: -r.priority) return rules def resolve_harmony_strategy(path: str, dimension: str = "*") -> str: """Return the first matching Harmony strategy for *path* and *dimension*. A convenience wrapper over :func:`~muse.core.attributes.resolve_strategy` using the pre-built Harmony rule list. Returns ``"auto"`` when no rule matches (the ``default`` policy ensures this never happens in practice). Args: path: Vault-relative POSIX path (e.g. ``"inbox/note.md"``). dimension: Dimension name (e.g. ``"frontmatter"``), or ``"*"``. Returns: Strategy string. """ return resolve_strategy(to_attribute_rules(), path, dimension) def write_harmony_museattributes(root: pathlib.Path, *, overwrite: bool = False) -> None: """Write the six Harmony policies to ``.museattributes`` in *root*. The generated file is valid TOML and can be edited by the user after generation. An existing file is not overwritten unless ``overwrite=True``. Args: root: Repository root directory. overwrite: When ``False`` (default), raise ``FileExistsError`` if ``.museattributes`` already exists. Raises: FileExistsError: When the file exists and ``overwrite=False``. """ dest = root / ".museattributes" if dest.exists() and not overwrite: raise FileExistsError( f"{dest} already exists. Pass overwrite=True to replace it." ) lines: list[str] = [ "# .museattributes — Knowtation Harmony merge policies.", "# Generated by muse.plugins.knowtation.policies.", "# Edit freely; these rules are the starting point, not a constraint.", "", "[meta]", 'domain = "knowtation"', "", ] for policy in HARMONY_POLICIES: lines.append(f"# Policy: {policy.name} (priority {policy.priority})") lines.append("[[rules]]") lines.append(f'path = "{policy.path_pattern}"') lines.append(f'dimension = "{policy.dimension}"') lines.append(f'strategy = "{policy.strategy}"') if policy.comment: lines.append(f'comment = "{policy.comment}"') lines.append(f"priority = {policy.priority}") lines.append("") dest.write_text("\n".join(lines), encoding="utf-8")