selftune 0.2.9 → 0.2.12

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 (140) hide show
  1. package/README.md +35 -35
  2. package/apps/local-dashboard/dist/assets/index-4_dAY17K.js +16 -0
  3. package/apps/local-dashboard/dist/assets/index-BxV5WZHc.css +2 -0
  4. package/apps/local-dashboard/dist/assets/rolldown-runtime-Dw2cE7zH.js +1 -0
  5. package/apps/local-dashboard/dist/assets/vendor-react-CKkiCskZ.js +11 -0
  6. package/apps/local-dashboard/dist/assets/vendor-table-pHbDxq36.js +8 -0
  7. package/apps/local-dashboard/dist/assets/vendor-ui-7xD7fNEU.js +12 -0
  8. package/apps/local-dashboard/dist/index.html +16 -15
  9. package/bin/selftune.cjs +1 -1
  10. package/cli/selftune/activation-rules.ts +1 -0
  11. package/cli/selftune/alpha-upload/build-payloads.ts +18 -2
  12. package/cli/selftune/alpha-upload/stage-canonical.ts +94 -0
  13. package/cli/selftune/auth/device-code.ts +32 -0
  14. package/cli/selftune/auto-update.ts +12 -0
  15. package/cli/selftune/badge/badge.ts +1 -0
  16. package/cli/selftune/canonical-export.ts +5 -0
  17. package/cli/selftune/claude-agents.ts +154 -0
  18. package/cli/selftune/contribute/bundle.ts +1 -0
  19. package/cli/selftune/contribute/contribute.ts +1 -0
  20. package/cli/selftune/cron/setup.ts +2 -2
  21. package/cli/selftune/dashboard-server.ts +1 -0
  22. package/cli/selftune/eval/hooks-to-evals.ts +1 -0
  23. package/cli/selftune/eval/import-skillsbench.ts +1 -0
  24. package/cli/selftune/eval/synthetic-evals.ts +2 -3
  25. package/cli/selftune/eval/unit-test.ts +1 -0
  26. package/cli/selftune/evolution/deploy-proposal.ts +9 -238
  27. package/cli/selftune/evolution/evolve-body.ts +93 -6
  28. package/cli/selftune/evolution/evolve.ts +3 -7
  29. package/cli/selftune/evolution/propose-body.ts +3 -2
  30. package/cli/selftune/evolution/propose-routing.ts +3 -2
  31. package/cli/selftune/evolution/refine-body.ts +3 -2
  32. package/cli/selftune/evolution/rollback.ts +1 -1
  33. package/cli/selftune/export.ts +1 -0
  34. package/cli/selftune/grading/grade-session.ts +8 -0
  35. package/cli/selftune/hooks/auto-activate.ts +1 -0
  36. package/cli/selftune/hooks/evolution-guard.ts +1 -1
  37. package/cli/selftune/hooks/prompt-log.ts +1 -0
  38. package/cli/selftune/hooks/session-stop.ts +34 -40
  39. package/cli/selftune/hooks/skill-change-guard.ts +1 -0
  40. package/cli/selftune/hooks/skill-eval.ts +1 -1
  41. package/cli/selftune/index.ts +23 -14
  42. package/cli/selftune/ingestors/claude-replay.ts +1 -0
  43. package/cli/selftune/ingestors/codex-rollout.ts +1 -0
  44. package/cli/selftune/ingestors/codex-wrapper.ts +1 -0
  45. package/cli/selftune/ingestors/openclaw-ingest.ts +1 -0
  46. package/cli/selftune/ingestors/opencode-ingest.ts +1 -0
  47. package/cli/selftune/init.ts +121 -29
  48. package/cli/selftune/localdb/db.ts +1 -0
  49. package/cli/selftune/localdb/direct-write.ts +39 -0
  50. package/cli/selftune/localdb/materialize.ts +2 -0
  51. package/cli/selftune/localdb/queries.ts +53 -0
  52. package/cli/selftune/localdb/schema.ts +28 -0
  53. package/cli/selftune/normalization.ts +1 -0
  54. package/cli/selftune/observability.ts +1 -0
  55. package/cli/selftune/repair/skill-usage.ts +1 -0
  56. package/cli/selftune/routes/orchestrate-runs.ts +1 -0
  57. package/cli/selftune/routes/overview.ts +1 -0
  58. package/cli/selftune/routes/report.ts +1 -1
  59. package/cli/selftune/routes/skill-report.ts +2 -1
  60. package/cli/selftune/status.ts +1 -1
  61. package/cli/selftune/sync.ts +30 -1
  62. package/cli/selftune/uninstall.ts +412 -0
  63. package/cli/selftune/utils/canonical-log.ts +2 -0
  64. package/cli/selftune/utils/frontmatter.ts +50 -7
  65. package/cli/selftune/utils/jsonl.ts +1 -0
  66. package/cli/selftune/utils/llm-call.ts +131 -3
  67. package/cli/selftune/utils/skill-log.ts +1 -0
  68. package/cli/selftune/utils/transcript.ts +1 -0
  69. package/cli/selftune/utils/trigger-check.ts +1 -1
  70. package/cli/selftune/workflows/skill-md-writer.ts +5 -5
  71. package/cli/selftune/workflows/workflows.ts +1 -0
  72. package/package.json +37 -33
  73. package/packages/telemetry-contract/fixtures/golden.test.ts +1 -0
  74. package/packages/telemetry-contract/package.json +1 -1
  75. package/packages/telemetry-contract/src/schemas.ts +1 -0
  76. package/packages/telemetry-contract/tests/compatibility.test.ts +1 -0
  77. package/packages/ui/README.md +35 -34
  78. package/packages/ui/package.json +3 -3
  79. package/packages/ui/src/components/ActivityTimeline.tsx +50 -43
  80. package/packages/ui/src/components/EvidenceViewer.tsx +306 -182
  81. package/packages/ui/src/components/EvolutionTimeline.tsx +83 -72
  82. package/packages/ui/src/components/InfoTip.tsx +4 -3
  83. package/packages/ui/src/components/OrchestrateRunsPanel.tsx +60 -53
  84. package/packages/ui/src/components/section-cards.tsx +20 -25
  85. package/packages/ui/src/components/skill-health-grid.tsx +213 -193
  86. package/packages/ui/src/lib/constants.tsx +1 -0
  87. package/packages/ui/src/primitives/badge.tsx +12 -15
  88. package/packages/ui/src/primitives/button.tsx +7 -7
  89. package/packages/ui/src/primitives/card.tsx +15 -26
  90. package/packages/ui/src/primitives/checkbox.tsx +7 -8
  91. package/packages/ui/src/primitives/collapsible.tsx +5 -5
  92. package/packages/ui/src/primitives/dropdown-menu.tsx +45 -55
  93. package/packages/ui/src/primitives/label.tsx +6 -6
  94. package/packages/ui/src/primitives/select.tsx +28 -37
  95. package/packages/ui/src/primitives/table.tsx +17 -44
  96. package/packages/ui/src/primitives/tabs.tsx +14 -21
  97. package/packages/ui/src/primitives/tooltip.tsx +10 -22
  98. package/skill/SKILL.md +70 -57
  99. package/skill/Workflows/AlphaUpload.md +4 -4
  100. package/skill/Workflows/AutoActivation.md +11 -6
  101. package/skill/Workflows/Badge.md +22 -16
  102. package/skill/Workflows/Baseline.md +34 -36
  103. package/skill/Workflows/Composability.md +16 -11
  104. package/skill/Workflows/Contribute.md +26 -21
  105. package/skill/Workflows/Cron.md +23 -22
  106. package/skill/Workflows/Dashboard.md +32 -27
  107. package/skill/Workflows/Doctor.md +33 -27
  108. package/skill/Workflows/Evals.md +48 -47
  109. package/skill/Workflows/EvolutionMemory.md +31 -21
  110. package/skill/Workflows/Evolve.md +84 -82
  111. package/skill/Workflows/EvolveBody.md +58 -47
  112. package/skill/Workflows/Grade.md +16 -13
  113. package/skill/Workflows/ImportSkillsBench.md +9 -6
  114. package/skill/Workflows/Ingest.md +36 -21
  115. package/skill/Workflows/Initialize.md +108 -40
  116. package/skill/Workflows/Orchestrate.md +22 -16
  117. package/skill/Workflows/Replay.md +12 -7
  118. package/skill/Workflows/Rollback.md +13 -6
  119. package/skill/Workflows/Schedule.md +6 -6
  120. package/skill/Workflows/Sync.md +18 -11
  121. package/skill/Workflows/UnitTest.md +28 -17
  122. package/skill/Workflows/Watch.md +28 -21
  123. package/skill/agents/diagnosis-analyst.md +11 -0
  124. package/skill/agents/evolution-reviewer.md +15 -1
  125. package/skill/agents/integration-guide.md +10 -0
  126. package/skill/agents/pattern-analyst.md +12 -1
  127. package/skill/references/grading-methodology.md +23 -24
  128. package/skill/references/interactive-config.md +7 -7
  129. package/skill/references/invocation-taxonomy.md +22 -20
  130. package/skill/references/logs.md +14 -6
  131. package/skill/references/setup-patterns.md +4 -2
  132. package/.claude/agents/diagnosis-analyst.md +0 -156
  133. package/.claude/agents/evolution-reviewer.md +0 -180
  134. package/.claude/agents/integration-guide.md +0 -212
  135. package/.claude/agents/pattern-analyst.md +0 -160
  136. package/apps/local-dashboard/dist/assets/index-Bs3Y4ixf.css +0 -1
  137. package/apps/local-dashboard/dist/assets/index-C4UYGWKr.js +0 -15
  138. package/apps/local-dashboard/dist/assets/vendor-react-BQH_6WrG.js +0 -60
  139. package/apps/local-dashboard/dist/assets/vendor-table-dK1QMLq9.js +0 -26
  140. package/apps/local-dashboard/dist/assets/vendor-ui-CO2mrx6e.js +0 -341
