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
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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) }}</span>
|
||||
<span
|
||||
v-if="rulingsShown(system)"
|
||||
class="usage-tag"
|
||||
:title="rulingsTitle(system)"
|
||||
>rulings shown {{ rulingsShown(system) }}×</span>
|
||||
</div>
|
||||
<p v-if="system.description" class="system-description">{{ system.description }}</p>
|
||||
<ul v-if="system.path_patterns.length" class="system-paths" aria-label="Files">
|
||||
|
||||
@@ -39,7 +39,10 @@ export const useSystemsStore = defineStore("systems", () => {
|
||||
const list = systemsByProject.value[projectId];
|
||||
if (list) {
|
||||
const idx = list.findIndex((s) => s.id === systemId);
|
||||
if (idx >= 0) list[idx] = system;
|
||||
// Merged, not replaced: the PATCH reply is the bare System, and the
|
||||
// list's read-side decorations (issue count, rulings usage) would
|
||||
// otherwise vanish from the row until the next reload.
|
||||
if (idx >= 0) list[idx] = { ...list[idx], ...system };
|
||||
}
|
||||
return system;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user