docs(6205): forge comments state the reason, not the instance that taught it
CI & Build / Python lint (push) Successful in 2s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 55s
CI & Build / integration (push) Successful in 1m22s
CI & Build / Python tests (push) Successful in 2m11s
CI & Build / Build & push image (push) Successful in 25s

The module header drops the milestone/step map and record citations; the
per-user optionality, the keyring model and the empty-selector baseline are
stated as present-tense facts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 20:26:18 -04:00
co-authored by Claude Opus 5.5
parent 16367b02d2
commit 29fd64f67e
+23 -24
View File
@@ -1,25 +1,24 @@
"""Forge adapter — optional server-side READ access to the operator's git forge.
Step 4 of milestone 288 (#2689, decision #2686). The recorded location of a
snippet is the source of truth for its code and the stored body is a cache;
this module is the seam that lets the SERVER read that source of truth, so the
cache can be refreshed at pull time (step 5), drift can be flagged from push
webhooks (step 6), and coverage can be measured (step 7).
The recorded location of a snippet is the source of truth for its code and
the stored body is a cache; this module is the seam that lets the SERVER read
that source of truth, so the cache can be refreshed at pull time, drift can be
flagged from push webhooks, and coverage can be measured.
Design constraints, in force everywhere below:
- OPTIONAL per user (rule #115, sharpened by #2778). Connections are
- OPTIONAL per user. Connections are
per-user keyring rows resolved by repo host on the PROJECT OWNER's
keyring; `get_forges()` returns an empty selector when the owner has
nothing configured, and every consumer must treat that as "keep today's
behavior". A user who never configures a forge is not degraded — that is
nothing configured, and every consumer must treat that as "behave as if
there were no forge". A user who never configures a forge is not degraded — that is
the baseline.
- READ-ONLY by construction. The adapter exposes reads; there is no write
method to misuse. The token an operator mints for it only ever needs read
scope, and the docs say so.
- The contract stays as small as its consumers (steps 5-7): read_file /
- The contract stays as small as its consumers: read_file /
latest_commit / archive / default_branch / resolve_repo / check. Two
implementations (Gitea, GitHub — step 8) keep it honest; resist widening
implementations (Gitea, GitHub) keep it honest; resist widening
it speculatively.
- Repo identity is the repo-binding key — `normalize_repo_key`'s
host/owner/repo — so the join between a snippet's recorded repo and the
@@ -53,12 +52,12 @@ logger = logging.getLogger(__name__)
FORGE_KINDS = ("gitea", "github")
# Total budget per forge call. Consumers either have a cache to fall back to
# (step 5) or a user watching a button (the test probe) — neither tolerates a
# or a user watching a button (the test probe) — neither tolerates a
# 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
# request path (coverage recompute), 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)
@@ -79,13 +78,13 @@ class ForgeFile:
"""One file read from the forge at a specific point in history."""
content: str
# The commit the content was served at — what provenance stores (#2688).
# The commit the content was served at — what provenance stores.
commit_sha: str
path: str
def host_of(url: str) -> str:
"""The lowercase hostname of a URL — the keyring's lookup key (#2778)."""
"""The lowercase hostname of a URL — the keyring's lookup key."""
return (urlsplit(url).hostname or "").lower()
@@ -199,7 +198,7 @@ class ForgeAdapter:
async def latest_commit(self, repo: str, path: str, ref: str = "") -> str:
"""The newest commit touching ``path`` — "" when it can't be told.
The cached-SHA short-circuit (#2693): when a snippet's provenance
The cached-SHA short-circuit: when a snippet's provenance
already names a commit, this one small call can prove the file
hasn't moved since — no content transfer, which is what keeps
pull-time freshness inside GitHub's rate limits.
@@ -213,7 +212,7 @@ class ForgeAdapter:
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
Coverage measurement 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.
@@ -287,8 +286,8 @@ class GiteaForge(ForgeAdapter):
class GitHubForge(ForgeAdapter):
"""The GitHub implementation — the second one, which is the point (#2693):
it proves the seam is a contract rather than a Gitea-shaped hole. Works
"""The GitHub implementation — the second one, which is the point: it
proves the seam is a contract rather than a Gitea-shaped hole. Works
against github.com and GitHub Enterprise; the token is a fine-grained PAT
with Contents: Read-only (or a classic token with `repo` read)."""
@@ -379,8 +378,8 @@ def build_adapter(
) -> ForgeAdapter | None:
"""One validated adapter from raw connection values, or None.
None means "this connection cannot serve reads" — the same contract the
old instance-wide lookup had, applied per keyring row. Misconfigurations
None means "this connection cannot serve reads", decided per keyring
row. Misconfigurations
are logged, never raised: a bad row must not break the reads the good
rows can still serve.
"""
@@ -403,13 +402,13 @@ def build_adapter(
@dataclass(frozen=True)
class ForgeSelector:
"""The forge reads available to one project owner (#2778).
"""The forge reads available to one project owner.
Consumers ask it to serve a REPO, not to hand over "the forge": resolve()
walks the owner's adapters and returns the (adapter, api_repo) pair for
the first one whose host serves the repo — or None, which every consumer
treats exactly as the old "no forge configured" state. An empty selector
IS rule #115's baseline.
treats exactly as "no forge configured". An empty selector is the
baseline every install starts from, not a degraded mode.
"""
adapters: tuple[ForgeAdapter, ...] = ()
@@ -431,7 +430,7 @@ async def get_forges(
) -> ForgeSelector:
"""The forge selector for reads on behalf of ``owner_id``'s records.
The keyring model (#2778): every server-side forge read for a record runs
The keyring model: every server-side forge read for a record runs
on the PROJECT OWNER's connections, resolved by repo host — a forge token
is a user's credential, and one user's reads must never ride another
user's token. Pass the record's ``project_id`` so the per-project pin