@@ -1,4 +1,3 @@
1
- import * as React from "react"
2
1
  import {
3
2
  closestCenter,
4
3
  DndContext,
@@ -9,16 +8,16 @@ import {
9
8
  useSensors,
10
9
  type DragEndEvent,
11
10
  type UniqueIdentifier,
12
- } from "@dnd-kit/core"
13
- import { restrictToVerticalAxis } from "@dnd-kit/modifiers"
11
+ } from "@dnd-kit/core";
12
+ import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
14
13
  import {
15
14
  arrayMove,
16
15
  SortableContext,
17
16
  sortableKeyboardCoordinates,
18
17
  useSortable,
19
18
  verticalListSortingStrategy,
20
- } from "@dnd-kit/sortable"
21
- import { CSS } from "@dnd-kit/utilities"
19
+ } from "@dnd-kit/sortable";
20
+ import { CSS } from "@dnd-kit/utilities";
22
21
  import {
23
22
  flexRender,
24
23
  getCoreRowModel,
@@ -33,45 +32,7 @@ import {
33
32
  type Row,
34
33
  type SortingState,
35
34
  type VisibilityState,
36
- } from "@tanstack/react-table"
37
-
38
- import { Badge } from "../primitives/badge"
39
- import { Button } from "../primitives/button"
40
- import { Checkbox } from "../primitives/checkbox"
41
- import {
42
- DropdownMenu,
43
- DropdownMenuCheckboxItem,
44
- DropdownMenuContent,
45
- DropdownMenuRadioGroup,
46
- DropdownMenuRadioItem,
47
- DropdownMenuTrigger,
48
- } from "../primitives/dropdown-menu"
49
- import { Label } from "../primitives/label"
50
- import {
51
- Select,
52
- SelectContent,
53
- SelectGroup,
54
- SelectItem,
55
- SelectTrigger,
56
- SelectValue,
57
- } from "../primitives/select"
58
- import {
59
- Table,
60
- TableBody,
61
- TableCell,
62
- TableHead,
63
- TableHeader,
64
- TableRow,
65
- } from "../primitives/table"
66
- import {
67
- Tabs,
68
- TabsContent,
69
- TabsList,
70
- TabsTrigger,
71
- } from "../primitives/tabs"
72
- import { STATUS_CONFIG } from "../lib/constants"
73
- import type { SkillCard, SkillHealthStatus } from "../types"
74
- import { formatRate, timeAgo } from "../lib/format"
35
+ } from "@tanstack/react-table";
75
36
  import {
76
37
  GripVerticalIcon,
77
38
  Columns3Icon,
@@ -88,17 +49,47 @@ import {
88
49
  XCircleIcon,
89
50
  CircleDotIcon,
90
51
  HelpCircleIcon,
91
- } from "lucide-react"
52
+ } from "lucide-react";
53
+ import * as React from "react";
54
+
55
+ import { STATUS_CONFIG } from "../lib/constants";
56
+ import { formatRate, timeAgo } from "../lib/format";
57
+ import { Badge } from "../primitives/badge";
58
+ import { Button } from "../primitives/button";
59
+ import { Checkbox } from "../primitives/checkbox";
60
+ import {
61
+ DropdownMenu,
62
+ DropdownMenuCheckboxItem,
63
+ DropdownMenuContent,
64
+ DropdownMenuRadioGroup,
65
+ DropdownMenuRadioItem,
66
+ DropdownMenuTrigger,
67
+ } from "../primitives/dropdown-menu";
68
+ import { Label } from "../primitives/label";
69
+ import {
70
+ Select,
71
+ SelectContent,
72
+ SelectGroup,
73
+ SelectItem,
74
+ SelectTrigger,
75
+ SelectValue,
76
+ } from "../primitives/select";
77
+ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../primitives/table";
78
+ import { Tabs, TabsContent, TabsList, TabsTrigger } from "../primitives/tabs";
79
+ import type { SkillCard, SkillHealthStatus } from "../types";
92
80
 
