2026-06-18 04:00:39 +02:00
|
|
|
|
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/",
|
2026-07-07 18:14:30 +02:00
|
|
|
|
".forgejo/",
|
2026-06-18 04:00:39 +02:00
|
|
|
|
"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
|