feat(coverage): pattern-library coverage measurement (#2692, milestone 288 step 7)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 25s
CI & Build / TypeScript typecheck (push) Successful in 35s
CI & Build / Python tests (push) Successful in 58s
CI & Build / Build & push image (push) Successful in 41s

Server-side shape enumeration per bound repo — one archive download via the
forge adapter, definitions extracted with a Python mirror of the write-path
hook's awk rules (shared test vectors pin the two together) — compared
against recorded snippet locations by path+symbol. Summary is cached in the
settings KV with a freshness stamp; recomputed on webhook push (spawned off
the delivery path) or explicit refresh, never in a request path.

Surfaces: GET/POST /api/projects/<id>/coverage[/refresh], a project-page
card (estimate-labeled, largest-gaps chips), and a one-line evidence-carrying
entry in enter_project read from cache only.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-16 16:05:43 -04:00
co-authored by Claude Fable 5
parent 89b07f7857
commit cbccb6bd5d
8 changed files with 957 additions and 2 deletions
+22
View File
@@ -58,6 +58,12 @@ FORGE_KINDS = ("gitea",)
# hung socket, and there is no retry: the fallback IS the retry policy.
_TIMEOUT = httpx.Timeout(5.0)
# Archive downloads move a whole-repo tarball and only ever run off the
# request path (coverage recompute, step 7), so they get a bigger budget than
# the per-file reads — but still a bound, because a hung background task
# holds a connection slot as surely as a foreground one.
_ARCHIVE_TIMEOUT = httpx.Timeout(60.0)
class ForgeError(RuntimeError):
"""A forge call failed (network, auth, unexpected payload). Token-free."""
@@ -179,6 +185,22 @@ class GiteaForge:
path=payload.get("path") or path,
)
async def archive(self, repo: str, ref: str) -> bytes:
"""The repo's content at ``ref`` as a gzipped tarball, in one request.
Coverage measurement (step 7) needs every source file's text; per-file
reads would mean one API call per file, so the archive endpoint is the
only shape that scales past toy repos. Callers must never run this in
a request path — it moves the whole repo.
"""
async with self._client() as client:
resp = await self._get(
client,
f"/repos/{repo}/archive/{quote(ref, safe='')}.tar.gz",
timeout=_ARCHIVE_TIMEOUT,
)
return resp.content
async def default_branch(self, repo: str) -> str:
async with self._client() as client:
resp = await self._get(client, f"/repos/{repo}")