93
81
  // ---------- Drag handle ----------
94
82
 
95
- type SortableContextValue = Pick<ReturnType<typeof useSortable>, "attributes" | "listeners" | "setActivatorNodeRef">
83
+ type SortableContextValue = Pick<
84
+ ReturnType<typeof useSortable>,
85
+ "attributes" | "listeners" | "setActivatorNodeRef"
86
+ >;
96
87
 
97
- const SortableRowContext = React.createContext<SortableContextValue | null>(null)
88
+ const SortableRowContext = React.createContext<SortableContextValue | null>(null);
98
89
 
99
90
  function DragHandle() {
100
- const ctx = React.useContext(SortableRowContext)
101
- if (!ctx) return null
91
+ const ctx = React.useContext(SortableRowContext);
92
+ if (!ctx) return null;
102
93
  return (
103
94
  <Button
104
95
  ref={ctx.setActivatorNodeRef}
@@ -111,12 +102,14 @@ function DragHandle() {
111
102
  <GripVerticalIcon className="size-3 text-muted-foreground" />
112
103
  <span className="sr-only">Drag to reorder</span>
113
104
  </Button>
114
- )
105
+ );
115
106
  }
116
107
 
117
108
  // ---------- Column definitions ----------
118
109
 
119
- function createColumns(renderSkillName?: (skill: SkillCard) => React.ReactNode): ColumnDef<SkillCard>[] {
110
+ function createColumns(
111
+ renderSkillName?: (skill: SkillCard) => React.ReactNode,
112
+ ): ColumnDef<SkillCard>[] {
120
113
  return [
121
114
  {
122
115
  id: "drag",
@@ -129,10 +122,7 @@ function createColumns(renderSkillName?: (skill: SkillCard) => React.ReactNode):
129
122
  <div className="flex items-center justify-center">
130
123
  <Checkbox
131
124
  checked={table.getIsAllPageRowsSelected()}
132
- indeterminate={
133
- table.getIsSomePageRowsSelected() &&
134
- !table.getIsAllPageRowsSelected()
135
- }
125
+ indeterminate={table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected()}
136
126
  onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
137
127
  aria-label="Select all"
138
128
  />
@@ -153,77 +143,82 @@ function createColumns(renderSkillName?: (skill: SkillCard) => React.ReactNode):
153
143
  {
154
144
  accessorKey: "name",
155
145
  header: "Skill",
156
- cell: ({ row }) => renderSkillName
157
- ? renderSkillName(row.original)
158
- : <span className="text-sm font-medium">{row.original.name}</span>,
146
+ cell: ({ row }) =>
147
+ renderSkillName ? (
148
+ renderSkillName(row.original)
149
+ ) : (
150
+ <span className="text-sm font-medium">{row.original.name}</span>
151
+ ),
159
152
  enableHiding: false,
160
153
  },
161
154
  {
162
155
  accessorKey: "scope",
163
156
  header: "Scope",
164
157
  cell: ({ row }) => {
165
- const scope = row.original.scope
166
- if (!scope) return <span className="text-xs text-muted-foreground">--</span>
158
+ const scope = row.original.scope;
159
+ if (!scope) return <span className="text-xs text-muted-foreground">--</span>;
167
160
  return (
168
161
  <Badge variant="secondary" className="text-[10px]">
169
162
  {scope}
170
163
  </Badge>
171
- )
164
+ );
172
165
  },
173
166
  },
174
167
  {
175
168
  accessorKey: "status",
176
169
  header: "Status",
177
170
  cell: ({ row }) => {
178
- const config = STATUS_CONFIG[row.original.status]
171
+ const config = STATUS_CONFIG[row.original.status];
179
172
  return (
180
173
  <Badge variant={config.variant} className="gap-1 px-1.5 text-muted-foreground">
181
174
  {config.icon}
182
175
  {config.label}
183
176
  </Badge>
184
- )
177
+ );
185
178
  },
186
179
  sortingFn: (rowA, rowB) => {
187
180
  const order: Record<SkillHealthStatus, number> = {
188
- CRITICAL: 0, WARNING: 1, UNGRADED: 2, UNKNOWN: 3, HEALTHY: 4,
189
- }
190
- return order[rowA.original.status] - order[rowB.original.status]
181
+ CRITICAL: 0,
182
+ WARNING: 1,
183
+ UNGRADED: 2,
184
+ UNKNOWN: 3,
185
+ HEALTHY: 4,
186
+ };
187
+ return order[rowA.original.status] - order[rowB.original.status];
191
188
  },
192
189
  },
193
190
  {
194
191
  accessorKey: "passRate",
195
192
  header: () => <div className="w-full text-right">Pass Rate</div>,
196
193
  cell: ({ row }) => {
197
- const rate = row.original.passRate
198
- const isLow = rate !== null && rate < 0.5
194
+ const rate = row.original.passRate;
195
+ const isLow = rate !== null && rate < 0.5;
199
196
  return (
200
- <div className={`text-right font-mono tabular-nums ${isLow ? "text-red-600 font-semibold" : ""}`}>
197
+ <div
198
+ className={`text-right font-mono tabular-nums ${isLow ? "text-red-600 font-semibold" : ""}`}
199
+ >
201
200
  {formatRate(rate)}
202
201
  </div>
203
- )
202
+ );
204
203
  },
205
204
  sortingFn: (rowA, rowB) => {
206
- const a = rowA.original.passRate ?? -1
207
- const b = rowB.original.passRate ?? -1
208
- return a - b
205
+ const a = rowA.original.passRate ?? -1;
206
+ const b = rowB.original.passRate ?? -1;
207
+ return a - b;
209
208
  },
210
209
  },
