scope refactoring

This commit is contained in:
2026-05-01 01:47:14 +02:00
parent fc725ec65f
commit b8eb744e79
5 changed files with 118 additions and 10 deletions

View File

@@ -257,6 +257,11 @@ def _resolve_path(repo: dict) -> str:
return ""
def resolve_repo_path(repo: dict) -> str:
"""Resolve the repo path using the same host-aware rules as DoI checks."""
return _resolve_path(repo)
def _get_sync(api_base: str, path: str, params: dict | None = None) -> object:
url = f"{api_base}{path}"
if params:

View File

@@ -13,7 +13,12 @@ from sqlalchemy.ext.asyncio import AsyncSession
from api.config import settings
from api.database import get_session
from api.doi_engine import compute_fingerprint, evaluate as _doi_evaluate, evaluate_scope_health
from api.doi_engine import (
compute_fingerprint,
evaluate as _doi_evaluate,
evaluate_scope_health,
resolve_repo_path,
)
from api.models.doi_cache import DOICache
from api.models.domain import Domain
from api.models.interface_change import InterfaceChange
@@ -31,6 +36,7 @@ from api.schemas.managed_repo import (
RepoDispatch,
RepoPathRegister,
RepoRead,
RepoScopeHealth,
RepoUpdate,
ScopeIssueDetail,
)
@@ -346,6 +352,53 @@ async def get_repo_by_id(
return repo
@router.get("/scope-health", response_model=list[RepoScopeHealth])
async def list_repo_scope_health(
needs_review: bool | None = None,
reachable_only: bool = False,
session: AsyncSession = Depends(get_session),
) -> list[RepoScopeHealth]:
"""Return machine-readable SCOPE.md health for active repos.
Repo-scoping uses this to refresh only repos and SCOPE.md sections that
need attention, without guessing from free-text DoI output.
"""
result = await session.execute(
select(ManagedRepo, Domain.slug)
.join(Domain, Domain.id == ManagedRepo.domain_id)
.where(ManagedRepo.status == "active")
.order_by(ManagedRepo.slug)
)
entries: list[RepoScopeHealth] = []
for repo, domain_slug in result.all():
repo_dict = _repo_doi_dict(repo, domain_slug)
resolved_path = resolve_repo_path(repo_dict)
scope_issue_details = [
ScopeIssueDetail(**issue)
for issue in evaluate_scope_health(repo_dict)
]
scope_needs_review = any(
issue.id in {"C5a", "C5b", "C5c"} and issue.status in {"fail", "warn"}
for issue in scope_issue_details
)
entry = RepoScopeHealth(
repo_slug=repo.slug,
domain_slug=domain_slug,
local_path=resolved_path or repo.local_path,
path_available=bool(resolved_path),
scope_needs_review=scope_needs_review,
scope_issue_details=scope_issue_details,
)
if needs_review is not None and entry.scope_needs_review != needs_review:
continue
if reachable_only and not entry.path_available:
continue
entries.append(entry)
return entries
@router.get("/{slug}", response_model=RepoRead)
async def get_repo(
slug: str,
@@ -496,15 +549,7 @@ async def get_repo_dispatch(
domain_obj = domain_result.scalar_one_or_none()
scope_issue_details = [
ScopeIssueDetail(**issue)
for issue in evaluate_scope_health({
"slug": repo.slug,
"domain_slug": domain_obj.slug if domain_obj else None,
"local_path": repo.local_path,
"remote_url": repo.remote_url,
"host_paths": repo.host_paths or {},
"last_sbom_at": str(repo.last_sbom_at) if repo.last_sbom_at else None,
"updated_at": str(repo.updated_at) if repo.updated_at else "",
})
for issue in evaluate_scope_health(_repo_doi_dict(repo, domain_obj.slug if domain_obj else None))
]
scope_needs_review = any(
issue.id in {"C5a", "C5b", "C5c"} and issue.status in {"fail", "warn"}
@@ -576,3 +621,15 @@ async def _get_repo_by_slug(slug: str, session: AsyncSession) -> ManagedRepo:
if repo is None:
raise HTTPException(status_code=404, detail=f"Repo '{slug}' not found")
return repo
def _repo_doi_dict(repo: ManagedRepo, domain_slug: str | None) -> dict:
return {
"slug": repo.slug,
"domain_slug": domain_slug,
"local_path": repo.local_path,
"remote_url": repo.remote_url,
"host_paths": repo.host_paths or {},
"last_sbom_at": str(repo.last_sbom_at) if repo.last_sbom_at else None,
"updated_at": str(repo.updated_at) if repo.updated_at else "",
}

View File

@@ -98,3 +98,12 @@ class RepoDispatch(BaseModel):
scope_needs_review: bool
scope_issue_details: list[ScopeIssueDetail]
last_state_synced_at: datetime | None
class RepoScopeHealth(BaseModel):
repo_slug: str
domain_slug: str | None = None
local_path: str | None = None
path_available: bool
scope_needs_review: bool
scope_issue_details: list[ScopeIssueDetail]