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>
188 lines
5.8 KiB
Python
188 lines
5.8 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import yaml
|
|
|
|
from reuse_surface.establish import scaffold_registry
|
|
from reuse_surface.maintain import run_maintain
|
|
from reuse_surface.maintain_llm import build_maintain_prompt, load_patch_schema
|
|
from reuse_surface.patches import (
|
|
apply_patches,
|
|
evidence_gate,
|
|
filter_auto_patches,
|
|
patches_from_suggestions,
|
|
promotion_delta_gate,
|
|
suggestion_to_patch,
|
|
)
|
|
from reuse_surface.registry import load_index_at, registry_paths
|
|
from reuse_surface.registry_update import collect_deterministic_suggestions
|
|
|
|
|
|
def _seed_repo(tmp_path: Path) -> str:
|
|
scaffold_registry(tmp_path)
|
|
cap_id = "capability.demo.sample"
|
|
rel = "registry/capabilities/capability-demo-sample.md"
|
|
front_matter = {
|
|
"id": cap_id,
|
|
"name": "Sample",
|
|
"summary": "Sample",
|
|
"owner": "demo",
|
|
"status": "draft",
|
|
"domain": "helix_forge",
|
|
"tags": ["demo"],
|
|
"maturity": {
|
|
"discovery": {"current": "D2", "target": "D5", "confidence": "low"},
|
|
"availability": {"current": "A0", "target": "A3", "confidence": "low"},
|
|
},
|
|
"external_evidence": {
|
|
"completeness": {"level": "C0", "confidence": "low"},
|
|
"reliability": {"level": "R0", "confidence": "low"},
|
|
},
|
|
"discovery": {"intent": "demo", "includes": [], "excludes": []},
|
|
"availability": {
|
|
"current_level": "A0",
|
|
"target_level": "A3",
|
|
"current_artifacts": [],
|
|
"consumption_modes": ["informational"],
|
|
},
|
|
"relations": {"depends_on": [], "supports": [], "related_to": []},
|
|
"evidence": {"documentation": [], "tests": []},
|
|
"consumer_guidance": {
|
|
"recommended_for": [],
|
|
"not_recommended_for": [],
|
|
"known_limitations": [],
|
|
},
|
|
}
|
|
entry = tmp_path / rel
|
|
entry.parent.mkdir(parents=True, exist_ok=True)
|
|
entry.write_text("---\n" + yaml.safe_dump(front_matter, sort_keys=False) + "---\n")
|
|
index_path = registry_paths(tmp_path)["index"]
|
|
index = load_index_at(index_path)
|
|
index["capabilities"] = [
|
|
{
|
|
"id": cap_id,
|
|
"name": "Sample",
|
|
"summary": "Sample",
|
|
"vector": "D3 / A0 / C0 / R0",
|
|
"domain": "helix_forge",
|
|
"status": "draft",
|
|
"owner": "demo",
|
|
"path": rel,
|
|
"tags": ["demo"],
|
|
"consumption_modes": ["informational"],
|
|
}
|
|
]
|
|
index_path.write_text(yaml.safe_dump(index, sort_keys=False), encoding="utf-8")
|
|
return cap_id
|
|
|
|
|
|
def test_patch_schema_loads():
|
|
schema = load_patch_schema()
|
|
assert "patches" in schema["properties"]
|
|
|
|
|
|
def test_build_maintain_prompt(tmp_path: Path):
|
|
cap_id = _seed_repo(tmp_path)
|
|
prompt = build_maintain_prompt(tmp_path, cap_id, git_since=None)
|
|
assert cap_id in prompt
|
|
assert "Return ONLY JSON" in prompt
|
|
|
|
|
|
def test_suggestion_to_patch_vector_sync():
|
|
patch = suggestion_to_patch(
|
|
{
|
|
"capability_id": "capability.demo.sample",
|
|
"kind": "vector_drift",
|
|
"detail": "drift",
|
|
"apply_patch": {"field": "index.vector", "value": "D2 / A0 / C0 / R0"},
|
|
}
|
|
)
|
|
assert patch is not None
|
|
assert patch["kind"] == "vector_sync"
|
|
|
|
|
|
def test_evidence_gate_requires_files(tmp_path: Path):
|
|
evidence = (tmp_path / "tests" / "test_x.py")
|
|
evidence.parent.mkdir(parents=True)
|
|
evidence.write_text("def test_x(): pass\n")
|
|
patch = {
|
|
"kind": "maturity_promote",
|
|
"evidence_citations": ["tests/test_x.py"],
|
|
}
|
|
assert evidence_gate(tmp_path, patch)
|
|
patch["evidence_citations"] = ["tests/missing.py"]
|
|
assert not evidence_gate(tmp_path, patch)
|
|
|
|
|
|
def test_promotion_delta_gate():
|
|
patch = {
|
|
"kind": "maturity_promote",
|
|
"dimension": "availability",
|
|
"from_level": "A2",
|
|
"to_level": "A3",
|
|
}
|
|
assert promotion_delta_gate(patch, 1)
|
|
patch["to_level"] = "A5"
|
|
assert not promotion_delta_gate(patch, 1)
|
|
|
|
|
|
def test_apply_patches_vector_sync(tmp_path: Path):
|
|
cap_id = _seed_repo(tmp_path)
|
|
suggestions = collect_deterministic_suggestions(tmp_path, capability_id=cap_id)
|
|
patches = patches_from_suggestions(suggestions)
|
|
changed = apply_patches(tmp_path, patches)
|
|
assert changed
|
|
index = load_index_at(registry_paths(tmp_path)["index"])
|
|
assert index["capabilities"][0]["vector"] == "D2 / A0 / C0 / R0"
|
|
|
|
|
|
def test_filter_auto_patches(tmp_path: Path):
|
|
cap_id = _seed_repo(tmp_path)
|
|
suggestions = collect_deterministic_suggestions(tmp_path, capability_id=cap_id)
|
|
patches = patches_from_suggestions(suggestions)
|
|
selected = filter_auto_patches(patches, tmp_path)
|
|
assert selected
|
|
|
|
|
|
def test_run_maintain_auto_no_llm(tmp_path: Path):
|
|
_seed_repo(tmp_path)
|
|
|
|
def _validate() -> tuple[int, list[str], list[str]]:
|
|
return 0, [], []
|
|
|
|
result = run_maintain(
|
|
tmp_path,
|
|
all_capabilities=True,
|
|
auto=True,
|
|
no_llm=True,
|
|
validate_fn=_validate,
|
|
)
|
|
assert result.exit_code == 0
|
|
assert result.selected_count >= 1
|
|
|
|
|
|
def test_request_maintain_patches_mock(tmp_path: Path):
|
|
cap_id = _seed_repo(tmp_path)
|
|
payload = {
|
|
"patches": [
|
|
{
|
|
"capability_id": cap_id,
|
|
"kind": "consumer_feedback",
|
|
"confidence": "medium",
|
|
"rationale": "note",
|
|
"append": "helpful",
|
|
}
|
|
],
|
|
"notes": [],
|
|
}
|
|
with patch(
|
|
"reuse_surface.maintain_llm.execute_prompt",
|
|
return_value=json.dumps(payload),
|
|
):
|
|
from reuse_surface.maintain_llm import request_maintain_patches
|
|
|
|
result = request_maintain_patches(tmp_path, cap_id, llm_url="http://example")
|
|
assert len(result["patches"]) == 1 |