generated from coulomb/repo-seed
session-memory: distributor base + Artifact (WP-0007 T01)
distribute/base.py: Artifact dataclass + Distributor protocol + idempotent BEGIN/END snippet markers (upsert_block replaces a pattern's block in place so re-distribution doesn't duplicate) + agnostic markdown body rendering from SolutionPattern fields. BaseDistributor honours per-flavor body/target hints. 8 new tests; suite 110/110. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
9
session_memory/distribute/__init__.py
Normal file
9
session_memory/distribute/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
||||
"""Distribute phase (PRD §6.4) — render approved Solution Patterns into per-flavor
|
||||
artifacts. Mirror of the collector design: agnostic core, thin distributor edges.
|
||||
|
||||
base.py Artifact + Distributor protocol + idempotent snippet markers (T01)
|
||||
claude.py CLAUDE.md snippet distributor (T02)
|
||||
codex.py AGENTS.md snippet distributor (T03)
|
||||
grok.py native instruction distributor (T03)
|
||||
__main__.py `python -m session_memory.distribute` (T05)
|
||||
"""
|
||||
115
session_memory/distribute/base.py
Normal file
115
session_memory/distribute/base.py
Normal file
@@ -0,0 +1,115 @@
|
||||
"""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"<!-- BEGIN {_MARK}:{pattern_id} -->"
|
||||
|
||||
|
||||
def end_marker(pattern_id: str) -> str:
|
||||
return f"<!-- END {_MARK}:{pattern_id} -->"
|
||||
|
||||
|
||||
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)
|
||||
88
tests/test_distribute_base.py
Normal file
88
tests/test_distribute_base.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""Distributor base tests (WP-0007 T01): markers, idempotent upsert, rendering."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from session_memory.curate.schema import Resolution, SolutionPattern # noqa: E402
|
||||
from session_memory.distribute.base import ( # noqa: E402
|
||||
Artifact,
|
||||
BaseDistributor,
|
||||
Distributor,
|
||||
render_markdown_body,
|
||||
upsert_block,
|
||||
wrap_block,
|
||||
)
|
||||
|
||||
|
||||
def _pattern(pid="sp-x", polarity="problem"):
|
||||
return SolutionPattern(
|
||||
id=pid, name="Read before edit", version="1.2.0", polarity=polarity,
|
||||
problem="Agents edit files they have not read.",
|
||||
resolutions=[Resolution(summary="Read the file first", detail="then Edit",
|
||||
steps=["Read", "Edit"])],
|
||||
rendering_hints={"claude": {"target": "CLAUDE.md"}},
|
||||
)
|
||||
|
||||
|
||||
def test_render_markdown_body_has_problem_and_resolution():
|
||||
body = render_markdown_body(_pattern())
|
||||
assert "### Read before edit" in body
|
||||
assert "Agents edit files" in body
|
||||
assert "**Avoid:**" in body # problem polarity
|
||||
assert "- Read the file first — then Edit" in body
|
||||
assert " - Read" in body
|
||||
|
||||
|
||||
def test_success_polarity_label():
|
||||
assert "**Prefer:**" in render_markdown_body(_pattern(polarity="success"))
|
||||
|
||||
|
||||
def test_wrap_block_has_markers_and_version():
|
||||
block = wrap_block("sp-x", "hello", "1.2.0")
|
||||
assert block.startswith("<!-- BEGIN helix-forge pattern:sp-x --> v1.2.0")
|
||||
assert block.rstrip().endswith("<!-- END helix-forge pattern:sp-x -->")
|
||||
|
||||
|
||||
def test_upsert_inserts_then_replaces_in_place():
|
||||
doc = "# Title\n\nsome text\n"
|
||||
b1 = wrap_block("sp-x", "first", "1")
|
||||
once = upsert_block(doc, "sp-x", b1)
|
||||
assert "first" in once and once.count("BEGIN helix-forge pattern:sp-x") == 1
|
||||
# re-distributing the same id replaces, does not duplicate
|
||||
b2 = wrap_block("sp-x", "second", "2")
|
||||
twice = upsert_block(once, "sp-x", b2)
|
||||
assert "second" in twice and "first" not in twice
|
||||
assert twice.count("BEGIN helix-forge pattern:sp-x") == 1
|
||||
|
||||
|
||||
def test_upsert_keeps_other_patterns():
|
||||
doc = upsert_block("", "sp-a", wrap_block("sp-a", "A"))
|
||||
doc = upsert_block(doc, "sp-b", wrap_block("sp-b", "B"))
|
||||
assert "sp-a" in doc and "sp-b" in doc
|
||||
|
||||
|
||||
def test_base_distributor_renders_artifact():
|
||||
d = BaseDistributor(flavor="claude", target_path="CLAUDE.md")
|
||||
art = d.render(_pattern())
|
||||
assert isinstance(art, Artifact)
|
||||
assert isinstance(d, Distributor) # satisfies the protocol
|
||||
assert art.flavor == "claude"
|
||||
assert art.target_path == "CLAUDE.md"
|
||||
assert "BEGIN helix-forge pattern:sp-x" in art.content
|
||||
assert "Read before edit" in art.content
|
||||
|
||||
|
||||
def test_body_hint_overrides_default():
|
||||
p = _pattern()
|
||||
p.rendering_hints["claude"]["body"] = "custom claude body"
|
||||
d = BaseDistributor(flavor="claude", target_path="CLAUDE.md")
|
||||
assert "custom claude body" in d.render(p).content
|
||||
|
||||
|
||||
def test_target_hint_overrides_default():
|
||||
p = _pattern()
|
||||
p.rendering_hints["claude"]["target"] = "docs/CLAUDE.md"
|
||||
d = BaseDistributor(flavor="claude", target_path="CLAUDE.md")
|
||||
assert d.render(p).target_path == "docs/CLAUDE.md"
|
||||
@@ -29,7 +29,7 @@ active-pattern registry tracking which patterns are live where (FR-X4).
|
||||
|
||||
```task
|
||||
id: AGENTIC-WP-0007-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
state_hub_task_id: "ff618fa6-a78b-4b80-846b-8cde7ad65451"
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user