generated from coulomb/repo-seed
Add quality criteria registry
This commit is contained in:
15
src/repo_registry/acceptance/__init__.py
Normal file
15
src/repo_registry/acceptance/__init__.py
Normal 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",
|
||||
]
|
||||
148
src/repo_registry/acceptance/criteria.py
Normal file
148
src/repo_registry/acceptance/criteria.py
Normal 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],
|
||||
)
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user