Add quality criteria registry

This commit is contained in:
2026-05-15 15:22:45 +02:00
parent f029d6bba9
commit a9baf5ae52
15 changed files with 629 additions and 7 deletions

View File

@@ -0,0 +1,15 @@
from repo_registry.acceptance.criteria import (
active_quality_criteria_version,
criteria_registry_dict,
criteria_registry_json,
criteria_registry_markdown,
load_quality_criteria,
)
__all__ = [
"active_quality_criteria_version",
"criteria_registry_dict",
"criteria_registry_json",
"criteria_registry_markdown",
"load_quality_criteria",
]

View File

@@ -0,0 +1,148 @@
from __future__ import annotations
import json
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any
CRITERIA_SCHEMA_VERSION = "quality-criteria-registry/v1"
DEFAULT_CRITERIA_PATH = (
Path(__file__).resolve().parents[3]
/ "docs"
/ "quality-criteria"
/ "acceptance-quality-criteria.v1.json"
)
REQUIRED_CRITERION_FIELDS = {
"id",
"title",
"category",
"severity",
"applies_to",
"description",
"deterministic_action",
"deterministic_action_when",
"reviewer_guidance",
}
@dataclass(frozen=True)
class QualityCriterion:
id: str
title: str
category: str
severity: str
applies_to: list[str]
description: str
deterministic_action: str
deterministic_action_when: str
reviewer_guidance: str
agentic_guidance: str = ""
examples: list[str] | None = None
@dataclass(frozen=True)
class QualityCriteriaRegistry:
schema_version: str
criteria_version: str
status: str
updated_at: str
criteria: list[QualityCriterion]
def load_quality_criteria(path: str | Path | None = None) -> QualityCriteriaRegistry:
criteria_path = Path(path) if path is not None else DEFAULT_CRITERIA_PATH
payload = json.loads(criteria_path.read_text(encoding="utf-8"))
return _registry_from_payload(payload)
def active_quality_criteria_version(path: str | Path | None = None) -> str:
return load_quality_criteria(path).criteria_version
def criteria_registry_dict(registry: QualityCriteriaRegistry) -> dict[str, Any]:
return asdict(registry)
def criteria_registry_json(registry: QualityCriteriaRegistry) -> str:
return json.dumps(criteria_registry_dict(registry), indent=2, sort_keys=True) + "\n"
def criteria_registry_markdown(registry: QualityCriteriaRegistry) -> str:
lines = [
f"# Quality Criteria Registry: {registry.criteria_version}",
"",
f"- Schema: `{registry.schema_version}`",
f"- Status: `{registry.status}`",
f"- Updated: `{registry.updated_at}`",
"",
]
for criterion in registry.criteria:
lines.extend(
[
f"## {criterion.id}: {criterion.title}",
"",
f"- Category: `{criterion.category}`",
f"- Severity: `{criterion.severity}`",
f"- Applies to: `{', '.join(criterion.applies_to)}`",
f"- Deterministic action: `{criterion.deterministic_action}`",
"",
criterion.description,
"",
f"Deterministic trigger: {criterion.deterministic_action_when}",
"",
f"Reviewer guidance: {criterion.reviewer_guidance}",
"",
]
)
return "\n".join(lines)
def _registry_from_payload(payload: dict[str, Any]) -> QualityCriteriaRegistry:
if payload.get("schema_version") != CRITERIA_SCHEMA_VERSION:
raise ValueError(
"unsupported quality criteria schema: "
f"{payload.get('schema_version', '<missing>')}"
)
criteria_payload = payload.get("criteria")
if not isinstance(criteria_payload, list) or not criteria_payload:
raise ValueError("quality criteria registry must contain criteria")
criteria = [_criterion_from_payload(item) for item in criteria_payload]
ids = [criterion.id for criterion in criteria]
if len(ids) != len(set(ids)):
raise ValueError("quality criteria ids must be unique")
return QualityCriteriaRegistry(
schema_version=str(payload.get("schema_version", "")),
criteria_version=str(payload.get("criteria_version", "")),
status=str(payload.get("status", "")),
updated_at=str(payload.get("updated_at", "")),
criteria=criteria,
)
def _criterion_from_payload(payload: dict[str, Any]) -> QualityCriterion:
missing = sorted(REQUIRED_CRITERION_FIELDS - set(payload))
if missing:
raise ValueError(
f"quality criterion {payload.get('id', '<unknown>')} missing fields: "
f"{', '.join(missing)}"
)
applies_to = payload.get("applies_to")
if not isinstance(applies_to, list) or not applies_to:
raise ValueError(
f"quality criterion {payload.get('id', '<unknown>')} must list applies_to"
)
examples = payload.get("examples") or []
return QualityCriterion(
id=str(payload["id"]),
title=str(payload["title"]),
category=str(payload["category"]),
severity=str(payload["severity"]),
applies_to=[str(item) for item in applies_to],
description=str(payload["description"]),
deterministic_action=str(payload["deterministic_action"]),
deterministic_action_when=str(payload["deterministic_action_when"]),
reviewer_guidance=str(payload["reviewer_guidance"]),
agentic_guidance=str(payload.get("agentic_guidance", "")),
examples=[str(item) for item in examples],
)

View File

