From 0a1bb68808ceab4231a5508496bc20c1601d7e73 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sat, 3 Oct 2026 08:49:20 -0400 Subject: [PATCH] =?UTF-8?q?feat(rulings):=20system=5Fusage=5Fevents=20is?= =?UTF-8?q?=20read=20back=20=E2=80=94=20per-System=20counts=20and=20a=20te?= =?UTF-8?q?lemetry=20block=20(#4769)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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 --- frontend/src/api/systems.ts | 8 + frontend/src/components/SystemsSection.vue | 25 ++++ frontend/src/stores/systems.ts | 5 +- src/scribe/mcp/tools/search.py | 12 +- src/scribe/mcp/tools/systems.py | 9 +- src/scribe/routes/systems.py | 6 + src/scribe/services/retrieval_registry.py | 24 +++ src/scribe/services/retrieval_telemetry.py | 121 +++++++++++++++- src/scribe/services/system_usage.py | 67 +++++++++ tests/conftest.py | 20 +++ tests/test_retrieval_registry.py | 3 + tests/test_system_usage_readout.py | 161 +++++++++++++++++++++ 12 files changed, 457 insertions(+), 4 deletions(-) create mode 100644 tests/test_system_usage_readout.py diff --git a/frontend/src/api/systems.ts b/frontend/src/api/systems.ts index 7d19ed1..3b75050 100644 --- a/frontend/src/api/systems.ts +++ b/frontend/src/api/systems.ts @@ -1,5 +1,6 @@ import { apiGet, apiPost, apiPatch, apiDelete } from "@/api/client"; import type { CanonicalMatch } from "@/api/canonicalSystems"; +import type { RecordUsage } from "@/types/usage"; export interface System { id: number; @@ -21,6 +22,13 @@ export interface System { */ path_patterns: string[]; open_issue_count: number; + /** + * How often this area's rulings were shown to a session because its files + * were touched (`surfaced_count`), and how often it was opened (#4769). + * Present on the list and detail reads; optional because a System returned + * by a create or update carries none. + */ + usage?: RecordUsage; created_at: string | null; updated_at: string | null; } diff --git a/frontend/src/components/SystemsSection.vue b/frontend/src/components/SystemsSection.vue index f69fffd..7171436 100644 --- a/frontend/src/components/SystemsSection.vue +++ b/frontend/src/components/SystemsSection.vue @@ -7,6 +7,7 @@ import { getProjectIssues } from "@/api/systems"; import type { System, TaskLike } from "@/api/systems"; import type { CanonicalMatch } from "@/api/canonicalSystems"; import { apiErrorMessage } from "@/api/client"; +import { fmtDate } from "@/utils/dateFormat"; import { Pencil, Trash2, Archive, ArchiveRestore } from "lucide-vue-next"; const props = defineProps<{ projectId: number }>(); @@ -71,6 +72,25 @@ function areaName(system: System): string | null { return canon.byId(system.canonical_id)?.name ?? null; } +/** How many times this area's rulings reached a session (#4769). Not the + * UsageBadge: its "N/M used" reads opens over showings, and rulings are + * delivered in full in the line, so an unopened System is the arm working, + * not dead weight. */ +function rulingsShown(system: System): number { + return system.usage?.surfaced_count ?? 0; +} + +function rulingsTitle(system: System): string { + const u = system.usage; + if (!u) return ""; + const last = u.last_surfaced_at ? ` Last shown ${fmtDate(u.last_surfaced_at)}.` : ""; + return ( + `This area's rulings were shown in full to a session ${u.surfaced_count}×, ` + + `when a command or edit touched its files.${last} ` + + `Opened with get_system ${u.pull_count}×.` + ); +} + async function load() { error.value = null; try { @@ -471,6 +491,11 @@ async function confirmDelete() { class="area-chip" :title="`Filed under the shared area “${areaName(system)}” — records and rules about this area line up across projects.`" >{{ areaName(system) }} + rulings shown {{ rulingsShown(system) }}×

{{ system.description }}