211
210
  {
212
211
  accessorKey: "checks",
213
212
  header: () => <div className="w-full text-right">Checks</div>,
214
213
  cell: ({ row }) => (
215
- <div className="text-right font-mono tabular-nums">
216
- {row.original.checks}
217
- </div>
214
+ <div className="text-right font-mono tabular-nums">{row.original.checks}</div>
218
215
  ),
219
216
  },
220
217
  {
221
218
  accessorKey: "uniqueSessions",
222
219
  header: () => <div className="w-full text-right">Sessions</div>,
223
220
  cell: ({ row }) => (
224
- <div className="text-right font-mono tabular-nums">
225
- {row.original.uniqueSessions}
226
- </div>
221
+ <div className="text-right font-mono tabular-nums">{row.original.uniqueSessions}</div>
227
222
  ),
228
223
  },
229
224
  {
@@ -243,13 +238,13 @@ function createColumns(renderSkillName?: (skill: SkillCard) => React.ReactNode):
243
238
  ),
244
239
  sortingFn: (rowA, rowB) => {
245
240
  const toEpoch = (v: string | null) => {
246
- if (!v) return 0
247
- const t = new Date(v).getTime()
248
- return Number.isNaN(t) ? 0 : t
249
- }
250
- const a = toEpoch(rowA.original.lastSeen)
251
- const b = toEpoch(rowB.original.lastSeen)
252
- return a - b
241
+ if (!v) return 0;
242
+ const t = new Date(v).getTime();
243
+ return Number.isNaN(t) ? 0 : t;
244
+ };
245
+ const a = toEpoch(rowA.original.lastSeen);
246
+ const b = toEpoch(rowB.original.lastSeen);
247
+ return a - b;
253
248
  },
