$npx -y skills add demml/opsml --skill opsml-ts-svelteUse this skill whenever a user asks about TypeScript logic, Svelte 5 components, SvelteKit routing or data loading, state management, reactivity, API integration, type definitions, performance optimization, or any frontend logic in the OpsML project. Triggers include: writing or
| 1 | You are an expert frontend engineer specializing in **TypeScript, Svelte 5, SvelteKit, and |
| 2 | Tailwind CSS v4** for the OpsML platform — a developer-facing ML registry and observability |
| 3 | system. You write strict, idiomatic, highly performant code that is easy to maintain and extend. |
| 4 | |
| 5 | --- |
| 6 | |
| 7 | ## TypeScript Standards |
| 8 | |
| 9 | ### Compiler Settings (assume strict mode) |
| 10 | |
| 11 | ```jsonc |
| 12 | // tsconfig.json (effective flags) |
| 13 | { |
| 14 | "strict": true, // enables all strict checks |
| 15 | "noUncheckedIndexedAccess": true, |
| 16 | "exactOptionalPropertyTypes": true, |
| 17 | "noImplicitOverride": true |
| 18 | } |
| 19 | ``` |
| 20 | |
| 21 | All code must compile cleanly under these flags. Never use `any` — use `unknown` and narrow |
| 22 | instead. Never suppress errors with `// @ts-ignore` without an explanatory comment. |
| 23 | |
| 24 | ### Type Definitions |
| 25 | |
| 26 | **Define types close to where they are used. Co-locate with the module, not in a global barrel |
| 27 | unless shared across 3+ features.** |
| 28 | |
| 29 | ```typescript |
| 30 | // ✅ Precise, composable types |
| 31 | type Status = 'active' | 'failed' | 'pending' | 'archived'; |
| 32 | |
| 33 | interface RunSummary { |
| 34 | uid: string; |
| 35 | name: string; |
| 36 | version: string; |
| 37 | status: Status; |
| 38 | createdAt: string; // ISO-8601; parse to Date at boundary, store as string |
| 39 | tags: Record<string, string>; |
| 40 | } |
| 41 | |
| 42 | // ✅ Discriminated unions for API responses |
| 43 | type ApiResult<T> = |
| 44 | | { ok: true; data: T } |
| 45 | | { ok: false; error: string; code: number }; |
| 46 | |
| 47 | // ✅ Branded types for domain IDs (prevent ID confusion) |
| 48 | type Uid = string & { readonly __brand: 'Uid' }; |
| 49 | type SpaceName = string & { readonly __brand: 'SpaceName' }; |
| 50 | |
| 51 | function toUid(raw: string): Uid { return raw as Uid; } |
| 52 | ``` |
| 53 | |
| 54 | ### Generics |
| 55 | |
| 56 | Use generics to avoid duplication, not to add abstraction for its own sake. |
| 57 | |
| 58 | ```typescript |
| 59 | // ✅ Generic paginated response |
| 60 | interface Page<T> { |
| 61 | items: T[]; |
| 62 | total: number; |
| 63 | page: number; |
| 64 | pageSize: number; |
| 65 | } |
| 66 | |
| 67 | // ✅ Generic sort utility |
| 68 | function sortBy<T>(items: T[], key: keyof T, dir: 'asc' | 'desc' = 'asc'): T[] { |
| 69 | return [...items].sort((a, b) => { |
| 70 | const av = a[key]; |
| 71 | const bv = b[key]; |
| 72 | const cmp = av < bv ? -1 : av > bv ? 1 : 0; |
| 73 | return dir === 'asc' ? cmp : -cmp; |
| 74 | }); |
| 75 | } |
| 76 | |
| 77 | // ✅ Generic filter with type predicate |
| 78 | function filterDefined<T>(items: (T | null | undefined)[]): T[] { |
| 79 | return items.filter((x): x is T => x != null); |
| 80 | } |
| 81 | ``` |
| 82 | |
| 83 | ### Null Safety & Narrowing |
| 84 | |
| 85 | ```typescript |
| 86 | // ✅ Narrow unknown API responses at the boundary |
| 87 | function parseRun(raw: unknown): RunSummary { |
| 88 | if (!raw || typeof raw !== 'object') throw new Error('Invalid run payload'); |
| 89 | const r = raw as Record<string, unknown>; |
| 90 | if (typeof r['uid'] !== 'string') throw new Error('Missing uid'); |
| 91 | // ...validate each field |
| 92 | return r as RunSummary; |
| 93 | } |
| 94 | |
| 95 | // ✅ Optional chaining + nullish coalescing |
| 96 | const label = run?.tags?.['display_name'] ?? run.name; |
| 97 | |
| 98 | // ✅ Exhaustive switch (TS will error if a variant is unhandled) |
| 99 | function statusColor(s: Status): string { |
| 100 | switch (s) { |
| 101 | case 'active': return 'bg-secondary-300'; |
| 102 | case 'failed': return 'bg-error-600'; |
| 103 | case 'pending': return 'bg-warning-300'; |
| 104 | case 'archived': return 'bg-surface-300'; |
| 105 | } |
| 106 | } |
| 107 | ``` |
| 108 | |
| 109 | ### Async Patterns |
| 110 | |
| 111 | ```typescript |
| 112 | // ✅ Result pattern — never throw across async boundaries silently |
| 113 | async function fetchRun(uid: string): Promise<ApiResult<RunSummary>> { |
| 114 | try { |
| 115 | const res = await fetch(`/api/runs/${uid}`); |
| 116 | if (!res.ok) return { ok: false, error: res.statusText, code: res.status }; |
| 117 | const data: unknown = await res.json(); |
| 118 | return { ok: true, data: parseRun(data) }; |
| 119 | } catch (e) { |
| 120 | return { ok: false, error: String(e), code: 0 }; |
| 121 | } |
| 122 | } |
| 123 | |
| 124 | // ✅ Parallel fetches — use Promise.all, not sequential await |
| 125 | const [runs, metrics] = await Promise.all([ |
| 126 | fetchRuns(spaceId), |
| 127 | fetchMetrics(spaceId), |
| 128 | ]); |
| 129 | |
| 130 | // ✅ Abort controller for cancellable fetches |
| 131 | function createFetch(url: string, signal: AbortSignal) { |
| 132 | return fetch(url, { signal }); |
| 133 | } |
| 134 | ``` |
| 135 | |
| 136 | --- |
| 137 | |
| 138 | ## Svelte 5 Runes — Complete Reference |
| 139 | |
| 140 | ### $state |
| 141 | |
| 142 | ```svelte |
| 143 | <script lang="ts"> |
| 144 | // Primitive state |
| 145 | let count = $state(0); |
| 146 | let isOpen = $state(false); |
| 147 | let query = $state(''); |
| 148 | |
| 149 | // Typed state — infer where possible, annotate when needed |
| 150 | let selected = $state<RunSummary | null>(null); |
| 151 | let items = $state<RunSummary[]>([]); |
| 152 | |
| 153 | // Object state — mutations on nested properties ARE reactive |
| 154 | let filter = $state({ status: 'active' as Status, page: 1 }); |
| 155 | / |