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,
|
||||
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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
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": {
|
||||
"get": {"tags": ["scope"], "success_schema": None}
|
||||
},
|
||||
"/repos/{repo_slug}/scope/context": {
|
||||
"get": {
|
||||
"tags": ["scope"],
|
||||
"success_schema": "RepositoryScopeContextResponse",
|
||||
}
|
||||
},
|
||||
"/repos/{repo_slug}/scope/diff": {
|
||||
"get": {"tags": ["scope"], "success_schema": "object"}
|
||||
},
|
||||
|
||||
@@ -1,152 +1,178 @@
|
||||
---
|
||||
id: RREG-WP-0012
|
||||
type: workplan
|
||||
domain: custodian
|
||||
title: "Activity-Core Scope Context API"
|
||||
domain: capabilities
|
||||
repo: repo-scoping
|
||||
status: active
|
||||
state_hub_workstream_id: 83a43ab6-d566-41e0-afd7-f2833812564f
|
||||
tasks:
|
||||
- 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
|
||||
owner: codex
|
||||
topic_slug: foerster-capabilities
|
||||
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
|
||||
and instructions. One of its context sources is repo-scoping: given a repo slug,
|
||||
it queries repo-scoping for the repo's capability profile (tags, capabilities,
|
||||
whether a SCOPE.md exists, etc.).
|
||||
This is a contract workplan. The goal is a stable JSON interface for
|
||||
activity-core, not new scope analysis functionality.
|
||||
|
||||
This workplan ensures repo-scoping exposes a stable, documented context API
|
||||
endpoint that activity-core can call reliably. It is a **contract workplan** —
|
||||
the goal is a stable interface, not new functionality.
|
||||
## Contract Decision
|
||||
|
||||
## 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`
|
||||
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.
|
||||
Add a new provider-side context endpoint instead:
|
||||
|
||||
See: `docs/adr/adr-001-event-bridge-architecture.md` in activity-core, section
|
||||
on context resolution adapters.
|
||||
```text
|
||||
GET /repos/{repo_slug}/scope/context
|
||||
```
|
||||
|
||||
## Scope
|
||||
|
||||
**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:
|
||||
The endpoint returns `application/json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"repo_slug": "string",
|
||||
"capabilities": ["string"],
|
||||
"tags": ["string"],
|
||||
"repo_slug": "repo-scoping",
|
||||
"capabilities": ["Generate SCOPE.md"],
|
||||
"tags": ["api", "generation", "scope"],
|
||||
"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 |
|
||||
| `capabilities` | string[] | yes | Declared capability types |
|
||||
| `tags` | string[] | yes | Free-form classification tags |
|
||||
| `scope_md_exists` | boolean | yes | True if SCOPE.md present in repo root |
|
||||
| `scope_summary` | string or null | no | First paragraph of SCOPE.md if present |
|
||||
| `repo_slug` | string | yes | Slug requested by the caller after normal slugification |
|
||||
| `capabilities` | string[] | yes | Approved capability names from the repository ability map |
|
||||
| `tags` | string[] | yes | Stable, sorted union of approved ability and capability `primary_class` values plus their `attributes` |
|
||||
| `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 | 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
|
||||
activity-core agent before deployment.
|
||||
The `scope_md_exists` field is intentionally conservative. The provider must
|
||||
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
|
||||
|
||||
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
|
||||
## T01: Define Context Query Response Schema
|
||||
|
||||
```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
|
||||
|
||||
1. `GET /repos/{slug}/scope` returns all five required fields
|
||||
2. `scope_md_exists` is accurate for at least three test repos
|
||||
3. JSON Schema file committed to `docs/schemas/`
|
||||
4. Contract test passes in CI
|
||||
5. activity-core agent confirmed the `RepoScopingContextResolver` integration works
|
||||
- `GET /repos/{repo_slug}/scope/context` returns the five-field JSON contract.
|
||||
- Existing `GET /repos/{repo_slug}/scope` Markdown preview behavior is unchanged.
|
||||
- `docs/schemas/repo-scope-context-response.json` exists.
|
||||
- Provider-side contract tests pass in CI.
|
||||
- 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
|
||||
|
||||
- The local agent implementing this should coordinate with the activity-core agent
|
||||
to confirm the exact field names and types before implementing T02.
|
||||
- Do **not** add dynamic operational state to this endpoint (e.g., last_commit_at,
|
||||
open_issues_count) — that belongs to the state hub context adapter, not repo-scoping.
|
||||
- `scope_summary` is optional (null OK) — do not block on extracting it if the
|
||||
SCOPE.md parsing is complex.
|
||||
- Dynamic operational state such as last commit time, open issues, active tasks,
|
||||
or runtime health belongs to the State Hub context adapter, not this endpoint.
|
||||
- This endpoint should be cheap and deterministic. It must not trigger scans,
|
||||
clones, LLM calls, or SCOPE.md generation.
|
||||
- Backward-compatible additions can be optional fields. Required field changes
|
||||
need coordination with activity-core.
|
||||
|
||||
## Change History
|
||||
|
||||
- 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