254
249
  },
255
250
  {
@@ -264,19 +259,27 @@ function createColumns(renderSkillName?: (skill: SkillCard) => React.ReactNode):
264
259
  </Badge>
265
260
  ),
266
261
  },
267
- ]
262
+ ];
268
263
  }
269
264
 
270
265
  // ---------- Draggable row ----------
271
266
 
272
267
  function DraggableRow({ row }: { row: Row<SkillCard> }) {
273
- const { transform, transition, setNodeRef, setActivatorNodeRef, isDragging, attributes, listeners } = useSortable({
268
+ const {
269
+ transform,
270
+ transition,
271
+ setNodeRef,
272
+ setActivatorNodeRef,
273
+ isDragging,
274
+ attributes,
275
+ listeners,
276
+ } = useSortable({
274
277
  id: row.original.name,
275
- })
278
+ });
276
279
  const sortableCtx = React.useMemo(
277
280
  () => ({ attributes, listeners, setActivatorNodeRef }),
278
281
  [attributes, listeners, setActivatorNodeRef],
279
- )
282
+ );
280
283
  return (
281
284
  <SortableRowContext.Provider value={sortableCtx}>
282
285
  <TableRow
@@ -296,7 +299,7 @@ function DraggableRow({ row }: { row: Row<SkillCard> }) {
296
299
  ))}
297
300
  </TableRow>
298
301
  </SortableRowContext.Provider>
299
- )
302
+ );
300
303
  }
301
304
 
302
305
  // ---------- Main component ----------
@@ -308,59 +311,64 @@ export function SkillHealthGrid({
308
311
  onStatusFilterChange,
309
312
  renderSkillName,
310
313
  }: {
311
- cards: SkillCard[]
312
- totalCount: number
313
- statusFilter?: SkillHealthStatus | "ALL"
314
- onStatusFilterChange?: (v: SkillHealthStatus | "ALL") => void
315
- renderSkillName?: (skill: SkillCard) => React.ReactNode
314
+ cards: SkillCard[];
315
+ totalCount: number;
316
+ statusFilter?: SkillHealthStatus | "ALL";
317
+ onStatusFilterChange?: (v: SkillHealthStatus | "ALL") => void;
318
+ renderSkillName?: (skill: SkillCard) => React.ReactNode;
316
319
  }) {
317
- const [activeView, setActiveView] = React.useState("all")
318
- const [data, setData] = React.useState<SkillCard[]>([])
319
- const [rowSelection, setRowSelection] = React.useState({})
320
- const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
321
- const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
322
- const [sorting, setSorting] = React.useState<SortingState>([])
320
+ const [activeView, setActiveView] = React.useState("all");
321
+ const [data, setData] = React.useState<SkillCard[]>([]);
322
+ const [rowSelection, setRowSelection] = React.useState({});
323
+ const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({});
324
+ const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
325
+ const [sorting, setSorting] = React.useState<SortingState>([]);
323
326
  const [pagination, setPagination] = React.useState({
324
327
  pageIndex: 0,
325
328
  pageSize: 20,
326
- })
329
+ });
327
330
 
328
- const columns = React.useMemo(() => createColumns(renderSkillName), [renderSkillName])
331
+ const columns = React.useMemo(() => createColumns(renderSkillName), [renderSkillName]);
329
332
 
330
333
  // View counts for tab badges
