feat(retrieval): a tuned number carries the space it was measured in (#4104)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 10s
CI & Build / integration (push) Successful in 48s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / Python tests (push) Failing after 1m4s
CI & Build / Build & push image (push) Skipped

Milestone 416 step 6. A retrieval floor is a cosine similarity, which only
means something inside one embedding model's geometry over documents cut one
particular way. Change either and every floor on the install keeps applying
while describing nothing — and nothing anywhere says so, because the scores
simply come out different and the bar goes on cutting.

`CHUNKER_VERSION` already solved this for documents: stamped per row, so the
backfill re-embeds precisely what is stale. The same idea, applied to the
numbers:

- `calibration_stamp()` — embedding model + document shape, one definition.
  TWO fields, never a fused string (rule 149): a mismatch has to say WHICH half
  moved, because they call for different responses.
- `retrieval_tuning_events` gains `embedding_model` / `shape_version`
  (migration 0104), stamped on every write. Nullable and NOT backfilled —
  "unstamped" is the honest answer for a row written before this existed, and
  it reports as `stale: null`, never as fine.
- `current_settings` reports calibration per dial: tuned rows from their event,
  untouched dials from the registry default's own stamp.
- `retrieval_surfaces` and the Settings panel show the mismatch. The panel
  renders ONLY when something is stale, so seeing it at all is the signal.
- `migrate_floor` / `migrate_retrieval_floor` answers "a path for thresholds to
  be inherited by the next model so that they don't have to recalibrate a lot":
  the raw cosine cannot cross models, but the PERCENTILE it represented can.
  Measure what fraction of a surface's logged calls the old floor admitted,
  re-score those queries under the current model, take the value admitting the
  same fraction. Dry run by default; applying writes an ordinary tuning event
  with the arithmetic in its reason.

Nothing auto-retunes. A stale stamp says a number is no longer a measurement;
it does not say what the number should be, and #4102 measured the one case
where the statistic and the correct action pointed opposite ways.

The load-bearing test is an ABSENCE: no chat-model identifier may appear
anywhere in the calibration path. Claude produces none of these scores, so a
Claude upgrade must trigger nothing — a false alarm here teaches the operator
to ignore the real one on the day bge-small becomes bge-base.

Backup v17 carries both columns, unfilled on the way out and on the way back:
a round trip must not turn "we don't know" into a stated fact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01821k5B3Ysecp9fNYs92Kuy
This commit is contained in:
2026-09-17 12:45:48 -04:00
co-authored by Claude Opus 5
parent dcf800ed65
commit aee24c9c1c
13 changed files with 1002 additions and 2 deletions
+74
View File
@@ -5,6 +5,7 @@ bar did; these say what the bar IS, and let it be changed with the argument
attached.
"""
from scribe.mcp._context import current_user_id
from scribe.services import retrieval_migration as migration_svc
from scribe.services import retrieval_tuning as tuning_svc
@@ -37,6 +38,33 @@ async def retrieval_surfaces() -> dict:
are starting points: they were measured against one corpus with one
embedding model and cannot be right for another install by construction.
That is why this tool exists rather than a better set of defaults.
`calibration` says what SPACE each number was chosen in, per dial, and
whether that space has moved. A floor is a cosine distance in one embedding
model's geometry over documents cut one particular way; swap the model and
every score shifts at once, re-cut the documents and the same record embeds
different text. Either way the number is a measurement of something that no
longer exists, and no amount of telemetry will say so — the scores simply
come out different and the bar keeps applying.
Read the three fields apart:
- `stale: true` with `model_changed` — the geometry is new. Every floor on
this install needs re-measuring, not adjusting; a number that meant "quite
similar" in the old space means nothing particular in this one.
- `stale: true` with `shape_changed` — the documents are cut differently.
The scale still holds, but what a record embeds has changed, so which
records clear a bar has.
- `stale: null`, `source: "unstamped"` — the dial was moved before Scribe
recorded this. It was measured under SOMETHING and there is no way to say
what, which is a different answer from "it is fine". Treat it as worth
re-measuring, and the next tuning call stamps it.
NOTHING IS RETUNED AUTOMATICALLY on the strength of this, here or anywhere.
A stale stamp says a number is no longer a measurement; it does not say what
the number should be. That judgement wants the same procedure as any other
tuning change — `retrieval_telemetry(near_miss_samples=5)`, open the records
it names, then `tune_retrieval` with what you read in the reason.
"""
return {"surfaces": await tuning_svc.current_settings(current_user_id())}
@@ -109,7 +137,53 @@ async def retrieval_tuning_history(surface: str = "", limit: int = 20) -> dict:
}
async def migrate_retrieval_floor(
surface: str, sample: int = 200, apply: bool = False,
) -> dict:
"""Carry one surface's floor across an embedding-model or chunker change.
Reach for this when `retrieval_surfaces` reports a dial `stale` — and only
then. A floor is a cosine similarity, so a new embedding model moves every
score on the install at once and the stored number silently stops describing
anything. Re-deriving six floors by reading telemetry is the work this
avoids.
WHAT IT TRANSFERS. Not the number — the SELECTIVITY. A floor's real content
is a decision about how much of what an arm sees is worth spending attention
on, and that decision survives a change of units. This measures what fraction
of the surface's recent calls the old floor admitted, re-scores those same
queries under the current model, and proposes the value that admits the same
fraction.
DRY RUN BY DEFAULT. With `apply=False` it returns the arithmetic and writes
nothing. Read it before applying: the sample sizes, both score ranges, and
whether the shift looks like a change of scale or like a corpus that has not
finished re-embedding. A model change is the moment every number is
uncertain at once, which is the worst moment to let a statistic move dials
unattended.
AND IT IS STILL A STARTING POINT. Percentile-preserving means the new floor
is as good as the old one was, not better — if the old floor was wrong, this
faithfully carries the wrongness onto the new scale. It is the number to
begin from while the surface collects enough calls to judge properly, which
is `retrieval_telemetry(near_miss_samples=5)` and `tune_retrieval` as usual.
Args:
surface: the surface name from `retrieval_surfaces`.
sample: how many recent logged calls to re-score, default 200. Each one
is an embedding plus a scan, so this is real work; a surface with
only a handful of logged calls gives a percentile made of noise.
apply: False (default) returns the proposal. True writes it, as an
ordinary tuning event with the arithmetic in its reason — so a
migrated floor is reviewable and revertible like any other.
"""
return await migration_svc.migrate_floor(
current_user_id(), surface, sample=sample, apply=apply,
)
def register(mcp) -> None:
mcp.tool(name="retrieval_surfaces")(retrieval_surfaces)
mcp.tool(name="migrate_retrieval_floor")(migrate_retrieval_floor)
mcp.tool(name="tune_retrieval")(tune_retrieval)
mcp.tool(name="retrieval_tuning_history")(retrieval_tuning_history)