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
119 lines
5.7 KiB
Python
119 lines
5.7 KiB
Python
from datetime import datetime, timezone
|
|
|
|
from sqlalchemy import DateTime, Float, Index, Integer, Text
|
|
from sqlalchemy.orm import Mapped, mapped_column
|
|
|
|
from scribe.models import Base
|
|
|
|
|
|
class RetrievalTuningEvent(Base):
|
|
"""One row per change to a retrieval surface's floor or budget (#4102).
|
|
|
|
WHY A TABLE AND NOT JUST THE SETTING
|
|
|
|
The setting holds the number. It cannot hold the argument, and from this
|
|
step onward the argument is the interesting part: the operator's decision is
|
|
that the model moves these values, *"9 times out of 10"*, without being
|
|
asked each time. Delegation like that is only safe if it leaves a trail the
|
|
operator can read afterwards — what changed, from what to what, and on what
|
|
evidence — and can disagree with.
|
|
|
|
So `reason` is not decoration and not optional. It is the guardrail: a model
|
|
that has to state why is a model that has to have looked, and a reason that
|
|
turns out to be wrong is a reason someone can point at. A bare number in a
|
|
settings row records that something moved and nothing about whether it
|
|
should have.
|
|
|
|
WHY `actor` IS NOT JUST `user_id`
|
|
|
|
Both a person and a model act as the same user — the MCP tools run under the
|
|
operator's own id — so the id cannot distinguish them. That distinction is
|
|
exactly what an operator scanning this table wants first: "did I do this, or
|
|
did the session?" A change the operator made themselves needs no review; one
|
|
made on their behalf might.
|
|
|
|
FK-FREE ON user_id, like `RetrievalLog` and `AppLog` above it: the history of
|
|
how a surface was tuned should outlive the account that tuned it, and a
|
|
deleted user must not cascade away the record of why a number is where it is.
|
|
"""
|
|
|
|
__tablename__ = "retrieval_tuning_events"
|
|
|
|
id: Mapped[int] = mapped_column(primary_key=True)
|
|
# Declared here rather than via CreatedAtMixin because the index below
|
|
# orders on `created_at.desc()`, which needs the column object in this class
|
|
# body — a mixin's column is not in scope there.
|
|
created_at: Mapped[datetime] = mapped_column(
|
|
DateTime(timezone=True), default=lambda: datetime.now(timezone.utc)
|
|
)
|
|
user_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
# The surface's registry name, which is also its `retrieval_logs.source`.
|
|
# That identity is what lets a reader put a change next to what the change
|
|
# did; `tests/test_retrieval_surfaces.py` asserts it rather than trusting it.
|
|
surface: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
# "floor" or "budget". Text rather than an enum because rule 36 makes a new
|
|
# CHECK value a migration, and this is a young surface that may well grow a
|
|
# third tunable — a constraint bought nothing here except a future migration.
|
|
dial: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
# Both stored as float so one pair of columns serves both dials; a budget is
|
|
# a whole number and reads back as one. `old_value` is nullable because the
|
|
# first change to a surface has no stored predecessor — it moved off the
|
|
# shipped starting point, which is a different event from moving off a value
|
|
# somebody chose, and worth being able to tell apart.
|
|
old_value: Mapped[float | None] = mapped_column(Float, nullable=True)
|
|
new_value: Mapped[float] = mapped_column(Float, nullable=False)
|
|
|
|
# "model" | "human". See the class docstring: the user id cannot tell these
|
|
# apart, and the difference is the first thing a reviewer wants.
|
|
actor: Mapped[str] = mapped_column(Text, nullable=False, default="model")
|
|
|
|
# REQUIRED at the service boundary, not merely non-null here. A column that
|
|
# is technically non-null is satisfied by "", which is how a required field
|
|
# becomes a formality; the service refuses a blank one.
|
|
reason: Mapped[str] = mapped_column(Text, nullable=False)
|
|
|
|
# WHAT SPACE THIS NUMBER WAS MEASURED IN (#4104). A floor is a cosine
|
|
# distance in one embedding model's geometry, over documents cut one
|
|
# particular way — change either and the number describes something that
|
|
# no longer exists.
|
|
#
|
|
# On the EVENT rather than beside the setting, because this table already
|
|
# holds one row per change and `current_settings` already reads the latest
|
|
# per dial. A parallel stamp row next to the value would be a second place
|
|
# to keep in sync, and the two disagreeing is worse than neither.
|
|
#
|
|
# Both NULLABLE for the rows written before this step: those were measured
|
|
# under something, but claiming to know which would be inventing a fact.
|
|
# Null here means "unstamped", which is a different and honest answer from
|
|
# naming a model that may be wrong.
|
|
embedding_model: Mapped[str | None] = mapped_column(Text, nullable=True)
|
|
shape_version: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
|
|
|
__table_args__ = (
|
|
# The only read this table has: one surface's history, newest first.
|
|
Index(
|
|
"ix_retrieval_tuning_surface_created",
|
|
"surface",
|
|
created_at.desc(),
|
|
),
|
|
)
|
|
|
|
def to_dict(self) -> dict:
|
|
return {
|
|
"id": self.id,
|
|
"created_at": self.created_at.isoformat() if self.created_at else None,
|
|
"surface": self.surface,
|
|
"dial": self.dial,
|
|
"old_value": self.old_value,
|
|
"new_value": self.new_value,
|
|
"actor": self.actor,
|
|
"reason": self.reason,
|
|
# Null for every row written before #4104, and rendered as
|
|
# "unstamped" rather than guessed at — see the column comments.
|
|
"embedding_model": self.embedding_model,
|
|
"shape_version": self.shape_version,
|
|
}
|