Compare commits

...
2 Commits
Author SHA1 Message Date
bvandeusenandClaude Opus 5.5 29fd64f67e 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>
2026-10-21 20:26:18 -04:00
bvandeusenandClaude Opus 5.5 16367b02d2 docs(6205): retrieval_registry comments state the reason, not the instance that taught it
Record and milestone citations go. The logs_unconditionally docstring, the
place arm's comment and the fan-out key note state the condition they guard
rather than the bug that once hit it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:25:48 -04:00
2 changed files with 50 additions and 53 deletions
+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
+27 -29
View File
@@ -1,12 +1,12 @@
"""Every point that retrieves or surfaces a record, declared (#3431, #3432).
"""Every point that retrieves or surfaces a record, declared.
WHY A DECLARED LIST WHEN THE ROWS ALREADY NAME THEIR SOURCE. Because the rows
can only describe arms that FIRED. A surface that emits nothing writes no row,
so it is indistinguishable from a surface that does not exist — and the two
call for opposite responses. `retrieval_telemetry` could report perfect health
for a window in which half the arms never ran. #3430 hit exactly this: its
"silent surface" finding could only be made by a human who happened to know
which arms were supposed to be there.
for a window in which half the arms never ran, and a "silent surface"
finding could only be made by a human who happened to know which arms were
supposed to be there.
This module is that knowledge, written down. It carries the two facts the rows
structurally cannot:
@@ -16,7 +16,7 @@ structurally cannot:
for a week is an arm that has lost the ability to speak. Same row shape,
opposite meanings, and only the caller's intent separates them.
* **expected to emit, or deliberately quiet with a reason.** A justified
silence must not read as a gap (#2475). `rest_*` sources fire only when a
silence must not read as a gap. `rest_*` sources fire only when a
person opens the web UI; on an install driven entirely through MCP their
absence is correct and permanent, and reporting it every window would
train the reader to ignore the list that also carries the real ones.
@@ -31,15 +31,14 @@ reserved slots belong in it precisely because they are judgeable without being
tunable. One is a control panel; the other is an inventory. A test asserts
every tunable surface also appears here, so the two cannot drift apart.
ADDING A RANKED ARM MEANS WRITING ITS SPEC. Since milestone 456 step 5 the
ADDING A RANKED ARM MEANS WRITING ITS SPEC. The
ranked rows are read off `retrieval_pipeline.RANKED`, each spec declaring its
own (`Declared`). Any OTHER point — a lookup, an asked search, an ambient
carrier, a pull — still means adding a row here. `tests/test_retrieval_registry.py`
walks the call sites with the ast module and fails on a source it cannot find
below — deliberately not a grep, because two of the sources in this file
(`wide_net`, `preference_slot`) reach their recorder through a
module constant and a grep for `source="` misses both. That is the
narrowing #3191 warns about, caught here in the act.
module constant and a grep for `source="` misses both.
"""
from __future__ import annotations
@@ -89,7 +88,7 @@ class Point:
False means "this can be quiet forever and that is correct" — and then
`quiet_because` must say why, so a reader meets a decision rather than a
hole (#2475).
hole.
"""
quiet_because: str = ""
@@ -97,11 +96,11 @@ class Point:
logs_unconditionally: bool = True
"""Whether this arm writes a row even when it returns NOTHING.
THE #3497 GUARD, and the reason this field is not merely documentation.
Both rule arms once logged only their hits, so their zero-result count was
structurally 0 — and a "cannot decline" warning computed over that would
have fired on a LOGGING BUG while reporting a ranking problem, sending the
reader to move a floor that was never involved. An arm whose logging has
The reason this field is not merely documentation: an arm that logs only
its hits has a zero-result count that is structurally 0, and a "cannot
decline" warning computed over that would fire on a LOGGING BUG while
reporting a ranking problem, sending the reader to move a floor that was
never involved. An arm whose logging has
not been confirmed unconditional is not eligible for that warning; it is
excluded from the check rather than trusted.
"""
@@ -114,8 +113,7 @@ def _p(source, kind, what, **kw) -> tuple[str, Point]:
POINTS: dict[str, Point] = dict([
# ── Unbidden push arms ───────────────────────────────────────────────
#
# The RANKED ones are read off the pipeline's specs (milestone 456 step
# 5): every arm, and every stage that runs a query of its own and records
# The RANKED ones are read off the pipeline's specs: every arm, and every stage that runs a query of its own and records
# under its own name, declares its row where it is defined. What stays
# here is what no spec describes — the lookups below, and everything asked,
# ambient or pulled.
@@ -123,7 +121,7 @@ POINTS: dict[str, Point] = dict([
expects_traffic=not spec.declared.quiet_because,
quiet_because=spec.declared.quiet_because)
for spec in RANKED),
# A LOOKUP, not a ranker (#4796): a record the operator named by number
# A LOOKUP, not a ranker: a record the operator named by number
# in the message, fetched by id. No score and no bar, so it writes no
# retrieval_logs row and no score-shaped warning can apply; its rows are
# in `note_usage_events`, where pull-through says whether naming a record
@@ -133,7 +131,7 @@ POINTS: dict[str, Point] = dict([
expects_traffic=False,
quiet_because="speaks only when a message names a record by its id; "
"a window in which none did is correctly silent here"),
# The moment arm (milestone 458). A LOOKUP, not a ranker: a rule mounted on
# The moment arm. A LOOKUP, not a ranker: a rule mounted on
# a moment arrives when that moment happens, with no score, so it writes no
# retrieval_logs row and no score-shaped warning can apply. Its rows are in
# `rule_usage_events`, each carrying the moment in `detail`.
@@ -142,11 +140,11 @@ POINTS: dict[str, Point] = dict([
expects_traffic=False,
quiet_because="speaks only once some rule is mounted on a moment; an "
"install where none is mounted is correctly silent here"),
# The rulings arm (milestone 444). A LOOKUP, not a ranker: a path falls
# The rulings arm. A LOOKUP, not a ranker: a path falls
# under a System's patterns or it does not, so nothing here writes to
# retrieval_logs and no score-shaped warning can apply. Its rows are in
# `system_usage_events`, read back as `retrieval_telemetry`'s
# `system_usage` block (#4769).
# `system_usage` block.
_p("rulings_pre_tool", UNBIDDEN,
"an area's rulings, shown because a command touched its files",
expects_traffic=False,
@@ -179,9 +177,9 @@ POINTS: dict[str, Point] = dict([
# ── The write-path menu's own arms ───────────────────────────────────
#
# These are NOT in retrieval_logs. The place arm carries no score and so
# has no row there at all (#2085) — before it was split out, the arm
# firing on the strongest possible claim, "there is already a canonical
# helper in this exact file", was the one arm nobody could measure.
# has no row there at all; declaring it here is what lets the arm making
# the strongest possible claim, "there is already a canonical helper in
# this exact file", be measured.
_p("write_path_place", AMBIENT, "a snippet already placed in this very file"),
_p("write_path_semantic", AMBIENT, "prior art matched by meaning"),
_p("write_path_sync", AMBIENT,
@@ -225,12 +223,12 @@ POINTS: dict[str, Point] = dict([
# out would not fail here. What catches that is `assert_registered`, which the
# recorders call as the row is written.
# Keyed by the EXACT string the extractor produces, matched by equality rather
# than by prefix. A prefix match was the first spelling and it silently matched
# nothing, because the paths are relative to `src/` and so begin `scribe/` —
# every site sailed through a check that looked like it was running.
# than by prefix. The paths are relative to `src/` and so begin `scribe/`; a
# prefix spelled any other way silently matches nothing, and every site sails
# through a check that looks like it is running.
FAN_OUT_SITES: dict[str, tuple[str, ...]] = {
# The write path's two LOOKUPS. Its ranked arm (`write_path_semantic`)
# records through the pipeline's site below (milestone 456 step 4).
# records through the pipeline's site below.
"scribe/services/plugin_context.py::record_surfaced(source=arm)": (
"write_path_place", "write_path_sync",
),
@@ -241,7 +239,7 @@ FAN_OUT_SITES: dict[str, tuple[str, ...]] = {
"enter_project", "start_planning", "get_task", "get_project",
"get_milestone",
),
# The one retrieval pipeline (milestone 456): every ranked arm's call row
# The one retrieval pipeline: every ranked arm's call row
# and surfacing rows — both corpora — are written by these sites, so
# `source` arrives as a value. The values are the pipeline's own specs,
# read rather than restated, so an arm added there is declared here by
@@ -255,7 +253,7 @@ FAN_OUT_SITES: dict[str, tuple[str, ...]] = {
"scribe/services/retrieval_pipeline.py::record_rule_surfaced(source=source)": (
tuple(arm.source for arm in RULE_ARMS)
),
# The reply moment's mounted half (milestone 458): it records under the
# The reply moment's mounted half: it records under the
# moment arm's source, read from the pipeline's constant so the two doors
# cannot drift apart on the name.
"scribe/services/moment_delivery.py::record_rule_surfaced(source=<expression>)": (