Compare commits
2
Commits
502ecf6743
...
29fd64f67e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
29fd64f67e | ||
|
|
16367b02d2 |
@@ -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
|
||||
|
||||
@@ -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>)": (
|
||||
|
||||
Reference in New Issue
Block a user