Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 8 additions & 60 deletions src/readme2demo/distill.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@
from readme2demo import llm
from readme2demo.types import CommandLog, DistillOutput, Plan, TapeCommand

from readme2demo.escaping import ( # noqa: F401 — re-export, callers import from distill
DistillError,
_VHS_REGEX_METAS,
_grep_flags_and_pattern,
vhs_quote,
vhs_wait_pattern,
)

_PROMPTS_DIR = Path(__file__).resolve().parent / "prompts"
_TEMPLATES_DIR = Path(__file__).resolve().parent / "templates"

Expand Down Expand Up @@ -65,19 +73,6 @@ def heredoc_prefix(cmd: str) -> Optional[str]:
return " ".join(cmd[: m.start()].split()).strip()


class DistillError(RuntimeError):
"""Raised when the distiller cannot produce a fully grounded output.

Carries ``cost_usd`` so spend already incurred before the failure is not
lost: the grounding retry means this error can arrive *after* two paid
LLM calls, and the orchestrator records it against the failed stage.
"""

def __init__(self, *args, cost_usd: float = 0.0) -> None:
super().__init__(*args)
self.cost_usd = cost_usd


# -- grounding ----------------------------------------------------------------


Expand Down Expand Up @@ -350,18 +345,6 @@ def run_distiller(
# -- artifact writing ---------------------------------------------------------


def _grep_flags_and_pattern(pattern: str) -> tuple[str, str]:
"""Translate a Python-style regex to grep -E usage.

GNU grep -E does not understand inline flags like ``(?i)``; the planner
(an LLM) writes Python-style patterns. Handle the common case by
stripping a leading ``(?i)`` and adding grep's ``-i`` flag.
"""
if pattern.startswith("(?i)"):
return "-qiE", pattern[4:]
return "-qE", pattern


def _tolerate_findings_steps(commands: list[str], log: CommandLog | None) -> list[str]:
"""Append ``|| true`` to step commands that legitimately exit nonzero.

Expand Down Expand Up @@ -455,41 +438,6 @@ def _render_commands_sh(out: DistillOutput, plan: Plan, repo_url: str, log: "Com
return "\n".join(lines) + "\n"


# Go-regexp metacharacters (VHS Wait+Screen patterns), plus the / delimiter.
_VHS_REGEX_METAS = set("\\.+*?()|[]{}^$/")


def vhs_wait_pattern(s: str, max_len: int = 40) -> str:
"""Make a Wait+Screen pattern safe: treat it as a LITERAL substring.

The distiller is told to use plain substrings, but an LLM instruction is
not enforcement — e.g. "ToolHive (thv) is a lightweight" silently becomes
a regex with a capture group that never matches the on-screen parens and
times the render out. Escape every metacharacter, and truncate so the
pattern can't span a wrapped terminal line.
"""
s = s[:max_len]
return "".join("\\" + ch if ch in _VHS_REGEX_METAS else ch for ch in s)


def vhs_quote(s: str) -> str:
"""Quote a string for a VHS ``Type`` argument.

VHS string literals do not support backslash escapes; instead VHS accepts
three delimiters. Pick one the string doesn't contain: double quotes,
then backticks, then single quotes.
"""
if '"' not in s:
return f'"{s}"'
if "`" not in s:
return f"`{s}`"
if "'" not in s:
return f"'{s}'"
raise DistillError(
f"Command cannot be quoted for VHS (contains \", ` and '): {s!r}"
)


def write_commands_sh(out: DistillOutput, run_dir: Path, plan: Plan, repo_url: str, log: "CommandLog | None" = None) -> Path:
"""Write the executable commands.sh (header + clone preamble + assertion)."""
run_dir.mkdir(parents=True, exist_ok=True)
Expand Down
67 changes: 67 additions & 0 deletions src/readme2demo/escaping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
"""DSL escaping helpers for VHS tape syntax and grep -E patterns.

Extracted from distill.py so the pure string-escaping surface can evolve
without touching grounding logic.
"""

from __future__ import annotations


class DistillError(RuntimeError):
"""Raised when the distiller cannot produce a fully grounded output.

Carries ``cost_usd`` so spend already incurred before the failure is not
lost: the grounding retry means this error can arrive *after* two paid
LLM calls, and the orchestrator records it against the failed stage.
"""

def __init__(self, *args, cost_usd: float = 0.0) -> None:
super().__init__(*args)
self.cost_usd = cost_usd


def _grep_flags_and_pattern(pattern: str) -> tuple[str, str]:
"""Translate a Python-style regex to grep -E usage.

GNU grep -E does not understand inline flags like ``(?i)``; the planner
(an LLM) writes Python-style patterns. Handle the common case by
stripping a leading ``(?i)`` and adding grep's ``-i`` flag.
"""
if pattern.startswith("(?i)"):
return "-qiE", pattern[4:]
return "-qE", pattern


# Go-regexp metacharacters (VHS Wait+Screen patterns), plus the / delimiter.
_VHS_REGEX_METAS = set("\\.+*?()|[]{}^$/")


def vhs_wait_pattern(s: str, max_len: int = 40) -> str:
"""Make a Wait+Screen pattern safe: treat it as a LITERAL substring.

The distiller is told to use plain substrings, but an LLM instruction is
not enforcement — e.g. "ToolHive (thv) is a lightweight" silently becomes
a regex with a capture group that never matches the on-screen parens and
times the render out. Escape every metacharacter, and truncate so the
pattern can't span a wrapped terminal line.
"""
s = s[:max_len]
return "".join("\\" + ch if ch in _VHS_REGEX_METAS else ch for ch in s)


def vhs_quote(s: str) -> str:
"""Quote a string for a VHS ``Type`` argument.

VHS string literals do not support backslash escapes; instead VHS accepts
three delimiters. Pick one the string doesn't contain: double quotes,
then backticks, then single quotes.
"""
if '"' not in s:
return f'"{s}"'
if "`" not in s:
return f"`{s}`"
if "'" not in s:
return f"'{s}'"
raise DistillError(
f"Command cannot be quoted for VHS (contains \", ` and '): {s!r}"
)