331
- const viewCounts = React.useMemo(() => ({
332
- all: cards.length,
333
- attention: cards.filter((c) => c.status === "CRITICAL" || c.status === "WARNING").length,
334
- recent: cards.filter((c) => c.lastSeen !== null).length,
335
- ungraded: cards.filter((c) => c.status === "UNGRADED" || c.status === "UNKNOWN").length,
336
- }), [cards])
334
+ const viewCounts = React.useMemo(
335
+ () => ({
336
+ all: cards.length,
337
+ attention: cards.filter((c) => c.status === "CRITICAL" || c.status === "WARNING").length,
338
+ recent: cards.filter((c) => c.lastSeen !== null).length,
339
+ ungraded: cards.filter((c) => c.status === "UNGRADED" || c.status === "UNKNOWN").length,
340
+ }),
341
+ [cards],
342
+ );
337
343
 
338
344
  // Filter cards based on active view tab, then sync into local state for DnD
339
345
  React.useEffect(() => {
340
- let filtered = cards
346
+ let filtered = cards;
341
347
  if (activeView === "attention") {
342
- filtered = cards.filter((c) => c.status === "CRITICAL" || c.status === "WARNING")
348
+ filtered = cards.filter((c) => c.status === "CRITICAL" || c.status === "WARNING");
343
349
  } else if (activeView === "recent") {
344
- filtered = [...cards.filter((c) => c.lastSeen !== null)].sort((a, b) => {
345
- const aTime = a.lastSeen ? new Date(a.lastSeen).getTime() : 0
346
- const bTime = b.lastSeen ? new Date(b.lastSeen).getTime() : 0
347
- return bTime - aTime
348
- })
350
+ filtered = cards
351
+ .filter((c) => c.lastSeen !== null)
352
+ .sort((a: SkillCard, b: SkillCard) => {
353
+ const aTime = a.lastSeen ? new Date(a.lastSeen).getTime() : 0;
354
+ const bTime = b.lastSeen ? new Date(b.lastSeen).getTime() : 0;
355
+ return bTime - aTime;
356
+ });
349
357
  } else if (activeView === "ungraded") {
350
- filtered = cards.filter((c) => c.status === "UNGRADED" || c.status === "UNKNOWN")
358
+ filtered = cards.filter((c) => c.status === "UNGRADED" || c.status === "UNKNOWN");
351
359
  }
352
- setData(filtered)
353
- setPagination((prev) => ({ ...prev, pageIndex: 0 }))
354
- }, [cards, activeView])
360
+ setData(filtered);
361
+ setPagination((prev) => ({ ...prev, pageIndex: 0 }));
362
+ }, [cards, activeView]);
355
363
 
356
- const sortableId = React.useId()
364
+ const sortableId = React.useId();
357
365
  const sensors = useSensors(
358
366
  useSensor(MouseSensor, {}),
359
367
  useSensor(TouchSensor, {}),
360
368
  useSensor(KeyboardSensor, {
361
369
  coordinateGetter: sortableKeyboardCoordinates,
362
- })
363
- )
370
+ }),
371
+ );
364
372
 
365
373
  const table = useReactTable({
366
374
  data,
@@ -385,26 +393,26 @@ export function SkillHealthGrid({
385
393
  getSortedRowModel: getSortedRowModel(),
386
394
  getFacetedRowModel: getFacetedRowModel(),
387
395
  getFacetedUniqueValues: getFacetedUniqueValues(),
388
- })
396
+ });
389
397
 
390
398
  const dataIds = React.useMemo<UniqueIdentifier[]>(
391
399
  () => table.getRowModel().rows.map((r) => r.id),
392
- [table.getRowModel().rows]
393
- )
400
+ [table.getRowModel().rows],
401
+ );
394
402
 
395
- const isSorted = sorting.length > 0
403
+ const isSorted = sorting.length > 0;
396
404
 
397
405
  function handleDragEnd(event: DragEndEvent) {
398
- if (isSorted) return
399
- const { active, over } = event
406
+ if (isSorted) return;
407
+ const { active, over } = event;
400
408
  if (active && over && active.id !== over.id) {
401
409
  setData((prev) => {
402
- const ids = prev.map((d) => d.name)
403
- const oldIndex = ids.indexOf(active.id as string)
404
- const newIndex = ids.indexOf(over.id as string)
405
- if (oldIndex === -1 || newIndex === -1) return prev
406
- return arrayMove(prev, oldIndex, newIndex)
407
- })
410
+ const ids = prev.map((d) => d.name);
411
+ const oldIndex = ids.indexOf(active.id as string);
412
+ const newIndex = ids.indexOf(over.id as string);
413
+ if (oldIndex === -1 || newIndex === -1) return prev;
414
+ return arrayMove(prev, oldIndex, newIndex);
415
+ });
408
416
  }
409
417
  }
410
418
 
