feat(rulings): system_usage_events is read back — per-System counts and a telemetry block (#4769)
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / integration (push) Successful in 50s
CI & Build / TypeScript typecheck (push) Successful in 52s
CI & Build / Python tests (push) Successful in 1m44s
CI & Build / Build & push image (push) Successful in 36s

#4769 "Rulings are counted where someone will read them": milestone 444
step 4 wrote system_usage_events and nothing read it.

- retrieval_telemetry gains a `system_usage` block: surfacings and opens by
  source, distinct counts, and `by_system` naming the areas most shown.
  There is deliberately no pull-through ratio, because rulings travel in full
  in the line and opens are the exception.
- usage_for_systems (one GROUP BY) adds `usage` to the REST Systems list and
  detail, and to MCP get_system. MCP list_systems is unchanged.
- The Systems UI shows a "rulings shown N×" chip.
- rulings_pre_tool, rulings_write_path and mcp_get_system are now declared
  registry points; the registry guard covers their recorders.
- The Systems store merges a PATCH reply instead of replacing the row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-03 08:49:20 -04:00
co-authored by Claude Opus 5.5
parent 1b973ebd13
commit 0a1bb68808
12 changed files with 457 additions and 4 deletions
+11 -1
View File
@@ -380,7 +380,7 @@ async def retrieval_telemetry(
— so `near_miss_samples=5` and opening the ids it returns is the step that
separates a real miss from a bar doing its job.
Three readouts, from the three tables built for them:
Four readouts, from the four tables built for them:
`sources` — per retrieval surface (`auto_inject`, `write_path`,
`mcp_search`, …), from `retrieval_logs`: `calls`, `zero_result_calls`,
@@ -529,6 +529,16 @@ It is an UPPER BOUND per surface: a pull records the door it came
the zeros were missing rather than absent (#3497). Measured since, it
declines the large majority of its calls like any other surface.
`system_usage` — the RULINGS arm (#4769), from `system_usage_events`: how
often an area's rulings were shown because a command (`rulings_pre_tool`)
or an edit (`rulings_write_path`) touched its files, how often a System
was then opened (`mcp_get_system`), and `by_system` naming the areas, most
shown first. A lookup, not a ranker, so it has no row in `sources` and no
floor to tune. And NO `pull_through`, on purpose: the rulings travel in
full in the line, so a session reads them without opening anything, and
a ratio of opens would read near zero on an arm that is working.
`system_usage_failed: true` means its read broke and the zeros mean nothing.
EVERY COUNTER BLOCK CARRIES ITS OWN COVERAGE — `complete_from` and
`covers_window`. `complete_from` is when the number became trustworthy:
for one source, its first recorded row; for a section that sums several,
+8 -1
View File
@@ -18,6 +18,7 @@ from scribe.services import canonical_systems as canonical_systems_svc
from scribe.services import milestones as milestones_svc
from scribe.services import notes as notes_svc
from scribe.services import systems as systems_svc
from scribe.services import system_usage as system_usage_svc
from scribe.services.system_usage import record_system_pulled
# Below this, a project is young enough that the mild "which area is this
@@ -288,7 +289,9 @@ async def get_system(system_id: int) -> dict:
Returns the system, plus its associated records split into `issues`,
`tasks` (work/plan), and `notes` — each saying what the record is and where
it sits, not what it says (open one with get_note / get_task).
it sits, not what it says (open one with get_note / get_task) — and
`usage`: how many times its rulings were shown to a session because its
files were touched (`surfaced_count`), and how many times it was opened.
"""
uid = current_user_id()
system = await systems_svc.get_system(uid, system_id)
@@ -312,6 +315,10 @@ async def get_system(system_id: int) -> dict:
data["issues"] = issues
data["tasks"] = tasks
data["notes"] = notes
# How often this area's rulings were shown to a session and the System
# opened (#4769). Here and not on list_systems: one lookup on a read that
# asked for the System, rather than a column on every project handshake.
data["usage"] = (await system_usage_svc.usage_for_systems([system.id]))[system.id]
return data