generated from coulomb/repo-seed
WP-0016 finished: interactive registry maintain with llm-connect automation
Some checks failed
ci / validate-registry (push) Has been cancelled
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:
160
reuse_surface/maintain_llm.py
Normal file
160
reuse_surface/maintain_llm.py
Normal 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 | D0–D7 | Planning/orientation reuse strength |
|
||||
| availability | A0–A7 | Consumption mode and delivery artifacts |
|
||||
| completeness | C0–C6 | Scope vs intent and expectations |
|
||||
| reliability | R0–R6 | 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
|
||||
Reference in New Issue
Block a user