generated from coulomb/repo-seed
Add activity-core scope context API
This commit is contained in:
59
docs/schemas/repo-scope-context-response.json
Normal file
59
docs/schemas/repo-scope-context-response.json
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
"$id": "https://repo-scoping.local/schemas/repo-scope-context-response.json",
|
||||||
|
"title": "Repository Scope Context Response",
|
||||||
|
"description": "Provider-side context contract returned by GET /repos/{repo_slug}/scope/context for activity-core.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": [
|
||||||
|
"repo_slug",
|
||||||
|
"capabilities",
|
||||||
|
"tags",
|
||||||
|
"scope_md_exists",
|
||||||
|
"scope_summary"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"repo_slug": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "Slug requested by the caller after normal slugification.",
|
||||||
|
"examples": ["repo-scoping"]
|
||||||
|
},
|
||||||
|
"capabilities": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Approved capability names from the repository ability map.",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"examples": [["Generate SCOPE.md", "Preview generated SCOPE.md"]]
|
||||||
|
},
|
||||||
|
"tags": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "Stable, sorted union of approved ability and capability primary_class values plus their attributes.",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"examples": [["api", "generation", "scope"]]
|
||||||
|
},
|
||||||
|
"scope_md_exists": {
|
||||||
|
"type": "boolean",
|
||||||
|
"description": "True when repo-scoping can inspect a local or cached checkout and root SCOPE.md exists; false when absent or unknown.",
|
||||||
|
"examples": [true]
|
||||||
|
},
|
||||||
|
"scope_summary": {
|
||||||
|
"description": "First non-empty paragraph of root SCOPE.md when inspectable; otherwise approved scope description, repository description, or null.",
|
||||||
|
"type": ["string", "null"],
|
||||||
|
"examples": [
|
||||||
|
"Repository Scoping maps repositories into reviewable scope graphs."
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"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."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -62,6 +62,7 @@ from repo_registry.web_api.schemas import (
|
|||||||
RepositoryComparisonResponse,
|
RepositoryComparisonResponse,
|
||||||
RepositoryCreate,
|
RepositoryCreate,
|
||||||
RepositoryResponse,
|
RepositoryResponse,
|
||||||
|
RepositoryScopeContextResponse,
|
||||||
RepositoryUpdate,
|
RepositoryUpdate,
|
||||||
ReviewDecisionResponse,
|
ReviewDecisionResponse,
|
||||||
ScanSummaryResponse,
|
ScanSummaryResponse,
|
||||||
@@ -1346,6 +1347,45 @@ def export_repository_registry_entry(
|
|||||||
return PlainTextResponse(content, media_type="application/x-yaml")
|
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(
|
@app.get(
|
||||||
"/repos/{repo_slug}/scope",
|
"/repos/{repo_slug}/scope",
|
||||||
tags=["scope"],
|
tags=["scope"],
|
||||||
@@ -1413,6 +1453,97 @@ def write_repository_scope(
|
|||||||
return {"written": True, "path": str(scope_path)}
|
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(
|
def ensure_scope_generation_ready(
|
||||||
service: RegistryService,
|
service: RegistryService,
|
||||||
repo_slug: str,
|
repo_slug: str,
|
||||||
|
|||||||
@@ -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):
|
class IdResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
|
|
||||||
|
|||||||
162
tests/test_scope_context_api.py
Normal file
162
tests/test_scope_context_api.py
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from repo_registry.web_api.app import Settings, app, get_settings
|
||||||
|
|
||||||
|
|
||||||
|
def override_settings(tmp_path):
|
||||||
|
def _override():
|
||||||
|
return Settings(
|
||||||
|
database_path=str(tmp_path / "scope-context.sqlite3"),
|
||||||
|
checkout_root=str(tmp_path / "checkouts"),
|
||||||
|
state_hub_base_url="",
|
||||||
|
)
|
||||||
|
|
||||||
|
return _override
|
||||||
|
|
||||||
|
|
||||||
|
def test_scope_context_endpoint_returns_activity_core_contract(tmp_path):
|
||||||
|
scoped_source = tmp_path / "scoped-repo"
|
||||||
|
scoped_source.mkdir()
|
||||||
|
(scoped_source / "SCOPE.md").write_text(
|
||||||
|
"# SCOPE\n\n"
|
||||||
|
"> Generated context note.\n\n"
|
||||||
|
"---\n\n"
|
||||||
|
"## One-liner\n\n"
|
||||||
|
"Maps repositories into concise operating context.\n\n"
|
||||||
|
"## Notes\n\n"
|
||||||
|
"Further details stay out of the summary.\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
plain_source = tmp_path / "plain-repo"
|
||||||
|
plain_source.mkdir()
|
||||||
|
|
||||||
|
app.dependency_overrides[get_settings] = override_settings(tmp_path)
|
||||||
|
client = TestClient(app)
|
||||||
|
try:
|
||||||
|
scoped = client.post(
|
||||||
|
"/repos",
|
||||||
|
json={
|
||||||
|
"name": "Scoped Repo",
|
||||||
|
"url": str(scoped_source),
|
||||||
|
"description": "Registry fallback for scoped repo.",
|
||||||
|
},
|
||||||
|
).json()
|
||||||
|
ability_id = client.post(
|
||||||
|
f"/repos/{scoped['id']}/abilities",
|
||||||
|
json={
|
||||||
|
"name": "Context Publishing",
|
||||||
|
"description": "Expose repository context to callers.",
|
||||||
|
},
|
||||||
|
).json()["id"]
|
||||||
|
client.post(
|
||||||
|
f"/repos/{scoped['id']}/capabilities",
|
||||||
|
json={
|
||||||
|
"ability_id": ability_id,
|
||||||
|
"name": "Generate SCOPE.md",
|
||||||
|
"description": "Render repository scope from approved entries.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
client.post(
|
||||||
|
"/repos",
|
||||||
|
json={
|
||||||
|
"name": "Plain Repo",
|
||||||
|
"url": str(plain_source),
|
||||||
|
"description": "Plain repository metadata summary.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
client.post(
|
||||||
|
"/repos",
|
||||||
|
json={
|
||||||
|
"name": "Remote Repo",
|
||||||
|
"url": "https://example.test/remote-repo.git",
|
||||||
|
"description": "Remote repository metadata summary.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
scoped_context = client.get("/repos/scoped-repo/scope/context")
|
||||||
|
assert scoped_context.status_code == 200
|
||||||
|
assert scoped_context.json() == {
|
||||||
|
"repo_slug": "scoped-repo",
|
||||||
|
"capabilities": ["Generate SCOPE.md"],
|
||||||
|
"tags": ["ability", "capability"],
|
||||||
|
"scope_md_exists": True,
|
||||||
|
"scope_summary": "Maps repositories into concise operating context.",
|
||||||
|
}
|
||||||
|
|
||||||
|
plain_context = client.get("/repos/plain-repo/scope/context")
|
||||||
|
assert plain_context.status_code == 200
|
||||||
|
assert plain_context.json() == {
|
||||||
|
"repo_slug": "plain-repo",
|
||||||
|
"capabilities": [],
|
||||||
|
"tags": [],
|
||||||
|
"scope_md_exists": False,
|
||||||
|
"scope_summary": "Plain repository metadata summary.",
|
||||||
|
}
|
||||||
|
|
||||||
|
remote_context = client.get("/repos/remote-repo/scope/context")
|
||||||
|
assert remote_context.status_code == 200
|
||||||
|
assert remote_context.json() == {
|
||||||
|
"repo_slug": "remote-repo",
|
||||||
|
"capabilities": [],
|
||||||
|
"tags": [],
|
||||||
|
"scope_md_exists": False,
|
||||||
|
"scope_summary": "Remote repository metadata summary.",
|
||||||
|
}
|
||||||
|
|
||||||
|
missing_context = client.get("/repos/missing-repo/scope/context")
|
||||||
|
assert missing_context.status_code == 404
|
||||||
|
assert missing_context.json()["detail"]
|
||||||
|
|
||||||
|
markdown_scope = client.get("/repos/scoped-repo/scope")
|
||||||
|
assert markdown_scope.status_code == 200
|
||||||
|
assert markdown_scope.headers["content-type"].startswith("text/markdown")
|
||||||
|
assert "# SCOPE" in markdown_scope.text
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
|
||||||
|
def test_scope_context_schema_artifact_matches_contract_shape():
|
||||||
|
schema = json.loads(
|
||||||
|
(
|
||||||
|
Path(__file__).parents[1]
|
||||||
|
/ "docs"
|
||||||
|
/ "schemas"
|
||||||
|
/ "repo-scope-context-response.json"
|
||||||
|
).read_text(encoding="utf-8")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert schema["additionalProperties"] is False
|
||||||
|
assert schema["required"] == [
|
||||||
|
"repo_slug",
|
||||||
|
"capabilities",
|
||||||
|
"tags",
|
||||||
|
"scope_md_exists",
|
||||||
|
"scope_summary",
|
||||||
|
]
|
||||||
|
assert schema["properties"]["scope_md_exists"]["type"] == "boolean"
|
||||||
|
assert schema["properties"]["scope_summary"]["type"] == ["string", "null"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_scope_context_openapi_contract_is_exposed(tmp_path):
|
||||||
|
app.dependency_overrides[get_settings] = override_settings(tmp_path)
|
||||||
|
client = TestClient(app)
|
||||||
|
try:
|
||||||
|
schema = client.get("/openapi.json").json()
|
||||||
|
finally:
|
||||||
|
app.dependency_overrides.clear()
|
||||||
|
|
||||||
|
operation = schema["paths"]["/repos/{repo_slug}/scope/context"]["get"]
|
||||||
|
assert operation["tags"] == ["scope"]
|
||||||
|
response_schema = operation["responses"]["200"]["content"]["application/json"][
|
||||||
|
"schema"
|
||||||
|
]
|
||||||
|
assert response_schema["$ref"].endswith("/RepositoryScopeContextResponse")
|
||||||
|
assert (
|
||||||
|
"RepositoryScopeContextResponse"
|
||||||
|
in schema["components"]["schemas"]
|
||||||
|
)
|
||||||
@@ -333,6 +333,12 @@ def test_openapi_contract_snapshot_for_stable_agent_paths():
|
|||||||
"/repos/{repo_slug}/scope": {
|
"/repos/{repo_slug}/scope": {
|
||||||
"get": {"tags": ["scope"], "success_schema": None}
|
"get": {"tags": ["scope"], "success_schema": None}
|
||||||
},
|
},
|
||||||
|
"/repos/{repo_slug}/scope/context": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["scope"],
|
||||||
|
"success_schema": "RepositoryScopeContextResponse",
|
||||||
|
}
|
||||||
|
},
|
||||||
"/repos/{repo_slug}/scope/diff": {
|
"/repos/{repo_slug}/scope/diff": {
|
||||||
"get": {"tags": ["scope"], "success_schema": "object"}
|
"get": {"tags": ["scope"], "success_schema": "object"}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,152 +1,178 @@
|
|||||||
---
|
---
|
||||||
id: RREG-WP-0012
|
id: RREG-WP-0012
|
||||||
type: workplan
|
type: workplan
|
||||||
domain: custodian
|
title: "Activity-Core Scope Context API"
|
||||||
|
domain: capabilities
|
||||||
repo: repo-scoping
|
repo: repo-scoping
|
||||||
status: active
|
status: active
|
||||||
state_hub_workstream_id: 83a43ab6-d566-41e0-afd7-f2833812564f
|
owner: codex
|
||||||
tasks:
|
topic_slug: foerster-capabilities
|
||||||
- id: T01
|
|
||||||
title: Define context query response schema for activity-core
|
|
||||||
state_hub_task_id: 83482f1d-4f44-440d-a27f-0c375786b87f
|
|
||||||
status: todo
|
|
||||||
- id: T02
|
|
||||||
title: Verify/implement GET /repos/{slug}/scope endpoint
|
|
||||||
state_hub_task_id: 75f6123a-2703-42ec-9564-dd665e16c5ef
|
|
||||||
status: todo
|
|
||||||
- id: T03
|
|
||||||
title: Add scope_md_exists boolean to scope response
|
|
||||||
state_hub_task_id: cb0b17f4-30c4-4d68-b2ff-9a3fa9ef73a3
|
|
||||||
status: todo
|
|
||||||
- id: T04
|
|
||||||
title: Write integration test for activity-core repo-scoping adapter
|
|
||||||
state_hub_task_id: f70c1c58-b23f-4b61-adca-a02644b6ee7e
|
|
||||||
status: todo
|
|
||||||
created: "2026-05-14"
|
created: "2026-05-14"
|
||||||
|
updated: "2026-05-15"
|
||||||
|
state_hub_workstream_id: "83a43ab6-d566-41e0-afd7-f2833812564f"
|
||||||
---
|
---
|
||||||
|
|
||||||
# RREG-WP-0012: Activity-Core Context API
|
# Activity-Core Scope Context API
|
||||||
|
|
||||||
## Purpose
|
activity-core's `RunActivityWorkflow` resolves repository context before
|
||||||
|
evaluating rules and instructions. One of its context sources is repo-scoping:
|
||||||
|
given a repo slug, activity-core needs a compact capability profile with
|
||||||
|
capability names, classification tags, whether a `SCOPE.md` is known to exist,
|
||||||
|
and a short scope summary when available.
|
||||||
|
|
||||||
activity-core's `RunActivityWorkflow` resolves context before evaluating rules
|
This is a contract workplan. The goal is a stable JSON interface for
|
||||||
and instructions. One of its context sources is repo-scoping: given a repo slug,
|
activity-core, not new scope analysis functionality.
|
||||||
it queries repo-scoping for the repo's capability profile (tags, capabilities,
|
|
||||||
whether a SCOPE.md exists, etc.).
|
|
||||||
|
|
||||||
This workplan ensures repo-scoping exposes a stable, documented context API
|
## Contract Decision
|
||||||
endpoint that activity-core can call reliably. It is a **contract workplan** —
|
|
||||||
the goal is a stable interface, not new functionality.
|
|
||||||
|
|
||||||
## Context
|
Do not change `GET /repos/{repo_slug}/scope`. That endpoint already exists as
|
||||||
|
the generated `SCOPE.md` Markdown preview endpoint, with sibling diff/write
|
||||||
|
routes. Changing its success media type or response shape would break existing
|
||||||
|
repo-scoping API clients and the OpenAPI contract policy.
|
||||||
|
|
||||||
- **activity-core WP-0003** (in progress): implements `RepoScopingContextResolver`
|
Add a new provider-side context endpoint instead:
|
||||||
in `src/activity_core/context_resolvers/repo_scoping.py` which calls
|
|
||||||
`GET /repos/{slug}/scope` on the repo-scoping API.
|
|
||||||
- **repo-scoping** already has a scope API; the question is whether it returns
|
|
||||||
the exact fields activity-core needs, and whether those fields are stable.
|
|
||||||
|
|
||||||
See: `docs/adr/adr-001-event-bridge-architecture.md` in activity-core, section
|
```text
|
||||||
on context resolution adapters.
|
GET /repos/{repo_slug}/scope/context
|
||||||
|
```
|
||||||
|
|
||||||
## Scope
|
The endpoint returns `application/json`:
|
||||||
|
|
||||||
**In scope:**
|
|
||||||
- Documenting and stabilizing the `/repos/{slug}/scope` endpoint response schema
|
|
||||||
- Adding `scope_md_exists` if not present
|
|
||||||
- Contract/integration test
|
|
||||||
|
|
||||||
**Out of scope:**
|
|
||||||
- Dynamic operational state (that is the state hub's domain, not repo-scoping's)
|
|
||||||
- NATS-based context queries (REST only for now)
|
|
||||||
- Any new scope analysis features
|
|
||||||
|
|
||||||
## Contract
|
|
||||||
|
|
||||||
The endpoint `GET /repos/{slug}/scope` must return:
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"repo_slug": "string",
|
"repo_slug": "repo-scoping",
|
||||||
"capabilities": ["string"],
|
"capabilities": ["Generate SCOPE.md"],
|
||||||
"tags": ["string"],
|
"tags": ["api", "generation", "scope"],
|
||||||
"scope_md_exists": true,
|
"scope_md_exists": true,
|
||||||
"scope_summary": "string or null"
|
"scope_summary": "Repository Scoping maps repositories into reviewable scope graphs."
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| Field | Type | Required | Notes |
|
| Field | Type | Required | Source |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `repo_slug` | string | yes | URL-safe identifier |
|
| `repo_slug` | string | yes | Slug requested by the caller after normal slugification |
|
||||||
| `capabilities` | string[] | yes | Declared capability types |
|
| `capabilities` | string[] | yes | Approved capability names from the repository ability map |
|
||||||
| `tags` | string[] | yes | Free-form classification tags |
|
| `tags` | string[] | yes | Stable, sorted union of approved ability and capability `primary_class` values plus their `attributes` |
|
||||||
| `scope_md_exists` | boolean | yes | True if SCOPE.md present in repo root |
|
| `scope_md_exists` | boolean | yes | True when repo-scoping can inspect a local or cached checkout and root `SCOPE.md` exists; false when absent or unknown |
|
||||||
| `scope_summary` | string or null | no | First paragraph of SCOPE.md if present |
|
| `scope_summary` | string or null | yes | First non-empty paragraph of root `SCOPE.md` when inspectable; otherwise approved scope description, repository description, or null |
|
||||||
|
|
||||||
This schema is the contract. Changes must be versioned and communicated to the
|
The `scope_md_exists` field is intentionally conservative. The provider must
|
||||||
activity-core agent before deployment.
|
not clone or fetch remote repositories merely to answer this lightweight context
|
||||||
|
query. If no local or cached checkout is available, the value is `false` and
|
||||||
|
`scope_summary` falls back to registry metadata.
|
||||||
|
|
||||||
## Tasks
|
activity-core should call this new endpoint, not the Markdown preview endpoint.
|
||||||
|
If a future contract needs an unknown/absent distinction for `SCOPE.md`, add a
|
||||||
|
new optional field such as `scope_md_status` rather than changing the boolean.
|
||||||
|
|
||||||
### T01 — Define context query response schema for activity-core
|
## T01: Define Context Query Response Schema
|
||||||
|
|
||||||
Write a JSON Schema file (`docs/schemas/repo-scope-context-response.json`) that
|
|
||||||
formalizes the response shape above. Include field descriptions and examples.
|
|
||||||
This becomes the machine-readable contract between repo-scoping and activity-core.
|
|
||||||
|
|
||||||
### T02 — Verify/implement GET /repos/{slug}/scope endpoint
|
|
||||||
|
|
||||||
Check the existing API for `GET /repos/{slug}/scope`. Verify it returns all
|
|
||||||
required fields. If missing fields, implement them. If the endpoint does not
|
|
||||||
exist, implement it.
|
|
||||||
|
|
||||||
The endpoint is called by activity-core's `RepoScopingContextResolver` and
|
|
||||||
must be stable — no breaking changes without coordinating with activity-core.
|
|
||||||
|
|
||||||
### T03 — Add scope_md_exists boolean to scope response
|
|
||||||
|
|
||||||
Ensure `scope_md_exists` is present and accurate: `true` if a `SCOPE.md` file
|
|
||||||
exists in the repo's root directory on the default branch, `false` otherwise.
|
|
||||||
|
|
||||||
This field is used by activity-core rules to decide whether to emit a
|
|
||||||
generate-SCOPE.md task for a repo.
|
|
||||||
|
|
||||||
### T04 — Write integration test for activity-core repo-scoping adapter
|
|
||||||
|
|
||||||
Write a contract test that:
|
|
||||||
1. Calls `GET /repos/{slug}/scope` with a known test repo slug
|
|
||||||
2. Asserts all required fields are present
|
|
||||||
3. Asserts types match the schema (scope_md_exists is bool, capabilities is list, etc.)
|
|
||||||
|
|
||||||
This test lives in repo-scoping as the provider-side contract test. It should
|
|
||||||
run in CI so contract drift is caught before it breaks activity-core.
|
|
||||||
|
|
||||||
**Test file**: `tests/test_scope_context_api.py` (or alongside existing API tests)
|
|
||||||
|
|
||||||
## Build Order
|
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RREG-WP-0012-T01
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "83482f1d-4f44-440d-a27f-0c375786b87f"
|
||||||
```
|
```
|
||||||
T01 (schema) → T02 (implement) → T03 (add field) → T04 (test)
|
|
||||||
|
Write `docs/schemas/repo-scope-context-response.json` to formalize the response
|
||||||
|
shape above. Include field descriptions, required fields, `additionalProperties:
|
||||||
|
false`, and at least one complete example.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
- The schema describes `repo_slug`, `capabilities`, `tags`, `scope_md_exists`,
|
||||||
|
and `scope_summary`.
|
||||||
|
- Required fields match the contract table.
|
||||||
|
- The schema is easy for activity-core to vendor or validate against.
|
||||||
|
|
||||||
|
## T02: Implement Scope Context Endpoint
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RREG-WP-0012-T02
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "75f6123a-2703-42ec-9564-dd665e16c5ef"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Implement `GET /repos/{repo_slug}/scope/context` in repo-scoping. The endpoint
|
||||||
|
must return the JSON contract above and must preserve the existing
|
||||||
|
`GET /repos/{repo_slug}/scope` Markdown behavior.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
- The endpoint resolves repositories with the same slug rules as the existing
|
||||||
|
scope endpoints.
|
||||||
|
- Repositories without approved capabilities still return a valid context
|
||||||
|
response with empty `capabilities` and `tags`.
|
||||||
|
- Missing repository slugs return the existing `404` error shape.
|
||||||
|
- The OpenAPI snapshot includes the new endpoint and response model.
|
||||||
|
|
||||||
|
## T03: Resolve SCOPE.md Presence And Summary
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RREG-WP-0012-T03
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "cb0b17f4-30c4-4d68-b2ff-9a3fa9ef73a3"
|
||||||
|
```
|
||||||
|
|
||||||
|
Populate `scope_md_exists` and `scope_summary` without performing network
|
||||||
|
operations. Reuse local source paths, cached checkouts, and State Hub local path
|
||||||
|
lookup where available.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
- A local registered repo with root `SCOPE.md` returns `scope_md_exists: true`.
|
||||||
|
- A local registered repo without root `SCOPE.md` returns `scope_md_exists:
|
||||||
|
false`.
|
||||||
|
- A remote repo with no cached checkout returns `scope_md_exists: false` and a
|
||||||
|
metadata fallback summary when available.
|
||||||
|
- `scope_summary` extracts the first non-empty paragraph after headings or
|
||||||
|
frontmatter noise when a `SCOPE.md` file is inspectable.
|
||||||
|
|
||||||
|
## T04: Add Provider-Side Contract Tests
|
||||||
|
|
||||||
|
```task
|
||||||
|
id: RREG-WP-0012-T04
|
||||||
|
status: done
|
||||||
|
priority: high
|
||||||
|
state_hub_task_id: "f70c1c58-b23f-4b61-adca-a02644b6ee7e"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add `tests/test_scope_context_api.py` or equivalent focused API coverage for
|
||||||
|
the new context endpoint.
|
||||||
|
|
||||||
|
Acceptance criteria:
|
||||||
|
- Tests assert all required fields and types.
|
||||||
|
- Tests cover at least three repository cases: local with `SCOPE.md`, local
|
||||||
|
without `SCOPE.md`, and remote/uncached.
|
||||||
|
- Tests assert the existing Markdown `/scope` endpoint still returns Markdown.
|
||||||
|
- Tests assert the OpenAPI contract exposes the new response model.
|
||||||
|
|
||||||
## Completion Criteria
|
## Completion Criteria
|
||||||
|
|
||||||
1. `GET /repos/{slug}/scope` returns all five required fields
|
- `GET /repos/{repo_slug}/scope/context` returns the five-field JSON contract.
|
||||||
2. `scope_md_exists` is accurate for at least three test repos
|
- Existing `GET /repos/{repo_slug}/scope` Markdown preview behavior is unchanged.
|
||||||
3. JSON Schema file committed to `docs/schemas/`
|
- `docs/schemas/repo-scope-context-response.json` exists.
|
||||||
4. Contract test passes in CI
|
- Provider-side contract tests pass in CI.
|
||||||
5. activity-core agent confirmed the `RepoScopingContextResolver` integration works
|
- activity-core updates `RepoScopingContextResolver` to call `/scope/context`.
|
||||||
|
|
||||||
|
Implementation note 2026-05-15: repo-scoping provider work is complete. Added
|
||||||
|
`GET /repos/{repo_slug}/scope/context`, the Pydantic response model, the JSON
|
||||||
|
Schema artifact, focused provider-side contract tests, and OpenAPI snapshot
|
||||||
|
coverage. Full repo test suite passed locally with `.venv/bin/python -m pytest`
|
||||||
|
(`140 passed`). Workplan remains active until activity-core updates and confirms
|
||||||
|
its resolver.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- The local agent implementing this should coordinate with the activity-core agent
|
- Dynamic operational state such as last commit time, open issues, active tasks,
|
||||||
to confirm the exact field names and types before implementing T02.
|
or runtime health belongs to the State Hub context adapter, not this endpoint.
|
||||||
- Do **not** add dynamic operational state to this endpoint (e.g., last_commit_at,
|
- This endpoint should be cheap and deterministic. It must not trigger scans,
|
||||||
open_issues_count) — that belongs to the state hub context adapter, not repo-scoping.
|
clones, LLM calls, or SCOPE.md generation.
|
||||||
- `scope_summary` is optional (null OK) — do not block on extracting it if the
|
- Backward-compatible additions can be optional fields. Required field changes
|
||||||
SCOPE.md parsing is complex.
|
need coordination with activity-core.
|
||||||
|
|
||||||
## Change History
|
## Change History
|
||||||
|
|
||||||
- v0.1 (2026-05-14): Stub created by activity-core agent during WP-0003 planning.
|
- v0.1 (2026-05-14): Stub created by activity-core agent during WP-0003 planning.
|
||||||
Local agent to flesh out and implement.
|
- v0.2 (2026-05-15): Revised by repo-scoping agent to avoid breaking the
|
||||||
|
existing Markdown `/scope` endpoint, define `/scope/context`, and normalize
|
||||||
|
ADR-001 task blocks.
|
||||||
|
|||||||
Reference in New Issue
Block a user