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. """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 The recorded location of a snippet is the source of truth for its code and
snippet is the source of truth for its code and the stored body is a cache; the stored body is a cache; this module is the seam that lets the SERVER read
this module is the seam that lets the SERVER read that source of truth, so the that source of truth, so the cache can be refreshed at pull time, drift can be
cache can be refreshed at pull time (step 5), drift can be flagged from push flagged from push webhooks, and coverage can be measured.
webhooks (step 6), and coverage can be measured (step 7).
Design constraints, in force everywhere below: 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 per-user keyring rows resolved by repo host on the PROJECT OWNER's
keyring; `get_forges()` returns an empty selector when the owner has keyring; `get_forges()` returns an empty selector when the owner has
nothing configured, and every consumer must treat that as "keep today's nothing configured, and every consumer must treat that as "behave as if
behavior". A user who never configures a forge is not degraded — that is there were no forge". A user who never configures a forge is not degraded — that is
the baseline. the baseline.
- READ-ONLY by construction. The adapter exposes reads; there is no write - 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 method to misuse. The token an operator mints for it only ever needs read
scope, and the docs say so. 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 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. it speculatively.
- Repo identity is the repo-binding key — `normalize_repo_key`'s - 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 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") FORGE_KINDS = ("gitea", "github")
# Total budget per forge call. Consumers either have a cache to fall back to # 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. # hung socket, and there is no retry: the fallback IS the retry policy.
_TIMEOUT = httpx.Timeout(5.0) _TIMEOUT = httpx.Timeout(5.0)
# Archive downloads move a whole-repo tarball and only ever run off the # 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 # the per-file reads — but still a bound, because a hung background task
# holds a connection slot as surely as a foreground one. # holds a connection slot as surely as a foreground one.
_ARCHIVE_TIMEOUT = httpx.Timeout(60.0) _ARCHIVE_TIMEOUT = httpx.Timeout(60.0)
@@ -79,13 +78,13 @@ class ForgeFile:
"""One file read from the forge at a specific point in history.""" """One file read from the forge at a specific point in history."""
content: str 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 commit_sha: str
path: str path: str
def host_of(url: str) -> 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() return (urlsplit(url).hostname or "").lower()
@@ -199,7 +198,7 @@ class ForgeAdapter:
async def latest_commit(self, repo: str, path: str, ref: str = "") -> str: async def latest_commit(self, repo: str, path: str, ref: str = "") -> str:
"""The newest commit touching ``path`` — "" when it can't be told. """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 already names a commit, this one small call can prove the file
hasn't moved since — no content transfer, which is what keeps hasn't moved since — no content transfer, which is what keeps
pull-time freshness inside GitHub's rate limits. pull-time freshness inside GitHub's rate limits.
@@ -213,7 +212,7 @@ class ForgeAdapter:
async def archive(self, repo: str, ref: str) -> bytes: async def archive(self, repo: str, ref: str) -> bytes:
"""The repo's content at ``ref`` as a gzipped tarball, in one request. """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 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 only shape that scales past toy repos. Callers must never run this in
a request path — it moves the whole repo. a request path — it moves the whole repo.
@@ -287,8 +286,8 @@ class GiteaForge(ForgeAdapter):
class GitHubForge(ForgeAdapter): class GitHubForge(ForgeAdapter):
"""The GitHub implementation — the second one, which is the point (#2693): """The GitHub implementation — the second one, which is the point: it
it proves the seam is a contract rather than a Gitea-shaped hole. Works 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 against github.com and GitHub Enterprise; the token is a fine-grained PAT
with Contents: Read-only (or a classic token with `repo` read).""" with Contents: Read-only (or a classic token with `repo` read)."""
@@ -379,8 +378,8 @@ def build_adapter(
) -> ForgeAdapter | None: ) -> ForgeAdapter | None:
"""One validated adapter from raw connection values, or None. """One validated adapter from raw connection values, or None.
None means "this connection cannot serve reads" — the same contract the None means "this connection cannot serve reads", decided per keyring
old instance-wide lookup had, applied per keyring row. Misconfigurations row. Misconfigurations
are logged, never raised: a bad row must not break the reads the good are logged, never raised: a bad row must not break the reads the good
rows can still serve. rows can still serve.
""" """
@@ -403,13 +402,13 @@ def build_adapter(
@dataclass(frozen=True) @dataclass(frozen=True)
class ForgeSelector: 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() 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 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 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 treats exactly as "no forge configured". An empty selector is the
IS rule #115's baseline. baseline every install starts from, not a degraded mode.
""" """
adapters: tuple[ForgeAdapter, ...] = () adapters: tuple[ForgeAdapter, ...] = ()
@@ -431,7 +430,7 @@ async def get_forges(
) -> ForgeSelector: ) -> ForgeSelector:
"""The forge selector for reads on behalf of ``owner_id``'s records. """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 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 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 user's token. Pass the record's ``project_id`` so the per-project pin