selftune 0.2.22 → 0.2.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/README.md +4 -2
  2. package/apps/local-dashboard/dist/assets/index-CwOtTrUS.css +1 -0
  3. package/apps/local-dashboard/dist/assets/index-f1HQpbeH.js +59 -0
  4. package/apps/local-dashboard/dist/assets/vendor-ui-jVSaIZey.js +12 -0
  5. package/apps/local-dashboard/dist/index.html +3 -3
  6. package/cli/selftune/adapters/pi/hook.ts +273 -0
  7. package/cli/selftune/adapters/pi/install.ts +207 -0
  8. package/cli/selftune/constants.ts +10 -1
  9. package/cli/selftune/dashboard-contract.ts +14 -0
  10. package/cli/selftune/evolution/engines/judge-engine.ts +96 -0
  11. package/cli/selftune/evolution/engines/replay-engine.ts +158 -0
  12. package/cli/selftune/evolution/evidence.ts +2 -6
  13. package/cli/selftune/evolution/evolve-body.ts +73 -20
  14. package/cli/selftune/evolution/validate-body.ts +78 -42
  15. package/cli/selftune/evolution/validate-routing.ts +45 -104
  16. package/cli/selftune/hooks/skill-eval.ts +2 -1
  17. package/cli/selftune/hooks-shared/types.ts +1 -0
  18. package/cli/selftune/index.ts +23 -5
  19. package/cli/selftune/ingestors/pi-ingest.ts +726 -0
  20. package/cli/selftune/init.ts +11 -1
  21. package/cli/selftune/localdb/direct-write.ts +85 -0
  22. package/cli/selftune/localdb/materialize.ts +6 -7
  23. package/cli/selftune/localdb/queries.ts +126 -0
  24. package/cli/selftune/localdb/schema.ts +38 -0
  25. package/cli/selftune/observability.ts +8 -1
  26. package/cli/selftune/orchestrate.ts +43 -0
  27. package/cli/selftune/registry/client.ts +74 -0
  28. package/cli/selftune/registry/history.ts +54 -0
  29. package/cli/selftune/registry/index.ts +90 -0
  30. package/cli/selftune/registry/install.ts +141 -0
  31. package/cli/selftune/registry/list.ts +44 -0
  32. package/cli/selftune/registry/push.ts +171 -0
  33. package/cli/selftune/registry/rollback.ts +49 -0
  34. package/cli/selftune/registry/status.ts +62 -0
  35. package/cli/selftune/registry/sync.ts +125 -0
  36. package/cli/selftune/repair/skill-usage.ts +4 -1
  37. package/cli/selftune/status.ts +31 -0
  38. package/cli/selftune/sync.ts +127 -23
  39. package/cli/selftune/types.ts +2 -1
  40. package/cli/selftune/utils/jsonl.ts +1 -30
  41. package/cli/selftune/utils/skill-discovery.ts +22 -0
  42. package/node_modules/@selftune/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  43. package/node_modules/@selftune/telemetry-contract/fixtures/golden.test.ts +0 -1
  44. package/node_modules/@selftune/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  45. package/node_modules/@selftune/telemetry-contract/package.json +1 -1
  46. package/node_modules/@selftune/telemetry-contract/src/index.ts +1 -0
  47. package/node_modules/@selftune/telemetry-contract/src/schemas.ts +22 -4
  48. package/node_modules/@selftune/telemetry-contract/src/types.ts +1 -12
  49. package/node_modules/@selftune/telemetry-contract/tests/compatibility.test.ts +0 -1
  50. package/package.json +1 -1
  51. package/packages/telemetry-contract/fixtures/evidence-only-push.ts +1 -1
  52. package/packages/telemetry-contract/fixtures/golden.test.ts +0 -1
  53. package/packages/telemetry-contract/fixtures/partial-push-unresolved-parents.ts +1 -1
  54. package/packages/telemetry-contract/package.json +1 -1
  55. package/packages/telemetry-contract/src/index.ts +1 -0
  56. package/packages/telemetry-contract/src/schemas.ts +22 -4
  57. package/packages/telemetry-contract/src/types.ts +1 -12
  58. package/packages/telemetry-contract/tests/compatibility.test.ts +0 -1
  59. package/packages/ui/AGENTS.md +16 -0
  60. package/packages/ui/README.md +1 -1
  61. package/packages/ui/package.json +1 -1
  62. package/packages/ui/src/components/ActivityTimeline.tsx +152 -168
  63. package/packages/ui/src/components/AnalyticsCharts.tsx +344 -0
  64. package/packages/ui/src/components/EvidenceViewer.tsx +153 -443
  65. package/packages/ui/src/components/EvolutionTimeline.tsx +34 -87
  66. package/packages/ui/src/components/InfoTip.tsx +1 -2
  67. package/packages/ui/src/components/InvocationsPanel.tsx +413 -0
  68. package/packages/ui/src/components/JobHistoryTimeline.tsx +156 -0
  69. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +18 -36
  70. package/packages/ui/src/components/OverviewPanels.tsx +652 -0
  71. package/packages/ui/src/components/PipelineStatusBar.tsx +65 -0
  72. package/packages/ui/src/components/SkillReportGuide.tsx +215 -0
  73. package/packages/ui/src/components/SkillReportPanels.tsx +919 -0
  74. package/packages/ui/src/components/SkillsLibrary.tsx +437 -0
  75. package/packages/ui/src/components/index.ts +56 -1
  76. package/packages/ui/src/components/section-cards.tsx +18 -35
  77. package/packages/ui/src/components/skill-health-grid.tsx +47 -37
  78. package/packages/ui/src/lib/constants.tsx +0 -1
  79. package/packages/ui/src/primitives/card.tsx +1 -1
  80. package/packages/ui/src/primitives/checkbox.tsx +1 -1
  81. package/packages/ui/src/primitives/dropdown-menu.tsx +2 -2
  82. package/packages/ui/src/primitives/select.tsx +2 -2
  83. package/packages/ui/src/types.ts +172 -4
  84. package/skill/SKILL.md +18 -4
  85. package/skill/Workflows/Ingest.md +60 -2
  86. package/skill/Workflows/Initialize.md +8 -5
  87. package/skill/Workflows/PlatformHooks.md +19 -3
  88. package/skill/Workflows/Registry.md +99 -0
  89. package/skill/Workflows/Sync.md +3 -1
  90. package/apps/local-dashboard/dist/assets/index-D8O-RG1I.js +0 -60
  91. package/apps/local-dashboard/dist/assets/index-_EcLywDg.css +0 -1
  92. package/apps/local-dashboard/dist/assets/vendor-ui-CGEmUayx.js +0 -12
  93. package/cli/selftune/utils/html.ts +0 -27
  94. package/packages/ui/src/components/RecentActivityFeed.tsx +0 -117
