policies.py
python
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6
chore(tests): add docstring to tests/__init__.py so rc14 tr…
Human
32 days ago
| 1 | """Knowtation Harmony merge policies — the six-row default policy table. |
| 2 | |
| 3 | A *Harmony policy* is a named, semantically-motivated merge rule that maps a |
| 4 | (path_glob, dimension, strategy) triple onto a priority. Six default policies |
| 5 | cover the common Knowtation vault layouts without requiring the user to author |
| 6 | custom ``.museattributes`` rules from scratch. |
| 7 | |
| 8 | Policy table (in evaluation order, highest priority first) |
| 9 | ---------------------------------------------------------- |
| 10 | |
| 11 | +---------+----------------------+-------------+------------------+----------+ |
| 12 | | Name | Path glob | Dimension | Strategy | Priority | |
| 13 | +=========+======================+=============+==================+==========+ |
| 14 | | archive | ``archive/**`` | ``*`` | ``ours`` | 50 | |
| 15 | +---------+----------------------+-------------+------------------+----------+ |
| 16 | | inbox | ``inbox/**`` | ``*`` | ``prefer-newer- | 40 | |
| 17 | | | | | date`` | | |
| 18 | +---------+----------------------+-------------+------------------+----------+ |
| 19 | | capture | ``captures/**`` | ``*`` | ``prefer-newer- | 35 | |
| 20 | | | | | date`` | | |
| 21 | +---------+----------------------+-------------+------------------+----------+ |
| 22 | | tags | ``**/*.md`` | ``frontmat- | ``union-sorted`` | 30 | |
| 23 | | | | ter`` | | | |
| 24 | +---------+----------------------+-------------+------------------+----------+ |
| 25 | | project | ``projects/**`` | ``*`` | ``knowtation- | 20 | |
| 26 | | | | | 3way`` | | |
| 27 | +---------+----------------------+-------------+------------------+----------+ |
| 28 | | default | ``*`` | ``*`` | ``auto`` | 0 | |
| 29 | +---------+----------------------+-------------+------------------+----------+ |
| 30 | |
| 31 | Rationale |
| 32 | --------- |
| 33 | |
| 34 | ``archive`` (priority 50) |
| 35 | Archived notes are immutable. ``ours`` prevents any remote change from |
| 36 | re-opening resolved content. |
| 37 | |
| 38 | ``inbox`` (priority 40) |
| 39 | Captured notes are time-ordered. ``prefer-newer-date`` resolves conflicts |
| 40 | by retaining the most recent capture without losing earlier timestamps. |
| 41 | |
| 42 | ``capture`` (priority 35) |
| 43 | Import-pipeline output in ``captures/`` follows the same logic as inbox. |
| 44 | Lower priority than ``inbox`` so path-specific inbox rules take precedence. |
| 45 | |
| 46 | ``tags`` (priority 30) |
| 47 | Frontmatter tag/entity/attachment sets are independent across branches. |
| 48 | ``union-sorted`` accumulates additions deterministically. Applies to the |
| 49 | ``frontmatter`` dimension only; body/section conflicts escalate to |
| 50 | ``knowtation-3way`` via the ``project`` rule. |
| 51 | |
| 52 | ``project`` (priority 20) |
| 53 | Project notes are collaborative. ``knowtation-3way`` applies the full |
| 54 | four-layer structured merge; falls back to ``auto`` until Phase 2.4 lands. |
| 55 | |
| 56 | ``default`` (priority 0) |
| 57 | Catch-all. Defers all unmatched paths to the engine. |
| 58 | |
| 59 | Public API |
| 60 | ---------- |
| 61 | - :data:`HARMONY_POLICIES` — ordered list of :class:`HarmonyPolicy` objects. |
| 62 | - :func:`to_attribute_rules` — convert all policies to sorted ``AttributeRule`` list. |
| 63 | - :func:`write_harmony_museattributes` — write the policy table to ``.museattributes``. |
| 64 | - :func:`resolve_harmony_strategy` — look up the strategy for a (path, dimension). |
| 65 | """ |
| 66 | |
| 67 | from __future__ import annotations |
| 68 | |
| 69 | import pathlib |
| 70 | from dataclasses import dataclass, field |
| 71 | |
| 72 | from muse.core.attributes import AttributeRule, resolve_strategy |
| 73 | |
| 74 | |
| 75 | # --------------------------------------------------------------------------- |
| 76 | # HarmonyPolicy dataclass |
| 77 | # --------------------------------------------------------------------------- |
| 78 | |
| 79 | |
| 80 | @dataclass(frozen=True) |
| 81 | class HarmonyPolicy: |
| 82 | """A single Harmony merge policy. |
| 83 | |
| 84 | Attributes: |
| 85 | name: Short identifier used in documentation and audit logs. |
| 86 | path_pattern: ``fnmatch`` glob matched against vault-relative paths. |
| 87 | dimension: Knowtation dimension name (e.g. ``"frontmatter"``) or |
| 88 | ``"*"`` to match any dimension. |
| 89 | strategy: Resolution strategy string (must be in |
| 90 | :data:`muse.core.attributes.VALID_STRATEGIES`). |
| 91 | priority: Evaluation order weight; higher-priority policies are |
| 92 | tested before lower-priority ones. |
| 93 | comment: Human-readable rationale written to ``.museattributes``. |
| 94 | """ |
| 95 | |
| 96 | name: str |
| 97 | path_pattern: str |
| 98 | dimension: str |
| 99 | strategy: str |
| 100 | priority: int |
| 101 | comment: str = field(default="") |
| 102 | |
| 103 | |
| 104 | # --------------------------------------------------------------------------- |
| 105 | # The six canonical Harmony policies |
| 106 | # --------------------------------------------------------------------------- |
| 107 | |
| 108 | HARMONY_POLICIES: tuple[HarmonyPolicy, ...] = ( |
| 109 | HarmonyPolicy( |
| 110 | name="archive", |
| 111 | path_pattern="archive/**", |
| 112 | dimension="*", |
| 113 | strategy="ours", |
| 114 | priority=50, |
| 115 | comment=( |
| 116 | "Archived notes are immutable. " |
| 117 | "Prefer the local version and discard any remote change." |
| 118 | ), |
| 119 | ), |
| 120 | HarmonyPolicy( |
| 121 | name="inbox", |
| 122 | path_pattern="inbox/**", |
| 123 | dimension="*", |
| 124 | strategy="prefer-newer-date", |
| 125 | priority=40, |
| 126 | comment=( |
| 127 | "Inbox captures are time-ordered. " |
| 128 | "Keep the note with the later 'date' frontmatter field." |
| 129 | ), |
| 130 | ), |
| 131 | HarmonyPolicy( |
| 132 | name="capture", |
| 133 | path_pattern="captures/**", |
| 134 | dimension="*", |
| 135 | strategy="prefer-newer-date", |
| 136 | priority=35, |
| 137 | comment=( |
| 138 | "Import-pipeline output in captures/ follows the same rule as inbox: " |
| 139 | "the note with the later date wins." |
| 140 | ), |
| 141 | ), |
| 142 | HarmonyPolicy( |
| 143 | name="tags", |
| 144 | path_pattern="**/*.md", |
| 145 | dimension="frontmatter", |
| 146 | strategy="union-sorted", |
| 147 | priority=30, |
| 148 | comment=( |
| 149 | "Frontmatter tag, entity, and attachment sets are independent across " |
| 150 | "branches. Accumulate additions and sort alphabetically." |
| 151 | ), |
| 152 | ), |
| 153 | HarmonyPolicy( |
| 154 | name="project", |
| 155 | path_pattern="projects/**", |
| 156 | dimension="*", |
| 157 | strategy="knowtation-3way", |
| 158 | priority=20, |
| 159 | comment=( |
| 160 | "Project notes are collaborative. " |
| 161 | "Apply the full Knowtation four-layer structured merge." |
| 162 | ), |
| 163 | ), |
| 164 | HarmonyPolicy( |
| 165 | name="default", |
| 166 | path_pattern="*", |
| 167 | dimension="*", |
| 168 | strategy="auto", |
| 169 | priority=0, |
| 170 | comment="Catch-all: defer unmatched paths to the engine's three-way algorithm.", |
| 171 | ), |
| 172 | ) |
| 173 | |
| 174 | |
| 175 | # --------------------------------------------------------------------------- |
| 176 | # Conversion helpers |
| 177 | # --------------------------------------------------------------------------- |
| 178 | |
| 179 | |
| 180 | def to_attribute_rules() -> list[AttributeRule]: |
| 181 | """Convert all Harmony policies to a priority-sorted ``AttributeRule`` list. |
| 182 | |
| 183 | The returned list is sorted by priority (descending), then by declaration |
| 184 | order (ascending), matching the contract of |
| 185 | :func:`~muse.core.attributes.load_attributes_full`. |
| 186 | |
| 187 | Returns: |
| 188 | List of :class:`~muse.core.attributes.AttributeRule` objects. |
| 189 | """ |
| 190 | rules: list[AttributeRule] = [] |
| 191 | for idx, policy in enumerate(HARMONY_POLICIES): |
| 192 | rules.append( |
| 193 | AttributeRule( |
| 194 | path_pattern=policy.path_pattern, |
| 195 | dimension=policy.dimension, |
| 196 | strategy=policy.strategy, |
| 197 | comment=policy.comment, |
| 198 | priority=policy.priority, |
| 199 | source_index=idx, |
| 200 | ) |
| 201 | ) |
| 202 | rules.sort(key=lambda r: -r.priority) |
| 203 | return rules |
| 204 | |
| 205 | |
| 206 | def resolve_harmony_strategy(path: str, dimension: str = "*") -> str: |
| 207 | """Return the first matching Harmony strategy for *path* and *dimension*. |
| 208 | |
| 209 | A convenience wrapper over :func:`~muse.core.attributes.resolve_strategy` |
| 210 | using the pre-built Harmony rule list. Returns ``"auto"`` when no rule |
| 211 | matches (the ``default`` policy ensures this never happens in practice). |
| 212 | |
| 213 | Args: |
| 214 | path: Vault-relative POSIX path (e.g. ``"inbox/note.md"``). |
| 215 | dimension: Dimension name (e.g. ``"frontmatter"``), or ``"*"``. |
| 216 | |
| 217 | Returns: |
| 218 | Strategy string. |
| 219 | """ |
| 220 | return resolve_strategy(to_attribute_rules(), path, dimension) |
| 221 | |
| 222 | |
| 223 | def write_harmony_museattributes(root: pathlib.Path, *, overwrite: bool = False) -> None: |
| 224 | """Write the six Harmony policies to ``.museattributes`` in *root*. |
| 225 | |
| 226 | The generated file is valid TOML and can be edited by the user after |
| 227 | generation. An existing file is not overwritten unless ``overwrite=True``. |
| 228 | |
| 229 | Args: |
| 230 | root: Repository root directory. |
| 231 | overwrite: When ``False`` (default), raise ``FileExistsError`` if |
| 232 | ``.museattributes`` already exists. |
| 233 | |
| 234 | Raises: |
| 235 | FileExistsError: When the file exists and ``overwrite=False``. |
| 236 | """ |
| 237 | dest = root / ".museattributes" |
| 238 | if dest.exists() and not overwrite: |
| 239 | raise FileExistsError( |
| 240 | f"{dest} already exists. Pass overwrite=True to replace it." |
| 241 | ) |
| 242 | |
| 243 | lines: list[str] = [ |
| 244 | "# .museattributes — Knowtation Harmony merge policies.", |
| 245 | "# Generated by muse.plugins.knowtation.policies.", |
| 246 | "# Edit freely; these rules are the starting point, not a constraint.", |
| 247 | "", |
| 248 | "[meta]", |
| 249 | 'domain = "knowtation"', |
| 250 | "", |
| 251 | ] |
| 252 | |
| 253 | for policy in HARMONY_POLICIES: |
| 254 | lines.append(f"# Policy: {policy.name} (priority {policy.priority})") |
| 255 | lines.append("[[rules]]") |
| 256 | lines.append(f'path = "{policy.path_pattern}"') |
| 257 | lines.append(f'dimension = "{policy.dimension}"') |
| 258 | lines.append(f'strategy = "{policy.strategy}"') |
| 259 | if policy.comment: |
| 260 | lines.append(f'comment = "{policy.comment}"') |
| 261 | lines.append(f"priority = {policy.priority}") |
| 262 | lines.append("") |
| 263 | |
| 264 | dest.write_text("\n".join(lines), encoding="utf-8") |
File History
2 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6
chore(tests): add docstring to tests/__init__.py so rc14 tr…
Human
32 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62
baseline: rc14 re-baseline after rc3 store corruption recovery
Human
patch
32 days ago