Files
reuse-surface/reuse_surface/maintain_llm.py
tegwick b24ec507aa
Some checks failed
ci / validate-registry (push) Has been cancelled
WP-0016 finished: interactive registry maintain with llm-connect automation
Closes the registry maintenance loop from inside each domain repo:
interactive prompting for judgment calls, full automation for safe and
high-confidence changes, both backed by the llm-connect HTTP bridge.

- New modules: maintain.py, maintain_llm.py, patches.py, interactive.py
- Schema: schemas/registry-patch.schema.json
- CLI: reuse-surface maintain; establish --scaffold --hook
- Sibling templates: Makefile fragment, pre-commit hook
- Deterministic signal collectors extended; validate cwd auto-detect
- Docs, gap priority 28, SCOPE update
- Tests: test_maintain.py, test_interactive.py (59 pytest total)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 04:00:39 +02:00

160 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import json
import subprocess
import textwrap
from pathlib import Path
from typing import Any
import yaml
from jsonschema import Draft202012Validator
from reuse_surface.llm_bridge import execute_prompt, extract_json_object
from reuse_surface.registry import ROOT, load_index_at, parse_front_matter, registry_paths
PATCH_SCHEMA_PATH = ROOT / "schemas" / "registry-patch.schema.json"
MATURITY_SUMMARY = """
| Dimension | Levels | Question |
|---|---|---|
| discovery | D0D7 | Planning/orientation reuse strength |
| availability | A0A7 | Consumption mode and delivery artifacts |
| completeness | C0C6 | Scope vs intent and expectations |
| reliability | R0R6 | Consumer quality signals |
Promotion rules:
- Cite repo-relative evidence paths for every maturity_promote patch.
- Prefer single-step promotions (one level per dimension).
- Do not invent files; only cite paths visible in git diff or context.
"""
def load_patch_schema() -> dict[str, Any]:
return json.loads(PATCH_SCHEMA_PATH.read_text(encoding="utf-8"))
def _git_diff(repo_root: Path, git_since: str | None) -> str:
if not git_since:
return ""
proc = subprocess.run(
[
"git",
"-C",
str(repo_root),
"diff",
git_since,
"HEAD",
"--",
"registry/",
"reuse_surface/",
"tests/",
"docs/",
".gitea/",
"pyproject.toml",
],
capture_output=True,
text=True,
check=False,
)
return proc.stdout[:12000]
def build_maintain_prompt(
repo_root: Path,
capability_id: str,
*,
git_since: str | None = None,
context_files: list[str] | None = None,
) -> str:
paths = registry_paths(repo_root)
index = load_index_at(paths["index"])
row = next((item for item in index["capabilities"] if item["id"] == capability_id), None)
if not row:
raise ValueError(f"capability not in index: {capability_id}")
entry = parse_front_matter(repo_root / row["path"])
diff = _git_diff(repo_root, git_since)
context_chunks: list[str] = []
for rel in context_files or []:
path = repo_root / rel
if path.is_file():
context_chunks.append(f"### {rel}\n{path.read_text(encoding='utf-8')[:4000]}")
schema_hint = json.dumps(
{
"patches": [
{
"capability_id": capability_id,
"kind": "maturity_promote",
"confidence": "medium",
"rationale": "CI gate added",
"dimension": "reliability",
"from_level": "R2",
"to_level": "R3",
"evidence_citations": ["tests/test_example.py"],
"promotion_history_entry": {
"date": "2026-06-16",
"dimension": "reliability",
"from": "R2",
"to": "R3",
"rationale": "pytest coverage for consumption path",
},
}
],
"notes": ["optional human review items"],
},
indent=2,
)
return textwrap.dedent(
f"""
Propose structured registry maintenance patches for `{capability_id}`.
Return ONLY JSON matching this shape (no markdown fences):
{schema_hint}
Allowed patch kinds: vector_sync, evidence_append, artifact_append,
maturity_promote, consumer_feedback, relation_add, index_row_add,
index_updated_bump.
Maturity reference:
{MATURITY_SUMMARY}
Current entry YAML:
{yaml.safe_dump(entry, sort_keys=False)}
Git diff since {git_since or 'N/A'}:
{diff or '(none)'}
Context files:
{chr(10).join(context_chunks) if context_chunks else '(none)'}
"""
).strip()
def request_maintain_patches(
repo_root: Path,
capability_id: str,
*,
git_since: str | None = None,
context_files: list[str] | None = None,
llm_url: str | None = None,
) -> dict[str, Any]:
prompt = build_maintain_prompt(
repo_root,
capability_id,
git_since=git_since,
context_files=context_files,
)
content = execute_prompt(
prompt,
base_url=llm_url,
config={"temperature": 0.2, "max_tokens": 3000},
)
payload = extract_json_object(content)
validator = Draft202012Validator(load_patch_schema())
errors = sorted(validator.iter_errors(payload), key=lambda err: list(err.path))
if errors:
messages = "; ".join(error.message for error in errors[:3])
raise ValueError(f"patch schema validation failed: {messages}")
return payload