@@ -0,0 +1,919 @@
1
+ import { InfoTip } from "./InfoTip";
2
+ import { formatRate, timeAgo } from "../lib/format";
3
+ import {
4
+ Badge,
5
+ Button,
6
+ Card,
7
+ CardAction,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ Table,
13
+ TableBody,
14
+ TableCell,
15
+ TableHead,
16
+ TableHeader,
17
+ TableRow,
18
+ Tabs,
19
+ TabsContent,
20
+ TabsList,
21
+ TabsTrigger,
22
+ } from "../primitives";
23
+ import {
24
+ ActivityIcon,
25
+ BarChart3Icon,
26
+ DatabaseIcon,
27
+ GitBranchIcon,
28
+ SearchIcon,
29
+ TargetIcon,
30
+ } from "lucide-react";
31
+ import type { ReactNode } from "react";
32
+
33
+ import type {
34
+ ExampleRow,
35
+ TrustFields,
36
+ TrustState,
37
+ ObservationKind,
38
+ HistoricalContext,
39
+ } from "../types";
40
+
41
+ export function observationBadge(kind: ObservationKind | null | undefined): {
42
+ label: string;
43
+ variant: "default" | "secondary" | "destructive" | "outline";
44
+ } | null {
45
+ switch (kind) {
46
+ case "repaired_contextual_miss":
47
+ return { label: "repaired miss", variant: "destructive" };
48
+ case "repaired_trigger":
49
+ return { label: "repaired trigger", variant: "secondary" };
50
+ case "legacy_materialized":
51
+ return { label: "legacy row", variant: "outline" };
52
+ default:
53
+ return null;
54
+ }
55
+ }
56
+
57
+ export function historicalContextBadge(context: HistoricalContext | null | undefined): {
58
+ label: string;
59
+ variant: "default" | "secondary" | "destructive" | "outline";
60
+ } | null {
61
+ switch (context) {
62
+ case "previously_missed":
63
+ return { label: "previously missed", variant: "secondary" };
64
+ default:
65
+ return null;
66
+ }
67
+ }
68
+
69
+ function ExampleRowItem({ row }: { row: ExampleRow }) {
70
+ const workspace = row.workspace_path ? row.workspace_path.split("/").slice(-2).join("/") : null;
71
+ const observation = observationBadge(row.observation_kind);
72
+ const historicalContext = historicalContextBadge(row.historical_context);
73
+
74
+ return (
75
+ <TableRow className={!row.triggered ? "bg-destructive/5" : ""}>
76
+ <TableCell
77
+ className="max-w-[420px] truncate py-2 text-[12px]"
78
+ title={row.query_text || undefined}
79
+ >
80
+ {row.query_text || (
81
+ <span className="italic text-muted-foreground/40">No prompt recorded</span>
82
+ )}
83
+ </TableCell>
84
+ <TableCell className="py-2">
85
+ <div className="flex items-center gap-1.5">
86
+ {row.triggered ? (
87
+ <Badge
88
+ variant="outline"
89
+ className="border-green-600/30 text-[10px] font-normal text-green-600"
90
+ >
91
+ triggered
92
+ </Badge>
93
+ ) : (
94
+ <Badge variant="destructive" className="text-[10px] font-normal">
95
+ missed
96
+ </Badge>
97
+ )}
98
+ {observation && (
99
+ <Badge variant={observation.variant} className="text-[10px] font-normal">
100
+ {observation.label}
101
+ </Badge>
102
+ )}
103
+ {historicalContext && (
104
+ <Badge variant={historicalContext.variant} className="text-[10px] font-normal">
105
+ {historicalContext.label}
106
+ </Badge>
107
+ )}
108
+ </div>
109
+ </TableCell>
110
+ <TableCell className="py-2 font-mono text-[11px] tabular-nums text-muted-foreground">
111
+ {row.confidence != null ? `${Math.round(row.confidence * 100)}%` : "Not recorded"}
112
+ </TableCell>
113
+ <TableCell className="py-2">
114
+ {row.invocation_mode ? (
115
+ <Badge variant="secondary" className="text-[10px] font-normal">
116
+ {row.invocation_mode}
117
+ </Badge>
118
+ ) : (
119
+ <span className="text-[11px] text-muted-foreground">Unknown mode</span>
120
+ )}
121
+ </TableCell>
122
+ <TableCell className="py-2 text-[11px] text-muted-foreground">
123
+ {row.prompt_kind ?? "Unclassified"}
124
+ </TableCell>
125
+ <TableCell className="py-2 text-[11px] text-muted-foreground">
126
+ {row.source ?? "No data"}
127
+ </TableCell>
128
+ <TableCell className="py-2 text-[11px] text-muted-foreground">
129
+ {row.platform ?? "No data"}
130
+ </TableCell>
131
+ <TableCell
132
+ className="py-2 font-mono text-[11px] text-muted-foreground"
133
+ title={row.workspace_path ?? undefined}
134
+ >
135
+ {workspace ?? "No data"}
136
+ </TableCell>
137
+ <TableCell className="py-2">
138
+ <Badge
139
+ variant={
140
+ row.query_origin === "inline_query"
141
+ ? "outline"
142
+ : row.query_origin === "matched_prompt"
143
+ ? "secondary"
144
+ : "destructive"
145
+ }
146
+ className="text-[10px] font-normal"
147
+ >
148
+ {row.query_origin}
149
+ </Badge>
150
+ </TableCell>
151
+ </TableRow>
152
+ );
153
+ }
154
+
155
+ function ExamplesTable({ rows, emptyMessage }: { rows: ExampleRow[]; emptyMessage: string }) {
156
+ if (rows.length === 0) {
157
+ return (
158
+ <div className="flex items-center justify-center py-12 text-sm text-muted-foreground">
159
+ {emptyMessage}
160
+ </div>
161
+ );
162
+ }
163
+
164
+ return (
165
+ <div className="themed-scroll max-h-[340px] overflow-auto">
166
+ <Table>
167
+ <TableHeader>
168
+ <TableRow className="sticky top-0 z-10 bg-muted/70 backdrop-blur hover:bg-muted/70">
169
+ <TableHead className="h-8 font-headline text-[10px] uppercase tracking-[0.15em]">
170
+ Prompt
171
+ </TableHead>
172
+ <TableHead className="h-8 w-[80px] font-headline text-[10px] uppercase tracking-[0.15em]">
173
+ Status
174
+ </TableHead>
175
+ <TableHead className="h-8 w-[70px] font-headline text-[10px] uppercase tracking-[0.15em]">
176
+ Confidence
177
+ </TableHead>
178
+ <TableHead className="h-8 w-[80px] font-headline text-[10px] uppercase tracking-[0.15em]">
179
+ Mode
180
+ </TableHead>
181
+ <TableHead className="h-8 w-[80px] font-headline text-[10px] uppercase tracking-[0.15em]">
182
+ Kind
183
+ </TableHead>
184
+ <TableHead className="h-8 w-[70px] font-headline text-[10px] uppercase tracking-[0.15em]">
185
+ Source
186
+ </TableHead>
187
+ <TableHead className="h-8 w-[70px] font-headline text-[10px] uppercase tracking-[0.15em]">
188
+ Platform
189
+ </TableHead>
190
+ <TableHead className="h-8 w-[100px] font-headline text-[10px] uppercase tracking-[0.15em]">
191
+ Workspace
192
+ </TableHead>
193
+ <TableHead className="h-8 w-[100px] font-headline text-[10px] uppercase tracking-[0.15em]">
194
+ Origin
195
+ </TableHead>
196
+ </TableRow>
197
+ </TableHeader>
198
+ <TableBody>
199
+ {rows.map((row, i) => (
200
+ <ExampleRowItem key={`${row.session_id}-${i}`} row={row} />
201
+ ))}
202
+ </TableBody>
203
+ </Table>
204
+ </div>
205
+ );
206
+ }
207
+
208
+ function RateBar({
209
+ label,
210
+ value,
211
+ warn,
212
+ }: {
213
+ label: string;
214
+ value: number | null | undefined;
215
+ warn?: boolean;
216
+ }) {
217
+ const pct = value != null ? Math.round(value * 100) : null;
218
+ return (
219
+ <div className="flex items-center gap-3">
220
+ <span className="w-40 shrink-0 font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
221
+ {label}
222
+ </span>
223
+ <div className="h-2 flex-1 overflow-hidden rounded-full bg-muted">
224
+ {pct != null && (
225
+ <div
226
+ className={`h-full rounded-full transition-all ${warn ? "bg-destructive" : "bg-primary"}`}
227
+ style={{ width: `${Math.min(pct, 100)}%` }}
228
+ />
229
+ )}
230
+ </div>
231
+ <span
232
+ className={`w-10 text-right font-mono text-xs tabular-nums ${warn ? "text-destructive" : "text-muted-foreground"}`}
233
+ >
234
+ {pct != null ? `${pct}%` : "No data"}
235
+ </span>
236
+ </div>
237
+ );
238
+ }
239
+
240
+ function BreakdownTable({
241
+ title,
242
+ data,
243
+ }: {
244
+ title: string;
245
+ data: Array<{ source?: string; kind?: string; count: number }> | null | undefined;
246
+ }) {
247
+ if (!data || data.length === 0) return null;
248
+
249
+ const labelForValue = (value: string) => {
250
+ switch (value) {
251
+ case "repaired_contextual_miss":
252
+ return "repaired contextual miss";
253
+ case "repaired_trigger":
254
+ return "repaired trigger";
255
+ case "legacy_materialized":
256
+ return "legacy materialized";
257
+ default:
258
+ return value;
259
+ }
260
+ };
261
+
262
+ const entries = data
263
+ .map((d) => [labelForValue(d.source ?? d.kind ?? "(unknown)"), d.count] as [string, number])
264
+ .sort(([, a], [, b]) => b - a);
265
+ const total = entries.reduce((s, [, v]) => s + v, 0);
266
+
267
+ return (
268
+ <div>
269
+ <h4 className="mb-2 font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
270
+ {title}
271
+ </h4>
272
+ <Table>
273
+ <TableHeader>
274
+ <TableRow className="hover:bg-transparent">
275
+ <TableHead className="h-7 font-headline text-[10px] uppercase tracking-[0.15em]">
276
+ Value
277
+ </TableHead>
278
+ <TableHead className="h-7 w-[80px] text-right font-headline text-[10px] uppercase tracking-[0.15em]">
279
+ Count
280
+ </TableHead>
281
+ <TableHead className="h-7 w-[80px] text-right font-headline text-[10px] uppercase tracking-[0.15em]">
282
+ Rate
283
+ </TableHead>
284
+ </TableRow>
285
+ </TableHeader>
286
+ <TableBody>
287
+ {entries.map(([value, count]) => (
288
+ <TableRow key={value}>
289
+ <TableCell className="py-2 text-[11px]">{value}</TableCell>
290
+ <TableCell className="py-2 text-right font-mono text-[11px]">{count}</TableCell>
291
+ <TableCell className="py-2 text-right font-mono text-[11px] text-muted-foreground">
292
+ {total > 0 ? `${Math.round((count / total) * 100)}%` : "0%"}
293
+ </TableCell>
294
+ </TableRow>
295
+ ))}
296
+ </TableBody>
297
+ </Table>
298
+ </div>
299
+ );
300
+ }
301
+
302
+ export function SkillReportTopRow({
303
+ nextAction,
304
+ latestDecision,
305
+ }: {
306
+ nextAction: {
307
+ icon: ReactNode;
308
+ text: string;
309
+ actionLabel: string;
310
+ variant: "default" | "secondary" | "destructive" | "outline";
311
+ };
312
+ latestDecision?:
313
+ | {
314
+ action: string;
315
+ timestamp: string | null;
316
+ evolutionCount: number;
317
+ }
318
+ | undefined;
319
+ }) {
320
+ const nextActionBorder =
321
+ nextAction.variant === "destructive"
322
+ ? "border-destructive/25"
323
+ : nextAction.variant === "default"
324
+ ? "border-primary/20"
325
+ : "border-border/15";
326
+
327
+ return (
328
+ <div className="grid grid-cols-1 gap-3 @4xl/main:grid-cols-12">
329
+ <Card
330
+ className={`rounded-xl border bg-muted/35 shadow-none ${latestDecision ? "@4xl/main:col-span-8" : "@4xl/main:col-span-12"} ${nextActionBorder}`}
331
+ >
332
+ <CardContent className="flex items-start gap-3 px-4 py-4">
333
+ <div className="shrink-0 pt-0.5">{nextAction.icon}</div>
334
+ <div className="flex-1">
335
+ <h3 className="mb-1 font-headline text-[10px] uppercase tracking-[0.18em] text-muted-foreground">
336
+ Next Best Action
337
+ </h3>
338
+ <p className="text-[15px] font-medium leading-6 text-foreground">{nextAction.text}</p>
339
+ </div>
340
+ <Badge variant={nextAction.variant} className="shrink-0 self-start text-[10px]">
341
+ {nextAction.actionLabel}
342
+ </Badge>
343
+ </CardContent>
344
+ </Card>
345
+
346
+ {latestDecision && (
347
+ <Card className="rounded-xl border border-border/10 bg-muted/20 @4xl/main:col-span-4">
348
+ <CardContent className="flex h-full items-start gap-3 px-4 py-4">
349
+ <GitBranchIcon className="mt-0.5 size-4 shrink-0 text-primary/80" />
350
+ <div className="min-w-0 flex-1">
351
+ <h3 className="mb-1 font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
352
+ Latest Decision
353
+ </h3>
354
+ <p className="truncate text-sm font-medium leading-6">{latestDecision.action}</p>
355
+ {latestDecision.timestamp && (
356
+ <p className="mt-0.5 font-mono text-[10px] text-muted-foreground">
357
+ {timeAgo(latestDecision.timestamp)}
358
+ </p>
359
+ )}
360
+ </div>
361
+ <Badge variant="outline" className="shrink-0 self-start text-[9px]">
362
+ {latestDecision.evolutionCount} evolution
363
+ {latestDecision.evolutionCount !== 1 ? "s" : ""}
364
+ </Badge>
365
+ </CardContent>
366
+ </Card>
367
+ )}
368
+ </div>
369
+ );
370
+ }
371
+
372
+ function narrativeObservedText({
373
+ checks,
374
+ sessions,
375
+ promptLinkRate,
376
+ }: {
377
+ checks: number;
378
+ sessions: number;
379
+ promptLinkRate: number | null | undefined;
380
+ }) {
381
+ const promptClause =
382
+ promptLinkRate != null
383
+ ? ` It could link ${formatRate(promptLinkRate)} of those checks back to prompts.`
384
+ : "";
385
+ return `Selftune watched ${checks} skill checks across ${sessions} sessions.${promptClause}`;
386
+ }
387
+
388
+ function narrativeDiagnosisText({
389
+ missRate,
390
+ missedTriggers,
391
+ systemLikeRate,
392
+ }: {
393
+ missRate: number | null | undefined;
394
+ missedTriggers: number | null | undefined;
395
+ systemLikeRate: number | null | undefined;
396
+ }) {
397
+ if ((missedTriggers ?? 0) > 0 && missRate != null) {
398
+ return `It found ${missedTriggers} likely misses (${formatRate(missRate)} miss rate), which means people asked for this skill and it probably should have triggered.`;
399
+ }
400
+ if ((systemLikeRate ?? 0) > 0.05) {
401
+ return `Routing looks mostly stable, but some records appear system-generated, so selftune is being cautious about making strong claims.`;
402
+ }
403
+ return `Routing looks consistent in the current sample, with no strong signs that this skill is missing obvious requests.`;
404
+ }
405
+
406
+ function narrativeDecisionText({
407
+ trustState,
408
+ latestAction,
409
+ nextActionText,
410
+ }: {
411
+ trustState: TrustState;
412
+ latestAction?: string | null;
413
+ nextActionText: string;
414
+ }) {
415
+ switch (trustState) {
416
+ case "validated":
417
+ return `Selftune found a candidate that looks promising, but it has not been deployed yet. ${nextActionText}`;
418
+ case "deployed":
419
+ return `A change has already been deployed for this skill. Selftune is now watching for regressions in real use.`;
420
+ case "rolled_back":
421
+ return `A previous change was rolled back, so the live skill is back on the safer version while selftune keeps observing.`;
422
+ case "watch":
423
+ return `Selftune sees enough signal to keep a close eye on this skill, but not enough to blindly change it. ${nextActionText}`;
424
+ case "observed":
425
+ return `Selftune is still learning how people use this skill before making stronger recommendations.`;
426
+ case "low_sample":
427
+ return `There is not enough evidence yet to trust a big change here. Selftune is still collecting examples.`;
428
+ default:
429
+ return latestAction
430
+ ? `The latest automated decision for this skill was ${latestAction}. ${nextActionText}`
431
+ : nextActionText;
432
+ }
433
+ }
434
+
435
+ function StoryStep({ title, icon, body }: { title: string; icon: ReactNode; body: string }) {
436
+ return (
437
+ <div className="rounded-xl border border-border/10 bg-muted/20 p-4">
438
+ <div className="mb-2 flex items-center gap-2">
439
+ <div className="text-primary/80">{icon}</div>
440
+ <h3 className="font-headline text-[10px] uppercase tracking-[0.18em] text-muted-foreground">
441
+ {title}
442
+ </h3>
443
+ </div>
444
+ <p className="text-sm leading-6 text-foreground/90">{body}</p>
445
+ </div>
446
+ );
447
+ }
448
+
449
+ export function SkillTrustNarrativePanel({
450
+ trustState,
451
+ coverage,
452
+ evidenceQuality,
453
+ routingQuality,
454
+ evolutionState,
455
+ dataHygiene,
456
+ fallbackChecks,
457
+ fallbackSessions,
458
+ nextActionText,
459
+ onOpenGuide,
460
+ }: {
461
+ trustState: TrustState;
462
+ coverage?: TrustFields["coverage"];
463
+ evidenceQuality?: TrustFields["evidence_quality"];
464
+ routingQuality?: TrustFields["routing_quality"];
465
+ evolutionState?: TrustFields["evolution_state"];
466
+ dataHygiene?: TrustFields["data_hygiene"];
467
+ fallbackChecks: number;
468
+ fallbackSessions: number;
469
+ nextActionText: string;
470
+ onOpenGuide?: () => void;
471
+ }) {
472
+ const checks = coverage?.checks ?? fallbackChecks;
473
+ const sessions = coverage?.sessions ?? fallbackSessions;
474
+ const rawChecks = dataHygiene?.raw_checks ?? checks;
475
+ const internalRows = dataHygiene?.internal_prompt_rows ?? 0;
476
+ const legacyRows = dataHygiene?.legacy_rows ?? 0;
477
+ const repairedRows = dataHygiene?.repaired_rows ?? 0;
478
+ const excludedRows = Math.max(rawChecks - checks, 0);
479
+ const showTrustNote = excludedRows > 0 || legacyRows > 0 || repairedRows > 0;
480
+
481
+ return (
482
+ <Card className="rounded-xl border border-border/10 bg-card/95">
483
+ <CardHeader className="gap-2 px-4 py-4">
484
+ <div className="flex flex-wrap items-start justify-between gap-3">
485
+ <div className="space-y-1">
486
+ <CardTitle className="text-base">How selftune is improving this skill</CardTitle>
487
+ <CardDescription>
488
+ Read this first if you want the plain-English version before diving into the evidence
489
+ below.
490
+ </CardDescription>
491
+ </div>
492
+ {onOpenGuide && (
493
+ <Button variant="outline" size="sm" onClick={onOpenGuide}>
494
+ How to read this page
495
+ </Button>
496
+ )}
497
+ </div>
498
+ </CardHeader>
499
+ <CardContent className="space-y-4 px-4 pb-4 pt-0">
500
+ {showTrustNote && (
501
+ <div className="rounded-xl border border-primary/10 bg-primary/5 px-4 py-3 text-sm text-muted-foreground">
502
+ <span className="font-medium text-foreground">Trust note:</span> This summary is based
503
+ on <span className="font-medium text-foreground">{checks}</span> operational checks from
504
+ real usage.
505
+ {internalRows > 0 && (
506
+ <>
507
+ {" "}
508
+ <span className="font-medium text-foreground">{internalRows}</span> internal
509
+ selftune eval or optimizer prompts are excluded from trust scoring.
510
+ </>
511
+ )}
512
+ {legacyRows > 0 && (
513
+ <>
514
+ {" "}
515
+ <span className="font-medium text-foreground">{legacyRows}</span> legacy rows stay
516
+ in history only.
517
+ </>
518
+ )}
519
+ {repairedRows > 0 && (
520
+ <>
521
+ {" "}
522
+ <span className="font-medium text-foreground">{repairedRows}</span> repaired misses
523
+ come from transcript replay rather than first-party trigger events.
524
+ </>
525
+ )}
526
+ </div>
527
+ )}
528
+ <div className="grid grid-cols-1 gap-3 @4xl/main:grid-cols-3">
529
+ <StoryStep
530
+ title="What selftune saw"
531
+ icon={<ActivityIcon className="size-4" />}
532
+ body={narrativeObservedText({
533
+ checks,
534
+ sessions,
535
+ promptLinkRate: evidenceQuality?.prompt_link_rate,
536
+ })}
537
+ />
538
+ <StoryStep
539
+ title="Why it acted"
540
+ icon={<SearchIcon className="size-4" />}
541
+ body={narrativeDiagnosisText({
542
+ missRate: routingQuality?.miss_rate,
543
+ missedTriggers: routingQuality?.missed_triggers,
544
+ systemLikeRate: evidenceQuality?.system_like_rate,
545
+ })}
546
+ />
547
+ <StoryStep
548
+ title="What happened next"
549
+ icon={<GitBranchIcon className="size-4" />}
550
+ body={narrativeDecisionText({
551
+ trustState,
552
+ latestAction: evolutionState?.latest_action,
553
+ nextActionText,
554
+ })}
555
+ />
556
+ </div>
557
+ <div className="rounded-xl border border-border/10 bg-muted/15 px-4 py-3 text-sm text-muted-foreground">
558
+ If a proposal is rejected or still pending, your live skill has not changed yet. Selftune
559
+ only earns trust by testing changes before deployment.
560
+ </div>
561
+ </CardContent>
562
+ </Card>
563
+ );
564
+ }
565
+
566
+ export function TrustSignalsGrid({
567
+ coverage,
568
+ evidenceQuality,
569
+ routingQuality,
570
+ evolutionState,
571
+ fallbackChecks,
572
+ fallbackSessions,
573
+ fallbackEvidenceRows,
574
+ fallbackEvolutionRows,
575
+ fallbackLatestAction,
576
+ }: {
577
+ coverage?: TrustFields["coverage"];
578
+ evidenceQuality?: TrustFields["evidence_quality"];
579
+ routingQuality?: TrustFields["routing_quality"];
580
+ evolutionState?: TrustFields["evolution_state"];
581
+ fallbackChecks: number;
582
+ fallbackSessions: number;
583
+ fallbackEvidenceRows: number;
584
+ fallbackEvolutionRows: number;
585
+ fallbackLatestAction?: string;
586
+ }) {
587
+ const hasEvolutionData = (evolutionState?.evolution_rows ?? fallbackEvolutionRows) > 0;
588
+
589
+ return (
590
+ <div>
591
+ <h2 className="mb-2 font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
592
+ Trust Signals
593
+ </h2>
594
+ <div className="grid grid-cols-1 gap-3 @xl/main:grid-cols-2 @5xl/main:grid-cols-4">
595
+ <Card className="rounded-xl border border-border/10 bg-muted/20 transition-colors hover:border-border/20 @container/card">
596
+ <CardHeader className="gap-2 px-4 py-3">
597
+ <CardDescription className="flex items-center gap-1.5">
598
+ <ActivityIcon className="size-3.5" />
599
+ <span className="font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
600
+ Coverage
601
+ </span>
602
+ </CardDescription>
603
+ <CardTitle className="text-[32px] font-semibold leading-none tabular-nums text-foreground">
604
+ {coverage?.checks ?? fallbackChecks}
605
+ </CardTitle>
606
+ <CardAction>
607
+ <span className="font-mono text-[10px] text-muted-foreground">
608
+ {coverage?.sessions ?? fallbackSessions} sessions /{" "}
609
+ {coverage?.workspaces ?? "No data"} dirs
610
+ </span>
611
+ </CardAction>
612
+ </CardHeader>
613
+ </Card>
614
+
615
+ <Card className="rounded-xl border border-border/10 bg-muted/20 transition-colors hover:border-border/20 @container/card">
616
+ <CardHeader className="gap-2 px-4 py-3">
617
+ <CardDescription className="flex items-center gap-1.5">
618
+ <SearchIcon className="size-3.5" />
619
+ <span className="font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
620
+ Evidence Quality
621
+ </span>
622
+ <InfoTip text="How well prompts are linked to invocations. Higher prompt-link rate = more trustworthy data." />
623
+ </CardDescription>
624
+ <CardTitle className="text-[32px] font-semibold leading-none tabular-nums text-foreground">
625
+ {evidenceQuality?.prompt_link_rate != null
626
+ ? formatRate(evidenceQuality.prompt_link_rate)
627
+ : "No data"}
628
+ </CardTitle>
629
+ <CardAction>
630
+ <div className="flex flex-col items-end gap-0.5">
631
+ <span className="text-[10px] text-muted-foreground">
632
+ inline:{" "}
633
+ {evidenceQuality?.inline_query_rate != null
634
+ ? formatRate(evidenceQuality.inline_query_rate)
635
+ : "No data"}
636
+ </span>
637
+ {(evidenceQuality?.system_like_rate ?? 0) > 0.05 && (
638
+ <Badge variant="destructive" className="text-[9px]">
639
+ {formatRate(evidenceQuality?.system_like_rate ?? 0)} system-like
640
+ </Badge>
641
+ )}
642
+ </div>
643
+ </CardAction>
644
+ </CardHeader>
645
+ </Card>
646
+
647
+ <Card className="rounded-xl border border-border/10 bg-muted/20 transition-colors hover:border-border/20 @container/card">
648
+ <CardHeader className="gap-2 px-4 py-3">
649
+ <CardDescription className="flex items-center gap-1.5">
650
+ <TargetIcon className="size-3.5" />
651
+ <span className="font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
652
+ Routing
653
+ </span>
654
+ <InfoTip text="Routing accuracy: average confidence when triggering, and miss rate" />
655
+ </CardDescription>
656
+ <CardTitle className="text-[32px] font-semibold leading-none tabular-nums text-foreground">
657
+ {routingQuality?.avg_confidence != null
658
+ ? formatRate(routingQuality.avg_confidence)
659
+ : "No data"}
660
+ </CardTitle>
661
+ <CardAction>
662
+ <div className="flex flex-col items-end gap-0.5">
663
+ <span className="text-[10px] text-muted-foreground">
664
+ miss:{" "}
665
+ {routingQuality?.miss_rate != null
666
+ ? formatRate(routingQuality.miss_rate)
667
+ : "No data"}
668
+ </span>
669
+ <span className="text-[10px] tabular-nums text-muted-foreground">
670
+ {routingQuality?.missed_triggers ?? "No data"} missed
671
+ </span>
672
+ </div>
673
+ </CardAction>
674
+ </CardHeader>
675
+ </Card>
676
+
677
+ <Card className="rounded-xl border border-border/10 bg-muted/20 transition-colors hover:border-border/20 @container/card">
678
+ <CardHeader className="gap-2 px-4 py-3">
679
+ <CardDescription className="flex items-center gap-1.5">
680
+ <GitBranchIcon className="size-3.5" />
681
+ <span className="font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
682
+ Evolution
683
+ </span>
684
+ </CardDescription>
685
+ {hasEvolutionData ? (
686
+ <>
687
+ <CardTitle className="text-sm font-medium leading-6">
688
+ {evolutionState?.latest_action ?? fallbackLatestAction ?? "No data"}
689
+ </CardTitle>
690
+ <CardAction>
691
+ <div className="flex flex-col items-end gap-0.5">
692
+ <span className="text-[10px] tabular-nums text-muted-foreground">
693
+ {evolutionState?.evidence_rows ?? fallbackEvidenceRows} evidence
694
+ </span>
695
+ <span className="text-[10px] tabular-nums text-muted-foreground">
696
+ {evolutionState?.evolution_rows ?? fallbackEvolutionRows} evolution
697
+ </span>
698
+ {evolutionState?.latest_timestamp && (
699
+ <span className="font-mono text-[10px] text-muted-foreground">
700
+ {timeAgo(evolutionState.latest_timestamp)}
701
+ </span>
702
+ )}
703
+ </div>
704
+ </CardAction>
705
+ </>
706
+ ) : (
707
+ <CardTitle className="text-sm font-normal text-muted-foreground">
708
+ No evolution yet
709
+ </CardTitle>
710
+ )}
711
+ </CardHeader>
712
+ </Card>
713
+ </div>
714
+ </div>
715
+ );
716
+ }
717
+
718
+ export function PromptEvidencePanel({ examples }: { examples?: TrustFields["examples"] }) {
719
+ if (!examples) return null;
720
+ if (examples.good.length === 0 && examples.missed.length === 0 && examples.noisy.length === 0) {
721
+ return null;
722
+ }
723
+
724
+ return (
725
+ <Card className="rounded-xl border border-border/10 bg-card/90">
726
+ <CardHeader className="px-4 pb-2 pt-4">
727
+ <div className="flex items-start justify-between gap-4">
728
+ <div>
729
+ <CardTitle className="font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
730
+ Prompt Evidence
731
+ </CardTitle>
732
+ <CardDescription>
733
+ Representative real usage first. Internal selftune traffic is separated so it does not
734
+ masquerade as normal skill use.
735
+ </CardDescription>
736
+ </div>
737
+ <div className="hidden items-center gap-2 text-[10px] text-muted-foreground @3xl/main:flex">
738
+ <span>{examples.good.length} successful</span>
739
+ <span className="text-border">|</span>
740
+ <span>{examples.missed.length} missed</span>
741
+ <span className="text-border">|</span>
742
+ <span>{examples.noisy.length} internal/polluted</span>
743
+ </div>
744
+ </div>
745
+ </CardHeader>
746
+ <CardContent className="px-4 pb-4 pt-0">
747
+ <Tabs defaultValue="good">
748
+ <TabsList
749
+ variant="line"
750
+ className="min-h-0 rounded-xl border border-border/10 bg-muted/20 px-1.5 py-1"
751
+ >
752
+ <TabsTrigger
753
+ value="good"
754
+ className="rounded-lg px-3 data-active:bg-background/70 data-active:text-foreground"
755
+ >
756
+ Successful Triggers
757
+ <Badge variant="outline" className="ml-1.5 text-[10px]">
758
+ {examples.good.length}
759
+ </Badge>
760
+ </TabsTrigger>
761
+ <TabsTrigger
762
+ value="missed"
763
+ className="rounded-lg px-3 data-active:bg-background/70 data-active:text-foreground"
764
+ >
765
+ Missed Real Usage
766
+ <Badge
767
+ variant={examples.missed.length > 0 ? "destructive" : "outline"}
768
+ className="ml-1.5 text-[10px]"
769
+ >
770
+ {examples.missed.length}
771
+ </Badge>
772
+ </TabsTrigger>
773
+ <TabsTrigger
774
+ value="noisy"
775
+ className="rounded-lg px-3 data-active:bg-background/70 data-active:text-foreground"
776
+ >
777
+ Internal / Polluted
778
+ <Badge
779
+ variant={examples.noisy.length > 0 ? "destructive" : "outline"}
780
+ className="ml-1.5 text-[10px]"
781
+ >
782
+ {examples.noisy.length}
783
+ </Badge>
784
+ </TabsTrigger>
785
+ </TabsList>
786
+ <TabsContent value="good" className="mt-2">
787
+ <ExamplesTable rows={examples.good} emptyMessage="No successful trigger samples yet." />
788
+ </TabsContent>
789
+ <TabsContent value="missed" className="mt-2">
790
+ <ExamplesTable
791
+ rows={examples.missed}
792
+ emptyMessage="No missed real-usage samples detected."
793
+ />
794
+ </TabsContent>
795
+ <TabsContent value="noisy" className="mt-2">
796
+ <ExamplesTable
797
+ rows={examples.noisy}
798
+ emptyMessage="No internal or polluted samples detected."
799
+ />
800
+ </TabsContent>
801
+ </Tabs>
802
+ </CardContent>
803
+ </Card>
804
+ );
805
+ }
806
+
807
+ export function DataQualityPanel({
808
+ evidenceQuality,
809
+ dataHygiene,
810
+ }: {
811
+ evidenceQuality?: TrustFields["evidence_quality"];
812
+ dataHygiene?: TrustFields["data_hygiene"];
813
+ }) {
814
+ return (
815
+ <div className="grid grid-cols-1 gap-6 @5xl/main:grid-cols-2 @5xl/main:items-start">
816
+ <Card className="rounded-2xl border border-border/15 bg-card">
817
+ <CardHeader>
818
+ <CardTitle className="flex items-center gap-2 font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
819
+ <BarChart3Icon className="size-4" />
820
+ Evidence Quality Rates
821
+ </CardTitle>
822
+ </CardHeader>
823
+ <CardContent className="space-y-3 p-4">
824
+ <RateBar label="Prompt-linked" value={evidenceQuality?.prompt_link_rate} />
825
+ <RateBar label="Inline query" value={evidenceQuality?.inline_query_rate} />
826
+ <RateBar label="User prompt" value={evidenceQuality?.user_prompt_rate} />
827
+ <RateBar label="Meta prompt" value={evidenceQuality?.meta_prompt_rate} />
828
+ <RateBar label="No prompt" value={evidenceQuality?.no_prompt_rate} />
829
+ <RateBar
830
+ label="System-like"
831
+ value={evidenceQuality?.system_like_rate}
832
+ warn={(evidenceQuality?.system_like_rate ?? 0) > 0.05}
833
+ />
834
+ <div className="mt-3 border-t border-border/40 pt-3" />
835
+ <RateBar label="Invocation mode" value={evidenceQuality?.invocation_mode_coverage} />
836
+ <RateBar label="Confidence" value={evidenceQuality?.confidence_coverage} />
837
+ <RateBar label="Source" value={evidenceQuality?.source_coverage} />
838
+ <RateBar label="Scope" value={evidenceQuality?.scope_coverage} />
839
+ </CardContent>
840
+ </Card>
841
+
842
+ {dataHygiene && (
843
+ <Card className="rounded-2xl border border-border/15 bg-card">
844
+ <CardHeader>
845
+ <CardTitle className="flex items-center gap-2 font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
846
+ <DatabaseIcon className="size-4" />
847
+ Data Hygiene
848
+ </CardTitle>
849
+ </CardHeader>
850
+ <CardContent className="space-y-6 p-4">
851
+ <div className="grid grid-cols-2 gap-3">
852
+ <div className="rounded-xl border border-border/15 bg-muted/30 p-3">
853
+ <div className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
854
+ Raw vs Operational
855
+ </div>
856
+ <div className="mt-2 flex items-end gap-2">
857
+ <div className="text-lg font-semibold tabular-nums text-foreground">
858
+ {dataHygiene.operational_checks}
859
+ </div>
860
+ <div className="pb-0.5 text-xs text-muted-foreground">
861
+ of {dataHygiene.raw_checks} checks
862
+ </div>
863
+ </div>
864
+ <p className="mt-1 text-[11px] text-muted-foreground">
865
+ Operational checks exclude internal selftune eval and optimizer traffic.
866
+ </p>
867
+ </div>
868
+ <div className="rounded-xl border border-border/15 bg-muted/30 p-3">
869
+ <div className="text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
870
+ Historical Residue
871
+ </div>
872
+ <div className="mt-2 flex items-end gap-2">
873
+ <div className="text-lg font-semibold tabular-nums text-foreground">
874
+ {dataHygiene.legacy_rows}
875
+ </div>
876
+ <div className="pb-0.5 text-xs text-muted-foreground">
877
+ legacy / {dataHygiene.repaired_rows} repaired
878
+ </div>
879
+ </div>
880
+ <p className="mt-1 text-[11px] text-muted-foreground">
881
+ Legacy rows are older materialized history. Repaired rows are transcript-based
882
+ reconstructions.
883
+ </p>
884
+ </div>
885
+ </div>
886
+
887
+ {dataHygiene.naming_variants && dataHygiene.naming_variants.length > 1 && (
888
+ <div>
889
+ <h4 className="mb-2 font-headline text-[10px] uppercase tracking-[0.2em] text-muted-foreground">
890
+ Naming Variants
891
+ </h4>
892
+ <div className="flex flex-wrap gap-1.5">
893
+ {dataHygiene.naming_variants.map((v) => (
894
+ <Badge key={v} variant="outline" className="font-mono text-[10px]">
895
+ {v}
896
+ </Badge>
897
+ ))}
898
+ </div>
899
+ <p className="mt-1 text-[11px] text-muted-foreground">
900
+ Multiple naming variants may indicate inconsistent skill registration.
901
+ </p>
902
+ </div>
903
+ )}
904
+
905
+ <BreakdownTable title="Source Breakdown" data={dataHygiene.source_breakdown} />
906
+ <BreakdownTable
907
+ title="Prompt Kind Breakdown"
908
+ data={dataHygiene.prompt_kind_breakdown}
909
+ />
910
+ <BreakdownTable
911
+ title="Observation Breakdown"
912
+ data={dataHygiene.observation_breakdown}
913
+ />
914
+ </CardContent>
915
+ </Card>
916
+ )}
917
+ </div>
918
+ );
919
+ }