@nurix/etna 0.18.19 → 0.18.20
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.
- package/data/harness-index.json +1 -1
- package/data/harnesses/code/harness.json +2 -2
- package/data/harnesses/code/skills/design/SKILL.md +65 -0
- package/data/harnesses/code/skills/design/data/catalog/rules.csv +132 -0
- package/data/harnesses/code/skills/design/data/design-md/SCHEMA.md +317 -0
- package/data/harnesses/code/skills/design/data/design-md/library/airbnb.md +235 -0
- package/data/harnesses/code/skills/design/data/design-md/library/airtable.md +232 -0
- package/data/harnesses/code/skills/design/data/design-md/library/anthropic-claude.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/apple.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/binance.md +223 -0
- package/data/harnesses/code/skills/design/data/design-md/library/bmw.md +219 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cal.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/clay.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/clickhouse.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cohere.md +227 -0
- package/data/harnesses/code/skills/design/data/design-md/library/coinbase.md +237 -0
- package/data/harnesses/code/skills/design/data/design-md/library/composio.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/cursor.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/elevenlabs.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/expo.md +228 -0
- package/data/harnesses/code/skills/design/data/design-md/library/figma.md +221 -0
- package/data/harnesses/code/skills/design/data/design-md/library/framer.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/hashicorp.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/ibm.md +234 -0
- package/data/harnesses/code/skills/design/data/design-md/library/intercom.md +240 -0
- package/data/harnesses/code/skills/design/data/design-md/library/kraken.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/linear-app.md +237 -0
- package/data/harnesses/code/skills/design/data/design-md/library/lovable.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/meta.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/minimax.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mintlify.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/miro.md +233 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mistral-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/mongodb.md +236 -0
- package/data/harnesses/code/skills/design/data/design-md/library/notion.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/nvidia.md +222 -0
- package/data/harnesses/code/skills/design/data/design-md/library/ollama.md +226 -0
- package/data/harnesses/code/skills/design/data/design-md/library/opencode-ai.md +223 -0
- package/data/harnesses/code/skills/design/data/design-md/library/posthog.md +234 -0
- package/data/harnesses/code/skills/design/data/design-md/library/raycast.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/replicate.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/resend.md +285 -0
- package/data/harnesses/code/skills/design/data/design-md/library/revolut.md +225 -0
- package/data/harnesses/code/skills/design/data/design-md/library/runwayml.md +219 -0
- package/data/harnesses/code/skills/design/data/design-md/library/sanity.md +251 -0
- package/data/harnesses/code/skills/design/data/design-md/library/sentry.md +244 -0
- package/data/harnesses/code/skills/design/data/design-md/library/shopify.md +227 -0
- package/data/harnesses/code/skills/design/data/design-md/library/slack.md +240 -0
- package/data/harnesses/code/skills/design/data/design-md/library/spacex.md +252 -0
- package/data/harnesses/code/skills/design/data/design-md/library/spotify.md +248 -0
- package/data/harnesses/code/skills/design/data/design-md/library/stripe.md +241 -0
- package/data/harnesses/code/skills/design/data/design-md/library/supabase.md +255 -0
- package/data/harnesses/code/skills/design/data/design-md/library/superhuman.md +226 -0
- package/data/harnesses/code/skills/design/data/design-md/library/tesla.md +233 -0
- package/data/harnesses/code/skills/design/data/design-md/library/theverge.md +248 -0
- package/data/harnesses/code/skills/design/data/design-md/library/together-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/uber.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/vercel.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/voltagent.md +245 -0
- package/data/harnesses/code/skills/design/data/design-md/library/warp.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/webflow.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/wired.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/wise.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/x-ai.md +220 -0
- package/data/harnesses/code/skills/design/data/design-md/library/zapier.md +238 -0
- package/data/harnesses/code/skills/design/data/design-md/nurix.md +217 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/command-center.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/create-edit.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/dashboard.tsx +248 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/entity-detail.tsx +245 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/list-detail.tsx +223 -0
- package/data/harnesses/code/skills/design/data/operator-pack/exemplars/log-stream.tsx +249 -0
- package/data/harnesses/code/skills/design/data/operator-pack/fixtures.md +88 -0
- package/data/harnesses/code/skills/design/data/operator-pack/surface-catalog.md +132 -0
- package/data/harnesses/code/skills/design/data/operator-pack/trust-rubric.md +76 -0
- package/data/harnesses/code/skills/design/references/create.md +126 -0
- package/data/harnesses/code/skills/design/references/diagnosis.md +108 -0
- package/data/harnesses/code/skills/design/references/hard-rules.md +155 -0
- package/data/harnesses/code/skills/design/references/improve.md +85 -0
- package/data/harnesses/code/skills/design/references/interaction.md +77 -0
- package/data/harnesses/code/skills/design/references/introspect.md +100 -0
- package/data/harnesses/code/skills/design/references/lifecycle.md +73 -0
- package/data/harnesses/code/skills/design/references/personality.md +504 -0
- package/data/harnesses/code/skills/design/references/structure.md +138 -0
- package/data/harnesses/code/skills/design/scripts/derive-personality.mjs +706 -0
- package/data/harnesses/code/skills/design/scripts/detector.mjs +898 -0
- package/data/harnesses/code/skills/design/scripts/generate-fixtures.mjs +551 -0
- package/data/harnesses/code/skills/design/scripts/hook-config.json +11 -0
- package/data/harnesses/code/skills/design/scripts/phase-gate.mjs +386 -0
- package/data/harnesses/code/skills/design/scripts/preship-sweep.mjs +280 -0
- package/data/harnesses/code/skills/design/scripts/render-rung.mjs +690 -0
- package/data/harnesses/code/skills/design/scripts/scaffold.mjs +552 -0
- package/data/harnesses/code/skills/design/scripts/self-integrity.mjs +432 -0
- package/package.json +1 -1
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
// skills/design/data/operator-pack/exemplars/log-stream.tsx
|
|
2
|
+
// personality-invariant: composition anchor, not a look source.
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { ScrollText, Search, ChevronRight, ChevronDown, Circle, TriangleAlert, WifiOff, RotateCw, ListFilter, Inbox } from "lucide-react";
|
|
5
|
+
import { Button } from "@nurix/components/button";
|
|
6
|
+
import { Input } from "@nurix/components/input";
|
|
7
|
+
import { Switch } from "@nurix/components/switch";
|
|
8
|
+
import { Badge } from "@nurix/components/badge";
|
|
9
|
+
import { Kbd } from "@nurix/components/kbd";
|
|
10
|
+
import { Skeleton } from "@nurix/components/skeleton";
|
|
11
|
+
import { EmptyState } from "@nurix/components/empty-state";
|
|
12
|
+
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@nurix/components/native-select";
|
|
13
|
+
import { cn } from "@nurix/components/utils";
|
|
14
|
+
|
|
15
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
16
|
+
|
|
17
|
+
export interface LogLine {
|
|
18
|
+
id: string;
|
|
19
|
+
timestamp: string;
|
|
20
|
+
level: LogLevel;
|
|
21
|
+
source: string;
|
|
22
|
+
message: string;
|
|
23
|
+
status: number | null;
|
|
24
|
+
latencyMs: number | null;
|
|
25
|
+
detail: Record<string, string | number>;
|
|
26
|
+
}
|
|
27
|
+
export interface DegradedInfo {
|
|
28
|
+
droppedLines: number;
|
|
29
|
+
reason: string;
|
|
30
|
+
}
|
|
31
|
+
export interface LogStreamProps {
|
|
32
|
+
lines?: LogLine[];
|
|
33
|
+
isLoading?: boolean;
|
|
34
|
+
error?: string | null;
|
|
35
|
+
degraded?: DegradedInfo | null;
|
|
36
|
+
initiallyFollowing?: boolean;
|
|
37
|
+
onRetry?: () => void;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const RANK: Record<LogLevel, number> = { debug: 0, info: 1, warn: 2, error: 3 };
|
|
41
|
+
const RANK_FLOOR: Record<string, number> = { all: 0, info: 1, warn: 2, error: 3 };
|
|
42
|
+
const LEVEL_META: Record<LogLevel, { label: string; className: string }> = {
|
|
43
|
+
error: { label: "ERR", className: "text-destructive" },
|
|
44
|
+
warn: { label: "WARN", className: "text-warning" },
|
|
45
|
+
info: { label: "INFO", className: "text-foreground" },
|
|
46
|
+
debug: { label: "DBG", className: "text-muted-foreground" },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const LEVEL_OPTIONS: { value: string; label: string }[] = [
|
|
50
|
+
{ value: "all", label: "All levels" },
|
|
51
|
+
{ value: "info", label: "Info and up" },
|
|
52
|
+
{ value: "warn", label: "Warnings and up" },
|
|
53
|
+
{ value: "error", label: "Errors only" },
|
|
54
|
+
];
|
|
55
|
+
// Slice the stable "HH:MM:SS.mmm" clock out of an ISO timestamp (timezone-free, deterministic).
|
|
56
|
+
function formatClock(iso: string): string {
|
|
57
|
+
return iso.slice(11, 23);
|
|
58
|
+
}
|
|
59
|
+
// Test whether a line matches the free-text filter across message and source.
|
|
60
|
+
function matchesQuery(line: LogLine, needle: string): boolean {
|
|
61
|
+
if (!needle) return true;
|
|
62
|
+
const q = needle.toLowerCase();
|
|
63
|
+
return line.message.toLowerCase().includes(q) || line.source.toLowerCase().includes(q);
|
|
64
|
+
}
|
|
65
|
+
// Append-only edge-ingest log stream: filter, tail-follow, and inline-expand immutable events.
|
|
66
|
+
export function LogStream({ lines, isLoading = false, error = null, degraded = null, initiallyFollowing = true, onRetry }: LogStreamProps) {
|
|
67
|
+
const seed = lines ?? SEED_LINES;
|
|
68
|
+
const [query, setQuery] = React.useState("");
|
|
69
|
+
const [level, setLevel] = React.useState("all");
|
|
70
|
+
const [isFollowing, setIsFollowing] = React.useState(initiallyFollowing);
|
|
71
|
+
const [liveCount, setLiveCount] = React.useState(0);
|
|
72
|
+
const [activeIndex, setActiveIndex] = React.useState(0);
|
|
73
|
+
const [expandedId, setExpandedId] = React.useState<string | null>(null);
|
|
74
|
+
const rowRefs = React.useRef<(HTMLButtonElement | null)[]>([]);
|
|
75
|
+
const bottomRef = React.useRef<HTMLDivElement | null>(null);
|
|
76
|
+
const searchRef = React.useRef<HTMLInputElement | null>(null);
|
|
77
|
+
|
|
78
|
+
const allLines = [...seed, ...STREAM_LINES.slice(0, liveCount)];
|
|
79
|
+
const visible = allLines.filter((l) => RANK[l.level] >= RANK_FLOOR[level] && matchesQuery(l, query));
|
|
80
|
+
const hasFilter = query.trim() !== "" || level !== "all";
|
|
81
|
+
|
|
82
|
+
// Advance the deterministic tail one line at a time while following, until the pool drains.
|
|
83
|
+
React.useEffect(() => {
|
|
84
|
+
if (!isFollowing || liveCount >= STREAM_LINES.length) return;
|
|
85
|
+
const id = setInterval(() => setLiveCount((n) => Math.min(STREAM_LINES.length, n + 1)), 2000);
|
|
86
|
+
return () => clearInterval(id);
|
|
87
|
+
}, [isFollowing, liveCount]);
|
|
88
|
+
|
|
89
|
+
// Keep the newest line in view whenever the tail grows and follow is engaged.
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
if (isFollowing) bottomRef.current?.scrollIntoView({ block: "end" });
|
|
92
|
+
}, [visible.length, isFollowing]);
|
|
93
|
+
|
|
94
|
+
// Roving-tabindex row travel: arrows and Home/End move focus without leaving the list.
|
|
95
|
+
function onListKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
|
|
96
|
+
const last = visible.length - 1;
|
|
97
|
+
let next = activeIndex;
|
|
98
|
+
if (e.key === "ArrowDown") next = Math.min(last, activeIndex + 1);
|
|
99
|
+
else if (e.key === "ArrowUp") next = Math.max(0, activeIndex - 1);
|
|
100
|
+
else if (e.key === "Home") next = 0;
|
|
101
|
+
else if (e.key === "End") next = last;
|
|
102
|
+
else return;
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
setActiveIndex(next);
|
|
105
|
+
rowRefs.current[next]?.focus();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Focus the filter input on "/" from anywhere outside a text field.
|
|
109
|
+
function onRootKeyDown(e: React.KeyboardEvent<HTMLElement>) {
|
|
110
|
+
if (e.key === "/" && !(e.target as HTMLElement).matches("input, textarea, [contenteditable]")) {
|
|
111
|
+
e.preventDefault();
|
|
112
|
+
searchRef.current?.focus();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<section onKeyDown={onRootKeyDown} data-density-step="tighter" className="flex min-h-[100dvh] flex-col bg-background text-foreground">
|
|
118
|
+
<header className="flex flex-col gap-2 border-b border-border px-4 py-3">
|
|
119
|
+
<div className="flex items-center justify-between gap-3">
|
|
120
|
+
<div className="flex items-center gap-2">
|
|
121
|
+
<ScrollText aria-hidden className="size-4 text-muted-foreground" />
|
|
122
|
+
<h1 className="text-base font-semibold">Edge ingest log</h1>
|
|
123
|
+
<Badge variant="outline" className="gap-1.5">
|
|
124
|
+
<Circle aria-hidden className={cn("size-2 fill-current", isFollowing ? "text-success animate-pulse motion-reduce:animate-none" : "text-muted-foreground")} />{isFollowing ? "Live" : "Paused"}
|
|
125
|
+
</Badge>
|
|
126
|
+
</div>
|
|
127
|
+
<label className="flex items-center gap-2 text-xs text-muted-foreground">Follow tail
|
|
128
|
+
<Switch checked={isFollowing} onChange={setIsFollowing} aria-label="Follow the live tail" />
|
|
129
|
+
</label>
|
|
130
|
+
</div>
|
|
131
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
132
|
+
<div className="relative min-w-0 flex-1">
|
|
133
|
+
<Search aria-hidden className="pointer-events-none absolute left-2 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
|
|
134
|
+
<Input ref={searchRef} type="search" aria-label="Filter log lines" value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Filter by message or source" className="pl-8" />
|
|
135
|
+
</div>
|
|
136
|
+
<Select value={level} onValueChange={setLevel}>
|
|
137
|
+
<SelectTrigger aria-label="Minimum severity" className="w-44"><SelectValue /></SelectTrigger>
|
|
138
|
+
<SelectContent>{LEVEL_OPTIONS.map((o) => (<SelectItem key={o.value} value={o.value}>{o.label}</SelectItem>))}</SelectContent>
|
|
139
|
+
</Select>
|
|
140
|
+
<span className="hidden items-center gap-1 text-xs text-muted-foreground sm:flex">
|
|
141
|
+
<Kbd size="sm">↑</Kbd><Kbd size="sm">↓</Kbd> move<Kbd size="sm">Enter</Kbd> expand
|
|
142
|
+
</span>
|
|
143
|
+
</div>
|
|
144
|
+
</header>
|
|
145
|
+
{degraded ? (
|
|
146
|
+
<div role="status" className="flex items-center gap-2 border-b border-border bg-muted px-4 py-1.5 text-xs">
|
|
147
|
+
<TriangleAlert aria-hidden className="size-4 shrink-0 text-warning" />
|
|
148
|
+
<span className="font-medium">Buffer truncated.</span>
|
|
149
|
+
<span className="tabular-nums text-muted-foreground">{degraded.droppedLines.toLocaleString()} lines dropped. {degraded.reason}</span>
|
|
150
|
+
</div>
|
|
151
|
+
) : null}
|
|
152
|
+
{error ? (
|
|
153
|
+
<div role="alert" className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-4 py-8 text-center">
|
|
154
|
+
<WifiOff aria-hidden className="size-8 text-destructive" />
|
|
155
|
+
<div className="space-y-1">
|
|
156
|
+
<p className="text-sm font-semibold">Log stream disconnected</p>
|
|
157
|
+
<p className="max-w-md text-sm text-muted-foreground">{error}</p>
|
|
158
|
+
</div>
|
|
159
|
+
<div role="status" className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
160
|
+
<RotateCw aria-hidden className="size-4 animate-spin motion-reduce:animate-none" />Reconnecting to the collector…
|
|
161
|
+
</div>
|
|
162
|
+
<Button size="sm" onClick={onRetry}>Retry connection</Button>
|
|
163
|
+
</div>
|
|
164
|
+
) : isLoading ? (
|
|
165
|
+
<div aria-hidden className="min-h-0 flex-1 overflow-hidden font-mono">
|
|
166
|
+
{SKELETON_ROWS.map((w, i) => (
|
|
167
|
+
<div key={i} className="flex items-center gap-3 border-b border-border px-4 py-1">
|
|
168
|
+
<Skeleton className="size-3.5 shrink-0 rounded-full" /><Skeleton className="h-3 w-24 shrink-0" /><Skeleton className="h-3 w-12 shrink-0" />
|
|
169
|
+
<Skeleton className="h-3 w-36 shrink-0" /><Skeleton className={cn("h-3", w)} />
|
|
170
|
+
</div>
|
|
171
|
+
))}
|
|
172
|
+
</div>
|
|
173
|
+
) : (
|
|
174
|
+
<div className="flex items-center gap-3 border-b border-border px-4 py-1.5 text-xs font-medium uppercase tracking-wide text-muted-foreground">
|
|
175
|
+
<span className="w-24 shrink-0 pl-6">Time</span><span className="w-12 shrink-0">Level</span><span className="w-36 shrink-0">Source</span>
|
|
176
|
+
<span className="min-w-0 flex-1">Message</span><span className="w-14 shrink-0 text-right">Status</span><span className="w-24 shrink-0 text-right">Latency</span>
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
{!error && !isLoading ? (
|
|
180
|
+
<div role="log" aria-live="polite" aria-relevant="additions" aria-label="Edge ingest log stream, newest last" onKeyDown={onListKeyDown} className="min-h-0 flex-1 overflow-auto font-mono">
|
|
181
|
+
{visible.length === 0 ? (
|
|
182
|
+
<div className="flex h-full items-center justify-center px-4 py-8">
|
|
183
|
+
{hasFilter ? (
|
|
184
|
+
<EmptyState icon={<ListFilter aria-hidden className="size-8 text-muted-foreground" />} title="No lines match these filters" description="Events are still arriving, but none match the current search and severity. Loosen the filter to see the tail again." action={<Button size="sm" variant="secondary" onClick={() => { setQuery(""); setLevel("all"); }}>Clear filters</Button>} />
|
|
185
|
+
) : (
|
|
186
|
+
<EmptyState icon={<Inbox aria-hidden className="size-8 text-muted-foreground" />} title="No events received yet" description="This stream is connected and following the edge-ingest collector. New lines appear here as requests are processed." />
|
|
187
|
+
)}
|
|
188
|
+
</div>
|
|
189
|
+
) : (
|
|
190
|
+
<>
|
|
191
|
+
{visible.map((line, i) => {
|
|
192
|
+
const meta = LEVEL_META[line.level];
|
|
193
|
+
const isOpen = expandedId === line.id;
|
|
194
|
+
return (
|
|
195
|
+
<div key={line.id} className="border-b border-border">
|
|
196
|
+
<button ref={(el) => { rowRefs.current[i] = el; }} tabIndex={i === activeIndex ? 0 : -1} aria-expanded={isOpen} onClick={() => { setActiveIndex(i); setExpandedId((cur) => (cur === line.id ? null : line.id)); }} className="flex w-full items-center gap-3 px-4 py-1 text-left hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring">
|
|
197
|
+
{isOpen ? <ChevronDown aria-hidden className="size-3.5 shrink-0 text-muted-foreground" /> : <ChevronRight aria-hidden className="size-3.5 shrink-0 text-muted-foreground" />}
|
|
198
|
+
<span className="w-24 shrink-0 tabular-nums text-muted-foreground">{formatClock(line.timestamp)}</span>
|
|
199
|
+
<span className={cn("w-12 shrink-0 font-medium", meta.className)}>{meta.label}</span>
|
|
200
|
+
<span className="w-36 shrink-0 truncate text-muted-foreground">{line.source}</span>
|
|
201
|
+
<span className="min-w-0 flex-1 truncate">{line.message}</span>
|
|
202
|
+
<span className="w-14 shrink-0 text-right tabular-nums text-muted-foreground">{line.status ?? "–"}</span>
|
|
203
|
+
<span className="w-24 shrink-0 text-right tabular-nums text-muted-foreground">{line.latencyMs == null ? "–" : `${line.latencyMs} ms`}</span>
|
|
204
|
+
</button>
|
|
205
|
+
{isOpen ? (
|
|
206
|
+
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 border-t border-border bg-muted px-4 py-2 pl-11 text-muted-foreground">
|
|
207
|
+
{Object.entries(line.detail).map(([k, v]) => (
|
|
208
|
+
<React.Fragment key={k}><dt>{k}</dt><dd className="truncate tabular-nums text-foreground">{String(v)}</dd></React.Fragment>
|
|
209
|
+
))}
|
|
210
|
+
</dl>
|
|
211
|
+
) : null}
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
})}
|
|
215
|
+
<div ref={bottomRef} aria-hidden />
|
|
216
|
+
</>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
) : null}
|
|
220
|
+
<footer className="flex items-center justify-between gap-3 border-t border-border px-4 py-1.5 text-xs text-muted-foreground">
|
|
221
|
+
<span className="tabular-nums">Showing {visible.length} of {allLines.length} lines</span>
|
|
222
|
+
<span className="hidden items-center gap-1 sm:flex"><Kbd size="sm">/</Kbd> to filter</span>
|
|
223
|
+
</footer>
|
|
224
|
+
</section>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// exemplar fixture data — seeded, domain-shaped
|
|
229
|
+
const SEED_LINES: LogLine[] = [
|
|
230
|
+
{ id: "l-01", timestamp: "2026-07-04T09:12:03.109Z", level: "debug", source: "edge-ingest.tls", message: "TLS handshake completed for client 10.2.4.19", status: null, latencyMs: 4, detail: { cipher: "TLS_AES_128_GCM", client_ip: "10.2.4.19", alpn: "h2" } },
|
|
231
|
+
{ id: "l-02", timestamp: "2026-07-04T09:12:03.842Z", level: "info", source: "edge-ingest.router", message: "POST /v1/events accepted, 3 records queued", status: 202, latencyMs: 34, detail: { upstream: "queue.primary", partition: 4, trace_id: "trc_a19f3c", bytes_in: 2048 } },
|
|
232
|
+
{ id: "l-03", timestamp: "2026-07-04T09:12:04.210Z", level: "info", source: "edge-ingest.router", message: "GET /v1/streams/live upgraded to websocket", status: 101, latencyMs: 12, detail: { upstream: "gateway.ws", trace_id: "trc_b2e0d1", conn_id: "ws_7741" } },
|
|
233
|
+
{ id: "l-04", timestamp: "2026-07-04T09:12:05.556Z", level: "warn", source: "edge-ingest.ratelimit", message: "Client tenant_4821 throttled, 429 returned", status: 429, latencyMs: 8, detail: { tenant: "tenant_4821", limit: "600/min", retry_after_s: 30, trace_id: "trc_c4d1a8" } },
|
|
234
|
+
{ id: "l-05", timestamp: "2026-07-04T09:12:06.001Z", level: "debug", source: "edge-ingest.cache", message: "Config snapshot refreshed from control-plane", status: null, latencyMs: 21, detail: { version: 1183, keys: 47, source_region: "eu-central" } },
|
|
235
|
+
{ id: "l-06", timestamp: "2026-07-04T09:12:06.733Z", level: "error", source: "edge-ingest.writer", message: "Upstream queue.primary timed out after 5000 ms", status: 504, latencyMs: 5003, detail: { upstream: "queue.primary", trace_id: "trc_d9a2f0", attempt: 2, error: "context deadline exceeded" } },
|
|
236
|
+
{ id: "l-07", timestamp: "2026-07-04T09:12:07.418Z", level: "info", source: "edge-ingest.writer", message: "Retry succeeded on queue.secondary", status: 202, latencyMs: 61, detail: { upstream: "queue.secondary", trace_id: "trc_d9a2f0", attempt: 3, bytes_in: 2048 } },
|
|
237
|
+
{ id: "l-08", timestamp: "2026-07-04T09:12:08.902Z", level: "warn", source: "edge-ingest.schema", message: "Event field ts coerced from string to int64", status: 202, latencyMs: 39, detail: { trace_id: "trc_e5f712", field: "ts", records: 3 } },
|
|
238
|
+
{ id: "l-09", timestamp: "2026-07-04T09:12:09.377Z", level: "info", source: "edge-ingest.router", message: "POST /v1/events accepted, 12 records queued", status: 202, latencyMs: 44, detail: { upstream: "queue.primary", partition: 1, trace_id: "trc_f00312", bytes_in: 8192 } },
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
const STREAM_LINES: LogLine[] = [
|
|
242
|
+
{ id: "s-01", timestamp: "2026-07-04T09:12:10.845Z", level: "info", source: "edge-ingest.router", message: "GET /v1/health checked by readiness probe", status: 200, latencyMs: 3, detail: { probe: "readiness", trace_id: "trc_1a4409" } },
|
|
243
|
+
{ id: "s-02", timestamp: "2026-07-04T09:12:12.110Z", level: "debug", source: "edge-ingest.cache", message: "Evicted 128 stale entries from hot set", status: null, latencyMs: 2, detail: { evicted: 128, hot_set: 4096 } },
|
|
244
|
+
{ id: "s-03", timestamp: "2026-07-04T09:12:13.492Z", level: "error", source: "edge-ingest.auth", message: "Token signature rejected for tenant_5590", status: 401, latencyMs: 6, detail: { tenant: "tenant_5590", reason: "kid not found", trace_id: "trc_2b7133" } },
|
|
245
|
+
{ id: "s-04", timestamp: "2026-07-04T09:12:14.938Z", level: "warn", source: "edge-ingest.ratelimit", message: "Client tenant_4821 at 92% of quota", status: 200, latencyMs: 7, detail: { tenant: "tenant_4821", used: "552/600", trace_id: "trc_3c0955" } },
|
|
246
|
+
{ id: "s-05", timestamp: "2026-07-04T09:12:16.201Z", level: "info", source: "edge-ingest.writer", message: "Batch flushed to queue.primary, 12 records", status: 202, latencyMs: 58, detail: { upstream: "queue.primary", trace_id: "trc_f00312", flushed: 12 } },
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
const SKELETON_ROWS = ["w-2/3", "w-1/2", "w-3/4", "w-2/5", "w-3/5", "w-1/2", "w-2/3", "w-1/3", "w-3/5", "w-1/2"];
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Fixtures — data as design material
|
|
2
|
+
|
|
3
|
+
The rows an operator surface renders are not scaffolding for the "real" design — they **are** the design input. Density, truncation, overflow, and every lifecycle state are properties of *the data under them*: a queue is dense or sparse because of how many rows it holds and how long their strings run; a cell truncates or wraps because of what it contains; an empty state exists only when the query returns nothing. Design them against three short rows named "Item 1" and you have designed nothing that production will look like.
|
|
4
|
+
|
|
5
|
+
So fixtures come **first** — generated before any surface is composed (the create `fixtures` phase, [create.md](../../references/create.md)) — and they are realistic, seeded, and volume-bearing. **Rows exist to count.** The `dep`/`spc` density thresholds and the `sta` state-presence rules are only *bindable* against real volumes, long strings, and unhappy values; without them the proxies (rows per viewport, cells per screen) have nothing to measure.
|
|
6
|
+
|
|
7
|
+
**Loads** at the create `fixtures` phase, and whenever improve/introspect diagnoses density or state coverage on an existing UI. Reads the entity and `temporal` facts from `docs/design/structure.md`. Pairs with [lifecycle.md](../../references/lifecycle.md) — this file produces the data those states are filled with.
|
|
8
|
+
|
|
9
|
+
## What "realistic" means
|
|
10
|
+
|
|
11
|
+
A fixture requirement is not "some data" — it is data shaped like production, exercising the conditions the hard dimensions live in. Every entity's fixtures satisfy all of these:
|
|
12
|
+
|
|
13
|
+
- **Domain-shaped.** Names, values, and identifiers that could be a real row — real assignee names, real-looking subjects, plausible timestamps and statuses. Never `Item 1` / `Row 2`, never lorem ipsum, never `John Doe` / `Acme` / `Nexus`.
|
|
14
|
+
- **Seeded and deterministic.** Same seed → same rows, every run. Randomness runs through falso's seed API (`seed(...)` before generation) or a local PRNG — never `Math.random()`. A fixture that shifts between runs is not a fixture.
|
|
15
|
+
- **Volume that exercises density.** Queues and tables carry realistic depth — hundreds of rows (a 500-row queue), not a handful. Density is unmeasurable on five rows.
|
|
16
|
+
- **Long strings that exercise truncation.** Some subjects, names, and paths run long enough to force the truncation and wrapping decisions — mixed with short ones, so both the tight and the overflowing case are on screen at once.
|
|
17
|
+
- **Unhappy values.** `null` and missing fields, zero, negative numbers, huge numbers, empty collections, overdue and far-future dates, statuses at every lifecycle stage. The happy row is the rare row.
|
|
18
|
+
- **Degraded payloads.** Partial objects and failed enrichments — a row whose join didn't resolve, a record whose secondary fetch returned nothing — so the `partial/degraded` state has real data to render.
|
|
19
|
+
- **Explicit empty and error variants per surface.** Every surface's declared states ([lifecycle.md](../../references/lifecycle.md)) get their own fixture: an empty variant (`[]`) and a degraded variant. Error is the no-data case the state-shell owns, but the pack still declares the state so it is exercised.
|
|
20
|
+
|
|
21
|
+
## Module shape
|
|
22
|
+
|
|
23
|
+
One module per entity, at `src/fixtures/<entity>.ts`, exporting **typed arrays** plus the **seed constant** it was generated under. Per-surface state variants are named exports co-located in the entity's module, keyed to the surface states they feed (`emptyTickets`, `degradedTickets`). Surfaces import these arrays; the model **never** freehands data inline in a surface file.
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
// src/fixtures/tickets.ts — seeded ticket-queue fixtures (generated; do not hand-edit rows)
|
|
27
|
+
import { seed, randUuid, randFullName, randCompanyName, randRecentDate, randPastDate } from "@ngneat/falso";
|
|
28
|
+
import type { Ticket } from "../types/ticket";
|
|
29
|
+
|
|
30
|
+
export const TICKETS_SEED = 4173;
|
|
31
|
+
seed(String(TICKETS_SEED));
|
|
32
|
+
|
|
33
|
+
const STATUSES = ["new", "open", "pending", "escalated", "resolved", "closed"] as const;
|
|
34
|
+
const LONG_SUBJECT =
|
|
35
|
+
"Checkout returns 500 on multi-currency carts after the 3.4 billing migration when tax is estimated";
|
|
36
|
+
|
|
37
|
+
// One row shaped like production — real names, real timestamps, unhappy values seeded in by index.
|
|
38
|
+
function buildTicket(_: unknown, i: number): Ticket {
|
|
39
|
+
const overdue = i % 7 === 0;
|
|
40
|
+
const enrichmentFailed = i % 11 === 0;
|
|
41
|
+
return {
|
|
42
|
+
id: randUuid(),
|
|
43
|
+
subject: i % 5 === 0 ? LONG_SUBJECT : `Payment retry failing for ${randCompanyName()}`,
|
|
44
|
+
status: STATUSES[i % STATUSES.length],
|
|
45
|
+
assignee: i % 9 === 0 ? null : randFullName(),
|
|
46
|
+
account: enrichmentFailed ? null : randCompanyName(),
|
|
47
|
+
slaMinutesRemaining: overdue ? -Math.floor(i * 3.5) : 240 - (i % 240),
|
|
48
|
+
replies: i % 13 === 0 ? 0 : i % 47,
|
|
49
|
+
dueAt: overdue ? randPastDate() : randRecentDate(),
|
|
50
|
+
createdAt: randPastDate(),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 500 rows exercise queue density; statuses distributed across the full lifecycle.
|
|
55
|
+
export const tickets: Ticket[] = Array.from({ length: 500 }, buildTicket);
|
|
56
|
+
|
|
57
|
+
// The same surface, empty: a first-time or filtered-to-nothing queue.
|
|
58
|
+
export const emptyTickets: Ticket[] = [];
|
|
59
|
+
|
|
60
|
+
// The same surface, degraded: enrichment failed — assignee and account come back null.
|
|
61
|
+
export const degradedTickets: Ticket[] = tickets
|
|
62
|
+
.slice(0, 12)
|
|
63
|
+
.map((t) => ({ ...t, assignee: null, account: null, slaMinutesRemaining: 0 }));
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
The `Ticket` type lives in `src/types/`; fixtures import it, never redefine it. The seed constant is exported so the data is reproducible and traceable — regeneration under the same seed yields byte-identical rows.
|
|
67
|
+
|
|
68
|
+
## The generator contract
|
|
69
|
+
|
|
70
|
+
`generate-fixtures.mjs <structure.md> --seed <n> --out <dir>` derives the entity shapes from the structure map and emits these modules. It never guesses a shape — the map is the source:
|
|
71
|
+
|
|
72
|
+
- **From each entity's `attributes`** it derives the field set and picks a falso generator per field by name and type (a `name` field → `randFullName`, a `*_at` date → `randRecentDate`, an amount → a seeded number in a plausible range).
|
|
73
|
+
- **From each entity's `lifecycle`** it distributes rows across the status values, so every state in the model appears in the data.
|
|
74
|
+
- **From `relationships`** it wires cross-entity references to real ids from the related module, so joins resolve to actual rows (and degraded variants are the ones where they deliberately don't).
|
|
75
|
+
- **From each surface's declared `states`** ([surface-catalog.md](surface-catalog.md), `structure.md`) it emits the empty and degraded variants that surface needs.
|
|
76
|
+
|
|
77
|
+
The run's `--seed <n>` is the base; each module records the effective seed it generated under (`<ENTITY>_SEED`) so a single module reproduces independently and the whole run reproduces from one number ([create.md](../../references/create.md): "a fixed seed makes every run reproducible"). Volumes, long-string injection, unhappy-value distribution, and degraded payloads are the generator's job, not the model's — **the model wires surfaces to the emitted fixtures; it never authors rows.**
|
|
78
|
+
|
|
79
|
+
## Anti-patterns
|
|
80
|
+
|
|
81
|
+
Fixtures are mock data, so the anti-fabrication discipline applies to them in full — a mock row must read as *domain-real*, never as *fabricated-real*:
|
|
82
|
+
|
|
83
|
+
- **Lorem ipsum, `Item 1`, placeholder slop.** Domain-shaped values only.
|
|
84
|
+
- **`John Doe` / `Acme` / `Nexus` and invented vanity metrics.** No fabricated hero statistics (`99.98% UPTIME SLA`, `10,000+ customers`, fake round numbers) dressed as production achievements. Operator fixtures carry operational values — queue depths, timestamps, statuses — not marketing numbers.
|
|
85
|
+
- **The mock-marker convention on display numbers.** A fixture-supplied metric that a viewer could mistake for an authoritative production statistic is marked at the render site per the catalog convention — fake-precise mock numbers carry `<!-- mock -->`, unknowable values use a `[metric]` placeholder — so a fixture number is never read as ground truth.
|
|
86
|
+
- **Unseeded randomness.** `Math.random()`, `Date.now()`, or any non-reproducible source. Seed everything.
|
|
87
|
+
- **Fixtures authored after surfaces.** Data comes first; a surface composed before its data exists is designed against nothing. The `fixtures` phase gate fails if a surface file was written before the fixtures ([create.md](../../references/create.md)).
|
|
88
|
+
- **Happy-path-only rows.** No volume, no long strings, no unhappy or degraded values, no empty/error variants — the states and density that make an operator UI hard go unexercised, and their thresholds unbindable.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Surface catalog
|
|
2
|
+
|
|
3
|
+
The closed set of six operator surface archetypes. Structure derivation (D28) runs the object pass, then assigns every surface in the entity/surface map an archetype from this catalog — mechanically, by applying the choosing rules below to the map's facts: entities, attributes, cardinalities, relationships, lifecycle states, the operator's unit of work, and the core loop. The pass is never skipped; a small brief earns a small pass, never a guessed archetype. Navigation derives from the object model, never decorated onto it.
|
|
4
|
+
|
|
5
|
+
Each entry carries five things: **choosing rules** (the object shapes, cardinalities, and lifecycle signals that select it), a **density modifier** (one relative step on the global tier), **composition** (the parts the surface holds and their arrangement, plus the lifecycle states that matter most here), a **navigation slot**, and an **exemplar pointer**.
|
|
6
|
+
|
|
7
|
+
**Density.** The global tier — spacious · standard · dense — resolves at look time from the design.md / product-type traits (D31) and rewrites the spacing scale through the density primitive and the D6 `--spacing` remap. Each archetype then applies a single relative step on that tier: one step looser, at tier, or one step denser. Steps clamp at the scale ends (spacious … standard … dense) and never name an absolute value — an absolute override is banned (D31). Structure derivation records the relative step in each surface's `density-modifier` as a signed integer (positive = denser, `+1` / `0` / `-1`); the scaffold emits it as `data-density-step="tighter"` (+1) or `data-density-step="looser"` (-1), no attribute at 0. Modifiers compose with the behavioral dials (D4), never replace them.
|
|
8
|
+
|
|
9
|
+
**Exemplars load just-in-time.** Every entry points at one token-pure compiling exemplar under `data/operator-pack/exemplars/<slug>.tsx`. The exemplar is read at that archetype's emission phase gate during the surfaces phase (D23) — emitting a surface of archetype X without exemplar X in context is a gate violation (D20). Exemplars are never boot-loaded.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## list-detail
|
|
14
|
+
|
|
15
|
+
**Choosing rules.** Selects when all hold:
|
|
16
|
+
- The surface's primary object is one homogeneous collection — many instances of a single entity type (cardinality: N rows of one entity).
|
|
17
|
+
- Each row carries a workable per-row action or lifecycle transition (assign, resolve, approve, retry) — the operator acts row by row.
|
|
18
|
+
- The unit of work is one row (one ticket, one case, one device); the core loop is scan → select → act.
|
|
19
|
+
- Detail is a projection of one selected row, not a standalone destination.
|
|
20
|
+
|
|
21
|
+
This is the default archetype for the operator's primary entity — the queue shape. Distinguish from entity-detail: if a row's attribute count and relationship fan-out overflow a side panel and the entity is inspected/configured over time, that entity earns its own entity-detail route instead.
|
|
22
|
+
|
|
23
|
+
**Density modifier.** At tier — the reference archetype. The global tier is calibrated on the operator's primary table, so list-detail runs at it unmodified.
|
|
24
|
+
|
|
25
|
+
**Composition.** Toolbar (search, filters, bulk actions, column/view picker) + a virtualized table + a side-panel detail. Detail opens **beside** the table, never in a modal (floor P3). Bounded visible columns (rest behind the picker), right-aligned numerics in tabular figures (floor P4), sticky header, bounded pagination ("1–25 of 150") over infinite scroll, virtualization at ≥50 rows. Keyboard-first (floor P6): roving tabindex, arrow/j-k row travel, Enter opens the panel, shortcuts for the frequent verbs. Lifecycle states that matter most: **empty** (first-run vs filtered-to-nothing kept distinct), **loading** (skeleton rows mirroring the table, not a centered spinner), **partial/degraded** (stale rows marked honestly), and the per-row action's post-action state with optimistic-update-then-rollback.
|
|
26
|
+
|
|
27
|
+
**Navigation.** A top-level nav item per primary entity collection — the queues and tables the operator lives in; the default landing for that entity. Detail is not a nav item; it is a panel state on the list route.
|
|
28
|
+
|
|
29
|
+
**Exemplar.** `data/operator-pack/exemplars/list-detail.tsx` — JIT at the emission gate, never boot (D20).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## dashboard
|
|
34
|
+
|
|
35
|
+
**Choosing rules.** Selects when all hold:
|
|
36
|
+
- A read-mostly aggregate view over many entities or many metrics — the surface answers "how are things," not "act on this row."
|
|
37
|
+
- Its content is derived (counts, rates, trends, distributions), presented as instances of one Metric object — never minted as N first-class entities (the instances-mistaken-for-objects trap; guard it in the object pass).
|
|
38
|
+
- No per-row lifecycle action is the primary job; drill-down links out to a list-detail or entity-detail.
|
|
39
|
+
- The core loop is glance → notice → drill.
|
|
40
|
+
|
|
41
|
+
Selects on an overview / home / summary brief. Distinguish from command-center: a dashboard is read-mostly and glanceable; a command-center adds live streams and cross-entity intervention under time pressure.
|
|
42
|
+
|
|
43
|
+
**Density modifier.** One step looser — a dashboard is a glanceable aggregate; KPI cards and charts need whitespace to separate and read at a glance, not row-scan density.
|
|
44
|
+
|
|
45
|
+
**Composition.** A KPI / StatCard row at the top (≤4 primary metrics visible without scrolling — answer a few questions well, not many badly) + a small number of self-contained chart/panel tiles below, each a single question with a drill link. Hierarchy from spacing, not borders. Tabular figures on every KPI number (floor P4); charts read `--chart-1..5`; color is never the only signal. Lifecycle states that matter most: **loading** (per-card skeletons, never one page spinner), **partial/degraded** (a downed source names *which* panel is stale — "can't get data" is distinct from "no data"), **empty** (new-tenant zero-state that teaches the surface's value).
|
|
46
|
+
|
|
47
|
+
**Navigation.** The home/overview landing — typically the app root or the first nav item. It is a hub that links out; never a leaf. Its panels' drill targets are the list-detail and entity-detail routes.
|
|
48
|
+
|
|
49
|
+
**Exemplar.** `data/operator-pack/exemplars/dashboard.tsx` — JIT at the emission gate, never boot (D20).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## entity-detail
|
|
54
|
+
|
|
55
|
+
**Choosing rules.** Selects when all hold:
|
|
56
|
+
- The subject is a single rich entity with many attributes and outgoing relationships (1 entity : M related collections) that the operator inspects and configures over its life.
|
|
57
|
+
- The entity's data and actions belong together on one surface — splitting them across screens is the broken-object failure; keep the object whole.
|
|
58
|
+
- The temporal probe flagged the entity as long-lived, edited in place, with history that matters.
|
|
59
|
+
- Its attribute count and relationship fan-out exceed what a list-detail side panel holds.
|
|
60
|
+
|
|
61
|
+
Distinguish from list-detail: list-detail's detail is a lightweight row projection; entity-detail is a destination the operator returns to.
|
|
62
|
+
|
|
63
|
+
**Density modifier.** At tier — it mixes dense read views (related lists, activity) with edit forms, so it sits at the global tier: neither a scan surface nor a pure form.
|
|
64
|
+
|
|
65
|
+
**Composition.** Header (entity identity, status, primary actions) + a tab bar (overview / related collections / activity-history) + tabbed content. Related lists render as compact list-details inline; editing happens in place or opens the create-edit stepper beside the work (floor P3). Lifecycle states that matter most: **loading** (skeleton mirroring the header + tab shell), **error** (entity-not-found / load-failure with a recovery path, never a dead end), the edit **save states** (saving / saved / conflict — the concurrent-edit path from the temporal probe), and per-tab **empty** states.
|
|
66
|
+
|
|
67
|
+
**Navigation.** Not a top-level nav item — reached by drilling from its list-detail (row → entity route), a dashboard, or a command-center link. Route nests under the entity's collection (`/entities/:id`); its tabs are in-surface sub-nav, never top-level entries.
|
|
68
|
+
|
|
69
|
+
**Exemplar.** `data/operator-pack/exemplars/entity-detail.tsx` — JIT at the emission gate, never boot (D20).
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## create-edit
|
|
74
|
+
|
|
75
|
+
**Choosing rules.** Selects when all hold:
|
|
76
|
+
- The surface's job is to mint a new instance or transition one through a multi-field or multi-step change.
|
|
77
|
+
- The lifecycle signal is a **create** (the entity's birth) or a **staged edit** (a governed mutation), not scanning or reading.
|
|
78
|
+
- The change is too large or field-heavy to complete inline in a list-detail panel.
|
|
79
|
+
|
|
80
|
+
Create mirrors edit — the same field set and order; templates prefill. Distinguish from entity-detail: entity-detail is where an entity *lives*; create-edit is a focused, dismissible task that returns the operator to where it began.
|
|
81
|
+
|
|
82
|
+
**Density modifier.** One step looser — forms need field breathing room and comfortable target sizes; loosening cuts entry error on a surface where mistakes are costly.
|
|
83
|
+
|
|
84
|
+
**Composition.** A side-stepper opened **beside** the originating surface (floor P3), or a full form route for long creates. Labels above inputs; errors inline below the field, validated on blur; never placeholder-as-label. Logical field order preserved; one primary CTA bottom-right; primary targets ≥44px. Preview-then-commit for irreversible creates. Lifecycle states that matter most: **validation** (inline, on blur — diagnose → explain → recover), **submitting** (button loading, double-submit disabled), **server-error** (non-dead-end, entered values preserved), **success** (confirmation plus the next step).
|
|
85
|
+
|
|
86
|
+
**Navigation.** Never a top-level nav item — invoked from a list-detail toolbar ("New") or an entity-detail action, and it returns to that surface on complete. A create route may exist (`/entities/new`) but is entered from the collection, not from the nav.
|
|
87
|
+
|
|
88
|
+
**Exemplar.** `data/operator-pack/exemplars/create-edit.tsx` — JIT at the emission gate, never boot (D20).
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## log-stream
|
|
93
|
+
|
|
94
|
+
**Choosing rules.** Selects when all hold:
|
|
95
|
+
- The primary object is an append-only, time-ordered event or record stream (monotonic; newest-first or tail-following).
|
|
96
|
+
- Cardinality is unbounded and growing; rows are read, filtered, and searched — not acted on individually (no per-row lifecycle action).
|
|
97
|
+
- Events are immutable once written; the temporal axis is the spine.
|
|
98
|
+
|
|
99
|
+
Selects on a log / event / audit / activity-feed entity — high volume, time-keyed, read + filter. Distinguish from list-detail: list-detail rows carry a workable action and a lifecycle; log-stream lines are frozen history.
|
|
100
|
+
|
|
101
|
+
**Density modifier.** One step denser — the value of the surface is lines-on-screen; tighten to maximize visible history under one glance.
|
|
102
|
+
|
|
103
|
+
**Composition.** Filter/search bar + a follow/tail toggle (live vs paused) + a virtualized monospace line list, with inline expansion of a selected line (expand in place, not a modal). Tabular timestamps and IDs (floor P4), level-coded severity via status tokens with a second non-color cue, hairline dividers, minimal chrome. Lifecycle states that matter most: **streaming/live** (new lines announced via `aria-live` without stealing focus), **empty** (no events yet vs filtered-to-nothing, kept distinct), **error** (stream disconnected shows reconnecting, never a dead spinner), **partial/degraded** (dropped or truncated buffer marked honestly).
|
|
104
|
+
|
|
105
|
+
**Navigation.** A top-level nav item when logs/audit/activity is a primary operator surface; otherwise a tab inside the owning entity-detail (that entity's activity/history). Time-keyed, so deep-links carry a timestamp or anchor.
|
|
106
|
+
|
|
107
|
+
**Exemplar.** `data/operator-pack/exemplars/log-stream.tsx` — JIT at the emission gate, never boot (D20).
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## command-center
|
|
112
|
+
|
|
113
|
+
**Choosing rules.** Selects when all hold:
|
|
114
|
+
- A real-time, multi-pane monitoring surface composes several live entities or streams under one view.
|
|
115
|
+
- The operator acts under time pressure; the core loop is monitor → detect → intervene, across multiple entities at once.
|
|
116
|
+
- The unit of work spans entities, not one row — it fuses aggregate signals (dashboard-like) with live streams (log-stream-like) and quick cross-entity actions.
|
|
117
|
+
|
|
118
|
+
Reserved for genuine live-ops briefs (NOC, incident console, dispatch, ops bridge). Distinguish from dashboard: a dashboard is read-mostly and drills out; a command-center is live and acts in place. Never assign it to decorate an ordinary dashboard.
|
|
119
|
+
|
|
120
|
+
**Density modifier.** One step denser — the most information-rich archetype; the operator needs maximum live signal simultaneously visible.
|
|
121
|
+
|
|
122
|
+
**Composition.** A fixed multi-pane grid: a status/health header + several independently refreshing panels (aggregate tiles, a live stream pane, an alert/queue pane, quick-action controls), composed under one non-scrolling view where the viewport allows. Each pane refreshes on its own; keyboard reach spans panes; irreversible interventions are consent-gated with preview. Lifecycle states that matter most: **per-panel loading and partial/degraded** (each pane fails independently and says so — a command-center that hides a dead feed is dangerous), **live update**, **error**, and the alert/action **post-action** with undo.
|
|
123
|
+
|
|
124
|
+
**Navigation.** The app root or first nav item for a live-ops tool where monitoring is the job — the operator's home, aggregating routes that also exist standalone. Rare; only for genuine live-ops briefs.
|
|
125
|
+
|
|
126
|
+
**Exemplar.** `data/operator-pack/exemplars/command-center.tsx` — JIT at the emission gate, never boot (D20).
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Extension
|
|
131
|
+
|
|
132
|
+
The catalog is closed at six. A seventh archetype is never added by prose, by a reference edit, or by improvisation at run time. Membership extends **only as operator-pack data**, and only by a ledger-recorded decision (D28) — the new entry carrying its own choosing rules, density modifier, composition, navigation slot, and exemplar, and the exemplar passing the full machine catalog and personality-invariance validation before it ships (D20). Structure derivation assigns from these six; a brief that fits none is surfaced to the operator, never force-fit into the nearest archetype.
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# AI-surface trust rubric
|
|
2
|
+
|
|
3
|
+
Judged diagnosis dimensions for any operator surface that shows AI-originated content or takes AI-initiated actions. Four dimensions, anchors `trust-01`..`trust-04`. The rubric grades what a surface already does with AI. Absence of `trust-01`, `trust-02`, or `trust-03` grades **auto-critical** — no exception, no waiver.
|
|
4
|
+
|
|
5
|
+
## When it fires
|
|
6
|
+
|
|
7
|
+
Runs inside improve and introspect sweeps, on every surface whose composition holds AI-originated content (drafts, summaries, enrichments, scores, suggestions) or AI-initiated actions (auto-triage, auto-close, agent send / delete / charge). A surface with no AI content skips it. A copilot panel, an agent console, an enrichment column, an auto-triage queue all trip it.
|
|
8
|
+
|
|
9
|
+
## Scope: audit only
|
|
10
|
+
|
|
11
|
+
This rubric grades AI surfaces; it does not carry the generative patterns for building them. Streaming / generating states, confidence display, preview-then-apply, undo semantics for agent actions, the copilot-panel archetype — none are authored here. That pattern set enters the operator pack as a recorded addition when its trigger fires (the first embedded-copilot brief, or Nurix's own agent-console needs); `docs/history/backlog.md` holds the trigger. Until then the rubric audits, from day one, whatever other means produced the AI surface.
|
|
12
|
+
|
|
13
|
+
## The four dimensions
|
|
14
|
+
|
|
15
|
+
### trust-01 — AI presence is disclosed
|
|
16
|
+
|
|
17
|
+
**Demands.** Every AI-originated element carries a marker at its point of consumption — a badge, a label, a distinct visual treatment — legible before an operator acts on it. Disclosure also signals *how far to trust*: where the model emits a confidence, a source, or a known-unreliable class, the surface shows it. This folds the accuracy / confidence axis into disclosure — an AI badge sitting over a low-confidence guess presented as fact still fails, because the operator cannot tell the value is soft.
|
|
18
|
+
|
|
19
|
+
**Passes.** An AI-drafted reply in a support queue sits under an "AI draft" chip with a muted card treatment, distinct from a teammate's message. An enrichment-filled CRM field shows an "auto-filled" marker and its source. A model summary is captioned "AI-generated" and carries a confidence band when the model is unsure.
|
|
20
|
+
|
|
21
|
+
**Fails.** An AI-drafted reply rendered visually indistinguishable from a human draft. A model-derived "priority" score in a column with no sign it is generated. A low-confidence extraction shown as a plain, authoritative value.
|
|
22
|
+
|
|
23
|
+
**Judge.** For each AI element on the surface: could a first-time operator, glancing, tell it apart from human or authoritative content — and does the surface tell them how much to trust it?
|
|
24
|
+
|
|
25
|
+
**Grade.** Absence auto-critical.
|
|
26
|
+
|
|
27
|
+
### trust-02 — A human override path is visible
|
|
28
|
+
|
|
29
|
+
**Demands.** Wherever the AI decides or acts, the operator can override, correct, or reject it — per item, on the surface where the decision shows, and keyboard-reachable. A global off-switch is not an override path.
|
|
30
|
+
|
|
31
|
+
**Passes.** An auto-triage queue gives every item a visible "reassign" and "revert". An AI-suggested value offers inline "accept / edit / dismiss". An auto-close agent exposes per-ticket "reopen" and "stop auto-closing this".
|
|
32
|
+
|
|
33
|
+
**Fails.** An auto-triage rule with no per-item revert. An AI-set status field that is read-only in the UI. A settings-only "disable AI" toggle with no per-decision correction on the surface itself.
|
|
34
|
+
|
|
35
|
+
**Judge.** At each point the AI acted, is there a countermand affordance, reachable where the decision appears (not buried in settings) and by keyboard?
|
|
36
|
+
|
|
37
|
+
**Grade.** Absence auto-critical.
|
|
38
|
+
|
|
39
|
+
### trust-03 — AI-initiated irreversible actions are consent-gated with preview
|
|
40
|
+
|
|
41
|
+
**Demands.** Any AI-initiated action that is destructive or irreversible — send, delete, charge, publish, external side-effect — stops before executing, shows a preview of exactly what it will do, and waits for explicit human consent. It is never auto-fired. A reversible AI action needs trust-02's override, not necessarily a preview gate.
|
|
42
|
+
|
|
43
|
+
**Passes.** An outreach agent stops at a preview of the exact recipients and body, gated on a send click. A bulk-remediation agent shows the diff of the 40 records it will touch and waits for confirm. A refund-capable AI presents amount, account, and reason behind an explicit confirm.
|
|
44
|
+
|
|
45
|
+
**Fails.** An agent that sends the drafted reply the instant it is generated. An auto-remediation that deletes flagged rows on a timer with no preview step. A "resolve all" action that fires irreversible external calls without showing what it will touch.
|
|
46
|
+
|
|
47
|
+
**Judge.** Enumerate the AI-initiated actions on the surface; for each irreversible one, is there a concrete preview of the effect and an explicit consent step before execution?
|
|
48
|
+
|
|
49
|
+
**Grade.** Absence auto-critical.
|
|
50
|
+
|
|
51
|
+
### trust-04 — AI actions are auditable
|
|
52
|
+
|
|
53
|
+
**Demands.** AI-taken actions leave a durable, operator-inspectable trail — what acted, when, on what, and on what basis — visible in the UI, not only in backend logs. Attribution distinguishes AI from human.
|
|
54
|
+
|
|
55
|
+
**Passes.** Each AI-modified record carries an activity entry: "auto-triaged by <agent> at <time>, rule <x>". A command center runs an AI-actions feed with timestamps and revert links. An entity's audit tab lists model actions beside human ones with provenance.
|
|
56
|
+
|
|
57
|
+
**Fails.** An AI that mutates records with no history entry. Actions attributed generically to "system", indistinguishable from a human edit. An auto-close leaving no timestamped, attributable trace.
|
|
58
|
+
|
|
59
|
+
**Judge.** For the AI actions on this surface, can an operator reconstruct after the fact what the AI did, when, and on what basis, from the UI itself?
|
|
60
|
+
|
|
61
|
+
**Grade.** Absence is a finding, severity by judgment — critical or major by the blast radius and reversibility of the actions. It never auto-grades; only trust-01..03 do.
|
|
62
|
+
|
|
63
|
+
## Writing the finding
|
|
64
|
+
|
|
65
|
+
Trust findings are judged findings on the canonical schema — peers of machine findings, consumed directly by improve, rendered per reader register by introspect, and carried unchanged into the dated findings report and the decay ledger.
|
|
66
|
+
|
|
67
|
+
- **provenance** — the trust anchor (`trust-01`..`trust-04`), marked judged. This is the load-bearing tag; anchor parity between this rubric and the finding's provenance is enforced.
|
|
68
|
+
- **dimension** — the AI-surface trust dimension the rubric adds. Where the canonical dimension slot wants a taxonomy code, disclosure copy reads under B5 Content & Copy and the override / consent / audit affordances under B1 Interactive States; the provenance stays the trust anchor either way.
|
|
69
|
+
- **cause layer** — a trust failure is felt in Perception (the operator cannot tell, cannot control, cannot audit) but its cause is almost always Structure (a missing marker or affordance) or Behavior (an ungated action, an absent trail). Name the causal layer, per the causes-first walk.
|
|
70
|
+
- **evidence** — the `file:line` of the AI element and what it renders or does.
|
|
71
|
+
- **before → after** — descriptive, not numeric, because the rubric is judged: the failing state → the affordance that fixes it. E.g. `AI draft renders identically to human messages → AI draft carries a disclosure chip and a muted treatment`.
|
|
72
|
+
- **consequence** — the trust harm: automation bias, a silent irreversible action, an uncorrectable error, an unauditable mutation.
|
|
73
|
+
- **tier** — within-levers when the fix is an affordance the surface can already express (a chip, a confirm dialog, an activity entry); lever-change when the fix needs a generative pattern the pack does not yet carry (route it to the backlogged pattern set, and report it, never freehand it).
|
|
74
|
+
- **severity** — trust-01..03 absence is auto-critical; trust-04 absence is critical or major by judgment; a present-but-weak dimension grades major or minor.
|
|
75
|
+
|
|
76
|
+
**Presence-shaped machine facets.** Where a facet holds with zero legitimate exceptions — an AI-marked element must carry its disclosure attribute, a consent dialog must trap focus — it is registered as a machine rule in the catalog and fires at its own point. This rubric is the judged layer the machine cannot reach: whether the marker is *legible*, whether the preview shows the *right* effect, whether the trail is *reconstructable* are judgments, and they stay here.
|