From a9baf5ae523a11f025940c666f56f2a7b704b991 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 15 May 2026 15:22:45 +0200 Subject: [PATCH] Add quality criteria registry --- docs/acceptance-policy.md | 9 +- docs/quality-criteria/README.md | 17 ++ .../acceptance-quality-criteria.v1.json | 100 ++++++++++++ .../quality-criteria-registry.schema.json | 105 +++++++++++++ src/repo_registry/acceptance/__init__.py | 15 ++ src/repo_registry/acceptance/criteria.py | 148 ++++++++++++++++++ src/repo_registry/cli.py | 36 +++++ src/repo_registry/self_scoping/assessment.py | 7 +- src/repo_registry/web_api/app.py | 11 ++ src/repo_registry/web_api/schemas.py | 42 +++++ tests/test_cli.py | 26 +++ tests/test_quality_criteria.py | 51 ++++++ tests/test_self_scoping_assessment_export.py | 32 ++++ tests/test_web_api.py | 26 +++ ...-0014-agentic-characteristic-acceptance.md | 11 +- 15 files changed, 629 insertions(+), 7 deletions(-) create mode 100644 docs/quality-criteria/README.md create mode 100644 docs/quality-criteria/acceptance-quality-criteria.v1.json create mode 100644 docs/schemas/quality-criteria-registry.schema.json create mode 100644 src/repo_registry/acceptance/__init__.py create mode 100644 src/repo_registry/acceptance/criteria.py create mode 100644 tests/test_quality_criteria.py diff --git a/docs/acceptance-policy.md b/docs/acceptance-policy.md index d0402f3..f1b396e 100644 --- a/docs/acceptance-policy.md +++ b/docs/acceptance-policy.md @@ -87,10 +87,11 @@ that deterministic approval is allowed. ## Quality Criteria Relationship -The quality criteria registry defines the formal checks deterministic gates may -apply. Criteria should be versioned, reviewable, and specific enough to explain -why a candidate was rejected, downgraded, invalidated, merged, flagged, or sent -to review. +The quality criteria registry in `docs/quality-criteria/` defines the formal +checks deterministic gates may apply. Criteria should be versioned, reviewable, +and specific enough to explain why a candidate was rejected, downgraded, +invalidated, merged, flagged, or sent to review. The active registry can be +listed with `repo-scoping list-quality-criteria` or `GET /quality-criteria`. Examples of criteria families: diff --git a/docs/quality-criteria/README.md b/docs/quality-criteria/README.md new file mode 100644 index 0000000..d76e455 --- /dev/null +++ b/docs/quality-criteria/README.md @@ -0,0 +1,17 @@ +# Quality Criteria Registry + +`acceptance-quality-criteria.v1.json` is the active reviewable criteria registry +for candidate characteristics. It defines criteria that deterministic gates may +use to reject, downgrade, invalidate, flag, merge, or require review. + +The registry is intentionally not an approval policy. Approval belongs to human +reviewers or trusted agentic reviewers that inspect evidence and record a +rationale. + +List the active registry from the CLI: + +```bash +repo-scoping list-quality-criteria --format markdown +``` + +The same registry is available from the API at `GET /quality-criteria`. diff --git a/docs/quality-criteria/acceptance-quality-criteria.v1.json b/docs/quality-criteria/acceptance-quality-criteria.v1.json new file mode 100644 index 0000000..8fa3383 --- /dev/null +++ b/docs/quality-criteria/acceptance-quality-criteria.v1.json @@ -0,0 +1,100 @@ +{ + "schema_version": "quality-criteria-registry/v1", + "criteria_version": "repo-scoping-quality-criteria/v1", + "status": "active", + "updated_at": "2026-05-15", + "criteria": [ + { + "id": "RREG-QC-001", + "title": "Source Role Supports The Claim", + "category": "source-role-quality", + "severity": "medium", + "applies_to": ["ability", "capability", "feature", "evidence"], + "description": "Candidate claims need evidence whose source role can support the abstraction. Intent docs, implementation source, API surfaces, and product documentation carry stronger claim support than fixtures, schema examples, tests, agent instructions, generated scope text, dependency manifests, or incidental vocabulary.", + "deterministic_action": "requires_review", + "deterministic_action_when": "All supporting refs are weak source roles such as fixture, schema-example, generated-scope, dependency, test-vocabulary, or agent-guidance.", + "reviewer_guidance": "Check whether any cited source actually expresses product intent or implementation behavior for the candidate, not merely matching words.", + "agentic_guidance": "Do not approve when evidence only proves scanner vocabulary, test coverage, examples, or dependency presence.", + "examples": [ + "Provider names in tests can prove scanner coverage but not a native provider-routing capability.", + "A FastAPI route in source can support an API feature when it belongs under the right capability." + ] + }, + { + "id": "RREG-QC-002", + "title": "Native Utility Is Repo-Owned", + "category": "native-utility", + "severity": "high", + "applies_to": ["ability", "capability"], + "description": "Owned, facade, and adapter claims require explicit product evidence. Dependency, tooling, configuration, fixture, schema-example, and mention-only contexts are not native repository capabilities.", + "deterministic_action": "downgraded", + "deterministic_action_when": "The candidate is supported primarily by dependency, configuration, tooling, fixture, schema-example, or mention-only evidence.", + "reviewer_guidance": "Decide whether the repository exposes this utility as its own behavior or merely uses, tests, configures, or mentions another system.", + "agentic_guidance": "Approve only when product intent and implementation evidence show the repo owns the user-facing utility.", + "examples": [ + "Using llm-connect as extraction infrastructure does not make repo-scoping an LLM router.", + "A repository that exposes a scope context API owns that context API capability." + ] + }, + { + "id": "RREG-QC-003", + "title": "Feature Fits Parent Capability", + "category": "hierarchy-fit", + "severity": "high", + "applies_to": ["feature"], + "description": "Features must support the parent capability they are nested under. API, CLI, UI, storage, and backend features should not be attached to unrelated capabilities just because evidence was discovered nearby.", + "deterministic_action": "requires_review", + "deterministic_action_when": "Feature type, name, or source refs conflict with the parent capability class or native-utility claim.", + "reviewer_guidance": "Move or relink features to the capability they actually support before approval.", + "agentic_guidance": "Prefer relinking or proposing hierarchy edits over approving a mismatched parent-child relationship.", + "examples": [ + "HTTP API and CLI surfaces should not be nested below provider-routing when they describe repo-scoping registry operations." + ] + }, + { + "id": "RREG-QC-004", + "title": "Evidence Supports The Abstraction", + "category": "evidence-sufficiency", + "severity": "high", + "applies_to": ["ability", "capability", "feature", "evidence"], + "description": "Evidence must support the actual abstraction claimed by the candidate, not just share vocabulary with it. Claims need enough source refs to justify the level of abstraction.", + "deterministic_action": "requires_review", + "deterministic_action_when": "Source refs are absent, too vague, or only vocabulary matches for the candidate name.", + "reviewer_guidance": "Trace each source ref to the claim. Reject or rewrite candidates whose evidence only supports a narrower or different behavior.", + "agentic_guidance": "Name the exact evidence inspected and explain how it supports or fails to support the claim.", + "examples": [ + "A provider registry constant can support scanner fixture detection, not necessarily provider routing as product behavior." + ] + }, + { + "id": "RREG-QC-005", + "title": "Generated Scope Is Not Primary Proof", + "category": "circularity", + "severity": "critical", + "applies_to": ["ability", "capability", "feature", "evidence"], + "description": "Generated SCOPE.md text cannot be primary evidence for rebuilding the same characteristic model. It may be comparison context, bootstrap context, or a generated output under review.", + "deterministic_action": "rejected", + "deterministic_action_when": "A candidate is supported only or primarily by generated SCOPE.md content from the same scoping process.", + "reviewer_guidance": "Use source, docs, tests, and product intent instead of accepting circular evidence.", + "agentic_guidance": "Treat circular generated-scope evidence as a blocker unless independent evidence supports the same claim.", + "examples": [ + "Do not use repo-scoping's generated SCOPE.md as the main proof for repo-scoping's own ability tree." + ] + }, + { + "id": "RREG-QC-006", + "title": "Fixtures And Schemas Do Not Become Product Claims", + "category": "fixture-contamination", + "severity": "high", + "applies_to": ["ability", "capability", "feature", "evidence"], + "description": "Tests, fixtures, schemas, examples, and expectation files can prove scanner behavior or API contract examples, but they should not become native product capability claims unless backed by product or implementation evidence.", + "deterministic_action": "downgraded", + "deterministic_action_when": "The claim is primarily supported by tests, fixtures, schemas, examples, expectation files, or sample vocabulary.", + "reviewer_guidance": "Keep scanner/test coverage facts separate from repository capability truth.", + "agentic_guidance": "Reject or downgrade candidates that turn examples and fixtures into owned utility.", + "examples": [ + "Schema examples mentioning model providers should not create native model-provider capabilities." + ] + } + ] +} diff --git a/docs/schemas/quality-criteria-registry.schema.json b/docs/schemas/quality-criteria-registry.schema.json new file mode 100644 index 0000000..71c5c5e --- /dev/null +++ b/docs/schemas/quality-criteria-registry.schema.json @@ -0,0 +1,105 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://example.local/repo-scoping/quality-criteria-registry.schema.json", + "title": "Repository Scoping Quality Criteria Registry", + "type": "object", + "required": [ + "schema_version", + "criteria_version", + "status", + "updated_at", + "criteria" + ], + "properties": { + "schema_version": { + "const": "quality-criteria-registry/v1" + }, + "criteria_version": { + "type": "string", + "minLength": 1 + }, + "status": { + "enum": ["draft", "active", "deprecated"] + }, + "updated_at": { + "type": "string", + "minLength": 1 + }, + "criteria": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": [ + "id", + "title", + "category", + "severity", + "applies_to", + "description", + "deterministic_action", + "deterministic_action_when", + "reviewer_guidance" + ], + "properties": { + "id": { + "type": "string", + "pattern": "^RREG-QC-[0-9]{3}$" + }, + "title": { + "type": "string", + "minLength": 1 + }, + "category": { + "type": "string", + "minLength": 1 + }, + "severity": { + "enum": ["low", "medium", "high", "critical"] + }, + "applies_to": { + "type": "array", + "minItems": 1, + "items": { + "enum": ["ability", "capability", "feature", "evidence"] + } + }, + "description": { + "type": "string", + "minLength": 1 + }, + "deterministic_action": { + "enum": [ + "pass", + "requires_review", + "downgraded", + "rejected", + "invalidated", + "merged", + "flagged" + ] + }, + "deterministic_action_when": { + "type": "string", + "minLength": 1 + }, + "reviewer_guidance": { + "type": "string", + "minLength": 1 + }, + "agentic_guidance": { + "type": "string" + }, + "examples": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "additionalProperties": false + } + } + }, + "additionalProperties": false +} diff --git a/src/repo_registry/acceptance/__init__.py b/src/repo_registry/acceptance/__init__.py new file mode 100644 index 0000000..b7ae45c --- /dev/null +++ b/src/repo_registry/acceptance/__init__.py @@ -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", +] diff --git a/src/repo_registry/acceptance/criteria.py b/src/repo_registry/acceptance/criteria.py new file mode 100644 index 0000000..7edc09c --- /dev/null +++ b/src/repo_registry/acceptance/criteria.py @@ -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', '')}" + ) + 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', '')} 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', '')} 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], + ) diff --git a/src/repo_registry/cli.py b/src/repo_registry/cli.py index 6745308..50c3b3e 100644 --- a/src/repo_registry/cli.py +++ b/src/repo_registry/cli.py @@ -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, diff --git a/src/repo_registry/self_scoping/assessment.py b/src/repo_registry/self_scoping/assessment.py index a270f19..4a3e3bb 100644 --- a/src/repo_registry/self_scoping/assessment.py +++ b/src/repo_registry/self_scoping/assessment.py @@ -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( diff --git a/src/repo_registry/web_api/app.py b/src/repo_registry/web_api/app.py index ac2f125..d8df7b3 100644 --- a/src/repo_registry/web_api/app.py +++ b/src/repo_registry/web_api/app.py @@ -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, diff --git a/src/repo_registry/web_api/schemas.py b/src/repo_registry/web_api/schemas.py index 6ed66b9..05089e1 100644 --- a/src/repo_registry/web_api/schemas.py +++ b/src/repo_registry/web_api/schemas.py @@ -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 diff --git a/tests/test_cli.py b/tests/test_cli.py index 8967cf2..aae4620 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -156,6 +156,32 @@ def test_compare_assessment_cli_writes_markdown_report(tmp_path): assert "Route LLM Requests Across Providers" in report +def test_list_quality_criteria_cli_writes_json(tmp_path): + output_path = tmp_path / "criteria.json" + + exit_code = main( + [ + "list-quality-criteria", + "--output", + str(output_path), + "--format", + "json", + ] + ) + + registry = json.loads(output_path.read_text(encoding="utf-8")) + assert exit_code == 0 + assert registry["criteria_version"] == "repo-scoping-quality-criteria/v1" + assert {criterion["id"] for criterion in registry["criteria"]} >= { + "RREG-QC-002", + "RREG-QC-005", + } + assert all( + criterion["deterministic_action"] != "approve" + for criterion in registry["criteria"] + ) + + def test_self_assess_cli_exports_challenger_and_comparison(tmp_path): source = write_repo(tmp_path) golden_path = tmp_path / "golden.json" diff --git a/tests/test_quality_criteria.py b/tests/test_quality_criteria.py new file mode 100644 index 0000000..23a4993 --- /dev/null +++ b/tests/test_quality_criteria.py @@ -0,0 +1,51 @@ +from repo_registry.acceptance import ( + active_quality_criteria_version, + criteria_registry_markdown, + load_quality_criteria, +) + + +def test_quality_criteria_registry_is_versioned_and_reviewable(): + registry = load_quality_criteria() + + assert registry.schema_version == "quality-criteria-registry/v1" + assert registry.criteria_version == "repo-scoping-quality-criteria/v1" + assert registry.status == "active" + assert {criterion.id for criterion in registry.criteria} == { + "RREG-QC-001", + "RREG-QC-002", + "RREG-QC-003", + "RREG-QC-004", + "RREG-QC-005", + "RREG-QC-006", + } + for criterion in registry.criteria: + assert criterion.description + assert criterion.severity in {"low", "medium", "high", "critical"} + assert criterion.deterministic_action in { + "pass", + "requires_review", + "downgraded", + "rejected", + "invalidated", + "merged", + "flagged", + } + assert criterion.deterministic_action != "approve" + assert criterion.deterministic_action_when + assert criterion.reviewer_guidance + + +def test_quality_criteria_markdown_lists_transparent_review_guidance(): + registry = load_quality_criteria() + + markdown = criteria_registry_markdown(registry) + + assert "# Quality Criteria Registry" in markdown + assert "RREG-QC-002: Native Utility Is Repo-Owned" in markdown + assert "Deterministic action: `downgraded`" in markdown + assert "Reviewer guidance:" in markdown + + +def test_active_quality_criteria_version_matches_registry(): + assert active_quality_criteria_version() == "repo-scoping-quality-criteria/v1" diff --git a/tests/test_self_scoping_assessment_export.py b/tests/test_self_scoping_assessment_export.py index a73e677..f2d0cb3 100644 --- a/tests/test_self_scoping_assessment_export.py +++ b/tests/test_self_scoping_assessment_export.py @@ -52,6 +52,10 @@ def test_export_assessment_artifact_binds_analysis_to_engine_identity(tmp_path): assert artifact["target_repository"]["repo_slug"] == "exportable-repo" assert artifact["target_repository"]["target_commit"] assert artifact["engine_identity"]["engine_commit"] + assert ( + artifact["engine_identity"]["quality_criteria_version"] + == "repo-scoping-quality-criteria/v1" + ) assert artifact["engine_identity"]["release_binding_status"] == "complete" assert artifact["assessment"]["comparison_eligibility"] == "eligible" assert artifact["execution"]["mode"] == "deterministic-only" @@ -95,3 +99,31 @@ def test_export_assessment_artifact_flags_known_provider_regression(tmp_path): item["path"] == "providers.py" for item in artifact["fact_summary"]["contamination_sources"] ) is False + + +def test_export_assessment_review_decisions_include_quality_criteria_version(tmp_path): + service = make_service(tmp_path) + source = write_repo(tmp_path) + repository = service.register_repository( + name="Review Criteria Repo", + url=str(source), + ) + summary = service.analyze_repository( + repository.id, + use_llm_assistance=False, + ) + service.approve_candidate_graph(repository.id, summary.analysis_run.id) + + artifact = export_assessment_artifact( + service, + repository.id, + summary.analysis_run.id, + role="challenger", + outcome="challenger", + reviewer="test", + ) + + assert artifact["review_decisions"] + assert artifact["review_decisions"][0]["quality_criteria_version"] == ( + "repo-scoping-quality-criteria/v1" + ) diff --git a/tests/test_web_api.py b/tests/test_web_api.py index 437cb88..7c5f451 100644 --- a/tests/test_web_api.py +++ b/tests/test_web_api.py @@ -115,6 +115,26 @@ def test_openapi_groups_agent_facing_endpoints(): ) +def test_quality_criteria_api_lists_active_registry(): + client = TestClient(app) + + response = client.get("/quality-criteria") + + assert response.status_code == 200 + payload = response.json() + assert payload["schema_version"] == "quality-criteria-registry/v1" + assert payload["criteria_version"] == "repo-scoping-quality-criteria/v1" + assert {criterion["id"] for criterion in payload["criteria"]} >= { + "RREG-QC-001", + "RREG-QC-002", + "RREG-QC-005", + } + assert all( + criterion["deterministic_action"] != "approve" + for criterion in payload["criteria"] + ) + + def test_openapi_contract_snapshot_for_stable_agent_paths(): client = TestClient(app) @@ -164,6 +184,12 @@ def test_openapi_contract_snapshot_for_stable_agent_paths(): "post": {"tags": ["discovery"], "success_schema": "CapabilityGapResponse"} }, "/health": {"get": {"tags": ["health"], "success_schema": "object"}}, + "/quality-criteria": { + "get": { + "tags": ["review"], + "success_schema": "QualityCriteriaRegistryResponse", + } + }, "/repos": { "get": {"tags": ["repositories"], "success_schema": "list[RepositoryResponse]"}, "post": {"tags": ["repositories"], "success_schema": "RepositoryResponse"}, diff --git a/workplans/RREG-WP-0014-agentic-characteristic-acceptance.md b/workplans/RREG-WP-0014-agentic-characteristic-acceptance.md index 456eae1..865b270 100644 --- a/workplans/RREG-WP-0014-agentic-characteristic-acceptance.md +++ b/workplans/RREG-WP-0014-agentic-characteristic-acceptance.md @@ -67,7 +67,7 @@ that locks in the boundary language. ```task id: RREG-WP-0014-T02 -status: todo +status: done priority: high state_hub_task_id: "101998a4-8cf8-4df0-8d05-c4e2041c0cac" ``` @@ -98,6 +98,15 @@ Acceptance criteria: - Criteria can be listed through CLI and/or API. - Assessment and review records include the criteria version used. +Implementation note 2026-05-15: added +`docs/quality-criteria/acceptance-quality-criteria.v1.json` and schema +documentation with active criteria version `repo-scoping-quality-criteria/v1`. +Added `src/repo_registry/acceptance/criteria.py`, the +`repo-scoping list-quality-criteria` CLI command, `GET /quality-criteria`, and +threaded the active criteria version into self-scoping assessment engine +identity. Focused tests cover the registry, CLI, API, and assessment export +version binding. + ## T03: Implement Deterministic Quality Gate Outcomes ```task