generated from coulomb/repo-seed
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>
160 lines
4.8 KiB
Python
160 lines
4.8 KiB
Python
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 |