@@ -419,15 +427,8 @@ export function SkillHealthGrid({
419
427
  <Label htmlFor="view-selector" className="sr-only">
420
428
  View
421
429
  </Label>
422
- <Select
423
- value={activeView}
424
- onValueChange={(v) => v && setActiveView(v)}
425
- >
426
- <SelectTrigger
427
- className="flex w-fit @4xl/main:hidden"
428
- size="sm"
429
- id="view-selector"
430
- >
430
+ <Select value={activeView} onValueChange={(v) => v && setActiveView(v)}>
431
+ <SelectTrigger className="flex w-fit @4xl/main:hidden" size="sm" id="view-selector">
431
432
  <SelectValue placeholder="Select a view" />
432
433
  </SelectTrigger>
433
434
  <SelectContent>
@@ -447,21 +448,15 @@ export function SkillHealthGrid({
447
448
  </TabsTrigger>
448
449
  <TabsTrigger value="attention">
449
450
  Needs Attention{" "}
450
- {viewCounts.attention > 0 && (
451
- <Badge variant="secondary">{viewCounts.attention}</Badge>
452
- )}
451
+ {viewCounts.attention > 0 && <Badge variant="secondary">{viewCounts.attention}</Badge>}
453
452
  </TabsTrigger>
454
453
  <TabsTrigger value="recent">
455
454
  Recently Active{" "}
456
- {viewCounts.recent > 0 && (
457
- <Badge variant="secondary">{viewCounts.recent}</Badge>
458
- )}
455
+ {viewCounts.recent > 0 && <Badge variant="secondary">{viewCounts.recent}</Badge>}
459
456
  </TabsTrigger>
460
457
  <TabsTrigger value="ungraded">
461
458
  Ungraded{" "}
462
- {viewCounts.ungraded > 0 && (
463
- <Badge variant="secondary">{viewCounts.ungraded}</Badge>
464
- )}
459
+ {viewCounts.ungraded > 0 && <Badge variant="secondary">{viewCounts.ungraded}</Badge>}
465
460
  </TabsTrigger>
466
461
  </TabsList>
467
462
 
@@ -470,7 +465,9 @@ export function SkillHealthGrid({
470
465
  <DropdownMenu>
471
466
  <DropdownMenuTrigger render={<Button variant="outline" size="sm" />}>
472
467
  <FilterIcon data-icon="inline-start" className="size-3.5" />
473
- {statusFilter && statusFilter !== "ALL" ? statusFilter.charAt(0) + statusFilter.slice(1).toLowerCase() : "Status"}
468
+ {statusFilter && statusFilter !== "ALL"
469
+ ? statusFilter.charAt(0) + statusFilter.slice(1).toLowerCase()
470
+ : "Status"}
474
471
  <ChevronDownIcon data-icon="inline-end" />
475
472
  </DropdownMenuTrigger>
476
473
  <DropdownMenuContent align="end" className="w-40">
@@ -478,14 +475,40 @@ export function SkillHealthGrid({
478
475
  value={statusFilter ?? "ALL"}
479
476
  onValueChange={(v) => onStatusFilterChange(v as SkillHealthStatus | "ALL")}
480
477
  >
481
- {([
482
- { label: "All", value: "ALL" as const, icon: <LayersIcon className="size-3.5" /> },
483
- { label: "Healthy", value: "HEALTHY" as const, icon: <CheckCircleIcon className="size-3.5 text-emerald-600" /> },
484
- { label: "Warning", value: "WARNING" as const, icon: <AlertTriangleIcon className="size-3.5 text-amber-500" /> },
485
- { label: "Critical", value: "CRITICAL" as const, icon: <XCircleIcon className="size-3.5 text-red-500" /> },
486
- { label: "Ungraded", value: "UNGRADED" as const, icon: <CircleDotIcon className="size-3.5 text-muted-foreground" /> },
487
- { label: "Unknown", value: "UNKNOWN" as const, icon: <HelpCircleIcon className="size-3.5 text-muted-foreground/60" /> },
488
- ] as const).map((f) => (
478
+ {(
479
+ [
480
+ {
481
+ label: "All",
482
+ value: "ALL" as const,
483
+ icon: <LayersIcon className="size-3.5" />,
484
+ },
485
+ {
486
+ label: "Healthy",
487
+ value: "HEALTHY" as const,
488
+ icon: <CheckCircleIcon className="size-3.5 text-emerald-600" />,
489
+ },
490
+ {
491
+ label: "Warning",
492
+ value: "WARNING" as const,
493
+ icon: <AlertTriangleIcon className="size-3.5 text-amber-500" />,
494
+ },
495
+ {
496
+ label: "Critical",
497
+ value: "CRITICAL" as const,
498
+ icon: <XCircleIcon className="size-3.5 text-red-500" />,
499
+ },
500
+ {
501
+ label: "Ungraded",
502
+ value: "UNGRADED" as const,
503
+ icon: <CircleDotIcon className="size-3.5 text-muted-foreground" />,
504
+ },
505
+ {
506
+ label: "Unknown",
507
+ value: "UNKNOWN" as const,
508
+ icon: <HelpCircleIcon className="size-3.5 text-muted-foreground/60" />,
509
+ },
510
+ ] as const
511
+ ).map((f) => (
489
512
  <DropdownMenuRadioItem key={f.value} value={f.value}>
490
513
  <span className="flex items-center gap-2">
491
514
  {f.icon}
@@ -506,26 +529,25 @@ export function SkillHealthGrid({
506
529
  <DropdownMenuContent align="end" className="w-40">
507
530
  {table
508
531
  .getAllColumns()
509
- .filter(
510
- (column) =>
511
- typeof column.accessorFn !== "undefined" &&
512
- column.getCanHide()
513
- )
532
+ .filter((column) => typeof column.accessorFn !== "undefined" && column.getCanHide())
514
533
  .map((column) => (
515
534
  <DropdownMenuCheckboxItem
516
535
  key={column.id}
517
536
  className="capitalize"
518
537
  checked={column.getIsVisible()}
519
- onCheckedChange={(value) =>
520
- column.toggleVisibility(!!value)
521
- }
538
+ onCheckedChange={(value) => column.toggleVisibility(!!value)}
522
539
  >
523
- {column.id === "scope" ? "Scope"
524
- : column.id === "passRate" ? "Pass Rate"
525
- : column.id === "uniqueSessions" ? "Sessions"
526
- : column.id === "lastSeen" ? "Last Seen"
527
- : column.id === "hasEvidence" ? "Evidence"
528
- : column.id}
540
+ {column.id === "scope"
541
+ ? "Scope"
542
+ : column.id === "passRate"
543
+ ? "Pass Rate"
544
+ : column.id === "uniqueSessions"
545
+ ? "Sessions"
546
+ : column.id === "lastSeen"
547
+ ? "Last Seen"
548
+ : column.id === "hasEvidence"
549
+ ? "Evidence"
550
+ : column.id}
529
551
  </DropdownMenuCheckboxItem>
530
552
  ))}
531
553
  </DropdownMenuContent>
@@ -533,7 +555,10 @@ export function SkillHealthGrid({
533
555
  </div>
534
556
  </div>
535
557
 
536
- <TabsContent value={activeView} className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6">
558
+ <TabsContent
559
+ value={activeView}
560
+ className="relative flex flex-col gap-4 overflow-auto px-4 lg:px-6"
561
+ >
537
562
  <div className="overflow-hidden rounded-lg border">
538
563
  <DndContext
539
564
  collisionDetection={closestCenter}
@@ -556,13 +581,12 @@ export function SkillHealthGrid({
556
581
  <div className="flex items-center gap-1">
557
582
  {header.isPlaceholder
558
583
  ? null
559
- : flexRender(
560
- header.column.columnDef.header,
561
- header.getContext()
562
- )}
563
- {header.column.getIsSorted() === "asc" ? " "
564
- : header.column.getIsSorted() === "desc" ? " ↓"
565
- : null}
584
+ : flexRender(header.column.columnDef.header, header.getContext())}
585
+ {header.column.getIsSorted() === "asc"
586
+ ? " ↑"
587
+ : header.column.getIsSorted() === "desc"
588
+ ? " "
589
+ : null}
566
590
  </div>
567
591
  </TableHead>
568
592
  ))}
@@ -571,10 +595,7 @@ export function SkillHealthGrid({
571
595
  </TableHeader>
572
596
  <TableBody className="**:data-[slot=table-cell]:first:w-8">
573
597
  {table.getRowModel().rows?.length ? (
574
- <SortableContext
575
- items={dataIds}
576
- strategy={verticalListSortingStrategy}
577
- >
598
+ <SortableContext items={dataIds} strategy={verticalListSortingStrategy}>
578
599
  {table.getRowModel().rows.map((row) => (
579
600
  <DraggableRow key={row.id} row={row} />
580
601
  ))}
@@ -610,7 +631,7 @@ export function SkillHealthGrid({
610
631
  <Select
611
632
  value={`${table.getState().pagination.pageSize}`}
612
633
  onValueChange={(value) => {
613
- table.setPageSize(Number(value))
634
+ table.setPageSize(Number(value));
614
635
  }}
615
636
  items={[10, 20, 50, 100].map((s) => ({
616
637
  label: `${s}`,
@@ -633,8 +654,7 @@ export function SkillHealthGrid({
633
654
  </div>
634
655
  {table.getRowModel().rows.length > 0 && (
635
656
  <div className="flex w-fit items-center justify-center text-sm font-medium">
636
- Page {table.getState().pagination.pageIndex + 1} of{" "}
637
- {table.getPageCount()}
657
+ Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
638
658
  </div>
639
659
  )}
640
660
  <div className="ml-auto flex items-center gap-2 lg:ml-0">
@@ -682,5 +702,5 @@ export function SkillHealthGrid({
682
702
  </div>
683
703
  </TabsContent>
684
704
  </Tabs>
685
- )
705
+ );
686
706
  }
@@ -5,6 +5,7 @@ import {
5
5
  HelpCircleIcon,
6
6
  XCircleIcon,
7
7
  } from "lucide-react";
8
+
8
9
  import type { SkillHealthStatus } from "../types";
9
10
 
10
11
  export const STATUS_CONFIG: Record<