Add activity-core scope context API

This commit is contained in:
2026-05-15 01:40:49 +02:00
parent 8ea0b18ac3
commit 1990f75b73
6 changed files with 542 additions and 113 deletions

View File

@@ -62,6 +62,7 @@ from repo_registry.web_api.schemas import (
RepositoryComparisonResponse,
RepositoryCreate,
RepositoryResponse,
RepositoryScopeContextResponse,
RepositoryUpdate,
ReviewDecisionResponse,
ScanSummaryResponse,
@@ -1346,6 +1347,45 @@ def export_repository_registry_entry(
return PlainTextResponse(content, media_type="application/x-yaml")
@app.get(
"/repos/{repo_slug}/scope/context",
tags=["scope"],
response_model=RepositoryScopeContextResponse,
)
def get_repository_scope_context(
repo_slug: str,
service: RegistryService = Depends(get_service),
settings: Settings = Depends(get_settings),
) -> dict[str, object]:
try:
repository = repository_by_slug(service, repo_slug)
ability_map = service.ability_map(repository.id)
except NotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
scope_path = inspectable_scope_file_path(service, repository, repo_slug, settings)
scope_md_exists = bool(scope_path and scope_path.is_file())
scope_summary = (
scope_summary_from_file(scope_path)
if scope_md_exists and scope_path is not None
else None
)
if not scope_summary:
scope_summary = (
ability_map.scope.description.strip()
or (repository.description.strip() if repository.description else None)
or None
)
return {
"repo_slug": slugify(repo_slug),
"capabilities": scope_context_capabilities(ability_map),
"tags": scope_context_tags(ability_map),
"scope_md_exists": scope_md_exists,
"scope_summary": scope_summary,
}
@app.get(
"/repos/{repo_slug}/scope",
tags=["scope"],
@@ -1413,6 +1453,97 @@ def write_repository_scope(
return {"written": True, "path": str(scope_path)}
def scope_context_capabilities(ability_map) -> list[str]:
return unique_preserving_order(
[
capability.name
for ability in ability_map.abilities
for capability in ability.capabilities
]
)
def scope_context_tags(ability_map) -> list[str]:
tags = []
for ability in ability_map.abilities:
tags.append(ability.primary_class)
tags.extend(ability.attributes)
for capability in ability.capabilities:
tags.append(capability.primary_class)
tags.extend(capability.attributes)
return sorted(unique_preserving_order(tags), key=str.lower)
def unique_preserving_order(values: list[str]) -> list[str]:
result: list[str] = []
seen: set[str] = set()
for value in values:
item = str(value).strip()
key = item.lower()
if not item or key in seen:
continue
seen.add(key)
result.append(item)
return result
def inspectable_scope_file_path(
service: RegistryService,
repository,
repo_slug: str,
settings: Settings,
) -> Path | None:
try:
state_hub_path = state_hub_scope_file_path(repo_slug, settings)
except ValueError:
state_hub_path = None
if state_hub_path is not None:
return state_hub_path
source_path = Path(repository.url)
if source_path.exists() and source_path.is_dir():
return source_path / "SCOPE.md"
checkout = service.ingestion.cached_checkout(repository.url)
if checkout is not None and checkout.source_path.exists():
return checkout.source_path / "SCOPE.md"
return None
def scope_summary_from_file(scope_path: Path) -> str | None:
try:
content = scope_path.read_text(encoding="utf-8")
except OSError:
return None
paragraph: list[str] = []
in_frontmatter = False
frontmatter_checked = False
for line in content.splitlines():
stripped = line.strip()
if not frontmatter_checked:
frontmatter_checked = True
if stripped == "---":
in_frontmatter = True
continue
if in_frontmatter:
if stripped == "---":
in_frontmatter = False
continue
if not stripped:
if paragraph:
return " ".join(paragraph)
continue
if stripped == "---" or stripped.startswith("#") or stripped.startswith(">"):
if paragraph:
return " ".join(paragraph)
continue
paragraph.append(stripped)
if paragraph:
return " ".join(paragraph)
return None
def ensure_scope_generation_ready(
service: RegistryService,
repo_slug: str,

View File

@@ -862,6 +862,51 @@ class RepositoryAbilityMapResponse(BaseModel):
}
class RepositoryScopeContextResponse(BaseModel):
repo_slug: str = Field(
description="Slug requested by the caller after normal slugification."
)
capabilities: list[str] = Field(
description="Approved capability names from the repository ability map."
)
tags: list[str] = Field(
description=(
"Sorted union of approved ability and capability primary classes "
"plus their attributes."
)
)
scope_md_exists: bool = Field(
description=(
"True when repo-scoping can inspect a local or cached checkout and "
"root SCOPE.md exists."
)
)
scope_summary: str | None = Field(
description=(
"First non-empty paragraph of root SCOPE.md when available, "
"otherwise registry metadata."
)
)
model_config = {
"extra": "forbid",
"json_schema_extra": {
"examples": [
{
"repo_slug": "repo-scoping",
"capabilities": ["Generate SCOPE.md"],
"tags": ["api", "generation", "scope"],
"scope_md_exists": True,
"scope_summary": (
"Repository Scoping maps repositories into reviewable "
"scope graphs."
),
}
]
},
}
class IdResponse(BaseModel):
id: int