docs(6206): retrieval, snippets, notes routes and rule_usage model drop citations
Corrected while reading: routes/notes.py said lessons and snippets pass the owner to set_record_systems, "a real inconsistency". Every door passes the caller; the comment now says so. rule_usage's model docstring states what an outcome measures and why the row cannot share the note table, without the measurement and record ids that motivated it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,16 +7,15 @@ from scribe.models.base import CreatedAtMixin, iso
|
||||
SURFACED = "surfaced"
|
||||
PULLED = "pulled"
|
||||
|
||||
# The outcome half (#4212, milestone 419). A rule that was read and then
|
||||
# ignored has always been indistinguishable from one that was read and
|
||||
# obeyed; these are the two events that can tell them apart.
|
||||
# The outcome half. Without these, a rule that was read and then ignored is
|
||||
# indistinguishable from one that was read and obeyed; these are the two
|
||||
# events that can tell them apart.
|
||||
#
|
||||
# There is deliberately NO third value for "read and ignored". That state is
|
||||
# real and is the whole point of the milestone, but it cannot be reported:
|
||||
# an agent that knew it was ignoring a rule would not be ignoring it. It is
|
||||
# real and is the point of measuring outcomes, but it cannot be reported: an
|
||||
# agent that knew it was ignoring a rule would not be ignoring it. It is
|
||||
# DERIVED — a pull with no outcome — and an enum member for it would collect
|
||||
# nothing while reading as though it had measured something, which is #3311's
|
||||
# failure exactly.
|
||||
# nothing while reading as though it had measured something.
|
||||
APPLIED = "applied"
|
||||
DEPARTED = "departed"
|
||||
OUTCOMES = (APPLIED, DEPARTED)
|
||||
@@ -26,15 +25,14 @@ class RuleUsageEvent(Base, CreatedAtMixin):
|
||||
"""One row per time a rule was SURFACED to the agent, PULLED in full, or
|
||||
ACTED ON — applied, or departed from with a stated reason.
|
||||
|
||||
The sibling `note_usage_events` has had since 2026-07, third in the line
|
||||
after `rule_embeddings` and `rule_versions` — and, like those, it exists
|
||||
because the rule side kept inheriting machinery built for notes and
|
||||
quietly getting the weaker version of it.
|
||||
The rule-side sibling of `note_usage_events`, beside `rule_embeddings`
|
||||
and `rule_versions` — each exists because machinery built for notes gives
|
||||
rules the weaker version of it.
|
||||
|
||||
WHY RULES NEED THEIR OWN AND CANNOT SHARE THE NOTE TABLE. Not squeamishness
|
||||
about a polymorphic column — the row shares no note-specific fields and the
|
||||
aggregate readout is the same shape, which is the strongest case for
|
||||
sharing that note #3163 admits. What decides it is IDENTITY AT RESTORE. A
|
||||
sharing there is. What decides it is IDENTITY AT RESTORE. A
|
||||
note id and a rule id are different namespaces resolved through different
|
||||
maps, and `note_usage_events`'s importer maps `note_id` through
|
||||
`note_id_map` and drops what does not resolve. A rule id parked in that
|
||||
@@ -42,11 +40,10 @@ class RuleUsageEvent(Base, CreatedAtMixin):
|
||||
happened to take that number — telemetry that is not merely lost but wrong,
|
||||
and wrong in a way nothing downstream could detect.
|
||||
|
||||
WHAT THIS MEASURES, AND WHY IT DID NOT EXIST. The write-path standing-rule
|
||||
arm is the only retrieval surface in Scribe whose usefulness cannot be
|
||||
observed — and, not coincidentally, the only one that has never declined to
|
||||
fire (#3311: 296 calls, zero zero-result, 100% clearing its threshold).
|
||||
`retrieval_logs` gives it scores; scores say what the ranker thought, never
|
||||
WHAT THIS MEASURES. A standing-rule arm's usefulness cannot be observed
|
||||
from its scores: an arm can fire on every call, clearing its threshold
|
||||
every time, and say nothing about whether it helped. `retrieval_logs`
|
||||
gives it scores; scores say what the ranker thought, never
|
||||
whether the hint landed. Without a pull counter no install can tune the arm
|
||||
from evidence, only from the shape of a histogram.
|
||||
|
||||
@@ -58,7 +55,7 @@ class RuleUsageEvent(Base, CreatedAtMixin):
|
||||
forty times and opened never, because that evidence is precisely the case
|
||||
for having deleted it.
|
||||
|
||||
Cells left deliberately empty (note #3163's step 3): no share ACL — rules
|
||||
Cells left deliberately empty: no share ACL — rules
|
||||
have none of their own; no soft delete — nothing restores a telemetry row,
|
||||
and the table is append-only; no embedding — an event is not a document.
|
||||
"""
|
||||
@@ -76,7 +73,7 @@ class RuleUsageEvent(Base, CreatedAtMixin):
|
||||
# 'surfaced' | 'pulled' | 'applied' | 'departed'
|
||||
#
|
||||
# Plain Text with no CHECK, as created in 0094 — which is why 0106 added
|
||||
# the outcome pair without a DROP/ADD migration. Rule 36 governs
|
||||
# the outcome pair without a DROP/ADD migration — that step is for
|
||||
# CHECK-whitelisted columns and this is not one. Said here as well as in
|
||||
# the migration because this is where the next person adding a value will
|
||||
# look first.
|
||||
@@ -84,9 +81,9 @@ class RuleUsageEvent(Base, CreatedAtMixin):
|
||||
|
||||
# Which surface produced it. A CONVENTION, not a fixed vocabulary, and the
|
||||
# note twin's comment explains why this one deliberately does not enumerate
|
||||
# its members: the previous such list went stale, naming a source nothing
|
||||
# wrote while omitting ones that existed, and a half-true enumeration reads
|
||||
# as authoritative in exactly the way that misleads (#2476).
|
||||
# its members: such a list goes stale, naming a source nothing writes while
|
||||
# omitting ones that exist, and a half-true enumeration reads as
|
||||
# authoritative in exactly the way that misleads.
|
||||
# `grep -rn record_rule_pulled\|record_rule_surfaced src/` is the
|
||||
# authoritative list, and unlike a comment it cannot drift.
|
||||
#
|
||||
|
||||
+11
-15
@@ -131,10 +131,8 @@ async def create_note_route():
|
||||
# 403, not 400: the request is well-formed and the caller simply
|
||||
# may not write the target. The note itself was created.
|
||||
return jsonify({"error": str(exc), "note": note.to_dict()}), 403
|
||||
# #4249: this door could not file a note to a System at all, while the
|
||||
# MCP door could — the reverse of the asymmetry the same issue records for
|
||||
# lessons. The pattern is not that one door is richer; it is that the door
|
||||
# nobody exercised for a kind is the one that never grew the parameter.
|
||||
# Both doors file a note to Systems: the door nobody exercises for a kind
|
||||
# is the one that never grows the parameter.
|
||||
if data.get("system_ids") is not None:
|
||||
await systems_svc.set_record_systems(uid, note.id, data["system_ids"])
|
||||
out = note.to_dict()
|
||||
@@ -235,7 +233,7 @@ async def get_note_route(note_id: int):
|
||||
# join away, but which surface asked is not recoverable after the fact.
|
||||
# Keeping rest_* apart from mcp_* is load-bearing, not tidiness — "was that
|
||||
# injected line useful?" is answered by agent pulls alone, and a human
|
||||
# clicking a link would inflate exactly the number #1038 and #2085 gate on.
|
||||
# clicking a link would inflate exactly the number pruning gates on.
|
||||
record_pulled(user_id=uid, note_id=note_id, source="rest_note")
|
||||
await supersession_svc.attach_relations(uid, note_id, data)
|
||||
return jsonify(data)
|
||||
@@ -246,8 +244,8 @@ async def get_note_route(note_id: int):
|
||||
async def update_note_route(note_id: int):
|
||||
"""Partial update — only the keys present in the payload change. PUT and
|
||||
PATCH are the same handler on purpose: the form sends the field set it
|
||||
edited, and the two verbs used to be two near-identical copies of this
|
||||
function that drifted (one carried the supersedes contract, one did not)."""
|
||||
edited, and two copies of this function would drift (one carrying the
|
||||
supersedes contract, one not)."""
|
||||
uid = get_current_user_id()
|
||||
# Share-aware: resolve through the ACL and write as the OWNER, so a shared
|
||||
# editor's save isn't rejected by the owner-scoped update service.
|
||||
@@ -265,7 +263,7 @@ async def update_note_route(note_id: int):
|
||||
"milestone_id", "status", "priority", "note_type",
|
||||
# A cleared form input arrives as "" and the service reads that as
|
||||
# NULL (NULLABLE_NOTE_TEXT), so this door expresses "remove the check"
|
||||
# with its own idiom and needs no `clear` list (milestone 317).
|
||||
# with its own idiom and needs no `clear` list.
|
||||
"verify_with", "expires_when",
|
||||
):
|
||||
if key in data:
|
||||
@@ -298,12 +296,10 @@ async def update_note_route(note_id: int):
|
||||
return jsonify({"error": str(exc)}), 403
|
||||
# Set-semantics like the above: present-and-empty clears, absent leaves
|
||||
# alone. Scoped by the CALLER rather than owner_uid, for the same reason
|
||||
# the supersedes call is (#47) — `set_record_systems` links only Systems
|
||||
# the acting user can read, and an editor-share holder should tag from
|
||||
# what they can see rather than inherit the owner's reach. This matches
|
||||
# routes/tasks.py; routes/lessons.py and routes/snippets.py pass the owner
|
||||
# for the same operation, which is a real inconsistency, recorded in #4249
|
||||
# rather than changed here.
|
||||
# the supersedes call is — `set_record_systems` links only Systems the
|
||||
# acting user can read, and an editor-share holder should tag from what
|
||||
# they can see rather than inherit the owner's reach. Every door that
|
||||
# tags a record (tasks, lessons, snippets) does the same.
|
||||
if "system_ids" in data:
|
||||
await systems_svc.set_record_systems(uid, note_id, data["system_ids"] or [])
|
||||
out = note.to_dict()
|
||||
@@ -521,7 +517,7 @@ async def graph_route():
|
||||
return jsonify(graph)
|
||||
|
||||
|
||||
# ── The staleness sweep (milestone 317) ──────────────────────────────────────
|
||||
# ── The staleness sweep ──────────────────────────────────────────────────────
|
||||
# The web half of notes_due_for_verification / mark_note_verified. Same
|
||||
# contract as the MCP door and the rules routes beside it — the service holds
|
||||
# the behaviour, these two just parse and serialise.
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
"""The operator's view of retrieval tuning — what the dials are, and who moved them (#4102).
|
||||
"""The operator's view of retrieval tuning — what the dials are, and who moved them.
|
||||
|
||||
WHY THIS EXISTS SEPARATELY FROM /api/settings
|
||||
|
||||
The numbers themselves are ordinary settings rows and could be written through
|
||||
the generic KV endpoint. They must not be, and that is the whole point of this
|
||||
blueprint: a floor changed through `/api/settings` moves silently, leaving the
|
||||
history saying nothing happened. From milestone 416 those dials are moved by
|
||||
the model on the operator's behalf, so a trail with holes in it is worse than
|
||||
history saying nothing happened. Those dials are moved by the model on the
|
||||
operator's behalf, so a trail with holes in it is worse than
|
||||
no trail — it reads as complete.
|
||||
|
||||
So every change to a retrieval dial goes through `set_dial`, from the UI as
|
||||
@@ -39,7 +39,7 @@ retrieval_bp = Blueprint("retrieval", __name__, url_prefix="/api/retrieval")
|
||||
# behalf. The operator IS that someone: they are the audience the trail is
|
||||
# written for, they cannot be uninformed about their own decision, and a
|
||||
# mandatory justification textarea on every control would be friction charged
|
||||
# to the one participant who owes no explanation (rule 24).
|
||||
# to the one participant who owes no explanation.
|
||||
#
|
||||
# The event is still written, because "the operator set this by hand" is the
|
||||
# single most useful thing the history can tell a later session — it is the one
|
||||
@@ -116,12 +116,12 @@ async def tuning_history_route():
|
||||
@retrieval_bp.route("/moments", methods=["GET"])
|
||||
@login_required
|
||||
async def moments_route():
|
||||
"""The moments of work that rules mount on (milestone 458), with the
|
||||
"""The moments of work that rules mount on, with the
|
||||
actions that reach each one on this install.
|
||||
|
||||
The same payload `list_moments` returns, from the same services, so the
|
||||
session and the Settings view cannot name different moments — plus what
|
||||
the view shows beside each one (step 6): `mounted`, how many rules each
|
||||
the view shows beside each one: `mounted`, how many rules each
|
||||
carries, and `usage`, the deliveries and opens per moment over `days`
|
||||
(the block `retrieval_telemetry` reports as `moment_usage`).
|
||||
"""
|
||||
@@ -147,11 +147,11 @@ async def moments_route():
|
||||
@retrieval_bp.route("/reply-shapes", methods=["GET"])
|
||||
@login_required
|
||||
async def reply_shapes_route():
|
||||
"""The default reply shapes and the moment each rides (milestone 500).
|
||||
"""The default reply shapes and the moment each rides.
|
||||
|
||||
`list_reply_shapes`' payload from the same service, so the Settings view
|
||||
and the session cannot show different defaults — plus what only the
|
||||
operator's view needs (step 5): per shape, the preferences mounted on its
|
||||
operator's view needs: per shape, the preferences mounted on its
|
||||
moment and its deliveries over `days` (default 30, 1–90).
|
||||
"""
|
||||
try:
|
||||
@@ -201,7 +201,7 @@ async def unmap_action_route():
|
||||
@retrieval_bp.route("/moments/proposals", methods=["GET"])
|
||||
@login_required
|
||||
async def moment_proposals_route():
|
||||
"""The moment proposals waiting on a judgment (milestone 458 step 7) —
|
||||
"""The moment proposals waiting on a judgment —
|
||||
`rule_moment_proposals`' payload, from the same service. `?rule_id=`
|
||||
narrows to one rule, for the rule editor."""
|
||||
try:
|
||||
|
||||
@@ -5,7 +5,7 @@ routes feed the web management UI; the MCP tools (mcp/tools/snippets.py) are the
|
||||
agent-facing surface. Both go through services/snippets.py, so the
|
||||
serialize/parse contract and embedding-on-create live in one place (DRY).
|
||||
|
||||
ACL (rule #78): reads/writes of a single snippet resolve through the share-aware
|
||||
ACL: reads/writes of a single snippet resolve through the share-aware
|
||||
`get_note_for_user` + `can_write_note`, and writes are performed as the OWNER so
|
||||
a shared editor isn't rejected by the owner-scoped service — mirroring
|
||||
routes/notes.py. The list is owner-scoped, matching the note-browse surface.
|
||||
@@ -65,7 +65,7 @@ async def list_snippets_route():
|
||||
repo = request.args.get("repo", "")
|
||||
path = request.args.get("path", "")
|
||||
symbol = request.args.get("symbol", "")
|
||||
# Drift check (#2086). "attention" is what the UI's filter chip sends.
|
||||
# Drift check. "attention" is what the UI's filter chip sends.
|
||||
verification = request.args.get("verification", "")
|
||||
limit, offset = parse_pagination()
|
||||
items, total = await snippets_svc.list_snippets(
|
||||
@@ -115,7 +115,7 @@ async def create_snippet_route():
|
||||
is_task=False,
|
||||
note_type=snippets_svc.SNIPPET_NOTE_TYPE,
|
||||
# Matched on the artefact — location and code — before prose, the
|
||||
# same way the MCP create path does (#2518). Both surfaces must
|
||||
# same way the MCP create path does. Both surfaces must
|
||||
# apply the identical gate or the web UI becomes the way to record
|
||||
# a duplicate the agent would have been stopped from writing.
|
||||
code=data.get("code", ""),
|
||||
@@ -163,7 +163,7 @@ async def get_snippet_route(snippet_id: int):
|
||||
return not_found("Snippet")
|
||||
note, permission = loaded
|
||||
data = snippets_svc.snippet_to_dict(note)
|
||||
# Forge-checked freshness (#2690) — same decoration the MCP pull gets.
|
||||
# Forge-checked freshness — same decoration the MCP pull gets.
|
||||
await snippets_svc.attach_live_body(note, data)
|
||||
data["permission"] = permission
|
||||
# Read the association as the OWNER: a shared reader isn't scoped to the
|
||||
@@ -177,7 +177,7 @@ async def get_snippet_route(snippet_id: int):
|
||||
await attach_usage([data])
|
||||
# Opening the detail view IS a pull — the operator chose to look. Tagged
|
||||
# apart from the MCP sources so "the agent reused it" and "a human read it"
|
||||
# stay distinguishable; they mean different things for pruning (#2085).
|
||||
# stay distinguishable; they mean different things for pruning.
|
||||
record_pulled(user_id=uid, note_id=snippet_id, source="rest_snippet")
|
||||
return jsonify(data)
|
||||
|
||||
@@ -212,7 +212,7 @@ async def update_snippet_route(snippet_id: int):
|
||||
if updated is None:
|
||||
return not_found("Snippet")
|
||||
if data.get("system_ids") is not None:
|
||||
# The CALLER, not owner_uid — see routes/lessons.py for the why (#4249).
|
||||
# The CALLER, not owner_uid — see routes/lessons.py for the why.
|
||||
await systems_svc.set_record_systems(uid, snippet_id, data["system_ids"])
|
||||
out = snippets_svc.snippet_to_dict(updated)
|
||||
out["systems"] = [
|
||||
|
||||
Reference in New Issue
Block a user