@@ -4,6 +4,11 @@ import argparse
from pathlib import Path
from typing import Sequence
from repo_registry.acceptance import (
criteria_registry_json,
criteria_registry_markdown,
load_quality_criteria,
)
from repo_registry.core.models import CharacteristicRebuildResult, Repository
from repo_registry.core.service import RegistryService
from repo_registry.llm_extraction import LLMCandidateExtractor, create_llm_connect_adapter
@@ -151,6 +156,21 @@ def build_parser() -> argparse.ArgumentParser:
self_assess.add_argument("--database-path", help="Override REPO_REGISTRY_DATABASE_PATH.")
self_assess.add_argument("--checkout-root", help="Override REPO_REGISTRY_CHECKOUT_ROOT.")
self_assess.set_defaults(no_llm=True)
criteria = subparsers.add_parser(
"list-quality-criteria",
help="List the active characteristic quality criteria registry.",
)
criteria.add_argument(
"--criteria-path",
help="Override the default quality criteria registry JSON path.",
)
criteria.add_argument("--output", help="Write criteria output to this path instead of stdout.")
criteria.add_argument(
"--format",
choices=["json", "markdown"],
default="markdown",
help="Criteria output format.",
)
return parser
@@ -165,6 +185,8 @@ def main(argv: Sequence[str] | None = None) -> int:
return compare_assessment_command(args)
if args.command == "self-assess":
return self_assess_command(args, parser)
if args.command == "list-quality-criteria":
return list_quality_criteria_command(args)
parser.error(f"unknown command: {args.command}")
return 2
@@ -218,6 +240,20 @@ def compare_assessment_command(args: argparse.Namespace) -> int:
return 0
def list_quality_criteria_command(args: argparse.Namespace) -> int:
registry = load_quality_criteria(args.criteria_path)
content = (
criteria_registry_json(registry)
if args.format == "json"
else criteria_registry_markdown(registry)
)
if args.output:
write_text(args.output, content)
else:
print(content, end="" if content.endswith("\n") else "\n")
return 0
def self_assess_command(
args: argparse.Namespace,
parser: argparse.ArgumentParser,

View File

@@ -9,6 +9,7 @@ from importlib import metadata
from pathlib import Path
from typing import Any
from repo_registry.acceptance import active_quality_criteria_version
from repo_registry.core.models import (
Ability,
CandidateAbility,
@@ -132,7 +133,7 @@ def _engine_identity(scanner_version: str, engine_root: Path) -> dict[str, Any]:
"engine_dirty_state": dirty_state,
"scanner_version": scanner_version,
"candidate_generator_version": "unversioned",
"quality_criteria_version": "none",
"quality_criteria_version": active_quality_criteria_version(),
"prompt_version": None,
"release_binding_status": release_binding_status,
"release_binding_note": (
@@ -349,7 +350,9 @@ def _source_ref(ref: SourceReference) -> dict[str, Any]:
def _review_decision(decision: ReviewDecision) -> dict[str, Any]:
return asdict(decision)
payload = asdict(decision)
payload["quality_criteria_version"] = active_quality_criteria_version()
return payload
def _known_regression_patterns(

View File

@@ -12,6 +12,7 @@ from fastapi.responses import PlainTextResponse
from pydantic import Field
from pydantic_settings import BaseSettings, SettingsConfigDict
from repo_registry.acceptance import criteria_registry_dict, load_quality_criteria
from repo_registry.core.service import RegistryService
from repo_registry.llm_extraction import LLMCandidateExtractor, create_llm_connect_adapter
from repo_registry.repo_ingestion.git import GitIngestionService
@@ -58,6 +59,7 @@ from repo_registry.web_api.schemas import (
FeatureUpdate,
IdResponse,
ObservedFactResponse,
QualityCriteriaRegistryResponse,
RepositoryAbilityMapResponse,
RepositoryComparisonResponse,
RepositoryCreate,
@@ -183,6 +185,15 @@ def health(settings: Settings = Depends(get_settings)) -> dict[str, object]:
}
@app.get(
"/quality-criteria",
tags=["review"],
response_model=QualityCriteriaRegistryResponse,
)
def list_quality_criteria() -> dict[str, object]:
return criteria_registry_dict(load_quality_criteria())
@app.post(
"/repos",
status_code=201,

View File

@@ -502,6 +502,48 @@ class ReviewDecisionResponse(BaseModel):
created_at: str
class QualityCriterionResponse(BaseModel):
id: str
title: str
category: str
severity: str
applies_to: list[str]
description: str
deterministic_action: str
deterministic_action_when: str
reviewer_guidance: str
agentic_guidance: str = ""
examples: list[str] = Field(default_factory=list)
model_config = {
"json_schema_extra": {
"examples": [
{
"id": "RREG-QC-002",
"title": "Native Utility Is Repo-Owned",
"category": "native-utility",
"severity": "high",
"applies_to": ["ability", "capability"],
"description": "Owned claims require product evidence.",
"deterministic_action": "downgraded",
"deterministic_action_when": "Evidence is dependency-only.",
"reviewer_guidance": "Check whether the repo owns the utility.",
"agentic_guidance": "Approve only with product and source evidence.",
"examples": ["Dependency use is not native product behavior."],
}
]
}
}
class QualityCriteriaRegistryResponse(BaseModel):
schema_version: str
criteria_version: str
status: str
updated_at: str
criteria: list[QualityCriterionResponse]
class ObservedFactResponse(BaseModel):
id: int
repository_id: int