"""Distributor base — Artifact, the Distributor protocol, and idempotent markers (PRD §6.4 FR-X1; T01). A **distributor** turns one agnostic :class:`SolutionPattern` into a per-flavor :class:`Artifact` (a target path + a snippet of content). Everything flavor-neutral lives here; each flavor adapter (T02/T03) only supplies its target filename and may override the rendered body using the pattern's ``rendering_hints``. Snippets carry stable ``BEGIN/END`` markers keyed on the pattern id, so re-distributing a pattern **updates its block in place** instead of duplicating it — the property that lets Distribute run repeatedly (HITL) without drift. """ from __future__ import annotations import re from dataclasses import dataclass from typing import Any, Optional, Protocol, runtime_checkable from ..curate.schema import SolutionPattern @dataclass class Artifact: """A proposed per-flavor rendering of a pattern (FR-X1/FR-X3 — proposed, not applied).""" flavor: str target_path: str # repo-relative file the snippet belongs in (e.g. "CLAUDE.md") pattern_id: str content: str # the marker-wrapped snippet block @runtime_checkable class Distributor(Protocol): flavor: str target_path: str def render(self, pattern: SolutionPattern) -> Artifact: ... # --- idempotent snippet markers --------------------------------------------- _MARK = "helix-forge pattern" def begin_marker(pattern_id: str) -> str: return f"" def end_marker(pattern_id: str) -> str: return f"" def wrap_block(pattern_id: str, body: str, version: str = "") -> str: """Wrap a rendered body in stable BEGIN/END markers.""" ver = f" v{version}" if version else "" return f"{begin_marker(pattern_id)}{ver}\n{body.strip()}\n{end_marker(pattern_id)}" def upsert_block(doc_text: str, pattern_id: str, block: str) -> str: """Insert or replace a pattern's marked block within a document (idempotent).""" pat = re.compile( re.escape(begin_marker(pattern_id)) + r".*?" + re.escape(end_marker(pattern_id)), re.DOTALL, ) if pat.search(doc_text): return pat.sub(block, doc_text) sep = "" if doc_text.endswith("\n\n") or not doc_text else "\n\n" return f"{doc_text}{sep}{block}\n" # --- agnostic body rendering ------------------------------------------------ def render_markdown_body(pattern: SolutionPattern) -> str: """Default flavor-neutral snippet body from the agnostic pattern fields.""" label = "Avoid" if pattern.polarity == "problem" else "Prefer" lines = [f"### {pattern.name}", "", pattern.problem.strip(), ""] if pattern.resolutions: lines.append(f"**{label}:**") for r in pattern.resolutions: detail = f" — {r.detail}" if r.detail else "" lines.append(f"- {r.summary}{detail}") for step in r.steps: lines.append(f" - {step}") return "\n".join(lines).strip() def hint(pattern: SolutionPattern, flavor: str, key: str, default: Any = None) -> Any: """Read a per-flavor rendering hint, falling back to ``default``.""" return (pattern.rendering_hints.get(flavor) or {}).get(key, default) class BaseDistributor: """Shared distributor: renders the agnostic body, honouring a ``body`` hint override and a ``target`` hint, then wraps it in idempotent markers.""" flavor: str = "" target_path: str = "" def __init__(self, flavor: Optional[str] = None, target_path: Optional[str] = None) -> None: if flavor is not None: self.flavor = flavor if target_path is not None: self.target_path = target_path def body(self, pattern: SolutionPattern) -> str: return hint(pattern, self.flavor, "body") or render_markdown_body(pattern) def target(self, pattern: SolutionPattern) -> str: return hint(pattern, self.flavor, "target") or self.target_path def render(self, pattern: SolutionPattern) -> Artifact: block = wrap_block(pattern.id, self.body(pattern), pattern.version) return Artifact(flavor=self.flavor, target_path=self.target(pattern), pattern_id=pattern.id, content=block)