WP-0016 finished: interactive registry maintain with llm-connect automation
Some checks failed
ci / validate-registry (push) Has been cancelled

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>
This commit is contained in:
2026-06-18 04:00:39 +02:00
parent 1afa7e5ee5
commit b24ec507aa
22 changed files with 3604 additions and 39 deletions

View File

@@ -0,0 +1,160 @@
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