chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:33:58 +08:00
commit 6a2f6934bb
38 changed files with 20049 additions and 0 deletions
+454
View File
@@ -0,0 +1,454 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debounced search — three approaches</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-150: #F0EEE6;
--gray-300: #D1CFC5;
--gray-500: #87867F;
--gray-700: #3D3D3A;
--white: #FFFFFF;
--serif: ui-serif, Georgia, 'Times New Roman', serif;
--sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--sans);
background: var(--ivory);
color: var(--gray-700);
line-height: 1.55;
padding: 56px 32px 96px;
-webkit-font-smoothing: antialiased;
}
.page {
max-width: 1360px;
margin: 0 auto;
}
/* ---------- header ---------- */
header.page-head {
margin-bottom: 48px;
max-width: 760px;
}
.eyebrow {
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 12px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 38px;
line-height: 1.15;
color: var(--slate);
margin-bottom: 18px;
letter-spacing: -0.01em;
}
.prompt-box {
background: var(--gray-150);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
padding: 16px 20px;
font-size: 14.5px;
color: var(--gray-700);
}
.prompt-box .label {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
display: block;
margin-bottom: 6px;
}
/* ---------- approach grid ---------- */
.approaches {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 28px;
margin-bottom: 56px;
}
@media (max-width: 1100px) {
.approaches { grid-template-columns: 1fr; }
}
.approach {
background: var(--white);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
padding: 24px;
display: flex;
flex-direction: column;
gap: 20px;
}
.approach-head h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 21px;
color: var(--slate);
margin-bottom: 6px;
}
.approach-head .num {
display: inline-block;
font-family: var(--mono);
font-size: 12px;
background: var(--oat);
color: var(--slate);
padding: 2px 8px;
border-radius: 8px;
margin-right: 8px;
vertical-align: 3px;
}
.approach-head p {
font-size: 14px;
color: var(--gray-500);
}
/* ---------- code panel ---------- */
.code {
background: var(--slate);
border-radius: 12px;
padding: 18px 20px;
overflow-x: auto;
}
.code pre {
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.65;
color: #E8E6DE;
white-space: pre;
}
.code .kw { color: var(--clay); } /* keywords */
.code .str { color: var(--olive); } /* strings */
.code .cm { color: var(--gray-500); } /* comments */
.code .fn { color: #C9B98A; } /* identifiers, subtle warm */
/* ---------- tradeoffs table ---------- */
.tradeoffs {
border: 1.5px solid var(--gray-300);
border-radius: 8px;
overflow: hidden;
font-size: 13px;
}
.tradeoffs .row {
display: grid;
grid-template-columns: 1fr 1fr;
}
.tradeoffs .row + .row {
border-top: 1.5px solid var(--gray-300);
}
.tradeoffs .cell {
padding: 10px 14px;
}
.tradeoffs .cell:first-child {
border-right: 1.5px solid var(--gray-300);
}
.tradeoffs .head {
background: var(--gray-150);
font-weight: 600;
color: var(--slate);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.tradeoffs .pro::before,
.tradeoffs .con::before {
content: '';
display: inline-block;
width: 6px;
height: 6px;
border-radius: 50%;
margin-right: 8px;
vertical-align: 2px;
}
.tradeoffs .pro::before { background: var(--olive); }
.tradeoffs .con::before { background: var(--clay); }
/* ---------- chip footer ---------- */
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.chip {
font-family: var(--mono);
font-size: 11.5px;
background: var(--gray-150);
border: 1.5px solid var(--gray-300);
color: var(--gray-700);
padding: 5px 10px;
border-radius: 8px;
white-space: nowrap;
}
.chip strong { color: var(--slate); font-weight: 600; }
/* ---------- recommendation ---------- */
.reco {
border-left: 4px solid var(--clay);
background: var(--white);
border-radius: 0 12px 12px 0;
padding: 24px 28px;
max-width: 860px;
}
.reco h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 22px;
color: var(--slate);
margin-bottom: 10px;
}
.reco p {
font-size: 15px;
margin-bottom: 8px;
}
.reco code {
font-family: var(--mono);
font-size: 0.92em;
background: var(--gray-150);
padding: 1px 6px;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="page">
<header class="page-head">
<div class="eyebrow">Exploration · Acme web client</div>
<h1>Three ways to implement debounced search</h1>
<div class="prompt-box">
<span class="label">Prompt</span>
Show me three different ways to implement debounced search for the task
filter input in our React codebase, with tradeoffs for each.
</div>
</header>
<!-- ============================================================= -->
<section class="approaches">
<!-- ---------- Approach 1 ---------- -->
<article class="approach">
<header class="approach-head">
<h2><span class="num">01</span>Inline useEffect + setTimeout</h2>
<p>Debounce logic lives directly inside the component that owns the input.</p>
</header>
<div class="code"><pre><span class="kw">export function</span> <span class="fn">TaskSearch</span>() {
<span class="kw">const</span> [draft, setDraft] = <span class="fn">useState</span>(<span class="str">''</span>);
<span class="kw">const</span> [query, setQuery] = <span class="fn">useState</span>(<span class="str">''</span>);
<span class="fn">useEffect</span>(() <span class="kw">=&gt;</span> {
<span class="kw">const</span> id = <span class="fn">setTimeout</span>(() <span class="kw">=&gt;</span> setQuery(draft), <span class="str">300</span>);
<span class="kw">return</span> () <span class="kw">=&gt;</span> <span class="fn">clearTimeout</span>(id);
}, [draft]);
<span class="kw">const</span> { data } = <span class="fn">useTasks</span>({ search: query });
<span class="kw">return</span> (
&lt;<span class="fn">input</span>
value={draft}
onChange={(e) <span class="kw">=&gt;</span> setDraft(e.target.value)}
placeholder=<span class="str">"Filter tasks…"</span>
/&gt;
);
}</pre></div>
<div class="tradeoffs">
<div class="row head">
<div class="cell">Pro</div>
<div class="cell">Con</div>
</div>
<div class="row">
<div class="cell pro">Zero new abstractions to learn</div>
<div class="cell con">Logic duplicated everywhere search exists</div>
</div>
<div class="row">
<div class="cell pro">Easy to step through in devtools</div>
<div class="cell con">Two pieces of state for one conceptual value</div>
</div>
<div class="row">
<div class="cell pro">No dependency or bundle change</div>
<div class="cell con">Delay constant is buried in component body</div>
</div>
</div>
<div class="chips">
<span class="chip">Bundle impact: <strong>+0 kb</strong></span>
<span class="chip">Testability: <strong>medium</strong></span>
<span class="chip">Reuse: <strong>low</strong></span>
<span class="chip">SSR safe: <strong>yes</strong></span>
</div>
</article>
<!-- ---------- Approach 2 ---------- -->
<article class="approach">
<header class="approach-head">
<h2><span class="num">02</span>Custom useDebounce hook</h2>
<p>Extract the timer into a shared hook under <code style="font-family:var(--mono)">src/hooks/</code>.</p>
</header>
<div class="code"><pre><span class="cm">// src/hooks/useDebounce.ts</span>
<span class="kw">export function</span> <span class="fn">useDebounce</span>&lt;T&gt;(value: T, ms = <span class="str">300</span>): T {
<span class="kw">const</span> [debounced, setDebounced] = <span class="fn">useState</span>(value);
<span class="fn">useEffect</span>(() <span class="kw">=&gt;</span> {
<span class="kw">const</span> id = <span class="fn">setTimeout</span>(() <span class="kw">=&gt;</span> setDebounced(value), ms);
<span class="kw">return</span> () <span class="kw">=&gt;</span> <span class="fn">clearTimeout</span>(id);
}, [value, ms]);
<span class="kw">return</span> debounced;
}
<span class="cm">// TaskSearch.tsx</span>
<span class="kw">const</span> [draft, setDraft] = <span class="fn">useState</span>(<span class="str">''</span>);
<span class="kw">const</span> query = <span class="fn">useDebounce</span>(draft, <span class="str">300</span>);
<span class="kw">const</span> { data } = <span class="fn">useTasks</span>({ search: query });</pre></div>
<div class="tradeoffs">
<div class="row head">
<div class="cell">Pro</div>
<div class="cell">Con</div>
</div>
<div class="row">
<div class="cell pro">Single import reused across filter, command bar, board search</div>
<div class="cell con">One more file to maintain and document</div>
</div>
<div class="row">
<div class="cell pro">Trivial to unit test with fake timers</div>
<div class="cell con">Generic <code style="font-family:var(--mono)">T</code> hides intent slightly</div>
</div>
<div class="row">
<div class="cell pro">Delay is a visible, tunable argument</div>
<div class="cell con">Still re-renders on every keystroke</div>
</div>
</div>
<div class="chips">
<span class="chip">Bundle impact: <strong>+0.2 kb</strong></span>
<span class="chip">Testability: <strong>high</strong></span>
<span class="chip">Reuse: <strong>high</strong></span>
<span class="chip">SSR safe: <strong>yes</strong></span>
</div>
</article>
<!-- ---------- Approach 3 ---------- -->
<article class="approach">
<header class="approach-head">
<h2><span class="num">03</span>Tiny external library</h2>
<p>Adopt <code style="font-family:var(--mono)">use-debounce</code> for both values and callbacks.</p>
</header>
<div class="code"><pre><span class="kw">import</span> { useDebouncedCallback }
<span class="kw">from</span> <span class="str">'use-debounce'</span>;
<span class="kw">export function</span> <span class="fn">TaskSearch</span>() {
<span class="kw">const</span> [query, setQuery] = <span class="fn">useState</span>(<span class="str">''</span>);
<span class="kw">const</span> onChange = <span class="fn">useDebouncedCallback</span>(
(next: <span class="kw">string</span>) <span class="kw">=&gt;</span> setQuery(next),
<span class="str">300</span>,
{ leading: <span class="kw">false</span>, maxWait: <span class="str">1000</span> },
);
<span class="kw">const</span> { data } = <span class="fn">useTasks</span>({ search: query });
<span class="kw">return</span> (
&lt;<span class="fn">input</span>
defaultValue=<span class="str">""</span>
onChange={(e) <span class="kw">=&gt;</span> onChange(e.target.value)}
/&gt;
);
}</pre></div>
<div class="tradeoffs">
<div class="row head">
<div class="cell">Pro</div>
<div class="cell">Con</div>
</div>
<div class="row">
<div class="cell pro">leading / trailing / maxWait handled for us</div>
<div class="cell con">New runtime dependency to audit and update</div>
</div>
<div class="row">
<div class="cell pro">Callback form skips intermediate re-renders</div>
<div class="cell con">Uncontrolled input diverges from Acme form patterns</div>
</div>
<div class="row">
<div class="cell pro">Well-tested edge cases (unmount, flush, cancel)</div>
<div class="cell con">~1.4 kb gzipped for something we could own</div>
</div>
</div>
<div class="chips">
<span class="chip">Bundle impact: <strong>+1.4 kb</strong></span>
<span class="chip">Testability: <strong>high</strong></span>
<span class="chip">Reuse: <strong>high</strong></span>
<span class="chip">SSR safe: <strong>yes</strong></span>
</div>
</article>
</section>
<!-- ============================================================= -->
<aside class="reco">
<h2>Recommendation</h2>
<p>
Go with <strong>approach 02, the custom <code>useDebounce</code> hook</strong>.
Acme already has three places that hand-roll the inline pattern
(task filter, command palette, member picker), so extracting one
shared hook removes duplication without taking on a new dependency.
</p>
<p>
Revisit approach 03 only if we later need <code>maxWait</code> or
<code>flush()</code> semantics — the library earns its bundle cost
once the requirements outgrow a ten-line hook.
</p>
</aside>
</div>
</body>
</html>
+516
View File
@@ -0,0 +1,516 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Empty state — four visual directions</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-150: #F0EEE6;
--gray-300: #D1CFC5;
--gray-500: #87867F;
--gray-700: #3D3D3A;
--white: #FFFFFF;
--serif: ui-serif, Georgia, 'Times New Roman', serif;
--sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--sans);
background: var(--ivory);
color: var(--gray-700);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
padding-bottom: 96px;
}
/* ---------- sticky toolbar ---------- */
.toolbar {
position: sticky;
top: 0;
z-index: 10;
background: var(--ivory);
border-bottom: 1.5px solid var(--gray-300);
padding: 14px 32px;
display: flex;
align-items: center;
gap: 18px;
font-size: 13px;
}
.toolbar .title {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.04em;
color: var(--gray-500);
margin-right: auto;
}
.toolbar .field {
display: flex;
align-items: center;
gap: 10px;
}
.toolbar .field > span {
color: var(--gray-700);
font-weight: 500;
}
.seg {
display: inline-flex;
border: 1.5px solid var(--gray-300);
border-radius: 8px;
overflow: hidden;
background: var(--white);
}
.seg label {
padding: 6px 14px;
cursor: pointer;
font-size: 13px;
color: var(--gray-700);
user-select: none;
}
.seg label + label {
border-left: 1.5px solid var(--gray-300);
}
.seg input { display: none; }
.seg input:checked + span {
color: var(--slate);
font-weight: 600;
}
.seg label:has(input:checked) {
background: var(--oat);
}
/* ---------- page header ---------- */
.page {
max-width: 1240px;
margin: 0 auto;
padding: 48px 32px 0;
}
header.page-head {
max-width: 720px;
margin-bottom: 40px;
}
.eyebrow {
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 12px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 38px;
line-height: 1.15;
color: var(--slate);
margin-bottom: 18px;
letter-spacing: -0.01em;
}
.prompt-box {
background: var(--gray-150);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
padding: 16px 20px;
font-size: 14.5px;
}
.prompt-box .label {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
display: block;
margin-bottom: 6px;
}
/* ---------- artboard grid ---------- */
.board {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32px;
}
@media (max-width: 960px) {
.board { grid-template-columns: 1fr; }
}
.artboard {
background: var(--white);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
padding: 20px;
position: relative;
}
.tag {
position: absolute;
top: 14px;
left: 14px;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.02em;
background: var(--oat);
color: var(--slate);
padding: 4px 10px;
border-radius: 8px;
z-index: 2;
}
.stage {
height: 280px;
border-radius: 8px;
border: 1.5px solid var(--gray-300);
background: var(--ivory);
display: flex;
align-items: center;
justify-content: center;
padding: 24px;
transition: background 0.15s, border-color 0.15s;
overflow: hidden;
}
.stage.dark {
background: var(--slate);
border-color: var(--slate);
}
.rationale {
margin-top: 16px;
font-size: 13px;
color: var(--gray-500);
line-height: 1.5;
}
/* =========================================================
Empty-state variants — each scoped under .es-*
Theme tokens flip when .stage.dark is present.
========================================================= */
.stage { --fg: var(--slate); --muted: var(--gray-500); --line: var(--gray-300); --panel: var(--white); }
.stage.dark { --fg: #F0EEE6; --muted: #9C9A93; --line: #3D3D3A; --panel: #1F1E1B; }
/* ---- A · Minimal ---- */
.es-a {
text-align: center;
max-width: 300px;
}
.es-a h3 {
font-family: var(--serif);
font-weight: 500;
font-size: 19px;
color: var(--fg);
margin-bottom: 6px;
}
.es-a p {
font-size: 13px;
color: var(--muted);
margin-bottom: 18px;
}
.es-a .btn {
display: inline-block;
font-size: 13px;
color: var(--fg);
border: 1.5px solid var(--line);
border-radius: 8px;
padding: 8px 16px;
background: transparent;
}
/* ---- B · Illustrated ---- */
.es-b {
text-align: center;
max-width: 320px;
}
.es-b svg {
display: block;
margin: 0 auto 18px;
}
.es-b h3 {
font-family: var(--serif);
font-weight: 500;
font-size: 19px;
color: var(--fg);
margin-bottom: 6px;
}
.es-b p {
font-size: 13px;
color: var(--muted);
margin-bottom: 16px;
}
.es-b .btn {
display: inline-block;
font-size: 13px;
font-weight: 500;
color: var(--white);
background: var(--clay);
border-radius: 8px;
padding: 8px 16px;
}
.stage.dark .es-b .btn { color: var(--slate); background: var(--oat); }
/* ---- C · Playful ---- */
.es-c {
text-align: center;
max-width: 300px;
}
.es-c .float {
width: 56px;
height: 56px;
margin: 0 auto 20px;
position: relative;
animation: bob 3.2s ease-in-out infinite;
}
.es-c .float .card {
position: absolute;
inset: 0;
border-radius: 10px;
background: var(--panel);
border: 1.5px solid var(--line);
}
.es-c .float .card:nth-child(1) { transform: rotate(-8deg) translate(-6px, 4px); opacity: 0.5; }
.es-c .float .card:nth-child(2) { transform: rotate( 6deg) translate( 6px, 2px); opacity: 0.75; }
.es-c .float .card:nth-child(3) { background: var(--oat); border-color: var(--oat); }
.stage.dark .es-c .float .card:nth-child(3) { background: var(--clay); border-color: var(--clay); }
.es-c .shadow {
width: 44px;
height: 8px;
margin: -8px auto 22px;
border-radius: 50%;
background: rgba(0,0,0,0.12);
filter: blur(2px);
animation: shadow 3.2s ease-in-out infinite;
}
.stage.dark .es-c .shadow { background: rgba(0,0,0,0.5); }
.es-c h3 {
font-family: var(--serif);
font-weight: 500;
font-size: 19px;
color: var(--fg);
margin-bottom: 6px;
}
.es-c p {
font-size: 13px;
color: var(--muted);
}
@keyframes bob {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-10px); }
}
@keyframes shadow {
0%, 100% { transform: scaleX(1); opacity: 0.9; }
50% { transform: scaleX(0.8); opacity: 0.5; }
}
/* ---- D · Instructional ---- */
.es-d {
width: 100%;
max-width: 360px;
}
.es-d h3 {
font-family: var(--serif);
font-weight: 500;
font-size: 18px;
color: var(--fg);
margin-bottom: 14px;
}
.es-d ol {
list-style: none;
counter-reset: step;
display: flex;
flex-direction: column;
gap: 10px;
}
.es-d li {
counter-increment: step;
display: flex;
align-items: flex-start;
gap: 12px;
background: var(--panel);
border: 1.5px solid var(--line);
border-radius: 8px;
padding: 10px 14px;
font-size: 13px;
color: var(--fg);
}
.es-d li::before {
content: counter(step);
font-family: var(--mono);
font-size: 11px;
flex: 0 0 20px;
height: 20px;
border-radius: 50%;
background: var(--oat);
color: var(--slate);
display: inline-flex;
align-items: center;
justify-content: center;
margin-top: 1px;
}
.es-d li span { color: var(--muted); display: block; font-size: 12px; margin-top: 2px; }
</style>
</head>
<body>
<div class="toolbar">
<div class="title">Acme · design exploration</div>
<div class="field">
<span>Background:</span>
<div class="seg" id="bg-seg">
<label><input type="radio" name="bg" value="light" checked><span>Light</span></label>
<label><input type="radio" name="bg" value="dark"><span>Dark</span></label>
</div>
</div>
</div>
<div class="page">
<header class="page-head">
<div class="eyebrow">Exploration · Empty states</div>
<h1>Four visual directions for the “no tasks yet” state</h1>
<div class="prompt-box">
<span class="label">Prompt</span>
Explore four visual directions for our empty-state component. Render each
live so we can compare tone, density, and how well they hold up on light
and dark surfaces.
</div>
</header>
<section class="board">
<!-- ================= A — Minimal ================= -->
<article class="artboard">
<span class="tag">A — Minimal</span>
<div class="stage">
<div class="es-a">
<h3>No tasks yet</h3>
<p>When you create a task it will show up here.</p>
<span class="btn">New task</span>
</div>
</div>
<p class="rationale">
Pure typography, single quiet action. Reads as calm and confident;
assumes the surrounding UI already carries enough personality.
</p>
</article>
<!-- ================= B — Illustrated ================= -->
<article class="artboard">
<span class="tag">B — Illustrated</span>
<div class="stage">
<div class="es-b">
<svg width="120" height="90" viewBox="0 0 120 90" aria-hidden="true">
<rect x="14" y="20" width="72" height="54" rx="8"
fill="var(--panel)" stroke="var(--line)" stroke-width="1.5"/>
<rect x="34" y="10" width="72" height="54" rx="8"
fill="var(--oat)"/>
<line x1="46" y1="26" x2="92" y2="26"
stroke="var(--slate)" stroke-width="2" stroke-linecap="round"/>
<line x1="46" y1="38" x2="80" y2="38"
stroke="var(--slate)" stroke-width="2" stroke-linecap="round" opacity="0.5"/>
<circle cx="98" cy="60" r="12" fill="var(--clay)"/>
<path d="M98 55 v10 M93 60 h10"
stroke="white" stroke-width="2" stroke-linecap="round"/>
</svg>
<h3>Start your first list</h3>
<p>Group related work and watch progress roll up automatically.</p>
<span class="btn">Create a task</span>
</div>
</div>
<p class="rationale">
A small geometric spot illustration anchors the eye and explains the
object model (lists contain tasks) without a wall of copy.
</p>
</article>
<!-- ================= C — Playful ================= -->
<article class="artboard">
<span class="tag">C — Playful</span>
<div class="stage">
<div class="es-c">
<div class="float">
<div class="card"></div>
<div class="card"></div>
<div class="card"></div>
</div>
<div class="shadow"></div>
<h3>Nothing on your plate</h3>
<p>Enjoy the quiet, or add something to get moving.</p>
</div>
</div>
<p class="rationale">
A gently bobbing stack adds life to an otherwise static screen. Motion
is subtle enough to loop indefinitely without drawing complaints.
</p>
</article>
<!-- ================= D — Instructional ================= -->
<article class="artboard">
<span class="tag">D — Instructional</span>
<div class="stage">
<div class="es-d">
<h3>Set up this project</h3>
<ol>
<li>
Create your first task
<span>Give it a name and an owner.</span>
</li>
<li>
Add a due date
<span>Acme will surface it on the timeline.</span>
</li>
<li>
Invite a teammate
<span>Shared projects stay in sync automatically.</span>
</li>
</ol>
</div>
</div>
<p class="rationale">
Treats the empty state as onboarding. Higher density, but every line is
actionable — best when the user is new to the product, not just the view.
</p>
</article>
</section>
</div>
<script>
const stages = document.querySelectorAll('.stage');
document.getElementById('bg-seg').addEventListener('change', (e) => {
const dark = e.target.value === 'dark';
stages.forEach((s) => s.classList.toggle('dark', dark));
});
</script>
</body>
</html>
+639
View File
@@ -0,0 +1,639 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PR #247 — Review Summary</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--rust: #B04A3F;
--gray-150: #F0EEE6;
--gray-300: #D1CFC5;
--gray-500: #87867F;
--gray-700: #3D3D3A;
--serif: ui-serif, Georgia, 'Times New Roman', serif;
--sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--ivory);
color: var(--gray-700);
font-family: var(--sans);
font-size: 15px;
line-height: 1.6;
padding: 48px 24px 80px;
}
.page {
max-width: 920px;
margin: 0 auto;
}
/* ---------- Header ---------- */
header.pr-head {
border: 1.5px solid var(--gray-300);
border-radius: 12px;
padding: 28px 32px;
background: #fff;
margin-bottom: 36px;
}
.repo-line {
font-family: var(--mono);
font-size: 12.5px;
color: var(--gray-500);
letter-spacing: 0.01em;
margin-bottom: 10px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 30px;
line-height: 1.25;
color: var(--slate);
margin-bottom: 18px;
}
.meta-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 20px;
}
.author {
display: flex;
align-items: center;
gap: 10px;
}
.avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: var(--oat);
color: var(--slate);
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 13px;
letter-spacing: 0.02em;
border: 1.5px solid var(--gray-300);
}
.author-name { font-weight: 500; color: var(--slate); }
.author-sub { font-size: 12px; color: var(--gray-500); }
.branch {
font-family: var(--mono);
font-size: 12.5px;
color: var(--gray-700);
background: var(--gray-150);
border: 1.5px solid var(--gray-300);
border-radius: 8px;
padding: 6px 10px;
}
.branch .arrow { color: var(--gray-500); margin: 0 6px; }
.stat {
font-family: var(--mono);
font-size: 13px;
}
.stat .add { color: var(--olive); font-weight: 600; }
.stat .del { color: var(--rust); font-weight: 600; }
.stat .files { color: var(--gray-500); margin-left: 10px; }
/* ---------- Section blocks ---------- */
section { margin-bottom: 40px; }
h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 21px;
color: var(--slate);
margin-bottom: 14px;
}
.prose ul {
list-style: none;
padding: 0;
}
.prose li {
position: relative;
padding-left: 22px;
margin-bottom: 10px;
}
.prose li::before {
content: "";
position: absolute;
left: 4px;
top: 9px;
width: 6px;
height: 6px;
background: var(--gray-500);
border-radius: 2px;
}
/* ---------- Risk map ---------- */
.risk-map {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.chip {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
border: 1.5px solid var(--gray-300);
font-family: var(--mono);
font-size: 12.5px;
color: var(--slate);
text-decoration: none;
background: #fff;
transition: transform 0.12s ease;
}
.chip:hover { transform: translateY(-1px); }
.chip .dot {
width: 9px;
height: 9px;
border-radius: 50%;
flex-shrink: 0;
}
.chip.safe { background: rgba(120,140,93,0.10); border-color: rgba(120,140,93,0.45); }
.chip.safe .dot { background: var(--olive); }
.chip.medium { background: var(--oat); }
.chip.medium .dot { background: #B89B6E; }
.chip.attention { background: rgba(217,119,87,0.12); border-color: rgba(217,119,87,0.55); }
.chip.attention .dot { background: var(--clay); }
.legend {
margin-top: 12px;
font-size: 12px;
color: var(--gray-500);
display: flex;
gap: 18px;
}
.legend span { display: inline-flex; align-items: center; gap: 6px; }
.legend .dot { width: 8px; height: 8px; border-radius: 50%; }
/* ---------- File sections ---------- */
.file-card {
border: 1.5px solid var(--gray-300);
border-radius: 12px;
background: #fff;
margin-bottom: 24px;
overflow: hidden;
scroll-margin-top: 20px;
}
.file-head {
padding: 16px 20px;
border-bottom: 1.5px solid var(--gray-150);
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
.file-path {
font-family: var(--mono);
font-size: 13.5px;
color: var(--slate);
}
.file-delta {
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
}
.file-delta .add { color: var(--olive); }
.file-delta .del { color: var(--rust); }
.risk-tag {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 3px 8px;
border-radius: 6px;
font-weight: 600;
}
.risk-tag.safe { background: rgba(120,140,93,0.15); color: var(--olive); }
.risk-tag.medium { background: var(--oat); color: #7A6A4F; }
.risk-tag.attention { background: rgba(217,119,87,0.15); color: var(--clay); }
/* ---------- Diff block ---------- */
.diff {
background: var(--slate);
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.7;
overflow-x: auto;
}
.diff-row {
display: grid;
grid-template-columns: 48px 18px 1fr;
align-items: baseline;
padding: 0 14px 0 0;
white-space: pre;
}
.diff-row .ln {
text-align: right;
padding-right: 14px;
color: var(--gray-500);
user-select: none;
}
.diff-row .mark {
text-align: center;
color: var(--gray-500);
}
.diff-row .code { color: #E8E6DC; }
.diff-row.ctx .code { color: #B8B6AC; }
.diff-row.add { background: rgba(120,140,93,0.15); }
.diff-row.add .mark { color: var(--olive); }
.diff-row.del { background: rgba(176,74,63,0.15); }
.diff-row.del .mark { color: var(--rust); }
.diff-row.hunk {
background: rgba(255,255,255,0.04);
color: var(--gray-500);
}
.diff-row.hunk .code { color: var(--gray-500); }
/* ---------- Review comments ---------- */
.comments {
padding: 18px 20px 20px;
display: flex;
flex-direction: column;
gap: 14px;
background: var(--gray-150);
}
.bubble {
position: relative;
background: #fff;
border: 1.5px solid var(--gray-300);
border-left-width: 4px;
border-radius: 8px;
padding: 12px 14px 12px 16px;
max-width: 680px;
}
.bubble.blocking { border-left-color: var(--clay); }
.bubble.nit { border-left-color: var(--gray-300); }
.bubble::before {
content: "";
position: absolute;
left: -9px;
top: 16px;
width: 12px;
height: 12px;
background: #fff;
border-left: 1.5px solid var(--gray-300);
border-bottom: 1.5px solid var(--gray-300);
transform: rotate(45deg);
}
.bubble.blocking::before { border-left-color: var(--clay); border-bottom-color: var(--clay); }
.bubble .anchor {
font-family: var(--mono);
font-size: 11.5px;
color: var(--gray-500);
margin-bottom: 4px;
}
.bubble .label {
display: inline-block;
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.08em;
font-weight: 700;
margin-right: 8px;
}
.bubble.blocking .label { color: var(--clay); }
.bubble.nit .label { color: var(--gray-500); }
.bubble p { font-size: 13.5px; color: var(--gray-700); }
.bubble code {
font-family: var(--mono);
font-size: 12.5px;
background: var(--gray-150);
padding: 1px 5px;
border-radius: 4px;
}
/* ---------- Collapsed files ---------- */
details.file-collapsed {
border: 1.5px solid var(--gray-300);
border-radius: 12px;
background: #fff;
margin-bottom: 14px;
scroll-margin-top: 20px;
}
details.file-collapsed summary {
list-style: none;
cursor: pointer;
padding: 14px 20px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
details.file-collapsed summary::-webkit-details-marker { display: none; }
details.file-collapsed summary::after {
content: "+";
font-family: var(--mono);
color: var(--gray-500);
font-size: 16px;
}
details.file-collapsed[open] summary::after { content: ""; }
details.file-collapsed .body {
padding: 0 20px 16px;
font-size: 13.5px;
color: var(--gray-700);
}
/* ---------- Footer ---------- */
footer.next-steps {
border: 1.5px solid var(--gray-300);
border-radius: 12px;
background: #fff;
padding: 24px 28px;
}
.checklist { list-style: none; padding: 0; }
.checklist li {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 0;
}
.checklist input[type="checkbox"] {
width: 17px;
height: 17px;
margin-top: 2px;
accent-color: var(--olive);
cursor: pointer;
}
.checklist label { cursor: pointer; flex: 1; }
.checklist code {
font-family: var(--mono);
font-size: 12.5px;
background: var(--gray-150);
padding: 1px 5px;
border-radius: 4px;
}
</style>
</head>
<body>
<div class="page">
<header class="pr-head">
<div class="repo-line">acme/web · Pull Request #247</div>
<h1>Add optimistic updates to task list mutations</h1>
<div class="meta-row">
<div class="author">
<div class="avatar">MO</div>
<div>
<div class="author-name">Mira Okafor</div>
<div class="author-sub">opened 2 days ago</div>
</div>
</div>
<div class="branch">
mo/optimistic-tasks <span class="arrow"></span> main
</div>
<div class="stat">
<span class="add">+142</span> / <span class="del">38</span>
<span class="files">6 files changed</span>
</div>
</div>
</header>
<section class="prose">
<h2>What this PR does</h2>
<ul>
<li>Replaces the await-then-refetch pattern in <code style="font-family:var(--mono);font-size:12.5px;background:var(--gray-150);padding:1px 5px;border-radius:4px">TaskList</code> with optimistic cache writes, so toggling or reordering a task feels instant instead of waiting ~300ms for the round-trip.</li>
<li>Introduces a small <code style="font-family:var(--mono);font-size:12.5px;background:var(--gray-150);padding:1px 5px;border-radius:4px">useOptimisticTasks</code> hook that wraps the mutation, snapshots the previous list, and rolls back on error.</li>
<li>Extends the API client to accept an idempotency key per mutation and adds a toast when a rollback fires.</li>
</ul>
</section>
<section>
<h2>Risk map</h2>
<div class="risk-map">
<a class="chip attention" href="#file-hook"><span class="dot"></span>useOptimisticTasks.ts</a>
<a class="chip medium" href="#file-tasklist"><span class="dot"></span>TaskList.tsx</a>
<a class="chip medium" href="#file-api"><span class="dot"></span>api/tasks.ts</a>
<a class="chip safe" href="#file-toast"><span class="dot"></span>Toast.tsx</a>
<a class="chip safe" href="#file-types"><span class="dot"></span>types/task.ts</a>
<a class="chip safe" href="#file-test"><span class="dot"></span>TaskList.test.tsx</a>
</div>
<div class="legend">
<span><span class="dot" style="background:var(--olive)"></span> safe</span>
<span><span class="dot" style="background:#B89B6E"></span> worth a look</span>
<span><span class="dot" style="background:var(--clay)"></span> needs attention</span>
</div>
</section>
<section>
<h2>Files</h2>
<!-- File 1: hook (attention) -->
<article class="file-card" id="file-hook">
<div class="file-head">
<div>
<div class="file-path">src/hooks/useOptimisticTasks.ts</div>
</div>
<div style="display:flex;align-items:center;gap:12px">
<span class="risk-tag attention">needs attention</span>
<span class="file-delta"><span class="add">+58</span> <span class="del">0</span></span>
</div>
</div>
<div class="diff">
<div class="diff-row hunk"><span class="ln"></span><span class="mark"></span><span class="code">@@ -0,0 +1,58 @@</span></div>
<div class="diff-row add"><span class="ln">1</span><span class="mark">+</span><span class="code">import { useMutation, useQueryClient } from '@tanstack/react-query';</span></div>
<div class="diff-row add"><span class="ln">2</span><span class="mark">+</span><span class="code">import { updateTask, TaskPatch } from '../api/tasks';</span></div>
<div class="diff-row add"><span class="ln">3</span><span class="mark">+</span><span class="code">import type { Task } from '../types/task';</span></div>
<div class="diff-row add"><span class="ln">4</span><span class="mark">+</span><span class="code"> </span></div>
<div class="diff-row add"><span class="ln">5</span><span class="mark">+</span><span class="code">export function useOptimisticTasks(boardId: string) {</span></div>
<div class="diff-row add"><span class="ln">6</span><span class="mark">+</span><span class="code"> const qc = useQueryClient();</span></div>
<div class="diff-row add"><span class="ln">7</span><span class="mark">+</span><span class="code"> const key = ['tasks', boardId];</span></div>
<div class="diff-row add"><span class="ln">8</span><span class="mark">+</span><span class="code"> </span></div>
<div class="diff-row add"><span class="ln">9</span><span class="mark">+</span><span class="code"> return useMutation({</span></div>
<div class="diff-row add"><span class="ln">10</span><span class="mark">+</span><span class="code"> mutationFn: (patch: TaskPatch) =&gt; updateTask(patch),</span></div>
<div class="diff-row add"><span class="ln">11</span><span class="mark">+</span><span class="code"> onMutate: async (patch) =&gt; {</span></div>
<div class="diff-row add"><span class="ln">12</span><span class="mark">+</span><span class="code"> const prev = qc.getQueryData&lt;Task[]&gt;(key);</span></div>
<div class="diff-row add"><span class="ln">13</span><span class="mark">+</span><span class="code"> qc.setQueryData&lt;Task[]&gt;(key, (old = []) =&gt;</span></div>
<div class="diff-row add"><span class="ln">14</span><span class="mark">+</span><span class="code"> old.map(t =&gt; t.id === patch.id ? { ...t, ...patch } : t)</span></div>
<div class="diff-row add"><span class="ln">15</span><span class="mark">+</span><span class="code"> );</span></div>
<div class="diff-row add"><span class="ln">16</span><span class="mark">+</span><span class="code"> return { prev };</span></div>
<div class="diff-row add"><span class="ln">17</span><span class="mark">+</span><span class="code"> },</span></div>
<div class="diff-row add"><span class="ln">18</span><span class="mark">+</span><span class="code"> onError: (_e, _p, ctx) =&gt; qc.setQueryData(key, ctx?.prev),</span></div>
<div class="diff-row add"><span class="ln">19</span><span class="mark">+</span><span class="code"> });</span></div>
<div class="diff-row add"><span class="ln">20</span><span class="mark">+</span><span class="code">}</span></div>
</div>
<div class="comments">
<div class="bubble blocking">
<div class="anchor">line 11</div>
<p><span class="label">Blocking</span><code>onMutate</code> doesn't call <code>qc.cancelQueries(key)</code> first. If a background refetch lands between the optimistic write and the server response, it will clobber the optimistic state and the UI will flicker back to the old value.</p>
</div>
<div class="bubble nit">
<div class="anchor">line 18</div>
<p><span class="label">Nit</span>Rollback restores the list but never surfaces the error. Consider wiring the existing <code>pushToast</code> here so users know the toggle didn't stick.</p>
</div>
</div>
</article>
<!-- File 2: TaskList (medium) -->
<article class="file-card" id="file-tasklist">
<div class="file-head">
<div>
<div class="file-path">src/components/TaskList.tsx</div>
</div>
<div style="display:flex;align-items:center;gap:12px">
<span class="risk-tag medium">worth a look</span>
<span class="file-delta"><span class="add">+31</span> <span class="del">24</span></span>
</div>
</div>
<div class="diff">
<div class="diff-row hunk"><span class="ln"></span><span class="mark"></span><span class="code">@@ -42,14 +42,17 @@ export function TaskList({ boardId }: Props) {</span></div>
<div class="diff-row ctx"><span class="ln">42</span><span class="mark"> </span><span class="code"> const { data: tasks } = useTasks(boardId);</span></div>
<div class="diff-row del"><span class="ln">43</span><span class="mark">-</span><span class="code"> const [pending, setPending] = useState&lt;string | null&gt;(null);</span></div>
<div class="diff-row del"><span class="ln">44</span><span class="mark">-</span><span class="code"> </span></div>
<div class="diff-row del"><span class="ln">45</span><span class="mark">-</span><span class="code"> async function toggle(task: Task) {</span></div>
<div class="diff-row del"><span class="ln">46</span><span class="mark">-</span><span class="code"> setPending(task.id);</span></div>
<div class="diff-row del"><span class="ln">47</span><span class="mark">-</span><span class="code"> await updateTask({ id: task.id, done: !task.done });</span></div>
<div class="diff-row del"><span class="ln">48</span><span class="mark">-</span><span class="code"> await refetch();</span></div>
<div class="diff-row del"><span class="ln">49</span><span class="mark">-</span><span class="code"> setPending(null);</span></div>
<div class="diff-row del"><span class="ln">50</span><span class="mark">-</span><span class="code"> }</span></div>
<div class="diff-row add"><span class="ln">43</span><span class="mark">+</span><span class="code"> const { mutate, isPending } = useOptimisticTasks(boardId);</span></div>
<div class="diff-row add"><span class="ln">44</span><span class="mark">+</span><span class="code"> </span></div>
<div class="diff-row add"><span class="ln">45</span><span class="mark">+</span><span class="code"> const toggle = (task: Task) =&gt;</span></div>
<div class="diff-row add"><span class="ln">46</span><span class="mark">+</span><span class="code"> mutate({ id: task.id, done: !task.done });</span></div>
<div class="diff-row ctx"><span class="ln">47</span><span class="mark"> </span><span class="code"> </span></div>
<div class="diff-row ctx"><span class="ln">48</span><span class="mark"> </span><span class="code"> return (</span></div>
<div class="diff-row ctx"><span class="ln">49</span><span class="mark"> </span><span class="code"> &lt;ul className="tasks"&gt;</span></div>
<div class="diff-row del"><span class="ln">50</span><span class="mark">-</span><span class="code"> {tasks?.map(t =&gt; &lt;TaskRow key={t.id} task={t} busy={pending === t.id} /&gt;)}</span></div>
<div class="diff-row add"><span class="ln">50</span><span class="mark">+</span><span class="code"> {tasks?.map(t =&gt; &lt;TaskRow key={t.id} task={t} onToggle={toggle} /&gt;)}</span></div>
<div class="diff-row ctx"><span class="ln">51</span><span class="mark"> </span><span class="code"> &lt;/ul&gt;</span></div>
</div>
<div class="comments">
<div class="bubble nit">
<div class="anchor">line 43</div>
<p><span class="label">Nit</span><code>isPending</code> is destructured but never read. Either drop it or pass it to <code>TaskRow</code> so the checkbox can dim while the request is in flight.</p>
</div>
</div>
</article>
<!-- File 3: api/tasks (medium) -->
<article class="file-card" id="file-api">
<div class="file-head">
<div>
<div class="file-path">src/api/tasks.ts</div>
</div>
<div style="display:flex;align-items:center;gap:12px">
<span class="risk-tag medium">worth a look</span>
<span class="file-delta"><span class="add">+19</span> <span class="del">6</span></span>
</div>
</div>
<div class="diff">
<div class="diff-row hunk"><span class="ln"></span><span class="mark"></span><span class="code">@@ -12,10 +12,15 @@ export type TaskPatch = Partial&lt;Task&gt; &amp; { id: string };</span></div>
<div class="diff-row ctx"><span class="ln">12</span><span class="mark"> </span><span class="code"> </span></div>
<div class="diff-row del"><span class="ln">13</span><span class="mark">-</span><span class="code">export async function updateTask(patch: TaskPatch) {</span></div>
<div class="diff-row del"><span class="ln">14</span><span class="mark">-</span><span class="code"> return http.patch(`/tasks/${patch.id}`, patch);</span></div>
<div class="diff-row add"><span class="ln">13</span><span class="mark">+</span><span class="code">export async function updateTask(</span></div>
<div class="diff-row add"><span class="ln">14</span><span class="mark">+</span><span class="code"> patch: TaskPatch,</span></div>
<div class="diff-row add"><span class="ln">15</span><span class="mark">+</span><span class="code"> key = crypto.randomUUID(),</span></div>
<div class="diff-row add"><span class="ln">16</span><span class="mark">+</span><span class="code">) {</span></div>
<div class="diff-row add"><span class="ln">17</span><span class="mark">+</span><span class="code"> return http.patch(`/tasks/${patch.id}`, patch, {</span></div>
<div class="diff-row add"><span class="ln">18</span><span class="mark">+</span><span class="code"> headers: { 'Idempotency-Key': key },</span></div>
<div class="diff-row add"><span class="ln">19</span><span class="mark">+</span><span class="code"> });</span></div>
<div class="diff-row ctx"><span class="ln">20</span><span class="mark"> </span><span class="code">}</span></div>
</div>
<div class="comments">
<div class="bubble blocking">
<div class="anchor">line 15</div>
<p><span class="label">Blocking</span>Generating the idempotency key as a default parameter means retries from the mutation layer get a <em>new</em> key each time, which defeats the purpose. The key should be minted once in <code>onMutate</code> and threaded through.</p>
</div>
</div>
</article>
<!-- Collapsed files -->
<details class="file-collapsed" id="file-toast">
<summary>
<span class="file-path">src/components/Toast.tsx</span>
<span style="display:flex;align-items:center;gap:12px">
<span class="risk-tag safe">safe</span>
<span class="file-delta"><span class="add">+14</span> <span class="del">2</span></span>
</span>
</summary>
<div class="body">Adds a <code style="font-family:var(--mono)">variant="warning"</code> style and exports <code style="font-family:var(--mono)">pushToast</code>. Purely additive, no behaviour change for existing call sites.</div>
</details>
<details class="file-collapsed" id="file-types">
<summary>
<span class="file-path">src/types/task.ts</span>
<span style="display:flex;align-items:center;gap:12px">
<span class="risk-tag safe">safe</span>
<span class="file-delta"><span class="add">+6</span> <span class="del">2</span></span>
</span>
</summary>
<div class="body">Widens <code style="font-family:var(--mono)">Task.status</code> to include <code style="font-family:var(--mono)">"archived"</code> and adds an optional <code style="font-family:var(--mono)">updatedAt</code> timestamp. Type-only change.</div>
</details>
<details class="file-collapsed" id="file-test">
<summary>
<span class="file-path">src/components/__tests__/TaskList.test.tsx</span>
<span style="display:flex;align-items:center;gap:12px">
<span class="risk-tag safe">safe</span>
<span class="file-delta"><span class="add">+14</span> <span class="del">4</span></span>
</span>
</summary>
<div class="body">Adds a test asserting the row updates synchronously after click, and one asserting rollback when the mocked request rejects. Both pass locally.</div>
</details>
</section>
<footer class="next-steps">
<h2>Suggested next steps</h2>
<ul class="checklist">
<li>
<input type="checkbox" id="step1">
<label for="step1">Add <code>await qc.cancelQueries(key)</code> at the top of <code>onMutate</code> in <code>useOptimisticTasks.ts</code>.</label>
</li>
<li>
<input type="checkbox" id="step2">
<label for="step2">Move idempotency-key generation into the mutation context so retries reuse the same key.</label>
</li>
<li>
<input type="checkbox" id="step3">
<label for="step3">Either consume <code>isPending</code> in <code>TaskRow</code> or remove it from the destructure to keep lint clean.</label>
</li>
</ul>
</footer>
</div>
<script>
// Briefly highlight a file card when reached via the risk-map anchors.
document.querySelectorAll('.risk-map a').forEach(function (a) {
a.addEventListener('click', function () {
var target = document.querySelector(a.getAttribute('href'));
if (!target) return;
target.style.transition = 'box-shadow 180ms ease';
target.style.boxShadow = '0 0 0 3px rgba(217,119,87,0.35)';
setTimeout(function () { target.style.boxShadow = 'none'; }, 1400);
});
});
</script>
</body>
</html>
+492
View File
@@ -0,0 +1,492 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>How authentication flows through acme/web</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--rust: #B04A3F;
--gray-150: #F0EEE6;
--gray-300: #D1CFC5;
--gray-500: #87867F;
--gray-700: #3D3D3A;
--serif: ui-serif, Georgia, 'Times New Roman', serif;
--sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--ivory);
color: var(--gray-700);
font-family: var(--sans);
font-size: 15px;
line-height: 1.65;
padding: 48px 24px 80px;
}
.page {
max-width: 1080px;
margin: 0 auto;
display: grid;
grid-template-columns: minmax(0, 1fr) 280px;
gap: 40px;
}
@media (max-width: 960px) {
.page { grid-template-columns: 1fr; }
}
/* ---------- Header ---------- */
header {
grid-column: 1 / -1;
margin-bottom: 8px;
}
.repo-line {
font-family: var(--mono);
font-size: 12.5px;
color: var(--gray-500);
margin-bottom: 10px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 32px;
line-height: 1.25;
color: var(--slate);
margin-bottom: 16px;
}
.summary {
max-width: 760px;
font-size: 15.5px;
}
.summary code { font-family: var(--mono); font-size: 13px; }
h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 22px;
color: var(--slate);
margin: 36px 0 16px;
}
/* ---------- Diagram ---------- */
.diagram-panel {
border: 1.5px solid var(--gray-300);
border-radius: 12px;
background: #fff;
padding: 20px;
overflow-x: auto;
}
svg.flow { display: block; max-width: 100%; }
.flow text {
font-family: var(--mono);
font-size: 12px;
fill: var(--slate);
}
.flow .sub { font-size: 10px; fill: var(--gray-500); }
.flow .box { fill: #fff; stroke: var(--gray-300); stroke-width: 1.5; }
.flow .box.hot { fill: rgba(217,119,87,0.10); stroke: var(--clay); }
.flow .arrow { stroke: var(--gray-500); stroke-width: 1.5; fill: none; }
/* ---------- Walkthrough ---------- */
.step {
display: grid;
grid-template-columns: 44px 1fr;
gap: 18px;
padding: 20px 0;
border-bottom: 1.5px solid var(--gray-150);
}
.step:last-of-type { border-bottom: none; }
.badge {
width: 34px;
height: 34px;
border-radius: 50%;
background: var(--oat);
border: 1.5px solid var(--gray-300);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--mono);
font-weight: 600;
color: var(--slate);
font-size: 14px;
}
.step.hot .badge {
background: rgba(217,119,87,0.14);
border-color: var(--clay);
color: var(--clay);
}
.step-loc {
font-family: var(--mono);
font-size: 13px;
color: var(--slate);
margin-bottom: 6px;
}
.step-loc .range { color: var(--gray-500); }
.step-body p { margin-bottom: 10px; }
details.snippet { margin-top: 6px; }
details.snippet summary {
list-style: none;
cursor: pointer;
font-size: 12.5px;
color: var(--gray-500);
font-family: var(--mono);
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 0;
user-select: none;
}
details.snippet summary::-webkit-details-marker { display: none; }
details.snippet summary::before {
content: "▸";
font-size: 10px;
transition: transform 0.15s ease;
}
details.snippet[open] summary::before { transform: rotate(90deg); }
pre.code {
background: var(--slate);
color: #E8E6DC;
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.7;
border-radius: 8px;
padding: 14px 16px;
overflow-x: auto;
margin-top: 10px;
}
pre.code .dim { color: var(--gray-500); }
pre.code .kw { color: #C9B98A; }
pre.code .str { color: #A8BC8C; }
/* ---------- Sidebar ---------- */
aside {
position: sticky;
top: 24px;
align-self: start;
}
.panel {
border: 1.5px solid var(--gray-300);
border-radius: 12px;
background: #fff;
padding: 18px 20px;
margin-bottom: 20px;
}
.panel h3 {
font-family: var(--sans);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--gray-500);
margin-bottom: 12px;
}
.key-files { list-style: none; padding: 0; }
.key-files li { margin-bottom: 12px; }
.key-files li:last-child { margin-bottom: 0; }
.key-files .path {
font-family: var(--mono);
font-size: 12px;
color: var(--slate);
display: block;
margin-bottom: 2px;
word-break: break-all;
}
.key-files .desc {
font-size: 12.5px;
color: var(--gray-500);
line-height: 1.45;
}
.gotchas {
border: 1.5px solid var(--clay);
border-radius: 12px;
background: rgba(217,119,87,0.06);
padding: 18px 20px;
}
.gotchas h3 {
font-family: var(--sans);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--clay);
margin-bottom: 10px;
}
.gotchas ul { list-style: none; padding: 0; }
.gotchas li {
position: relative;
padding-left: 16px;
font-size: 13px;
margin-bottom: 8px;
}
.gotchas li::before {
content: "";
position: absolute;
left: 0; top: 8px;
width: 5px; height: 5px;
background: var(--clay);
border-radius: 2px;
}
.gotchas code { font-family: var(--mono); font-size: 11.5px; }
</style>
</head>
<body>
<div class="page">
<header>
<div class="repo-line">acme/web · architecture note</div>
<h1>How authentication flows through the codebase</h1>
<p class="summary">
Acme uses cookie-based sessions: the browser never holds a bearer token directly. Every authenticated request hits <code>/api/*</code>, passes through a single <code>verifyToken()</code> middleware, and resolves to a <code>Session</code> row that downstream handlers read off <code>req.ctx</code>. The middleware is the only place that talks to the session store, which is the only place that talks to the <code>sessions</code> table — so there's exactly one trust boundary to reason about.
</p>
</header>
<main>
<h2 style="margin-top:0">Request path</h2>
<div class="diagram-panel">
<svg class="flow" viewBox="0 0 720 280" width="720" height="280" role="img" aria-label="Authentication flow diagram">
<defs>
<marker id="arrowHead" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#87867F"></path>
</marker>
</defs>
<!-- Row 1 -->
<g>
<rect class="box" x="30" y="40" width="150" height="64" rx="10"></rect>
<text x="105" y="68" text-anchor="middle">Browser</text>
<text class="sub" x="105" y="84" text-anchor="middle">acme.app</text>
</g>
<g>
<rect class="box" x="245" y="40" width="150" height="64" rx="10"></rect>
<text x="320" y="68" text-anchor="middle">/api/session</text>
<text class="sub" x="320" y="84" text-anchor="middle">route handler</text>
</g>
<g>
<rect class="box hot" x="460" y="40" width="180" height="64" rx="10"></rect>
<text x="550" y="68" text-anchor="middle">verifyToken()</text>
<text class="sub" x="550" y="84" text-anchor="middle">middleware/auth.ts</text>
</g>
<!-- Row 2 -->
<g>
<rect class="box" x="460" y="170" width="180" height="64" rx="10"></rect>
<text x="550" y="198" text-anchor="middle">SessionStore</text>
<text class="sub" x="550" y="214" text-anchor="middle">lib/sessionStore.ts</text>
</g>
<g>
<rect class="box" x="245" y="170" width="150" height="64" rx="10"></rect>
<text x="320" y="198" text-anchor="middle">Postgres</text>
<text class="sub" x="320" y="214" text-anchor="middle">sessions table</text>
</g>
<!-- Arrows -->
<line class="arrow" x1="180" y1="72" x2="245" y2="72" marker-end="url(#arrowHead)"></line>
<line class="arrow" x1="395" y1="72" x2="460" y2="72" marker-end="url(#arrowHead)"></line>
<line class="arrow" x1="550" y1="104" x2="550" y2="170" marker-end="url(#arrowHead)"></line>
<line class="arrow" x1="460" y1="202" x2="395" y2="202" marker-end="url(#arrowHead)"></line>
<text class="sub" x="212" y="62" text-anchor="middle">cookie</text>
<text class="sub" x="560" y="140" text-anchor="start">lookup</text>
</svg>
</div>
<h2>Callstack walkthrough</h2>
<!-- Step 1 -->
<div class="step">
<div class="badge">1</div>
<div class="step-body">
<div class="step-loc">src/app/providers/AuthProvider.tsx<span class="range"> :22-48</span></div>
<p>On mount, the React provider issues a <code style="font-family:var(--mono);font-size:12.5px">GET /api/session</code> with <code style="font-family:var(--mono);font-size:12.5px">credentials: 'include'</code> so the <code style="font-family:var(--mono);font-size:12.5px">fw_sid</code> cookie rides along. The response either hydrates <code style="font-family:var(--mono);font-size:12.5px">currentUser</code> into context or leaves it <code style="font-family:var(--mono);font-size:12.5px">null</code>, which the router treats as "show the sign-in screen".</p>
<details class="snippet">
<summary>show source</summary>
<pre class="code"><span class="dim">// src/app/providers/AuthProvider.tsx</span>
<span class="kw">export function</span> AuthProvider({ children }: Props) {
<span class="kw">const</span> [user, setUser] = useState&lt;User | <span class="kw">null</span>&gt;(<span class="kw">null</span>);
useEffect(() =&gt; {
fetch(<span class="str">'/api/session'</span>, { credentials: <span class="str">'include'</span> })
.then(r =&gt; r.ok ? r.json() : <span class="kw">null</span>)
.then(setUser);
}, []);
<span class="kw">return</span> &lt;AuthCtx.Provider value={{ user }}&gt;{children}&lt;/AuthCtx.Provider&gt;;
}</pre>
</details>
</div>
</div>
<!-- Step 2 -->
<div class="step">
<div class="badge">2</div>
<div class="step-body">
<div class="step-loc">src/server/routes/session.ts<span class="range"> :9-27</span></div>
<p>The route itself is thin: it just returns whatever <code style="font-family:var(--mono);font-size:12.5px">req.ctx.session</code> the middleware attached. If the middleware short-circuited with a 401, this handler never runs — so there's no auth logic duplicated here.</p>
<details class="snippet">
<summary>show source</summary>
<pre class="code"><span class="dim">// src/server/routes/session.ts</span>
router.get(<span class="str">'/session'</span>, verifyToken, (req, res) =&gt; {
<span class="kw">const</span> { session } = req.ctx;
res.json({
id: session.userId,
email: session.email,
role: session.role,
exp: session.expiresAt,
});
});</pre>
</details>
</div>
</div>
<!-- Step 3 -->
<div class="step hot">
<div class="badge">3</div>
<div class="step-body">
<div class="step-loc">src/middleware/auth.ts<span class="range"> :14-31</span></div>
<p>This is the trust boundary. <code style="font-family:var(--mono);font-size:12.5px">verifyToken</code> reads the signed <code style="font-family:var(--mono);font-size:12.5px">fw_sid</code> cookie, asks <code style="font-family:var(--mono);font-size:12.5px">SessionStore</code> to resolve it, and either populates <code style="font-family:var(--mono);font-size:12.5px">req.ctx.session</code> or responds 401. Every protected route in the app is mounted behind this function, so changing its behaviour changes auth globally.</p>
<details class="snippet" open>
<summary>show source</summary>
<pre class="code"><span class="dim">// src/middleware/auth.ts</span>
<span class="kw">export async function</span> verifyToken(req, res, next) {
<span class="kw">const</span> raw = req.signedCookies[<span class="str">'fw_sid'</span>];
<span class="kw">if</span> (!raw) <span class="kw">return</span> res.status(401).end();
<span class="kw">const</span> session = <span class="kw">await</span> SessionStore.get(raw);
<span class="kw">if</span> (!session || session.expiresAt &lt; Date.now()) {
<span class="kw">return</span> res.status(401).end();
}
req.ctx = { session };
next();
}</pre>
</details>
</div>
</div>
<!-- Step 4 -->
<div class="step">
<div class="badge">4</div>
<div class="step-body">
<div class="step-loc">src/lib/sessionStore.ts<span class="range"> :8-52</span></div>
<p><code style="font-family:var(--mono);font-size:12.5px">SessionStore</code> is a small read-through cache: it checks an in-process LRU first, then falls back to Postgres. Writes (<code style="font-family:var(--mono);font-size:12.5px">create</code>, <code style="font-family:var(--mono);font-size:12.5px">revoke</code>) always go straight to the DB and invalidate the cache entry so other workers don't serve a stale session.</p>
<details class="snippet">
<summary>show source</summary>
<pre class="code"><span class="dim">// src/lib/sessionStore.ts</span>
<span class="kw">const</span> cache = <span class="kw">new</span> LRU&lt;string, Session&gt;({ max: 5000, ttl: 60_000 });
<span class="kw">export const</span> SessionStore = {
<span class="kw">async</span> get(id: string) {
<span class="kw">const</span> hit = cache.get(id);
<span class="kw">if</span> (hit) <span class="kw">return</span> hit;
<span class="kw">const</span> row = <span class="kw">await</span> db.one(SELECT_SESSION, [id]);
<span class="kw">if</span> (row) cache.set(id, row);
<span class="kw">return</span> row ?? <span class="kw">null</span>;
},
<span class="dim">/* create, revoke, touch ... */</span>
};</pre>
</details>
</div>
</div>
<!-- Step 5 -->
<div class="step">
<div class="badge">5</div>
<div class="step-body">
<div class="step-loc">db/migrations/004_sessions.sql<span class="range"> :1-18</span></div>
<p>The <code style="font-family:var(--mono);font-size:12.5px">sessions</code> table is keyed on a random 32-byte id (the cookie value) with a covering index on <code style="font-family:var(--mono);font-size:12.5px">user_id</code> for "sign out everywhere". Expiry is enforced both here (<code style="font-family:var(--mono);font-size:12.5px">expires_at</code>) and again in the middleware as defence in depth.</p>
<details class="snippet">
<summary>show source</summary>
<pre class="code"><span class="dim">-- db/migrations/004_sessions.sql</span>
<span class="kw">create table</span> sessions (
id <span class="kw">text primary key</span>,
user_id <span class="kw">uuid not null references</span> users(id),
created_at <span class="kw">timestamptz default</span> now(),
expires_at <span class="kw">timestamptz not null</span>,
ip <span class="kw">inet</span>,
user_agent <span class="kw">text</span>
);
<span class="kw">create index</span> sessions_user_id_idx <span class="kw">on</span> sessions(user_id);</pre>
</details>
</div>
</div>
</main>
<aside>
<div class="panel">
<h3>Key files</h3>
<ul class="key-files">
<li>
<span class="path">src/middleware/auth.ts</span>
<span class="desc">Single entry point for request authentication.</span>
</li>
<li>
<span class="path">src/lib/sessionStore.ts</span>
<span class="desc">LRU + Postgres session lookup; only DB caller.</span>
</li>
<li>
<span class="path">src/server/routes/session.ts</span>
<span class="desc">Returns the current session to the client.</span>
</li>
<li>
<span class="path">src/server/routes/login.ts</span>
<span class="desc">Exchanges credentials for a cookie via SessionStore.create.</span>
</li>
<li>
<span class="path">src/app/providers/AuthProvider.tsx</span>
<span class="desc">Client-side context that mirrors the server session.</span>
</li>
<li>
<span class="path">db/migrations/004_sessions.sql</span>
<span class="desc">Schema for the sessions table and indexes.</span>
</li>
</ul>
</div>
<div class="gotchas">
<h3>Gotchas</h3>
<ul>
<li>The LRU in <code>SessionStore</code> is per-process. Revoking a session only clears the local cache — other workers may serve it for up to 60s until their TTL lapses.</li>
<li><code>verifyToken</code> compares <code>expiresAt</code> against <code>Date.now()</code>, but the column is <code>timestamptz</code>. The driver returns a <code>Date</code>, so the comparison works, but don't refactor it to a raw string without adjusting the check.</li>
</ul>
</div>
</aside>
</div>
<script>
// Keep at most one snippet open at a time so the walkthrough stays scannable.
var snippets = document.querySelectorAll('details.snippet');
snippets.forEach(function (d) {
d.addEventListener('toggle', function () {
if (!d.open) return;
snippets.forEach(function (other) {
if (other !== d) other.open = false;
});
});
});
</script>
</body>
</html>
+630
View File
@@ -0,0 +1,630 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Acme — Design System Reference</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-100: #F0EEE6;
--gray-300: #D1CFC5;
--gray-500: #87867F;
--gray-700: #3D3D3A;
--white: #FFFFFF;
--serif: ui-serif, Georgia, serif;
--sans: system-ui, -apple-system, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, monospace;
--radius-panel: 12px;
--radius-row: 8px;
--border: 1.5px solid var(--gray-300);
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 56px 24px 96px;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
font-size: 15px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.page {
max-width: 980px;
margin: 0 auto;
}
header h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 40px;
letter-spacing: -0.01em;
margin: 0 0 6px;
}
header .sub {
color: var(--gray-500);
font-size: 14px;
margin: 0 0 48px;
}
header .sub code {
font-family: var(--mono);
font-size: 13px;
background: var(--gray-100);
padding: 1px 5px;
border-radius: 4px;
}
section { margin-bottom: 64px; }
h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 26px;
margin: 0 0 8px;
letter-spacing: -0.01em;
}
hr.rule {
border: none;
border-top: 1px solid var(--gray-300);
margin: 0 0 28px;
}
/* ---------- Color ---------- */
.swatch-group {
margin-bottom: 28px;
}
.swatch-group-label {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--gray-500);
margin-bottom: 12px;
}
.swatch-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
gap: 20px 16px;
}
.swatch {}
.swatch .chip {
width: 64px;
height: 64px;
border-radius: var(--radius-row);
border: var(--border);
margin-bottom: 8px;
}
.swatch .chip.no-border { border-color: transparent; }
.swatch .hex {
font-family: var(--mono);
font-size: 12px;
color: var(--gray-700);
display: block;
}
.swatch .token {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
display: block;
}
/* ---------- Typography ---------- */
.type-scale {
border: var(--border);
border-radius: var(--radius-panel);
background: var(--white);
overflow: hidden;
}
.type-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 24px;
padding: 20px 24px;
border-bottom: 1px solid var(--gray-100);
}
.type-row:last-child { border-bottom: none; }
.type-specimen {
flex: 1;
min-width: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--slate);
}
.type-meta {
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
text-align: right;
flex-shrink: 0;
}
.type-meta .name {
color: var(--gray-700);
display: block;
margin-bottom: 2px;
}
.t-display { font-family: var(--serif); font-size: 48px; line-height: 1.1; font-weight: 500; letter-spacing: -0.02em; }
.t-h1 { font-family: var(--serif); font-size: 32px; line-height: 1.2; font-weight: 500; letter-spacing: -0.01em; }
.t-h2 { font-family: var(--serif); font-size: 24px; line-height: 1.3; font-weight: 500; }
.t-body { font-family: var(--sans); font-size: 16px; line-height: 1.55; font-weight: 430; }
.t-small { font-family: var(--sans); font-size: 14px; line-height: 1.5; font-weight: 430; }
.t-caption { font-family: var(--sans); font-size: 12px; line-height: 1.4; font-weight: 500; color: var(--gray-500); }
/* ---------- Spacing ---------- */
.spacing-ruler {
display: flex;
align-items: flex-end;
gap: 28px;
padding: 28px 24px;
background: var(--white);
border: var(--border);
border-radius: var(--radius-panel);
overflow-x: auto;
}
.space-token {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
flex-shrink: 0;
}
.space-bar {
background: var(--clay);
border-radius: 3px;
height: 14px;
}
.space-label {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-700);
text-align: center;
}
.space-label span {
display: block;
color: var(--gray-500);
}
/* ---------- Radius & Elevation ---------- */
.token-row {
display: flex;
flex-wrap: wrap;
gap: 20px;
margin-bottom: 28px;
}
.radius-card {
width: 120px;
height: 88px;
background: var(--oat);
border: var(--border);
display: flex;
align-items: flex-end;
padding: 10px 12px;
}
.radius-card .lbl {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-700);
}
.radius-card .lbl span { color: var(--gray-500); display: block; }
.shadow-card {
width: 160px;
height: 96px;
background: var(--white);
border-radius: var(--radius-panel);
display: flex;
align-items: flex-end;
padding: 12px 14px;
}
.shadow-card .lbl {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-700);
}
.shadow-card .lbl span { color: var(--gray-500); display: block; }
/* ---------- Core components ---------- */
.component {
margin-bottom: 32px;
}
.component-name {
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
margin-bottom: 12px;
}
.component-stage {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 16px;
padding: 24px;
background: var(--white);
border: var(--border);
border-radius: var(--radius-panel);
}
/* Button */
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 36px;
padding: 0 16px;
font-family: var(--sans);
font-size: 14px;
font-weight: 500;
border-radius: var(--radius-row);
border: 1.5px solid transparent;
cursor: pointer;
transition: background 0.12s ease, border-color 0.12s ease;
}
.btn-primary { background: var(--clay); color: var(--white); }
.btn-primary:hover { background: #C7684C; }
.btn-secondary { background: var(--white); color: var(--slate); border-color: var(--gray-300); }
.btn-secondary:hover { background: var(--gray-100); }
.btn-ghost { background: transparent; color: var(--gray-700); }
.btn-ghost:hover { background: var(--gray-100); }
.btn-danger { background: #B04A4A; color: var(--white); }
.btn-danger:hover { background: #9A3F3F; }
/* Input */
.input {
height: 38px;
padding: 0 12px;
font-family: var(--sans);
font-size: 14px;
color: var(--slate);
background: var(--white);
border: var(--border);
border-radius: var(--radius-row);
width: 260px;
outline: none;
transition: border-color 0.12s ease, box-shadow 0.12s ease;
}
.input::placeholder { color: var(--gray-500); }
.input:focus {
border-color: var(--clay);
box-shadow: 0 0 0 3px rgba(217, 119, 87, 0.15);
}
/* Checkbox */
.checkbox {
display: inline-flex;
align-items: center;
gap: 10px;
font-size: 14px;
cursor: pointer;
user-select: none;
}
.checkbox input {
appearance: none;
width: 18px;
height: 18px;
border: var(--border);
border-radius: 5px;
background: var(--white);
margin: 0;
cursor: pointer;
position: relative;
transition: background 0.12s ease, border-color 0.12s ease;
}
.checkbox input:checked {
background: var(--clay);
border-color: var(--clay);
}
.checkbox input:checked::after {
content: "";
position: absolute;
left: 5px; top: 1px;
width: 5px; height: 10px;
border: solid var(--white);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* Badge */
.badge {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 9px;
font-size: 12px;
font-weight: 500;
border-radius: 999px;
}
.badge-neutral { background: var(--gray-100); color: var(--gray-700); }
.badge-accent { background: rgba(217, 119, 87, 0.14); color: var(--clay); }
.badge-success { background: rgba(120, 140, 93, 0.16); color: var(--olive); }
.badge-warning { background: rgba(199, 142, 63, 0.16); color: #A06A2A; }
</style>
</head>
<body>
<div class="page">
<header>
<h1>Acme design system</h1>
<p class="sub">
Generated from <code>src/styles/tokens.ts</code> and <code>src/components/</code> &mdash; use as a portable reference when prompting.
</p>
</header>
<!-- ===================== COLOR ===================== -->
<section id="color">
<h2>Color</h2>
<hr class="rule">
<div class="swatch-group">
<div class="swatch-group-label">Primary</div>
<div class="swatch-grid">
<div class="swatch">
<div class="chip no-border" style="background:#D97757"></div>
<span class="hex">#D97757</span>
<span class="token">--clay</span>
</div>
<div class="swatch">
<div class="chip no-border" style="background:#141413"></div>
<span class="hex">#141413</span>
<span class="token">--slate</span>
</div>
<div class="swatch">
<div class="chip" style="background:#FAF9F5"></div>
<span class="hex">#FAF9F5</span>
<span class="token">--ivory</span>
</div>
<div class="swatch">
<div class="chip no-border" style="background:#E3DACC"></div>
<span class="hex">#E3DACC</span>
<span class="token">--oat</span>
</div>
</div>
</div>
<div class="swatch-group">
<div class="swatch-group-label">Neutral</div>
<div class="swatch-grid">
<div class="swatch">
<div class="chip" style="background:#FFFFFF"></div>
<span class="hex">#FFFFFF</span>
<span class="token">--white</span>
</div>
<div class="swatch">
<div class="chip" style="background:#F0EEE6"></div>
<span class="hex">#F0EEE6</span>
<span class="token">--gray-100</span>
</div>
<div class="swatch">
<div class="chip no-border" style="background:#D1CFC5"></div>
<span class="hex">#D1CFC5</span>
<span class="token">--gray-300</span>
</div>
<div class="swatch">
<div class="chip no-border" style="background:#87867F"></div>
<span class="hex">#87867F</span>
<span class="token">--gray-500</span>
</div>
<div class="swatch">
<div class="chip no-border" style="background:#3D3D3A"></div>
<span class="hex">#3D3D3A</span>
<span class="token">--gray-700</span>
</div>
</div>
</div>
<div class="swatch-group">
<div class="swatch-group-label">Semantic</div>
<div class="swatch-grid">
<div class="swatch">
<div class="chip no-border" style="background:#788C5D"></div>
<span class="hex">#788C5D</span>
<span class="token">--success</span>
</div>
<div class="swatch">
<div class="chip no-border" style="background:#C78E3F"></div>
<span class="hex">#C78E3F</span>
<span class="token">--warning</span>
</div>
<div class="swatch">
<div class="chip no-border" style="background:#B04A4A"></div>
<span class="hex">#B04A4A</span>
<span class="token">--danger</span>
</div>
<div class="swatch">
<div class="chip no-border" style="background:#5C7CA3"></div>
<span class="hex">#5C7CA3</span>
<span class="token">--info</span>
</div>
</div>
</div>
</section>
<!-- ===================== TYPOGRAPHY ===================== -->
<section id="typography">
<h2>Typography</h2>
<hr class="rule">
<div class="type-scale">
<div class="type-row">
<div class="type-specimen t-display">Plan the week ahead</div>
<div class="type-meta">
<span class="name">Display</span>
48 / 1.1 / 500
</div>
</div>
<div class="type-row">
<div class="type-specimen t-h1">Plan the week ahead</div>
<div class="type-meta">
<span class="name">Heading 1</span>
32 / 1.2 / 500
</div>
</div>
<div class="type-row">
<div class="type-specimen t-h2">Plan the week ahead</div>
<div class="type-meta">
<span class="name">Heading 2</span>
24 / 1.3 / 500
</div>
</div>
<div class="type-row">
<div class="type-specimen t-body">Review milestones, assign owners, and surface blockers before they cascade.</div>
<div class="type-meta">
<span class="name">Body</span>
16 / 1.55 / 430
</div>
</div>
<div class="type-row">
<div class="type-specimen t-small">Review milestones, assign owners, and surface blockers before they cascade.</div>
<div class="type-meta">
<span class="name">Small</span>
14 / 1.5 / 430
</div>
</div>
<div class="type-row">
<div class="type-specimen t-caption">UPDATED 2 HOURS AGO</div>
<div class="type-meta">
<span class="name">Caption</span>
12 / 1.4 / 500
</div>
</div>
</div>
</section>
<!-- ===================== SPACING ===================== -->
<section id="spacing">
<h2>Spacing</h2>
<hr class="rule">
<div class="spacing-ruler">
<div class="space-token">
<div class="space-bar" style="width:4px"></div>
<div class="space-label">4<span>--sp-1</span></div>
</div>
<div class="space-token">
<div class="space-bar" style="width:8px"></div>
<div class="space-label">8<span>--sp-2</span></div>
</div>
<div class="space-token">
<div class="space-bar" style="width:12px"></div>
<div class="space-label">12<span>--sp-3</span></div>
</div>
<div class="space-token">
<div class="space-bar" style="width:16px"></div>
<div class="space-label">16<span>--sp-4</span></div>
</div>
<div class="space-token">
<div class="space-bar" style="width:24px"></div>
<div class="space-label">24<span>--sp-5</span></div>
</div>
<div class="space-token">
<div class="space-bar" style="width:32px"></div>
<div class="space-label">32<span>--sp-6</span></div>
</div>
<div class="space-token">
<div class="space-bar" style="width:48px"></div>
<div class="space-label">48<span>--sp-7</span></div>
</div>
<div class="space-token">
<div class="space-bar" style="width:64px"></div>
<div class="space-label">64<span>--sp-8</span></div>
</div>
</div>
</section>
<!-- ===================== RADIUS & ELEVATION ===================== -->
<section id="shape">
<h2>Radius &amp; Elevation</h2>
<hr class="rule">
<div class="token-row">
<div class="radius-card" style="border-radius:4px">
<div class="lbl">4px<span>--r-xs</span></div>
</div>
<div class="radius-card" style="border-radius:8px">
<div class="lbl">8px<span>--r-sm</span></div>
</div>
<div class="radius-card" style="border-radius:12px">
<div class="lbl">12px<span>--r-md</span></div>
</div>
<div class="radius-card" style="border-radius:20px">
<div class="lbl">20px<span>--r-lg</span></div>
</div>
</div>
<div class="token-row">
<div class="shadow-card" style="box-shadow:0 1px 2px rgba(20,20,19,0.06)">
<div class="lbl">--shadow-sm<span>0 1px 2px / 6%</span></div>
</div>
<div class="shadow-card" style="box-shadow:0 4px 10px rgba(20,20,19,0.08)">
<div class="lbl">--shadow-md<span>0 4px 10px / 8%</span></div>
</div>
<div class="shadow-card" style="box-shadow:0 12px 28px rgba(20,20,19,0.12)">
<div class="lbl">--shadow-lg<span>0 12px 28px / 12%</span></div>
</div>
</div>
</section>
<!-- ===================== CORE COMPONENTS ===================== -->
<section id="components">
<h2>Core components</h2>
<hr class="rule">
<div class="component">
<div class="component-name">&lt;Button /&gt;</div>
<div class="component-stage">
<button class="btn btn-primary">Create task</button>
<button class="btn btn-secondary">Cancel</button>
<button class="btn btn-ghost">Skip</button>
<button class="btn btn-danger">Delete</button>
</div>
</div>
<div class="component">
<div class="component-name">&lt;Input /&gt;</div>
<div class="component-stage">
<input class="input" type="text" placeholder="Search tasks&hellip;">
<input class="input" type="text" value="Weekly planning">
</div>
</div>
<div class="component">
<div class="component-name">&lt;Checkbox /&gt;</div>
<div class="component-stage">
<label class="checkbox">
<input type="checkbox">
Notify assignees
</label>
<label class="checkbox">
<input type="checkbox" checked>
Archive on complete
</label>
</div>
</div>
<div class="component">
<div class="component-name">&lt;Badge /&gt;</div>
<div class="component-stage">
<span class="badge badge-neutral">Draft</span>
<span class="badge badge-accent">In review</span>
<span class="badge badge-success">Done</span>
<span class="badge badge-warning">Overdue</span>
</div>
</div>
</section>
</div>
</body>
</html>
+606
View File
@@ -0,0 +1,606 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Acme — Card Variant Matrix</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-100: #F0EEE6;
--gray-300: #D1CFC5;
--gray-500: #87867F;
--gray-700: #3D3D3A;
--white: #FFFFFF;
--serif: ui-serif, Georgia, serif;
--sans: system-ui, -apple-system, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, monospace;
/* live-controlled tokens */
--card-pad: 20px;
--card-border: 1.5px solid var(--gray-300);
--card-shadow: 0 4px 14px rgba(20, 20, 19, 0.08);
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
font-size: 15px;
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
.page {
max-width: 1100px;
margin: 0 auto;
padding: 0 28px 80px;
}
header {
padding: 48px 0 20px;
}
header h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 36px;
letter-spacing: -0.01em;
margin: 0 0 6px;
}
header p {
color: var(--gray-500);
font-size: 14px;
margin: 0;
max-width: 640px;
}
/* ---------- Toolbar ---------- */
.toolbar {
position: sticky;
top: 0;
z-index: 10;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 28px;
padding: 16px 20px;
margin: 24px 0 36px;
background: var(--white);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
}
.control {
display: flex;
align-items: center;
gap: 10px;
}
.control-label {
font-family: var(--mono);
font-size: 12px;
color: var(--gray-700);
}
.control-value {
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
min-width: 36px;
}
input[type="range"] {
appearance: none;
width: 140px;
height: 4px;
background: var(--gray-300);
border-radius: 2px;
outline: none;
}
input[type="range"]::-webkit-slider-thumb {
appearance: none;
width: 16px; height: 16px;
background: var(--clay);
border-radius: 50%;
cursor: pointer;
}
input[type="range"]::-moz-range-thumb {
width: 16px; height: 16px;
background: var(--clay);
border: none;
border-radius: 50%;
cursor: pointer;
}
.radio-group {
display: inline-flex;
border: 1.5px solid var(--gray-300);
border-radius: 8px;
overflow: hidden;
}
.radio-group label {
padding: 6px 12px;
font-size: 13px;
cursor: pointer;
user-select: none;
color: var(--gray-700);
border-right: 1px solid var(--gray-300);
}
.radio-group label:last-child { border-right: none; }
.radio-group input { display: none; }
.radio-group input:checked + span {
color: var(--slate);
}
.radio-group label:has(input:checked) {
background: var(--gray-100);
}
.check {
display: inline-flex;
align-items: center;
gap: 8px;
font-size: 13px;
cursor: pointer;
user-select: none;
}
.check input {
appearance: none;
width: 16px; height: 16px;
border: 1.5px solid var(--gray-300);
border-radius: 4px;
margin: 0;
cursor: pointer;
position: relative;
}
.check input:checked {
background: var(--clay);
border-color: var(--clay);
}
.check input:checked::after {
content: "";
position: absolute;
left: 4px; top: 0;
width: 4px; height: 9px;
border: solid var(--white);
border-width: 0 2px 2px 0;
transform: rotate(45deg);
}
/* ---------- Grid ---------- */
.grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 28px;
}
@media (max-width: 960px) {
.grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
.grid { grid-template-columns: 1fr; }
}
.cell {}
.variant-label {
display: inline-flex;
align-items: center;
height: 22px;
padding: 0 9px;
font-family: var(--mono);
font-size: 11px;
font-weight: 500;
color: var(--gray-700);
background: var(--gray-100);
border-radius: 999px;
margin-bottom: 10px;
}
.variant-note {
font-size: 12px;
color: var(--gray-500);
margin-top: 10px;
}
/* ---------- Card base ---------- */
.card {
border-radius: 12px;
padding: var(--card-pad);
transition: padding 0.15s ease, box-shadow 0.15s ease, border 0.15s ease;
cursor: pointer;
}
.card:hover { outline: 2px solid var(--clay); outline-offset: 2px; }
.card-head {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.avatar {
width: 36px; height: 36px;
border-radius: 50%;
background: var(--oat);
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 600;
color: var(--gray-700);
}
.card-titles { min-width: 0; }
.card-title {
font-family: var(--serif);
font-size: 17px;
font-weight: 500;
margin: 0 0 2px;
line-height: 1.3;
}
.card-sub {
font-size: 13px;
color: var(--gray-500);
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chips {
display: flex;
gap: 6px;
margin-bottom: 14px;
}
.chip {
display: inline-flex;
align-items: center;
height: 20px;
padding: 0 8px;
font-size: 11px;
font-weight: 500;
border-radius: 999px;
background: var(--gray-100);
color: var(--gray-700);
}
.chip.olive { background: rgba(120,140,93,0.16); color: var(--olive); }
.ghost-btn {
display: inline-flex;
align-items: center;
height: 30px;
padding: 0 12px;
font-size: 13px;
font-weight: 500;
color: var(--gray-700);
background: transparent;
border: 1.5px solid var(--gray-300);
border-radius: 8px;
cursor: pointer;
}
.ghost-btn:hover { background: var(--gray-100); }
/* ---------- Variant treatments ---------- */
.v-flat {
background: var(--white);
border: none;
box-shadow: none;
}
.v-outlined {
background: var(--white);
border: var(--card-border);
box-shadow: none;
}
.v-elevated {
background: var(--white);
border: none;
box-shadow: var(--card-shadow);
}
.v-stripe {
background: var(--white);
border: var(--card-border);
box-shadow: none;
position: relative;
overflow: hidden;
}
.v-stripe::before {
content: "";
position: absolute;
left: 0; right: 0; top: 0;
height: 4px;
background: var(--clay);
}
.v-inset {
background: var(--oat);
border: none;
box-shadow: none;
}
.v-inset .chip { background: rgba(255,255,255,0.6); }
.v-inset .ghost-btn { border-color: rgba(20,20,19,0.15); }
.v-inset .avatar { background: var(--white); }
.v-horizontal {
background: var(--white);
border: var(--card-border);
box-shadow: none;
display: flex;
align-items: center;
gap: 14px;
}
.v-horizontal .card-head { margin: 0; flex: 1; min-width: 0; }
.v-horizontal .chips { margin: 0; }
.v-horizontal .card-sub { display: none; }
.v-horizontal .ghost-btn { flex-shrink: 0; }
/* ---------- Snippet panel ---------- */
.snippet-panel {
margin-top: 40px;
border: 1.5px solid var(--gray-300);
border-radius: 12px;
overflow: hidden;
}
.snippet-head {
padding: 10px 16px;
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
background: var(--gray-100);
border-bottom: 1px solid var(--gray-300);
}
pre#snippet {
margin: 0;
padding: 20px;
background: var(--slate);
color: var(--ivory);
font-family: var(--mono);
font-size: 13px;
line-height: 1.6;
overflow-x: auto;
min-height: 120px;
}
</style>
</head>
<body>
<div class="page">
<header>
<h1>Card variant matrix</h1>
<p>Six structural treatments of the Acme <code style="font-family:var(--mono);font-size:13px">&lt;Card /&gt;</code> component. Adjust density and emphasis with the controls, hover a variant to see its prop combo.</p>
</header>
<!-- ---------- Toolbar ---------- -->
<div class="toolbar">
<div class="control">
<span class="control-label">Padding</span>
<input id="ctl-pad" type="range" min="12" max="32" value="20" step="2">
<span class="control-value" id="pad-out">20px</span>
</div>
<div class="control">
<span class="control-label">Border</span>
<div class="radio-group">
<label><input type="radio" name="border" value="none"><span>none</span></label>
<label><input type="radio" name="border" value="hairline" checked><span>hairline</span></label>
<label><input type="radio" name="border" value="solid"><span>solid</span></label>
</div>
</div>
<div class="control">
<label class="check">
<input id="ctl-shadow" type="checkbox" checked>
Show shadow
</label>
</div>
</div>
<!-- ---------- Grid ---------- -->
<div class="grid" id="grid">
<!-- A: Flat -->
<div class="cell">
<span class="variant-label">A &middot; Flat</span>
<div class="card v-flat"
data-snippet='<Card variant="flat" padding={PAD}>
<Card.Header avatar title="Weekly planning" subtitle="12 tasks &middot; due Friday" />
<Card.Meta tags={["Q2", "Roadmap"]} />
<Button variant="ghost">Open</Button>
</Card>'>
<div class="card-head">
<div class="avatar">WP</div>
<div class="card-titles">
<p class="card-title">Weekly planning</p>
<p class="card-sub">12 tasks &middot; due Friday</p>
</div>
</div>
<div class="chips">
<span class="chip">Q2</span>
<span class="chip olive">Roadmap</span>
</div>
<button class="ghost-btn">Open</button>
</div>
<p class="variant-note">best for: dense lists on tinted backgrounds</p>
</div>
<!-- B: Outlined -->
<div class="cell">
<span class="variant-label">B &middot; Outlined</span>
<div class="card v-outlined"
data-snippet='<Card variant="outlined" padding={PAD} border="BORDER">
<Card.Header avatar title="Weekly planning" subtitle="12 tasks &middot; due Friday" />
<Card.Meta tags={["Q2", "Roadmap"]} />
<Button variant="ghost">Open</Button>
</Card>'>
<div class="card-head">
<div class="avatar">WP</div>
<div class="card-titles">
<p class="card-title">Weekly planning</p>
<p class="card-sub">12 tasks &middot; due Friday</p>
</div>
</div>
<div class="chips">
<span class="chip">Q2</span>
<span class="chip olive">Roadmap</span>
</div>
<button class="ghost-btn">Open</button>
</div>
<p class="variant-note">best for: default content cards on ivory</p>
</div>
<!-- C: Elevated -->
<div class="cell">
<span class="variant-label">C &middot; Elevated</span>
<div class="card v-elevated"
data-snippet='<Card variant="elevated" padding={PAD} shadow="md">
<Card.Header avatar title="Weekly planning" subtitle="12 tasks &middot; due Friday" />
<Card.Meta tags={["Q2", "Roadmap"]} />
<Button variant="ghost">Open</Button>
</Card>'>
<div class="card-head">
<div class="avatar">WP</div>
<div class="card-titles">
<p class="card-title">Weekly planning</p>
<p class="card-sub">12 tasks &middot; due Friday</p>
</div>
</div>
<div class="chips">
<span class="chip">Q2</span>
<span class="chip olive">Roadmap</span>
</div>
<button class="ghost-btn">Open</button>
</div>
<p class="variant-note">best for: draggable items, popovers</p>
</div>
<!-- D: Accent stripe -->
<div class="cell">
<span class="variant-label">D &middot; Accent stripe</span>
<div class="card v-stripe"
data-snippet='<Card variant="outlined" accent="clay" padding={PAD} border="BORDER">
<Card.Header avatar title="Weekly planning" subtitle="12 tasks &middot; due Friday" />
<Card.Meta tags={["Q2", "Roadmap"]} />
<Button variant="ghost">Open</Button>
</Card>'>
<div class="card-head">
<div class="avatar">WP</div>
<div class="card-titles">
<p class="card-title">Weekly planning</p>
<p class="card-sub">12 tasks &middot; due Friday</p>
</div>
</div>
<div class="chips">
<span class="chip">Q2</span>
<span class="chip olive">Roadmap</span>
</div>
<button class="ghost-btn">Open</button>
</div>
<p class="variant-note">best for: pinned or priority items</p>
</div>
<!-- E: Inset / filled -->
<div class="cell">
<span class="variant-label">E &middot; Inset</span>
<div class="card v-inset"
data-snippet='<Card variant="filled" tone="oat" padding={PAD}>
<Card.Header avatar title="Weekly planning" subtitle="12 tasks &middot; due Friday" />
<Card.Meta tags={["Q2", "Roadmap"]} />
<Button variant="ghost">Open</Button>
</Card>'>
<div class="card-head">
<div class="avatar">WP</div>
<div class="card-titles">
<p class="card-title">Weekly planning</p>
<p class="card-sub">12 tasks &middot; due Friday</p>
</div>
</div>
<div class="chips">
<span class="chip">Q2</span>
<span class="chip olive">Roadmap</span>
</div>
<button class="ghost-btn">Open</button>
</div>
<p class="variant-note">best for: nested cards inside white panels</p>
</div>
<!-- F: Horizontal / compact -->
<div class="cell">
<span class="variant-label">F &middot; Horizontal</span>
<div class="card v-horizontal"
data-snippet='<Card variant="outlined" layout="row" padding={PAD} border="BORDER">
<Card.Header avatar title="Weekly planning" />
<Card.Meta tags={["Q2", "Roadmap"]} />
<Button variant="ghost">Open</Button>
</Card>'>
<div class="card-head">
<div class="avatar">WP</div>
<div class="card-titles">
<p class="card-title">Weekly planning</p>
<p class="card-sub">12 tasks &middot; due Friday</p>
</div>
</div>
<div class="chips">
<span class="chip">Q2</span>
<span class="chip olive">Roadmap</span>
</div>
<button class="ghost-btn">Open</button>
</div>
<p class="variant-note">best for: compact row lists, sidebars</p>
</div>
</div>
<!-- ---------- Snippet ---------- -->
<div class="snippet-panel">
<div class="snippet-head">JSX &mdash; hover a variant above</div>
<pre id="snippet">// hover a card to preview its props</pre>
</div>
</div>
<script>
(function () {
var root = document.documentElement;
var pad = document.getElementById('ctl-pad');
var padOut = document.getElementById('pad-out');
var shadow = document.getElementById('ctl-shadow');
var snippet = document.getElementById('snippet');
var currentBorder = 'hairline';
var borderMap = {
none: 'none',
hairline: '1px solid var(--gray-300)',
solid: '1.5px solid var(--gray-300)'
};
function applyPad() {
root.style.setProperty('--card-pad', pad.value + 'px');
padOut.textContent = pad.value + 'px';
}
function applyBorder(v) {
currentBorder = v;
root.style.setProperty('--card-border', borderMap[v]);
}
function applyShadow() {
root.style.setProperty('--card-shadow',
shadow.checked ? '0 4px 14px rgba(20,20,19,0.08)' : 'none');
}
pad.addEventListener('input', applyPad);
shadow.addEventListener('change', applyShadow);
document.querySelectorAll('input[name="border"]').forEach(function (r) {
r.addEventListener('change', function () { applyBorder(r.value); });
});
document.querySelectorAll('.card').forEach(function (card) {
card.addEventListener('mouseenter', function () {
var raw = card.getAttribute('data-snippet') || '';
snippet.textContent = raw
.replace(/PAD/g, pad.value)
.replace(/BORDER/g, currentBorder);
});
});
applyPad(); applyBorder('hairline'); applyShadow();
})();
</script>
</body>
</html>
+456
View File
@@ -0,0 +1,456 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Acme — Task completed micro-interaction</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-50: #F0EEE6;
--gray-200: #D1CFC5;
--gray-500: #87867F;
--gray-800: #3D3D3A;
--white: #ffffff;
--serif: ui-serif, Georgia, "Times New Roman", serif;
--sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
/* swapped at runtime by the easing panel */
--ease: cubic-bezier(0.34, 1.56, 0.64, 1);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
font-size: 14px;
line-height: 1.5;
}
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: 48px 32px 64px;
}
header { margin-bottom: 28px; }
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 30px;
letter-spacing: -0.01em;
margin: 6px 0 4px;
}
.sub {
color: var(--gray-500);
max-width: 560px;
}
/* ---------- layout ---------- */
.bench {
display: grid;
grid-template-columns: 1fr 240px;
gap: 24px;
align-items: start;
}
.stage {
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 12px;
height: 360px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.hint {
position: absolute;
bottom: 14px;
left: 50%;
transform: translateX(-50%);
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
}
/* ---------- the task row ---------- */
.task {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 18px;
border: 1.5px solid var(--gray-200);
border-radius: 8px;
background: var(--white);
width: 360px;
max-height: 64px;
position: relative;
transition: max-height 280ms var(--ease) 600ms,
opacity 280ms var(--ease) 600ms,
padding 280ms var(--ease) 600ms,
border-color 200ms var(--ease);
cursor: pointer;
user-select: none;
}
.task:hover { border-color: var(--gray-500); }
.check {
flex: none;
width: 24px;
height: 24px;
border-radius: 50%;
border: 1.5px solid var(--gray-500);
background: var(--white);
position: relative;
transition: background 180ms var(--ease),
border-color 180ms var(--ease),
transform 260ms var(--ease);
}
.check svg {
position: absolute;
inset: 0;
margin: auto;
width: 14px;
height: 14px;
}
.check path {
stroke: var(--white);
stroke-width: 2.5;
stroke-linecap: round;
stroke-linejoin: round;
fill: none;
stroke-dasharray: 20;
stroke-dashoffset: 20;
transition: stroke-dashoffset 220ms var(--ease) 80ms;
}
.label {
position: relative;
font-size: 15px;
color: var(--slate);
transition: color 200ms var(--ease) 120ms;
}
.label::after {
content: "";
position: absolute;
left: 0;
top: 52%;
height: 1.5px;
width: 0;
background: var(--gray-500);
transition: width 240ms var(--ease) 120ms;
}
.due {
margin-left: auto;
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
background: var(--gray-50);
border: 1.5px solid var(--gray-200);
padding: 3px 8px;
border-radius: 8px;
transition: opacity 200ms var(--ease) 120ms;
}
/* confetti — six particles, css-only, driven by .done on the row */
.confetti {
position: absolute;
left: 22px;
top: 50%;
width: 6px;
height: 6px;
border-radius: 2px;
opacity: 0;
pointer-events: none;
}
.confetti.c1 { background: var(--clay); }
.confetti.c2 { background: var(--olive); }
.confetti.c3 { background: var(--oat); }
.confetti.c4 { background: var(--clay); }
.confetti.c5 { background: var(--olive); }
.confetti.c6 { background: var(--gray-800); }
/* ---------- done state ---------- */
.task.done .check {
background: var(--olive);
border-color: var(--olive);
transform: scale(1.12);
animation: settle 380ms var(--ease) forwards;
}
@keyframes settle {
0% { transform: scale(0.8); background: var(--clay); border-color: var(--clay); }
55% { transform: scale(1.18); }
100% { transform: scale(1); background: var(--olive); border-color: var(--olive); }
}
.task.done .check path { stroke-dashoffset: 0; }
.task.done .label { color: var(--gray-500); }
.task.done .label::after { width: 100%; }
.task.done .due { opacity: 0.35; }
.task.done {
max-height: 44px;
padding-top: 8px;
padding-bottom: 8px;
opacity: 0.6;
}
.task.done .confetti { animation: pop 520ms var(--ease) 200ms forwards; }
.task.done .confetti.c1 { --dx: -28px; --dy: -22px; --rot: 40deg; }
.task.done .confetti.c2 { --dx: -6px; --dy: -34px; --rot: -60deg; }
.task.done .confetti.c3 { --dx: 18px; --dy: -26px; --rot: 90deg; }
.task.done .confetti.c4 { --dx: 30px; --dy: -4px; --rot: -30deg; }
.task.done .confetti.c5 { --dx: -24px; --dy: 14px; --rot: 20deg; }
.task.done .confetti.c6 { --dx: 12px; --dy: 22px; --rot: -80deg; }
@keyframes pop {
0% { opacity: 0; transform: translate(0, 0) rotate(0) scale(0.6); }
15% { opacity: 1; }
100% { opacity: 0; transform: translate(var(--dx), var(--dy)) rotate(var(--rot)) scale(1); }
}
/* ---------- easing panel ---------- */
.panel {
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 12px;
padding: 18px;
}
.panel h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 16px;
margin: 0 0 4px;
}
.panel p {
margin: 0 0 14px;
font-size: 12px;
color: var(--gray-500);
}
.ease-btn {
display: block;
width: 100%;
text-align: left;
background: var(--gray-50);
border: 1.5px solid var(--gray-200);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 8px;
font-family: var(--sans);
font-size: 13px;
color: var(--slate);
cursor: pointer;
}
.ease-btn:hover { border-color: var(--gray-500); }
.ease-btn.active {
border-color: var(--clay);
background: #FBF1EC;
}
.ease-btn code {
display: block;
font-family: var(--mono);
font-size: 10px;
color: var(--gray-500);
margin-top: 2px;
}
/* ---------- timeline ---------- */
.timeline {
margin-top: 28px;
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 12px;
padding: 22px 22px 30px;
}
.timeline h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 16px;
margin: 0 0 18px;
}
.track {
position: relative;
height: 2px;
background: var(--gray-200);
margin: 20px 8px 0;
}
.key {
position: absolute;
top: -5px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--clay);
border: 1.5px solid var(--white);
box-shadow: 0 0 0 1.5px var(--clay);
transform: translateX(-50%);
}
.key span {
position: absolute;
top: 16px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
font-family: var(--mono);
font-size: 10px;
color: var(--gray-800);
}
.key em {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
white-space: nowrap;
font-style: normal;
font-size: 11px;
color: var(--gray-500);
}
.k1 { left: 0%; }
.k2 { left: 13%; }
.k3 { left: 20%; }
.k4 { left: 33%; }
.k5 { left: 100%; background: var(--olive); box-shadow: 0 0 0 1.5px var(--olive); }
/* ---------- snippet ---------- */
.snippet {
margin-top: 28px;
}
.snippet h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 16px;
margin: 0 0 10px;
}
pre {
background: var(--gray-800);
color: var(--gray-50);
border-radius: 12px;
padding: 18px 20px;
font-family: var(--mono);
font-size: 12px;
line-height: 1.7;
overflow-x: auto;
margin: 0;
}
pre .c { color: var(--gray-500); }
pre .k { color: var(--clay); }
pre .v { color: #B8C99D; }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">Acme / prototype / micro-interaction</div>
<h1>Task completed</h1>
<p class="sub">
A single click should feel like a tiny win. Circle fills, check draws,
label strikes, a small burst, then the row quietly steps back. Click
the row to play; click again to reset.
</p>
</header>
<div class="bench">
<div class="stage">
<div class="task" id="task">
<div class="check">
<svg viewBox="0 0 16 16"><path d="M3 8.5 L6.5 12 L13 4.5"/></svg>
</div>
<span class="label">Send weekly digest</span>
<span class="due">Fri</span>
<div class="confetti c1"></div>
<div class="confetti c2"></div>
<div class="confetti c3"></div>
<div class="confetti c4"></div>
<div class="confetti c5"></div>
<div class="confetti c6"></div>
</div>
<div class="hint">click to toggle</div>
</div>
<aside class="panel">
<h2>Easing</h2>
<p>Swaps <code>--ease</code> across every transition so you can feel the difference.</p>
<button class="ease-btn" data-ease="linear">
Linear
<code>linear</code>
</button>
<button class="ease-btn" data-ease="cubic-bezier(0.16, 1, 0.3, 1)">
Ease-out
<code>cubic-bezier(.16, 1, .3, 1)</code>
</button>
<button class="ease-btn active" data-ease="cubic-bezier(0.34, 1.56, 0.64, 1)">
Spring
<code>cubic-bezier(.34, 1.56, .64, 1)</code>
</button>
</aside>
</div>
<section class="timeline">
<h2>Keyframes</h2>
<div class="track">
<div class="key k1"><em>fill</em><span>0ms</span></div>
<div class="key k2"><em>check</em><span>80ms</span></div>
<div class="key k3"><em>strike</em><span>120ms</span></div>
<div class="key k4"><em>confetti</em><span>200ms</span></div>
<div class="key k5"><em>collapse</em><span>600ms</span></div>
</div>
</section>
<section class="snippet">
<h2>Copy-paste CSS</h2>
<pre><span class="c">/* circle: clay flash, settle to olive with spring overshoot */</span>
<span class="k">.task.done .check</span> {
animation: settle 380ms <span class="v">cubic-bezier(.34,1.56,.64,1)</span> forwards;
}
<span class="k">@keyframes settle</span> {
0% { transform: scale(.8); background: <span class="v">#D97757</span>; }
55% { transform: scale(1.18); }
100% { transform: scale(1); background: <span class="v">#788C5D</span>; }
}
<span class="c">/* checkmark draws via stroke-dashoffset, 80ms delay */</span>
<span class="k">.check path</span> { stroke-dasharray: 20; stroke-dashoffset: 20;
transition: stroke-dashoffset 220ms var(--ease) 80ms; }
<span class="k">.task.done path</span> { stroke-dashoffset: 0; }
<span class="c">/* strikethrough grows left → right */</span>
<span class="k">.label::after</span> { width: 0; transition: width 240ms var(--ease) 120ms; }
<span class="k">.task.done .label::after</span> { width: 100%; }
<span class="c">/* row steps back after the celebration */</span>
<span class="k">.task.done</span> { max-height: 44px; opacity: .6;
transition-delay: 600ms; }</pre>
</section>
</div>
<script>
var task = document.getElementById('task');
task.addEventListener('click', function () {
task.classList.toggle('done');
});
var root = document.documentElement;
var buttons = document.querySelectorAll('.ease-btn');
buttons.forEach(function (btn) {
btn.addEventListener('click', function () {
buttons.forEach(function (b) { b.classList.remove('active'); });
btn.classList.add('active');
root.style.setProperty('--ease', btn.dataset.ease);
});
});
</script>
</body>
</html>
+397
View File
@@ -0,0 +1,397 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Acme — Sidebar drag-to-reorder</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-50: #F0EEE6;
--gray-200: #D1CFC5;
--gray-500: #87867F;
--gray-800: #3D3D3A;
--white: #ffffff;
--serif: ui-serif, Georgia, "Times New Roman", serif;
--sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
font-size: 14px;
line-height: 1.55;
}
.wrap {
max-width: 1040px;
margin: 0 auto;
padding: 48px 32px 64px;
}
header { margin-bottom: 28px; }
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 30px;
letter-spacing: -0.01em;
margin: 6px 0 4px;
}
.sub {
color: var(--gray-500);
max-width: 600px;
}
code.inline {
font-family: var(--mono);
font-size: 12px;
background: var(--gray-50);
border: 1.5px solid var(--gray-200);
padding: 1px 5px;
border-radius: 4px;
}
/* ---------- layout ---------- */
.bench {
display: grid;
grid-template-columns: 300px 1fr;
gap: 28px;
align-items: start;
}
/* ---------- sidebar mock ---------- */
.sidebar {
width: 260px;
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 12px;
padding: 14px 10px;
}
.sidebar-title {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gray-500);
padding: 4px 10px 10px;
}
.list {
position: relative;
list-style: none;
margin: 0;
padding: 0;
}
.item {
display: flex;
align-items: center;
gap: 10px;
padding: 9px 10px;
margin: 2px 0;
border-radius: 8px;
background: var(--white);
cursor: grab;
user-select: none;
transition: background 120ms linear, transform 120ms ease-out, opacity 120ms linear;
}
.item:hover { background: var(--gray-50); }
.item:hover .grip i { background: var(--gray-800); }
.item.dragging {
opacity: 0.35;
transform: rotate(2deg);
background: var(--gray-50);
cursor: grabbing;
}
.item .label { font-size: 14px; color: var(--slate); }
.item .count {
margin-left: auto;
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
}
/* grip: 2 × 3 dot grid, pure css */
.grip {
flex: none;
width: 10px;
height: 16px;
display: grid;
grid-template-columns: repeat(2, 3px);
grid-template-rows: repeat(3, 3px);
gap: 3px;
align-content: center;
}
.grip i {
width: 3px;
height: 3px;
border-radius: 50%;
background: var(--gray-200);
transition: background 120ms linear;
}
/* drop indicator line */
.indicator {
position: absolute;
left: 6px;
right: 6px;
height: 2px;
background: var(--clay);
border-radius: 2px;
pointer-events: none;
display: none;
}
.indicator::before {
content: "";
position: absolute;
left: -5px;
top: -3px;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--clay);
}
.indicator.on { display: block; }
/* ---------- annotation panel ---------- */
.notes {
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 12px;
padding: 22px 24px;
}
.notes h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 18px;
margin: 0 0 6px;
}
.notes .lede {
color: var(--gray-500);
font-size: 13px;
margin: 0 0 16px;
}
.notes ul {
list-style: none;
margin: 0;
padding: 0;
}
.notes li {
position: relative;
padding: 10px 0 10px 22px;
border-top: 1.5px solid var(--gray-50);
font-size: 13px;
color: var(--gray-800);
}
.notes li:first-child { border-top: none; }
.notes li::before {
content: "";
position: absolute;
left: 4px;
top: 16px;
width: 7px;
height: 7px;
border-radius: 2px;
background: var(--clay);
}
.notes li b { color: var(--slate); font-weight: 600; }
/* ---------- open questions ---------- */
.questions {
margin-top: 28px;
background: var(--oat);
border: 1.5px solid var(--gray-200);
border-radius: 12px;
padding: 20px 24px;
}
.questions h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 17px;
margin: 0 0 12px;
}
.questions ol {
margin: 0;
padding-left: 20px;
}
.questions li {
font-size: 13px;
color: var(--gray-800);
padding: 4px 0;
}
.questions li::marker {
font-family: var(--mono);
color: var(--olive);
font-weight: 600;
}
.footnote {
margin-top: 18px;
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">Acme / prototype / interaction</div>
<h1>Sidebar drag-to-reorder</h1>
<p class="sub">
Throwaway HTML so we can <em>feel</em> the reorder before porting it to
React. Native <code class="inline">dragstart / dragover / drop</code>,
~40 lines of JS, no libraries. Grab a row by the dots and move it.
</p>
</header>
<div class="bench">
<div>
<nav class="sidebar">
<div class="sidebar-title">Views</div>
<ul class="list" id="list">
<li class="item" draggable="true">
<span class="grip"><i></i><i></i><i></i><i></i><i></i><i></i></span>
<span class="label">Inbox</span>
<span class="count">14</span>
</li>
<li class="item" draggable="true">
<span class="grip"><i></i><i></i><i></i><i></i><i></i><i></i></span>
<span class="label">Today</span>
<span class="count">3</span>
</li>
<li class="item" draggable="true">
<span class="grip"><i></i><i></i><i></i><i></i><i></i><i></i></span>
<span class="label">Upcoming</span>
<span class="count">21</span>
</li>
<li class="item" draggable="true">
<span class="grip"><i></i><i></i><i></i><i></i><i></i><i></i></span>
<span class="label">Projects</span>
<span class="count">8</span>
</li>
<li class="item" draggable="true">
<span class="grip"><i></i><i></i><i></i><i></i><i></i><i></i></span>
<span class="label">Archive</span>
<span class="count"></span>
</li>
<li class="item" draggable="true">
<span class="grip"><i></i><i></i><i></i><i></i><i></i><i></i></span>
<span class="label">Trash</span>
<span class="count"></span>
</li>
<div class="indicator" id="indicator"></div>
</ul>
</nav>
<p class="footnote">Order persists in the DOM only — refresh to reset.</p>
</div>
<div>
<section class="notes">
<h2>What you're feeling</h2>
<p class="lede">Design decisions baked into this prototype, so you can push back on them.</p>
<ul>
<li>
<b>Drop indicator snaps to the nearest gap</b>, not the raw cursor Y.
It only moves when you cross a row's midpoint — feels more decisive,
less jittery.
</li>
<li>
<b>Dragged row stays in place at 35% opacity</b> with a 2&deg; tilt.
Keeping the ghost in the list preserves your sense of where you
started; the tilt reads as "lifted."
</li>
<li>
<b>Grip dots are the affordance, but the whole row is draggable.</b>
Dots darken on hover to teach the gesture without forcing a
tiny hit target.
</li>
<li>
<b>No auto-scroll, no drop animation.</b> Left out on purpose so the
core feel is easy to judge — say the word and we add them next.
</li>
</ul>
</section>
<section class="questions">
<h2>Open questions</h2>
<ol>
<li>Should <b>Trash</b> (and maybe <b>Archive</b>) be pinned to the bottom and excluded from reordering?</li>
<li>Do we want rows to <b>slide</b> to their new slot on drop, or is the instant snap acceptable?</li>
<li>Keyboard path: is <code class="inline">Alt + Arrow</code> to move the focused row enough for the first ship?</li>
</ol>
</section>
</div>
</div>
</div>
<script>
var list = document.getElementById('list');
var indicator = document.getElementById('indicator');
var dragging = null;
var dropBefore = null; // node to insert before, or null = append
list.addEventListener('dragstart', function (e) {
var item = e.target.closest('.item');
if (!item) return;
dragging = item;
item.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', '');
});
list.addEventListener('dragover', function (e) {
if (!dragging) return;
e.preventDefault();
var siblings = Array.prototype.filter.call(
list.children,
function (el) { return el.classList && el.classList.contains('item'); }
);
dropBefore = null;
for (var i = 0; i < siblings.length; i++) {
var r = siblings[i].getBoundingClientRect();
if (e.clientY < r.top + r.height / 2) { dropBefore = siblings[i]; break; }
}
var listRect = list.getBoundingClientRect();
var edge = dropBefore
? dropBefore.getBoundingClientRect().top
: siblings[siblings.length - 1].getBoundingClientRect().bottom;
indicator.style.top = (edge - listRect.top - 1) + 'px';
indicator.classList.add('on');
});
list.addEventListener('drop', function (e) {
e.preventDefault();
if (!dragging) return;
if (dropBefore) list.insertBefore(dragging, dropBefore);
else list.insertBefore(dragging, indicator);
cleanup();
});
list.addEventListener('dragleave', function (e) {
if (!list.contains(e.relatedTarget)) indicator.classList.remove('on');
});
document.addEventListener('dragend', cleanup);
function cleanup() {
if (dragging) dragging.classList.remove('dragging');
dragging = null;
dropBefore = null;
indicator.classList.remove('on');
}
</script>
</body>
</html>
+593
View File
@@ -0,0 +1,593 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Platform Eng — Week of Mar 10</title>
<style>
/* ------------------------------------------------------------------
Design tokens
------------------------------------------------------------------ */
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-150:#F0EEE6;
--gray-300:#D1CFC5;
--gray-500:#87867F;
--gray-700:#3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
font-family: var(--sans);
background: var(--ivory);
color: var(--slate);
-webkit-font-smoothing: antialiased;
scroll-snap-type: y mandatory;
overflow-x: hidden;
}
/* ------------------------------------------------------------------
Slide shell
------------------------------------------------------------------ */
.slide {
width: 100vw;
height: 100vh;
scroll-snap-align: start;
scroll-snap-stop: always;
display: flex;
align-items: center;
justify-content: center;
padding: 8vh 6vw;
}
.slide-inner {
width: 100%;
max-width: 780px;
}
.slide.invert {
background: var(--slate);
color: var(--ivory);
}
/* ------------------------------------------------------------------
Typography
------------------------------------------------------------------ */
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 28px;
}
.invert .eyebrow { color: var(--gray-300); }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(40px, 6vw, 64px);
line-height: 1.08;
letter-spacing: -0.01em;
margin-bottom: 20px;
}
h2 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(30px, 4vw, 42px);
line-height: 1.15;
letter-spacing: -0.005em;
margin-bottom: 44px;
}
.subtitle {
font-size: 17px;
line-height: 1.6;
color: var(--gray-700);
max-width: 520px;
}
.mono { font-family: var(--mono); }
/* ------------------------------------------------------------------
Slide 1 — title
------------------------------------------------------------------ */
.title-slide .slide-inner { text-align: left; }
.ornament {
display: block;
margin: 0 0 40px 0;
}
.byline {
margin-top: 56px;
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
display: flex;
gap: 24px;
}
/* ------------------------------------------------------------------
Slide 2 — shipped list
------------------------------------------------------------------ */
.ship-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 28px;
}
.ship-item {
display: grid;
grid-template-columns: 14px 1fr auto;
column-gap: 20px;
align-items: baseline;
padding-bottom: 24px;
border-bottom: 1px solid var(--gray-150);
}
.ship-item:last-child { border-bottom: none; }
.ship-dot {
width: 9px;
height: 9px;
border-radius: 50%;
background: var(--olive);
margin-top: 8px;
align-self: start;
}
.ship-title {
font-family: var(--serif);
font-size: 20px;
font-weight: 500;
margin-bottom: 6px;
}
.ship-desc {
font-size: 14px;
line-height: 1.55;
color: var(--gray-700);
}
.ship-ref {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
white-space: nowrap;
padding-top: 4px;
}
/* ------------------------------------------------------------------
Slide 3 — in progress
------------------------------------------------------------------ */
.prog-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 40px;
}
.prog-item .prog-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 10px;
}
.prog-title {
font-family: var(--serif);
font-size: 20px;
font-weight: 500;
}
.prog-pct {
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
}
.prog-track {
width: 100%;
height: 5px;
background: var(--gray-150);
border-radius: 3px;
overflow: hidden;
margin-bottom: 10px;
}
.prog-fill {
height: 100%;
background: var(--clay);
border-radius: 3px;
}
.prog-note {
font-size: 13px;
line-height: 1.55;
color: var(--gray-700);
}
/* ------------------------------------------------------------------
Slide 4 — metrics
------------------------------------------------------------------ */
.metrics {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 56px;
margin-bottom: 48px;
}
.metric-label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 14px;
}
.metric-value {
font-family: var(--serif);
font-size: 52px;
font-weight: 500;
line-height: 1;
letter-spacing: -0.01em;
}
.metric-delta {
font-family: var(--mono);
font-size: 13px;
margin-top: 12px;
}
.metric-delta.down { color: var(--olive); }
.metric-delta.up { color: var(--clay); }
.sparkline-wrap {
border-top: 1px solid var(--gray-300);
padding-top: 28px;
}
.sparkline-caption {
font-size: 12px;
color: var(--gray-500);
margin-top: 10px;
}
/* ------------------------------------------------------------------
Slide 5 — decision (inverted)
------------------------------------------------------------------ */
.decision-card {
border: 1.5px solid var(--clay);
border-radius: 14px;
padding: 36px 38px;
background: rgba(217, 119, 87, 0.06);
}
.decision-q {
font-family: var(--serif);
font-size: 24px;
line-height: 1.4;
margin-bottom: 12px;
}
.decision-context {
font-size: 14px;
line-height: 1.6;
color: var(--gray-300);
}
.options {
display: flex;
gap: 14px;
margin-top: 32px;
flex-wrap: wrap;
}
.chip {
font-family: var(--mono);
font-size: 12px;
padding: 10px 18px;
border-radius: 999px;
border: 1px solid var(--gray-700);
color: var(--ivory);
background: transparent;
}
.chip.lean {
border-color: var(--clay);
color: var(--clay);
}
/* ------------------------------------------------------------------
Slide 6 — next week
------------------------------------------------------------------ */
.next-list {
list-style: none;
display: flex;
flex-direction: column;
gap: 22px;
}
.next-list li {
font-family: var(--serif);
font-size: 21px;
line-height: 1.45;
padding-left: 36px;
position: relative;
}
.next-list li::before {
content: "";
position: absolute;
left: 0;
top: 0.55em;
width: 18px;
height: 1.5px;
background: var(--clay);
}
.footnote {
margin-top: 56px;
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
}
/* ------------------------------------------------------------------
Slide counter
------------------------------------------------------------------ */
#counter {
position: fixed;
bottom: 22px;
right: 28px;
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
user-select: none;
z-index: 10;
}
@media (max-width: 640px) {
.metrics { grid-template-columns: 1fr; gap: 36px; }
.ship-item { grid-template-columns: 14px 1fr; }
.ship-ref { grid-column: 2; padding-top: 8px; }
}
</style>
</head>
<body>
<!-- ============================================================
Slide 1 — Title
============================================================ -->
<section class="slide title-slide" id="s1">
<div class="slide-inner">
<svg class="ornament" width="56" height="56" viewBox="0 0 56 56" aria-hidden="true">
<circle cx="28" cy="28" r="22" fill="none" stroke="#D1CFC5" stroke-width="1.5"/>
<path d="M16 28 H40 M28 16 V40" stroke="#D97757" stroke-width="1.5" stroke-linecap="round"/>
<circle cx="28" cy="28" r="3" fill="#141413"/>
</svg>
<h1>Platform Eng<br>— Week of Mar 10</h1>
<p class="subtitle">
What shipped, what's moving, and one decision we need from the
room before the Acme 2.4 cut.
</p>
<div class="byline">
<span>Friday demo</span>
<span>6 slides</span>
<span>~4 min</span>
</div>
</div>
</section>
<!-- ============================================================
Slide 2 — Shipped
============================================================ -->
<section class="slide" id="s2">
<div class="slide-inner">
<div class="eyebrow">Shipped this week</div>
<h2>Three things out the door</h2>
<ul class="ship-list">
<li class="ship-item">
<span class="ship-dot"></span>
<div>
<div class="ship-title">Bulk task import</div>
<div class="ship-desc">
CSV and JSON uploads now land straight into a board with
column mapping — no more paste-and-pray.
</div>
</div>
<span class="ship-ref">#4211</span>
</li>
<li class="ship-item">
<span class="ship-dot"></span>
<div>
<div class="ship-title">Webhook retries v2</div>
<div class="ship-desc">
Exponential backoff with jitter; dead-letter queue surfaces
in the workspace admin panel.
</div>
</div>
<span class="ship-ref">#4188 #4203</span>
</li>
<li class="ship-item">
<span class="ship-dot"></span>
<div>
<div class="ship-title">Postgres 16 migration</div>
<div class="ship-desc">
All read replicas cut over Tuesday night; zero customer-facing
downtime, ~9% faster aggregate queries.
</div>
</div>
<span class="ship-ref">#4179</span>
</li>
</ul>
</div>
</section>
<!-- ============================================================
Slide 3 — In progress
============================================================ -->
<section class="slide" id="s3">
<div class="slide-inner">
<div class="eyebrow">In progress</div>
<h2>Carrying into next week</h2>
<ul class="prog-list">
<li class="prog-item">
<div class="prog-head">
<span class="prog-title">Recurring tasks engine</span>
<span class="prog-pct">~70%</span>
</div>
<div class="prog-track"><div class="prog-fill" style="width:70%"></div></div>
<p class="prog-note">
Scheduler and RRULE parsing are done; remaining work is the
timezone edge cases and the "skip holidays" toggle.
</p>
</li>
<li class="prog-item">
<div class="prog-head">
<span class="prog-title">Audit log export</span>
<span class="prog-pct">~35%</span>
</div>
<div class="prog-track"><div class="prog-fill" style="width:35%"></div></div>
<p class="prog-note">
Streaming NDJSON endpoint is up behind a flag; still wiring
the S3 destination picker and retention policy UI.
</p>
</li>
</ul>
</div>
</section>
<!-- ============================================================
Slide 4 — Metrics
============================================================ -->
<section class="slide" id="s4">
<div class="slide-inner">
<div class="eyebrow">Metrics</div>
<h2>Numbers we watch</h2>
<div class="metrics">
<div class="metric">
<div class="metric-label">API p95 latency</div>
<div class="metric-value">184<span style="font-size:28px">ms</span></div>
<div class="metric-delta down">↓ 12% wk/wk</div>
</div>
<div class="metric">
<div class="metric-label">Background job error rate</div>
<div class="metric-value">0.21<span style="font-size:28px">%</span></div>
<div class="metric-delta down">↓ 0.08pp</div>
</div>
</div>
<div class="sparkline-wrap">
<svg width="100%" height="64" viewBox="0 0 780 64" preserveAspectRatio="none" aria-hidden="true">
<polyline
points="0,22 110,30 220,18 330,34 440,26 550,40 660,24 780,12"
fill="none"
stroke="#788C5D"
stroke-width="2"
stroke-linejoin="round"
stroke-linecap="round"/>
<circle cx="780" cy="12" r="3.5" fill="#788C5D"/>
<line x1="0" y1="63" x2="780" y2="63" stroke="#D1CFC5" stroke-width="1"/>
</svg>
<div class="sparkline-caption">
p95 latency, trailing 8 days — lower is better
</div>
</div>
</div>
</section>
<!-- ============================================================
Slide 5 — Decision needed (inverted)
============================================================ -->
<section class="slide invert" id="s5">
<div class="slide-inner">
<div class="eyebrow">Decision needed</div>
<h2>One call to make</h2>
<div class="decision-card">
<p class="decision-q">
Do we ship recurring tasks behind a workspace flag in 2.4, or
hold one more week for the timezone fixes?
</p>
<p class="decision-context">
Flagged rollout gets it to design partners Friday but means two
code paths for ~2 weeks. Holding keeps a single path but slips
the partner promise.
</p>
</div>
<div class="options">
<span class="chip lean">A — Flag it, ship Friday</span>
<span class="chip">B — Hold for 2.5</span>
</div>
</div>
</section>
<!-- ============================================================
Slide 6 — Next week
============================================================ -->
<section class="slide" id="s6">
<div class="slide-inner">
<div class="eyebrow">Next week</div>
<h2>On deck</h2>
<ul class="next-list">
<li>Finish recurring tasks — timezone matrix tests, then dogfood on the internal ops board.</li>
<li>Audit log export to private beta — three workspaces lined up.</li>
<li>Start scoping rate-limit headers for the public API; RFC draft by Thursday.</li>
</ul>
<div class="footnote">
Questions → drop them in the platform channel or grab anyone after standup.
</div>
</div>
</section>
<!-- ============================================================
Slide counter
============================================================ -->
<div id="counter">1 / 6</div>
<script>
(function () {
const slides = Array.from(document.querySelectorAll('.slide'));
const counter = document.getElementById('counter');
let current = 0;
function go(i) {
current = Math.max(0, Math.min(slides.length - 1, i));
slides[current].scrollIntoView({ behavior: 'smooth' });
}
document.addEventListener('keydown', function (e) {
if (e.key === 'ArrowRight' || e.key === 'ArrowDown' || e.key === ' ') { e.preventDefault(); go(current + 1); }
if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') { e.preventDefault(); go(current - 1); }
});
const obs = new IntersectionObserver(function (entries) {
entries.forEach(function (en) {
if (en.isIntersecting) {
current = slides.indexOf(en.target);
counter.textContent = (current + 1) + ' / ' + slides.length;
}
});
}, { threshold: 0.6 });
slides.forEach(function (s) { obs.observe(s); });
})();
</script>
</body>
</html>
+493
View File
@@ -0,0 +1,493 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Background jobs — header illustrations</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-150:#F0EEE6;
--gray-300:#D1CFC5;
--gray-500:#87867F;
--gray-700:#3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
padding: 64px 24px 96px;
}
.sheet {
max-width: 760px;
margin: 0 auto;
}
header { margin-bottom: 56px; }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 34px;
letter-spacing: -0.01em;
margin-bottom: 10px;
}
.lead {
font-size: 14px;
line-height: 1.6;
color: var(--gray-700);
max-width: 560px;
}
.meta {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
margin-top: 18px;
}
/* ---------------------------------------------------------------- */
figure {
margin: 0 0 72px 0;
}
.canvas {
border: 1px solid var(--gray-300);
border-radius: 12px;
overflow: hidden;
background: var(--ivory);
}
.canvas svg { display: block; width: 100%; height: auto; }
figcaption {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 24px;
padding: 18px 4px 0;
}
.cap-text { flex: 1; }
.cap-title {
font-family: var(--serif);
font-size: 19px;
font-weight: 500;
margin-bottom: 4px;
}
.cap-sub {
font-size: 13px;
line-height: 1.55;
color: var(--gray-500);
}
.dl {
flex-shrink: 0;
font-family: var(--mono);
font-size: 11px;
color: var(--slate);
background: var(--gray-150);
border: 1px solid var(--gray-300);
border-radius: 6px;
padding: 7px 12px;
cursor: pointer;
}
.dl:hover { background: var(--oat); }
.dl:active { transform: translateY(1px); }
/* ----------------------------------------------------------------
Palette reference strip
---------------------------------------------------------------- */
.palette {
border-top: 1px solid var(--gray-300);
padding-top: 36px;
margin-top: 8px;
}
.palette h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 19px;
margin-bottom: 18px;
}
.swatches {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(112px, 1fr));
gap: 10px;
margin-bottom: 32px;
}
.swatch {
border: 1px solid var(--gray-300);
border-radius: 8px;
overflow: hidden;
background: #fff;
}
.swatch-chip {
height: 44px;
}
.swatch-meta {
padding: 8px 10px 10px;
}
.swatch-name {
font-size: 12px;
color: var(--gray-700);
}
.swatch-hex {
font-family: var(--mono);
font-size: 10px;
color: var(--gray-500);
}
.notes {
font-size: 13px;
line-height: 1.7;
color: var(--gray-700);
list-style: none;
}
.notes li {
padding-left: 18px;
position: relative;
margin-bottom: 6px;
}
.notes li::before {
content: "";
position: absolute;
left: 0;
top: 0.65em;
width: 8px;
height: 1px;
background: var(--gray-500);
}
</style>
</head>
<body>
<div class="sheet">
<header>
<h1>Background jobs — header illustrations</h1>
<p class="lead">
Three 720×320 hand-drawn SVGs for the Acme docs section on
background jobs. Flat fills, 1.52px strokes, palette-locked. Each
exports standalone via the button below it.
</p>
<div class="meta">720 × 320 · inline SVG · no external assets</div>
</header>
<!-- ============================================================
A — Queue
============================================================ -->
<figure>
<div class="canvas">
<svg id="ill-queue" xmlns="http://www.w3.org/2000/svg" width="720" height="320" viewBox="0 0 720 320">
<defs>
<style>
.m { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 11px; }
.s { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 12px; }
</style>
<marker id="qa" markerWidth="10" markerHeight="10" refX="8" refY="4" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L0,8 L8,4 z" fill="#87867F"/>
</marker>
</defs>
<rect width="720" height="320" fill="#FAF9F5"/>
<!-- queue label -->
<text x="60" y="96" class="s" fill="#87867F">queue</text>
<!-- five job rects -->
<g>
<rect x="60" y="110" width="70" height="100" rx="10" fill="#F0EEE6" stroke="#3D3D3A" stroke-width="1.5"/>
<text x="95" y="164" text-anchor="middle" class="m" fill="#3D3D3A">job 5</text>
<rect x="140" y="110" width="70" height="100" rx="10" fill="#F0EEE6" stroke="#3D3D3A" stroke-width="1.5"/>
<text x="175" y="164" text-anchor="middle" class="m" fill="#3D3D3A">job 4</text>
<rect x="220" y="110" width="70" height="100" rx="10" fill="#F0EEE6" stroke="#3D3D3A" stroke-width="1.5"/>
<text x="255" y="164" text-anchor="middle" class="m" fill="#3D3D3A">job 3</text>
<rect x="300" y="110" width="70" height="100" rx="10" fill="#F0EEE6" stroke="#3D3D3A" stroke-width="1.5"/>
<text x="335" y="164" text-anchor="middle" class="m" fill="#3D3D3A">job 2</text>
<!-- head of queue, highlighted -->
<rect x="380" y="110" width="70" height="100" rx="10" fill="#D97757" stroke="#141413" stroke-width="2"/>
<text x="415" y="164" text-anchor="middle" class="m" fill="#FAF9F5">job 1</text>
</g>
<!-- arrow to worker -->
<line x1="458" y1="160" x2="522" y2="160" stroke="#87867F" stroke-width="1.5" marker-end="url(#qa)"/>
<!-- worker -->
<rect x="530" y="90" width="130" height="140" rx="10" fill="#E3DACC" stroke="#141413" stroke-width="2"/>
<text x="595" y="156" text-anchor="middle" class="m" fill="#141413">worker</text>
<text x="595" y="172" text-anchor="middle" class="m" fill="#87867F">pool=4</text>
<!-- annotations -->
<text x="415" y="234" text-anchor="middle" class="s" fill="#87867F">next to run</text>
<text x="60" y="258" class="s" fill="#87867F">Jobs are pulled FIFO; the worker leases one at a time.</text>
</svg>
</div>
<figcaption>
<div class="cap-text">
<div class="cap-title">Queue</div>
<div class="cap-sub">For "How jobs are picked up" — intro page header.</div>
</div>
<button class="dl" data-target="ill-queue" data-filename="acme-jobs-queue.svg">Download SVG</button>
</figcaption>
</figure>
<!-- ============================================================
B — Retry with backoff
============================================================ -->
<figure>
<div class="canvas">
<svg id="ill-retry" xmlns="http://www.w3.org/2000/svg" width="720" height="320" viewBox="0 0 720 320">
<defs>
<style>
.m { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 11px; }
.s { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 12px; }
</style>
</defs>
<rect width="720" height="320" fill="#FAF9F5"/>
<!-- timeline axis -->
<line x1="60" y1="190" x2="660" y2="190" stroke="#D1CFC5" stroke-width="1.5"/>
<text x="60" y="214" class="s" fill="#87867F">t = 0</text>
<text x="660" y="214" text-anchor="end" class="s" fill="#87867F">time →</text>
<!-- attempts: exponential spacing at x = 100, 170, 310, 590 -->
<!-- attempt 1 (fail) -->
<circle cx="100" cy="190" r="8" fill="#FAF9F5" stroke="#D97757" stroke-width="2"/>
<line x1="96" y1="186" x2="104" y2="194" stroke="#D97757" stroke-width="1.5"/>
<line x1="104" y1="186" x2="96" y2="194" stroke="#D97757" stroke-width="1.5"/>
<text x="100" y="222" text-anchor="middle" class="m" fill="#3D3D3A">try 1</text>
<!-- attempt 2 (fail) -->
<circle cx="170" cy="190" r="8" fill="#FAF9F5" stroke="#D97757" stroke-width="2"/>
<line x1="166" y1="186" x2="174" y2="194" stroke="#D97757" stroke-width="1.5"/>
<line x1="174" y1="186" x2="166" y2="194" stroke="#D97757" stroke-width="1.5"/>
<text x="170" y="222" text-anchor="middle" class="m" fill="#3D3D3A">try 2</text>
<!-- attempt 3 (fail) -->
<circle cx="310" cy="190" r="8" fill="#FAF9F5" stroke="#D97757" stroke-width="2"/>
<line x1="306" y1="186" x2="314" y2="194" stroke="#D97757" stroke-width="1.5"/>
<line x1="314" y1="186" x2="306" y2="194" stroke="#D97757" stroke-width="1.5"/>
<text x="310" y="222" text-anchor="middle" class="m" fill="#3D3D3A">try 3</text>
<!-- attempt 4 (success) -->
<circle cx="590" cy="190" r="9" fill="#788C5D" stroke="#141413" stroke-width="1.5"/>
<path d="M585,190 L589,194 L596,185" fill="none" stroke="#FAF9F5" stroke-width="1.5" stroke-linecap="round"/>
<text x="590" y="222" text-anchor="middle" class="m" fill="#3D3D3A">try 4</text>
<!-- dashed backoff arcs (loop back over the gap) -->
<path d="M 100 182 Q 135 130 170 182" fill="none" stroke="#87867F" stroke-width="1.5" stroke-dasharray="4 4"/>
<text x="135" y="124" text-anchor="middle" class="m" fill="#87867F">+1s</text>
<path d="M 170 182 Q 240 110 310 182" fill="none" stroke="#87867F" stroke-width="1.5" stroke-dasharray="4 4"/>
<text x="240" y="104" text-anchor="middle" class="m" fill="#87867F">+2s</text>
<path d="M 310 182 Q 450 80 590 182" fill="none" stroke="#87867F" stroke-width="1.5" stroke-dasharray="4 4"/>
<text x="450" y="74" text-anchor="middle" class="m" fill="#87867F">+4s</text>
<!-- annotation -->
<text x="60" y="262" class="s" fill="#87867F">Each failure waits twice as long before re-queuing; jitter not pictured.</text>
</svg>
</div>
<figcaption>
<div class="cap-text">
<div class="cap-title">Retry with backoff</div>
<div class="cap-sub">For "Handling failures" — retry policy header.</div>
</div>
<button class="dl" data-target="ill-retry" data-filename="acme-jobs-retry.svg">Download SVG</button>
</figcaption>
</figure>
<!-- ============================================================
C — Fan-out
============================================================ -->
<figure>
<div class="canvas">
<svg id="ill-fanout" xmlns="http://www.w3.org/2000/svg" width="720" height="320" viewBox="0 0 720 320">
<defs>
<style>
.m { font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace; font-size: 11px; }
.s { font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif; font-size: 12px; }
</style>
<marker id="fa" markerWidth="9" markerHeight="9" refX="7" refY="3.5" orient="auto" markerUnits="userSpaceOnUse">
<path d="M0,0 L0,7 L7,3.5 z" fill="#87867F"/>
</marker>
</defs>
<rect width="720" height="320" fill="#FAF9F5"/>
<!-- source -->
<rect x="60" y="128" width="110" height="64" rx="10" fill="#E3DACC" stroke="#141413" stroke-width="2"/>
<text x="115" y="158" text-anchor="middle" class="m" fill="#141413">enqueue</text>
<text x="115" y="172" text-anchor="middle" class="m" fill="#87867F">batch()</text>
<!-- fan-out arrows: from (170,160) to four shard boxes at x=300 -->
<line x1="170" y1="160" x2="296" y2="64" stroke="#87867F" stroke-width="1.5" marker-end="url(#fa)"/>
<line x1="170" y1="160" x2="296" y2="128" stroke="#87867F" stroke-width="1.5" marker-end="url(#fa)"/>
<line x1="170" y1="160" x2="296" y2="192" stroke="#87867F" stroke-width="1.5" marker-end="url(#fa)"/>
<line x1="170" y1="160" x2="296" y2="256" stroke="#87867F" stroke-width="1.5" marker-end="url(#fa)"/>
<!-- four shard boxes -->
<g>
<rect x="300" y="44" width="120" height="40" rx="10" fill="#F0EEE6" stroke="#3D3D3A" stroke-width="1.5"/>
<text x="360" y="69" text-anchor="middle" class="m" fill="#3D3D3A">shard 0</text>
<rect x="300" y="108" width="120" height="40" rx="10" fill="#F0EEE6" stroke="#3D3D3A" stroke-width="1.5"/>
<text x="360" y="133" text-anchor="middle" class="m" fill="#3D3D3A">shard 1</text>
<rect x="300" y="172" width="120" height="40" rx="10" fill="#F0EEE6" stroke="#3D3D3A" stroke-width="1.5"/>
<text x="360" y="197" text-anchor="middle" class="m" fill="#3D3D3A">shard 2</text>
<rect x="300" y="236" width="120" height="40" rx="10" fill="#F0EEE6" stroke="#3D3D3A" stroke-width="1.5"/>
<text x="360" y="261" text-anchor="middle" class="m" fill="#3D3D3A">shard 3</text>
</g>
<!-- converge arrows to merge -->
<line x1="420" y1="64" x2="546" y2="160" stroke="#87867F" stroke-width="1.5" marker-end="url(#fa)"/>
<line x1="420" y1="128" x2="546" y2="160" stroke="#87867F" stroke-width="1.5" marker-end="url(#fa)"/>
<line x1="420" y1="192" x2="546" y2="160" stroke="#87867F" stroke-width="1.5" marker-end="url(#fa)"/>
<line x1="420" y1="256" x2="546" y2="160" stroke="#87867F" stroke-width="1.5" marker-end="url(#fa)"/>
<!-- merge box -->
<rect x="550" y="128" width="110" height="64" rx="10" fill="#788C5D" stroke="#141413" stroke-width="2"/>
<text x="605" y="158" text-anchor="middle" class="m" fill="#FAF9F5">merge</text>
<text x="605" y="172" text-anchor="middle" class="m" fill="#E3DACC">await all</text>
<!-- annotations -->
<text x="232" y="42" class="s" fill="#87867F">fan-out</text>
<text x="486" y="42" text-anchor="end" class="s" fill="#87867F">fan-in</text>
<text x="60" y="300" class="s" fill="#87867F">Parent job spawns N children, then blocks on a completion barrier.</text>
</svg>
</div>
<figcaption>
<div class="cap-text">
<div class="cap-title">Fan-out / fan-in</div>
<div class="cap-sub">For "Batch and parallel work" — fan-out pattern header.</div>
</div>
<button class="dl" data-target="ill-fanout" data-filename="acme-jobs-fanout.svg">Download SVG</button>
</figcaption>
</figure>
<!-- ============================================================
Palette + usage notes
============================================================ -->
<section class="palette">
<h2>Palette &amp; rules</h2>
<div class="swatches">
<div class="swatch">
<div class="swatch-chip" style="background:#FAF9F5"></div>
<div class="swatch-meta">
<div class="swatch-name">ivory</div>
<div class="swatch-hex">#FAF9F5</div>
</div>
</div>
<div class="swatch">
<div class="swatch-chip" style="background:#141413"></div>
<div class="swatch-meta">
<div class="swatch-name">slate</div>
<div class="swatch-hex">#141413</div>
</div>
</div>
<div class="swatch">
<div class="swatch-chip" style="background:#D97757"></div>
<div class="swatch-meta">
<div class="swatch-name">clay</div>
<div class="swatch-hex">#D97757</div>
</div>
</div>
<div class="swatch">
<div class="swatch-chip" style="background:#788C5D"></div>
<div class="swatch-meta">
<div class="swatch-name">olive</div>
<div class="swatch-hex">#788C5D</div>
</div>
</div>
<div class="swatch">
<div class="swatch-chip" style="background:#E3DACC"></div>
<div class="swatch-meta">
<div class="swatch-name">oat</div>
<div class="swatch-hex">#E3DACC</div>
</div>
</div>
<div class="swatch">
<div class="swatch-chip" style="background:#F0EEE6"></div>
<div class="swatch-meta">
<div class="swatch-name">gray-150</div>
<div class="swatch-hex">#F0EEE6</div>
</div>
</div>
<div class="swatch">
<div class="swatch-chip" style="background:#D1CFC5"></div>
<div class="swatch-meta">
<div class="swatch-name">gray-300</div>
<div class="swatch-hex">#D1CFC5</div>
</div>
</div>
<div class="swatch">
<div class="swatch-chip" style="background:#87867F"></div>
<div class="swatch-meta">
<div class="swatch-name">gray-500</div>
<div class="swatch-hex">#87867F</div>
</div>
</div>
</div>
<ul class="notes">
<li>Strokes are 1.5px for neutral boxes, 2px for emphasised containers.</li>
<li>All rectangles use <code>rx="10"</code>; no drop shadows or gradients.</li>
<li>Labels inside boxes are 11px mono; annotations outside are 12px sans, gray-500.</li>
<li>Clay marks "the thing in focus"; olive marks "success / done".</li>
<li>Each SVG carries its own <code>&lt;style&gt;</code> block so the download stands alone.</li>
</ul>
</section>
</div>
<script>
document.querySelectorAll('.dl').forEach(function (btn) {
btn.addEventListener('click', function () {
var svg = document.getElementById(btn.dataset.target);
var src = new XMLSerializer().serializeToString(svg);
var blob = new Blob([src], { type: 'image/svg+xml;charset=utf-8' });
var url = URL.createObjectURL(blob);
var a = document.createElement('a');
a.href = url;
a.download = btn.dataset.filename || 'illustration.svg';
a.click();
setTimeout(function () { URL.revokeObjectURL(url); }, 1000);
});
});
</script>
</body>
</html>
+529
View File
@@ -0,0 +1,529 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Acme — Engineering Status — Week 11</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--rust: #B04A3F;
--gray-100: #F0EEE6;
--gray-300: #D1CFC5;
--gray-500: #87867F;
--gray-700: #3D3D3A;
--white: #FFFFFF;
--serif: ui-serif, Georgia, serif;
--sans: system-ui, -apple-system, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, monospace;
--radius-panel: 12px;
--radius-row: 8px;
--border: 1.5px solid var(--gray-300);
}
* { box-sizing: border-box; }
body {
margin: 0;
padding: 56px 24px 120px;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
font-size: 15px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.page {
max-width: 860px;
margin: 0 auto;
}
/* ---------- Header ---------- */
header {
margin-bottom: 40px;
}
.header-top {
display: flex;
align-items: baseline;
justify-content: space-between;
flex-wrap: wrap;
gap: 12px;
margin-bottom: 8px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 38px;
letter-spacing: -0.01em;
margin: 0;
}
.auto-pill {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
background: var(--gray-100);
border: var(--border);
border-radius: 999px;
padding: 5px 11px;
white-space: nowrap;
}
.date-range {
color: var(--gray-500);
font-size: 14px;
}
.date-range .repo {
font-family: var(--mono);
font-size: 13px;
color: var(--gray-700);
}
/* ---------- Sections ---------- */
section {
margin-bottom: 52px;
}
h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 24px;
letter-spacing: -0.01em;
margin: 0 0 6px;
}
hr.rule {
border: none;
border-top: 1px solid var(--gray-300);
margin: 0 0 22px;
}
/* ---------- Summary band ---------- */
.summary-band {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
}
@media (max-width: 720px) {
.summary-band { grid-template-columns: repeat(2, 1fr); }
}
.stat-card {
background: var(--white);
border: var(--border);
border-radius: var(--radius-panel);
padding: 20px 22px 18px;
}
.stat-card.warn {
border-left: 4px solid var(--clay);
padding-left: 19px;
}
.stat-num {
font-family: var(--serif);
font-size: 44px;
font-weight: 500;
line-height: 1;
color: var(--slate);
margin-bottom: 8px;
}
.stat-label {
font-family: var(--sans);
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--gray-500);
}
.stat-delta {
font-family: var(--mono);
font-size: 11px;
margin-top: 6px;
}
.stat-delta.up { color: var(--olive); }
.stat-delta.flat { color: var(--gray-500); }
/* ---------- Highlights ---------- */
.highlights {
list-style: none;
margin: 0;
padding: 0;
}
.highlights li {
position: relative;
padding: 0 0 14px 26px;
font-size: 15px;
color: var(--gray-700);
}
.highlights li::before {
content: "";
position: absolute;
left: 6px;
top: 8px;
width: 7px;
height: 7px;
border-radius: 2px;
background: var(--clay);
}
.highlights li strong {
color: var(--slate);
font-weight: 600;
}
/* ---------- Shipped table ---------- */
table.shipped {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background: var(--white);
border: var(--border);
border-radius: var(--radius-panel);
overflow: hidden;
}
table.shipped thead th {
text-align: left;
font-family: var(--sans);
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
background: var(--gray-100);
padding: 12px 16px;
border-bottom: 1px solid var(--gray-300);
}
table.shipped tbody td {
padding: 13px 16px;
border-bottom: 1px solid var(--gray-100);
font-size: 14px;
vertical-align: middle;
}
table.shipped tbody tr:last-child td {
border-bottom: none;
}
table.shipped tbody tr:hover {
background: var(--ivory);
}
.pr-link {
font-family: var(--mono);
font-size: 13px;
color: var(--clay);
text-decoration: none;
border-bottom: 1px dotted transparent;
}
.pr-link:hover {
border-bottom-color: var(--clay);
}
.author {
color: var(--gray-700);
font-size: 13px;
}
.risk {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 12px;
color: var(--gray-500);
}
.risk-dot {
width: 9px;
height: 9px;
border-radius: 50%;
flex-shrink: 0;
}
.risk-dot.low { background: var(--olive); }
.risk-dot.med { background: var(--clay); }
.risk-dot.high { background: var(--rust); }
/* ---------- Velocity chart ---------- */
.chart-panel {
background: var(--white);
border: var(--border);
border-radius: var(--radius-panel);
padding: 24px 28px 18px;
}
.chart-panel svg {
display: block;
width: 100%;
height: auto;
}
.chart-caption {
font-size: 12px;
color: var(--gray-500);
margin-top: 12px;
}
/* ---------- Carryover ---------- */
.carryover {
background: var(--oat);
border-radius: var(--radius-panel);
padding: 20px 22px;
}
.carry-item {
display: flex;
align-items: baseline;
gap: 14px;
padding: 8px 0;
}
.carry-item + .carry-item {
border-top: 1px solid rgba(20, 20, 19, 0.08);
}
.carry-tag {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--gray-700);
background: var(--ivory);
border-radius: 4px;
padding: 3px 7px;
flex-shrink: 0;
}
.carry-body {
font-size: 14px;
color: var(--gray-700);
}
.carry-body .who {
color: var(--gray-500);
font-size: 12px;
}
/* ---------- Footer ---------- */
footer {
margin-top: 64px;
padding-top: 20px;
border-top: 1px solid var(--gray-300);
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
}
</style>
</head>
<body>
<div class="page">
<header>
<div class="header-top">
<h1>Engineering Status &mdash; Week 11</h1>
<span class="auto-pill">auto-generated</span>
</div>
<div class="date-range">
Mar 10 &ndash; Mar 16, 2025 &nbsp;&middot;&nbsp;
<span class="repo">acme/app @ main</span>
</div>
</header>
<!-- Summary band -->
<section>
<div class="summary-band">
<div class="stat-card">
<div class="stat-num">14</div>
<div class="stat-label">PRs merged</div>
<div class="stat-delta up">+3 vs wk10</div>
</div>
<div class="stat-card">
<div class="stat-num">6</div>
<div class="stat-label">Deploys</div>
<div class="stat-delta flat">&plusmn;0</div>
</div>
<div class="stat-card warn">
<div class="stat-num">1</div>
<div class="stat-label">Incidents</div>
<div class="stat-delta flat">SEV-2 &middot; 47m</div>
</div>
<div class="stat-card">
<div class="stat-num">3</div>
<div class="stat-label">Flaky tests fixed</div>
<div class="stat-delta up">suite now 99.1%</div>
</div>
</div>
</section>
<!-- Highlights -->
<section>
<h2>Highlights</h2>
<hr class="rule">
<ul class="highlights">
<li>
<strong>Bulk task editing shipped to 100%.</strong> The multi-select
toolbar landed behind a flag on Monday and ramped to all workspaces by
Thursday with no error-rate regression.
</li>
<li>
<strong>Sync API p95 down 38%.</strong> Replacing per-task auth checks
with a scoped batch lookup cut the hot path from 410ms to 255ms on the
staging load test.
</li>
<li>
<strong>One SEV-2 on Wednesday</strong> &mdash; a config rollout pushed
a bad connection-pool limit to the sync workers. Mitigated in 47 minutes;
full postmortem linked below.
</li>
</ul>
</section>
<!-- Shipped -->
<section>
<h2>Shipped</h2>
<hr class="rule">
<table class="shipped">
<thead>
<tr>
<th style="width: 90px;">PR</th>
<th>Title</th>
<th style="width: 140px;">Author</th>
<th style="width: 100px;">Risk</th>
</tr>
</thead>
<tbody>
<tr>
<td><a class="pr-link" href="#">#4871</a></td>
<td>Bulk edit toolbar: selection model + keyboard shortcuts</td>
<td class="author">Mira Okafor</td>
<td><span class="risk"><span class="risk-dot med"></span>Med</span></td>
</tr>
<tr>
<td><a class="pr-link" href="#">#4874</a></td>
<td>Batch auth lookup for <code>/v2/sync</code> hot path</td>
<td class="author">Devon Park</td>
<td><span class="risk"><span class="risk-dot med"></span>Med</span></td>
</tr>
<tr>
<td><a class="pr-link" href="#">#4878</a></td>
<td>Fix race in attachment uploader retry loop</td>
<td class="author">Sam Reyes</td>
<td><span class="risk"><span class="risk-dot low"></span>Low</span></td>
</tr>
<tr>
<td><a class="pr-link" href="#">#4879</a></td>
<td>Migrate reminder scheduler to idempotent job keys</td>
<td class="author">Priya Anand</td>
<td><span class="risk"><span class="risk-dot high"></span>High</span></td>
</tr>
<tr>
<td><a class="pr-link" href="#">#4882</a></td>
<td>Board view: collapse empty swimlanes by default</td>
<td class="author">Mira Okafor</td>
<td><span class="risk"><span class="risk-dot low"></span>Low</span></td>
</tr>
<tr>
<td><a class="pr-link" href="#">#4885</a></td>
<td>Quarantine 3 flaky webhook integration tests</td>
<td class="author">Jules Tan</td>
<td><span class="risk"><span class="risk-dot low"></span>Low</span></td>
</tr>
<tr>
<td><a class="pr-link" href="#">#4888</a></td>
<td>Connection pool limits configurable per worker tier</td>
<td class="author">Devon Park</td>
<td><span class="risk"><span class="risk-dot med"></span>Med</span></td>
</tr>
<tr>
<td><a class="pr-link" href="#">#4891</a></td>
<td>Dark mode pass on settings &amp; billing panels</td>
<td class="author">Noor Halabi</td>
<td><span class="risk"><span class="risk-dot low"></span>Low</span></td>
</tr>
</tbody>
</table>
</section>
<!-- Velocity -->
<section>
<h2>Velocity</h2>
<hr class="rule">
<div class="chart-panel">
<svg viewBox="0 0 640 180" role="img" aria-label="PRs merged per day">
<!-- gridlines -->
<line x1="48" y1="20" x2="620" y2="20" stroke="#F0EEE6" stroke-width="1"/>
<line x1="48" y1="60" x2="620" y2="60" stroke="#F0EEE6" stroke-width="1"/>
<line x1="48" y1="100" x2="620" y2="100" stroke="#F0EEE6" stroke-width="1"/>
<line x1="48" y1="140" x2="620" y2="140" stroke="#D1CFC5" stroke-width="1.5"/>
<!-- y-axis labels -->
<text x="40" y="144" text-anchor="end" font-family="system-ui" font-size="11" fill="#87867F">0</text>
<text x="40" y="104" text-anchor="end" font-family="system-ui" font-size="11" fill="#87867F">1</text>
<text x="40" y="64" text-anchor="end" font-family="system-ui" font-size="11" fill="#87867F">2</text>
<text x="40" y="24" text-anchor="end" font-family="system-ui" font-size="11" fill="#87867F">3</text>
<!-- bars: 7 days, values 2,3,1,4,2,1,1 (sum 14)
baseline y=140, unit=40px, bar width=56, gap=24, start x=64 -->
<!-- Mon: 2 -->
<rect x="64" y="60" width="56" height="80" rx="6" fill="#E3DACC"/>
<!-- Tue: 3 -->
<rect x="144" y="20" width="56" height="120" rx="6" fill="#E3DACC"/>
<!-- Wed: 1 -->
<rect x="224" y="100" width="56" height="40" rx="6" fill="#E3DACC"/>
<!-- Thu: 4 (peak) -->
<rect x="304" y="0" width="56" height="140" rx="6" fill="#D97757"/>
<!-- Fri: 2 -->
<rect x="384" y="60" width="56" height="80" rx="6" fill="#E3DACC"/>
<!-- Sat: 1 -->
<rect x="464" y="100" width="56" height="40" rx="6" fill="#E3DACC"/>
<!-- Sun: 1 -->
<rect x="544" y="100" width="56" height="40" rx="6" fill="#E3DACC"/>
<!-- value labels -->
<text x="92" y="54" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">2</text>
<text x="172" y="14" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">3</text>
<text x="252" y="94" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">1</text>
<text x="332" y="14" text-anchor="middle" font-family="system-ui" font-size="11" fill="#3D3D3A" font-weight="600">4</text>
<text x="412" y="54" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">2</text>
<text x="492" y="94" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">1</text>
<text x="572" y="94" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">1</text>
<!-- x-axis labels -->
<text x="92" y="158" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">Mon</text>
<text x="172" y="158" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">Tue</text>
<text x="252" y="158" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">Wed</text>
<text x="332" y="158" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">Thu</text>
<text x="412" y="158" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">Fri</text>
<text x="492" y="158" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">Sat</text>
<text x="572" y="158" text-anchor="middle" font-family="system-ui" font-size="11" fill="#87867F">Sun</text>
</svg>
<div class="chart-caption">
PRs merged per day. Thursday spike is the bulk-edit feature train (4 PRs landed together).
</div>
</div>
</section>
<!-- Carryover -->
<section>
<h2>Carryover</h2>
<hr class="rule">
<div class="carryover">
<div class="carry-item">
<span class="carry-tag">In review</span>
<div class="carry-body">
Workspace export to CSV &mdash; waiting on pagination review.
<span class="who">&middot; Sam Reyes</span>
</div>
</div>
<div class="carry-item">
<span class="carry-tag">Blocked</span>
<div class="carry-body">
SSO group mapping &mdash; blocked on staging IdP credentials from IT.
<span class="who">&middot; Priya Anand</span>
</div>
</div>
<div class="carry-item">
<span class="carry-tag">Slipped</span>
<div class="carry-body">
Mobile push reliability dashboard &mdash; deprioritized for incident follow-up.
<span class="who">&middot; Devon Park</span>
</div>
</div>
</div>
</section>
<footer>
Sources: git log main..HEAD &middot; CI dashboard &middot; deploy log
&nbsp;&mdash;&nbsp; generated Mar 16 2025 18:02
</footer>
</div>
</body>
</html>
+597
View File
@@ -0,0 +1,597 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>INC-2025-0412 — Elevated 502s on task sync</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--rust: #B04A3F;
--gray-100: #F0EEE6;
--gray-300: #D1CFC5;
--gray-500: #87867F;
--gray-700: #3D3D3A;
--white: #FFFFFF;
--serif: ui-serif, Georgia, serif;
--sans: system-ui, -apple-system, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, monospace;
--radius-panel: 12px;
--radius-row: 8px;
--border: 1.5px solid var(--gray-300);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
padding: 56px 24px 120px;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
font-size: 15px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
.page {
max-width: 820px;
margin: 0 auto;
}
/* ---------- Header ---------- */
header {
margin-bottom: 32px;
}
.inc-id {
font-family: var(--mono);
font-size: 13px;
color: var(--gray-500);
letter-spacing: 0.02em;
margin-bottom: 8px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 36px;
letter-spacing: -0.01em;
margin: 0 0 18px;
line-height: 1.2;
}
.meta-row {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 12px;
}
.pill {
display: inline-flex;
align-items: baseline;
gap: 6px;
font-family: var(--sans);
font-size: 12px;
font-weight: 600;
border-radius: 999px;
padding: 5px 12px;
line-height: 1;
}
.pill .k {
font-weight: 400;
opacity: 0.75;
}
.pill.sev {
background: var(--clay);
color: var(--white);
letter-spacing: 0.03em;
}
.pill.resolved {
background: var(--olive);
color: var(--white);
}
.pill.neutral {
background: var(--gray-100);
color: var(--gray-700);
border: var(--border);
}
.pill.neutral .v {
font-family: var(--mono);
}
/* ---------- TL;DR ---------- */
.tldr {
background: var(--slate);
color: var(--ivory);
border-radius: var(--radius-panel);
padding: 22px 26px;
margin-bottom: 48px;
}
.tldr-label {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--oat);
margin-bottom: 10px;
}
.tldr p {
margin: 0;
font-size: 15.5px;
line-height: 1.65;
}
.tldr code {
font-family: var(--mono);
font-size: 13.5px;
background: rgba(250, 249, 245, 0.12);
padding: 1px 5px;
border-radius: 4px;
}
/* ---------- Sections ---------- */
section {
margin-bottom: 52px;
scroll-margin-top: 24px;
}
h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 24px;
letter-spacing: -0.01em;
margin: 0 0 6px;
}
hr.rule {
border: none;
border-top: 1px solid var(--gray-300);
margin: 0 0 22px;
}
p { margin: 0 0 14px; }
code.inline {
font-family: var(--mono);
font-size: 13px;
background: var(--gray-100);
padding: 1.5px 5px;
border-radius: 4px;
}
/* ---------- Timeline ---------- */
.timeline {
position: relative;
padding: 4px 0 4px 16px;
}
.timeline::before {
content: "";
position: absolute;
left: 16px;
top: 8px;
bottom: 8px;
width: 2px;
background: var(--gray-300);
}
.tl-entry {
position: relative;
padding: 0 0 22px 28px;
}
.tl-entry:last-child {
padding-bottom: 0;
}
.tl-dot {
position: absolute;
left: -5px;
top: 6px;
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--gray-500);
border: 2px solid var(--ivory);
box-sizing: content-box;
}
.tl-dot.impact { background: var(--clay); }
.tl-dot.mitigated { background: var(--olive); }
.tl-time {
display: inline-block;
font-family: var(--mono);
font-size: 12px;
color: var(--gray-700);
background: var(--gray-100);
border: 1px solid var(--gray-300);
border-radius: 6px;
padding: 2px 8px;
margin-bottom: 6px;
}
.tl-body {
font-size: 14px;
color: var(--gray-700);
}
.tl-body strong {
color: var(--slate);
font-weight: 600;
}
/* ---------- Code diff ---------- */
.code-panel {
background: var(--slate);
color: var(--gray-100);
border-radius: var(--radius-panel);
padding: 18px 20px;
font-family: var(--mono);
font-size: 13px;
line-height: 1.7;
overflow-x: auto;
margin: 8px 0 4px;
}
.code-panel .path {
color: var(--gray-500);
font-size: 12px;
margin-bottom: 10px;
display: block;
}
.diff-line { white-space: pre; }
.diff-line.ctx { color: var(--gray-300); }
.diff-line.del { color: #E0897A; }
.diff-line.add { color: #A3B88A; }
/* ---------- Impact table ---------- */
table.impact {
width: 100%;
max-width: 460px;
border-collapse: separate;
border-spacing: 0;
background: var(--white);
border: var(--border);
border-radius: var(--radius-panel);
overflow: hidden;
}
table.impact th,
table.impact td {
text-align: left;
padding: 12px 18px;
font-size: 14px;
border-bottom: 1px solid var(--gray-100);
}
table.impact tr:last-child th,
table.impact tr:last-child td {
border-bottom: none;
}
table.impact th {
font-weight: 400;
color: var(--gray-500);
width: 55%;
}
table.impact td {
font-family: var(--mono);
color: var(--slate);
}
/* ---------- Action items ---------- */
.actions {
background: var(--white);
border: var(--border);
border-radius: var(--radius-panel);
overflow: hidden;
}
.ai-row {
display: grid;
grid-template-columns: 36px 36px 1fr 96px;
align-items: center;
gap: 14px;
padding: 14px 18px;
border-bottom: 1px solid var(--gray-100);
}
.ai-row:last-child { border-bottom: none; }
.ai-check {
width: 18px;
height: 18px;
border: 1.5px solid var(--gray-300);
border-radius: 5px;
background: var(--white);
position: relative;
}
.ai-row.done .ai-check {
background: var(--olive);
border-color: var(--olive);
}
.ai-row.done .ai-check::after {
content: "";
position: absolute;
left: 4px;
top: 1px;
width: 5px;
height: 9px;
border: solid var(--white);
border-width: 0 2px 2px 0;
transform: rotate(40deg);
}
.ai-row.done .ai-desc {
color: var(--gray-500);
text-decoration: line-through;
text-decoration-color: var(--gray-300);
}
.ai-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background: var(--oat);
color: var(--gray-700);
font-size: 11px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 0.02em;
}
.ai-desc {
font-size: 14px;
color: var(--slate);
}
.ai-due {
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
text-align: right;
}
/* ---------- Fixed TOC ---------- */
.toc {
display: none;
}
@media (min-width: 1100px) {
.toc {
display: block;
position: fixed;
top: 72px;
right: max(24px, calc(50vw - 410px - 200px));
width: 170px;
font-size: 13px;
}
.toc-title {
font-family: var(--mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--gray-500);
margin-bottom: 12px;
}
.toc a {
display: block;
color: var(--gray-500);
text-decoration: none;
padding: 5px 0 5px 12px;
border-left: 1.5px solid var(--gray-300);
line-height: 1.3;
}
.toc a:hover {
color: var(--slate);
border-left-color: var(--clay);
}
}
footer {
margin-top: 64px;
padding-top: 20px;
border-top: 1px solid var(--gray-300);
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
}
</style>
</head>
<body>
<nav class="toc">
<div class="toc-title">On this page</div>
<a href="#tldr">TL;DR</a>
<a href="#timeline">Timeline</a>
<a href="#root-cause">Root cause</a>
<a href="#impact">Impact</a>
<a href="#actions">Action items</a>
</nav>
<div class="page">
<header>
<div class="inc-id">INC-2025-0412</div>
<h1>Elevated 502s on task sync</h1>
<div class="meta-row">
<span class="pill sev">SEV-2</span>
<span class="pill resolved">Resolved</span>
<span class="pill neutral"><span class="k">Duration</span> <span class="v">47 min</span></span>
<span class="pill neutral"><span class="k">Detected</span> <span class="v">Apr 12 &middot; 14:07</span></span>
<span class="pill neutral"><span class="k">Owner</span> <span class="v">Devon Park</span></span>
</div>
</header>
<div class="tldr" id="tldr">
<div class="tldr-label">TL;DR</div>
<p>
A config rollout lowered the database connection-pool limit on the
<code>sync-worker</code> tier from 64 to 8, exhausting connections under
normal afternoon load. The sync API returned 502s for roughly 21% of
requests over 47 minutes. We mitigated by reverting the config and
cycling the worker fleet; no data was lost.
</p>
</div>
<!-- Timeline -->
<section id="timeline">
<h2>Timeline</h2>
<hr class="rule">
<div class="timeline">
<div class="tl-entry">
<span class="tl-dot"></span>
<span class="tl-time">14:02</span>
<div class="tl-body">
Config change <code class="inline">cfg-9a12</code> promoted to
production via the standard rollout pipeline.
</div>
</div>
<div class="tl-entry">
<span class="tl-dot impact"></span>
<span class="tl-time">14:06</span>
<div class="tl-body">
<strong>Impact starts.</strong> Sync workers begin queueing on pool
checkout; p95 latency climbs past 4s and the load balancer starts
returning 502s.
</div>
</div>
<div class="tl-entry">
<span class="tl-dot"></span>
<span class="tl-time">14:07</span>
<div class="tl-body">
Alert fires: <code class="inline">sync_5xx_rate &gt; 2%</code> for 60s.
On-call (Devon) acknowledges.
</div>
</div>
<div class="tl-entry">
<span class="tl-dot"></span>
<span class="tl-time">14:18</span>
<div class="tl-body">
Initial hypothesis is a bad deploy of the API service; last two
application deploys are rolled back with no effect.
</div>
</div>
<div class="tl-entry">
<span class="tl-dot"></span>
<span class="tl-time">14:31</span>
<div class="tl-body">
Mira joins and notices pool-wait saturation in the worker dashboard.
Investigation pivots to infra config rather than app code.
</div>
</div>
<div class="tl-entry">
<span class="tl-dot mitigated"></span>
<span class="tl-time">14:44</span>
<div class="tl-body">
<strong>Mitigated.</strong> <code class="inline">cfg-9a12</code>
reverted; worker fleet cycled. 5xx rate drops below 0.2% within
three minutes.
</div>
</div>
<div class="tl-entry">
<span class="tl-dot"></span>
<span class="tl-time">14:49</span>
<div class="tl-body">
Monitors green for 5 minutes. Incident declared resolved; status
page updated.
</div>
</div>
</div>
</section>
<!-- Root cause -->
<section id="root-cause">
<h2>Root cause</h2>
<hr class="rule">
<p>
PR <code class="inline">#4888</code> made connection-pool limits
configurable per worker tier. The default for the new
<code class="inline">sync-worker</code> key was meant to inherit the
global value (64) but was hard-coded to 8 during a local test and
committed. The config linter only validates type, not magnitude, so the
change passed CI.
</p>
<p>
Because config rollouts and code deploys go through separate pipelines,
the on-call's first instinct &mdash; rolling back the most recent
application deploys &mdash; had no effect and cost roughly 13 minutes of
diagnosis time.
</p>
<div class="code-panel">
<span class="path">infra/config/workers.yaml</span>
<div class="diff-line ctx"> pool:</div>
<div class="diff-line ctx"> global_max_connections: 64</div>
<div class="diff-line ctx"> tiers:</div>
<div class="diff-line del">- sync-worker: { max_connections: 64 }</div>
<div class="diff-line add">+ sync-worker: { max_connections: 8 } # debug value, do not ship</div>
<div class="diff-line ctx"> webhook-worker: { max_connections: 32 }</div>
</div>
</section>
<!-- Impact -->
<section id="impact">
<h2>Impact</h2>
<hr class="rule">
<table class="impact">
<tr>
<th>Requests failed (502)</th>
<td>~41,200</td>
</tr>
<tr>
<th>Peak error rate</th>
<td>21.4%</td>
</tr>
<tr>
<th>Users affected</th>
<td>~2,300 workspaces</td>
</tr>
<tr>
<th>Data loss</th>
<td>None &mdash; clients retried</td>
</tr>
<tr>
<th>SLA breach</th>
<td>No (within monthly budget)</td>
</tr>
</table>
</section>
<!-- Action items -->
<section id="actions">
<h2>Action items</h2>
<hr class="rule">
<div class="actions">
<div class="ai-row done">
<span class="ai-check"></span>
<span class="ai-avatar" title="Devon Park">DP</span>
<span class="ai-desc">Revert cfg-9a12 and restore pool limit to 64</span>
<span class="ai-due">Apr 12</span>
</div>
<div class="ai-row">
<span class="ai-check"></span>
<span class="ai-avatar" title="Mira Okafor">MO</span>
<span class="ai-desc">Add config-linter range check for <code class="inline">max_connections</code> (warn &lt; 32)</span>
<span class="ai-due">Apr 18</span>
</div>
<div class="ai-row">
<span class="ai-check"></span>
<span class="ai-avatar" title="Sam Reyes">SR</span>
<span class="ai-desc">Surface &ldquo;recent config rollouts&rdquo; alongside deploys in the on-call dashboard</span>
<span class="ai-due">Apr 25</span>
</div>
<div class="ai-row">
<span class="ai-check"></span>
<span class="ai-avatar" title="Priya Anand">PA</span>
<span class="ai-desc">Canary config changes to one worker AZ for 10 min before fleet-wide promote</span>
<span class="ai-due">May 02</span>
</div>
</div>
</section>
<footer>
Authored from on-call notes + alert history &middot; reviewed by Devon Park, Mira Okafor
</footer>
</div>
</body>
</html>
+396
View File
@@ -0,0 +1,396 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Deploy pipeline — annotated flowchart</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--rust: #B04A3F;
--gray-150:#F0EEE6;
--gray-300:#D1CFC5;
--gray-500:#87867F;
--gray-700:#3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
-webkit-font-smoothing: antialiased;
padding: 56px 24px 96px;
}
.sheet { max-width: 980px; margin: 0 auto; }
header { margin-bottom: 40px; }
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 10px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 34px;
letter-spacing: -0.01em;
margin-bottom: 12px;
}
.lead {
font-size: 14.5px;
line-height: 1.6;
color: var(--gray-700);
max-width: 640px;
}
.lead code { font-family: var(--mono); font-size: 13px; }
/* ── layout ───────────────────────────── */
.layout {
display: grid;
grid-template-columns: minmax(0, 1fr) 300px;
gap: 32px;
align-items: start;
}
@media (max-width: 920px) { .layout { grid-template-columns: 1fr; } }
/* ── chart ────────────────────────────── */
.canvas {
border: 1.5px solid var(--gray-300);
border-radius: 14px;
background: #fff;
padding: 28px;
overflow-x: auto;
}
svg.flow { display: block; width: 100%; height: auto; }
.flow text {
font-family: var(--mono);
font-size: 12px;
fill: var(--slate);
pointer-events: none;
}
.flow .sub { font-size: 10px; fill: var(--gray-500); }
.flow .lbl { font-size: 10px; fill: var(--gray-500); }
.node rect { fill: #fff; stroke: var(--gray-300); stroke-width: 1.5; rx: 8; }
.node.term rect { fill: var(--gray-150); rx: 22; }
.node.gate path { fill: #fff; stroke: var(--gray-300); stroke-width: 1.5; }
.node.ok rect { fill: rgba(120,140,93,0.12); stroke: var(--olive); }
.node.bad rect { fill: rgba(176,74,63,0.10); stroke: var(--rust); }
.node { cursor: pointer; transition: transform 120ms ease; }
.node:hover { transform: translateY(-1px); }
.node.active rect,
.node.active path { stroke: var(--clay); stroke-width: 2; }
.edge { stroke: var(--gray-500); stroke-width: 1.5; fill: none; marker-end: url(#arrow); }
.edge.no { stroke: var(--rust); stroke-dasharray: 4 4; marker-end: url(#arrow-rust); }
.edge.yes { stroke: var(--olive); marker-end: url(#arrow-olive); }
/* ── side panel ───────────────────────── */
aside {
position: sticky;
top: 24px;
border: 1.5px solid var(--gray-300);
border-radius: 14px;
background: #fff;
padding: 20px 20px 22px;
}
aside .hint {
font-size: 12px;
color: var(--gray-500);
margin-bottom: 14px;
}
aside h3 {
font-family: var(--serif);
font-weight: 500;
font-size: 19px;
margin-bottom: 6px;
}
aside .meta {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
margin-bottom: 14px;
}
aside p {
font-size: 13.5px;
line-height: 1.6;
color: var(--gray-700);
margin-bottom: 12px;
}
aside pre {
font-family: var(--mono);
font-size: 11.5px;
line-height: 1.55;
background: var(--gray-150);
border: 1px solid var(--gray-300);
border-radius: 8px;
padding: 10px 12px;
white-space: pre-wrap;
color: var(--slate);
}
/* ── legend ───────────────────────────── */
.legend {
display: flex;
flex-wrap: wrap;
gap: 18px;
margin-top: 18px;
font-size: 12px;
color: var(--gray-700);
}
.legend span { display: inline-flex; align-items: center; gap: 8px; }
.chip { width: 22px; height: 14px; border: 1.5px solid var(--gray-300); border-radius: 4px; background: #fff; }
.chip.gate { transform: rotate(45deg); width: 12px; height: 12px; border-radius: 2px; }
.chip.ok { background: rgba(120,140,93,0.12); border-color: var(--olive); }
.chip.bad { background: rgba(176,74,63,0.10); border-color: var(--rust); }
</style>
</head>
<body>
<div class="sheet">
<header>
<div class="eyebrow">Illustrations &amp; Diagrams · Flowchart</div>
<h1>What happens when you <code>git push</code></h1>
<p class="lead">
The deploy pipeline for <code>acme/web</code>, drawn from <code>.github/workflows/</code> and the
Argo manifests. Click any step to see what runs, how long it usually takes, and where it can short-circuit.
</p>
</header>
<div class="layout">
<div>
<div class="canvas">
<svg class="flow" viewBox="0 0 620 920">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#87867F"/>
</marker>
<marker id="arrow-rust" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#B04A3F"/>
</marker>
<marker id="arrow-olive" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#788C5D"/>
</marker>
</defs>
<!-- edges -->
<path class="edge" d="M310,56 L310,92"/>
<path class="edge" d="M310,140 L310,176"/>
<path class="edge" d="M310,224 L310,262"/>
<path class="edge yes" d="M310,326 L310,368"/>
<text class="lbl" x="320" y="350">pass</text>
<path class="edge no" d="M268,294 C190,294 150,294 150,212 L150,212"/>
<text class="lbl" x="138" y="260" fill="#B04A3F">fail → status</text>
<path class="edge" d="M310,416 L310,452"/>
<path class="edge" d="M310,500 L310,538"/>
<path class="edge yes" d="M310,602 L310,644"/>
<text class="lbl" x="320" y="626">healthy</text>
<path class="edge no" d="M352,570 C470,570 470,690 392,720"/>
<text class="lbl" x="448" y="636" fill="#B04A3F">canary fails</text>
<path class="edge" d="M310,692 L310,730"/>
<path class="edge" d="M310,778 L310,816"/>
<!-- nodes -->
<g class="node term" data-k="push">
<rect x="230" y="12" width="160" height="44"/>
<text x="310" y="38" text-anchor="middle">git push main</text>
</g>
<g class="node" data-k="ci">
<rect x="210" y="92" width="200" height="48"/>
<text x="310" y="112" text-anchor="middle">CI · lint + typecheck</text>
<text class="sub" x="310" y="128" text-anchor="middle">~2 min · ci.yml</text>
</g>
<g class="node" data-k="test">
<rect x="210" y="176" width="200" height="48"/>
<text x="310" y="196" text-anchor="middle">Unit + integration tests</text>
<text class="sub" x="310" y="212" text-anchor="middle">~6 min · 3 shards</text>
</g>
<g class="node gate" data-k="gate-tests">
<path d="M310,262 L352,294 L310,326 L268,294 Z"/>
<text x="310" y="298" text-anchor="middle">pass?</text>
</g>
<g class="node bad" data-k="status">
<rect x="60" y="176" width="180" height="48"/>
<text x="150" y="196" text-anchor="middle">Post failure status</text>
<text class="sub" x="150" y="212" text-anchor="middle">slack #deploys</text>
</g>
<g class="node" data-k="build">
<rect x="210" y="368" width="200" height="48"/>
<text x="310" y="388" text-anchor="middle">Build + push image</text>
<text class="sub" x="310" y="404" text-anchor="middle">ghcr.io/acme/web</text>
</g>
<g class="node" data-k="canary">
<rect x="210" y="452" width="200" height="48"/>
<text x="310" y="472" text-anchor="middle">Argo canary 5%</text>
<text class="sub" x="310" y="488" text-anchor="middle">10 min soak</text>
</g>
<g class="node gate" data-k="gate-canary">
<path d="M310,538 L352,570 L310,602 L268,570 Z"/>
<text x="310" y="574" text-anchor="middle">SLO ok?</text>
</g>
<g class="node" data-k="promote">
<rect x="210" y="644" width="200" height="48"/>
<text x="310" y="664" text-anchor="middle">Promote 25 → 50 → 100%</text>
<text class="sub" x="310" y="680" text-anchor="middle">~8 min</text>
</g>
<g class="node bad" data-k="rollback">
<rect x="392" y="700" width="170" height="48"/>
<text x="477" y="720" text-anchor="middle">Auto-rollback</text>
<text class="sub" x="477" y="736" text-anchor="middle">revert image tag</text>
</g>
<g class="node" data-k="smoke">
<rect x="210" y="730" width="200" height="48"/>
<text x="310" y="750" text-anchor="middle">Smoke tests in prod</text>
<text class="sub" x="310" y="766" text-anchor="middle">playwright · 90s</text>
</g>
<g class="node ok term" data-k="done">
<rect x="230" y="816" width="160" height="44"/>
<text x="310" y="843" text-anchor="middle">✅ Deploy complete</text>
</g>
</svg>
</div>
<div class="legend">
<span><i class="chip"></i> process step</span>
<span><i class="chip gate"></i> decision</span>
<span><i class="chip ok"></i> terminal success</span>
<span><i class="chip bad"></i> failure path</span>
</div>
</div>
<aside id="panel">
<div class="hint">Click a step in the chart →</div>
<h3 id="p-title">git push main</h3>
<div class="meta" id="p-meta">trigger · 0s</div>
<p id="p-body">A push or merge to <code>main</code> fires the <code>deploy</code> workflow. Pushes to other branches stop after CI and never reach the image build.</p>
<pre id="p-code">on:
push:
branches: [main]</pre>
</aside>
</div>
</div>
<script>
const DETAIL = {
push: {
title: "git push main",
meta: "trigger · 0s",
body: "A push or merge to <code>main</code> fires the <code>deploy</code> workflow. Pushes to other branches stop after CI and never reach the image build.",
code: "on:\n push:\n branches: [main]"
},
ci: {
title: "CI · lint + typecheck",
meta: "github actions · ~2 min",
body: "Runs ESLint and <code>tsc --noEmit</code> in parallel. This is the only job that also runs on pull-request branches, so it stays fast on purpose.",
code: "jobs:\n lint:\n run: pnpm lint && pnpm typecheck"
},
test: {
title: "Unit + integration tests",
meta: "github actions · ~6 min · 3 shards",
body: "Vitest unit suite plus the API integration tests against an ephemeral Postgres. Sharded three ways; the slowest shard gates the pipeline.",
code: "strategy:\n matrix:\n shard: [1, 2, 3]\nrun: pnpm test --shard ${{ matrix.shard }}/3"
},
"gate-tests": {
title: "Tests pass?",
meta: "decision",
body: "Any shard failing short-circuits here. Nothing is built, and a red status is posted back to the PR and to <code>#deploys</code>.",
code: "needs: [test]\nif: success()"
},
status: {
title: "Post failure status",
meta: "side-effect · ~5s",
body: "The <code>notify</code> job posts the failing shard's summary to Slack and sets the commit status to <code>failure</code>. Pipeline ends.",
code: "uses: slackapi/slack-github-action@v1\nwith:\n channel: '#deploys'"
},
build: {
title: "Build + push image",
meta: "github actions · ~4 min",
body: "Docker buildx with layer caching. Tags the image with the short SHA and pushes to GHCR. This is the first step that produces an artifact.",
code: "tags: ghcr.io/acme/web:${{ github.sha }}"
},
canary: {
title: "Argo canary 5%",
meta: "argo rollouts · 10 min soak",
body: "Argo shifts 5% of traffic to the new ReplicaSet and watches the error-rate and p95 SLOs for ten minutes before deciding.",
code: "steps:\n - setWeight: 5\n - pause: { duration: 10m }\n - analysis: slo-check"
},
"gate-canary": {
title: "Canary healthy?",
meta: "decision · analysis template",
body: "The <code>slo-check</code> analysis compares error rate and p95 against the previous revision. Either metric breaching for two consecutive intervals fails the gate.",
code: "successCondition: result.error_rate < 0.5\n && result.p95_ms < 800"
},
promote: {
title: "Promote 25 → 50 → 100%",
meta: "argo rollouts · ~8 min",
body: "Three more weight steps with a short pause between each. The same analysis runs at every step, so a late regression still aborts.",
code: "- setWeight: 25\n- pause: { duration: 2m }\n- setWeight: 50\n- pause: { duration: 2m }\n- setWeight: 100"
},
rollback: {
title: "Auto-rollback",
meta: "argo rollouts · ~30s",
body: "On a failed analysis Argo flips traffic back to the stable ReplicaSet and marks the rollout <code>Degraded</code>. A page goes to the on-call.",
code: "abort:\n scaleDownDelaySeconds: 30"
},
smoke: {
title: "Smoke tests in prod",
meta: "github actions · ~90s",
body: "A tiny Playwright suite hits the public URL: load the homepage, sign in with a synthetic account, create and delete one record.",
code: "run: pnpm playwright test smoke/\nenv:\n BASE_URL: https://acme.app"
},
done: {
title: "Deploy complete",
meta: "terminal · ~30 min total",
body: "Commit status flips to <code>success</code>, the rollout is marked <code>Healthy</code>, and the SHA is recorded as the new stable revision.",
code: "✓ web@a1b9e3f live on prod"
}
};
const nodes = document.querySelectorAll(".node");
const T = document.getElementById("p-title");
const M = document.getElementById("p-meta");
const B = document.getElementById("p-body");
const C = document.getElementById("p-code");
nodes.forEach(n => {
n.addEventListener("click", () => {
nodes.forEach(x => x.classList.remove("active"));
n.classList.add("active");
const d = DETAIL[n.dataset.k];
if (!d) return;
T.textContent = d.title;
M.textContent = d.meta;
B.innerHTML = d.body;
C.textContent = d.code;
});
});
document.querySelector('.node[data-k="push"]').classList.add("active");
</script>
</body>
</html>
+382
View File
@@ -0,0 +1,382 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>How rate limiting works in acme/api</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-150:#F0EEE6;
--gray-300:#D1CFC5;
--gray-500:#87867F;
--gray-700:#3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--ivory);
color: var(--gray-700);
font-family: var(--sans);
font-size: 15px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
padding: 56px 24px 120px;
}
.page {
max-width: 1100px;
margin: 0 auto;
display: grid;
grid-template-columns: 200px minmax(0, 1fr);
gap: 48px;
}
@media (max-width: 920px) { .page { grid-template-columns: 1fr; } nav { display: none; } }
/* ── nav ──────────────────────────────── */
nav {
position: sticky;
top: 32px;
align-self: start;
font-size: 13px;
}
nav .label {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 12px;
}
nav a {
display: block;
padding: 5px 0 5px 12px;
border-left: 2px solid var(--gray-300);
color: var(--gray-700);
text-decoration: none;
}
nav a:hover { color: var(--slate); border-color: var(--slate); }
nav a.l2 { padding-left: 24px; font-size: 12.5px; color: var(--gray-500); }
nav .files {
margin-top: 28px;
border-top: 1px solid var(--gray-300);
padding-top: 16px;
}
nav .files code {
display: block;
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
padding: 3px 0;
}
/* ── header ───────────────────────────── */
header { margin-bottom: 12px; }
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 10px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 32px;
color: var(--slate);
letter-spacing: -0.01em;
margin-bottom: 14px;
}
.tldr {
border: 1.5px solid var(--gray-300);
border-left: 3px solid var(--clay);
border-radius: 10px;
background: #fff;
padding: 16px 18px;
margin-bottom: 8px;
}
.tldr b { color: var(--slate); }
h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 22px;
color: var(--slate);
margin: 40px 0 14px;
scroll-margin-top: 24px;
}
p { margin-bottom: 12px; max-width: 680px; }
code { font-family: var(--mono); font-size: 13px; }
/* ── collapsible ─────────────────────── */
details {
border: 1.5px solid var(--gray-300);
border-radius: 10px;
background: #fff;
margin: 14px 0;
overflow: hidden;
}
summary {
list-style: none;
cursor: pointer;
padding: 14px 16px;
font-family: var(--serif);
font-size: 16px;
color: var(--slate);
display: flex;
align-items: baseline;
gap: 10px;
}
summary::-webkit-details-marker { display: none; }
summary::before {
content: "▸";
color: var(--clay);
font-family: var(--sans);
font-size: 12px;
transition: transform 120ms;
}
details[open] summary::before { transform: rotate(90deg); }
summary .where {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
margin-left: auto;
}
details .body { padding: 0 16px 16px; }
details .body p { font-size: 14px; }
/* ── tabs ─────────────────────────────── */
.tabs {
border: 1.5px solid var(--gray-300);
border-radius: 10px;
background: #fff;
margin: 16px 0 8px;
overflow: hidden;
}
.tabbar {
display: flex;
border-bottom: 1px solid var(--gray-300);
background: var(--gray-150);
}
.tabbar button {
appearance: none;
border: none;
background: none;
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
padding: 10px 16px;
cursor: pointer;
border-right: 1px solid var(--gray-300);
}
.tabbar button.on {
background: #fff;
color: var(--slate);
border-bottom: 2px solid var(--clay);
margin-bottom: -1px;
}
.tabs pre {
display: none;
margin: 0;
padding: 16px 18px;
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.6;
color: var(--slate);
overflow-x: auto;
}
.tabs pre.on { display: block; }
.hl { color: var(--clay); }
.cm { color: var(--gray-500); }
/* ── callout ──────────────────────────── */
.callout {
display: flex;
gap: 12px;
border: 1.5px solid var(--oat);
background: rgba(227,218,204,0.35);
border-radius: 10px;
padding: 14px 16px;
margin: 18px 0;
font-size: 14px;
}
.callout .ico { color: var(--clay); font-weight: 600; }
/* ── faq ──────────────────────────────── */
dl.faq { margin-top: 8px; }
dl.faq dt {
font-family: var(--serif);
font-size: 16px;
color: var(--slate);
margin-top: 18px;
}
dl.faq dd { font-size: 14px; margin: 4px 0 0; max-width: 640px; }
</style>
</head>
<body>
<div class="page">
<!-- ── side nav ── -->
<nav>
<div class="label">On this page</div>
<a href="#tldr">TL;DR</a>
<a href="#path">Request path</a>
<a href="#path" class="l2">1. Identify</a>
<a href="#path" class="l2">2. Bucket lookup</a>
<a href="#path" class="l2">3. Consume</a>
<a href="#path" class="l2">4. Reject</a>
<a href="#config">Configuring a route</a>
<a href="#gotchas">Gotchas</a>
<a href="#faq">FAQ</a>
<div class="files">
<div class="label">Files read</div>
<code>middleware/ratelimit.ts</code>
<code>lib/tokenBucket.ts</code>
<code>config/limits.yaml</code>
<code>routes/*.ts</code>
</div>
</nav>
<!-- ── main ── -->
<main>
<header>
<div class="eyebrow">Research &amp; Learning · feature summary</div>
<h1>How rate limiting works in <code>acme/api</code></h1>
<div class="tldr" id="tldr">
<b>TL;DR</b> — Every request passes through <code>rateLimit()</code> middleware, which resolves the
caller to a <em>bucket key</em>, fetches a token-bucket from Redis, and either consumes one token or
returns <code>429</code>. Limits are declared per-route in <code>config/limits.yaml</code>; routes
without an entry inherit the <code>default</code> tier (100 req/min per API key).
</div>
</header>
<h2 id="path">The request path, step by step</h2>
<p>Expand each step to see what runs and where it lives. The whole path is ~40 lines and adds about
0.4&nbsp;ms p50 to every request.</p>
<details open>
<summary>1 · Identify the caller <span class="where">middleware/ratelimit.ts:21</span></summary>
<div class="body">
<p>The middleware first reduces the request to a <code>bucketKey</code>: API key if an
<code>Authorization</code> header is present, otherwise the client IP (via the
<code>x-forwarded-for</code> chain, trusting only our own LB). Anonymous IP traffic gets a much
lower default tier.</p>
</div>
</details>
<details>
<summary>2 · Look up the bucket <span class="where">lib/tokenBucket.ts:9</span></summary>
<div class="body">
<p>The route name plus bucket key map to a Redis hash (<code>rl:{route}:{key}</code>) holding
<code>tokens</code> and <code>updatedAt</code>. If the key is missing it's created lazily at full
capacity — there's no warm-up.</p>
</div>
</details>
<details>
<summary>3 · Refill and consume <span class="where">lib/tokenBucket.ts:31</span></summary>
<div class="body">
<p>Refill is computed from elapsed time (<code>rate × Δt</code>, capped at <code>burst</code>), then
one token is subtracted. The whole read-modify-write runs as a single Lua script so concurrent
requests can't double-spend.</p>
</div>
</details>
<details>
<summary>4 · Reject when empty <span class="where">middleware/ratelimit.ts:48</span></summary>
<div class="body">
<p>If the script returns <code>tokens &lt; 0</code> the middleware short-circuits with
<code>429 Too Many Requests</code> and sets <code>Retry-After</code> to the seconds until one token
refills. Successful responses always carry <code>X-RateLimit-Remaining</code>.</p>
</div>
</details>
<h2 id="config">Configuring a limit on your route</h2>
<p>You don't touch the middleware. Add an entry to <code>config/limits.yaml</code> keyed by route name,
and (optionally) tag the route so the middleware can find it.</p>
<div class="tabs" data-tabs>
<div class="tabbar">
<button class="on" data-t="0">limits.yaml</button>
<button data-t="1">route.ts</button>
<button data-t="2">client response</button>
</div>
<pre class="on"><span class="cm"># config/limits.yaml</span>
default:
rate: 100/min
burst: 120
<span class="hl">search.query</span>:
rate: 20/min
burst: 40
key: api_key <span class="cm"># or: ip</span></pre>
<pre><span class="cm">// routes/search.ts</span>
router.post(
"/search",
<span class="hl">rateLimit("search.query")</span>,
handler,
);</pre>
<pre>HTTP/1.1 429 Too Many Requests
Retry-After: 17
X-RateLimit-Limit: 20
X-RateLimit-Remaining: 0
{ "error": "rate_limited", "retry_after": 17 }</pre>
</div>
<div class="callout">
<span class="ico"></span>
<div>If you only need the default tier, you don't need a YAML entry at all — just wrap the handler in
<code>rateLimit()</code> with no argument. The route name is inferred from the path.</div>
</div>
<h2 id="gotchas">Gotchas worth knowing</h2>
<ul style="padding-left:20px; max-width:680px;">
<li style="margin-bottom:8px;"><b>Limits are per-process in dev.</b> The Redis client falls back to an
in-memory map when <code>REDIS_URL</code> is unset, so local testing won't reflect real cluster
behaviour.</li>
<li style="margin-bottom:8px;"><b>Burst ≠ rate.</b> <code>burst</code> is the bucket capacity; a caller
idle for a minute can fire <code>burst</code> requests instantly even if <code>rate</code> is low.</li>
<li><b>Streaming responses count once.</b> The token is consumed at request start; a 30-second SSE
stream still costs one token.</li>
</ul>
<h2 id="faq">FAQ</h2>
<dl class="faq">
<dt>How do I exempt internal traffic?</dt>
<dd>Set <code>x-acme-internal: 1</code> from the caller; the middleware checks it against the
mTLS peer name and skips the bucket entirely.</dd>
<dt>Where do I see who's getting limited?</dt>
<dd>Every <code>429</code> emits a <code>ratelimit.rejected</code> metric tagged with route and key
type. There's a Grafana panel under <em>API → Health</em>.</dd>
<dt>Can a single user have a higher limit?</dt>
<dd>Yes — add their API key under <code>overrides:</code> in the YAML. Overrides are reloaded without
a deploy.</dd>
</dl>
</main>
</div>
<script>
document.querySelectorAll("[data-tabs]").forEach(box => {
const btns = box.querySelectorAll("button");
const panes = box.querySelectorAll("pre");
btns.forEach(b => b.addEventListener("click", () => {
btns.forEach(x => x.classList.remove("on"));
panes.forEach(x => x.classList.remove("on"));
b.classList.add("on");
panes[+b.dataset.t].classList.add("on");
}));
});
</script>
</body>
</html>
+369
View File
@@ -0,0 +1,369 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Consistent hashing — an interactive explainer</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--sky: #6A8CAF;
--gray-150:#F0EEE6;
--gray-300:#D1CFC5;
--gray-500:#87867F;
--gray-700:#3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--ivory);
color: var(--gray-700);
font-family: var(--sans);
font-size: 15px;
line-height: 1.65;
-webkit-font-smoothing: antialiased;
padding: 56px 24px 120px;
}
.page {
max-width: 1100px;
margin: 0 auto;
display: grid;
grid-template-columns: minmax(0, 1fr) 240px;
gap: 48px;
}
@media (max-width: 960px) { .page { grid-template-columns: 1fr; } aside { order: 2; position: static; } }
/* ── header ───────────────────────────── */
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 10px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 33px;
color: var(--slate);
letter-spacing: -0.01em;
margin-bottom: 12px;
}
.lead { max-width: 640px; margin-bottom: 8px; }
h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 22px;
color: var(--slate);
margin: 40px 0 12px;
}
p { margin-bottom: 12px; max-width: 680px; }
code { font-family: var(--mono); font-size: 13px; }
.term {
border-bottom: 1.5px dotted var(--clay);
cursor: help;
color: var(--slate);
}
/* ── demo panel ───────────────────────── */
.demo {
border: 1.5px solid var(--gray-300);
border-radius: 14px;
background: #fff;
padding: 24px;
margin: 12px 0 8px;
}
.demo-grid {
display: grid;
grid-template-columns: 320px 1fr;
gap: 28px;
align-items: center;
}
@media (max-width: 760px) { .demo-grid { grid-template-columns: 1fr; } }
svg.ring { display: block; width: 100%; max-width: 320px; }
.ring .track { fill: none; stroke: var(--gray-300); stroke-width: 14; }
.ring .arc { fill: none; stroke-width: 14; transition: stroke-dasharray 300ms ease; }
.ring .node { stroke: #fff; stroke-width: 2; transition: cx 300ms, cy 300ms, opacity 200ms; }
.ring .key { fill: var(--slate); transition: cx 300ms, cy 300ms; }
.ring .lbl { font-family: var(--mono); font-size: 10px; fill: var(--gray-500); }
.controls { font-size: 13px; }
.controls .row { display: flex; align-items: center; gap: 12px; margin-bottom: 14px; }
.controls label { width: 64px; color: var(--gray-500); font-family: var(--mono); font-size: 11px; }
.controls .val { font-family: var(--mono); font-size: 12px; color: var(--slate); width: 28px; }
.controls input[type=range] { flex: 1; accent-color: var(--clay); }
.controls button {
appearance: none;
border: 1.5px solid var(--gray-300);
background: var(--gray-150);
border-radius: 6px;
font-family: var(--mono);
font-size: 11px;
padding: 6px 10px;
cursor: pointer;
margin-right: 6px;
}
.controls button:hover { background: var(--oat); }
.readout {
border-top: 1px solid var(--gray-300);
margin-top: 16px;
padding-top: 14px;
font-size: 13px;
}
.readout b { color: var(--slate); }
.moved { color: var(--clay); font-family: var(--mono); }
/* ── compare table ────────────────────── */
table {
border-collapse: collapse;
width: 100%;
max-width: 640px;
font-size: 13.5px;
margin: 12px 0;
}
th, td { text-align: left; padding: 10px 12px; border-bottom: 1px solid var(--gray-300); }
th { font-family: var(--mono); font-size: 11px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--gray-500); font-weight: 500; }
td.bad { color: #B04A3F; }
td.good { color: var(--olive); }
/* ── glossary ─────────────────────────── */
aside {
position: sticky;
top: 32px;
align-self: start;
border: 1.5px solid var(--gray-300);
border-radius: 12px;
background: #fff;
padding: 18px 18px 8px;
}
aside .label {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 12px;
}
aside dl dt {
font-family: var(--serif);
font-size: 15px;
color: var(--slate);
margin-top: 0;
}
aside dl dd {
font-size: 12.5px;
line-height: 1.5;
color: var(--gray-700);
margin: 2px 0 14px;
}
aside dl dt.hl, aside dl dt.hl + dd { background: rgba(217,119,87,0.10); margin-left: -8px; margin-right: -8px; padding-left: 8px; padding-right: 8px; border-radius: 4px; }
</style>
</head>
<body>
<div class="page">
<main>
<div class="eyebrow">Research &amp; Learning · concept explainer</div>
<h1>Consistent hashing, in one ring</h1>
<p class="lead">
You have <em>K</em> keys spread across <em>N</em> cache servers. A server dies, or you add one. How many
keys have to move? With naive <code>hash(key) mod N</code> the answer is "almost all of them." Consistent
hashing gets it down to roughly <code>K&nbsp;/&nbsp;N</code>. Here's why.
</p>
<h2>The trick: hash onto a circle, not a line</h2>
<p>
Map both <span class="term" data-term="node">nodes</span> and keys onto the same
<span class="term" data-term="ring">ring</span> (the hash output space, wrapped around). A key belongs to
the first node found by walking clockwise from the key's position. When a node leaves, only the keys in
its <span class="term" data-term="arc">arc</span> reassign — to the next node round — and everything else
stays put.
</p>
<div class="demo">
<div class="demo-grid">
<svg class="ring" id="ring" viewBox="0 0 260 260"></svg>
<div class="controls">
<div class="row">
<label>nodes</label>
<input id="nSlider" type="range" min="2" max="8" value="4">
<span class="val" id="nVal">4</span>
</div>
<div class="row">
<label>keys</label>
<input id="kSlider" type="range" min="10" max="60" value="32" step="2">
<span class="val" id="kVal">32</span>
</div>
<div>
<button id="rm">remove a node</button>
<button id="add">add a node</button>
<button id="reset">reset</button>
</div>
<div class="readout" id="readout">
<b>4</b> nodes · <b>32</b> keys · <span class="moved"></span> moved on last change
</div>
</div>
</div>
</div>
<p style="font-size:13px; color:var(--gray-500); max-width:640px;">
Colored arcs show ownership. Removing a node hands its arc to its clockwise neighbor; every dot outside
that arc keeps its color. That's the whole idea.
</p>
<h2>Versus <code>mod N</code></h2>
<table>
<thead><tr><th></th><th>hash mod N</th><th>consistent hashing</th></tr></thead>
<tbody>
<tr><td>Keys moved when N→N+1</td><td class="bad">~ (N1)/N of all keys</td><td class="good">~ 1/(N+1)</td></tr>
<tr><td>Hot-spot risk</td><td class="good">even by construction</td><td>uneven — fix with <span class="term" data-term="vnode">virtual nodes</span></td></tr>
<tr><td>Lookup cost</td><td class="good">O(1)</td><td>O(log N) (binary search on ring)</td></tr>
<tr><td>Used by</td><td>array sharding, simple LB</td><td>Dynamo, Cassandra, Memcached clients, Envoy</td></tr>
</tbody>
</table>
<h2>Where you'll meet it</h2>
<p>
Any time you're spreading state across a pool that changes size: cache fleets, partitioned queues, object
storage, request routing with sticky sessions. The
<span class="term" data-term="vnode">virtual-node</span> variant (each physical node owns many small arcs
instead of one big one) is what production systems actually run, because it smooths out load and makes
rebalancing even gentler.
</p>
</main>
<!-- glossary -->
<aside>
<div class="label">Glossary</div>
<dl id="gloss">
<dt data-g="ring">Ring</dt>
<dd>The hash function's output range, treated as a circle so the value after <code>max</code> is <code>0</code>.</dd>
<dt data-g="node">Node</dt>
<dd>A server placed on the ring at <code>hash(node_id)</code>. Owns every key between it and its anticlockwise neighbor.</dd>
<dt data-g="arc">Arc</dt>
<dd>The stretch of ring a node owns. Removing a node merges its arc into the next node's.</dd>
<dt data-g="vnode">Virtual node</dt>
<dd>Placing each physical node at many ring positions so arcs are small and evenly sized.</dd>
<dt data-g="successor">Successor</dt>
<dd>The first node clockwise from a given point — the owner of any key landing there.</dd>
</dl>
</aside>
</div>
<script>
/* ── tiny deterministic hash so the demo is stable ── */
function h(s) {
let x = 2166136261;
for (let i = 0; i < s.length; i++) { x ^= s.charCodeAt(i); x = (x * 16777619) >>> 0; }
return x / 4294967296;
}
const COLORS = ["#D97757", "#788C5D", "#6A8CAF", "#C2A83E", "#B04A3F", "#87867F", "#3D6E6E", "#A67C52"];
const CX = 130, CY = 130, R = 100, RKEY = 78;
const TAU = Math.PI * 2;
const ring = document.getElementById("ring");
const nSlider = document.getElementById("nSlider");
const kSlider = document.getElementById("kSlider");
const nVal = document.getElementById("nVal");
const kVal = document.getElementById("kVal");
const readout = document.getElementById("readout");
let nodes = [], keys = [], lastOwner = {};
function pt(r, t) { return [CX + r * Math.sin(t * TAU), CY - r * Math.cos(t * TAU)]; }
function ownerOf(t) {
const sorted = [...nodes].sort((a, b) => a.t - b.t);
for (const n of sorted) if (n.t >= t) return n;
return sorted[0];
}
function buildNodes(n) {
nodes = [];
for (let i = 0; i < n; i++) nodes.push({ id: "n" + i, t: h("node-" + i), color: COLORS[i % COLORS.length] });
}
function buildKeys(k) {
keys = [];
for (let i = 0; i < k; i++) keys.push({ id: "k" + i, t: h("key-" + i) });
}
function arcPath(t0, t1) {
const [x0, y0] = pt(R, t0), [x1, y1] = pt(R, t1);
let d = t1 - t0; if (d <= 0) d += 1;
const large = d > 0.5 ? 1 : 0;
return `M ${x0} ${y0} A ${R} ${R} 0 ${large} 1 ${x1} ${y1}`;
}
function render(moved) {
const sorted = [...nodes].sort((a, b) => a.t - b.t);
let svg = `<circle class="track" cx="${CX}" cy="${CY}" r="${R}"/>`;
for (let i = 0; i < sorted.length; i++) {
const cur = sorted[i];
const prev = sorted[(i - 1 + sorted.length) % sorted.length];
svg += `<path class="arc" stroke="${cur.color}" d="${arcPath(prev.t, cur.t)}"/>`;
}
for (const k of keys) {
const o = ownerOf(k.t);
const [x, y] = pt(RKEY, k.t);
svg += `<circle class="key" r="3.5" cx="${x}" cy="${y}" fill="${o.color}"/>`;
}
for (const n of sorted) {
const [x, y] = pt(R, n.t);
svg += `<circle class="node" r="9" cx="${x}" cy="${y}" fill="${n.color}"/>`;
}
svg += `<text class="lbl" x="${CX}" y="18" text-anchor="middle">0</text>`;
ring.innerHTML = svg;
readout.innerHTML =
`<b>${nodes.length}</b> nodes · <b>${keys.length}</b> keys · ` +
`<span class="moved">${moved == null ? "—" : moved + " (" + Math.round(moved / keys.length * 100) + "%)"}</span> moved on last change`;
}
function diffAndRender() {
let moved = 0;
for (const k of keys) {
const o = ownerOf(k.t).id;
if (lastOwner[k.id] && lastOwner[k.id] !== o) moved++;
lastOwner[k.id] = o;
}
render(moved);
}
function reset() {
buildNodes(+nSlider.value);
buildKeys(+kSlider.value);
lastOwner = {};
for (const k of keys) lastOwner[k.id] = ownerOf(k.t).id;
nVal.textContent = nSlider.value;
kVal.textContent = kSlider.value;
render(null);
}
nSlider.oninput = () => { nVal.textContent = nSlider.value; buildNodes(+nSlider.value); diffAndRender(); };
kSlider.oninput = () => { kVal.textContent = kSlider.value; buildKeys(+kSlider.value); lastOwner = {}; for (const k of keys) lastOwner[k.id] = ownerOf(k.t).id; render(null); };
document.getElementById("rm").onclick = () => { if (nodes.length > 2) { nodes.splice(Math.floor(Math.random() * nodes.length), 1); diffAndRender(); } };
document.getElementById("add").onclick = () => { if (nodes.length < 12) { const i = nodes.length; nodes.push({ id: "n" + Date.now(), t: Math.random(), color: COLORS[i % COLORS.length] }); diffAndRender(); } };
document.getElementById("reset").onclick = reset;
/* glossary highlight */
document.querySelectorAll(".term").forEach(el => {
const g = el.dataset.term;
el.addEventListener("mouseenter", () => document.querySelector(`dt[data-g="${g}"]`)?.classList.add("hl"));
el.addEventListener("mouseleave", () => document.querySelector(`dt[data-g="${g}"]`)?.classList.remove("hl"));
});
reset();
</script>
</body>
</html>
+703
View File
@@ -0,0 +1,703 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Implementation plan — Comment threads on task cards</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-150: #F0EEE6;
--gray-300: #D1CFC5;
--gray-500: #87867F;
--gray-700: #3D3D3A;
--white: #FFFFFF;
--serif: ui-serif, Georgia, 'Times New Roman', serif;
--sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--sans);
background: var(--ivory);
color: var(--gray-700);
line-height: 1.55;
padding: 56px 32px 120px;
-webkit-font-smoothing: antialiased;
}
.page { max-width: 1120px; margin: 0 auto; }
/* ---------- header ---------- */
header.page-head { margin-bottom: 48px; max-width: 820px; }
.eyebrow {
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 12px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 38px;
line-height: 1.15;
color: var(--slate);
margin-bottom: 18px;
letter-spacing: -0.01em;
}
.prompt-box {
background: var(--gray-150);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
padding: 16px 20px;
font-size: 14.5px;
color: var(--gray-700);
}
.prompt-box .label {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
display: block;
margin-bottom: 6px;
}
/* ---------- section chrome ---------- */
section { margin-bottom: 64px; }
.sec-head {
display: flex;
align-items: baseline;
gap: 14px;
margin-bottom: 8px;
}
.sec-head .num {
font-family: var(--mono);
font-size: 12px;
background: var(--oat);
color: var(--slate);
padding: 3px 9px;
border-radius: 8px;
}
.sec-head h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 26px;
color: var(--slate);
letter-spacing: -0.01em;
}
.sec-intro {
font-size: 14.5px;
color: var(--gray-500);
max-width: 720px;
margin-bottom: 28px;
}
/* ---------- summary strip ---------- */
.summary {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 64px;
}
@media (max-width: 900px) { .summary { grid-template-columns: repeat(2, 1fr); } }
.summary .cell {
background: var(--white);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
padding: 18px 20px;
}
.summary .k {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
margin-bottom: 6px;
}
.summary .v {
font-size: 17px;
color: var(--slate);
font-weight: 600;
}
.summary .v.accent { color: var(--clay); }
/* ---------- milestone timeline ---------- */
.milestones { display: flex; flex-direction: column; gap: 0; }
.milestone {
display: grid;
grid-template-columns: 120px 28px 1fr;
gap: 0 18px;
position: relative;
}
.milestone .when {
text-align: right;
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
padding-top: 4px;
}
.milestone .dot-col {
display: flex;
flex-direction: column;
align-items: center;
}
.milestone .dot {
width: 14px; height: 14px;
border-radius: 50%;
background: var(--white);
border: 3px solid var(--clay);
margin-top: 4px;
flex-shrink: 0;
}
.milestone .dot.done { background: var(--olive); border-color: var(--olive); }
.milestone .line {
width: 2px;
flex: 1;
background: var(--gray-300);
margin: 4px 0;
}
.milestone:last-child .line { display: none; }
.milestone .body { padding-bottom: 36px; }
.milestone .body h3 {
font-family: var(--serif);
font-weight: 500;
font-size: 19px;
color: var(--slate);
margin-bottom: 4px;
}
.milestone .body p { font-size: 14px; color: var(--gray-500); margin-bottom: 10px; max-width: 620px; }
.milestone .tags { display: flex; gap: 8px; flex-wrap: wrap; }
.milestone .tag {
font-family: var(--mono);
font-size: 11.5px;
background: var(--gray-150);
border: 1px solid var(--gray-300);
border-radius: 6px;
padding: 3px 8px;
color: var(--gray-700);
}
/* ---------- data flow diagram ---------- */
.diagram {
background: var(--white);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
padding: 28px;
overflow-x: auto;
}
.diagram svg { display: block; min-width: 760px; }
.diagram text { font-family: var(--mono); }
.caption { font-size: 13px; color: var(--gray-500); margin-top: 12px; }
/* ---------- mockups ---------- */
.mocks {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 28px;
}
@media (max-width: 900px) { .mocks { grid-template-columns: 1fr; } }
.mock {
background: var(--white);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
overflow: hidden;
}
.mock .mock-label {
padding: 12px 18px;
border-bottom: 1.5px solid var(--gray-300);
font-family: var(--mono);
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
background: var(--gray-150);
}
.mock .mock-body { padding: 22px; }
/* task card mock */
.card-mock {
border: 1.5px solid var(--gray-300);
border-radius: 10px;
padding: 16px;
background: var(--ivory);
}
.card-mock .cm-title { font-weight: 600; font-size: 15px; color: var(--slate); margin-bottom: 4px; }
.card-mock .cm-meta { font-size: 12px; color: var(--gray-500); margin-bottom: 14px; }
.card-mock .cm-divider { border-top: 1px dashed var(--gray-300); margin: 14px 0; }
.thread { display: flex; flex-direction: column; gap: 12px; }
.comment { display: flex; gap: 10px; }
.comment .avatar {
width: 26px; height: 26px; border-radius: 50%;
background: var(--oat);
flex-shrink: 0;
display: flex; align-items: center; justify-content: center;
font-size: 11px; font-weight: 600; color: var(--slate);
}
.comment .avatar.alt { background: var(--olive); color: var(--white); }
.comment .bubble { flex: 1; }
.comment .author { font-size: 12px; font-weight: 600; color: var(--slate); }
.comment .author .time { font-weight: 400; color: var(--gray-500); margin-left: 6px; }
.comment .text { font-size: 13px; color: var(--gray-700); margin-top: 2px; }
.comment .reply-link { font-size: 11.5px; color: var(--clay); margin-top: 4px; cursor: default; }
.comment.nested { margin-left: 36px; }
.composer {
display: flex; gap: 10px; align-items: center;
margin-top: 6px;
}
.composer .field {
flex: 1;
border: 1.5px solid var(--gray-300);
border-radius: 8px;
padding: 8px 12px;
font-size: 13px;
color: var(--gray-500);
background: var(--white);
}
.composer .send {
background: var(--clay);
color: var(--white);
border-radius: 8px;
padding: 8px 14px;
font-size: 13px;
font-weight: 600;
}
/* sidebar digest mock */
.digest { display: flex; flex-direction: column; gap: 10px; }
.digest .row {
display: flex; gap: 10px; align-items: flex-start;
padding: 10px 12px;
background: var(--ivory);
border: 1.5px solid var(--gray-300);
border-radius: 8px;
}
.digest .row.unread { border-left: 3px solid var(--clay); }
.digest .mini-av {
width: 22px; height: 22px; border-radius: 50%;
background: var(--oat);
flex-shrink: 0;
font-size: 10px; font-weight: 600; color: var(--slate);
display: flex; align-items: center; justify-content: center;
}
.digest .txt { font-size: 12.5px; color: var(--gray-700); line-height: 1.45; }
.digest .txt strong { color: var(--slate); }
.digest .txt .on { color: var(--gray-500); }
/* ---------- code panel ---------- */
.code-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 28px; }
@media (max-width: 980px) { .code-grid { grid-template-columns: 1fr; } }
.code-block { display: flex; flex-direction: column; gap: 10px; }
.code-block .file-label {
font-family: var(--mono);
font-size: 12px;
color: var(--gray-500);
}
.code {
background: var(--slate);
border-radius: 12px;
padding: 18px 20px;
overflow-x: auto;
flex: 1;
}
.code pre {
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.65;
color: #E8E6DE;
white-space: pre;
}
.code .kw { color: var(--clay); }
.code .str { color: var(--olive); }
.code .cm { color: var(--gray-500); }
.code .fn { color: #C9B98A; }
/* ---------- risk table ---------- */
.risks {
border: 1.5px solid var(--gray-300);
border-radius: 12px;
overflow: hidden;
background: var(--white);
}
.risks .row {
display: grid;
grid-template-columns: 1.6fr 90px 1.6fr;
gap: 0;
}
@media (max-width: 780px) { .risks .row { grid-template-columns: 1fr; } }
.risks .row + .row { border-top: 1.5px solid var(--gray-300); }
.risks .cell { padding: 14px 18px; font-size: 13.5px; }
.risks .cell + .cell { border-left: 1.5px solid var(--gray-300); }
@media (max-width: 780px) { .risks .cell + .cell { border-left: none; border-top: 1px dashed var(--gray-300); } }
.risks .head { background: var(--gray-150); font-weight: 600; color: var(--slate); font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; }
.sev {
display: inline-block;
font-family: var(--mono);
font-size: 11px;
padding: 2px 8px;
border-radius: 6px;
font-weight: 600;
}
.sev.high { background: #F3D9CC; color: #8A3B1E; }
.sev.med { background: var(--oat); color: var(--slate); }
.sev.low { background: #E4E9DC; color: #4B5C39; }
/* ---------- open questions ---------- */
.open-q { display: flex; flex-direction: column; gap: 14px; max-width: 820px; }
.q {
background: var(--white);
border: 1.5px solid var(--gray-300);
border-left: 4px solid var(--clay);
border-radius: 10px;
padding: 16px 20px;
}
.q .qt { font-weight: 600; font-size: 15px; color: var(--slate); margin-bottom: 4px; }
.q .qd { font-size: 13.5px; color: var(--gray-500); }
.q .owner {
font-family: var(--mono);
font-size: 11.5px;
color: var(--gray-500);
margin-top: 8px;
}
</style>
</head>
<body>
<div class="page">
<header class="page-head">
<div class="eyebrow">Implementation plan · Acme web client</div>
<h1>Comment threads on task cards</h1>
<div class="prompt-box">
<span class="label">Prompt</span>
Create a thorough implementation plan for adding threaded comments to
task cards. Include mockups, the data flow from client to persistence,
the key code I'll need to write, and a risk table. Make it easy to
skim on a phone — I'm going to pass this to the implementer as-is.
</div>
</header>
<!-- ============================================================= -->
<div class="summary">
<div class="cell"><div class="k">Effort</div><div class="v accent">~2 weeks</div></div>
<div class="cell"><div class="k">Surfaces touched</div><div class="v">3 packages</div></div>
<div class="cell"><div class="k">New tables</div><div class="v">2</div></div>
<div class="cell"><div class="k">Feature flag</div><div class="v">task_comments_v1</div></div>
</div>
<!-- ============================================================= -->
<section>
<div class="sec-head"><span class="num">01</span><h2>Milestones</h2></div>
<p class="sec-intro">Ship in four slices, each independently reviewable and each behind the flag. Nothing is user-visible until slice&nbsp;4.</p>
<div class="milestones">
<div class="milestone">
<div class="when">Week&nbsp;1 · MonTue</div>
<div class="dot-col"><span class="dot done"></span><span class="line"></span></div>
<div class="body">
<h3>Schema &amp; API contract</h3>
<p>New <code>comments</code> and <code>comment_reads</code> tables, migrations, and the tRPC router stubs. No UI. Contract reviewed before anything else lands.</p>
<div class="tags"><span class="tag">packages/db</span><span class="tag">packages/api</span><span class="tag">migration 0042</span></div>
</div>
</div>
<div class="milestone">
<div class="when">Week&nbsp;1 · WedFri</div>
<div class="dot-col"><span class="dot"></span><span class="line"></span></div>
<div class="body">
<h3>Thread component &amp; composer</h3>
<p>Static <code>&lt;CommentThread&gt;</code> rendered from fixtures. Optimistic insert on submit, rollback on failure, one level of nesting only.</p>
<div class="tags"><span class="tag">apps/web</span><span class="tag">storybook</span></div>
</div>
</div>
<div class="milestone">
<div class="when">Week&nbsp;2 · MonWed</div>
<div class="dot-col"><span class="dot"></span><span class="line"></span></div>
<div class="body">
<h3>Realtime fan-out &amp; unread state</h3>
<p>Subscribe the open card to its comment channel. Track per-user read cursors so the sidebar can show an unread count without a second query.</p>
<div class="tags"><span class="tag">packages/realtime</span><span class="tag">apps/web</span></div>
</div>
</div>
<div class="milestone">
<div class="when">Week&nbsp;2 · ThuFri</div>
<div class="dot-col"><span class="dot"></span><span class="line"></span></div>
<div class="body">
<h3>Notifications, flag ramp, docs</h3>
<p>Mention detection → notification row, email digest fallback, ramp <code>task_comments_v1</code> to internal, then 10% → 100% over three days.</p>
<div class="tags"><span class="tag">packages/notify</span><span class="tag">growthbook</span></div>
</div>
</div>
</div>
</section>
<!-- ============================================================= -->
<section>
<div class="sec-head"><span class="num">02</span><h2>Data flow</h2></div>
<p class="sec-intro">Optimistic write path on the left, fan-out on the right. The read cursor update is fire-and-forget — we never block the thread render on it.</p>
<div class="diagram">
<svg viewBox="0 0 860 340" xmlns="http://www.w3.org/2000/svg">
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#87867F"/>
</marker>
<marker id="arrowClay" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M0,0 L10,5 L0,10 z" fill="#D97757"/>
</marker>
</defs>
<!-- boxes -->
<g font-size="12" fill="#141413">
<rect x="20" y="20" width="180" height="54" rx="10" fill="#FFFFFF" stroke="#D1CFC5" stroke-width="1.5"/>
<text x="110" y="43" text-anchor="middle" font-weight="600">&lt;CommentComposer&gt;</text>
<text x="110" y="60" text-anchor="middle" fill="#87867F" font-size="10.5">apps/web</text>
<rect x="20" y="150" width="180" height="54" rx="10" fill="#FFFFFF" stroke="#D1CFC5" stroke-width="1.5"/>
<text x="110" y="173" text-anchor="middle" font-weight="600">React Query cache</text>
<text x="110" y="190" text-anchor="middle" fill="#87867F" font-size="10.5">optimistic insert</text>
<rect x="340" y="150" width="180" height="54" rx="10" fill="#FFFFFF" stroke="#D1CFC5" stroke-width="1.5"/>
<text x="430" y="173" text-anchor="middle" font-weight="600">comments.create</text>
<text x="430" y="190" text-anchor="middle" fill="#87867F" font-size="10.5">tRPC · packages/api</text>
<rect x="340" y="266" width="180" height="54" rx="10" fill="#141413" stroke="#141413"/>
<text x="430" y="289" text-anchor="middle" font-weight="600" fill="#FAF9F5">comments table</text>
<text x="430" y="306" text-anchor="middle" fill="#C9B98A" font-size="10.5">postgres · packages/db</text>
<rect x="660" y="150" width="180" height="54" rx="10" fill="#FFFFFF" stroke="#D1CFC5" stroke-width="1.5"/>
<text x="750" y="173" text-anchor="middle" font-weight="600">realtime channel</text>
<text x="750" y="190" text-anchor="middle" fill="#87867F" font-size="10.5">task:{id}:comments</text>
<rect x="660" y="20" width="180" height="54" rx="10" fill="#FFFFFF" stroke="#D1CFC5" stroke-width="1.5"/>
<text x="750" y="43" text-anchor="middle" font-weight="600">Other viewers</text>
<text x="750" y="60" text-anchor="middle" fill="#87867F" font-size="10.5">subscribed cards</text>
<rect x="660" y="266" width="180" height="54" rx="10" fill="#FFFFFF" stroke="#D1CFC5" stroke-width="1.5"/>
<text x="750" y="289" text-anchor="middle" font-weight="600">notify worker</text>
<text x="750" y="306" text-anchor="middle" fill="#87867F" font-size="10.5">@mentions → queue</text>
</g>
<!-- arrows -->
<g stroke="#87867F" stroke-width="1.5" fill="none">
<path d="M110 74 L110 150" marker-end="url(#arrow)"/>
<path d="M200 177 L340 177" marker-end="url(#arrow)"/>
<path d="M430 204 L430 266" marker-end="url(#arrow)"/>
<path d="M520 293 L660 293" marker-end="url(#arrow)"/>
</g>
<g stroke="#D97757" stroke-width="1.5" fill="none" stroke-dasharray="5 4">
<path d="M520 177 L660 177" marker-end="url(#arrowClay)"/>
<path d="M750 150 L750 74" marker-end="url(#arrowClay)"/>
<path d="M660 162 C 540 120, 280 120, 205 162" marker-end="url(#arrowClay)"/>
</g>
<!-- edge labels -->
<g font-size="10.5" fill="#87867F">
<text x="118" y="118">submit (id=temp)</text>
<text x="240" y="170">mutate</text>
<text x="438" y="240">INSERT + read cursor</text>
<text x="548" y="286">enqueue</text>
<text x="548" y="172" fill="#D97757">broadcast row</text>
<text x="758" y="118" fill="#D97757">live append</text>
<text x="360" y="112" fill="#D97757">reconcile temp id → real id</text>
</g>
</svg>
<p class="caption">Solid = request/response path. Dashed clay = realtime fan-out. The composer never waits on the dashed path.</p>
</div>
</section>
<!-- ============================================================= -->
<section>
<div class="sec-head"><span class="num">03</span><h2>Mockups</h2></div>
<p class="sec-intro">Not pixel-final — just enough that the reviewer and I agree on nesting depth, composer placement, and what the sidebar digest looks like.</p>
<div class="mocks">
<div class="mock">
<div class="mock-label">A · Thread inside an open task card</div>
<div class="mock-body">
<div class="card-mock">
<div class="cm-title">Ship onboarding empty-state rewrite</div>
<div class="cm-meta">BIR-1142 · Assigned to Priya · Due Fri</div>
<div class="cm-divider"></div>
<div class="thread">
<div class="comment">
<div class="avatar">JM</div>
<div class="bubble">
<div class="author">Jonah M. <span class="time">2h ago</span></div>
<div class="text">Should the illustration swap when the workspace already has one project? Feels odd to show the "start here" art twice.</div>
<div class="reply-link">Reply</div>
</div>
</div>
<div class="comment nested">
<div class="avatar alt">PS</div>
<div class="bubble">
<div class="author">Priya S. <span class="time">40m ago</span></div>
<div class="text">Good catch — I'll gate it on <code>projects.count &gt; 0</code> and fall back to the minimal variant.</div>
</div>
</div>
<div class="composer">
<div class="field">Add a comment…</div>
<div class="send">Post</div>
</div>
</div>
</div>
</div>
</div>
<div class="mock">
<div class="mock-label">B · Sidebar unread digest</div>
<div class="mock-body">
<div class="digest">
<div class="row unread">
<div class="mini-av">JM</div>
<div class="txt"><strong>Jonah</strong> commented <span class="on">on BIR-1142</span> — "Should the illustration swap when…"</div>
</div>
<div class="row unread">
<div class="mini-av">AK</div>
<div class="txt"><strong>Aiko</strong> mentioned you <span class="on">on BIR-1098</span> — "@priya can you confirm the copy here?"</div>
</div>
<div class="row">
<div class="mini-av">RW</div>
<div class="txt"><strong>Rowan</strong> replied <span class="on">on BIR-0971</span> — "Merged, thanks for the quick turnaround."</div>
</div>
</div>
</div>
</div>
</div>
</section>
<!-- ============================================================= -->
<section>
<div class="sec-head"><span class="num">04</span><h2>Key code</h2></div>
<p class="sec-intro">The two pieces most likely to be done wrong: the migration (soft deletes, read cursors) and the optimistic mutation (temp-id reconciliation).</p>
<div class="code-grid">
<div class="code-block">
<div class="file-label">packages/db/migrations/0042_comments.sql</div>
<div class="code"><pre><span class="kw">create table</span> <span class="fn">comments</span> (
id uuid <span class="kw">primary key default</span> gen_random_uuid(),
task_id uuid <span class="kw">not null references</span> tasks(id),
parent_id uuid <span class="kw">references</span> comments(id), <span class="cm">-- one level only,</span>
<span class="cm">-- enforced in API</span>
author_id uuid <span class="kw">not null references</span> users(id),
body text <span class="kw">not null</span>,
created_at timestamptz <span class="kw">not null default</span> now(),
deleted_at timestamptz <span class="cm">-- soft delete</span>
);
<span class="kw">create table</span> <span class="fn">comment_reads</span> (
task_id uuid <span class="kw">not null references</span> tasks(id),
user_id uuid <span class="kw">not null references</span> users(id),
read_up_to timestamptz <span class="kw">not null</span>,
<span class="kw">primary key</span> (task_id, user_id)
);
<span class="kw">create index</span> <span class="fn">comments_task_created</span>
<span class="kw">on</span> comments (task_id, created_at);</pre></div>
</div>
<div class="code-block">
<div class="file-label">apps/web/hooks/useAddComment.ts</div>
<div class="code"><pre><span class="kw">export function</span> <span class="fn">useAddComment</span>(taskId: string) {
<span class="kw">const</span> qc = useQueryClient();
<span class="kw">return</span> trpc.comments.create.<span class="fn">useMutation</span>({
<span class="fn">onMutate</span>: <span class="kw">async</span> (input) =&gt; {
<span class="kw">const</span> temp = { ...input, id: <span class="str">`temp-${nanoid()}`</span>,
createdAt: <span class="kw">new</span> Date(), pending: <span class="kw">true</span> };
qc.<span class="fn">setQueryData</span>(key(taskId), (prev) =&gt;
[...(prev ?? []), temp]);
<span class="kw">return</span> { tempId: temp.id };
},
<span class="fn">onSuccess</span>: (row, _v, ctx) =&gt; {
<span class="cm">// reconcile temp id → real id so the</span>
<span class="cm">// realtime append doesn't duplicate it</span>
qc.<span class="fn">setQueryData</span>(key(taskId), (prev) =&gt;
prev.map((c) =&gt; c.id === ctx.tempId ? row : c));
},
<span class="fn">onError</span>: (_e, _v, ctx) =&gt; {
qc.<span class="fn">setQueryData</span>(key(taskId), (prev) =&gt;
prev.filter((c) =&gt; c.id !== ctx.tempId));
},
});
}</pre></div>
</div>
</div>
</section>
<!-- ============================================================= -->
<section>
<div class="sec-head"><span class="num">05</span><h2>Risks &amp; mitigations</h2></div>
<div class="risks">
<div class="row">
<div class="cell head">Risk</div>
<div class="cell head">Sev</div>
<div class="cell head">Mitigation</div>
</div>
<div class="row">
<div class="cell">Realtime duplicate: socket append races with the HTTP response and the temp-id reconcile.</div>
<div class="cell"><span class="sev high">HIGH</span></div>
<div class="cell">Dedupe on server-assigned <code>id</code> in the cache updater; socket payload carries the real id, temp rows are filtered on reconcile.</div>
</div>
<div class="row">
<div class="cell">Unread counts go stale when a user reads the thread on another device.</div>
<div class="cell"><span class="sev med">MED</span></div>
<div class="cell">Broadcast <code>comment_reads</code> upserts on the same channel; client treats its own cursor as max(local, remote).</div>
</div>
<div class="row">
<div class="cell">Mention detection false-positives on pasted markdown (<code>@media</code>, <code>@2x</code>).</div>
<div class="cell"><span class="sev low">LOW</span></div>
<div class="cell">Resolve mentions against workspace members only, at write time, and store the resolved user ids — never re-parse on read.</div>
</div>
</div>
</section>
<!-- ============================================================= -->
<section>
<div class="sec-head"><span class="num">06</span><h2>Open questions</h2></div>
<div class="open-q">
<div class="q">
<div class="qt">Do we allow editing, or only delete-and-repost?</div>
<div class="qd">Editing needs an <code>edited_at</code> column and an "edited" affordance. Delete-and-repost is simpler but loses the reply anchor. Leaning toward delete-only for v1.</div>
<div class="owner">Decide with · design, before slice 2</div>
</div>
<div class="q">
<div class="qt">Email digest cadence when a user has the app closed</div>
<div class="qd">Immediate-per-mention will be noisy. Proposal: batch on a 15-minute window, collapse to one email per task, and respect quiet hours from the existing settings table.</div>
<div class="owner">Decide with · platform, before slice 4</div>
</div>
</div>
</section>
</div>
</body>
</html>
+596
View File
@@ -0,0 +1,596 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PR #312 — Move notification delivery onto a queue</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--oat: #E3DACC;
--olive: #788C5D;
--gray-150: #F0EEE6;
--gray-300: #D1CFC5;
--gray-500: #87867F;
--gray-700: #3D3D3A;
--white: #FFFFFF;
--serif: ui-serif, Georgia, 'Times New Roman', serif;
--sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--mono: ui-monospace, 'SF Mono', Menlo, Monaco, monospace;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--sans);
background: var(--ivory);
color: var(--gray-700);
line-height: 1.55;
padding: 56px 32px 120px;
-webkit-font-smoothing: antialiased;
}
.page { max-width: 1040px; margin: 0 auto; }
/* ---------- header ---------- */
header.page-head { margin-bottom: 44px; max-width: 820px; }
.eyebrow {
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 12px;
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 36px;
line-height: 1.15;
color: var(--slate);
margin-bottom: 10px;
letter-spacing: -0.01em;
}
.pr-meta {
display: flex;
flex-wrap: wrap;
gap: 18px;
font-family: var(--mono);
font-size: 12.5px;
color: var(--gray-500);
margin-bottom: 18px;
}
.pr-meta .stat strong { color: var(--slate); font-weight: 600; }
.pr-meta .add { color: var(--olive); }
.pr-meta .del { color: var(--clay); }
.prompt-box {
background: var(--gray-150);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
padding: 16px 20px;
font-size: 14.5px;
color: var(--gray-700);
}
.prompt-box .label {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
display: block;
margin-bottom: 6px;
}
/* ---------- layout ---------- */
.layout {
display: grid;
grid-template-columns: 1fr 240px;
gap: 48px;
align-items: start;
}
@media (max-width: 900px) { .layout { grid-template-columns: 1fr; } .toc { display: none; } }
section { margin-bottom: 56px; scroll-margin-top: 24px; }
h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 25px;
color: var(--slate);
margin-bottom: 14px;
letter-spacing: -0.01em;
}
p.lede { font-size: 15.5px; max-width: 680px; margin-bottom: 12px; }
p + p.lede { margin-top: 12px; }
.toc {
position: sticky;
top: 32px;
border-left: 1.5px solid var(--gray-300);
padding-left: 18px;
font-size: 13px;
}
.toc .t-head {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
margin-bottom: 10px;
}
.toc a { display: block; color: var(--gray-700); text-decoration: none; padding: 4px 0; }
.toc a:hover { color: var(--clay); }
.toc .sub { padding-left: 12px; color: var(--gray-500); font-size: 12.5px; }
/* ---------- TL;DR ---------- */
.tldr {
background: var(--white);
border: 1.5px solid var(--gray-300);
border-left: 4px solid var(--clay);
border-radius: 12px;
padding: 20px 24px;
margin-bottom: 48px;
max-width: 760px;
}
.tldr .k {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
margin-bottom: 8px;
}
.tldr p { font-size: 15px; color: var(--slate); }
/* ---------- before/after ---------- */
.ba {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
margin: 22px 0 4px;
}
@media (max-width: 720px) { .ba { grid-template-columns: 1fr; } }
.ba .panel {
background: var(--white);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
padding: 18px 20px;
}
.ba .panel.after { border-color: var(--olive); }
.ba .k {
font-family: var(--mono);
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--gray-500);
margin-bottom: 10px;
}
.ba .after .k { color: var(--olive); }
.ba ul { list-style: none; display: flex; flex-direction: column; gap: 8px; font-size: 13.5px; }
.ba li::before { content: '·'; color: var(--gray-500); margin-right: 8px; }
.ba .after li::before { color: var(--olive); }
/* ---------- file tour ---------- */
.file-tour { display: flex; flex-direction: column; gap: 22px; }
.file {
background: var(--white);
border: 1.5px solid var(--gray-300);
border-radius: 12px;
overflow: hidden;
}
.file summary {
padding: 14px 20px;
cursor: pointer;
display: flex;
align-items: center;
gap: 12px;
list-style: none;
background: var(--gray-150);
border-bottom: 1.5px solid var(--gray-300);
}
.file[open] summary { border-bottom-color: var(--gray-300); }
.file:not([open]) summary { border-bottom: none; }
.file summary::-webkit-details-marker { display: none; }
.file summary .chev {
width: 8px; height: 8px;
border-right: 2px solid var(--gray-500);
border-bottom: 2px solid var(--gray-500);
transform: rotate(-45deg);
transition: transform .15s ease;
flex-shrink: 0;
}
.file[open] summary .chev { transform: rotate(45deg); }
.file summary .path {
font-family: var(--mono);
font-size: 13px;
color: var(--slate);
flex: 1;
}
.file summary .badge {
font-family: var(--mono);
font-size: 10.5px;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 2px 8px;
border-radius: 6px;
font-weight: 600;
}
.badge.new { background: #E4E9DC; color: #4B5C39; }
.badge.mod { background: var(--oat); color: var(--slate); }
.badge.del { background: #F3D9CC; color: #8A3B1E; }
.file summary .stat { font-family: var(--mono); font-size: 12px; }
.file summary .stat .add { color: var(--olive); }
.file summary .stat .del { color: var(--clay); }
.file .file-body { padding: 18px 20px 22px; }
.file .why {
font-size: 14.5px;
margin-bottom: 14px;
max-width: 680px;
}
.file .why strong { color: var(--slate); }
/* ---------- code ---------- */
.code {
background: var(--slate);
border-radius: 10px;
padding: 16px 18px;
overflow-x: auto;
}
.code pre {
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.65;
color: #E8E6DE;
white-space: pre;
}
.code .kw { color: var(--clay); }
.code .str { color: var(--olive); }
.code .cm { color: var(--gray-500); }
.code .fn { color: #C9B98A; }
.code .add { background: rgba(120,140,93,.22); display: inline-block; width: 100%; }
.code .del { background: rgba(217,119,87,.18); display: inline-block; width: 100%; text-decoration: line-through; text-decoration-color: rgba(217,119,87,.6); }
/* ---------- review focus ---------- */
.focus { display: flex; flex-direction: column; gap: 14px; max-width: 760px; }
.focus .item {
background: var(--white);
border: 1.5px solid var(--gray-300);
border-radius: 10px;
padding: 16px 20px;
display: flex;
gap: 16px;
}
.focus .n {
font-family: var(--mono);
font-size: 13px;
font-weight: 600;
color: var(--white);
background: var(--clay);
width: 26px; height: 26px;
border-radius: 50%;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
margin-top: 2px;
}
.focus .t { font-weight: 600; color: var(--slate); font-size: 15px; margin-bottom: 2px; }
.focus .d { font-size: 13.5px; color: var(--gray-500); }
.focus .d code { font-size: 12.5px; }
/* ---------- test plan ---------- */
.tests { display: flex; flex-direction: column; gap: 10px; max-width: 760px; }
.test {
display: flex;
align-items: flex-start;
gap: 12px;
background: var(--white);
border: 1.5px solid var(--gray-300);
border-radius: 10px;
padding: 13px 18px;
font-size: 14px;
}
.test .check {
width: 18px; height: 18px;
border-radius: 5px;
flex-shrink: 0;
margin-top: 2px;
border: 1.5px solid var(--gray-300);
}
.test.done .check {
background: var(--olive);
border-color: var(--olive);
position: relative;
}
.test.done .check::after {
content: '';
position: absolute;
left: 5px; top: 2px;
width: 5px; height: 9px;
border-right: 2px solid var(--white);
border-bottom: 2px solid var(--white);
transform: rotate(40deg);
}
.test .label { color: var(--slate); }
.test .note { color: var(--gray-500); font-size: 13px; margin-top: 2px; }
/* ---------- rollout ---------- */
.rollout { display: flex; gap: 0; max-width: 780px; }
@media (max-width: 720px) { .rollout { flex-direction: column; } }
.rollout .step {
flex: 1;
background: var(--white);
border: 1.5px solid var(--gray-300);
padding: 16px 18px;
position: relative;
}
.rollout .step:first-child { border-radius: 12px 0 0 12px; }
.rollout .step:last-child { border-radius: 0 12px 12px 0; }
.rollout .step + .step { border-left: none; }
@media (max-width: 720px) {
.rollout .step, .rollout .step:first-child, .rollout .step:last-child { border-radius: 12px; }
.rollout .step + .step { border-left: 1.5px solid var(--gray-300); margin-top: 10px; }
}
.rollout .step .pct {
font-family: var(--mono);
font-size: 22px;
font-weight: 600;
color: var(--clay);
margin-bottom: 6px;
}
.rollout .step .d { font-size: 13px; color: var(--gray-500); }
.rollout .step .when { font-family: var(--mono); font-size: 11px; color: var(--gray-500); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 8px; }
</style>
</head>
<body>
<div class="page">
<header class="page-head">
<div class="eyebrow">Pull request · Acme</div>
<h1>#312 — Move notification delivery onto a queue</h1>
<div class="pr-meta">
<span class="stat"><strong>9</strong> files</span>
<span class="stat"><span class="add">+418</span> / <span class="del">190</span></span>
<span class="stat">branch <strong>notify-queue</strong><strong>main</strong></span>
<span class="stat">author <strong>@priya</strong></span>
</div>
<div class="prompt-box">
<span class="label">Prompt</span>
Write up PR #312 for my reviewers. Explain the motivation, walk them
through the change file by file with the <em>why</em> for each, show
before/after behavior, and tell them exactly where to focus. They
haven't touched the notification code in six months.
</div>
</header>
<div class="layout">
<main>
<div class="tldr">
<div class="k">TL;DR</div>
<p>Notification sends were happening inline in the request path. Under load they added 200800&nbsp;ms to mutation latency and silently dropped emails when the SMTP pool was exhausted. This PR moves delivery onto the existing <code>pg-boss</code> queue so the API returns immediately and failed sends retry with backoff.</p>
</div>
<!-- ======================= WHY ======================= -->
<section id="why">
<h2>Why</h2>
<p class="lede">When we added @mentions in comments last quarter, every mention started triggering up to three sends (in-app, email, Slack) inside the same transaction that saved the comment. That was fine at launch. It is not fine now that a single task update can fan out to forty watchers.</p>
<div class="ba">
<div class="panel">
<div class="k">Before</div>
<ul>
<li>Sends run inline in the mutation handler</li>
<li>SMTP timeout = 500 error for the <em>comment</em></li>
<li>No retries — a dropped email is gone</li>
<li>p99 on <code>comments.create</code>: <strong>1.4&nbsp;s</strong></li>
</ul>
</div>
<div class="panel after">
<div class="k">After</div>
<ul>
<li>Handler enqueues one job per recipient, returns</li>
<li>Worker retries 3× with exponential backoff</li>
<li>Dead-letter table for inspection after exhaustion</li>
<li>p99 on <code>comments.create</code>: <strong>180&nbsp;ms</strong> (staging)</li>
</ul>
</div>
</div>
</section>
<!-- ======================= FILE TOUR ======================= -->
<section id="tour">
<h2>File-by-file</h2>
<p class="lede">Ordered for reading, not alphabetically. Start at the worker — it's the new thing — then the enqueue call site, then the plumbing.</p>
<div class="file-tour">
<details class="file" open>
<summary>
<span class="chev"></span>
<span class="path">packages/notify/src/worker.ts</span>
<span class="badge new">new</span>
<span class="stat"><span class="add">+126</span></span>
</summary>
<div class="file-body">
<p class="why"><strong>The heart of the PR.</strong> A <code>pg-boss</code> subscriber that pulls <code>notify.deliver</code> jobs, resolves the user's channel preferences, and calls the right adapter. Retries are configured per-channel — email gets three attempts, Slack gets one because its API is already idempotent on our side.</p>
<div class="code"><pre>boss.<span class="fn">work</span>(<span class="str">'notify.deliver'</span>, { batchSize: 20 }, <span class="kw">async</span> (jobs) =&gt; {
<span class="kw">for</span> (<span class="kw">const</span> job <span class="kw">of</span> jobs) {
<span class="kw">const</span> { userId, event, channel } = job.data;
<span class="kw">const</span> prefs = <span class="kw">await</span> <span class="fn">getPrefs</span>(userId);
<span class="kw">if</span> (!prefs[channel]) <span class="kw">return</span>; <span class="cm">// user muted this channel</span>
<span class="kw">try</span> {
<span class="kw">await</span> adapters[channel].<span class="fn">send</span>(userId, event);
} <span class="kw">catch</span> (err) {
<span class="kw">if</span> (job.retryCount &gt;= MAX_RETRY[channel]) {
<span class="kw">await</span> <span class="fn">deadLetter</span>(job, err); <span class="cm">// don't throw — ack &amp; park</span>
<span class="kw">return</span>;
}
<span class="kw">throw</span> err; <span class="cm">// pg-boss reschedules</span>
}
}
});</pre></div>
</div>
</details>
<details class="file" open>
<summary>
<span class="chev"></span>
<span class="path">packages/api/src/routers/comments.ts</span>
<span class="badge mod">mod</span>
<span class="stat"><span class="add">+14</span> <span class="del">62</span></span>
</summary>
<div class="file-body">
<p class="why"><strong>Where the win shows up.</strong> The mutation used to call <code>sendEmail</code>, <code>sendSlack</code>, and <code>createInApp</code> directly. Now it inserts the comment, computes recipients, and enqueues. The try/catch soup is gone.</p>
<div class="code"><pre> <span class="kw">const</span> comment = <span class="kw">await</span> db.comments.<span class="fn">insert</span>(input);
<span class="kw">const</span> recipients = <span class="kw">await</span> <span class="fn">resolveWatchers</span>(input.taskId, input.mentions);
<span class="del"> <span class="kw">for</span> (<span class="kw">const</span> r <span class="kw">of</span> recipients) {</span>
<span class="del"> <span class="kw">await</span> <span class="fn">sendEmail</span>(r, comment); <span class="cm">// blocked the response</span></span>
<span class="del"> <span class="kw">await</span> <span class="fn">sendSlack</span>(r, comment);</span>
<span class="del"> }</span>
<span class="add"> <span class="kw">await</span> boss.<span class="fn">insert</span>(recipients.flatMap((r) =&gt;</span>
<span class="add"> CHANNELS.map((ch) =&gt; ({</span>
<span class="add"> name: <span class="str">'notify.deliver'</span>,</span>
<span class="add"> data: { userId: r.id, channel: ch, event: toEvent(comment) },</span>
<span class="add"> singletonKey: <span class="str">`${comment.id}:${r.id}:${ch}`</span>, <span class="cm">// idempotent</span></span>
<span class="add"> }))));</span>
<span class="kw">return</span> comment;</pre></div>
</div>
</details>
<details class="file">
<summary>
<span class="chev"></span>
<span class="path">packages/db/migrations/0051_dead_letter.sql</span>
<span class="badge new">new</span>
<span class="stat"><span class="add">+22</span></span>
</summary>
<div class="file-body">
<p class="why">Table for jobs that exhaust their retries. Deliberately <em>not</em> auto-pruned — we want to look at these weekly until we trust the new path. Has the full job payload and the last error string.</p>
</div>
</details>
<details class="file">
<summary>
<span class="chev"></span>
<span class="path">packages/notify/src/adapters/{email,slack,inapp}.ts</span>
<span class="badge mod">mod</span>
<span class="stat"><span class="add">+88</span> <span class="del">74</span></span>
</summary>
<div class="file-body">
<p class="why">Mostly moves. Each adapter now implements a shared <code>Adapter</code> interface and throws a typed <code>RetryableError</code> or <code>PermanentError</code> so the worker knows whether to retry. The email adapter also drops its internal retry loop — the queue owns retries now, double-retrying was how we got duplicate emails in April.</p>
</div>
</details>
<details class="file">
<summary>
<span class="chev"></span>
<span class="path">apps/worker/src/index.ts, infra/fly.toml</span>
<span class="badge mod">mod</span>
<span class="stat"><span class="add">+31</span> <span class="del">4</span></span>
</summary>
<div class="file-body">
<p class="why">Registers the new subscriber in the existing worker process and bumps its concurrency from 5 → 20. No new deploy unit.</p>
</div>
</details>
<details class="file">
<summary>
<span class="chev"></span>
<span class="path">packages/notify/src/__tests__/worker.test.ts</span>
<span class="badge new">new</span>
<span class="stat"><span class="add">+137</span></span>
</summary>
<div class="file-body">
<p class="why">Covers the retry boundary, the dead-letter path, channel muting, and the singleton key dedupe. Uses a real pg-boss against the test database — we got burned last quarter when mocked queue tests passed but prod ordering broke.</p>
</div>
</details>
</div>
</section>
<!-- ======================= FOCUS ======================= -->
<section id="focus">
<h2>Where to focus your review</h2>
<div class="focus">
<div class="item">
<div class="n">1</div>
<div>
<div class="t">The retry / dead-letter boundary</div>
<div class="d"><code>worker.ts:3144</code>. I catch, check <code>retryCount</code>, and either park or rethrow. If this logic is wrong we either retry forever or drop messages — the two failure modes this PR exists to fix.</div>
</div>
</div>
<div class="item">
<div class="n">2</div>
<div>
<div class="t">The singleton key</div>
<div class="d"><code>comments.ts:28</code>. <code>${commentId}:${userId}:${channel}</code> should make re-enqueues idempotent if the API handler retries. Sanity-check that this can't collide across tasks.</div>
</div>
</div>
<div class="item">
<div class="n">3</div>
<div>
<div class="t">What I deliberately did not do</div>
<div class="d">No per-user digest batching, no delivery receipts, no priority lanes. All of those layer on top of this cleanly; bundling them would make this unreviewable.</div>
</div>
</div>
</div>
</section>
<!-- ======================= TEST PLAN ======================= -->
<section id="tests">
<h2>Test plan</h2>
<div class="tests">
<div class="test done"><span class="check"></span><div><div class="label">Unit: retry → dead-letter path, channel mute, singleton dedupe</div><div class="note">packages/notify — 14 cases, real pg-boss on test db</div></div></div>
<div class="test done"><span class="check"></span><div><div class="label">Integration: create comment with 3 watchers, assert 9 jobs enqueued and drained</div></div></div>
<div class="test done"><span class="check"></span><div><div class="label">Staging load: 500 rps on comments.create for 10 min, p99 = 180 ms</div><div class="note">was 1.4 s before — dashboard linked in the PR description</div></div></div>
<div class="test"><span class="check"></span><div><div class="label">Manual: kill SMTP mid-burst, confirm jobs land in dead-letter and nothing 500s</div><div class="note">will do during the 10% ramp</div></div></div>
</div>
</section>
<!-- ======================= ROLLOUT ======================= -->
<section id="rollout">
<h2>Rollout</h2>
<p class="lede">Behind <code>notify_queue_v2</code>. The old inline path stays in the codebase, dead but dormant, for one release in case we need to flip back.</p>
<div class="rollout">
<div class="step"><div class="when">Day 0</div><div class="pct">internal</div><div class="d">Acme team only. Watch dead-letter table + worker error rate.</div></div>
<div class="step"><div class="when">Day 2</div><div class="pct">10%</div><div class="d">Random sample. Alert if dead-letter rate &gt; 0.5% of sends.</div></div>
<div class="step"><div class="when">Day 4</div><div class="pct">100%</div><div class="d">Ramp fully, delete the inline path in a follow-up PR next week.</div></div>
</div>
</section>
</main>
<nav class="toc">
<div class="t-head">In this PR</div>
<a href="#why">Why</a>
<a href="#tour">File-by-file</a>
<a href="#tour" class="sub">worker.ts</a>
<a href="#tour" class="sub">comments.ts</a>
<a href="#tour" class="sub">adapters</a>
<a href="#focus">Where to focus</a>
<a href="#tests">Test plan</a>
<a href="#rollout">Rollout</a>
</nav>
</div>
</div>
</body>
</html>
+574
View File
@@ -0,0 +1,574 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Acme — Cycle 14 triage</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--gray-50: #F0EEE6;
--gray-200: #D1CFC5;
--gray-500: #87867F;
--gray-800: #3D3D3A;
--white: #ffffff;
--serif: ui-serif, Georgia, "Times New Roman", serif;
--sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
font-size: 14px;
line-height: 1.5;
}
.wrap {
max-width: 1180px;
margin: 0 auto;
padding: 48px 32px 64px;
}
/* ---------- header ---------- */
header { margin-bottom: 22px; }
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 30px;
letter-spacing: -0.01em;
margin: 6px 0 4px;
}
.sub {
color: var(--gray-500);
max-width: 620px;
margin: 0;
}
.hintline {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
margin-top: 10px;
}
.hintline::before {
content: "";
color: var(--clay);
margin-right: 6px;
}
/* ---------- toolbar ---------- */
.toolbar {
position: sticky;
top: 0;
z-index: 5;
background: var(--ivory);
display: flex;
align-items: center;
gap: 14px;
padding: 14px 0 12px;
margin-bottom: 14px;
border-bottom: 1.5px solid var(--gray-200);
}
.summary {
font-family: var(--mono);
font-size: 12px;
color: var(--gray-800);
letter-spacing: 0.01em;
}
.summary b { color: var(--slate); font-weight: 600; }
.summary .dot { color: var(--gray-200); margin: 0 4px; }
.spacer { flex: 1; }
.filter-active {
font-family: var(--mono);
font-size: 11px;
color: var(--clay-d);
background: #F5E6DE;
border: 1.5px solid #E8C9BA;
border-radius: 999px;
padding: 4px 10px;
cursor: pointer;
display: none;
}
.filter-active.on { display: inline-block; }
.filter-active:hover { border-color: var(--clay); }
button.primary {
background: var(--slate);
color: var(--ivory);
border: 1.5px solid var(--slate);
border-radius: 999px;
font-family: var(--mono);
font-size: 12px;
padding: 9px 16px;
cursor: pointer;
letter-spacing: 0.01em;
}
button.primary:hover { background: var(--gray-800); }
button.primary.copied { background: var(--olive); border-color: var(--olive); }
button.ghost {
background: transparent;
color: var(--gray-800);
border: 1.5px solid var(--gray-200);
border-radius: 999px;
font-family: var(--mono);
font-size: 12px;
padding: 9px 16px;
cursor: pointer;
}
button.ghost:hover { border-color: var(--gray-500); }
/* ---------- board ---------- */
.board {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
align-items: start;
}
.col {
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 12px;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 200px;
}
.col[data-col="now"] { border-top: 3px solid var(--clay); }
.col[data-col="next"] { border-top: 3px solid var(--olive); }
.col[data-col="later"] { border-top: 3px solid var(--gray-500); }
.col[data-col="cut"] { border-top: 3px solid var(--gray-200); }
.col.dragover {
outline: 2px dashed var(--clay);
outline-offset: -6px;
background: #FBF6F2;
}
.col-head {
position: sticky;
top: 0;
z-index: 1;
background: var(--white);
display: flex;
align-items: baseline;
gap: 8px;
padding: 14px 14px 10px;
border-bottom: 1.5px solid var(--gray-50);
}
.col-head h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 17px;
margin: 0;
letter-spacing: -0.01em;
}
.col-head .count {
margin-left: auto;
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
background: var(--gray-50);
border: 1.5px solid var(--gray-200);
border-radius: 999px;
padding: 1px 8px;
min-width: 26px;
text-align: center;
}
.col-body {
padding: 10px 10px 6px;
display: flex;
flex-direction: column;
gap: 8px;
flex: 1;
min-height: 60px;
}
.col-foot {
border-top: 1.5px solid var(--gray-50);
padding: 8px 14px 12px;
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
display: flex;
justify-content: space-between;
}
.col-foot b { color: var(--gray-800); font-weight: 600; }
/* ---------- ticket card ---------- */
.ticket {
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 8px;
padding: 10px 11px 9px;
cursor: grab;
user-select: none;
transition: border-color 120ms ease, box-shadow 120ms ease, opacity 120ms ease;
}
.ticket:hover { border-color: var(--gray-500); box-shadow: 0 1px 3px rgba(20,20,19,0.06); }
.ticket:active { cursor: grabbing; }
.ticket.dragging { opacity: .4; }
.ticket.dim { opacity: .25; }
.ticket-top {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 5px;
}
.tid {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
letter-spacing: 0.01em;
}
.tag {
font-family: var(--mono);
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.05em;
border-radius: 999px;
padding: 1px 7px 2px;
border: 1px solid transparent;
cursor: pointer;
}
.tag:hover { filter: brightness(0.96); }
.tag-bug { background: #F5E2D8; color: var(--clay-d); border-color: #E8C9BA; }
.tag-feat { background: #E8EDE0; color: #5C6F44; border-color: #CFDAC0; }
.tag-chore { background: var(--gray-50); color: var(--gray-800); border-color: var(--gray-200); }
.tag-debt { background: var(--gray-50); color: var(--gray-800); border-color: var(--gray-200); }
.est {
margin-left: auto;
font-family: var(--mono);
font-size: 10px;
color: var(--gray-500);
border: 1.5px solid var(--gray-200);
border-radius: 4px;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.ttitle {
font-size: 13px;
line-height: 1.35;
color: var(--slate);
margin-bottom: 7px;
}
.ticket-bot {
display: flex;
align-items: center;
gap: 6px;
}
.owner {
font-family: var(--mono);
font-size: 10px;
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--oat);
color: var(--gray-800);
display: flex;
align-items: center;
justify-content: center;
letter-spacing: 0.02em;
}
.owner.o2 { background: #DCE4D2; }
.owner.o3 { background: #ECD9CE; }
.owner.o4 { background: var(--gray-50); }
@media (max-width: 920px) {
.board { grid-template-columns: repeat(2, 1fr); }
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">Acme / editor / triage</div>
<h1>Cycle 14 triage</h1>
<p class="sub">
Twenty-four open Linear tickets, pre-sorted into a best guess. Drag them
across Now / Next / Later / Cut until the cut feels right, then copy the
result back into the planning doc as markdown.
</p>
<div class="hintline">drag tickets between columns &nbsp;·&nbsp; click a tag to filter</div>
</header>
<div class="toolbar">
<div class="summary" id="summary"></div>
<div class="spacer"></div>
<button class="filter-active" id="filterBadge"></button>
<button class="ghost" id="resetBtn">Reset</button>
<button class="primary" id="copyBtn">Copy as markdown</button>
</div>
<div class="board" id="board"></div>
</div>
<script>
// ---------- data ----------
var COLUMNS = [
{ key: 'now', label: 'Now', rationale: 'Blocking the v2.4 release or actively losing user data.' },
{ key: 'next', label: 'Next', rationale: 'High-leverage and well-scoped — start the moment Now clears.' },
{ key: 'later', label: 'Later', rationale: 'Real, but can ride to a future cycle without anyone noticing.' },
{ key: 'cut', label: 'Cut', rationale: 'Not this quarter — close, dedupe, or push back to the requester.' }
];
var EST_PTS = { S: 1, M: 2, L: 3 };
// tag, est, owner, col
var INITIAL = [
{ id: 'BIR-241', title: 'Fix sync conflict toast firing twice on reconnect', tag: 'bug', est: 'M', owner: 'AK', col: 'now' },
{ id: 'BIR-238', title: 'Comments lost when editing offline then reloading', tag: 'bug', est: 'L', owner: 'JM', col: 'now' },
{ id: 'BIR-252', title: 'Billing webhook 500s on annual → monthly downgrade', tag: 'bug', est: 'M', owner: 'RS', col: 'now' },
{ id: 'BIR-219', title: 'Migrate workspace permissions to new ACL table', tag: 'debt', est: 'L', owner: 'AK', col: 'now' },
{ id: 'BIR-260', title: 'SSO login loop on Safari 17 with strict cookies', tag: 'bug', est: 'S', owner: 'TN', col: 'now' },
{ id: 'BIR-244', title: 'Inline @-mention picker in doc comments', tag: 'feat', est: 'M', owner: 'JM', col: 'next' },
{ id: 'BIR-231', title: 'Bulk-archive completed projects from the sidebar', tag: 'feat', est: 'S', owner: 'EL', col: 'next' },
{ id: 'BIR-247', title: 'Activity feed: collapse repeated edit events', tag: 'feat', est: 'M', owner: 'TN', col: 'next' },
{ id: 'BIR-228', title: 'Notification preferences per project', tag: 'feat', est: 'L', owner: 'EL', col: 'next' },
{ id: 'BIR-255', title: 'Keyboard shortcut cheat-sheet overlay (⌘/)', tag: 'feat', est: 'S', owner: 'RS', col: 'next' },
{ id: 'BIR-236', title: 'Empty-state illustrations for new workspaces', tag: 'chore', est: 'S', owner: 'EL', col: 'next' },
{ id: 'BIR-249', title: 'Audit log export to CSV for admins', tag: 'feat', est: 'M', owner: 'AK', col: 'next' },
{ id: 'BIR-213', title: 'Dark mode pass on settings + billing pages', tag: 'chore', est: 'M', owner: 'EL', col: 'later' },
{ id: 'BIR-258', title: 'Slack integration: post on milestone close', tag: 'feat', est: 'M', owner: 'RS', col: 'later' },
{ id: 'BIR-221', title: 'Replace moment.js with date-fns across web', tag: 'debt', est: 'L', owner: 'TN', col: 'later' },
{ id: 'BIR-263', title: 'Drag-to-reorder columns on the board view', tag: 'feat', est: 'M', owner: 'JM', col: 'later' },
{ id: 'BIR-209', title: 'Upgrade Postgres minor + reindex search vectors', tag: 'chore', est: 'M', owner: 'AK', col: 'later' },
{ id: 'BIR-251', title: 'Per-doc read receipts in the share dialog', tag: 'feat', est: 'L', owner: 'JM', col: 'later' },
{ id: 'BIR-240', title: 'Onboarding checklist resurfaces after dismissal', tag: 'bug', est: 'S', owner: 'TN', col: 'later' },
{ id: 'BIR-265', title: 'Add request-id to client error reports', tag: 'debt', est: 'S', owner: 'RS', col: 'later' },
{ id: 'BIR-198', title: 'Custom emoji reactions on comments', tag: 'feat', est: 'M', owner: 'EL', col: 'cut' },
{ id: 'BIR-204', title: 'Native Windows desktop wrapper', tag: 'feat', est: 'L', owner: 'TN', col: 'cut' },
{ id: 'BIR-217', title: 'AI summary of long comment threads', tag: 'feat', est: 'L', owner: 'JM', col: 'cut' },
{ id: 'BIR-233', title: 'Public roadmap page with voting', tag: 'feat', est: 'L', owner: 'RS', col: 'cut' }
];
var OWNER_CLASS = { AK: 'o1', JM: 'o2', RS: 'o3', TN: 'o4', EL: 'o1' };
var tickets = []; // live state
var activeFilter = null; // tag string or null
var dragId = null;
// ---------- build DOM ----------
var board = document.getElementById('board');
var colBodies = {};
var colCounts = {};
var colFoots = {};
COLUMNS.forEach(function (c) {
var col = document.createElement('section');
col.className = 'col';
col.dataset.col = c.key;
var head = document.createElement('div');
head.className = 'col-head';
var h2 = document.createElement('h2');
h2.textContent = c.label;
var count = document.createElement('span');
count.className = 'count';
head.appendChild(h2);
head.appendChild(count);
var body = document.createElement('div');
body.className = 'col-body';
var foot = document.createElement('div');
foot.className = 'col-foot';
col.appendChild(head);
col.appendChild(body);
col.appendChild(foot);
board.appendChild(col);
colBodies[c.key] = body;
colCounts[c.key] = count;
colFoots[c.key] = foot;
// drop targets
col.addEventListener('dragover', function (e) {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
col.classList.add('dragover');
});
col.addEventListener('dragleave', function (e) {
if (!col.contains(e.relatedTarget)) col.classList.remove('dragover');
});
col.addEventListener('drop', function (e) {
e.preventDefault();
col.classList.remove('dragover');
if (!dragId) return;
var t = tickets.find(function (x) { return x.id === dragId; });
if (t && t.col !== c.key) { t.col = c.key; render(); }
});
});
function makeCard(t) {
var card = document.createElement('article');
card.className = 'ticket';
card.draggable = true;
card.dataset.id = t.id;
if (activeFilter && t.tag !== activeFilter) card.classList.add('dim');
var top = document.createElement('div');
top.className = 'ticket-top';
var tid = document.createElement('span');
tid.className = 'tid';
tid.textContent = t.id;
var tag = document.createElement('button');
tag.type = 'button';
tag.className = 'tag tag-' + t.tag;
tag.textContent = t.tag;
tag.addEventListener('click', function (e) {
e.stopPropagation();
activeFilter = (activeFilter === t.tag) ? null : t.tag;
render();
});
var est = document.createElement('span');
est.className = 'est';
est.textContent = t.est;
top.appendChild(tid);
top.appendChild(tag);
top.appendChild(est);
var title = document.createElement('div');
title.className = 'ttitle';
title.textContent = t.title;
var bot = document.createElement('div');
bot.className = 'ticket-bot';
var owner = document.createElement('span');
owner.className = 'owner ' + (OWNER_CLASS[t.owner] || 'o1');
owner.textContent = t.owner;
bot.appendChild(owner);
card.appendChild(top);
card.appendChild(title);
card.appendChild(bot);
card.addEventListener('dragstart', function (e) {
dragId = t.id;
card.classList.add('dragging');
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', t.id);
});
card.addEventListener('dragend', function () {
dragId = null;
card.classList.remove('dragging');
document.querySelectorAll('.col.dragover').forEach(function (el) {
el.classList.remove('dragover');
});
});
return card;
}
function render() {
COLUMNS.forEach(function (c) { colBodies[c.key].innerHTML = ''; });
var counts = {};
var pts = {};
COLUMNS.forEach(function (c) { counts[c.key] = 0; pts[c.key] = 0; });
tickets.forEach(function (t) {
colBodies[t.col].appendChild(makeCard(t));
counts[t.col] += 1;
pts[t.col] += EST_PTS[t.est] || 0;
});
COLUMNS.forEach(function (c) {
colCounts[c.key].textContent = counts[c.key];
colFoots[c.key].innerHTML = 'estimate <b>' + pts[c.key] + ' pt' + (pts[c.key] === 1 ? '' : 's') + '</b>';
});
var summary = document.getElementById('summary');
summary.innerHTML = COLUMNS.map(function (c) {
return '<b>' + counts[c.key] + '</b>&nbsp;' + c.key;
}).join('<span class="dot">·</span>');
var badge = document.getElementById('filterBadge');
if (activeFilter) {
badge.textContent = 'filter: ' + activeFilter + ' ×';
badge.classList.add('on');
} else {
badge.classList.remove('on');
}
}
document.getElementById('filterBadge').addEventListener('click', function () {
activeFilter = null;
render();
});
// ---------- export ----------
function buildMarkdown() {
var lines = [];
lines.push('# Acme — Cycle 14 triage');
lines.push('');
COLUMNS.forEach(function (c) {
var rows = tickets.filter(function (t) { return t.col === c.key; });
var pts = rows.reduce(function (s, t) { return s + (EST_PTS[t.est] || 0); }, 0);
lines.push('## ' + c.label + ' (' + rows.length + ' · ' + pts + ' pts)');
lines.push('');
lines.push('_' + c.rationale + '_');
lines.push('');
rows.forEach(function (t) {
lines.push('- **' + t.id + '** ' + t.title + ' — ' + t.tag + ', ' + t.est + ', ' + t.owner);
});
lines.push('');
});
return lines.join('\n');
}
var copyBtn = document.getElementById('copyBtn');
var copyTimer = null;
copyBtn.addEventListener('click', function () {
var md = buildMarkdown();
function flash() {
copyBtn.textContent = 'Copied ✓';
copyBtn.classList.add('copied');
clearTimeout(copyTimer);
copyTimer = setTimeout(function () {
copyBtn.textContent = 'Copy as markdown';
copyBtn.classList.remove('copied');
}, 1200);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(md).then(flash, flash);
} else {
var ta = document.createElement('textarea');
ta.value = md;
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
flash();
}
});
document.getElementById('resetBtn').addEventListener('click', function () {
tickets = INITIAL.map(function (t) { return Object.assign({}, t); });
activeFilter = null;
render();
});
// ---------- init ----------
tickets = INITIAL.map(function (t) { return Object.assign({}, t); });
render();
</script>
</body>
</html>
+664
View File
@@ -0,0 +1,664 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Acme — flags.production.json</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--gray-50: #F0EEE6;
--gray-200: #D1CFC5;
--gray-500: #87867F;
--gray-800: #3D3D3A;
--white: #ffffff;
--serif: ui-serif, Georgia, "Times New Roman", serif;
--sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
font-size: 14px;
line-height: 1.5;
}
.wrap {
max-width: 1080px;
margin: 0 auto;
padding: 48px 32px 64px;
}
/* ---------- header ---------- */
header { margin-bottom: 28px; }
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 30px;
letter-spacing: -0.01em;
margin: 6px 0 4px;
}
.sub {
color: var(--gray-500);
max-width: 600px;
}
/* ---------- layout ---------- */
.layout {
display: grid;
grid-template-columns: 1fr 320px;
gap: 24px;
align-items: start;
}
@media (max-width: 880px) {
.layout { grid-template-columns: 1fr; }
.sidebar { position: static !important; }
}
/* ---------- warning banner ---------- */
.banner {
grid-column: 1 / -1;
display: none;
align-items: baseline;
gap: 8px;
padding: 10px 14px;
border: 1.5px solid #E8C2AE;
background: #F8E9E0;
color: var(--clay-d);
border-radius: 10px;
font-size: 13px;
}
.banner.show { display: flex; }
.banner .glyph { font-size: 13px; line-height: 1; transform: translateY(1px); }
/* ---------- groups / panels ---------- */
.form-col { display: flex; flex-direction: column; gap: 16px; }
.group {
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 12px;
overflow: hidden;
}
.group-head {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
padding: 12px 18px 11px;
border-bottom: 1.5px solid var(--gray-50);
display: flex;
justify-content: space-between;
gap: 12px;
}
.group-head .name { color: var(--gray-800); }
.group-head .meta { letter-spacing: 0.04em; }
/* ---------- flag rows ---------- */
.flag {
display: flex;
align-items: flex-start;
gap: 16px;
padding: 14px 18px;
border-bottom: 1px solid var(--gray-50);
border-left: 3px solid transparent;
transition: border-left-color 140ms ease, background 140ms ease;
}
.flag:last-child { border-bottom: none; }
.flag.warn {
border-left-color: var(--clay);
background: #FBF3EE;
}
.flag-info { flex: 1; min-width: 0; }
.flag-key {
font-family: var(--mono);
font-size: 13px;
color: var(--gray-800);
display: flex;
align-items: center;
gap: 7px;
flex-wrap: wrap;
}
.dot {
display: none;
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--clay);
flex: none;
}
.flag.changed .dot { display: inline-block; }
.rollout {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.03em;
color: var(--gray-500);
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: 999px;
padding: 1px 7px 2px;
}
.flag-desc {
color: var(--gray-500);
font-size: 12.5px;
margin-top: 2px;
}
.req-chip {
display: inline-flex;
align-items: baseline;
gap: 5px;
margin-top: 7px;
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: 999px;
padding: 2px 9px 3px;
}
.req-chip .warn-glyph { display: none; }
.flag.warn .req-chip {
color: var(--clay-d);
background: #F8E1D5;
border-color: #E8C2AE;
}
.flag.warn .req-chip .warn-glyph { display: inline; }
/* ---------- toggle switch ---------- */
.toggle {
position: relative;
flex: none;
width: 38px;
height: 22px;
margin-top: 1px;
}
.toggle input {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
margin: 0;
opacity: 0;
cursor: pointer;
}
.toggle .track {
position: absolute;
inset: 0;
background: var(--gray-200);
border-radius: 999px;
transition: background 160ms ease;
pointer-events: none;
}
.toggle .track::after {
content: "";
position: absolute;
top: 3px;
left: 3px;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--white);
box-shadow: 0 1px 2px rgba(20,20,19,0.18);
transition: transform 160ms ease;
}
.toggle input:checked + .track { background: var(--olive); }
.toggle input:checked + .track::after { transform: translateX(16px); }
.toggle input:focus-visible + .track {
outline: 2px solid var(--slate);
outline-offset: 2px;
}
/* ---------- sidebar ---------- */
.sidebar {
position: sticky;
top: 24px;
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 12px;
padding: 18px;
display: flex;
flex-direction: column;
gap: 14px;
}
.side-label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
}
.side-count {
font-family: var(--serif);
font-weight: 500;
font-size: 19px;
letter-spacing: -0.01em;
}
.side-count .warn-n { color: var(--clay-d); }
.side-count .sep { color: var(--gray-200); margin: 0 4px; }
.diff {
margin: 0;
font-family: var(--mono);
font-size: 12px;
line-height: 1.7;
background: var(--gray-50);
border: 1px solid var(--gray-200);
border-radius: 8px;
padding: 12px 14px;
overflow-x: auto;
max-height: 320px;
overflow-y: auto;
white-space: pre;
}
.diff .minus { color: var(--gray-500); }
.diff .plus { color: var(--olive); }
.diff .empty { color: var(--gray-500); }
.btn {
display: block;
width: 100%;
border: none;
border-radius: 999px;
font-family: var(--mono);
font-size: 12px;
padding: 9px 16px;
cursor: pointer;
text-align: center;
transition: background 140ms ease, color 140ms ease, border-color 140ms ease;
}
.btn.primary {
background: var(--slate);
color: var(--ivory);
}
.btn.primary:hover:not(:disabled) { background: var(--gray-800); }
.btn.primary:disabled {
background: var(--gray-200);
color: var(--gray-500);
cursor: not-allowed;
}
.btn.primary.copied { background: var(--olive); }
.btn.ghost {
background: transparent;
color: var(--gray-800);
border: 1.5px solid var(--gray-200);
}
.btn.ghost:hover { border-color: var(--gray-500); }
.btn.ghost.copied { border-color: var(--olive); color: var(--olive); }
.btn-stack { display: flex; flex-direction: column; gap: 8px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">Acme / editor / feature-flags</div>
<h1>flags.production.json</h1>
<p class="sub">A form-based editor for the production feature-flag config.
Toggle flags, fix dependency warnings as they surface, then copy out
only the lines that changed.</p>
</header>
<div class="layout">
<div class="banner" id="banner">
<span class="glyph">&#9888;</span>
<span id="bannerText">1 flag is enabled without its prerequisite</span>
</div>
<div class="form-col" id="formCol"><!-- groups injected --></div>
<aside class="sidebar">
<div>
<div class="side-label">Pending changes</div>
<div class="side-count" id="sideCount">0 changed</div>
</div>
<pre class="diff" id="diff"><span class="empty">// no changes yet</span></pre>
<div class="btn-stack">
<button class="btn primary" id="copyDiff" disabled>Copy diff</button>
<button class="btn ghost" id="copyJson">Copy full JSON</button>
<button class="btn ghost" id="reset">Reset</button>
</div>
</aside>
</div>
</div>
<script>
"use strict";
// ---------------------------------------------------------------------------
// data — the production flag set, as it exists right now
// ---------------------------------------------------------------------------
const GROUPS = [
{
id: "onboarding",
label: "Onboarding",
flags: [
{ key: "onboarding.checklist_v2",
desc: "New three-step setup checklist replacing the modal tour.",
on: true },
{ key: "onboarding.invite_nudge",
desc: "Nudge owners to invite teammates after creating their first project.",
on: true },
{ key: "onboarding.workspace_templates",
desc: "Offer prebuilt workspace templates during signup.",
on: false, requires: "onboarding.checklist_v2" },
{ key: "onboarding.skip_email_verify",
desc: "Let SSO-domain users in before email verification completes.",
on: false }
]
},
{
id: "sync",
label: "Sync engine",
flags: [
{ key: "sync.delta_compression",
desc: "Send field-level deltas instead of full document snapshots.",
on: true },
{ key: "sync.offline_queue_v2",
desc: "Persist offline edits to IndexedDB and replay on reconnect.",
on: false, requires: "sync.delta_compression" },
{ key: "sync.presence_cursors",
desc: "Show live collaborator cursors in board and doc views.",
on: true },
{ key: "sync.conflict_banner",
desc: "Surface a merge banner instead of silently last-write-wins.",
on: false, requires: "sync.offline_queue_v2" },
{ key: "sync.binary_ws_frames",
desc: "Switch the realtime channel to binary WebSocket frames.",
on: false, rollout: 10 }
]
},
{
id: "billing",
label: "Billing",
flags: [
{ key: "billing.usage_meter",
desc: "Show per-seat usage meter on the workspace billing page.",
on: false },
{ key: "billing.annual_discount_banner",
desc: "Promote the annual-plan discount in the upgrade flow.",
on: true },
{ key: "billing.proration_preview",
desc: "Preview the prorated charge before confirming a plan change.",
on: false, requires: "billing.usage_meter" },
{ key: "billing.dunning_emails_v3",
desc: "Use the rewritten dunning sequence with a 14-day grace window.",
on: true, rollout: 25 }
]
},
{
id: "internal",
label: "Internal",
flags: [
{ key: "internal.shadow_traffic",
desc: "Mirror 1% of API traffic to the staging cluster for diffing.",
on: false },
{ key: "internal.query_tracing",
desc: "Attach OpenTelemetry spans to every Postgres query.",
on: true },
{ key: "internal.kill_switch_ui",
desc: "Expose the emergency kill-switch panel in the admin console.",
on: true, requires: "internal.query_tracing" }
]
}
];
// flat lookup, plus a frozen copy of initial state
const FLAGS = {};
const INITIAL = {};
GROUPS.forEach(g => g.flags.forEach(f => {
FLAGS[f.key] = f;
INITIAL[f.key] = f.on;
}));
// ---------------------------------------------------------------------------
// render the form once; state lives in the checkboxes
// ---------------------------------------------------------------------------
const formCol = document.getElementById("formCol");
const banner = document.getElementById("banner");
const bannerTxt = document.getElementById("bannerText");
const sideCount = document.getElementById("sideCount");
const diffEl = document.getElementById("diff");
const copyDiffBtn = document.getElementById("copyDiff");
const copyJsonBtn = document.getElementById("copyJson");
const resetBtn = document.getElementById("reset");
function esc(s) {
return String(s).replace(/[&<>"]/g, c =>
({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
}
function buildForm() {
formCol.innerHTML = "";
for (const g of GROUPS) {
const panel = document.createElement("section");
panel.className = "group";
panel.dataset.group = g.id;
const head = document.createElement("div");
head.className = "group-head";
head.innerHTML =
'<span class="name">' + esc(g.label) + '</span>' +
'<span class="meta" data-meta></span>';
panel.appendChild(head);
for (const f of g.flags) {
const row = document.createElement("div");
row.className = "flag";
row.dataset.key = f.key;
const rolloutBadge = (typeof f.rollout === "number")
? '<span class="rollout">' + f.rollout + '%</span>'
: "";
const reqChip = f.requires
? '<div class="req-chip"><span class="warn-glyph">&#9888;</span>' +
'<span>requires ' + esc(f.requires) + '</span></div>'
: "";
row.innerHTML =
'<label class="toggle">' +
'<input type="checkbox" data-key="' + esc(f.key) + '"' +
(f.on ? " checked" : "") +
' aria-label="' + esc(f.key) + '">' +
'<span class="track"></span>' +
'</label>' +
'<div class="flag-info">' +
'<div class="flag-key"><span class="dot"></span>' +
'<span>' + esc(f.key) + '</span>' + rolloutBadge +
'</div>' +
'<div class="flag-desc">' + esc(f.desc) + '</div>' +
reqChip +
'</div>';
panel.appendChild(row);
}
formCol.appendChild(panel);
}
}
// ---------------------------------------------------------------------------
// state helpers
// ---------------------------------------------------------------------------
function currentState() {
const state = {};
formCol.querySelectorAll("input[type=checkbox]").forEach(cb => {
state[cb.dataset.key] = cb.checked;
});
return state;
}
function computeDiff(state) {
const lines = [];
for (const g of GROUPS) {
for (const f of g.flags) {
if (state[f.key] !== INITIAL[f.key]) {
lines.push({ key: f.key, from: INITIAL[f.key], to: state[f.key] });
}
}
}
return lines;
}
function diffText(lines) {
return lines.map(l =>
'- "' + l.key + '": ' + l.from + '\n' +
'+ "' + l.key + '": ' + l.to
).join("\n");
}
function fullJson(state) {
// grouped, ordered, pretty-printed — what you'd actually paste into the file
let out = "{\n";
GROUPS.forEach((g, gi) => {
g.flags.forEach((f, fi) => {
const last = gi === GROUPS.length - 1 && fi === g.flags.length - 1;
out += ' "' + f.key + '": ' + state[f.key] + (last ? "" : ",") + "\n";
});
});
out += "}";
return out;
}
// ---------------------------------------------------------------------------
// the live update pass
// ---------------------------------------------------------------------------
function update() {
const state = currentState();
let warnCount = 0;
// per-row decorations
for (const g of GROUPS) {
let onCount = 0;
for (const f of g.flags) {
const row = formCol.querySelector('.flag[data-key="' + CSS.escape(f.key) + '"]');
const on = state[f.key];
if (on) onCount++;
const changed = on !== INITIAL[f.key];
row.classList.toggle("changed", changed);
let warn = false;
if (f.requires && on && !state[f.requires]) warn = true;
row.classList.toggle("warn", warn);
if (warn) warnCount++;
}
const meta = formCol.querySelector(
'.group[data-group="' + g.id + '"] [data-meta]');
meta.textContent = g.flags.length + " flags, " + onCount + " on";
}
// diff + sidebar
const lines = computeDiff(state);
if (lines.length === 0) {
diffEl.innerHTML = '<span class="empty">// no changes yet</span>';
copyDiffBtn.disabled = true;
} else {
diffEl.innerHTML = lines.map(l =>
'<span class="minus">- "' + esc(l.key) + '": ' + l.from + '</span>\n' +
'<span class="plus">+ "' + esc(l.key) + '": ' + l.to + '</span>'
).join("\n");
copyDiffBtn.disabled = false;
}
sideCount.innerHTML =
lines.length + " changed" +
'<span class="sep">&middot;</span>' +
'<span class="' + (warnCount ? "warn-n" : "") + '">' +
warnCount + " warning" + (warnCount === 1 ? "" : "s") +
"</span>";
// banner
if (warnCount > 0) {
bannerTxt.textContent =
warnCount === 1
? "1 flag is enabled without its prerequisite"
: warnCount + " flags are enabled without their prerequisites";
banner.classList.add("show");
} else {
banner.classList.remove("show");
}
}
// ---------------------------------------------------------------------------
// clipboard + buttons
// ---------------------------------------------------------------------------
function flash(btn, label) {
const orig = btn.textContent;
btn.textContent = label;
btn.classList.add("copied");
btn.disabled = true;
setTimeout(() => {
btn.textContent = orig;
btn.classList.remove("copied");
btn.disabled = false;
update(); // restore disabled state on copyDiff if needed
}, 1200);
}
function writeClipboard(text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
return navigator.clipboard.writeText(text);
}
// fallback for file:// contexts without clipboard permission
const ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.left = "-9999px";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); } catch (e) { /* ignore */ }
document.body.removeChild(ta);
return Promise.resolve();
}
copyDiffBtn.addEventListener("click", () => {
const lines = computeDiff(currentState());
if (!lines.length) return;
writeClipboard(diffText(lines)).then(() => flash(copyDiffBtn, "Copied ✓"));
});
copyJsonBtn.addEventListener("click", () => {
writeClipboard(fullJson(currentState())).then(() => flash(copyJsonBtn, "Copied ✓"));
});
resetBtn.addEventListener("click", () => {
formCol.querySelectorAll("input[type=checkbox]").forEach(cb => {
cb.checked = INITIAL[cb.dataset.key];
});
update();
});
formCol.addEventListener("change", e => {
if (e.target.matches("input[type=checkbox]")) update();
});
// ---------------------------------------------------------------------------
// boot
// ---------------------------------------------------------------------------
buildForm();
update();
</script>
</body>
</html>
+723
View File
@@ -0,0 +1,723 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Acme — Support reply prompt tuner</title>
<style>
:root {
--ivory: #FAF9F5;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--gray-50: #F0EEE6;
--gray-200: #D1CFC5;
--gray-500: #87867F;
--gray-800: #3D3D3A;
--white: #ffffff;
--serif: ui-serif, Georgia, "Times New Roman", serif;
--sans: system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
html, body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
font-size: 14px;
line-height: 1.5;
}
.wrap {
max-width: 1180px;
margin: 0 auto;
padding: 48px 32px 64px;
}
/* ---------- header ---------- */
header { margin-bottom: 22px; }
.eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--gray-500);
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: 30px;
letter-spacing: -0.01em;
margin: 6px 0 4px;
}
.sub {
color: var(--gray-500);
max-width: 640px;
}
/* ---------- toolbar ---------- */
.toolbar {
position: sticky;
top: 0;
z-index: 10;
display: flex;
align-items: center;
gap: 10px;
background: var(--ivory);
padding: 10px 0 14px;
margin-bottom: 6px;
border-bottom: 1px solid var(--gray-200);
}
.toolbar .spacer { flex: 1; }
.toolbar .hint {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
letter-spacing: 0.02em;
}
button {
font-family: var(--mono);
font-size: 12px;
cursor: pointer;
-webkit-appearance: none;
appearance: none;
}
.btn-primary {
background: var(--slate);
color: var(--ivory);
border: 1.5px solid var(--slate);
border-radius: 999px;
padding: 9px 16px;
transition: background 140ms ease, transform 80ms ease;
}
.btn-primary:hover { background: var(--gray-800); }
.btn-primary:active { transform: translateY(1px); }
.btn-primary.copied { background: var(--olive); border-color: var(--olive); }
.btn-ghost {
background: transparent;
color: var(--gray-800);
border: 1.5px solid var(--gray-200);
border-radius: 999px;
padding: 8px 15px;
transition: border-color 140ms ease, color 140ms ease;
}
.btn-ghost:hover { border-color: var(--gray-500); color: var(--slate); }
/* ---------- two-column layout ---------- */
.cols {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(0, 1fr);
gap: 24px;
align-items: start;
margin-top: 18px;
}
@media (max-width: 880px) {
.cols { grid-template-columns: 1fr; }
}
.panel {
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 12px;
overflow: hidden;
}
/* ---------- left: editor ---------- */
.ed-toolbar {
display: flex;
align-items: center;
gap: 10px;
padding: 11px 16px;
border-bottom: 1.5px solid var(--gray-200);
background: var(--gray-50);
}
.ed-label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.1em;
color: var(--gray-500);
text-transform: uppercase;
}
.ed-toolbar .spacer { flex: 1; }
.counter {
font-family: var(--mono);
font-size: 11px;
color: var(--gray-800);
background: var(--white);
border: 1.5px solid var(--gray-200);
border-radius: 999px;
padding: 3px 10px;
white-space: nowrap;
font-variant-numeric: tabular-nums;
}
.editor {
font-family: var(--mono);
font-size: 13px;
line-height: 1.7;
padding: 18px;
min-height: 360px;
white-space: pre-wrap;
word-break: break-word;
background: var(--white);
color: var(--slate);
outline: none;
caret-color: var(--clay);
tab-size: 2;
}
.editor:focus-visible { outline: none; }
.slot {
background: var(--oat);
color: var(--clay-d);
border-radius: 4px;
padding: 0 3px;
font-weight: 600;
}
.slot.warn {
background: rgba(217, 119, 87, 0.14);
color: var(--clay-d);
text-decoration: underline dashed var(--clay) 1.5px;
text-underline-offset: 3px;
}
.legend {
border-top: 1.5px solid var(--gray-200);
padding: 12px 16px 14px;
background: var(--gray-50);
}
.legend-label {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gray-500);
margin-bottom: 8px;
}
.legend-chips { display: flex; flex-wrap: wrap; gap: 6px; }
.chip {
font-family: var(--mono);
font-size: 11px;
background: var(--oat);
color: var(--clay-d);
border-radius: 4px;
padding: 2px 7px;
font-weight: 600;
}
/* ---------- right: preview ---------- */
.preview-panel { padding: 16px; display: flex; flex-direction: column; gap: 14px; }
.preview-head {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--gray-500);
padding: 0 2px 2px;
}
.sample {
border: 1.5px solid var(--gray-200);
border-radius: 10px;
overflow: hidden;
}
.sample-head {
display: flex;
align-items: center;
gap: 9px;
padding: 9px 13px;
border-bottom: 1.5px solid var(--gray-200);
background: var(--gray-50);
}
.badge {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.08em;
color: var(--gray-500);
}
.cust {
font-family: var(--serif);
font-size: 14px;
font-weight: 500;
letter-spacing: -0.005em;
}
.sample-head .spacer { flex: 1; }
.plan {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.05em;
text-transform: uppercase;
border-radius: 999px;
padding: 2px 8px;
border: 1.5px solid var(--gray-200);
color: var(--gray-800);
background: var(--white);
}
.plan.team { background: var(--oat); border-color: var(--oat); color: var(--clay-d); }
.plan.studio { background: var(--olive); border-color: var(--olive); color: var(--ivory); }
.rendered {
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.6;
color: var(--gray-800);
white-space: pre-wrap;
word-break: break-word;
padding: 12px 13px 14px;
}
.filled {
background: var(--gray-50);
border-radius: 3px;
padding: 0 2px;
box-shadow: inset 0 0 0 1px var(--gray-200);
}
.missing {
background: rgba(217, 119, 87, 0.12);
color: var(--clay-d);
border-radius: 3px;
padding: 0 2px;
font-weight: 600;
}
footer {
margin-top: 36px;
font-family: var(--mono);
font-size: 11px;
color: var(--gray-500);
letter-spacing: 0.02em;
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">Acme / editor / prompt-tuner</div>
<h1>Support reply draft prompt</h1>
<p class="sub">Edit the system prompt on the left and watch three sample tickets re-render the filled template on the right, live as you type. When it reads well across all three moods, copy the template out.</p>
</header>
<div class="toolbar">
<button id="copyBtn" class="btn-primary">Copy prompt</button>
<button id="resetBtn" class="btn-ghost">Reset</button>
<div class="spacer"></div>
<div class="hint">slots use <strong>{{double_brace}}</strong> syntax</div>
</div>
<div class="cols">
<!-- left -->
<section class="panel">
<div class="ed-toolbar">
<span class="ed-label">Template</span>
<div class="spacer"></div>
<span id="counter" class="counter">0 chars · ~0 tokens</span>
</div>
<div id="editor" class="editor" contenteditable="true" spellcheck="false" autocapitalize="off" autocorrect="off"></div>
<div class="legend">
<div class="legend-label">Available slots</div>
<div id="legendChips" class="legend-chips"></div>
</div>
</section>
<!-- right -->
<section class="panel preview-panel">
<div class="preview-head">Live preview · 3 sample tickets</div>
<div id="samples"></div>
</section>
</div>
<footer>Highlighted slots fill from each sample's ticket fields. Anything underlined in dashed clay isn't a known field and will pass through unfilled.</footer>
</div>
<script>
(function () {
"use strict";
// ---------------------------------------------------------------------------
// Data
// ---------------------------------------------------------------------------
var DEFAULT_TEMPLATE =
'You are a support agent for Acme, a project workspace for small teams.\n' +
'\n' +
'A customer named {{customer_name}} on the {{plan_tier}} plan wrote in about:\n' +
'"{{ticket_subject}}"\n' +
'\n' +
'Their message:\n' +
'{{ticket_body}}\n' +
'\n' +
'Write a reply that is {{tone}}, no more than 120 words, and ends with a single concrete next step.\n' +
'Never promise a refund without escalating. Sign off as "The Acme team."';
var SAMPLES = [
{
label: 'SAMPLE 1',
planClass: 'free',
data: {
customer_name: 'Priya N.',
plan_tier: 'Free',
ticket_subject: 'Where did my board go?',
ticket_body: 'Hi — I made a board yesterday called "Spring launch" with my coworker and today I cant find it anywhere. Im new to Acme and Im not sure if I deleted it by accident or if Im just looking in the wrong place. Can you help?',
tone: 'warm and patient'
}
},
{
label: 'SAMPLE 2',
planClass: 'team',
data: {
customer_name: 'Marcus D.',
plan_tier: 'Team',
ticket_subject: 'Sync keeps dropping comments',
ticket_body: 'This is the third time this week. I leave comments on cards from my laptop, switch to my phone on the train, and theyre gone. My team thinks Im ignoring them. We pay for 14 seats and this is genuinely making us look bad to a client.',
tone: 'direct and apologetic'
}
},
{
label: 'SAMPLE 3',
planClass: 'studio',
data: {
customer_name: 'Lena K.',
plan_tier: 'Studio',
ticket_subject: 'Update billing entity on next invoice',
ticket_body: 'Hey folks! We just spun up a new holding company (ACME Corp Oy) and finance would love the May invoice to use that name + our new VAT ID, FI00000000. No rush at all, just want it sorted before the cycle runs. Thanks a million.',
tone: 'brisk and friendly'
}
}
];
var KNOWN_SLOTS = ['customer_name', 'plan_tier', 'ticket_subject', 'ticket_body', 'tone'];
var SLOT_RE = /\{\{\s*([a-zA-Z0-9_]+)\s*\}\}/g;
// ---------------------------------------------------------------------------
// DOM refs
// ---------------------------------------------------------------------------
var editor = document.getElementById('editor');
var counterEl = document.getElementById('counter');
var samplesEl = document.getElementById('samples');
var legendEl = document.getElementById('legendChips');
var copyBtn = document.getElementById('copyBtn');
var resetBtn = document.getElementById('resetBtn');
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
function escapeHtml(s) {
return s
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
// Read plain text out of the contenteditable. We rely on white-space:pre-wrap
// and only ever insert text + inline <span>s, so textContent is faithful as
// long as the browser hasn't injected block elements or <br>s. Normalize both.
function getPlainText() {
var out = '';
var walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, null);
var node;
while ((node = walker.nextNode())) {
if (node.nodeType === Node.TEXT_NODE) {
out += node.nodeValue;
} else if (node.nodeType === Node.ELEMENT_NODE) {
var tag = node.tagName;
if (tag === 'BR') out += '\n';
// Some browsers wrap new lines in <div>; emit a newline before a div
// that isn't the first child and doesn't already start after one.
if (tag === 'DIV' && node !== editor.firstChild && out.length && out[out.length - 1] !== '\n') {
out += '\n';
}
}
}
return out;
}
// ---- Caret save/restore by character offset ----
function getCaretOffset() {
var sel = window.getSelection();
if (!sel || sel.rangeCount === 0) return null;
var range = sel.getRangeAt(0);
if (!editor.contains(range.startContainer)) return null;
var pre = range.cloneRange();
pre.selectNodeContents(editor);
pre.setEnd(range.startContainer, range.startOffset);
// Count characters by walking text nodes within the pre-range. We mirror
// getPlainText's newline handling so offsets line up.
var frag = pre.cloneContents();
var off = 0;
var walker = document.createTreeWalker(frag, NodeFilter.SHOW_TEXT | NodeFilter.SHOW_ELEMENT, null);
var node, lastChar = '';
var first = true;
while ((node = walker.nextNode())) {
if (node.nodeType === Node.TEXT_NODE) {
off += node.nodeValue.length;
if (node.nodeValue.length) lastChar = node.nodeValue[node.nodeValue.length - 1];
} else {
if (node.tagName === 'BR') { off += 1; lastChar = '\n'; }
if (node.tagName === 'DIV' && !first && lastChar !== '\n') { off += 1; lastChar = '\n'; }
}
first = false;
}
return off;
}
function setCaretOffset(offset) {
if (offset == null) return;
var sel = window.getSelection();
if (!sel) return;
var walker = document.createTreeWalker(editor, NodeFilter.SHOW_TEXT, null);
var node;
var remaining = offset;
var lastNode = null;
while ((node = walker.nextNode())) {
lastNode = node;
var len = node.nodeValue.length;
if (remaining <= len) {
var range = document.createRange();
range.setStart(node, remaining);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
return;
}
remaining -= len;
}
// Past the end — place at end of last text node, or at end of editor.
var range2 = document.createRange();
if (lastNode) {
range2.setStart(lastNode, lastNode.nodeValue.length);
} else {
range2.selectNodeContents(editor);
range2.collapse(false);
}
range2.collapse(true);
sel.removeAllRanges();
sel.addRange(range2);
}
// ---- Highlighting ----
function highlightHtml(text) {
var html = '';
var last = 0;
SLOT_RE.lastIndex = 0;
var m;
while ((m = SLOT_RE.exec(text)) !== null) {
html += escapeHtml(text.slice(last, m.index));
var name = m[1];
var cls = KNOWN_SLOTS.indexOf(name) !== -1 ? 'slot' : 'slot warn';
html += '<span class="' + cls + '">' + escapeHtml(m[0]) + '</span>';
last = m.index + m[0].length;
}
html += escapeHtml(text.slice(last));
return html;
}
function renderEditor(text, caret) {
editor.innerHTML = highlightHtml(text);
if (caret != null && document.activeElement === editor) {
setCaretOffset(caret);
}
}
// ---- Preview ----
function fillHtml(text, data) {
var html = '';
var last = 0;
SLOT_RE.lastIndex = 0;
var m;
while ((m = SLOT_RE.exec(text)) !== null) {
html += escapeHtml(text.slice(last, m.index));
var name = m[1];
if (Object.prototype.hasOwnProperty.call(data, name)) {
html += '<span class="filled">' + escapeHtml(String(data[name])) + '</span>';
} else {
html += '<span class="missing">' + escapeHtml(m[0]) + '</span>';
}
last = m.index + m[0].length;
}
html += escapeHtml(text.slice(last));
return html;
}
var sampleNodes = [];
function buildSamples() {
samplesEl.innerHTML = '';
sampleNodes = [];
SAMPLES.forEach(function (s) {
var card = document.createElement('div');
card.className = 'sample';
var head = document.createElement('div');
head.className = 'sample-head';
var badge = document.createElement('span');
badge.className = 'badge';
badge.textContent = s.label;
var cust = document.createElement('span');
cust.className = 'cust';
cust.textContent = s.data.customer_name;
var spacer = document.createElement('span');
spacer.className = 'spacer';
var plan = document.createElement('span');
plan.className = 'plan ' + s.planClass;
plan.textContent = s.data.plan_tier;
head.appendChild(badge);
head.appendChild(cust);
head.appendChild(spacer);
head.appendChild(plan);
var body = document.createElement('div');
body.className = 'rendered';
card.appendChild(head);
card.appendChild(body);
samplesEl.appendChild(card);
sampleNodes.push({ body: body, data: s.data });
});
samplesEl.style.display = 'flex';
samplesEl.style.flexDirection = 'column';
samplesEl.style.gap = '14px';
}
function buildLegend() {
legendEl.innerHTML = '';
KNOWN_SLOTS.forEach(function (name) {
var c = document.createElement('span');
c.className = 'chip';
c.textContent = '{{' + name + '}}';
legendEl.appendChild(c);
});
}
// ---- Counter ----
function updateCounter(text) {
var chars = text.length;
var tokens = Math.round(chars / 4.2);
counterEl.textContent = chars + ' chars · ~' + tokens + ' tokens';
}
// ---- Master refresh ----
function refresh(opts) {
opts = opts || {};
var text = opts.text != null ? opts.text : getPlainText();
var caret = opts.preserveCaret ? getCaretOffset() : null;
renderEditor(text, caret);
updateCounter(text);
sampleNodes.forEach(function (sn) {
sn.body.innerHTML = fillHtml(text, sn.data);
});
}
// ---------------------------------------------------------------------------
// Events
// ---------------------------------------------------------------------------
var raf = null;
editor.addEventListener('input', function () {
if (raf) cancelAnimationFrame(raf);
raf = requestAnimationFrame(function () {
raf = null;
refresh({ preserveCaret: true });
});
});
// Force plain-text paste so we never inherit foreign HTML.
editor.addEventListener('paste', function (e) {
e.preventDefault();
var txt = (e.clipboardData || window.clipboardData).getData('text/plain');
var sel = window.getSelection();
if (!sel || !sel.rangeCount) return;
var range = sel.getRangeAt(0);
range.deleteContents();
range.insertNode(document.createTextNode(txt));
range.collapse(false);
sel.removeAllRanges();
sel.addRange(range);
refresh({ preserveCaret: true });
});
// Insert literal newline on Enter so we don't get nested <div>s.
editor.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
var sel = window.getSelection();
if (!sel || !sel.rangeCount) return;
var range = sel.getRangeAt(0);
range.deleteContents();
var nl = document.createTextNode('\n');
range.insertNode(nl);
range.setStartAfter(nl);
range.collapse(true);
sel.removeAllRanges();
sel.addRange(range);
refresh({ preserveCaret: true });
}
});
var copyTimer = null;
copyBtn.addEventListener('click', function () {
var text = getPlainText();
var done = function () {
copyBtn.textContent = 'Copied ✓';
copyBtn.classList.add('copied');
if (copyTimer) clearTimeout(copyTimer);
copyTimer = setTimeout(function () {
copyBtn.textContent = 'Copy prompt';
copyBtn.classList.remove('copied');
}, 1200);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () {
fallbackCopy(text); done();
});
} else {
fallbackCopy(text); done();
}
});
function fallbackCopy(text) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.left = '-9999px';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) { /* ignore */ }
document.body.removeChild(ta);
}
resetBtn.addEventListener('click', function () {
refresh({ text: DEFAULT_TEMPLATE });
});
// ---------------------------------------------------------------------------
// Boot
// ---------------------------------------------------------------------------
buildLegend();
buildSamples();
refresh({ text: DEFAULT_TEMPLATE });
})();
</script>
</body>
</html>
+49
View File
@@ -0,0 +1,49 @@
# Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, religion, or sexual identity and
orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best for the overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or advances of
any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email address,
without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the project maintainers at **opensource@anthropic.com**. All
complaints will be reviewed and investigated promptly and fairly.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.1, available at
https://www.contributor-covenant.org/version/2/1/code_of_conduct.html.
[homepage]: https://www.contributor-covenant.org
+201
View File
@@ -0,0 +1,201 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2026 Anthropic PBC
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
+41
View File
@@ -0,0 +1,41 @@
# The unreasonable effectiveness of HTML — examples
> **Sample code. Not maintained and not accepting contributions.**
A gallery of standalone HTML examples that accompany the blog post on using HTML
as a flexible output format. Each file is a self-contained `.html` page (no build
step, no dependencies) demonstrating a different use case — from code review and
design systems to slide decks, status reports, and small interactive editors.
Open [`index.html`](index.html) for the full, categorized index, or open any
numbered file directly in a browser.
## Contents
| Category | Examples |
|---|---|
| Exploration | code approaches, visual designs |
| Code | review, understanding, design systems, component variants |
| Prototyping | animation, interaction |
| Communication | slide deck, status report, incident report, PR write-up |
| Diagrams & research | flowchart, feature/concept explainers |
| Custom editing UIs | triage board, feature flags, prompt tuner |
## Running
There is nothing to install or build. Clone the repo and open `index.html` (or
any individual file) in a web browser.
## A note on sample data
All product names, data, and scenarios in these examples are fictional and used
only for illustration. The placeholder brand "Acme" and any figures shown are
not real.
## Security
See [SECURITY.md](SECURITY.md) for how to report a vulnerability.
## License
Released under the [Apache License 2.0](LICENSE).
+7
View File
@@ -0,0 +1,7 @@
# WeHub 来源说明
- 原始项目:`ThariqS/html-effectiveness`
- 原始仓库:https://github.com/ThariqS/html-effectiveness
- 导入方式:上游默认分支的最新快照
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
+22
View File
@@ -0,0 +1,22 @@
# Security Policy
## Reporting a vulnerability
If you believe you have found a security vulnerability in this repository,
please report it responsibly.
- **Do not** open a public issue for security problems.
- Email **security@anthropic.com** with a description of the issue and steps to
reproduce it.
We will acknowledge your report, investigate, and keep you informed of the
resolution.
## Scope
This repository contains static, self-contained HTML example files with no
server-side components, no build pipeline, and no third-party runtime
dependencies. The most likely relevant reports are issues such as unsafe inline
script behavior or content that could mislead a reader. General questions about
the accompanying blog post are not security issues — please use normal issues
for those.
+824
View File
@@ -0,0 +1,824 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The unreasonable effectiveness of HTML — examples</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 1120px; margin: 0 auto; padding: 0 32px 140px; }
/* ── masthead ─────────────────────────── */
header.masthead {
padding: 80px 0 56px;
border-bottom: 1.5px solid var(--g300);
margin-bottom: 12px;
position: relative;
overflow: visible;
}
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 12px;
}
.eyebrow::before {
content: "";
width: 24px; height: 1.5px;
background: var(--clay);
}
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(38px, 5.4vw, 62px);
line-height: 1.06;
letter-spacing: -0.018em;
margin: 0 0 8px;
max-width: 17ch;
}
h1 em {
font-style: italic;
color: var(--clay);
}
.intro {
font-size: 16.5px;
color: var(--g700);
margin: 22px 0 0;
max-width: 620px;
}
.intro a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
.intro a:hover { text-decoration-color: var(--clay); }
.hero-grid {
display: grid;
grid-template-columns: 1fr 340px;
gap: 48px;
align-items: end;
}
@media (max-width: 880px) { .hero-grid { grid-template-columns: 1fr; } }
/* hero figure — markdown vs html */
.hero-fig {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
align-items: end;
}
@media (max-width: 880px) { .hero-fig { max-width: 400px; margin-top: 28px; } }
.hero-fig .pane {
border-radius: 10px;
border: 1.5px solid var(--g300);
background: var(--paper);
padding: 14px;
aspect-ratio: 4/5;
display: flex;
flex-direction: column;
gap: 7px;
position: relative;
}
.hero-fig .pane.md {
background: var(--g100);
transform: rotate(-2.5deg) translateY(6px);
}
.hero-fig .pane.html {
transform: rotate(1.5deg);
border-color: var(--slate);
box-shadow: 0 12px 32px rgba(20,20,19,.10);
}
.hero-fig .tag {
position: absolute;
top: -10px; left: 12px;
font-family: var(--mono);
font-size: 9.5px;
letter-spacing: 0.08em;
text-transform: uppercase;
background: var(--ivory);
padding: 2px 7px;
border: 1.5px solid var(--g300);
border-radius: 6px;
color: var(--g500);
}
.hero-fig .pane.html .tag { border-color: var(--slate); color: var(--slate); }
.hero-fig .l { height: 6px; border-radius: 3px; background: var(--g300); }
.hero-fig .l.w90 { width: 90%; } .hero-fig .l.w75 { width: 75%; }
.hero-fig .l.w60 { width: 60%; } .hero-fig .l.w82 { width: 82%; }
.hero-fig .l.w70 { width: 70%; } .hero-fig .l.w50 { width: 50%; }
.hero-fig .pane.html .l { background: var(--g200); }
.hero-fig .pane.html .blk {
border-radius: 5px;
flex: 1;
background: linear-gradient(135deg, var(--oat), #ECE1CF);
position: relative;
overflow: hidden;
}
.hero-fig .pane.html .blk::after {
content: "";
position: absolute;
left: 18%; right: 18%; top: 28%;
height: 40%;
border: 2px solid var(--clay);
border-radius: 50%;
opacity: .7;
}
.hero-fig .pane.html .row {
display: flex; gap: 6px; align-items: flex-end;
}
.hero-fig .pane.html .bar { flex: 1; border-radius: 3px 3px 0 0; }
.hero-fig .pane.html .bar.b1 { height: 14px; background: var(--olive); }
.hero-fig .pane.html .bar.b2 { height: 26px; background: var(--clay); }
.hero-fig .pane.html .bar.b3 { height: 18px; background: var(--oat); }
.hero-fig .pane.html .bar.b4 { height: 30px; background: var(--slate); }
/* ── toc pills ────────────────────────── */
nav.toc {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 26px 0 0;
}
nav.toc a {
font-size: 12.5px;
padding: 7px 14px;
border: 1.5px solid var(--g300);
border-radius: 999px;
text-decoration: none;
color: var(--g700);
background: var(--paper);
transition: border-color 120ms, color 120ms, background 120ms;
display: inline-flex;
align-items: center;
gap: 7px;
}
nav.toc a .n {
font-family: var(--mono);
font-size: 10px;
color: var(--g500);
}
nav.toc a:hover { border-color: var(--slate); color: var(--slate); }
nav.toc a:hover .n { color: var(--clay); }
/* ── section ──────────────────────────── */
section {
margin-top: 72px;
scroll-margin-top: 28px;
}
.sec-head {
display: flex;
align-items: baseline;
gap: 16px;
margin-bottom: 10px;
}
.sec-head .idx {
font-family: var(--mono);
font-size: 13px;
color: var(--clay);
font-weight: 600;
width: 34px;
flex-shrink: 0;
}
.sec-head h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 27px;
margin: 0;
letter-spacing: -0.012em;
}
.sec-head .count {
font-family: var(--mono);
font-size: 11px;
color: var(--g500);
background: var(--g100);
padding: 2px 8px;
border-radius: 999px;
}
.sec-intro {
font-size: 14.5px;
color: var(--g700);
max-width: 700px;
margin: 0 0 24px 50px;
}
@media (max-width: 640px) { .sec-intro { margin-left: 0; } }
/* ── cards ────────────────────────────── */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(316px, 1fr));
gap: 20px;
margin-left: 50px;
}
@media (max-width: 640px) { .grid { margin-left: 0; } }
a.card {
display: flex;
flex-direction: column;
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
text-decoration: none;
color: inherit;
transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease;
overflow: hidden;
}
a.card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(20, 20, 19, 0.10);
border-color: var(--slate);
}
.thumb {
height: 132px;
background: var(--g100);
border-bottom: 1.5px solid var(--g200);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
transition: background 150ms ease;
}
a.card:hover .thumb { background: var(--oat); }
.thumb svg {
width: 100%; height: 100%;
overflow: visible;
}
.thumb svg .st { stroke: var(--g500); fill: none; stroke-width: 2.5; }
.thumb svg .fl { fill: var(--g300); }
.thumb svg .cl { fill: var(--clay); }
.thumb svg .ol { fill: var(--olive); }
.thumb svg .oa { fill: var(--oat); stroke: var(--g500); stroke-width: 2.5; }
.thumb svg .sl { fill: var(--slate); }
.thumb svg .wh { fill: var(--paper); stroke: var(--g500); stroke-width: 2.5; }
.thumb svg .ln { stroke: var(--g500); stroke-width: 2.5; fill: none; stroke-linecap: round; }
.thumb svg .lc { stroke: var(--clay); stroke-width: 2.5; fill: none; stroke-linecap: round; }
.thumb svg .da { stroke-dasharray: 4 4; }
a.card:hover .thumb svg .fl { fill: var(--g500); }
a.card:hover .thumb svg .oa { fill: var(--paper); }
.body { padding: 18px 20px 16px; display: flex; flex-direction: column; flex: 1; }
.title {
font-family: var(--serif);
font-size: 19px;
font-weight: 500;
line-height: 1.22;
color: var(--slate);
margin-bottom: 7px;
letter-spacing: -0.008em;
}
.desc {
font-size: 13.5px;
color: var(--g700);
line-height: 1.5;
margin-bottom: 16px;
flex: 1;
}
.file {
font-family: var(--mono);
font-size: 11px;
color: var(--g500);
border-top: 1px solid var(--g100);
padding-top: 11px;
display: flex;
align-items: center;
justify-content: space-between;
}
.file .arrow { transition: transform 150ms ease; color: var(--g300); }
a.card:hover .file { color: var(--clay); }
a.card:hover .file .arrow { transform: translateX(3px); color: var(--clay); }
/* ── footer ───────────────────────────── */
footer {
margin-top: 100px;
border-top: 1.5px solid var(--g300);
padding-top: 36px;
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 20px;
flex-wrap: wrap;
font-size: 13px;
color: var(--g500);
}
footer .k { font-family: var(--serif); font-style: italic; color: var(--g700); font-size: 15px; }
footer a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
</style>
</head>
<body>
<div class="wrap">
<header class="masthead">
<div class="hero-grid">
<div>
<div class="eyebrow">Companion to the blog post</div>
<h1>The unreasonable <em>effectiveness</em> of HTML</h1>
<p class="intro">
Twenty self-contained <code>.html</code> files an agent produced instead of a wall of markdown.
Each one trades a document you'd skim for one you'd actually read — open any of them directly in
a browser. Grouped by the kind of work they replace.
</p>
<nav class="toc">
<a href="#exploration">Exploration &amp; Planning <span class="n">3</span></a>
<a href="#code-review">Code Review <span class="n">3</span></a>
<a href="#design">Design <span class="n">2</span></a>
<a href="#prototyping">Prototyping <span class="n">2</span></a>
<a href="#illustrations">Diagrams <span class="n">2</span></a>
<a href="#decks">Decks <span class="n">1</span></a>
<a href="#research">Research <span class="n">2</span></a>
<a href="#reports">Reports <span class="n">2</span></a>
<a href="#editors">Custom Editors <span class="n">3</span></a>
<a href="unknowns/index.html" style="border-color: var(--clay); color: var(--clay-d);">Know your unknowns <span class="n">11</span></a>
</nav>
</div>
<div class="hero-fig" aria-hidden="true">
<div class="pane md">
<span class="tag">.md</span>
<span class="l w90"></span><span class="l w75"></span><span class="l w82"></span>
<span class="l w60"></span><span class="l w90"></span><span class="l w70"></span>
<span class="l w82"></span><span class="l w50"></span><span class="l w75"></span>
<span class="l w90"></span><span class="l w60"></span>
</div>
<div class="pane html">
<span class="tag">.html</span>
<span class="l w60"></span>
<span class="blk"></span>
<span class="row"><span class="bar b1"></span><span class="bar b2"></span><span class="bar b3"></span><span class="bar b4"></span></span>
<span class="l w75"></span><span class="l w50"></span>
</div>
</div>
</div>
</header>
<!-- ============================== Exploration & Planning ============================== -->
<section id="exploration">
<div class="sec-head"><span class="idx">01</span><h2>Exploration &amp; Planning</h2><span class="count">3 demos</span></div>
<p class="sec-intro">
When you're not sure what you want yet. Ask the agent to fan out across several directions and lay
them next to each other so you can point at one — instead of reading three sequential walls of text
and trying to hold them all in your head. And once you've picked, turn the pick into a plan the
implementer can actually read.
</p>
<div class="grid">
<a class="card" href="01-exploration-code-approaches.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="wh" x="4" y="10" width="32" height="60" rx="5"/>
<rect class="wh" x="44" y="10" width="32" height="60" rx="5"/>
<rect class="oa" x="84" y="10" width="32" height="60" rx="5"/>
<line class="ln" x1="10" y1="26" x2="30" y2="26"/><line class="ln" x1="10" y1="36" x2="26" y2="36"/>
<line class="ln" x1="50" y1="26" x2="70" y2="26"/><line class="ln" x1="50" y1="36" x2="66" y2="36"/>
<line class="lc" x1="90" y1="26" x2="110" y2="26"/><line class="lc" x1="90" y1="36" x2="106" y2="36"/>
<circle class="cl" cx="100" cy="56" r="6"/>
</svg></div>
<div class="body">
<div class="title">Three code approaches</div>
<div class="desc">Side-by-side comparison of three ways to solve the same problem, with trade-offs called out inline.</div>
<div class="file"><span>01-exploration-code-approaches.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="02-exploration-visual-designs.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="wh" x="6" y="8" width="50" height="30" rx="4"/>
<rect class="oa" x="64" y="8" width="50" height="30" rx="4"/>
<rect class="oa" x="6" y="44" width="50" height="30" rx="4"/>
<rect class="wh" x="64" y="44" width="50" height="30" rx="4"/>
<circle class="cl" cx="18" cy="20" r="5"/><rect class="fl" x="28" y="16" width="22" height="4" rx="2"/>
<rect class="ol" x="72" y="16" width="34" height="6" rx="3"/>
<rect class="sl" x="14" y="54" width="34" height="6" rx="3"/>
<circle class="ol" cx="102" cy="58" r="6"/>
</svg></div>
<div class="body">
<div class="title">Visual design directions</div>
<div class="desc">A handful of layout and palette options rendered live so you can react to them, not imagine them.</div>
<div class="file"><span>02-exploration-visual-designs.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="16-implementation-plan.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<line class="ln" x1="14" y1="12" x2="14" y2="68"/>
<circle class="cl" cx="14" cy="16" r="5"/><circle class="ol" cx="14" cy="40" r="5"/><circle class="fl" cx="14" cy="64" r="5"/>
<rect class="fl" x="28" y="12" width="48" height="4" rx="2"/><rect class="fl" x="28" y="21" width="34" height="4" rx="2"/>
<rect class="fl" x="28" y="36" width="42" height="4" rx="2"/><rect class="fl" x="28" y="45" width="28" height="4" rx="2"/>
<rect class="fl" x="28" y="60" width="44" height="4" rx="2"/>
<rect class="oa" x="86" y="12" width="28" height="24" rx="4"/>
<rect class="wh" x="86" y="44" width="28" height="24" rx="4"/>
<line class="lc" x1="92" y1="56" x2="108" y2="56"/>
</svg></div>
<div class="body">
<div class="title">Implementation plan</div>
<div class="desc">Milestones on a timeline, a data-flow diagram, inline mockups, the risky code, and a risk table — the plan you hand off.</div>
<div class="file"><span>16-implementation-plan.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<!-- ============================== Code Review & Understanding ============================== -->
<section id="code-review">
<div class="sec-head"><span class="idx">02</span><h2>Code Review &amp; Understanding</h2><span class="count">3 demos</span></div>
<p class="sec-intro">
Diffs and call-graphs are spatial information; markdown flattens them. Let the agent render the
change as an annotated diff, draw the module as boxes and arrows, or write the PR description your
reviewers actually want — so the shape of the code is visible at a glance.
</p>
<div class="grid">
<a class="card" href="03-code-review-pr.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="wh" x="6" y="8" width="72" height="64" rx="5"/>
<line class="ln" x1="14" y1="20" x2="66" y2="20"/>
<rect class="ol" x="14" y="28" width="56" height="7" rx="2" opacity=".5"/>
<rect class="cl" x="14" y="38" width="48" height="7" rx="2" opacity=".5"/>
<line class="ln" x1="14" y1="52" x2="58" y2="52"/><line class="ln" x1="14" y1="62" x2="50" y2="62"/>
<rect class="oa" x="86" y="22" width="28" height="20" rx="4"/>
<line class="lc da" x1="78" y1="32" x2="86" y2="32"/>
<circle class="cl" cx="100" cy="58" r="7"/>
</svg></div>
<div class="body">
<div class="title">Annotated pull request</div>
<div class="desc">A diff rendered with margin notes, severity tags and jump links — easier to scan than scrolling a terminal.</div>
<div class="file"><span>03-code-review-pr.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="17-pr-writeup.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="oa" x="6" y="8" width="48" height="28" rx="4"/>
<rect class="wh" x="6" y="44" width="48" height="28" rx="4"/>
<line class="ln" x1="14" y1="20" x2="46" y2="20"/><line class="ln" x1="14" y1="28" x2="38" y2="28"/>
<line class="lc" x1="14" y1="56" x2="46" y2="56"/><line class="ln" x1="14" y1="64" x2="38" y2="64"/>
<rect class="wh" x="64" y="8" width="50" height="64" rx="5"/>
<circle class="cl" cx="74" cy="22" r="5"/><rect class="fl" x="84" y="19" width="24" height="5" rx="2"/>
<circle class="ol" cx="74" cy="42" r="5"/><rect class="fl" x="84" y="39" width="22" height="5" rx="2"/>
<circle class="fl" cx="74" cy="62" r="5"/><rect class="fl" x="84" y="59" width="26" height="5" rx="2"/>
</svg></div>
<div class="body">
<div class="title">PR writeup for reviewers</div>
<div class="desc">The author's side: motivation, before/after, a file-by-file tour with the <em>why</em>, and where to focus the review.</div>
<div class="file"><span>17-pr-writeup.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="04-code-understanding.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="wh" x="44" y="8" width="32" height="20" rx="4"/>
<rect class="oa" x="8" y="52" width="32" height="20" rx="4"/>
<rect class="cl" x="44" y="52" width="32" height="20" rx="4"/>
<rect class="wh" x="80" y="52" width="32" height="20" rx="4"/>
<line class="ln" x1="50" y1="28" x2="26" y2="52"/>
<line class="lc" x1="60" y1="28" x2="60" y2="52"/>
<line class="ln" x1="70" y1="28" x2="94" y2="52"/>
</svg></div>
<div class="body">
<div class="title">Module map</div>
<div class="desc">An unfamiliar package drawn as boxes and arrows, with the hot path highlighted and entry points listed.</div>
<div class="file"><span>04-code-understanding.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<!-- ============================== Design ============================== -->
<section id="design">
<div class="sec-head"><span class="idx">03</span><h2>Design</h2><span class="count">2 demos</span></div>
<p class="sec-intro">
HTML <em>is</em> the medium your design system ships in, so it's the natural format for talking about
it. Tokens become swatches, components become contact sheets, and the artifact can be fed straight
back into the next prompt.
</p>
<div class="grid">
<a class="card" href="05-design-system.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="cl" x="8" y="10" width="22" height="22" rx="4"/>
<rect class="ol" x="36" y="10" width="22" height="22" rx="4"/>
<rect class="oa" x="64" y="10" width="22" height="22" rx="4"/>
<rect class="sl" x="92" y="10" width="22" height="22" rx="4"/>
<rect class="fl" x="8" y="42" width="80" height="8" rx="2"/>
<rect class="fl" x="8" y="56" width="56" height="6" rx="2"/>
<rect class="fl" x="8" y="68" width="40" height="5" rx="2"/>
</svg></div>
<div class="body">
<div class="title">Living design system</div>
<div class="desc">Colors, type scale and spacing tokens pulled from a repo and rendered as swatches you can copy from.</div>
<div class="file"><span>05-design-system.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="06-component-variants.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="cl" x="10" y="14" width="44" height="14" rx="7"/>
<rect class="wh" x="10" y="34" width="44" height="14" rx="7"/>
<rect class="oa" x="10" y="54" width="44" height="14" rx="7"/>
<rect class="cl" x="66" y="14" width="34" height="11" rx="5.5"/>
<rect class="wh" x="66" y="32" width="34" height="11" rx="5.5"/>
<rect class="oa" x="66" y="50" width="34" height="11" rx="5.5"/>
<circle class="cl" cx="110" cy="20" r="4"/><circle class="fl" cx="110" cy="37" r="4"/><circle class="ol" cx="110" cy="56" r="4"/>
</svg></div>
<div class="body">
<div class="title">Component variants</div>
<div class="desc">Every size, state and intent of one component laid out on a single sheet for review.</div>
<div class="file"><span>06-component-variants.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<!-- ============================== Prototyping ============================== -->
<section id="prototyping">
<div class="sec-head"><span class="idx">04</span><h2>Prototyping</h2><span class="count">2 demos</span></div>
<p class="sec-intro">
Motion and interaction can't be described, only felt. A throwaway page with the real easing curve or
the real click-through tells you in five seconds what a paragraph of prose never could.
</p>
<div class="grid">
<a class="card" href="07-prototype-animation.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="wh" x="8" y="10" width="104" height="34" rx="5"/>
<path class="lc" d="M16 38 C 40 38, 56 16, 104 16" fill="none"/>
<circle class="cl" cx="72" cy="23" r="4.5"/>
<line class="ln" x1="12" y1="58" x2="108" y2="58"/><circle class="cl" cx="44" cy="58" r="6"/>
<line class="ln" x1="12" y1="70" x2="108" y2="70"/><circle class="ol" cx="82" cy="70" r="6"/>
</svg></div>
<div class="body">
<div class="title">Animation sandbox</div>
<div class="desc">The transition in isolation with sliders for duration and easing, so you can tune it before wiring it in.</div>
<div class="file"><span>07-prototype-animation.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="08-prototype-interaction.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="wh" x="6" y="22" width="28" height="36" rx="4"/>
<rect class="oa" x="46" y="22" width="28" height="36" rx="4"/>
<rect class="wh" x="86" y="22" width="28" height="36" rx="4"/>
<line class="lc" x1="34" y1="40" x2="46" y2="40"/>
<line class="lc" x1="74" y1="40" x2="86" y2="40"/>
<circle class="cl" cx="20" cy="50" r="4"/><rect class="fl" x="52" y="30" width="16" height="4" rx="2"/><rect class="ol" x="92" y="46" width="16" height="6" rx="3"/>
</svg></div>
<div class="body">
<div class="title">Clickable flow</div>
<div class="desc">Four screens linked together — enough fidelity to feel whether the interaction is right.</div>
<div class="file"><span>08-prototype-interaction.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<!-- ============================== Illustrations & Diagrams ============================== -->
<section id="illustrations">
<div class="sec-head"><span class="idx">05</span><h2>Illustrations &amp; Diagrams</h2><span class="count">2 demos</span></div>
<p class="sec-intro">
Inline SVG gives the agent a real pen. Ask for the figures for a post or a flowchart of a process and
get vector art you can tweak by hand or paste straight into the final document.
</p>
<div class="grid">
<a class="card" href="10-svg-illustrations.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<circle class="oa" cx="32" cy="40" r="24"/>
<circle class="cl" cx="32" cy="40" r="11"/>
<path class="lc" d="M68 60 L86 20 L104 60" fill="none"/>
<line class="ln" x1="66" y1="66" x2="106" y2="66"/>
<circle class="ol" cx="86" cy="20" r="5"/>
</svg></div>
<div class="body">
<div class="title">SVG figure sheet</div>
<div class="desc">The diagrams for a blog post, drawn inline so they can be tweaked and copied out one by one.</div>
<div class="file"><span>10-svg-illustrations.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="13-flowchart-diagram.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="wh" x="10" y="10" width="30" height="16" rx="4"/>
<path class="oa" d="M60 34 L80 46 L60 58 L40 46 Z"/>
<rect class="wh" x="82" y="10" width="30" height="16" rx="4"/>
<rect class="cl" x="82" y="56" width="30" height="16" rx="4"/>
<line class="ln" x1="25" y1="26" x2="25" y2="46"/><line class="ln" x1="25" y1="46" x2="40" y2="46"/>
<line class="lc" x1="80" y1="46" x2="97" y2="46"/><line class="lc" x1="97" y1="46" x2="97" y2="56"/>
<line class="ln" x1="60" y1="34" x2="60" y2="22"/><line class="ln" x1="60" y1="22" x2="82" y2="22"/>
</svg></div>
<div class="body">
<div class="title">Annotated flowchart</div>
<div class="desc">A deploy pipeline drawn as a real flowchart — click any step to see what runs, timings, and failure paths.</div>
<div class="file"><span>13-flowchart-diagram.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<!-- ============================== Decks ============================== -->
<section id="decks">
<div class="sec-head"><span class="idx">06</span><h2>Decks</h2><span class="count">1 demo</span></div>
<p class="sec-intro">
A handful of <code>&lt;section&gt;</code> tags and twenty lines of JS is a slide deck. Point the agent at a
Slack thread or a design doc and get something you can arrow-key through in a meeting — no Keynote,
no export step.
</p>
<div class="grid">
<a class="card" href="09-slide-deck.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="oa" x="8" y="18" width="70" height="44" rx="5"/>
<rect class="wh" x="22" y="12" width="76" height="50" rx="5"/>
<rect class="cl" x="32" y="24" width="36" height="7" rx="3"/>
<rect class="fl" x="32" y="36" width="52" height="4" rx="2"/><rect class="fl" x="32" y="44" width="44" height="4" rx="2"/>
<circle class="fl" cx="50" cy="70" r="3"/><circle class="cl" cx="60" cy="70" r="3"/><circle class="fl" cx="70" cy="70" r="3"/>
</svg></div>
<div class="body">
<div class="title">Arrow-key slide deck</div>
<div class="desc">A short presentation as one HTML file. Left and right to navigate, no build step.</div>
<div class="file"><span>09-slide-deck.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<!-- ============================== Research & Learning ============================== -->
<section id="research">
<div class="sec-head"><span class="idx">07</span><h2>Research &amp; Learning</h2><span class="count">2 demos</span></div>
<p class="sec-intro">
An explainer with collapsible sections, tabbed code samples and a glossary in the margin reads very
differently from the same words dumped linearly. The agent can build the scaffolding that makes a new
topic navigable.
</p>
<div class="grid">
<a class="card" href="14-research-feature-explainer.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="cl" x="8" y="8" width="104" height="20" rx="4" opacity=".35"/>
<rect class="fl" x="14" y="15" width="60" height="6" rx="3"/>
<rect class="wh" x="8" y="34" width="104" height="14" rx="4"/>
<rect class="wh" x="8" y="52" width="104" height="14" rx="4"/>
<path class="ln" d="M16 39 l4 4 l-4 4"/><rect class="fl" x="28" y="38" width="46" height="5" rx="2.5"/>
<path class="ln" d="M16 57 l4 4 l-4 4"/><rect class="fl" x="28" y="56" width="38" height="5" rx="2.5"/>
</svg></div>
<div class="body">
<div class="title">How a feature works</div>
<div class="desc">"Explain rate limiting in this repo" — TL;DR box, collapsible request-path steps, tabbed config snippets, and an FAQ.</div>
<div class="file"><span>14-research-feature-explainer.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="15-research-concept-explainer.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<circle class="st" cx="42" cy="40" r="26"/>
<circle class="cl" cx="42" cy="14" r="5"/><circle class="ol" cx="68" cy="40" r="5"/>
<circle class="fl" cx="42" cy="66" r="5"/><circle class="fl" cx="16" cy="40" r="5"/>
<rect class="fl" x="82" y="20" width="30" height="5" rx="2.5"/><rect class="fl" x="82" y="32" width="24" height="5" rx="2.5"/>
<rect class="fl" x="82" y="44" width="28" height="5" rx="2.5"/><rect class="fl" x="82" y="56" width="20" height="5" rx="2.5"/>
</svg></div>
<div class="body">
<div class="title">Concept explainer</div>
<div class="desc">Consistent hashing taught with a live ring you can add/remove nodes from, a comparison table, and a hover-linked glossary.</div>
<div class="file"><span>15-research-concept-explainer.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<!-- ============================== Reports ============================== -->
<section id="reports">
<div class="sec-head"><span class="idx">08</span><h2>Reports</h2><span class="count">2 demos</span></div>
<p class="sec-intro">
Recurring documents — status updates, post-mortems — benefit most from a bit of structure and color.
A small chart and a colored timeline turn something people skim into something they actually read.
</p>
<div class="grid">
<a class="card" href="11-status-report.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="fl" x="8" y="10" width="54" height="6" rx="3"/>
<rect class="ol" x="8" y="24" width="36" height="8" rx="3" opacity=".6"/>
<rect class="oa" x="8" y="38" width="28" height="8" rx="3"/>
<rect class="cl" x="8" y="52" width="20" height="8" rx="3" opacity=".6"/>
<rect class="wh" x="72" y="16" width="40" height="48" rx="5"/>
<rect class="ol" x="80" y="44" width="6" height="14"/>
<rect class="cl" x="90" y="34" width="6" height="24"/>
<rect class="fl" x="100" y="50" width="6" height="8"/>
</svg></div>
<div class="body">
<div class="title">Weekly status</div>
<div class="desc">What shipped, what slipped, and a small chart — formatted for a quick skim on Monday morning.</div>
<div class="file"><span>11-status-report.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="12-incident-report.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<line class="ln" x1="18" y1="12" x2="18" y2="68"/>
<circle class="fl" cx="18" cy="16" r="5"/><circle class="cl" cx="18" cy="34" r="5"/>
<circle class="cl" cx="18" cy="50" r="5"/><circle class="ol" cx="18" cy="66" r="5"/>
<rect class="fl" x="32" y="13" width="40" height="5" rx="2.5"/>
<rect class="fl" x="32" y="31" width="56" height="5" rx="2.5"/>
<rect class="fl" x="32" y="47" width="48" height="5" rx="2.5"/>
<rect class="fl" x="32" y="63" width="36" height="5" rx="2.5"/>
<rect class="oa" x="94" y="26" width="20" height="30" rx="4"/>
</svg></div>
<div class="body">
<div class="title">Incident timeline</div>
<div class="desc">A post-mortem with a minute-by-minute timeline, log excerpts and the follow-up checklist.</div>
<div class="file"><span>12-incident-report.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<!-- ============================== Custom Editing Interfaces ============================== -->
<section id="editors">
<div class="sec-head"><span class="idx">09</span><h2>Custom Editing Interfaces</h2><span class="count">3 demos</span></div>
<p class="sec-intro">
Sometimes it's hard to describe what you want in a text box. Ask for a throwaway editor for the exact
thing you're working on — and always end with an export button that turns whatever you did in the UI
back into something you can paste into the agent or commit. You stay in the loop; the loop gets tighter.
</p>
<div class="grid">
<a class="card" href="18-editor-triage-board.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="wh" x="6" y="10" width="24" height="60" rx="4"/>
<rect class="wh" x="34" y="10" width="24" height="60" rx="4"/>
<rect class="wh" x="62" y="10" width="24" height="60" rx="4"/>
<rect class="wh" x="90" y="10" width="24" height="60" rx="4"/>
<rect class="cl" x="10" y="18" width="16" height="9" rx="2"/>
<rect class="fl" x="10" y="31" width="16" height="9" rx="2"/>
<rect class="fl" x="10" y="44" width="16" height="9" rx="2"/>
<rect class="fl" x="38" y="18" width="16" height="9" rx="2"/>
<rect class="ol" x="38" y="31" width="16" height="9" rx="2"/>
<rect class="fl" x="66" y="18" width="16" height="9" rx="2"/>
<rect class="oa" x="48" y="50" width="20" height="11" rx="2"/>
</svg></div>
<div class="body">
<div class="title">Ticket triage board</div>
<div class="desc">Drag thirty tickets across Now / Next / Later / Cut, then copy the final ordering out as markdown.</div>
<div class="file"><span>18-editor-triage-board.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="19-editor-feature-flags.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="wh" x="8" y="10" width="104" height="14" rx="4"/>
<rect class="wh" x="8" y="30" width="104" height="14" rx="4"/>
<rect class="wh" x="8" y="50" width="104" height="14" rx="4"/>
<rect class="ol" x="88" y="13" width="18" height="8" rx="4"/><circle class="wh" cx="100" cy="17" r="3"/>
<rect class="fl" x="88" y="33" width="18" height="8" rx="4"/><circle class="wh" cx="93" cy="37" r="3"/>
<rect class="cl" x="88" y="53" width="18" height="8" rx="4"/><circle class="wh" cx="100" cy="57" r="3"/>
<rect class="fl" x="14" y="14" width="34" height="5" rx="2.5"/>
<rect class="fl" x="14" y="34" width="42" height="5" rx="2.5"/>
<rect class="fl" x="14" y="54" width="28" height="5" rx="2.5"/>
<line class="lc da" x1="62" y1="24" x2="62" y2="50"/>
</svg></div>
<div class="body">
<div class="title">Feature flag editor</div>
<div class="desc">Toggles grouped by area, dependency warnings when a prerequisite is off, and a "copy diff" button for just the changed keys.</div>
<div class="file"><span>19-editor-feature-flags.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="20-editor-prompt-tuner.html">
<div class="thumb"><svg viewBox="0 0 120 80">
<rect class="wh" x="6" y="10" width="56" height="60" rx="4"/>
<rect class="fl" x="12" y="18" width="40" height="4" rx="2"/>
<rect class="cl" x="12" y="26" width="22" height="4" rx="2"/>
<rect class="fl" x="12" y="34" width="44" height="4" rx="2"/>
<rect class="ol" x="12" y="42" width="18" height="4" rx="2"/>
<rect class="fl" x="12" y="50" width="36" height="4" rx="2"/>
<rect class="oa" x="68" y="10" width="46" height="18" rx="4"/>
<rect class="oa" x="68" y="32" width="46" height="18" rx="4"/>
<rect class="oa" x="68" y="54" width="46" height="18" rx="4"/>
</svg></div>
<div class="body">
<div class="title">Prompt tuner</div>
<div class="desc">Editable template on the left with variable slots highlighted; three sample inputs on the right re-render live as you type.</div>
<div class="file"><span>20-editor-prompt-tuner.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<footer>
<span class="k">Everything on this page is itself a single <code>.html</code> file. See also: <a href="unknowns/index.html">Know your unknowns</a>.</span>
<span>View the repo · <a href="https://thariqs.github.io/html-effectiveness/">thariqs.github.io/html-effectiveness</a></span>
</footer>
</div>
</body>
</html>
+679
View File
@@ -0,0 +1,679 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Blindspot pass — Know your unknowns</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px 100px; }
/* ── page header ─────────────────────── */
header.page-head {
padding: 56px 0 8px;
position: relative;
}
.head-row {
display: flex;
align-items: baseline;
justify-content: space-between;
gap: 16px;
flex-wrap: wrap;
}
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
display: flex;
align-items: center;
gap: 12px;
}
.eyebrow::before {
content: "";
width: 24px; height: 1.5px;
background: var(--clay);
flex: none;
}
a.back {
font-family: var(--mono);
font-size: 12px;
color: var(--g500);
text-decoration: none;
letter-spacing: 0.04em;
white-space: nowrap;
}
a.back:hover { color: var(--clay); }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(32px, 5vw, 46px);
line-height: 1.08;
letter-spacing: -0.018em;
margin: 18px 0 10px;
}
.lede {
font-size: 16px;
color: var(--g700);
max-width: 640px;
margin: 0 0 28px;
}
/* ── prompt box ──────────────────────── */
.prompt-card {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 22px 26px 24px;
position: relative;
}
.prompt-label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--clay);
margin-bottom: 12px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
}
blockquote.prompt {
margin: 0;
font-family: var(--serif);
font-style: italic;
font-size: 17.5px;
line-height: 1.55;
color: var(--slate);
border-left: 3px solid var(--oat);
padding-left: 18px;
}
button.copy-btn {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.06em;
text-transform: uppercase;
color: var(--g700);
background: var(--g100);
border: 1.5px solid var(--g300);
border-radius: 8px;
padding: 5px 12px;
cursor: pointer;
transition: border-color 120ms, color 120ms, background 120ms;
white-space: nowrap;
}
button.copy-btn:hover { border-color: var(--clay); color: var(--clay-d); }
button.copy-btn.copied {
background: var(--olive);
border-color: var(--olive);
color: var(--paper);
}
/* ── divider ─────────────────────────── */
.divider {
display: flex;
align-items: center;
gap: 16px;
margin: 44px 0 32px;
font-family: var(--mono);
font-size: 11.5px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--g500);
white-space: nowrap;
}
.divider::before, .divider::after {
content: "";
flex: 1;
height: 1.5px;
background: var(--g300);
}
/* ── artifact frame ──────────────────── */
.artifact {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
overflow: hidden;
}
.artifact-bar {
display: flex;
align-items: center;
gap: 8px;
padding: 11px 18px;
border-bottom: 1.5px solid var(--g200);
background: var(--g100);
}
.artifact-bar .dot { width: 9px; height: 9px; border-radius: 50%; background: var(--g300); }
.artifact-bar .file {
font-family: var(--mono);
font-size: 11.5px;
color: var(--g500);
margin-left: 8px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.artifact-body { padding: 34px 34px 40px; }
@media (max-width: 560px) { .artifact-body { padding: 24px 18px 30px; } }
.artifact-body h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 27px;
letter-spacing: -0.014em;
line-height: 1.15;
margin: 0 0 6px;
}
.artifact-body .sub {
font-size: 14px;
color: var(--g500);
margin: 0 0 24px;
}
.artifact-body .sub code {
font-family: var(--mono);
font-size: 12.5px;
background: var(--g100);
border: 1px solid var(--g200);
border-radius: 5px;
padding: 1px 5px;
color: var(--g700);
}
/* asked vs walking into */
.vs-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
margin-bottom: 34px;
}
@media (max-width: 640px) { .vs-grid { grid-template-columns: 1fr; } }
.vs-cell {
border: 1.5px solid var(--g300);
border-radius: 12px;
padding: 16px 18px;
background: var(--ivory);
}
.vs-cell.actual { border-color: var(--clay); background: #FBF3EF; }
.vs-cell .vs-tag {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
margin-bottom: 8px;
}
.vs-cell.actual .vs-tag { color: var(--clay-d); }
.vs-cell p { margin: 0; font-size: 14px; color: var(--g700); }
.vs-cell p strong { color: var(--slate); }
/* stat strip */
.stat-strip {
display: flex;
flex-wrap: wrap;
gap: 10px 26px;
padding: 14px 18px;
border: 1.5px solid var(--g200);
border-radius: 12px;
background: var(--g100);
margin-bottom: 36px;
font-family: var(--mono);
font-size: 12px;
color: var(--g700);
}
.stat-strip b { color: var(--slate); font-weight: 600; }
.stat-strip .warn b { color: var(--clay-d); }
/* section labels inside artifact */
.art-eyebrow {
font-family: var(--mono);
font-size: 11.5px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
display: flex;
align-items: center;
gap: 12px;
margin: 0 0 18px;
}
.art-eyebrow::before {
content: "";
width: 24px; height: 1.5px;
background: var(--clay);
flex: none;
}
/* blindspot cards */
.bs-card {
border: 1.5px solid var(--g300);
border-radius: 12px;
background: var(--paper);
padding: 20px 22px 18px;
margin-bottom: 16px;
}
.bs-head {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 10px;
}
.badge {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.1em;
font-weight: 600;
padding: 3px 9px;
border-radius: 6px;
border: 1.5px solid;
white-space: nowrap;
}
.badge.landmine { color: var(--clay-d); border-color: var(--clay); background: #FBF0EA; }
.badge.history { color: #8A6A3B; border-color: #C9B48E; background: var(--oat); }
.badge.convention { color: var(--g700); border-color: var(--g300); background: var(--g100); }
.badge.concept { color: #55663F; border-color: var(--olive); background: #EFF2E9; }
.bs-num {
font-family: var(--mono);
font-size: 11px;
color: var(--g500);
}
.bs-title {
font-family: var(--serif);
font-weight: 500;
font-size: 18.5px;
letter-spacing: -0.01em;
color: var(--slate);
margin: 0;
flex: 1 1 100%;
}
.bs-card p { font-size: 14px; color: var(--g700); margin: 0 0 10px; }
.bs-card p:last-of-type { margin-bottom: 0; }
.bs-card code, .bite code {
font-family: var(--mono);
font-size: 12px;
background: var(--g100);
border: 1px solid var(--g200);
border-radius: 5px;
padding: 1px 5px;
color: var(--g700);
word-break: break-word;
}
.bite {
display: flex;
gap: 10px;
font-size: 13.5px;
color: var(--g700);
background: var(--ivory);
border-left: 3px solid var(--clay);
border-radius: 0 8px 8px 0;
padding: 10px 14px;
margin: 12px 0;
}
.bite .bite-tag {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--clay-d);
flex: none;
padding-top: 2px;
}
/* prompt chips */
.chip-row {
display: flex;
align-items: stretch;
gap: 8px;
margin-top: 12px;
}
.prompt-chip {
flex: 1;
font-family: var(--mono);
font-size: 12px;
line-height: 1.5;
color: var(--slate);
background: var(--g100);
border: 1.5px solid var(--g300);
border-radius: 9px;
padding: 10px 13px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-word;
}
.prompt-chip::before {
content: " ";
color: var(--clay);
font-weight: 600;
}
.chip-row button.copy-btn { align-self: flex-start; margin-top: 2px; }
@media (max-width: 480px) {
.chip-row { flex-direction: column; }
.chip-row button.copy-btn { align-self: flex-end; margin-top: 0; }
}
/* improved prompt card */
.final-card {
border: 1.5px solid var(--slate);
border-radius: 14px;
background: var(--paper);
padding: 24px 26px 26px;
margin-top: 38px;
box-shadow: 0 10px 28px rgba(20,20,19,.07);
}
.final-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 6px;
}
.final-head .art-eyebrow { margin: 0; color: var(--clay-d); }
.final-card h3 {
font-family: var(--serif);
font-weight: 500;
font-size: 22px;
letter-spacing: -0.012em;
margin: 8px 0 14px;
}
.final-prompt {
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.65;
color: var(--slate);
background: var(--ivory);
border: 1.5px solid var(--g200);
border-radius: 10px;
padding: 18px 20px;
white-space: pre-wrap;
word-break: break-word;
margin: 0;
overflow-x: auto;
}
.final-note {
font-size: 13px;
color: var(--g500);
margin: 14px 0 0;
}
/* footer */
footer {
margin-top: 56px;
padding-top: 22px;
border-top: 1.5px solid var(--g200);
font-size: 13px;
color: var(--g500);
}
footer a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
footer a:hover { text-decoration-color: var(--clay); }
</style>
</head>
<body>
<div class="wrap">
<header class="page-head">
<div class="head-row">
<div class="eyebrow">Know your unknowns · Pre-implementation</div>
<a class="back" href="index.html">&larr; All examples</a>
</div>
<h1>Blindspot pass</h1>
<p class="lede">Before touching unfamiliar code, ask Claude to surface what you don't know you don't know — and to tell you how to prompt around each gap.</p>
<div class="prompt-card">
<div class="prompt-label">
<span>The prompt</span>
<button class="copy-btn" data-copy-target="user-prompt">Copy prompt</button>
</div>
<blockquote class="prompt" id="user-prompt">I'm adding a new SSO auth provider to Acme but I've never touched the auth module. Do a blindspot pass: find my unknown unknowns in this part of the codebase, explain each one, and tell me how to prompt you better for the implementation.</blockquote>
</div>
</header>
<div class="divider">What Claude produced</div>
<div class="artifact">
<div class="artifact-bar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<span class="file">blindspot-pass · acme/services/auth · 47 files scanned</span>
</div>
<div class="artifact-body">
<h2>Unknown unknowns: the Acme auth module</h2>
<p class="sub">Scanned <code>services/auth/</code> (47 files), 214 commits touching it since Jan&nbsp;2025, 3 migrations in flight, 2 reverted attempts. Seven things you'd have learned the hard way.</p>
<div class="vs-grid">
<div class="vs-cell">
<div class="vs-tag">What you asked for</div>
<p>"Add a new SSO provider" — which sounds like: implement an interface, register it, write the redirect handlers, done. On a clean codebase, a two-day task.</p>
</div>
<div class="vs-cell actual">
<div class="vs-tag">What you're actually walking into</div>
<p>A module <strong>mid-migration on session storage</strong>, with <strong>one provider that quietly bypasses the middleware</strong> everyone assumes runs, a <strong>reverted previous attempt</strong> at exactly this task, and an <strong>identity-linking model</strong> that isn't visible from the provider interface at all.</p>
</div>
</div>
<div class="stat-strip">
<span class="warn"><b>4</b> landmines</span>
<span><b>2</b> unwritten conventions</span>
<span><b>1</b> missing concept</span>
<span><b>1</b> reverted attempt at this exact task (PR&nbsp;#2841)</span>
</div>
<div class="art-eyebrow">The blindspots</div>
<!-- 1 -->
<div class="bs-card">
<div class="bs-head">
<span class="badge landmine">Landmine</span>
<span class="bs-num">01</span>
<h3 class="bs-title">Sessions are double-written — the Redis store you'll find first is the wrong one</h3>
</div>
<p>Acme is mid-migration from Postgres-backed sessions to Redis (<code>MIG-118</code>, stalled since March). Every session write currently goes through <code>SessionBridge.write()</code>, which fans out to <em>both</em> <code>RedisSessionStore</code> and the legacy <code>pg_sessions</code> table. The migration was never finished, so reads still come from Postgres.</p>
<div class="bite"><span class="bite-tag">Why it bites</span><span>Grepping for "session" surfaces <code>RedisSessionStore</code> first — it looks canonical, has the newest code, and the best docs. Write to it directly and your logins will appear to work in dev (Redis reads are enabled there) and silently fail in prod, where reads still hit Postgres.</span></div>
<div class="chip-row">
<div class="prompt-chip" id="chip-1">When you create sessions for the new provider, route everything through SessionBridge — never RedisSessionStore or pg_sessions directly. Explain the MIG-118 read/write split before writing any session code.</div>
<button class="copy-btn" data-copy-target="chip-1">Copy</button>
</div>
</div>
<!-- 2 -->
<div class="bs-card">
<div class="bs-head">
<span class="badge landmine">Landmine</span>
<span class="bs-num">02</span>
<h3 class="bs-title">The SAML provider — your obvious template — bypasses the auth middleware</h3>
</div>
<p>Every provider except SAML mounts under <code>authPipeline</code>, which runs rate limiting, audit logging, and the device-trust check. <code>saml/handler.ts</code> mounts its ACS endpoint directly on the router because SP-initiated POST callbacks broke the CSRF layer in 2024, and nobody circled back. There's a <code>// TODO(priya): fold back into pipeline</code> from 14 months ago.</p>
<div class="bite"><span class="bite-tag">Why it bites</span><span>SAML is the most recently added provider, so it's the natural file to copy. Copy it and your new provider ships with no rate limiting and no audit trail — which won't fail any test, because the contract tests don't assert middleware presence.</span></div>
<div class="chip-row">
<div class="prompt-chip" id="chip-2">Use oauth/google.ts as the structural template, not saml/handler.ts — SAML intentionally bypasses authPipeline. Confirm the new provider's routes are mounted inside authPipeline and show me the mount point.</div>
<button class="copy-btn" data-copy-target="chip-2">Copy</button>
</div>
</div>
<!-- 3 -->
<div class="bs-card">
<div class="bs-head">
<span class="badge history">History</span>
<span class="bs-num">03</span>
<h3 class="bs-title">Someone already tried this — and it was reverted for a reason that still holds</h3>
</div>
<p>PR&nbsp;#2841 ("Add Okta OIDC provider", Sept&nbsp;2025) was merged and reverted nine days later. The revert commit explains why: Acme callback URLs must carry a tenant suffix (<code>/auth/callback/:provider/:workspaceSlug</code>) so workspace-scoped invite links survive the redirect round-trip. Okta's strict redirect-URI matching rejected the dynamic suffix, and invited users landed in the wrong workspace. The fix everyone agreed on — a state-parameter workspace claim signed with <code>AUTH_STATE_SECRET</code> — was designed in the revert thread but never built.</p>
<div class="bite"><span class="bite-tag">Why it bites</span><span>Nothing in the current code hints at this. You'd re-implement #2841's approach, pass every local test (single-workspace dev env), and rediscover the bug in production the first time an invited user signs in.</span></div>
<div class="chip-row">
<div class="prompt-chip" id="chip-3">Read the revert of PR #2841 first. Implement the signed state-parameter workspace claim it proposed instead of tenant-suffixed callback URLs, and add a test for the workspace-scoped invite flow.</div>
<button class="copy-btn" data-copy-target="chip-3">Copy</button>
</div>
</div>
<!-- 4 -->
<div class="bs-card">
<div class="bs-head">
<span class="badge concept">Missing concept</span>
<span class="bs-num">04</span>
<h3 class="bs-title">Identities and accounts are separate things — and the provider interface hides it</h3>
</div>
<p>Acme splits <code>users</code> from <code>identities</code>: one account can have several linked identities (email+password, Google, SAML), joined in the <code>identities</code> table. The provider interface only returns a profile; the linking decision happens in <code>identity/linker.ts</code>, keyed on verified email. Providers that can't guarantee verified emails (many SSO IdPs don't) must set <code>requiresManualLink: true</code> or the linker will happily merge two different people who share a corporate alias.</p>
<div class="bite"><span class="bite-tag">Why it bites</span><span>Implement the interface naively and everything works — until a customer's IdP sends an unverified or reassigned email and your provider silently attaches a stranger's identity to an existing account. This is an account-takeover bug, not a login bug.</span></div>
<div class="chip-row">
<div class="prompt-chip" id="chip-4">Walk me through identity/linker.ts before implementing. Decide explicitly whether this IdP guarantees verified emails; if not, set requiresManualLink and build the manual-link confirmation path.</div>
<button class="copy-btn" data-copy-target="chip-4">Copy</button>
</div>
</div>
<!-- 5 -->
<div class="bs-card">
<div class="bs-head">
<span class="badge landmine">Landmine</span>
<span class="bs-num">05</span>
<h3 class="bs-title">Token refresh is behind a flag that's on in dev and off in prod</h3>
</div>
<p>Refresh-token rotation is gated by <code>auth.refresh_rotation</code> in <code>flags.yaml</code>. It defaults to <code>true</code> in dev and staging, <code>false</code> in prod (a paused rollout after a clock-skew incident in May). With the flag off, prod sessions live exactly <code>ACCESS_TOKEN_TTL</code> (12h) and then hard-expire — there is no refresh path.</p>
<div class="bite"><span class="bite-tag">Why it bites</span><span>Any refresh logic you write will be tested against dev behavior and dead code in prod. Worse: if your provider assumes a refresh happens, prod users get logged out mid-session every 12 hours and the bug report will say "SSO randomly logs me out," pointing at your code.</span></div>
<div class="chip-row">
<div class="prompt-chip" id="chip-5">Implement against auth.refresh_rotation=false (prod reality). Make the provider work with hard 12h expiry, and note separately what changes if the rotation rollout resumes.</div>
<button class="copy-btn" data-copy-target="chip-5">Copy</button>
</div>
</div>
<!-- 6 -->
<div class="bs-card">
<div class="bs-head">
<span class="badge convention">Convention</span>
<span class="bs-num">06</span>
<h3 class="bs-title">Registering a provider takes three steps, and only two are discoverable</h3>
</div>
<p>A provider isn't live when it compiles. It must be (1)&nbsp;registered in <code>providers/registry.ts</code>, (2)&nbsp;inserted into the <code>auth_providers</code> table via a migration — the admin UI reads the DB, not the registry — and (3)&nbsp;added to the fixture list in <code>__tests__/provider-contract.spec.ts</code>, which runs the shared contract suite (state validation, replay protection, logout fan-out) against every registered provider. CI fails if the registry and fixture list disagree.</p>
<div class="bite"><span class="bite-tag">Why it bites</span><span>Skip the migration and the provider compiles, passes tests, and simply never appears in any workspace's SSO settings. Two engineers have independently lost half a day to this; it's in nobody's docs.</span></div>
<div class="chip-row">
<div class="prompt-chip" id="chip-6">Do all three registration steps: registry.ts, an auth_providers migration, and the provider-contract fixture list. Show me the contract suite passing for the new provider before we call it done.</div>
<button class="copy-btn" data-copy-target="chip-6">Copy</button>
</div>
</div>
<!-- 7 -->
<div class="bs-card">
<div class="bs-head">
<span class="badge landmine">Landmine</span>
<span class="bs-num">07</span>
<h3 class="bs-title">Logout isn't a function call — it's an event, and the review player listens</h3>
</div>
<p>Session revocation publishes <code>auth.session.revoked</code> on the internal event bus. The realtime review player holds its own long-lived playback grants and only releases them when it consumes that event. <code>logout.ts</code> handles this for existing providers, but SSO single-logout (SLO) callbacks arrive on a separate path and must publish the event themselves.</p>
<div class="bite"><span class="bite-tag">Why it bites</span><span>Miss the event and logout "works" — the session row is gone — but anyone with an open review tab keeps full playback access to private video until the grant TTL expires (up to 24h). For a company selling review workflows to studios, that's a security incident, not a bug.</span></div>
<div class="chip-row">
<div class="prompt-chip" id="chip-7">Handle SLO: any revocation path in the new provider must publish auth.session.revoked. Add a test asserting the review player's grant is released on IdP-initiated logout.</div>
<button class="copy-btn" data-copy-target="chip-7">Copy</button>
</div>
</div>
<!-- final -->
<div class="final-card">
<div class="final-head">
<div class="art-eyebrow">Your improved prompt</div>
<button class="copy-btn" data-copy-target="final-prompt">Copy prompt</button>
</div>
<h3>Everything above, folded into the prompt you should actually give me</h3>
<pre class="final-prompt" id="final-prompt">Add a new SSO auth provider to Acme. Constraints from the blindspot pass:
1. Sessions: route all session writes through SessionBridge (MIG-118 is
unfinished; prod still reads from pg_sessions). Never touch
RedisSessionStore directly.
2. Template: base the structure on oauth/google.ts, NOT saml/handler.ts —
SAML bypasses authPipeline. Mount all new routes inside authPipeline
and show me the mount point.
3. History: read the revert of PR #2841 first. Implement the signed
state-parameter workspace claim (AUTH_STATE_SECRET) instead of
tenant-suffixed callback URLs, and test the workspace-invite flow.
4. Identity linking: walk me through identity/linker.ts before coding.
If this IdP doesn't guarantee verified emails, set requiresManualLink
and build the confirmation path — do not auto-link.
5. Flags: implement against auth.refresh_rotation=false (prod). Sessions
hard-expire at 12h; no refresh path. Note what changes if rotation
ships.
6. Registration: all three steps — providers/registry.ts, an
auth_providers migration, and the provider-contract fixture list.
Show the contract suite green.
7. Logout: every revocation path must publish auth.session.revoked, and
add a test that the review player's playback grant is released on
IdP-initiated logout.
Work in this order: linker walkthrough → provider skeleton → callback +
state claim → registration → SLO + events → contract tests. Stop and
show me the plan after the walkthrough before writing code.</pre>
<p class="final-note">Seven sentences you couldn't have written this morning — each one bought with someone else's half-day. That's the point of the pass.</p>
</div>
</div>
</div>
<footer>Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.</footer>
</div>
<script>
(function () {
function copyText(text, btn) {
function done() {
var prev = btn.textContent;
btn.textContent = "Copied";
btn.classList.add("copied");
setTimeout(function () {
btn.textContent = prev;
btn.classList.remove("copied");
}, 1600);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallback(text); done(); });
} else {
fallback(text);
done();
}
}
function fallback(text) {
var ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed";
ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); } catch (e) {}
document.body.removeChild(ta);
}
document.addEventListener("click", function (e) {
var btn = e.target.closest("button[data-copy-target]");
if (!btn) return;
var el = document.getElementById(btn.getAttribute("data-copy-target"));
if (!el) return;
copyText(el.textContent.trim(), btn);
});
})();
</script>
</body>
</html>
+582
View File
@@ -0,0 +1,582 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Teach me my unknowns — Know your unknowns</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px 100px; }
/* ── header ─────────────────────────── */
header { padding: 56px 0 8px; position: relative; }
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 12px;
}
.eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
.backlink {
position: absolute; top: 60px; right: 0;
font-family: var(--mono); font-size: 12px;
color: var(--g500); text-decoration: none;
}
.backlink:hover { color: var(--clay); }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(32px, 5vw, 46px);
line-height: 1.08;
letter-spacing: -0.018em;
margin: 0 0 10px;
}
.lede { font-size: 16.5px; color: var(--g700); max-width: 640px; margin: 0 0 28px; }
h2, h3 { font-family: var(--serif); font-weight: 500; letter-spacing: -0.012em; }
.card {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
}
/* prompt box */
.promptbox { padding: 22px 26px 24px; position: relative; margin-bottom: 44px; }
.promptbox .label {
font-family: var(--mono); font-size: 12px; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--clay); margin-bottom: 10px;
}
.promptbox blockquote {
margin: 0; padding: 0 0 0 18px;
border-left: 3px solid var(--oat);
font-family: var(--serif); font-style: italic;
font-size: 17.5px; line-height: 1.5; color: var(--slate);
}
.copybtn {
position: absolute; top: 18px; right: 18px;
font-family: var(--mono); font-size: 11.5px;
background: var(--g100); color: var(--g700);
border: 1.5px solid var(--g300); border-radius: 8px;
padding: 6px 12px; cursor: pointer;
}
.copybtn:hover { border-color: var(--clay); color: var(--clay-d); }
.copybtn.done { background: var(--olive); border-color: var(--olive); color: #fff; }
/* divider */
.divider {
display: flex; align-items: center; gap: 16px;
font-family: var(--mono); font-size: 12px; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--g500);
margin: 0 0 40px;
}
.divider::before, .divider::after { content: ""; flex: 1; height: 1.5px; background: var(--g300); }
/* ── artifact ─────────────────────────── */
.artifact h2 { font-size: 27px; margin: 0 0 6px; }
.artifact .sub { color: var(--g700); font-size: 15px; max-width: 640px; margin: 0 0 26px; }
.sec-eyebrow {
font-family: var(--mono); font-size: 12px; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--g500);
display: flex; align-items: center; gap: 12px; margin: 44px 0 14px;
}
.sec-eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
/* mental model strip */
.pipeline { display: flex; gap: 0; align-items: stretch; flex-wrap: wrap; margin-bottom: 8px; }
.pipe-step {
flex: 1 1 150px; background: var(--paper); border: 1.5px solid var(--g300);
border-radius: 12px; padding: 12px 14px; margin: 0 10px 10px 0;
position: relative;
}
.pipe-step b { display: block; font-size: 14px; }
.pipe-step span { font-size: 12.5px; color: var(--g500); }
.pipe-step.hot { border-color: var(--clay); }
.pipe-step.hot b { color: var(--clay-d); }
/* vocab ladder */
.ladder { display: grid; gap: 12px; }
.rung { padding: 16px 20px 15px; display: grid; grid-template-columns: 158px 1fr; gap: 4px 22px; }
@media (max-width: 640px) { .rung { grid-template-columns: 1fr; } }
.rung .term { font-family: var(--serif); font-size: 17px; font-weight: 500; }
.rung .term small { display: block; font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--g500); margin-top: 2px; }
.rung .def { font-size: 14.5px; color: var(--g700); }
.rung .say {
grid-column: 2; font-family: var(--mono); font-size: 12.5px; color: var(--clay-d);
background: var(--g100); border-radius: 8px; padding: 7px 11px; margin-top: 6px;
}
@media (max-width: 640px) { .rung .say { grid-column: 1; } }
.rung .say::before { content: "say → "; color: var(--g500); }
/* grading console */
.console { overflow: hidden; }
.console-head {
display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;
padding: 14px 20px; border-bottom: 1.5px solid var(--g200); background: var(--g100);
}
.console-head .fname { font-family: var(--mono); font-size: 12px; color: var(--g700); }
.console-head .fname i { font-style: normal; color: var(--g500); }
.presets { display: flex; gap: 8px; flex-wrap: wrap; }
.presets button {
font-family: var(--mono); font-size: 11.5px; padding: 6px 12px;
background: var(--paper); border: 1.5px solid var(--g300); border-radius: 999px;
color: var(--g700); cursor: pointer;
}
.presets button:hover { border-color: var(--clay); }
.presets button.on { background: var(--slate); border-color: var(--slate); color: var(--ivory); }
.frame-wrap { position: relative; background: var(--slate); user-select: none; }
.frame-stack { position: relative; aspect-ratio: 16/9; overflow: hidden; }
.frame-stack svg { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#gradeLayer { position: absolute; inset: 0; }
#tempOverlay, #splitTone { position: absolute; inset: 0; pointer-events: none; }
#tempOverlay { mix-blend-mode: soft-light; opacity: 0; }
#splitTone { mix-blend-mode: soft-light; opacity: 0;
background: radial-gradient(circle at 72% 34%, rgba(255,166,87,.95) 0%, rgba(255,166,87,0) 42%),
linear-gradient(rgba(38,86,97,.85), rgba(38,86,97,.85)); }
.split-handle {
position: absolute; top: 0; bottom: 0; width: 2px; background: rgba(250,249,245,.9);
box-shadow: 0 0 6px rgba(0,0,0,.5); pointer-events: none;
}
.split-handle::after {
content: "◂ ▸"; position: absolute; top: 50%; left: 50%;
transform: translate(-50%,-50%);
background: var(--ivory); color: var(--slate); font-size: 10px;
padding: 3px 7px; border-radius: 999px; white-space: nowrap;
font-family: var(--mono); letter-spacing: 0.05em;
}
#splitRange {
position: absolute; inset: 0; width: 100%; height: 100%;
opacity: 0; cursor: ew-resize; margin: 0; -webkit-appearance: none; appearance: none;
}
.frame-tag {
position: absolute; top: 10px; font-family: var(--mono); font-size: 10px;
letter-spacing: 0.12em; padding: 3px 8px; border-radius: 4px;
background: rgba(20,20,19,.55); color: rgba(250,249,245,.9); pointer-events: none;
}
.frame-tag.before { left: 10px; }
.frame-tag.after { right: 10px; }
.sliders {
display: grid; grid-template-columns: 1fr 1fr; gap: 14px 28px;
padding: 18px 20px 20px; border-top: 1.5px solid var(--g200);
}
@media (max-width: 640px) { .sliders { grid-template-columns: 1fr; } }
.sl-row label {
display: flex; justify-content: space-between; align-items: baseline;
font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--g700); margin-bottom: 4px;
}
.sl-row label output { color: var(--clay-d); text-transform: none; }
.sl-row input[type=range] {
-webkit-appearance: none; appearance: none; width: 100%; height: 22px;
background: transparent; cursor: pointer; margin: 0;
}
.sl-row input[type=range]::-webkit-slider-runnable-track {
height: 4px; border-radius: 2px; background: var(--g200);
}
.sl-row input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none; appearance: none; width: 16px; height: 16px; margin-top: -6px;
border-radius: 50%; background: var(--clay); border: 2px solid var(--paper);
box-shadow: 0 1px 3px rgba(20,20,19,.25);
}
.sl-row input[type=range]::-moz-range-track {
height: 4px; border-radius: 2px; background: var(--g200);
}
.sl-row input[type=range]::-moz-range-thumb {
width: 14px; height: 14px; border-radius: 50%; background: var(--clay);
border: 2px solid var(--paper); box-shadow: 0 1px 3px rgba(20,20,19,.25);
}
.console-foot {
font-size: 13px; color: var(--g500); padding: 0 20px 16px;
}
.console-foot b { color: var(--g700); font-weight: 600; }
.resetbtn {
font-family: var(--mono); font-size: 11px; background: none; border: none;
color: var(--g500); text-decoration: underline; cursor: pointer; padding: 0;
}
.resetbtn:hover { color: var(--clay); }
/* checklist */
.checklist { padding: 6px 22px 10px; }
.checklist ul { list-style: none; margin: 12px 0 14px; padding: 0; display: grid; gap: 10px; }
.checklist li { display: flex; gap: 12px; font-size: 14.5px; color: var(--g700); }
.checklist li::before {
content: "✓"; flex: none; width: 20px; height: 20px; border-radius: 50%;
background: var(--olive); color: #fff; font-size: 12px; line-height: 20px;
text-align: center; margin-top: 2px;
}
.checklist li b { color: var(--slate); font-weight: 600; }
.checklist li.warn::before { content: "!"; background: var(--clay); }
/* prompts card */
.prompts-card { border-color: var(--slate); padding: 22px 24px 20px; }
.prompts-card h3 { margin: 0 0 4px; font-size: 21px; }
.prompts-card .note { font-size: 13.5px; color: var(--g500); margin: 0 0 16px; }
.pgrid { display: grid; gap: 10px; }
.pgrid .p {
font-family: var(--mono); font-size: 13px; line-height: 1.6;
background: var(--g100); border-left: 3px solid var(--clay);
border-radius: 0 10px 10px 0; padding: 12px 16px; color: var(--g700);
}
.pgrid .p b { color: var(--slate); font-weight: 600; }
footer {
margin-top: 72px; padding-top: 24px; border-top: 1.5px solid var(--g300);
font-size: 13.5px; color: var(--g500);
}
footer a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">Know your unknowns · Pre-implementation</div>
<a class="backlink" href="index.html">← All examples</a>
<h1>Teach me my unknowns</h1>
<p class="lede">Before doing unfamiliar work, ask Claude to teach you the domain's vocabulary — so your next prompt uses the words a professional would.</p>
<div class="card promptbox">
<div class="label">The prompt</div>
<blockquote id="promptText">I don't know what color grading is but I need to grade the Acme launch video. Teach me color grading well enough that I understand my unknown unknowns and can prompt you with real vocabulary.</blockquote>
<button class="copybtn" id="copyBtn" type="button">copy prompt</button>
</div>
</header>
<div class="divider">What Claude produced</div>
<main class="artifact">
<h2>Color grading in one sitting</h2>
<p class="sub">Everything you need to grade <em>acme-launch-v3.mp4</em> — or at least to give precise notes to whoever does. Correction first, then the creative look. Play with the frame below until the sliders feel obvious.</p>
<!-- mental model -->
<div class="sec-eyebrow">The mental model</div>
<div class="pipeline">
<div class="pipe-step"><b>1 · Ingest</b><span>Footage arrives "flat" (log) — grey and washed out, on purpose. It preserves detail.</span></div>
<div class="pipe-step hot"><b>2 · Correct</b><span>Fix exposure &amp; white balance so it looks <em>real</em>. Boring, essential, first.</span></div>
<div class="pipe-step hot"><b>3 · Grade</b><span>Add the <em>look</em>: contrast shape, color bias, mood. This is the creative part.</span></div>
<div class="pipe-step"><b>4 · Match</b><span>Make every shot in the cut agree, so the video feels like one piece.</span></div>
</div>
<!-- vocab ladder -->
<div class="sec-eyebrow">The vocabulary ladder</div>
<div class="ladder">
<div class="card rung">
<div class="term">Exposure<small>overall brightness</small></div>
<div class="def">How light or dark the whole image is, measured in "stops" (each stop doubles/halves the light). Wrong exposure = detail lost in blown highlights or murky shadows.</div>
<div class="say">"the interview shots look a half-stop underexposed — bring them up"</div>
</div>
<div class="card rung">
<div class="term">White balance<small>color temperature</small></div>
<div class="def">Whether white things actually look white. Measured in Kelvin: low = warm/orange (tungsten), high = cool/blue (daylight). Mixed lighting is the classic gotcha.</div>
<div class="say">"the office footage reads too warm — cool it toward neutral, protect skin tones"</div>
</div>
<div class="card rung">
<div class="term">Contrast curve<small>tone response</small></div>
<div class="def">The S-shaped curve mapping shadows→highlights. Steeper = punchier; flatter = softer, more "filmic". You bend the curve, you don't just crank a slider.</div>
<div class="say">"add a gentle S-curve — richer blacks but don't crush shadow detail"</div>
</div>
<div class="card rung">
<div class="term">Lift / Gamma / Gain<small>3-way correction</small></div>
<div class="def">Independent color+brightness control over shadows (lift), midtones (gamma), and highlights (gain). This is how pros push "teal in the shadows, warm in the highlights".</div>
<div class="say">"push the lift slightly teal and the gain slightly warm — subtle, not music-video"</div>
</div>
<div class="card rung">
<div class="term">Saturation vs vibrance<small>color intensity</small></div>
<div class="def">Saturation boosts <em>all</em> colors equally (skin goes orange fast). Vibrance boosts only the muted colors and protects skin. Almost always reach for vibrance first.</div>
<div class="say">"use vibrance not saturation — the UI screenshots can pop but faces stay natural"</div>
</div>
<div class="card rung">
<div class="term">LUT<small>look-up table</small></div>
<div class="def">A saved color transform file (.cube) that applies a whole look in one step — either technical (log→Rec.709 conversion) or creative (a stylized grade). Apply after correction, then dial its intensity down.</div>
<div class="say">"apply the Rec.709 conversion LUT first, then a creative LUT at ~60% strength"</div>
</div>
</div>
<!-- interactive console -->
<div class="sec-eyebrow">Feel it — grade a frame</div>
<div class="card console">
<div class="console-head">
<span class="fname">acme-launch-v3.mp4 <i>· frame 01:47:12 · drag the split to compare</i></span>
<div class="presets">
<button type="button" data-preset="flat">Flat / log</button>
<button type="button" data-preset="corporate" class="on">Corporate clean</button>
<button type="button" data-preset="teal">Cinematic teal-orange</button>
</div>
</div>
<div class="frame-wrap">
<div class="frame-stack">
<!-- scene definition -->
<svg viewBox="0 0 640 360" preserveAspectRatio="xMidYMid slice" aria-label="Ungraded landscape frame">
<defs>
<g id="scene">
<rect width="640" height="360" fill="#9DB3B8"/>
<circle cx="462" cy="118" r="34" fill="#EFD79A"/>
<path d="M0 190 L120 96 L238 190 Z" fill="#77857D"/>
<path d="M150 190 L300 66 L470 190 Z" fill="#5F6F66"/>
<path d="M400 190 L520 110 L640 190 L640 200 L400 200 Z" fill="#77857D"/>
<rect y="190" width="640" height="80" fill="#8AA39E"/>
<path d="M300 214 L470 190 L462 152 L448 190 Z" fill="#71898A" opacity="0.5"/>
<circle cx="462" cy="216" r="22" fill="#D9C08A" opacity="0.55"/>
<rect y="262" width="640" height="98" fill="#55614C"/>
<path d="M0 262 C 140 244, 300 276, 640 254 L640 360 L0 360 Z" fill="#4A5542"/>
<g fill="#3E4A38">
<path d="M70 262 L86 210 L102 262 Z"/><rect x="83" y="258" width="6" height="16"/>
<path d="M112 268 L126 224 L140 268 Z"/><rect x="123" y="264" width="5" height="14"/>
<path d="M560 258 L578 202 L596 258 Z"/><rect x="575" y="254" width="6" height="18"/>
</g>
<g fill="#2E2E2A">
<circle cx="332" cy="284" r="7"/>
<path d="M326 291 C 326 302, 338 302, 338 291 L337 316 L333 316 L332 300 L331 316 L327 316 Z"/>
</g>
<path d="M0 356 L640 356" stroke="#39422F" stroke-width="8"/>
</g>
</defs>
<use href="#scene"/>
</svg>
<!-- graded copy, clipped to the right of the split -->
<div id="gradeLayer">
<svg viewBox="0 0 640 360" preserveAspectRatio="xMidYMid slice" aria-hidden="true">
<use href="#scene"/>
</svg>
<div id="tempOverlay"></div>
<div id="splitTone"></div>
</div>
<span class="frame-tag before">UNGRADED</span>
<span class="frame-tag after">GRADED</span>
<div class="split-handle" id="splitHandle"></div>
<input type="range" id="splitRange" min="4" max="96" value="42" aria-label="Before/after split position">
</div>
<div class="sliders">
<div class="sl-row">
<label for="sExp">Exposure <output id="oExp">+0.2 EV</output></label>
<input type="range" id="sExp" min="-50" max="50" value="10">
</div>
<div class="sl-row">
<label for="sTemp">Temperature <output id="oTemp">+8 warm</output></label>
<input type="range" id="sTemp" min="-50" max="50" value="8">
</div>
<div class="sl-row">
<label for="sCon">Contrast <output id="oCon">+12</output></label>
<input type="range" id="sCon" min="-50" max="50" value="12">
</div>
<div class="sl-row">
<label for="sSat">Saturation <output id="oSat">+8</output></label>
<input type="range" id="sSat" min="-50" max="50" value="8">
</div>
</div>
</div>
<div class="console-foot">
<span id="presetNote"><b>Corporate clean:</b> slight lift in exposure, near-neutral temperature, modest contrast — trustworthy and bright. The default for product launch videos.</span>
· <button class="resetbtn" type="button" id="resetBtn">reset to ungraded</button>
</div>
</div>
<!-- checklist -->
<div class="sec-eyebrow">What good looks like</div>
<div class="card checklist">
<ul>
<li><b>Skin tones stay believable.</b> Every creative look bends around faces, never through them. If the founder looks seasick or sunburned, the grade is wrong.</li>
<li><b>Blacks are rich but not crushed.</b> You can still read detail in dark areas (hair, the navy hoodie at 00:42). A histogram slammed against the left edge means lost information.</li>
<li><b>Whites don't clip.</b> The Acme UI screens are mostly white — if they blow out to pure 100%, button borders and text vanish. Keep highlights just under the ceiling.</li>
<li><b>Shot-to-shot consistency.</b> Cutting from the office interview to the screen capture shouldn't feel like changing channels. Shot matching is half the job.</li>
<li><b>The look serves the story.</b> A launch video wants clean and confident, not moody thriller. If viewers notice the grade before the product, dial it back.</li>
<li class="warn"><b>Check on more than one screen.</b> Your laptop lies. Preview on a phone and, ideally, a TV before signing off — and confirm you're exporting in Rec.709 for web delivery.</li>
</ul>
</div>
<!-- prompts you couldn't have written -->
<div class="sec-eyebrow">The payoff</div>
<div class="card prompts-card">
<h3>Prompts you couldn't have written an hour ago</h3>
<p class="note">An hour ago the best you had was "make the video look nicer." Now you can say:</p>
<div class="pgrid">
<div class="p">"The interview footage was shot in <b>log</b> — apply the camera's <b>Rec.709 conversion LUT</b> first, then grade on top of that, not on the raw flat image."</div>
<div class="p">"Give the whole cut a restrained <b>teal-orange</b> look: push the <b>lift</b> slightly teal, <b>gain</b> slightly warm, keep <b>gamma</b> neutral so midtone skin stays natural."</div>
<div class="p">"Shot 14 is about <b>2/3 of a stop underexposed</b> and too warm versus shot 13 — match its <b>exposure</b> and <b>white balance</b> to shot 13, then A/B them at the cut point."</div>
<div class="p">"Add a soft <b>S-curve</b> for contrast and lift color intensity with <b>vibrance rather than saturation</b>, so the product UI pops without turning faces orange."</div>
</div>
</div>
</main>
<footer>Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.</footer>
</div>
<script>
(function () {
// ---- copy prompt ----
var copyBtn = document.getElementById('copyBtn');
var promptText = document.getElementById('promptText').textContent.trim();
copyBtn.addEventListener('click', function () {
function done() {
copyBtn.textContent = 'Copied';
copyBtn.classList.add('done');
setTimeout(function () {
copyBtn.textContent = 'copy prompt';
copyBtn.classList.remove('done');
}, 1600);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(promptText).then(done, fallback);
} else { fallback(); }
function fallback() {
var ta = document.createElement('textarea');
ta.value = promptText;
ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
done();
}
});
// ---- grading console ----
var sExp = document.getElementById('sExp'),
sTemp = document.getElementById('sTemp'),
sCon = document.getElementById('sCon'),
sSat = document.getElementById('sSat');
var oExp = document.getElementById('oExp'),
oTemp = document.getElementById('oTemp'),
oCon = document.getElementById('oCon'),
oSat = document.getElementById('oSat');
var gradeLayer = document.getElementById('gradeLayer');
var gradeSvg = gradeLayer.querySelector('svg');
var tempOverlay = document.getElementById('tempOverlay');
var splitTone = document.getElementById('splitTone');
var splitRange = document.getElementById('splitRange');
var splitHandle = document.getElementById('splitHandle');
var presetNote = document.getElementById('presetNote');
var presetBtns = document.querySelectorAll('.presets button');
var splitToneOn = false;
var presets = {
flat: {
exp: 6, temp: 0, con: -38, sat: -45, split: false,
note: '<b>Flat / log:</b> low contrast, low saturation — this is roughly what footage looks like straight off a camera shooting log. Ugly on purpose: it keeps maximum detail for grading.'
},
corporate: {
exp: 10, temp: 8, con: 12, sat: 8, split: false,
note: '<b>Corporate clean:</b> slight lift in exposure, near-neutral temperature, modest contrast — trustworthy and bright. The default for product launch videos.'
},
teal: {
exp: 4, temp: -22, con: 26, sat: 14, split: true,
note: '<b>Cinematic teal-orange:</b> shadows pushed teal, a warm glow held around the light source, stronger contrast. The blockbuster look — use at maybe 40% of this strength for a launch video.'
}
};
function fmtSigned(v) { return (v > 0 ? '+' : '') + v; }
function update() {
var exp = +sExp.value, temp = +sTemp.value, con = +sCon.value, sat = +sSat.value;
// filter math: sliders are -50..50
var brightness = 1 + exp / 100; // 0.5 .. 1.5
var contrast = 1 + con / 100; // 0.5 .. 1.5
var saturate = 1 + sat / 62.5; // 0.2 .. 1.8
var sepia = temp > 0 ? temp / 50 * 0.22 : 0;
gradeSvg.style.filter =
'brightness(' + brightness.toFixed(3) + ') ' +
'contrast(' + contrast.toFixed(3) + ') ' +
'saturate(' + saturate.toFixed(3) + ')' +
(sepia ? ' sepia(' + sepia.toFixed(3) + ')' : '');
// temperature wash overlay
if (temp === 0) {
tempOverlay.style.opacity = 0;
} else if (temp > 0) {
tempOverlay.style.background = '#E8955C';
tempOverlay.style.opacity = (temp / 50 * 0.55).toFixed(3);
} else {
tempOverlay.style.background = '#3E7286';
tempOverlay.style.opacity = (-temp / 50 * 0.6).toFixed(3);
}
splitTone.style.opacity = splitToneOn ? 0.75 : 0;
// readouts
oExp.textContent = (exp >= 0 ? '+' : '') + Math.abs(exp / 50).toFixed(1) + ' EV';
oTemp.textContent = temp === 0 ? 'neutral' : fmtSigned(temp) + (temp > 0 ? ' warm' : ' cool');
oCon.textContent = fmtSigned(con);
oSat.textContent = fmtSigned(sat);
}
[sExp, sTemp, sCon, sSat].forEach(function (s) {
s.addEventListener('input', function () {
splitToneOn = false; // manual moves drop the split-tone extra
presetBtns.forEach(function (b) { b.classList.remove('on'); });
update();
});
});
presetBtns.forEach(function (btn) {
btn.addEventListener('click', function () {
var p = presets[btn.dataset.preset];
sExp.value = p.exp; sTemp.value = p.temp; sCon.value = p.con; sSat.value = p.sat;
splitToneOn = p.split;
presetBtns.forEach(function (b) { b.classList.toggle('on', b === btn); });
presetNote.innerHTML = p.note;
update();
});
});
document.getElementById('resetBtn').addEventListener('click', function () {
sExp.value = 0; sTemp.value = 0; sCon.value = 0; sSat.value = 0;
splitToneOn = false;
presetBtns.forEach(function (b) { b.classList.remove('on'); });
presetNote.innerHTML = '<b>Ungraded:</b> all corrections zeroed — the graded side now matches the source frame exactly.';
update();
});
// before/after split
function updateSplit() {
var pct = +splitRange.value;
gradeLayer.style.clipPath = 'inset(0 0 0 ' + pct + '%)';
splitHandle.style.left = pct + '%';
}
splitRange.addEventListener('input', updateSplit);
update();
updateSplit();
})();
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff
+798
View File
@@ -0,0 +1,798 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mock before you wire — Know your unknowns</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px 100px; }
/* ── header ─────────────────────────── */
header { padding: 56px 0 8px; position: relative; }
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 12px;
}
.eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
.backlink {
position: absolute; top: 60px; right: 0;
font-family: var(--mono); font-size: 12px;
color: var(--g500); text-decoration: none;
letter-spacing: 0.04em;
}
.backlink:hover { color: var(--clay); }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(32px, 5vw, 46px);
line-height: 1.08;
letter-spacing: -0.015em;
margin: 0 0 10px;
}
.lede { font-size: 16.5px; color: var(--g700); max-width: 640px; margin: 0 0 30px; }
/* ── prompt box ─────────────────────── */
.promptbox {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 20px 24px 22px;
position: relative;
margin-bottom: 44px;
}
.promptbox .label {
font-family: var(--mono); font-size: 11px;
letter-spacing: 0.14em; text-transform: uppercase;
color: var(--clay); margin-bottom: 10px;
}
.promptbox blockquote {
margin: 0; padding: 0 0 0 18px;
border-left: 3px solid var(--oat);
font-family: var(--serif); font-style: italic;
font-size: 17.5px; line-height: 1.5; color: var(--slate);
max-width: 720px;
}
.copybtn {
position: absolute; top: 16px; right: 16px;
font-family: var(--mono); font-size: 11px;
letter-spacing: 0.04em;
background: var(--g100); color: var(--g700);
border: 1.5px solid var(--g300); border-radius: 8px;
padding: 5px 11px; cursor: pointer;
}
.copybtn:hover { border-color: var(--g500); }
.copybtn.done { background: var(--olive); border-color: var(--olive); color: #fff; }
/* ── divider ────────────────────────── */
.divider {
display: flex; align-items: center; gap: 16px;
font-family: var(--mono); font-size: 11px;
letter-spacing: 0.14em; text-transform: uppercase;
color: var(--g500); margin: 0 0 32px;
}
.divider::before, .divider::after {
content: ""; flex: 1; height: 1px; background: var(--g300);
}
/* ═══ THE ARTIFACT ═══════════════════ */
.artifact {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 22px;
}
@media (max-width: 480px) { .artifact { padding: 14px; } }
.art-head {
display: flex; flex-wrap: wrap; gap: 12px;
align-items: center; justify-content: space-between;
margin-bottom: 16px;
}
.art-title { font-family: var(--mono); font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--g500); }
.art-title b { color: var(--slate); font-weight: 600; }
/* segmented control */
.seg {
display: inline-flex; flex-wrap: wrap;
background: var(--g100);
border: 1.5px solid var(--g300);
border-radius: 10px; padding: 3px; gap: 3px;
}
.seg button {
font-family: var(--sans); font-size: 12.5px; font-weight: 500;
border: none; background: transparent; color: var(--g700);
padding: 6px 12px; border-radius: 7px; cursor: pointer;
white-space: nowrap;
}
.seg button:hover { color: var(--slate); }
.seg button.on {
background: var(--paper); color: var(--slate);
box-shadow: 0 1px 3px rgba(20,20,19,.12);
}
.seg button.on .dot { background: var(--clay); }
.seg .dot {
display: inline-block; width: 7px; height: 7px; border-radius: 50%;
background: var(--g300); margin-right: 7px; vertical-align: 1px;
}
.variant-note {
font-size: 13px; color: var(--g500);
margin: 0 0 14px; min-height: 20px;
}
.variant-note b { color: var(--g700); font-weight: 600; }
/* ── stage ──────────────────────────── */
.stagewrap { position: relative; }
.stage {
position: relative;
background: #1C1B18;
border-radius: 12px;
overflow: hidden;
aspect-ratio: 16 / 9;
min-height: 220px;
}
.stage svg.scene { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.chip-tc, .chip-paused {
position: absolute; top: 10px;
font-family: var(--mono); font-size: 11px;
padding: 3px 9px; border-radius: 6px;
background: rgba(20,20,19,.72); color: var(--ivory);
letter-spacing: 0.06em; z-index: 3;
}
.chip-tc { left: 10px; }
.chip-paused { right: 10px; color: var(--oat); }
.chip-paused::before { content: "❚❚ "; font-size: 9px; }
/* seekbar */
.seekbar {
position: absolute; left: 0; right: 0; bottom: 0;
padding: 26px 14px 10px; z-index: 3;
background: linear-gradient(to top, rgba(20,20,19,.78), rgba(20,20,19,0));
}
.seek-track { position: relative; height: 4px; border-radius: 2px; background: rgba(250,249,245,.25); }
.seek-buf { position: absolute; left: 0; top: 0; bottom: 0; width: 62%; border-radius: 2px; background: rgba(250,249,245,.35); }
.seek-play { position: absolute; left: 0; top: 0; bottom: 0; width: 34%; border-radius: 2px; background: var(--clay); }
.seek-head {
position: absolute; left: 34%; top: 50%;
width: 12px; height: 12px; border-radius: 50%;
background: var(--clay); border: 2px solid var(--ivory);
transform: translate(-50%, -50%);
}
.seek-pins { position: absolute; left: 14px; right: 14px; bottom: 20px; height: 0; }
.seek-pin {
position: absolute; bottom: -3px; width: 7px; height: 7px;
border-radius: 2px; transform: translateX(-50%) rotate(45deg);
background: var(--oat); opacity: .9;
}
/* ── toolbar ────────────────────────── */
.toolbar {
display: flex; align-items: center; gap: 10px;
background: rgba(28,27,24,.92);
border: 1.5px solid rgba(250,249,245,.16);
box-shadow: 0 8px 28px rgba(20,20,19,.35);
padding: 8px 12px;
z-index: 5;
color: var(--ivory);
max-width: 100%;
}
.tb-group { display: flex; align-items: center; gap: 4px; }
.tb-sep { width: 1px; align-self: stretch; margin: 2px 0; background: rgba(250,249,245,.18); flex: none; }
.tool {
width: 34px; height: 34px; flex: none;
display: grid; place-items: center;
border: 1.5px solid transparent; border-radius: 9px;
background: transparent; color: var(--g300);
cursor: pointer; padding: 0;
}
.tool:hover { color: var(--ivory); background: rgba(250,249,245,.08); }
.tool.on { background: var(--clay); border-color: var(--clay-d); color: #fff; }
.tool svg { width: 17px; height: 17px; display: block; }
.swatches { display: flex; align-items: center; gap: 6px; }
.swatch {
width: 18px; height: 18px; flex: none;
border-radius: 50%; cursor: pointer; padding: 0;
border: 2px solid rgba(250,249,245,.25);
}
.swatch.on { border-color: var(--ivory); box-shadow: 0 0 0 2px rgba(217,119,87,.55); }
.strokes { display: flex; align-items: center; gap: 4px; }
.stroke {
width: 26px; height: 26px; flex: none;
display: grid; place-items: center;
background: transparent; border: 1.5px solid transparent;
border-radius: 7px; cursor: pointer; padding: 0;
}
.stroke i { display: block; border-radius: 50%; background: var(--g300); }
.stroke.s1 i { width: 4px; height: 4px; }
.stroke.s2 i { width: 7px; height: 7px; }
.stroke.s3 i { width: 11px; height: 11px; }
.stroke:hover i { background: var(--ivory); }
.stroke.on { background: rgba(250,249,245,.12); border-color: rgba(250,249,245,.3); }
.stroke.on i { background: var(--ivory); }
.ann-chip {
font-family: var(--mono); font-size: 11px; white-space: nowrap;
background: rgba(227,218,204,.16); color: var(--oat);
border: 1px solid rgba(227,218,204,.3);
border-radius: 999px; padding: 3px 10px;
}
.cmt-btn {
display: flex; align-items: center; gap: 6px;
font-family: var(--sans); font-size: 12.5px; font-weight: 500;
background: transparent; color: var(--g300);
border: 1.5px solid rgba(250,249,245,.22); border-radius: 9px;
padding: 6px 11px; cursor: pointer; white-space: nowrap;
}
.cmt-btn:hover { color: var(--ivory); border-color: rgba(250,249,245,.45); }
.cmt-btn.on { background: var(--oat); color: var(--slate); border-color: var(--oat); }
.cmt-btn svg { width: 14px; height: 14px; }
/* variant: floating pill (default) */
.stagewrap.v-pill .toolbar {
position: absolute; left: 50%; bottom: 52px;
transform: translateX(-50%);
border-radius: 999px;
flex-wrap: nowrap;
width: max-content; max-width: calc(100% - 20px);
overflow-x: auto; scrollbar-width: none;
}
.stagewrap.v-pill .toolbar::-webkit-scrollbar { display: none; }
/* variant: docked left rail */
.stagewrap.v-rail .toolbar {
position: absolute; left: 10px; top: 50%;
transform: translateY(-50%);
flex-direction: column;
border-radius: 14px;
padding: 10px 8px;
gap: 8px;
max-height: calc(100% - 20px);
overflow-y: auto; scrollbar-width: none;
}
.stagewrap.v-rail .toolbar::-webkit-scrollbar { display: none; }
.stagewrap.v-rail .tb-group { flex-direction: column; }
.stagewrap.v-rail .tb-sep { width: auto; height: 1px; align-self: stretch; margin: 0 2px; }
.stagewrap.v-rail .swatches { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; }
.stagewrap.v-rail .strokes { flex-direction: column; }
.stagewrap.v-rail .ann-chip { writing-mode: initial; padding: 3px 6px; font-size: 10px; }
.stagewrap.v-rail .cmt-btn span { display: none; }
.stagewrap.v-rail .cmt-btn { padding: 8px; }
.stagewrap.v-rail .tool, .stagewrap.v-rail .stroke { width: 32px; height: 32px; }
/* variant: docked under seekbar */
.stagewrap.v-dock .toolbar {
position: static; transform: none;
margin-top: 10px;
border-radius: 12px;
background: var(--g700);
flex-wrap: wrap;
justify-content: center;
}
.stagewrap.v-dock .tb-sep { min-height: 22px; }
@media (max-width: 560px) {
.tool { width: 30px; height: 30px; }
.tool svg { width: 15px; height: 15px; }
.toolbar { gap: 7px; padding: 6px 9px; }
.stagewrap.v-pill .toolbar { bottom: 42px; }
.cmt-btn span { display: none; }
.cmt-btn { padding: 7px; }
.stagewrap.v-rail .swatches { grid-template-columns: 1fr; }
.stagewrap.v-rail .ann-chip { display: none; }
}
/* ── comment drawer ─────────────────── */
.drawer {
position: absolute; top: 0; right: 0; bottom: 0;
width: min(272px, 78%);
background: rgba(250,249,245,.97);
border-left: 1.5px solid var(--g300);
transform: translateX(105%);
transition: transform .28s cubic-bezier(.4,0,.2,1);
z-index: 6;
display: flex; flex-direction: column;
border-radius: 0 12px 12px 0;
}
.drawer.open { transform: translateX(0); }
.drawer-head {
display: flex; align-items: center; justify-content: space-between;
padding: 12px 14px 10px;
border-bottom: 1.5px solid var(--g200);
}
.drawer-head h3 { margin: 0; font-family: var(--serif); font-weight: 500; font-size: 15px; letter-spacing: -0.01em; }
.drawer-head .cnt { font-family: var(--mono); font-size: 11px; color: var(--g500); }
.drawer-close {
border: none; background: none; cursor: pointer;
font-size: 16px; line-height: 1; color: var(--g500); padding: 4px;
}
.drawer-close:hover { color: var(--slate); }
.drawer-body { flex: 1; overflow-y: auto; padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; }
.cmt {
background: var(--paper);
border: 1.5px solid var(--g200);
border-radius: 10px;
padding: 9px 11px;
font-size: 12.5px;
}
.cmt .who { display: flex; align-items: baseline; gap: 7px; margin-bottom: 3px; }
.cmt .av {
width: 18px; height: 18px; border-radius: 50%; flex: none;
display: grid; place-items: center; align-self: center;
font-size: 9px; font-weight: 700; color: #fff;
}
.cmt .nm { font-weight: 600; font-size: 12px; }
.cmt .tc { font-family: var(--mono); font-size: 10px; color: var(--clay); }
.cmt p { margin: 0; color: var(--g700); }
.cmt .pin { font-family: var(--mono); font-size: 10px; color: var(--g500); margin-top: 4px; }
.drawer-input {
margin: 0 12px 12px; padding: 8px 11px;
border: 1.5px solid var(--g300); border-radius: 9px;
font-size: 12.5px; color: var(--g500); background: var(--paper);
cursor: text;
}
.art-footnote {
margin: 14px 2px 0; font-size: 12.5px; color: var(--g500);
}
.art-footnote code { font-family: var(--mono); font-size: 11.5px; background: var(--g100); padding: 1px 5px; border-radius: 4px; }
/* ── open questions card ────────────── */
.questions {
margin-top: 28px;
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 22px 24px;
}
@media (max-width: 480px) { .questions { padding: 16px; } }
.questions .label {
font-family: var(--mono); font-size: 11px;
letter-spacing: 0.14em; text-transform: uppercase;
color: var(--clay); margin-bottom: 6px;
}
.questions h2 {
font-family: var(--serif); font-weight: 500;
font-size: 22px; letter-spacing: -0.012em; margin: 0 0 4px;
}
.questions .sub { font-size: 13.5px; color: var(--g500); margin: 0 0 18px; }
.q { padding: 14px 0; border-top: 1.5px solid var(--g100); }
.q:first-of-type { border-top: 1.5px solid var(--g200); }
.q-text { font-size: 14.5px; margin: 0 0 9px; }
.q-text .qn {
font-family: var(--mono); font-size: 11px; color: var(--g500);
margin-right: 8px;
}
.q-chips { display: flex; flex-wrap: wrap; gap: 7px; }
.chip {
font-family: var(--sans); font-size: 12.5px; font-weight: 500;
background: var(--g100); color: var(--g700);
border: 1.5px solid var(--g300); border-radius: 999px;
padding: 5px 13px; cursor: pointer;
}
.chip:hover { border-color: var(--g500); }
.chip.on { background: var(--slate); border-color: var(--slate); color: var(--ivory); }
.chip .k {
font-family: var(--mono); font-size: 10.5px; color: var(--clay);
margin-right: 6px;
}
.chip.on .k { color: var(--oat); }
/* ── reply template ─────────────────── */
.reply {
margin-top: 18px;
background: var(--g100);
border: 1.5px solid var(--g200);
border-radius: 12px;
padding: 14px 16px;
position: relative;
}
.reply .label {
font-family: var(--mono); font-size: 11px;
letter-spacing: 0.14em; text-transform: uppercase;
color: var(--g500); margin-bottom: 8px;
}
.reply pre {
margin: 0; font-family: var(--mono); font-size: 12px;
line-height: 1.7; color: var(--g700);
white-space: pre-wrap; word-break: break-word;
}
.reply pre .picked { color: var(--slate); font-weight: 600; }
.reply pre .blank { color: var(--clay); }
/* ── footer ─────────────────────────── */
footer {
margin-top: 64px; padding-top: 20px;
border-top: 1.5px solid var(--g300);
font-size: 13px; color: var(--g500);
}
footer a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
footer a:hover { text-decoration-color: var(--clay); }
</style>
</head>
<body>
<div class="wrap">
<header>
<a class="backlink" href="index.html">← All examples</a>
<div class="eyebrow">Know your unknowns · Pre-implementation</div>
<h1>Mock before you wire</h1>
<p class="lede">Before Claude touches the real codebase, ask for a throwaway HTML mock — you'll find out what you actually want the moment you can click it, not three PRs later.</p>
<div class="promptbox">
<div class="label">The prompt</div>
<button class="copybtn" id="copyPrompt">copy prompt</button>
<blockquote id="promptText">Before wiring anything up, make a single HTML file mocking Acme's new frame-annotation toolbar with fake data. I want to react to the layout before you touch the real app.</blockquote>
</div>
</header>
<div class="divider">What Claude produced</div>
<!-- ═══ ARTIFACT ═══ -->
<div class="artifact">
<div class="art-head">
<div class="art-title"><b>Acme</b> · frame-annotation toolbar · mock, no real code touched</div>
<div class="seg" id="variantSeg" role="tablist" aria-label="Toolbar layout variant">
<button class="on" data-v="v-pill"><span class="dot"></span>Floating pill</button>
<button data-v="v-rail"><span class="dot"></span>Docked left rail</button>
<button data-v="v-dock"><span class="dot"></span>Under seekbar</button>
</div>
</div>
<p class="variant-note" id="variantNote"><b>Floating pill, bottom-center.</b> Hovers above the seekbar; hides on playback. Closest to what Loom/Frame.io ship.</p>
<div class="stagewrap v-pill" id="stagewrap">
<div class="stage">
<!-- fake paused frame -->
<svg class="scene" viewBox="0 0 640 360" preserveAspectRatio="xMidYMid slice" aria-label="Paused video frame: sunset ride teaser, 00:41 — logo lockup annotated">
<!-- sky -->
<rect x="0" y="0" width="640" height="360" fill="#2A2722"/>
<rect x="0" y="0" width="640" height="230" fill="#3D3A32"/>
<circle cx="470" cy="118" r="46" fill="#D97757"/>
<circle cx="470" cy="118" r="46" fill="none" stroke="#E3DACC" stroke-opacity=".25" stroke-width="10"/>
<!-- far hills -->
<path d="M0 205 Q 110 150 230 198 T 470 192 T 640 178 V 360 H 0 Z" fill="#4A4A3C"/>
<!-- near hills -->
<path d="M0 250 Q 150 205 300 246 T 640 238 V 360 H 0 Z" fill="#5B6647"/>
<path d="M0 292 Q 200 258 400 288 T 640 280 V 360 H 0 Z" fill="#788C5D"/>
<!-- road -->
<path d="M270 360 L 315 240 L 331 240 L 410 360 Z" fill="#3D3D3A"/>
<path d="M322 240 L 322 268 M 328 282 L 332 310 M 336 326 L 342 356" stroke="#E3DACC" stroke-opacity=".5" stroke-width="3" stroke-linecap="round" fill="none"/>
<!-- rider silhouette -->
<g fill="#141413">
<circle cx="330" cy="252" r="9"/>
<circle cx="352" cy="252" r="9"/>
<path d="M330 252 L 341 238 L 352 252 Z" stroke="#141413" stroke-width="3" fill="none"/>
<path d="M341 238 L 338 222 L 346 218" stroke="#141413" stroke-width="4" stroke-linecap="round" fill="none"/>
<circle cx="348" cy="212" r="5"/>
</g>
<!-- title lockup being reviewed -->
<g>
<rect x="60" y="70" width="176" height="58" rx="6" fill="#141413" fill-opacity=".55"/>
<text x="76" y="98" font-family="Georgia, serif" font-size="22" fill="#FAF9F5" font-style="italic">Ridgeline '26</text>
<text x="76" y="117" font-family="monospace" font-size="10" letter-spacing="2" fill="#E3DACC">TEASER · CUT 04</text>
</g>
<!-- existing annotations (fake data) -->
<g>
<!-- box annotation on lockup -->
<rect x="52" y="62" width="192" height="74" rx="8" fill="none" stroke="#D97757" stroke-width="3"/>
<circle cx="52" cy="62" r="11" fill="#D97757"/>
<text x="52" y="66" text-anchor="middle" font-family="monospace" font-size="11" font-weight="bold" fill="#fff">1</text>
<!-- arrow annotation at sun flare -->
<path d="M560 220 Q 540 175 505 150" fill="none" stroke="#E3DACC" stroke-width="3"/>
<path d="M505 150 L 521 152 M 505 150 L 511 165" stroke="#E3DACC" stroke-width="3" stroke-linecap="round"/>
<circle cx="560" cy="220" r="11" fill="#788C5D"/>
<text x="560" y="224" text-anchor="middle" font-family="monospace" font-size="11" font-weight="bold" fill="#fff">2</text>
<!-- pen scribble on road -->
<path d="M380 330 q 14 -12 28 -2 q 14 10 26 -4" fill="none" stroke="#D97757" stroke-width="3" stroke-linecap="round"/>
<circle cx="380" cy="330" r="11" fill="#D97757"/>
<text x="380" y="334" text-anchor="middle" font-family="monospace" font-size="11" font-weight="bold" fill="#fff">3</text>
</g>
</svg>
<div class="chip-tc">00:00:41:07</div>
<div class="chip-paused">PAUSED</div>
<!-- seekbar -->
<div class="seekbar">
<div class="seek-pins">
<span class="seek-pin" style="left:34%"></span>
<span class="seek-pin" style="left:51%"></span>
<span class="seek-pin" style="left:78%"></span>
</div>
<div class="seek-track">
<div class="seek-buf"></div>
<div class="seek-play"></div>
<div class="seek-head"></div>
</div>
</div>
<!-- comment drawer -->
<aside class="drawer" id="drawer" aria-hidden="true">
<div class="drawer-head">
<h3>Comments <span class="cnt">@ 00:41</span></h3>
<button class="drawer-close" id="drawerClose" aria-label="Close comments"></button>
</div>
<div class="drawer-body">
<div class="cmt">
<div class="who"><span class="av" style="background:var(--clay)">MO</span><span class="nm">Maya Okafor</span><span class="tc">00:41:07</span></div>
<p>Lockup lands too early — we haven't earned the name yet. Can we hold it to the crest at 00:48?</p>
<div class="pin">↳ pinned to annotation #1</div>
</div>
<div class="cmt">
<div class="who"><span class="av" style="background:var(--olive)">DP</span><span class="nm">Dev Patel</span><span class="tc">00:41:07</span></div>
<p>Flare off the sun is clipping on HDR exports. Flagging for the color pass.</p>
<div class="pin">↳ pinned to annotation #2</div>
</div>
<div class="cmt">
<div class="who"><span class="av" style="background:var(--g500)">SR</span><span class="nm">Sam Ruiz</span><span class="tc">00:41:07</span></div>
<p>Road markings jitter one frame back. Probably the stabilization crop — see scribble.</p>
<div class="pin">↳ pinned to annotation #3</div>
</div>
</div>
<div class="drawer-input">Reply at 00:41…</div>
</aside>
</div>
<!-- toolbar (one markup, repositioned per variant) -->
<div class="toolbar" id="toolbar" role="toolbar" aria-label="Frame annotation tools">
<div class="tb-group" id="tools">
<button class="tool on" data-tool="select" title="Select (V)" aria-label="Select tool">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M5 3l14 8-6.5 1.5L9 19 5 3z"/></svg>
</button>
<button class="tool" data-tool="pen" title="Pen (P)" aria-label="Pen tool">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 3l4 4L8 20l-5 1 1-5L17 3z"/></svg>
</button>
<button class="tool" data-tool="arrow" title="Arrow (A)" aria-label="Arrow tool">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 19L19 5M19 5h-8M19 5v8"/></svg>
</button>
<button class="tool" data-tool="box" title="Box (B)" aria-label="Box tool">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="6" width="16" height="12" rx="2"/></svg>
</button>
<button class="tool" data-tool="text" title="Text (T)" aria-label="Text tool">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M5 6V4h14v2M12 4v16M9 20h6"/></svg>
</button>
<button class="tool" data-tool="blur" title="Blur (U)" aria-label="Blur tool">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M12 3s6 6.5 6 11a6 6 0 01-12 0c0-4.5 6-11 6-11z"/><path d="M9.5 14a2.5 2.5 0 002.5 2.5" stroke-linecap="round"/></svg>
</button>
</div>
<div class="tb-sep"></div>
<div class="swatches" id="swatches" aria-label="Annotation color">
<button class="swatch on" style="background:#D97757" data-c="clay" title="Clay" aria-label="Clay"></button>
<button class="swatch" style="background:#788C5D" data-c="olive" title="Olive" aria-label="Olive"></button>
<button class="swatch" style="background:#E3DACC" data-c="oat" title="Oat" aria-label="Oat"></button>
<button class="swatch" style="background:#FAF9F5" data-c="white" title="White" aria-label="White"></button>
<button class="swatch" style="background:#141413" data-c="ink" title="Ink" aria-label="Ink"></button>
</div>
<div class="tb-sep"></div>
<div class="strokes" id="strokes" aria-label="Stroke width">
<button class="stroke s1" data-w="2" title="Thin"><i></i></button>
<button class="stroke s2 on" data-w="4" title="Medium"><i></i></button>
<button class="stroke s3" data-w="7" title="Thick"><i></i></button>
</div>
<div class="tb-sep"></div>
<span class="ann-chip" title="Annotations on this frame">3 ann.</span>
<button class="cmt-btn" id="cmtBtn" aria-expanded="false">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round"><path d="M21 12a8 8 0 01-8 8H4l2-3.2A8 8 0 1121 12z"/></svg>
<span>Comments</span>
</button>
</div>
</div>
<p class="art-footnote">Everything above is fake data in one static file — nothing reads from <code>apps/player</code> yet. Real wiring would live in <code>apps/player/src/annotate/Toolbar.tsx</code> behind the <code>frame_annotations</code> flag.</p>
</div>
<!-- ═══ OPEN QUESTIONS ═══ -->
<div class="questions">
<div class="label">Open questions for you</div>
<h2>Four layout calls I'd rather not guess</h2>
<p class="sub">Tap a chip to record your pick — the reply template below fills itself in.</p>
<div class="q" data-q="placement">
<p class="q-text"><span class="qn">Q1</span>Which placement should ship? The pill stays near the pointer but covers action in the lower third; the rail never occludes footage but is far from the seekbar; under-seekbar steals ~56px of player height.</p>
<div class="q-chips">
<button class="chip" data-a="Floating pill"><span class="k">A</span>Floating pill</button>
<button class="chip" data-a="Docked left rail"><span class="k">B</span>Docked left rail</button>
<button class="chip" data-a="Under the seekbar"><span class="k">C</span>Under the seekbar</button>
</div>
</div>
<div class="q" data-q="colorstroke">
<p class="q-text"><span class="qn">Q2</span>Color + stroke controls: always visible in the bar (what's mocked), or collapsed behind a popover on the active tool? Inline is one click faster; popover shrinks the pill by ~140px, which matters on 13″ laptops.</p>
<div class="q-chips">
<button class="chip" data-a="Always inline"><span class="k">A</span>Always inline</button>
<button class="chip" data-a="Popover on active tool"><span class="k">B</span>Popover on active tool</button>
</div>
</div>
<div class="q" data-q="drawer">
<p class="q-text"><span class="qn">Q3</span>The comment drawer currently overlays the right edge of the frame (open it via the Comments button). Should it instead push the video left so annotations are never covered while reading?</p>
<div class="q-chips">
<button class="chip" data-a="Overlay the frame"><span class="k">A</span>Overlay the frame</button>
<button class="chip" data-a="Push the video left"><span class="k">B</span>Push the video left</button>
</div>
</div>
<div class="q" data-q="blur">
<p class="q-text"><span class="qn">Q4</span>Blur is the only tool that needs server-side rendering on export. Keep it in the v1 toolbar (with a "processing on export" badge), or hold it for v1.1 so the first release ships all-client-side?</p>
<div class="q-chips">
<button class="chip" data-a="Ship blur in v1"><span class="k">A</span>Ship in v1</button>
<button class="chip" data-a="Hold blur for v1.1"><span class="k">B</span>Hold for v1.1</button>
</div>
</div>
<div class="reply">
<div class="label">My reactions — copy &amp; paste back</div>
<button class="copybtn" id="copyReply">copy reply</button>
<pre id="replyText"></pre>
</div>
</div>
<footer>Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.</footer>
</div>
<script>
(function () {
"use strict";
/* clipboard helper with fallback */
function copyText(text, btn) {
function flash() {
var old = btn.textContent;
btn.textContent = "Copied";
btn.classList.add("done");
setTimeout(function () { btn.textContent = old; btn.classList.remove("done"); }, 1400);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(flash, function () { legacy(); });
} else { legacy(); }
function legacy() {
var ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); flash(); } catch (e) {}
document.body.removeChild(ta);
}
}
/* copy prompt */
var promptBtn = document.getElementById("copyPrompt");
promptBtn.addEventListener("click", function () {
copyText(document.getElementById("promptText").textContent.trim(), promptBtn);
});
/* layout variant segmented control */
var seg = document.getElementById("variantSeg");
var stagewrap = document.getElementById("stagewrap");
var note = document.getElementById("variantNote");
var notes = {
"v-pill": "<b>Floating pill, bottom-center.</b> Hovers above the seekbar; hides on playback. Closest to what Loom/Frame.io ship.",
"v-rail": "<b>Docked left rail.</b> Never occludes footage; tools stack vertically like an editor. Costs the most horizontal room on small screens.",
"v-dock": "<b>Attached under the seekbar.</b> Zero occlusion, always visible — but steals ~56px of player height and sits far from where you draw."
};
Array.prototype.forEach.call(seg.querySelectorAll("button"), function (b) {
b.addEventListener("click", function () {
Array.prototype.forEach.call(seg.querySelectorAll("button"), function (x) { x.classList.remove("on"); });
b.classList.add("on");
stagewrap.className = "stagewrap " + b.dataset.v;
note.innerHTML = notes[b.dataset.v];
});
});
/* single-select helper for button groups */
function group(containerId, selector) {
var c = document.getElementById(containerId);
Array.prototype.forEach.call(c.querySelectorAll(selector), function (b) {
b.addEventListener("click", function () {
Array.prototype.forEach.call(c.querySelectorAll(selector), function (x) { x.classList.remove("on"); });
b.classList.add("on");
});
});
}
group("tools", ".tool");
group("swatches", ".swatch");
group("strokes", ".stroke");
/* comment drawer */
var drawer = document.getElementById("drawer");
var cmtBtn = document.getElementById("cmtBtn");
function setDrawer(open) {
drawer.classList.toggle("open", open);
drawer.setAttribute("aria-hidden", String(!open));
cmtBtn.setAttribute("aria-expanded", String(open));
cmtBtn.classList.toggle("on", open);
}
cmtBtn.addEventListener("click", function () { setDrawer(!drawer.classList.contains("open")); });
document.getElementById("drawerClose").addEventListener("click", function () { setDrawer(false); });
/* open-question chips → reply template */
var answers = { placement: null, colorstroke: null, drawer: null, blur: null };
var labels = {
placement: "Placement",
colorstroke: "Color/stroke controls",
drawer: "Comment drawer",
blur: "Blur tool"
};
var replyEl = document.getElementById("replyText");
function esc(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
}
function renderReply() {
var n = 0;
var html = Object.keys(answers).map(function (k) {
n += 1;
var v = answers[k];
return n + ". " + esc(labels[k]) + ": " +
(v ? '<span class="picked">' + esc(v) + "</span>"
: '<span class="blank">___ (tap a chip above)</span>');
}).join("\n");
html += '\nOther notes: <span class="blank">___</span>';
replyEl.innerHTML = html;
}
function replyPlain() {
var n = 0;
return Object.keys(answers).map(function (k) {
n += 1;
return n + ". " + labels[k] + ": " + (answers[k] || "___");
}).join("\n") + "\nOther notes: ___";
}
Array.prototype.forEach.call(document.querySelectorAll(".q"), function (q) {
var key = q.dataset.q;
Array.prototype.forEach.call(q.querySelectorAll(".chip"), function (chip) {
chip.addEventListener("click", function () {
Array.prototype.forEach.call(q.querySelectorAll(".chip"), function (x) { x.classList.remove("on"); });
chip.classList.add("on");
answers[key] = chip.dataset.a;
renderReply();
});
});
});
renderReply();
var copyReplyBtn = document.getElementById("copyReply");
copyReplyBtn.addEventListener("click", function () {
copyText("My reactions to the toolbar mock:\n" + replyPlain(), copyReplyBtn);
});
})();
</script>
</body>
</html>
+716
View File
@@ -0,0 +1,716 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Brainstorm the intervention — Know your unknowns</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px 160px; }
/* ── header ─────────────────────────── */
header.page-head {
padding: 56px 0 8px;
position: relative;
}
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 12px;
}
.eyebrow::before {
content: "";
width: 24px; height: 1.5px;
background: var(--clay);
flex: none;
}
a.back {
position: absolute;
top: 58px; right: 0;
font-family: var(--mono);
font-size: 12px;
color: var(--g500);
text-decoration: none;
}
a.back:hover { color: var(--clay); }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(32px, 4.6vw, 46px);
line-height: 1.08;
letter-spacing: -0.018em;
margin: 0 0 10px;
}
.lede {
font-size: 16px;
color: var(--g700);
max-width: 640px;
margin: 0 0 30px;
}
/* ── prompt box ─────────────────────── */
.prompt-card {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 22px 24px 24px;
position: relative;
}
.prompt-label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--clay);
margin-bottom: 12px;
}
.prompt-card blockquote {
margin: 0;
font-family: var(--serif);
font-style: italic;
font-size: 17.5px;
line-height: 1.5;
color: var(--slate);
max-width: 60ch;
}
button.copy-prompt {
position: absolute;
top: 16px; right: 16px;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.04em;
color: var(--g700);
background: var(--g100);
border: 1.5px solid var(--g300);
border-radius: 8px;
padding: 6px 12px;
cursor: pointer;
transition: border-color 120ms, color 120ms;
}
button.copy-prompt:hover { border-color: var(--clay); color: var(--clay-d); }
button.copy-prompt.copied { color: var(--olive); border-color: var(--olive); }
@media (max-width: 560px) {
button.copy-prompt { position: static; margin-top: 14px; }
}
/* ── divider ────────────────────────── */
.divider {
display: flex;
align-items: center;
gap: 16px;
margin: 46px 0 34px;
font-family: var(--mono);
font-size: 11.5px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--g500);
white-space: nowrap;
}
.divider::before, .divider::after {
content: "";
flex: 1;
height: 1px;
background: var(--g300);
}
/* ── artifact frame ─────────────────── */
.artifact {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 30px clamp(16px, 3.5vw, 34px) 34px;
}
.artifact h2 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(23px, 3vw, 29px);
letter-spacing: -0.015em;
line-height: 1.15;
margin: 6px 0 6px;
}
.art-eyebrow {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--g500);
display: flex;
align-items: center;
gap: 10px;
}
.art-eyebrow::before {
content: "";
width: 24px; height: 1.5px;
background: var(--clay);
flex: none;
}
.art-sub {
font-size: 14.5px;
color: var(--g700);
max-width: 66ch;
margin: 0 0 26px;
}
.art-sub code {
font-family: var(--mono);
font-size: 12.5px;
background: var(--g100);
border-radius: 5px;
padding: 1px 5px;
}
/* ── spectrum strip ─────────────────── */
.spectrum {
margin: 8px 0 34px;
}
.spec-labels {
display: flex;
justify-content: space-between;
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--g500);
margin-bottom: 10px;
}
.spec-labels .lo { color: var(--olive); }
.spec-labels .hi { color: var(--clay-d); }
.spec-track {
position: relative;
height: 56px;
}
.spec-line {
position: absolute;
left: 0; right: 0; top: 27px;
height: 2px;
background: linear-gradient(90deg, var(--olive), var(--oat) 45%, var(--clay));
border-radius: 1px;
}
.spec-dot {
position: absolute;
top: 27px;
transform: translate(-50%, -50%);
width: 30px; height: 30px;
border-radius: 50%;
background: var(--paper);
border: 2px solid var(--g500);
color: var(--g700);
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
padding: 0;
transition: border-color 120ms, background 120ms, color 120ms, transform 120ms;
}
.spec-dot:hover { border-color: var(--slate); transform: translate(-50%, -50%) scale(1.12); }
.spec-dot.on {
background: var(--clay);
border-color: var(--clay-d);
color: #fff;
}
.spec-ticks {
display: flex;
justify-content: space-between;
font-size: 11px;
color: var(--g500);
margin-top: 2px;
}
@media (max-width: 560px) {
.spec-dot { width: 24px; height: 24px; font-size: 10.5px; }
.spec-labels { font-size: 9px; }
}
/* ── intervention cards ─────────────── */
.cards { display: flex; flex-direction: column; gap: 14px; }
.iv {
border: 1.5px solid var(--g300);
border-radius: 12px;
background: var(--ivory);
padding: 18px 20px 16px;
scroll-margin-top: 20px;
transition: border-color 140ms, background 140ms;
}
.iv.checked { border-color: var(--clay); background: #FDF6F2; }
.iv-top {
display: flex;
align-items: baseline;
gap: 12px;
flex-wrap: wrap;
}
.iv-num {
font-family: var(--mono);
font-size: 13px;
font-weight: 700;
color: var(--clay-d);
flex: none;
}
.iv h3 {
font-family: var(--serif);
font-weight: 500;
font-size: 19px;
letter-spacing: -0.01em;
margin: 0;
flex: 1 1 auto;
min-width: 200px;
}
.chips { display: flex; gap: 8px; align-items: center; flex: none; }
.effort {
font-family: var(--mono);
font-size: 10.5px;
font-weight: 700;
letter-spacing: 0.06em;
border-radius: 999px;
padding: 3px 10px;
border: 1.5px solid transparent;
}
.effort.s { background: #EEF2E6; color: var(--olive); border-color: #C9D4B6; }
.effort.m { background: var(--oat); color: #7A6A4F; border-color: var(--g300); }
.effort.l { background: #F8E7DE; color: var(--clay-d); border-color: #EBC5B4; }
.effort.xl { background: var(--clay); color: #fff; border-color: var(--clay-d); }
.path {
display: inline-block;
font-family: var(--mono);
font-size: 11.5px;
color: var(--g700);
background: var(--g100);
border: 1px solid var(--g200);
border-radius: 6px;
padding: 2px 8px;
margin: 8px 0 2px;
max-width: 100%;
overflow-wrap: anywhere;
}
.iv-body { font-size: 14px; color: var(--g700); margin: 8px 0 0; }
.iv-body strong { color: var(--slate); font-weight: 600; }
.iv-found {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--g500);
}
.iv-impact {
display: flex;
gap: 8px;
align-items: baseline;
margin-top: 10px;
font-size: 13.5px;
color: var(--g700);
}
.iv-impact .tag {
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--olive);
flex: none;
}
.iv-foot {
margin-top: 14px;
padding-top: 12px;
border-top: 1px dashed var(--g300);
}
label.resonate {
display: inline-flex;
align-items: center;
gap: 9px;
font-size: 13.5px;
font-weight: 500;
color: var(--g700);
cursor: pointer;
user-select: none;
}
label.resonate input {
appearance: none;
-webkit-appearance: none;
width: 18px; height: 18px;
border: 1.5px solid var(--g500);
border-radius: 5px;
background: var(--paper);
cursor: pointer;
margin: 0;
display: grid;
place-content: center;
transition: border-color 120ms, background 120ms;
}
label.resonate input::before {
content: "";
width: 9px; height: 9px;
transform: scale(0);
transition: transform 100ms;
clip-path: polygon(14% 44%, 0 60%, 40% 100%, 100% 16%, 84% 4%, 38% 71%);
background: #fff;
}
label.resonate input:checked { background: var(--clay); border-color: var(--clay-d); }
label.resonate input:checked::before { transform: scale(1.15); }
.iv.checked label.resonate { color: var(--clay-d); }
/* ── sticky reply bar ───────────────── */
.replybar {
position: fixed;
left: 0; right: 0; bottom: 0;
background: var(--slate);
color: var(--ivory);
z-index: 50;
box-shadow: 0 -8px 24px rgba(20,20,19,.15);
}
.replybar-inner {
max-width: 980px;
margin: 0 auto;
padding: 12px 24px;
display: flex;
align-items: center;
gap: 16px;
}
.rb-count {
font-family: var(--mono);
font-size: 12px;
color: var(--oat);
flex: none;
white-space: nowrap;
}
.rb-count b { color: var(--clay); font-size: 15px; }
.rb-reply {
flex: 1;
font-family: var(--mono);
font-size: 12.5px;
color: var(--g300);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
min-width: 0;
}
.rb-reply.live { color: var(--ivory); }
button.rb-copy {
flex: none;
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.03em;
background: var(--clay);
color: #fff;
border: 1.5px solid var(--clay-d);
border-radius: 8px;
padding: 8px 16px;
cursor: pointer;
transition: background 120ms, opacity 120ms;
}
button.rb-copy:hover:not(:disabled) { background: var(--clay-d); }
button.rb-copy:disabled { opacity: .35; cursor: default; }
button.rb-copy.copied { background: var(--olive); border-color: var(--olive); }
@media (max-width: 640px) {
.replybar-inner { flex-wrap: wrap; gap: 8px; padding: 10px 16px; }
.rb-reply { order: 3; flex-basis: 100%; }
}
/* ── footer ─────────────────────────── */
footer {
margin-top: 56px;
font-size: 13px;
color: var(--g500);
}
footer a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
footer a:hover { text-decoration-color: var(--clay); }
</style>
</head>
<body>
<div class="wrap">
<header class="page-head">
<a class="back" href="index.html">&larr; All examples</a>
<div class="eyebrow">Know your unknowns &middot; Pre-implementation</div>
<h1>Brainstorm the intervention</h1>
<p class="lede">Before committing to a fix, ask for the whole option space — grounded in what actually exists in the code — so the choice you make is informed by alternatives you didn't know you had.</p>
<div class="prompt-card">
<div class="prompt-label">The prompt</div>
<blockquote>&ldquo;Here's my rough problem: Acme users churn after onboarding. Search the codebase and brainstorm 10 places we could intervene, from cheapest to most ambitious. I'll tell you which ones resonate.&rdquo;</blockquote>
<button class="copy-prompt" id="copyPrompt" type="button">copy prompt</button>
</div>
</header>
<div class="divider">What Claude produced</div>
<div class="artifact">
<div class="art-eyebrow">Acme &middot; post-onboarding churn</div>
<h2>10 places to intervene, cheapest to most ambitious</h2>
<p class="art-sub">I searched <code>apps/web</code>, <code>services/</code>, and <code>packages/</code> for onboarding, invite, and first-session code paths. A pattern showed up: <strong>a lot of retention machinery already exists but is disconnected</strong> — the cheapest options below are mostly wiring, not building. Check the ones that resonate and copy the reply at the bottom.</p>
<div class="spectrum">
<div class="spec-labels">
<span class="lo">Ship this afternoon</span>
<span class="hi">Quarter-long bet</span>
</div>
<div class="spec-track" id="specTrack">
<div class="spec-line"></div>
<button class="spec-dot" style="left:3%" data-n="1" type="button" title="Fix the dead-end empty state">1</button>
<button class="spec-dot" style="left:10%" data-n="2" type="button" title="Turn on the sample project">2</button>
<button class="spec-dot" style="left:19%" data-n="3" type="button" title="Show pending invites in-app">3</button>
<button class="spec-dot" style="left:29%" data-n="4" type="button" title="Wire up the milestones checklist">4</button>
<button class="spec-dot" style="left:39%" data-n="5" type="button" title="Engineer the first-comment moment">5</button>
<button class="spec-dot" style="left:49%" data-n="6" type="button" title="Rescue silently failed first uploads">6</button>
<button class="spec-dot" style="left:60%" data-n="7" type="button" title="Watch-together first session">7</button>
<button class="spec-dot" style="left:71%" data-n="8" type="button" title="Review workflow templates">8</button>
<button class="spec-dot" style="left:84%" data-n="9" type="button" title="Built-in quick screen recording">9</button>
<button class="spec-dot" style="left:96%" data-n="10" type="button" title="First-class client review portals">10</button>
</div>
<div class="spec-ticks"><span>wiring</span><span>new UI</span><span>new lifecycle</span><span>new surface</span></div>
</div>
<div class="cards">
<div class="iv" id="iv1">
<div class="iv-top">
<span class="iv-num">#1</span>
<h3>Fix the dead-end empty state</h3>
<div class="chips"><span class="effort s">S</span></div>
</div>
<span class="path">apps/web/src/onboarding/EmptyState.tsx</span>
<p class="iv-body"><span class="iv-found">Found in code —</span> The zero-projects dashboard renders the illustration and the copy <em>&ldquo;No projects yet&rdquo;</em> — and nothing else. <code>NewProjectButton</code> exists two directories over but was never imported here; the only way forward is the tiny &ldquo;+&rdquo; in the top nav. Every churned user's last screen was probably this one.</p>
<div class="iv-impact"><span class="tag">Impact</span>100% of new users hit this screen; a real CTA plus a &ldquo;drop a video anywhere&rdquo; hint removes the most literal dead end in the product.</div>
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="1"> This resonates</label></div>
</div>
<div class="iv" id="iv2">
<div class="iv-top">
<span class="iv-num">#2</span>
<h3>Turn on the sample project that already exists</h3>
<div class="chips"><span class="effort s">S</span></div>
</div>
<span class="path">apps/web/src/projects/sampleProject.ts</span>
<p class="iv-body"><span class="iv-found">Found in code —</span> Someone built a complete seeded demo project — &ldquo;Product teaser v3&rdquo; with a 45s clip, four timestamped comments, and one resolved thread — behind the flag <code>sample_project_v1</code>. The flag is still <strong>false for everyone</strong> and the file hasn't been touched in 8 months. The feature is done; it was just never shipped.</p>
<div class="iv-impact"><span class="tag">Impact</span>New users see what a live review looks like before they've uploaded anything. Cheapest possible &ldquo;aha&rdquo; — flip a flag, verify the seed script.</div>
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="2"> This resonates</label></div>
</div>
<div class="iv" id="iv3">
<div class="iv-top">
<span class="iv-num">#3</span>
<h3>Show pending invites inside the app</h3>
<div class="chips"><span class="effort s">S</span></div>
</div>
<span class="path">services/mailer/templates/invite.mjml &middot; apps/web/src/team/TeamPanel.tsx</span>
<p class="iv-body"><span class="iv-found">Found in code —</span> Invites go out as email only. <code>TeamPanel</code> queries <code>team_members</code> but ignores the <code>invites</code> table entirely — an inviter can't see that their teammate never accepted, and there's no resend. For a <strong>collaborative</strong> review tool, a solo user is a churn-in-waiting.</p>
<div class="iv-impact"><span class="tag">Impact</span>Surfacing &ldquo;2 invites pending &middot; resend&rdquo; turns invisible invite decay into a visible, fixable state.</div>
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="3"> This resonates</label></div>
</div>
<div class="iv" id="iv4">
<div class="iv-top">
<span class="iv-num">#4</span>
<h3>Wire the milestones table to a dashboard checklist</h3>
<div class="chips"><span class="effort m">M</span></div>
</div>
<span class="path">services/api/src/models/userMilestones.ts &middot; apps/web/src/dashboard/Dashboard.tsx</span>
<p class="iv-body"><span class="iv-found">Found in code —</span> The backend already records <code>first_upload</code>, <code>first_comment</code>, <code>first_invite_accepted</code>, and <code>first_approval</code> in <code>user_milestones</code> — but <strong>no client code reads the table</strong>. It exists purely for an analytics dashboard. All the ingredients for a &ldquo;3 of 4 steps done&rdquo; checklist are sitting there.</p>
<div class="iv-impact"><span class="tag">Impact</span>Checklists reliably lift activation; this one needs one endpoint and one component because tracking already works.</div>
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="4"> This resonates</label></div>
</div>
<div class="iv" id="iv5">
<div class="iv-top">
<span class="iv-num">#5</span>
<h3>Engineer the first-comment moment</h3>
<div class="chips"><span class="effort m">M</span></div>
</div>
<span class="path">apps/web/src/review/CommentComposer.tsx &middot; apps/web/src/review/Timeline.tsx</span>
<p class="iv-body"><span class="iv-found">Found in code —</span> Timestamped comments are Acme's core loop, but the affordance is hidden: clicking the timeline seeds the timestamp into the composer, and nothing teaches this. <code>Timeline.tsx</code> has a <code>TODO: first-run hint?</code> from the original author. Add a ghost pin on first play — &ldquo;click anywhere to comment at this moment&rdquo;.</p>
<div class="iv-impact"><span class="tag">Impact</span>If first-session comment rate is the activation metric (it should be), this is the most direct lever on it.</div>
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="5"> This resonates</label></div>
</div>
<div class="iv" id="iv6">
<div class="iv-top">
<span class="iv-num">#6</span>
<h3>Rescue silently failed first uploads</h3>
<div class="chips"><span class="effort m">M</span></div>
</div>
<span class="path">services/ingest/src/transcode/queue.ts</span>
<p class="iv-body"><span class="iv-found">Found in code —</span> When transcode fails (unsupported codec, oversized ProRes, corrupt moov atom), the job moves to <code>failed</code>, logs to Sentry, and <strong>tells the user nothing</strong> — the project card just shows a spinner forever. There's a retry-with-fallback-preset code path that's only reachable from an internal admin script.</p>
<div class="iv-impact"><span class="tag">Impact</span>A user whose first upload hangs forever doesn't file a ticket — they leave. Auto-retry plus an honest error email converts a silent churn path into a support path.</div>
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="6"> This resonates</label></div>
</div>
<div class="iv" id="iv7">
<div class="iv-top">
<span class="iv-num">#7</span>
<h3>Watch-together first session for teams</h3>
<div class="chips"><span class="effort l">L</span></div>
</div>
<span class="path">apps/web/src/review/PresenceProvider.tsx &middot; services/realtime/</span>
<p class="iv-body"><span class="iv-found">Found in code —</span> Live presence is fully built — shared playhead, live cursors, &ldquo;Maya is watching&rdquo; badges — but it only lights up if two people <em>happen</em> to open the same review. Nothing engineers that coincidence. Add a &ldquo;review together&rdquo; scheduling nudge when a teammate accepts an invite, deep-linking both into the same session.</p>
<div class="iv-impact"><span class="tag">Impact</span>Teams that experience one live session likely retain at a different rate than solo users; this makes the multiplayer moment deliberate instead of lucky.</div>
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="7"> This resonates</label></div>
</div>
<div class="iv" id="iv8">
<div class="iv-top">
<span class="iv-num">#8</span>
<h3>Ship five review-workflow templates</h3>
<div class="chips"><span class="effort l">L</span></div>
</div>
<span class="path">packages/workflows/src/definitions/</span>
<p class="iv-body"><span class="iv-found">Found in code —</span> Approval workflows (rough cut &rarr; client review &rarr; final sign-off) are fully configurable — and start <strong>completely blank</strong>. The definitions directory contains only <code>__fixtures__</code> for tests. Judging by a comment in <code>WorkflowEditor.tsx</code>, power users copy a shared Notion doc to set theirs up. Ship 5 opinionated templates: agency delivery, YouTube pipeline, client sign-off, internal QA, localization pass.</p>
<div class="iv-impact"><span class="tag">Impact</span>Templates convert &ldquo;configure this abstraction&rdquo; into &ldquo;pick your job&rdquo; — the classic fix for blank-canvas churn in workflow tools.</div>
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="8"> This resonates</label></div>
</div>
<div class="iv" id="iv9">
<div class="iv-top">
<span class="iv-num">#9</span>
<h3>Built-in quick screen &amp; camera recording</h3>
<div class="chips"><span class="effort xl">XL</span></div>
</div>
<span class="path">new surface &middot; services/ingest already accepts chunked webm</span>
<p class="iv-body"><span class="iv-found">Found in code —</span> Today Acme's onboarding has a hard external dependency: <strong>you need a finished video before the product is useful</strong>. But <code>services/ingest</code> already accepts chunked webm uploads (built for the mobile app), so a MediaRecorder-based &ldquo;record your screen, get feedback&rdquo; flow needs no pipeline work — &ldquo;just&rdquo; the capture UI. Removes the biggest reason a trial goes idle: nothing to review yet.</p>
<div class="iv-impact"><span class="tag">Impact</span>Changes who can activate — designers and PMs without an editor can create the first reviewable artifact themselves. Big build, big surface area.</div>
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="9"> This resonates</label></div>
</div>
<div class="iv" id="iv10">
<div class="iv-top">
<span class="iv-num">#10</span>
<h3>First-class client review portals</h3>
<div class="chips"><span class="effort xl">XL</span></div>
</div>
<span class="path">services/auth/src/guestTokens.ts &middot; apps/web/src/share/</span>
<p class="iv-body"><span class="iv-found">Found in code —</span> Guest share links exist but are deliberately crippled: tokens expire after 24h, guests can't comment, and there's a <code>// TODO(sasha): revisit guest perms after launch</code> from two years ago. The users who churn are often the ones whose <em>clients</em> wouldn't sign up. Make guests first-class — persistent portal, comment-with-name, no account — and every external review becomes a retention loop (and an acquisition one).</p>
<div class="iv-impact"><span class="tag">Impact</span>Attacks the structural cause: Acme's value depends on people who refuse to create accounts. Quarter-long bet with auth, billing, and abuse-prevention implications.</div>
<div class="iv-foot"><label class="resonate"><input type="checkbox" data-n="10"> This resonates</label></div>
</div>
</div>
</div>
<footer>Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.</footer>
</div>
<div class="replybar">
<div class="replybar-inner">
<span class="rb-count"><b id="rbNum">0</b> resonate</span>
<span class="rb-reply" id="rbReply">Check the interventions that resonate — your reply builds here.</span>
<button class="rb-copy" id="rbCopy" type="button" disabled>copy reply</button>
</div>
</div>
<script>
(function () {
var PROMPT = "Here's my rough problem: Acme users churn after onboarding. Search the codebase and brainstorm 10 places we could intervene, from cheapest to most ambitious. I'll tell you which ones resonate.";
function copyText(text, btn, label, doneLabel) {
function done() {
btn.classList.add('copied');
btn.textContent = doneLabel;
setTimeout(function () {
btn.classList.remove('copied');
btn.textContent = label;
}, 1600);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallback(); });
} else { fallback(); }
function fallback() {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
done();
}
}
var copyPromptBtn = document.getElementById('copyPrompt');
copyPromptBtn.addEventListener('click', function () {
copyText(PROMPT, copyPromptBtn, 'copy prompt', 'copied');
});
var boxes = Array.prototype.slice.call(document.querySelectorAll('label.resonate input'));
var dots = {};
Array.prototype.forEach.call(document.querySelectorAll('.spec-dot'), function (d) {
dots[d.getAttribute('data-n')] = d;
d.addEventListener('click', function () {
var card = document.getElementById('iv' + d.getAttribute('data-n'));
if (card) card.scrollIntoView({ behavior: 'smooth', block: 'center' });
});
});
var rbNum = document.getElementById('rbNum');
var rbReply = document.getElementById('rbReply');
var rbCopy = document.getElementById('rbCopy');
var currentReply = '';
function update() {
var picked = boxes
.filter(function (b) { return b.checked; })
.map(function (b) { return parseInt(b.getAttribute('data-n'), 10); })
.sort(function (a, b) { return a - b; });
boxes.forEach(function (b) {
var n = b.getAttribute('data-n');
var card = document.getElementById('iv' + n);
if (card) card.classList.toggle('checked', b.checked);
if (dots[n]) dots[n].classList.toggle('on', b.checked);
});
rbNum.textContent = picked.length;
if (picked.length === 0) {
currentReply = '';
rbReply.textContent = 'Check the interventions that resonate — your reply builds here.';
rbReply.classList.remove('live');
rbCopy.disabled = true;
} else {
var list = picked.map(function (n) { return '#' + n; }).join(', ');
currentReply = 'These resonate: ' + list + ' — start with #' + picked[0];
rbReply.textContent = '“' + currentReply + '”';
rbReply.classList.add('live');
rbCopy.disabled = false;
}
}
boxes.forEach(function (b) { b.addEventListener('change', update); });
rbCopy.addEventListener('click', function () {
if (currentReply) copyText(currentReply, rbCopy, 'copy reply', 'copied');
});
update();
})();
</script>
</body>
</html>
+644
View File
@@ -0,0 +1,644 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The interview — Know your unknowns</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px 100px; }
/* ── header ─────────────────────────── */
header { padding: 56px 0 8px; position: relative; }
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 12px;
}
.eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
.backlink {
position: absolute; top: 60px; right: 0;
font-family: var(--mono); font-size: 12px;
color: var(--g500); text-decoration: none;
letter-spacing: 0.04em;
}
.backlink:hover { color: var(--clay); }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(34px, 5vw, 50px);
line-height: 1.08;
letter-spacing: -0.018em;
margin: 0 0 10px;
}
.lede { font-size: 16.5px; color: var(--g700); max-width: 640px; margin: 0 0 30px; }
/* ── prompt box ─────────────────────── */
.promptbox {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 22px 24px 20px;
position: relative;
}
.promptbox .label {
font-family: var(--mono); font-size: 12px;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--clay); margin-bottom: 10px;
display: flex; align-items: center; gap: 12px;
}
.promptbox .label::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
.promptbox blockquote {
margin: 0; padding: 0;
font-family: var(--serif); font-style: italic;
font-size: 17.5px; line-height: 1.5; color: var(--slate);
max-width: 46em;
}
.copybtn {
position: absolute; top: 18px; right: 18px;
font-family: var(--mono); font-size: 11.5px;
letter-spacing: 0.04em;
background: var(--g100); color: var(--g700);
border: 1.5px solid var(--g300); border-radius: 7px;
padding: 5px 11px; cursor: pointer;
}
.copybtn:hover { border-color: var(--clay); color: var(--clay-d); }
.copybtn.done { background: var(--olive); border-color: var(--olive); color: #fff; }
/* ── divider ────────────────────────── */
.divider {
display: flex; align-items: center; gap: 16px;
margin: 44px 0 30px;
font-family: var(--mono); font-size: 12px;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--g500); white-space: nowrap;
}
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--g300); }
/* ══ THE ARTIFACT ═════════════════════ */
.artifact {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
overflow: hidden;
}
.art-head {
padding: 16px 22px;
border-bottom: 1.5px solid var(--g200);
display: flex; align-items: baseline; gap: 14px; flex-wrap: wrap;
background: var(--g100);
}
.art-head .t { font-family: var(--serif); font-weight: 500; font-size: 17px; letter-spacing: -0.01em; }
.art-head .s { font-family: var(--mono); font-size: 11.5px; color: var(--g500); }
.art-body { display: grid; grid-template-columns: 216px 1fr; min-height: 480px; }
@media (max-width: 720px) { .art-body { grid-template-columns: 1fr; min-height: 0; } }
/* rail */
.rail {
border-right: 1.5px solid var(--g200);
padding: 20px 16px;
background: var(--ivory);
}
@media (max-width: 720px) {
.rail { border-right: none; border-bottom: 1.5px solid var(--g200); padding: 14px 16px; }
}
.rail .rail-label {
font-family: var(--mono); font-size: 10.5px;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--g500); margin-bottom: 12px;
}
.rail-item {
display: flex; align-items: center; gap: 10px;
padding: 6px 8px; margin: 0 -8px;
border-radius: 8px;
font-size: 12.5px; color: var(--g500);
cursor: default;
border: none; background: none; width: calc(100% + 16px);
text-align: left; font-family: var(--sans);
}
.rail-item.done { color: var(--g700); cursor: pointer; }
.rail-item.done:hover { background: var(--g100); }
.rail-item.current { color: var(--slate); font-weight: 600; background: var(--g100); }
.rail-dot {
width: 11px; height: 11px; border-radius: 50%; flex: none;
border: 2px solid var(--dotc, var(--g300));
background: transparent;
}
.rail-item.done .rail-dot { background: var(--dotc); }
.rail-item.current .rail-dot { box-shadow: 0 0 0 3px var(--g200); }
.rail-item .rail-q { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rail-item .tick { margin-left: auto; font-size: 11px; color: var(--olive); flex: none; }
.rail-meta {
margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--g200);
font-family: var(--mono); font-size: 11px; color: var(--g500); line-height: 1.8;
}
.rail-meta b { color: var(--g700); font-weight: 600; }
@media (max-width: 720px) {
.rail .rail-q, .rail .tick, .rail-meta, .rail .rail-label { display: none; }
.rail { display: flex; align-items: center; gap: 10px; }
.rail-item { width: auto; padding: 4px; margin: 0; }
}
/* stage */
.stage { padding: 28px 30px 30px; position: relative; }
@media (max-width: 720px) { .stage { padding: 22px 18px 24px; } }
.qcard { animation: qin .28s ease both; }
.qcard.leaving { animation: qout .18s ease both; }
@keyframes qin { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }
@keyframes qout { from { opacity: 1; } to { opacity: 0; transform: translateY(-8px); } }
.badge {
display: inline-block;
font-family: var(--mono); font-size: 11px;
letter-spacing: 0.1em; text-transform: uppercase;
padding: 3px 9px; border-radius: 5px;
color: #fff; background: var(--g500);
margin-bottom: 4px;
}
.qnum { font-family: var(--mono); font-size: 11.5px; color: var(--g500); margin-left: 10px; }
.qtext {
font-family: var(--serif); font-weight: 500;
font-size: clamp(20px, 3vw, 25px); line-height: 1.25;
letter-spacing: -0.012em;
margin: 10px 0 6px; max-width: 34em;
}
.why {
font-size: 13.5px; color: var(--g500); margin: 0 0 22px;
display: flex; gap: 8px; align-items: baseline;
}
.why::before { content: "↳"; color: var(--clay); flex: none; }
.opts { display: grid; gap: 10px; grid-template-columns: 1fr 1fr; }
@media (max-width: 620px) { .opts { grid-template-columns: 1fr; } }
.opt {
text-align: left;
background: var(--ivory);
border: 1.5px solid var(--g300);
border-radius: 11px;
padding: 14px 16px;
cursor: pointer;
font-family: var(--sans);
color: var(--slate);
transition: border-color .12s, background .12s, transform .12s;
}
.opt:hover { border-color: var(--clay); background: var(--paper); transform: translateY(-1px); }
.opt.picked { border-color: var(--clay); background: var(--paper); box-shadow: 0 0 0 3px rgba(217,119,87,.15); }
.opt .ot { font-weight: 600; font-size: 14.5px; display: block; margin-bottom: 3px; }
.opt .od { font-size: 12.5px; color: var(--g500); line-height: 1.45; display: block; }
.otherrow { display: flex; gap: 8px; margin-top: 12px; }
.otherrow input {
flex: 1; min-width: 0;
font-family: var(--sans); font-size: 14px;
padding: 10px 13px;
border: 1.5px solid var(--g300); border-radius: 9px;
background: var(--ivory); color: var(--slate);
}
.otherrow input::placeholder { color: var(--g500); }
.otherrow input:focus { outline: none; border-color: var(--clay); background: var(--paper); }
.otherrow button {
font-family: var(--mono); font-size: 12px;
padding: 0 16px; border-radius: 9px; cursor: pointer;
border: 1.5px solid var(--g300); background: var(--g100); color: var(--g700);
}
.otherrow button:hover { border-color: var(--clay); color: var(--clay-d); }
.navrow { display: flex; justify-content: space-between; align-items: center; margin-top: 24px; }
.backbtn {
font-family: var(--mono); font-size: 12px;
background: none; border: none; cursor: pointer;
color: var(--g500); padding: 6px 0;
}
.backbtn:hover { color: var(--clay-d); }
.backbtn[disabled] { visibility: hidden; }
.navhint { font-family: var(--mono); font-size: 11px; color: var(--g500); }
/* summary */
.sum-h { font-family: var(--serif); font-weight: 500; font-size: 24px; letter-spacing: -0.012em; margin: 4px 0 4px; }
.sum-sub { font-size: 14px; color: var(--g500); margin: 0 0 20px; }
.dtable { width: 100%; border-collapse: collapse; font-size: 13.5px; }
.dtable-scroll { overflow-x: auto; }
.dtable th {
font-family: var(--mono); font-size: 10.5px;
letter-spacing: 0.1em; text-transform: uppercase;
color: var(--g500); font-weight: 500;
text-align: left; padding: 8px 12px 8px 0;
border-bottom: 1.5px solid var(--g300);
white-space: nowrap;
}
.dtable td { padding: 10px 12px 10px 0; border-bottom: 1px solid var(--g200); vertical-align: top; }
.dtable tr:last-child td { border-bottom: none; }
.dtable .pill {
font-family: var(--mono); font-size: 10px;
letter-spacing: 0.08em; text-transform: uppercase;
color: #fff; padding: 2px 7px; border-radius: 4px; white-space: nowrap;
}
.dtable .topic { color: var(--g500); white-space: nowrap; }
.dtable .dec { font-weight: 600; }
.dtable .redo {
font-family: var(--mono); font-size: 11px; color: var(--g500);
background: none; border: none; cursor: pointer; padding: 0;
}
.dtable .redo:hover { color: var(--clay-d); }
.genblock { margin-top: 26px; }
.genblock .gb-label {
font-family: var(--mono); font-size: 11px;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--clay); margin-bottom: 8px;
display: flex; align-items: center; gap: 10px;
}
.genblock .gb-label::before { content: ""; width: 18px; height: 1.5px; background: var(--clay); flex: none; }
.genwrap { position: relative; }
.genwrap pre {
margin: 0;
background: var(--slate);
color: var(--g200);
border-radius: 11px;
padding: 18px 20px;
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.7;
white-space: pre-wrap;
word-break: break-word;
}
.genwrap .copybtn { top: 12px; right: 12px; background: var(--g700); border-color: var(--g700); color: var(--g300); }
.genwrap .copybtn:hover { color: var(--paper); border-color: var(--g500); }
.genwrap .copybtn.done { background: var(--olive); border-color: var(--olive); color: #fff; }
.restart {
margin-top: 18px;
font-family: var(--mono); font-size: 12px;
background: none; border: none; cursor: pointer;
color: var(--g500); padding: 0;
}
.restart:hover { color: var(--clay-d); }
footer {
margin-top: 56px; padding-top: 22px;
border-top: 1px solid var(--g300);
font-size: 13px; color: var(--g500);
}
footer a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
footer a:hover { text-decoration-color: var(--clay); }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">Know your unknowns · Pre-implementation</div>
<a class="backlink" href="index.html">← All examples</a>
<h1>The interview</h1>
<p class="lede">Instead of guessing at ambiguous requirements, ask Claude to interview you — one question at a time, ordered by how much each answer would change the architecture.</p>
<div class="promptbox">
<div class="label">The prompt</div>
<button class="copybtn" id="copyPrompt">copy prompt</button>
<blockquote id="promptText">Interview me one question at a time about anything still ambiguous in the annotation-export feature. Prioritize questions where my answer would change the architecture.</blockquote>
</div>
</header>
<div class="divider">What Claude produced</div>
<div class="artifact">
<div class="art-head">
<span class="t">Annotation export — spec interview</span>
<span class="s">acme/web · 7 open questions · ordered by blast radius</span>
</div>
<div class="art-body">
<nav class="rail" id="rail" aria-label="Interview progress"></nav>
<div class="stage" id="stage"></div>
</div>
</div>
<footer>Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.</footer>
</div>
<script>
(function () {
"use strict";
var RADII = {
arch: { label: "Architecture", color: "var(--clay)" },
data: { label: "Data model", color: "var(--clay-d)" },
ux: { label: "UX", color: "var(--olive)" },
pol: { label: "Polish", color: "var(--g500)" }
};
var QUESTIONS = [
{
id: "runtime",
radius: "arch",
short: "Where exports run",
topic: "Execution model",
q: "Where should export generation actually run?",
why: "This decides whether we need a job queue, a notification path, and an artifacts bucket — or none of those. Everything else layers on top.",
opts: [
{ t: "Synchronously in the API request", d: "Simple, but a 400-comment review on a 2-hour cut will blow past the 30s gateway timeout." },
{ t: "Background job via the existing sidekiq queue", d: "Reuses jobs/export_worker patterns from proxy transcodes; needs a “ready” notification." },
{ t: "Entirely client-side from the annotations API", d: "Zero new backend, but caps us at formats the browser can assemble, and leaks nothing to support later." }
]
},
{
id: "scope",
radius: "arch",
short: "Export scope",
topic: "Scope of one export",
q: "What is the unit of a single export?",
why: "Project-level export means fan-out across videos, pagination, and a manifest format — roughly 3× the surface of per-video.",
opts: [
{ t: "One video at a time", d: "Matches the review-page mental model; a project export is just N clicks for now." },
{ t: "A whole project (all videos)", d: "What the Meridian Post account asked for; implies zip bundling and per-video manifests." },
{ t: "Arbitrary user-picked selection", d: "Most flexible, most UI; needs a selection model that doesnt exist anywhere yet." }
]
},
{
id: "drawings",
radius: "data",
short: "Drawings included?",
topic: "Drawing annotations",
q: "Do frame drawings export, or only text comments?",
why: "Drawings live as canvas stroke JSON in annotation_shapes; including them means rasterizing server-side or shipping raw vectors.",
opts: [
{ t: "Text comments only (v1)", d: "Ships fastest; drawings show as “[drawing on frame 4211]” placeholders." },
{ t: "Rasterize drawings to PNG stills", d: "Best for PDF reports; needs a headless render step — the thumbnail worker is close but not identical." },
{ t: "Include raw stroke JSON", d: "Lossless and cheap, but only useful to someone re-importing into Acme." }
]
},
{
id: "timecode",
radius: "data",
short: "Timestamp format",
topic: "Timestamp representation",
q: "How should annotation timestamps be represented in the export?",
why: "We store milliseconds; editors live in SMPTE timecode. Converting needs the source fps, which is null for 3% of legacy uploads.",
opts: [
{ t: "SMPTE timecode (HH:MM:SS:FF)", d: "What Resolve and Premiere expect; requires an fps backfill for legacy videos." },
{ t: "Plain seconds / milliseconds", d: "No conversion risk, but every editor will hand-convert and some will do it wrong." },
{ t: "Both columns, always", d: "Slightly wider files; nobody ever has to ask which one is authoritative." }
]
},
{
id: "formats",
radius: "ux",
short: "v1 formats",
topic: "Export formats",
q: "Which formats does v1 ship with?",
why: "Each format is mostly independent work, but the choice signals who the feature is for — producers, editors, or clients.",
opts: [
{ t: "CSV only", d: "One afternoon of work once the pipeline exists; producers can pivot it however they like." },
{ t: "PDF review report", d: "Client-facing artifact with thumbnails; only meaningful if drawings rasterize." },
{ t: "NLE marker file (EDL / Resolve CSV)", d: "The power-user ask from the forum thread; timecode question becomes load-bearing." },
{ t: "CSV + NLE markers, PDF later", d: "Covers both internal and editor workflows without blocking on rasterization." }
]
},
{
id: "access",
radius: "ux",
short: "Who can export",
topic: "Export permissions",
q: "Who is allowed to export?",
why: "Guest reviewers use unauthenticated share links — if they can export, annotations silently leave the permission boundary.",
opts: [
{ t: "Anyone who can view the video", d: "Includes share-link guests; simplest rule, biggest data-egress surface." },
{ t: "Project members only", d: "Guests keep commenting but cant bulk-extract; needs a disabled-state explanation in the UI." },
{ t: "Owner and admins only", d: "Most conservative; likely generates “can you export this for me” busywork." }
]
},
{
id: "naming",
radius: "pol",
short: "Files & naming",
topic: "File naming & bundling",
q: "How are export files named and bundled?",
why: "Pure convention, but the first support ticket after launch is always “which file is which cut.”",
opts: [
{ t: "{project}_{video}_{date}.csv, flat", d: "Predictable and grep-able; long titles get truncated at 80 chars." },
{ t: "Zip per export with a manifest.json", d: "One artifact per export regardless of scope; slight friction for single-file cases." },
{ t: "Let the user pick at export time", d: "A settings surface for something almost nobody will change." }
]
}
];
var answers = new Array(QUESTIONS.length).fill(null); // {text, custom}
var current = 0; // index, or QUESTIONS.length => summary
var rail = document.getElementById("rail");
var stage = document.getElementById("stage");
function answeredCount() {
return answers.filter(function (a) { return a !== null; }).length;
}
function esc(s) {
return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
function renderRail() {
var html = '<div class="rail-label">Interview progress</div>';
QUESTIONS.forEach(function (q, i) {
var r = RADII[q.radius];
var cls = "rail-item" + (i === current ? " current" : "") + (answers[i] ? " done" : "");
html += '<button type="button" class="' + cls + '" data-i="' + i + '" style="--dotc:' + r.color + '"' +
(answers[i] ? '' : ' tabindex="-1"') + '>' +
'<span class="rail-dot"></span><span class="rail-q">' + esc(q.short) + '</span>' +
(answers[i] ? '<span class="tick">✓</span>' : '') + '</button>';
});
html += '<div class="rail-meta"><b>' + answeredCount() + '</b> of 7 answered<br>' +
'<span style="color:var(--clay)">●</span> architecture &nbsp;' +
'<span style="color:var(--clay-d)">●</span> data<br>' +
'<span style="color:var(--olive)">●</span> ux &nbsp;' +
'<span style="color:var(--g500)">●</span> polish</div>';
rail.innerHTML = html;
rail.querySelectorAll(".rail-item.done").forEach(function (el) {
el.addEventListener("click", function () { go(parseInt(el.dataset.i, 10)); });
});
}
function renderQuestion() {
var q = QUESTIONS[current];
var r = RADII[q.radius];
var picked = answers[current];
var html = '<div class="qcard">' +
'<span class="badge" style="background:' + r.color + '">' + esc(r.label) + '</span>' +
'<span class="qnum">Question ' + (current + 1) + ' of ' + QUESTIONS.length + '</span>' +
'<h2 class="qtext">' + esc(q.q) + '</h2>' +
'<p class="why">' + esc(q.why) + '</p>' +
'<div class="opts">';
q.opts.forEach(function (o, oi) {
var isPicked = picked && !picked.custom && picked.text === o.t;
html += '<button type="button" class="opt' + (isPicked ? ' picked' : '') + '" data-oi="' + oi + '">' +
'<span class="ot">' + esc(o.t) + '</span><span class="od">' + esc(o.d) + '</span></button>';
});
html += '</div>' +
'<div class="otherrow">' +
'<input type="text" id="otherInput" placeholder="Other — type your own answer…" value="' +
(picked && picked.custom ? esc(picked.text) : '') + '">' +
'<button type="button" id="otherGo">answer ↵</button></div>' +
'<div class="navrow">' +
'<button type="button" class="backbtn" id="backBtn"' + (current === 0 ? ' disabled' : '') + '>← previous question</button>' +
'<span class="navhint">pick a card to continue</span>' +
'</div></div>';
stage.innerHTML = html;
stage.querySelectorAll(".opt").forEach(function (el) {
el.addEventListener("click", function () {
var o = q.opts[parseInt(el.dataset.oi, 10)];
answers[current] = { text: o.t, custom: false };
el.classList.add("picked");
renderRail();
setTimeout(function () { go(current + 1); }, 240);
});
});
var input = stage.querySelector("#otherInput");
function submitOther() {
var v = input.value.trim();
if (!v) { input.focus(); return; }
answers[current] = { text: v, custom: true };
renderRail();
go(current + 1);
}
stage.querySelector("#otherGo").addEventListener("click", submitOther);
input.addEventListener("keydown", function (e) { if (e.key === "Enter") submitOther(); });
stage.querySelector("#backBtn").addEventListener("click", function () { go(current - 1); });
}
function buildFollowupPrompt() {
var lines = [
"Here's what we decided in the interview — implement the",
"annotation-export feature with these constraints:",
""
];
QUESTIONS.forEach(function (q, i) {
var a = answers[i];
lines.push((i + 1) + ". " + q.topic + ": " + (a ? a.text : "(unanswered — use your judgment)") +
(a && a.custom ? " [my own wording — ask if unclear]" : ""));
});
lines.push("");
lines.push("Treat the architecture and data-model decisions (14) as fixed.");
lines.push("If anything in 57 conflicts with them, flag it before writing code.");
lines.push("Start with a short plan, then implement.");
return lines.join("\n");
}
function renderSummary() {
var rows = "";
QUESTIONS.forEach(function (q, i) {
var r = RADII[q.radius];
var a = answers[i];
rows += "<tr>" +
'<td><span class="pill" style="background:' + r.color + '">' + esc(r.label) + '</span></td>' +
'<td class="topic">' + esc(q.topic) + '</td>' +
'<td class="dec">' + (a ? esc(a.text) + (a.custom ? ' <span style="font-weight:400;color:var(--g500)">(your wording)</span>' : '') : '<span style="color:var(--g500)">—</span>') + '</td>' +
'<td><button type="button" class="redo" data-i="' + i + '">edit</button></td>' +
"</tr>";
});
stage.innerHTML = '<div class="qcard">' +
'<span class="badge" style="background:var(--olive)">Interview complete</span>' +
'<h2 class="sum-h">Seven decisions, zero assumptions.</h2>' +
'<p class="sum-sub">Every answer below would otherwise have been guessed silently during implementation.</p>' +
'<div class="dtable-scroll"><table class="dtable"><thead><tr>' +
'<th>Radius</th><th>Question</th><th>Decision</th><th></th>' +
'</tr></thead><tbody>' + rows + '</tbody></table></div>' +
'<div class="genblock">' +
'<div class="gb-label">Generated follow-up prompt</div>' +
'<div class="genwrap"><button type="button" class="copybtn" id="copyGen">copy</button>' +
'<pre id="genPre">' + esc(buildFollowupPrompt()) + '</pre></div>' +
'</div>' +
'<button type="button" class="restart" id="restartBtn">↺ start over</button>' +
'</div>';
stage.querySelectorAll(".redo").forEach(function (el) {
el.addEventListener("click", function () { go(parseInt(el.dataset.i, 10)); });
});
wireCopy(stage.querySelector("#copyGen"), function () { return buildFollowupPrompt(); });
stage.querySelector("#restartBtn").addEventListener("click", function () {
answers = new Array(QUESTIONS.length).fill(null);
go(0);
});
}
function go(idx) {
var card = stage.querySelector(".qcard");
var doRender = function () {
current = idx;
renderRail();
if (current >= QUESTIONS.length) renderSummary();
else renderQuestion();
};
if (card) {
card.classList.add("leaving");
setTimeout(doRender, 160);
} else {
doRender();
}
}
function wireCopy(btn, getText) {
btn.addEventListener("click", function () {
var text = getText();
var done = function () {
var orig = btn.textContent;
btn.textContent = "copied ✓";
btn.classList.add("done");
setTimeout(function () { btn.textContent = orig; btn.classList.remove("done"); }, 1600);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallbackCopy(text); done(); });
} else {
fallbackCopy(text); done();
}
});
}
function fallbackCopy(text) {
var ta = document.createElement("textarea");
ta.value = text;
ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); } catch (e) {}
document.body.removeChild(ta);
}
wireCopy(document.getElementById("copyPrompt"), function () {
return document.getElementById("promptText").textContent;
});
renderRail();
renderQuestion();
})();
</script>
</body>
</html>
+651
View File
@@ -0,0 +1,651 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Point at a reference — Know your unknowns</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px 100px; }
/* ── header ── */
header { padding: 56px 0 8px; position: relative; }
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 18px;
}
.eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
.backlink {
position: absolute; top: 60px; right: 0;
font-family: var(--mono); font-size: 12px;
color: var(--g500); text-decoration: none;
}
.backlink:hover { color: var(--clay); }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(32px, 5vw, 48px);
line-height: 1.08;
letter-spacing: -0.018em;
margin: 0 0 10px;
}
h2 {
font-family: var(--serif);
font-weight: 500;
letter-spacing: -0.012em;
font-size: 24px;
margin: 0 0 6px;
}
h3 { font-family: var(--serif); font-weight: 500; font-size: 18px; letter-spacing: -0.01em; margin: 0 0 4px; }
.lede { font-size: 16.5px; color: var(--g700); max-width: 640px; margin: 0 0 32px; }
/* ── prompt box ── */
.card {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
}
.promptbox { padding: 22px 26px 24px; margin-bottom: 44px; position: relative; }
.promptbox .label {
font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em;
text-transform: uppercase; color: var(--clay); margin-bottom: 12px;
}
.promptbox blockquote {
margin: 0; padding: 0 0 0 18px;
border-left: 3px solid var(--oat);
font-family: var(--serif); font-style: italic;
font-size: 17.5px; line-height: 1.5; color: var(--slate);
max-width: 760px;
}
.copybtn {
position: absolute; top: 18px; right: 18px;
font-family: var(--mono); font-size: 11px;
padding: 6px 12px; border-radius: 8px;
border: 1.5px solid var(--g300); background: var(--ivory);
color: var(--g700); cursor: pointer;
transition: border-color 120ms, color 120ms;
}
.copybtn:hover { border-color: var(--clay); color: var(--clay); }
.copybtn.done { border-color: var(--olive); color: var(--olive); }
/* ── divider ── */
.divider {
display: flex; align-items: center; gap: 16px;
font-family: var(--mono); font-size: 12px; letter-spacing: 0.14em;
text-transform: uppercase; color: var(--g500);
margin: 0 0 40px;
}
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--g300); }
/* ══════════ artifact ══════════ */
.artifact { }
.art-head {
display: flex; flex-wrap: wrap; align-items: baseline; gap: 8px 16px;
margin-bottom: 6px;
}
.art-head h2 { font-size: clamp(24px, 3.4vw, 30px); }
.badge {
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.08em;
text-transform: uppercase; padding: 3px 10px; border-radius: 999px;
border: 1.5px solid var(--g300); color: var(--g500); background: var(--paper);
white-space: nowrap;
}
.badge.await { border-color: var(--clay); color: var(--clay-d); background: #FBF1EC; }
.art-sub { color: var(--g700); font-size: 14.5px; margin: 0 0 8px; max-width: 720px; }
.art-paths {
font-family: var(--mono); font-size: 12px; color: var(--g500);
display: flex; flex-wrap: wrap; gap: 6px 18px; margin: 10px 0 36px;
}
.art-paths span b { color: var(--g700); font-weight: 600; }
.sec { margin: 0 0 46px; }
.sec-eyebrow {
font-family: var(--mono); font-size: 11px; letter-spacing: 0.14em;
text-transform: uppercase; color: var(--g500);
display: flex; align-items: center; gap: 10px; margin-bottom: 10px;
}
.sec-eyebrow .num {
font-family: var(--mono); font-size: 11px;
width: 20px; height: 20px; border-radius: 50%;
background: var(--slate); color: var(--ivory);
display: inline-flex; align-items: center; justify-content: center;
flex: none;
}
/* section 1 — bullets */
.bullets { padding: 20px 24px 8px; }
.bullets ul { list-style: none; margin: 0; padding: 0; }
.bullets li {
display: flex; gap: 14px; align-items: baseline;
padding: 10px 0 14px; border-bottom: 1px dashed var(--g200);
font-size: 15px; color: var(--g700);
}
.bullets li:last-child { border-bottom: 0; }
.bullets li::before {
content: ""; align-self: center;
width: 8px; height: 8px; border-radius: 2px;
background: var(--clay); flex: none; transform: rotate(45deg);
}
.bullets li b { color: var(--slate); font-weight: 600; }
.bullets code, .prose code, td code, .note code {
font-family: var(--mono); font-size: 0.86em;
background: var(--g100); border: 1px solid var(--g200);
border-radius: 5px; padding: 1px 5px; white-space: nowrap;
}
/* section 2 — code pairs */
.pair { margin-bottom: 26px; }
.pair-title {
display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap;
margin-bottom: 10px;
}
.pair-title .pt-n {
font-family: var(--mono); font-size: 11px; color: var(--clay-d);
letter-spacing: 0.08em;
}
.pair-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
}
@media (max-width: 760px) { .pair-grid { grid-template-columns: 1fr; } }
.codecard { overflow: hidden; display: flex; flex-direction: column; }
.codecard .chead {
display: flex; justify-content: space-between; align-items: center; gap: 10px;
padding: 9px 14px;
background: var(--g100); border-bottom: 1.5px solid var(--g300);
font-family: var(--mono); font-size: 11px; color: var(--g500);
white-space: nowrap; overflow: hidden;
}
.codecard .chead .lang {
font-size: 10px; letter-spacing: 0.1em; text-transform: uppercase;
padding: 2px 8px; border-radius: 999px; flex: none;
}
.lang.rust { background: var(--oat); color: var(--g700); }
.lang.ts { background: var(--slate); color: var(--ivory); }
.chead .fp { overflow: hidden; text-overflow: ellipsis; }
.codecard pre {
margin: 0; padding: 14px 16px;
overflow-x: auto;
font-family: var(--mono); font-size: 12.5px; line-height: 1.62;
color: var(--slate); flex: 1;
-webkit-overflow-scrolling: touch;
}
.codecard pre code { display: block; min-width: max-content; }
/* syntax palette */
.kw { color: var(--clay-d); }
.ty { color: var(--olive); font-weight: 600; }
.fn2 { color: var(--slate); font-weight: 600; }
.str { color: var(--olive); }
.nu { color: var(--clay-d); }
.cm { color: var(--g500); font-style: italic; }
/* matched highlight regions */
mark.hl {
background: #F6E8DF;
border-bottom: 2px solid var(--clay);
border-radius: 3px; padding: 0 2px; margin: 0 -2px;
color: inherit; cursor: default;
transition: background 120ms;
}
mark.hl sup {
font-family: var(--mono); font-size: 9px; color: var(--clay-d);
margin-left: 1px;
}
mark.hl.active { background: var(--oat); border-bottom-color: var(--clay-d); }
/* margin notes */
.notes { margin: 12px 2px 0; display: grid; gap: 8px; }
.note {
display: flex; gap: 10px; align-items: baseline;
font-size: 13.5px; color: var(--g700);
}
.note .nmark {
font-family: var(--mono); font-size: 10px; flex: none;
width: 18px; height: 18px; border-radius: 50%;
border: 1.5px solid var(--clay); color: var(--clay-d);
display: inline-flex; align-items: center; justify-content: center;
align-self: center; cursor: default;
transition: background 120ms;
}
.note.active .nmark { background: var(--clay); color: var(--paper); }
.note b { color: var(--slate); font-weight: 600; }
/* tables */
.tbl-scroll { overflow-x: auto; border: 1.5px solid var(--g300); border-radius: 14px; background: var(--paper); }
table { border-collapse: collapse; width: 100%; min-width: 660px; font-size: 13.5px; }
th {
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em;
text-transform: uppercase; text-align: left; color: var(--g500);
padding: 12px 16px; background: var(--g100);
border-bottom: 1.5px solid var(--g300);
}
td {
padding: 12px 16px; vertical-align: top;
border-bottom: 1px solid var(--g200); color: var(--g700);
}
tr:last-child td { border-bottom: 0; }
td b { color: var(--slate); font-weight: 600; }
th.col-keep { color: var(--olive); }
th.col-chg { color: var(--clay-d); }
th.col-drop { color: var(--g500); }
.pill {
display: inline-block; font-family: var(--mono); font-size: 10px;
letter-spacing: 0.06em; text-transform: uppercase;
padding: 2px 9px; border-radius: 999px; white-space: nowrap;
}
.pill.same { background: #EDF1E6; color: var(--olive); border: 1px solid #C9D4B8; }
.pill.equiv { background: #FBF1EC; color: var(--clay-d); border: 1px solid #EBC9B8; }
/* three-column preserved/changed/dropped */
.three { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 14px; }
@media (max-width: 820px) { .three { grid-template-columns: 1fr; } }
.col { padding: 18px 18px 12px; }
.col .colhead {
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.12em;
text-transform: uppercase; margin-bottom: 12px;
display: flex; align-items: center; gap: 8px;
}
.col .dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.col.keep .colhead { color: var(--olive); } .col.keep .dot { background: var(--olive); }
.col.chg .colhead { color: var(--clay-d); } .col.chg .dot { background: var(--clay); }
.col.drop .colhead { color: var(--g500); } .col.drop .dot { background: var(--g300); }
.col ul { margin: 0; padding: 0; list-style: none; }
.col li {
font-size: 13.5px; color: var(--g700);
padding: 8px 0; border-top: 1px dashed var(--g200);
}
.col li:first-child { border-top: 0; }
.col li b { color: var(--slate); font-weight: 600; }
.col.drop li { color: var(--g500); }
.col.drop li b { color: var(--g700); }
/* confirm card */
.confirm {
border-color: var(--clay);
padding: 26px 28px;
display: flex; flex-wrap: wrap; gap: 20px 32px;
align-items: center; justify-content: space-between;
background: linear-gradient(0deg, #FBF4EF, var(--paper));
}
.confirm .ctext { max-width: 520px; }
.confirm .ctext p { margin: 6px 0 0; font-size: 14px; color: var(--g700); }
.cbtns { display: flex; gap: 10px; flex-wrap: wrap; }
.btn {
font-family: var(--mono); font-size: 12.5px;
padding: 11px 18px; border-radius: 10px; cursor: pointer;
border: 1.5px solid var(--slate);
transition: transform 100ms, background 120ms, color 120ms, border-color 120ms;
}
.btn:active { transform: translateY(1px); }
.btn.primary { background: var(--slate); color: var(--ivory); }
.btn.primary:hover { background: var(--clay-d); border-color: var(--clay-d); }
.btn.primary.done { background: var(--olive); border-color: var(--olive); }
.btn.ghost { background: var(--paper); color: var(--g700); border-color: var(--g300); }
.btn.ghost:hover { border-color: var(--clay); color: var(--clay-d); }
footer {
margin-top: 80px; padding-top: 24px;
border-top: 1.5px solid var(--g300);
font-size: 13px; color: var(--g500);
}
footer a { color: var(--clay-d); text-decoration-color: var(--oat); text-underline-offset: 3px; }
</style>
</head>
<body>
<div class="wrap">
<header>
<div class="eyebrow">Know your unknowns · Pre-implementation</div>
<a class="backlink" href="index.html">&larr; All examples</a>
<h1>Point at a reference</h1>
<p class="lede">When an existing implementation already encodes the behavior you want, make Claude prove it understood the reference — as a reviewable artifact — before a single line gets ported.</p>
<div class="card promptbox">
<div class="label">The prompt</div>
<button class="copybtn" id="copyPrompt">copy prompt</button>
<blockquote id="promptText">This Rust crate in vendor/rate-limiter implements the exact backoff behavior I want. Read it and reimplement the same semantics in our TypeScript API client &mdash; but first show me a semantics map so I can confirm you understood it.</blockquote>
</div>
</header>
<div class="divider">What Claude produced</div>
<div class="artifact">
<div class="art-head">
<h2>Semantics map: <em style="font-style:italic;color:var(--clay-d)">rate-limiter</em> &rarr; Acme API client</h2>
<span class="badge await">awaiting confirmation</span>
</div>
<p class="art-sub">What the crate does, how each behavior maps to TypeScript, and every place the port cannot be a literal translation. Nothing gets implemented until you sign off on this page.</p>
<div class="art-paths">
<span><b>source</b> vendor/rate-limiter (Rust, 612 loc, 3 modules)</span>
<span><b>target</b> src/api/retry/ in acme-web</span>
<span><b>read</b> bucket.rs · backoff.rs · budget.rs · 14 tests</span>
</div>
<!-- ── 1. what it does ── -->
<section class="sec">
<div class="sec-eyebrow"><span class="num">1</span> What the crate actually does</div>
<div class="card bullets">
<ul>
<li><span><b>Token bucket admission</b> (<code>bucket.rs</code>): every outgoing request withdraws 1 token from a bucket that refills at <code>rate_per_sec = 8</code> up to <code>burst_capacity = 40</code>. The bucket starts <b>full</b>, so a cold client can burst 40 requests instantly.</span></li>
<li><span><b>Refill is lazy and integer-truncated</b>: tokens are minted only when a request arrives, from elapsed nanoseconds &times; rate &divide; 10&#8313;. Fractional tokens are never stored &mdash; instead, <code>last_refill</code> only advances when at least one whole token is minted, so sub-token elapsed time carries forward.</span></li>
<li><span><b>Decorrelated jitter backoff</b> (<code>backoff.rs</code>): on retryable failure, the next delay is drawn uniformly from <code>[base, min(prev &times; 3, cap)]</code> with <code>base = 250ms</code>, <code>cap = 30s</code>. Each draw seeds the next &mdash; delays wander rather than doubling in lockstep, which desynchronizes clients after an outage.</span></li>
<li><span><b>Retry budget</b> (<code>budget.rs</code>): a second, separate bucket. Every <b>successful first-try</b> request deposits 1; every <b>retry attempt</b> withdraws 10. When the balance can&rsquo;t cover a withdrawal, retries are refused entirely (first-try requests still go out). This caps retry amplification at ~10% during a sustained outage.</span></li>
<li><span><b>Failure classification is the caller&rsquo;s job</b>: the crate never inspects errors. It exposes <code>acquire()</code>, <code>next_delay()</code>, <code>try_withdraw()</code> and the caller decides what counts as retryable. The TS port keeps that boundary &mdash; Acme&rsquo;s <code>isRetryable()</code> in <code>src/api/errors.ts</code> stays the single source of truth.</span></li>
</ul>
</div>
</section>
<!-- ── 2. side-by-side ── -->
<section class="sec">
<div class="sec-eyebrow"><span class="num">2</span> Side-by-side: Rust source &rarr; proposed TypeScript</div>
<p class="art-sub" style="margin-bottom:20px">Highlighted regions correspond across columns &mdash; hover one to light up its counterpart and the gotcha note it maps to.</p>
<!-- pair A -->
<div class="pair">
<div class="pair-title"><span class="pt-n">PAIR A / 3</span><h3>Lazy refill &mdash; integer truncation must survive the float world</h3></div>
<div class="pair-grid">
<div class="card codecard">
<div class="chead"><span class="fp">vendor/rate-limiter/src/bucket.rs:41</span><span class="lang rust">Rust</span></div>
<pre><code><span class="kw">fn</span> <span class="fn2">refill</span>(&amp;<span class="kw">mut</span> <span class="kw">self</span>, now: <span class="ty">Instant</span>) {
<span class="kw">let</span> elapsed = now
.<mark class="hl" data-p="a1">saturating_duration_since<sup>1</sup></mark>(<span class="kw">self</span>.last_refill);
<span class="kw">let</span> new_tokens = <mark class="hl" data-p="a2">elapsed.as_nanos() <span class="kw">as</span> <span class="ty">u64</span>
* <span class="kw">self</span>.rate_per_sec <span class="kw">as</span> <span class="ty">u64</span>
/ <span class="nu">1_000_000_000</span><sup>2</sup></mark>;
<span class="kw">if</span> <mark class="hl" data-p="a3">new_tokens &gt; <span class="nu">0</span><sup>3</sup></mark> {
<span class="kw">self</span>.tokens = (<span class="kw">self</span>.tokens + new_tokens)
.min(<span class="kw">self</span>.burst_capacity);
<mark class="hl" data-p="a3"><span class="kw">self</span>.last_refill = now;</mark>
}
}</code></pre>
</div>
<div class="card codecard">
<div class="chead"><span class="fp">src/api/retry/tokenBucket.ts (proposed)</span><span class="lang ts">TS</span></div>
<pre><code><span class="kw">private</span> <span class="fn2">refill</span>(now: <span class="ty">number</span>): <span class="ty">void</span> {
<span class="cm">// now comes from performance.now(), not Date.now()</span>
<span class="kw">const</span> elapsedMs = <mark class="hl" data-p="a1"><span class="ty">Math</span>.max(<span class="nu">0</span>, now - <span class="kw">this</span>.lastRefill)<sup>1</sup></mark>;
<span class="kw">const</span> newTokens = <mark class="hl" data-p="a2"><span class="ty">Math</span>.floor(
(elapsedMs * <span class="kw">this</span>.ratePerSec) / <span class="nu">1000</span>
)<sup>2</sup></mark>;
<span class="kw">if</span> (<mark class="hl" data-p="a3">newTokens &gt; <span class="nu">0</span><sup>3</sup></mark>) {
<span class="kw">this</span>.tokens = <span class="ty">Math</span>.min(
<span class="kw">this</span>.tokens + newTokens,
<span class="kw">this</span>.burstCapacity
);
<mark class="hl" data-p="a3"><span class="kw">this</span>.lastRefill = now;</mark>
}
}</code></pre>
</div>
</div>
<div class="notes">
<div class="note" data-p="a1"><span class="nmark">1</span><span><b>Clock going backwards.</b> <code>saturating_duration_since</code> clamps negative elapsed time to zero. <code>Date.now()</code> can jump backwards under NTP correction; the port uses monotonic <code>performance.now()</code> <em>and</em> keeps <code>Math.max(0, &hellip;)</code> as a belt-and-suspenders match.</span></div>
<div class="note" data-p="a2"><span class="nmark">2</span><span><b>Integer vs float math.</b> Rust&rsquo;s <code>u64</code> division truncates; JS division doesn&rsquo;t. <code>Math.floor</code> restores truncation. Millis instead of nanos is safe: at <code>rate = 8</code>, <code>elapsedMs * 8</code> stays far below <code>2&#8309;&sup3;</code>, so no precision loss.</span></div>
<div class="note" data-p="a3"><span class="nmark">3</span><span><b>The load-bearing guard.</b> <code>last_refill</code> only advances when a whole token is minted. Dropping this guard (an easy &ldquo;simplification&rdquo;) silently discards sub-token progress on every call &mdash; at low rates the bucket would <em>never</em> refill under frequent polling. Preserved exactly, plus a regression test.</span></div>
</div>
</div>
<!-- pair B -->
<div class="pair">
<div class="pair-title"><span class="pt-n">PAIR B / 3</span><h3>Decorrelated jitter &mdash; inclusive range, wandering seed</h3></div>
<div class="pair-grid">
<div class="card codecard">
<div class="chead"><span class="fp">vendor/rate-limiter/src/backoff.rs:27</span><span class="lang rust">Rust</span></div>
<pre><code><span class="kw">fn</span> <span class="fn2">next_delay</span>(&amp;<span class="kw">mut</span> <span class="kw">self</span>) -&gt; <span class="ty">Duration</span> {
<span class="kw">let</span> hi = (<mark class="hl" data-p="b1"><span class="kw">self</span>.prev_delay_ms.saturating_mul(<span class="nu">3</span>)<sup>4</sup></mark>)
.min(<span class="kw">self</span>.cap_ms);
<span class="kw">let</span> lo = <span class="kw">self</span>.base_ms;
<span class="kw">let</span> ms = <span class="kw">self</span>.rng
.<mark class="hl" data-p="b2">gen_range(lo..=hi.max(lo))<sup>5</sup></mark>;
<mark class="hl" data-p="b3"><span class="kw">self</span>.prev_delay_ms = ms;<sup>6</sup></mark>
<span class="ty">Duration</span>::from_millis(ms)
}</code></pre>
</div>
<div class="card codecard">
<div class="chead"><span class="fp">src/api/retry/backoff.ts (proposed)</span><span class="lang ts">TS</span></div>
<pre><code><span class="fn2">nextDelay</span>(): <span class="ty">number</span> {
<span class="kw">const</span> hi = <span class="ty">Math</span>.min(<mark class="hl" data-p="b1"><span class="kw">this</span>.prevDelayMs * <span class="nu">3</span><sup>4</sup></mark>, <span class="kw">this</span>.capMs);
<span class="kw">const</span> lo = <span class="kw">this</span>.baseMs;
<span class="kw">const</span> span = <span class="ty">Math</span>.max(hi, lo) - lo;
<span class="kw">const</span> ms = <mark class="hl" data-p="b2">lo + <span class="ty">Math</span>.floor(
<span class="kw">this</span>.random() * (span + <span class="nu">1</span>)
)<sup>5</sup></mark>;
<mark class="hl" data-p="b3"><span class="kw">this</span>.prevDelayMs = ms;<sup>6</sup></mark>
<span class="kw">return</span> ms;
}</code></pre>
</div>
</div>
<div class="notes">
<div class="note" data-p="b1"><span class="nmark">4</span><span><b>Saturating multiply.</b> <code>saturating_mul(3)</code> guards <code>u64</code> overflow. In JS this can&rsquo;t overflow &mdash; <code>capMs = 30_000</code> bounds the product long before <code>2&#8309;&sup3;</code> &mdash; so the guard is deliberately dropped (see &sect;3, &ldquo;dropped&rdquo; column).</span></div>
<div class="note" data-p="b2"><span class="nmark">5</span><span><b>Inclusive vs exclusive range.</b> Rust&rsquo;s <code>lo..=hi</code> includes both endpoints. Naive <code>lo + random() * (hi - lo)</code> never yields <code>hi</code>. The <code>+ 1</code> inside <code>Math.floor</code> restores inclusivity &mdash; a one-character bug magnet, called out so you can veto or bless it.</span></div>
<div class="note" data-p="b3"><span class="nmark">6</span><span><b>Stateful seed.</b> Each draw becomes the next iteration&rsquo;s <code>prev</code> &mdash; this is what makes it <em>decorrelated</em> rather than plain expo-backoff-with-jitter. <code>reset()</code> restores <code>prevDelayMs = baseMs</code> on success, matching the crate&rsquo;s <code>Backoff::reset</code>. <code>this.random</code> is injectable for deterministic tests (crate uses a seeded <code>SmallRng</code> in its tests).</span></div>
</div>
</div>
<!-- pair C -->
<div class="pair">
<div class="pair-title"><span class="pt-n">PAIR C / 3</span><h3>Retry budget &mdash; a mutex the event loop doesn&rsquo;t need (with one trap)</h3></div>
<div class="pair-grid">
<div class="card codecard">
<div class="chead"><span class="fp">vendor/rate-limiter/src/budget.rs:58</span><span class="lang rust">Rust</span></div>
<pre><code><span class="kw">pub fn</span> <span class="fn2">try_withdraw</span>(&amp;<span class="kw">self</span>) -&gt; <span class="ty">bool</span> {
<span class="kw">let mut</span> b = <mark class="hl" data-p="c1"><span class="kw">self</span>.inner.lock().unwrap()<sup>7</sup></mark>;
b.deposit_drip(<span class="ty">Instant</span>::now());
<span class="kw">if</span> <mark class="hl" data-p="c2">b.balance &gt;= <span class="ty">WITHDRAW_COST</span> {
b.balance -= <span class="ty">WITHDRAW_COST</span>;<sup>8</sup></mark>
<span class="kw">true</span>
} <span class="kw">else</span> {
<mark class="hl" data-p="c3"><span class="kw">false</span> <span class="cm">// refuse retry, don&rsquo;t queue</span><sup>9</sup></mark>
}
}</code></pre>
</div>
<div class="card codecard">
<div class="chead"><span class="fp">src/api/retry/budget.ts (proposed)</span><span class="lang ts">TS</span></div>
<pre><code><span class="fn2">tryWithdraw</span>(): <span class="ty">boolean</span> {
<span class="cm">// <mark class="hl" data-p="c1">no lock: single-threaded event loop<sup>7</sup></mark></span>
<span class="cm">// but NO await between check and debit.</span>
<span class="kw">this</span>.depositDrip(<span class="kw">this</span>.clock());
<span class="kw">if</span> (<mark class="hl" data-p="c2"><span class="kw">this</span>.balance &gt;= <span class="ty">WITHDRAW_COST</span>) {
<span class="kw">this</span>.balance -= <span class="ty">WITHDRAW_COST</span>;<sup>8</sup></mark>
<span class="kw">return</span> <span class="kw">true</span>;
}
<span class="kw">return</span> <mark class="hl" data-p="c3"><span class="kw">false</span><sup>9</sup></mark>;
}</code></pre>
</div>
</div>
<div class="notes">
<div class="note" data-p="c1"><span class="nmark">7</span><span><b>Thread-safety &rarr; event loop.</b> The crate&rsquo;s <code>Mutex</code> exists because Rust callers withdraw from worker threads. Acme&rsquo;s client runs on one event loop, so the lock disappears &mdash; <em>but the atomicity it provided must be preserved by convention</em>: <code>tryWithdraw</code> is synchronous end-to-end, and I&rsquo;ll add an eslint <code>no-await-in-budget</code> boundary comment plus a test that the method never returns a Promise.</span></div>
<div class="note" data-p="c2"><span class="nmark">8</span><span><b>Check-then-debit stays fused.</b> Same shape both sides. The failure mode isn&rsquo;t data races here &mdash; it&rsquo;s a future refactor inserting an <code>await</code> (e.g., to log) between the check and the debit, letting two in-flight retries both pass the check.</span></div>
<div class="note" data-p="c3"><span class="nmark">9</span><span><b>Refuse, never queue.</b> On an empty budget the crate returns <code>false</code> immediately &mdash; the request either goes out as a first try or fails fast. The port must <em>not</em> &ldquo;helpfully&rdquo; enqueue the retry for later; that would rebuild the retry storm the budget exists to prevent.</span></div>
</div>
</div>
</section>
<!-- ── 3. preserved / changed / dropped ── -->
<section class="sec">
<div class="sec-eyebrow"><span class="num">3</span> Behaviors: preserved / deliberately changed / dropped</div>
<div class="three">
<div class="card col keep">
<div class="colhead"><span class="dot"></span>Preserved exactly</div>
<ul>
<li><b>Refill truncation + guard</b> &mdash; whole tokens only; <code>lastRefill</code> advances only on mint</li>
<li><b>Jitter formula</b> &mdash; uniform over <code>[base, min(prev&times;3, cap)]</code>, inclusive both ends</li>
<li><b>Budget economics</b> &mdash; +1 per first-try success, &minus;10 per retry, ceiling 1000</li>
<li><b>Bucket starts full</b> &mdash; 40-request cold burst is intentional (matches crate test <code>burst_at_t0</code>)</li>
<li><b>Caller classifies errors</b> &mdash; limiter never inspects failures</li>
</ul>
</div>
<div class="card col chg">
<div class="colhead"><span class="dot"></span>Deliberately changed</div>
<ul>
<li><b><code>Instant</code> &rarr; <code>performance.now()</code></b> &mdash; both monotonic; ms resolution is sufficient at rate 8/s</li>
<li><b><code>u64</code> nanos &rarr; <code>number</code> ms</b> &mdash; all products provably &lt; 2&#8309;&sup3;; <code>Math.floor</code> replays integer division</li>
<li><b><code>Mutex&lt;Budget&gt;</code> &rarr; plain fields</b> &mdash; atomicity by sync-only convention + test</li>
<li><b><code>SmallRng</code> &rarr; injected <code>random()</code></b> &mdash; defaults to <code>Math.random</code>, seeded stub in tests</li>
</ul>
</div>
<div class="card col drop">
<div class="colhead"><span class="dot"></span>Dropped (not needed)</div>
<ul>
<li><b><code>saturating_mul</code> overflow guards</b> &mdash; unreachable once cap is applied in ms range</li>
<li><b><code>Send + Sync</code> impls, <code>Arc</code> cloning</b> &mdash; no threads to share across</li>
<li><b><code>tokio</code>/<code>async-std</code> feature flags</b> &mdash; port is runtime-agnostic by construction</li>
<li><b>Prometheus counters</b> &mdash; Acme emits via <code>telemetry.track()</code> instead; hook points kept</li>
</ul>
</div>
</div>
</section>
<!-- ── 4. edge cases ── -->
<section class="sec">
<div class="sec-eyebrow"><span class="num">4</span> Edge cases: expected behavior on both sides</div>
<div class="tbl-scroll">
<table>
<thead>
<tr><th style="width:22%">Edge case</th><th style="width:31%">Rust crate</th><th style="width:31%">TypeScript port</th><th>Match</th></tr>
</thead>
<tbody>
<tr>
<td><b>Clock skew</b><br>system clock steps back 5s mid-session</td>
<td><code>Instant</code> is monotonic &mdash; unaffected. <code>saturating_duration_since</code> is a second fence.</td>
<td><code>performance.now()</code> is monotonic &mdash; unaffected. <code>Math.max(0,&hellip;)</code> kept as the same second fence.</td>
<td><span class="pill same">identical</span></td>
</tr>
<tr>
<td><b>Burst at t=0</b><br>fresh client fires 45 requests at once</td>
<td>First 40 admitted instantly (bucket starts full); 41&ndash;45 rejected until refill. Crate test: <code>burst_at_t0</code>.</td>
<td>Same: 40 admitted, 5 rejected with <code>RateLimited</code>. Port test replays the crate&rsquo;s fixture numbers verbatim.</td>
<td><span class="pill same">identical</span></td>
</tr>
<tr>
<td><b>Budget exhaustion</b><br>sustained 100% failure for 60s</td>
<td>Balance drains to 0 in ~10 retries; further retries refused, first-tries continue. Retry rate settles at deposit&divide;cost = 10% of success rate (0 here).</td>
<td>Same economics, same settle point. Difference: refusal surfaces as <code>RetryBudgetExhausted</code> error so Acme&rsquo;s upload queue can show &ldquo;reconnecting&rdquo; instead of failing silently.</td>
<td><span class="pill equiv">equivalent*</span></td>
</tr>
<tr>
<td><b>Slow drip</b><br>rate 8/s but polled every 20ms (0.16 tokens/poll)</td>
<td>Guard carries sub-token time forward; a token mints every ~125ms regardless of poll cadence.</td>
<td>Same, via the preserved <code>newTokens &gt; 0</code> guard (Pair A, note 3). Regression test asserts mint cadence at 20ms polling.</td>
<td><span class="pill same">identical</span></td>
</tr>
<tr>
<td><b>Delay at cap</b><br>10th consecutive failure</td>
<td>Draw range collapses toward <code>[250ms, 30s]</code>; delay stays &le; 30s but still jittered &mdash; never a fixed 30s (avoids re-synchronizing clients).</td>
<td>Same bounds, same non-degenerate jitter &mdash; verified with a seeded RNG replaying the crate&rsquo;s <code>cap_still_jitters</code> test vector.</td>
<td><span class="pill same">identical</span></td>
</tr>
</tbody>
</table>
</div>
<p class="art-sub" style="margin-top:10px;font-size:13px">* &ldquo;equivalent&rdquo; = same decision, different surface. The crate returns bare <code>false</code>; the port wraps it in a typed error because Acme&rsquo;s UI needs to distinguish &ldquo;rate limited&rdquo; from &ldquo;offline&rdquo;. Flag this row if you&rsquo;d rather keep the bare boolean.</p>
</section>
<!-- ── 5. confirm ── -->
<section class="sec">
<div class="sec-eyebrow"><span class="num">5</span> Sign-off</div>
<div class="card confirm">
<div class="ctext">
<h3>Ready to port when you are</h3>
<p>Reply <b>&ldquo;semantics confirmed&rdquo;</b> and I&rsquo;ll implement <code>tokenBucket.ts</code>, <code>backoff.ts</code>, and <code>budget.ts</code> with the crate&rsquo;s 14 tests translated first. Or correct any row above &mdash; quote its number (e.g. &ldquo;note 5&rdquo;, &ldquo;budget exhaustion row&rdquo;) and I&rsquo;ll revise the map before writing code.</p>
</div>
<div class="cbtns">
<button class="btn primary" id="confirmBtn">copy &ldquo;semantics confirmed&rdquo;</button>
<button class="btn ghost" id="correctBtn">copy correction template</button>
</div>
</div>
</section>
</div>
<footer>Part of <a href="index.html">Know your unknowns</a> &mdash; companion examples to the blog post.</footer>
</div>
<script>
(function () {
function copy(text, btn, doneLabel) {
var original = btn.textContent;
function done() {
btn.textContent = doneLabel;
btn.classList.add('done');
setTimeout(function () {
btn.textContent = original;
btn.classList.remove('done');
}, 1800);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallback(); });
} else { fallback(); }
function fallback() {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed'; ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); done(); } catch (e) {}
document.body.removeChild(ta);
}
}
document.getElementById('copyPrompt').addEventListener('click', function () {
copy(document.getElementById('promptText').textContent.trim(), this, 'Copied');
});
document.getElementById('confirmBtn').addEventListener('click', function () {
copy('semantics confirmed', this, 'Copied — paste it back to Claude');
});
document.getElementById('correctBtn').addEventListener('click', function () {
copy('Correction to the semantics map:\n- section/row: \n- what you got wrong: \n- correct behavior: ', this, 'Copied');
});
// link highlighted regions across columns + their margin notes
function setActive(p, on) {
var els = document.querySelectorAll('[data-p="' + p + '"]');
for (var i = 0; i < els.length; i++) {
els[i].classList.toggle('active', on);
}
}
var linked = document.querySelectorAll('mark.hl, .note');
for (var i = 0; i < linked.length; i++) {
(function (el) {
var p = el.getAttribute('data-p');
if (!p) return;
el.addEventListener('mouseenter', function () { setActive(p, true); });
el.addEventListener('mouseleave', function () { setActive(p, false); });
})(linked[i]);
}
})();
</script>
</body>
</html>
+765
View File
@@ -0,0 +1,765 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The tweakable plan — Know your unknowns</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px 100px; }
@media (max-width: 480px) { .wrap { padding: 0 16px 80px; } }
/* ── page header ─────────────────────── */
header.page-head { padding: 56px 0 40px; position: relative; }
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 12px;
}
.eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex-shrink: 0; }
a.back {
position: absolute; top: 60px; right: 0;
font-family: var(--mono); font-size: 12px;
color: var(--g500); text-decoration: none;
}
a.back:hover { color: var(--clay); }
@media (max-width: 640px) { a.back { position: static; display: inline-block; margin-bottom: 16px; } }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(32px, 5vw, 46px);
line-height: 1.08;
letter-spacing: -0.018em;
margin: 0 0 14px;
}
.lede { font-size: 16.5px; color: var(--g700); max-width: 640px; margin: 0 0 32px; }
/* prompt box */
.prompt-card {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 22px 26px 24px;
position: relative;
}
.prompt-card .p-label {
font-family: var(--mono); font-size: 12px;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--clay); margin-bottom: 12px;
display: flex; align-items: center; gap: 12px;
}
.prompt-card .p-label::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); }
.prompt-card blockquote {
margin: 0;
font-family: var(--serif);
font-style: italic;
font-size: 17.5px;
line-height: 1.5;
color: var(--slate);
max-width: 62ch;
}
button.copy-btn {
position: absolute; top: 18px; right: 20px;
font-family: var(--mono); font-size: 11.5px;
background: var(--g100); color: var(--g700);
border: 1.5px solid var(--g300); border-radius: 8px;
padding: 5px 12px; cursor: pointer;
transition: background .15s, color .15s, border-color .15s;
}
button.copy-btn:hover { border-color: var(--clay); color: var(--clay-d); }
button.copy-btn.copied { background: var(--olive); border-color: var(--olive); color: #fff; }
@media (max-width: 640px) {
button.copy-btn { position: static; margin-top: 14px; }
}
/* divider row */
.produced {
display: flex; align-items: center; gap: 16px;
margin: 48px 0 36px;
font-family: var(--mono); font-size: 12px;
letter-spacing: 0.12em; text-transform: uppercase;
color: var(--g500); white-space: nowrap;
}
.produced::before, .produced::after { content: ""; height: 1px; background: var(--g300); flex: 1; }
/* ══ THE ARTIFACT ══════════════════════ */
.artifact {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 34px 38px 40px;
}
@media (max-width: 640px) { .artifact { padding: 22px 18px 28px; } }
.artifact h2.plan-title {
font-family: var(--serif); font-weight: 500;
font-size: clamp(24px, 3.6vw, 32px);
letter-spacing: -0.015em;
margin: 0 0 4px;
}
.plan-sub { font-size: 14px; color: var(--g500); margin: 0 0 20px; }
.plan-sub code { font-family: var(--mono); font-size: 12.5px; background: var(--g100); border-radius: 5px; padding: 1px 6px; }
/* chips */
.chips { display: flex; flex-wrap: wrap; gap: 10px; margin: 0 0 22px; }
.chip {
display: flex; align-items: baseline; gap: 8px;
font-size: 13px; color: var(--g700);
background: var(--g100); border: 1.5px solid var(--g200);
border-radius: 999px; padding: 6px 16px;
}
.chip .k { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.08em; text-transform: uppercase; color: var(--g500); }
.chip .v { font-weight: 600; color: var(--slate); }
.chip .v.risk { color: var(--clay-d); }
/* sort banner */
.sort-banner {
background: var(--oat);
border: 1.5px solid var(--g300);
border-radius: 12px;
padding: 16px 20px;
margin: 0 0 40px;
font-size: 14.5px;
color: var(--g700);
display: flex; gap: 14px; align-items: flex-start;
}
.sort-banner .glyph {
font-family: var(--mono); font-size: 15px; line-height: 1.35;
color: var(--clay-d); white-space: pre; flex-shrink: 0;
padding-top: 1px;
}
.sort-banner strong { color: var(--slate); }
/* section chrome inside artifact */
.plan-sec { margin: 0 0 46px; }
.plan-sec:last-of-type { margin-bottom: 0; }
.sec-head { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; flex-wrap: wrap; }
.sec-letter {
font-family: var(--mono); font-size: 13px; font-weight: 600;
background: var(--slate); color: var(--ivory);
width: 26px; height: 26px; border-radius: 8px;
display: inline-flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.sec-letter.soft { background: var(--g300); color: var(--g700); }
.sec-head h3 {
font-family: var(--serif); font-weight: 500;
font-size: 22px; letter-spacing: -0.01em; margin: 0;
}
.sec-head .likelihood {
font-family: var(--mono); font-size: 11px; letter-spacing: 0.08em;
text-transform: uppercase; border-radius: 6px; padding: 3px 9px;
}
.likelihood.hi { background: var(--clay); color: #fff; }
.likelihood.lo { background: var(--g100); color: var(--g500); border: 1px solid var(--g200); }
.sec-intro { font-size: 14px; color: var(--g500); margin: 0 0 22px; max-width: 66ch; }
h4.sub {
font-family: var(--mono); font-size: 12px;
letter-spacing: 0.1em; text-transform: uppercase;
color: var(--g500); margin: 34px 0 14px;
display: flex; align-items: center; gap: 10px;
}
h4.sub::before { content: ""; width: 16px; height: 1.5px; background: var(--clay); }
h4.sub:first-of-type { margin-top: 0; }
/* ── schema diagram ── */
.schema-wrap { display: grid; grid-template-columns: minmax(0, 340px) 1fr; gap: 22px; align-items: start; }
@media (max-width: 720px) { .schema-wrap { grid-template-columns: 1fr; } }
.table-box {
border: 1.5px solid var(--slate);
border-radius: 10px;
overflow: hidden;
font-family: var(--mono);
font-size: 12.5px;
background: var(--paper);
}
.table-box .t-name {
background: var(--slate); color: var(--ivory);
padding: 8px 14px; font-size: 12.5px; letter-spacing: 0.03em;
}
.table-box .t-name .new-badge {
float: right; background: var(--olive); color: #fff;
font-size: 9.5px; letter-spacing: 0.1em; border-radius: 4px;
padding: 2px 6px; margin-top: 1px;
}
.col-row {
display: flex; justify-content: space-between; gap: 12px;
padding: 6px 14px;
border-top: 1px solid var(--g200);
color: var(--g700);
}
.col-row .cn { color: var(--slate); }
.col-row .ct { color: var(--g500); white-space: nowrap; }
.col-row.flag {
background: #FBF1EC;
border-left: 3px solid var(--clay);
padding-left: 11px;
}
.col-row.flag .cn { color: var(--clay-d); font-weight: 600; }
.col-row.alt-note {
background: var(--g100);
color: var(--g500);
font-style: italic;
border-left: 3px solid var(--g300);
padding-left: 11px;
display: block;
}
.fk { color: var(--g500); font-size: 11px; }
/* variant switching */
[data-variant="main"] .v-alt { display: none; }
[data-variant="alt"] .v-main { display: none; }
.choice-card {
border: 1.5px solid var(--g300);
border-radius: 12px;
padding: 16px 18px 18px;
margin-bottom: 14px;
background: var(--ivory);
}
.choice-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.choice-flag {
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em;
background: var(--clay); color: #fff;
border-radius: 6px; padding: 3px 8px;
}
.choice-head .ch-title { font-weight: 600; font-size: 14.5px; }
button.alt-toggle {
margin-left: auto;
font-family: var(--mono); font-size: 11.5px;
background: var(--paper); color: var(--clay-d);
border: 1.5px solid var(--clay); border-radius: 8px;
padding: 5px 12px; cursor: pointer;
transition: background .15s;
}
button.alt-toggle:hover { background: #FBF1EC; }
@media (max-width: 560px) { button.alt-toggle { margin-left: 0; } }
.choice-body { font-size: 13.5px; color: var(--g700); }
.choice-body .pick-label {
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.08em;
text-transform: uppercase; color: var(--olive); margin-bottom: 3px;
}
.choice-body .pick-label.alt-l { color: var(--clay-d); }
.choice-body ul { margin: 8px 0 0; padding-left: 18px; }
.choice-body li { margin-bottom: 4px; }
.choice-body li::marker { color: var(--g500); }
.tradeoff { color: var(--g500); font-size: 12.5px; margin-top: 8px; }
.tradeoff strong { color: var(--clay-d); font-weight: 600; }
/* ── code blocks with margin notes ── */
.code-grid { display: grid; grid-template-columns: minmax(0, 1fr) 230px; gap: 18px; align-items: start; margin-bottom: 22px; }
@media (max-width: 720px) { .code-grid { grid-template-columns: 1fr; } }
.code-block {
background: var(--slate);
border-radius: 10px;
padding: 16px 0;
overflow-x: auto;
font-family: var(--mono);
font-size: 12.5px;
line-height: 1.6;
color: var(--g200);
}
.code-block .cl-line { padding: 0 18px; white-space: pre; }
.code-block .cl-line.flagged {
background: rgba(217,119,87,0.16);
border-left: 3px solid var(--clay);
padding-left: 15px;
}
.code-block .cm { color: #8a8a82; }
.code-block .kw { color: #E8B4A0; }
.code-block .ty { color: #C7CBA3; }
.code-block .st { color: #D9C7A7; }
.code-block .fnum {
display: inline-block; font-size: 10px; font-weight: 700;
background: var(--clay); color: #fff; border-radius: 50%;
width: 15px; height: 15px; line-height: 15px; text-align: center;
margin-left: 10px; vertical-align: 1px;
}
.margin-notes { display: flex; flex-direction: column; gap: 12px; }
.m-note {
font-size: 12.5px; color: var(--g700);
background: var(--g100); border: 1px solid var(--g200);
border-radius: 10px; padding: 10px 12px;
}
.m-note .n-num {
display: inline-block; font-family: var(--mono); font-size: 10px; font-weight: 700;
background: var(--clay); color: #fff; border-radius: 50%;
width: 15px; height: 15px; line-height: 15px; text-align: center;
margin-right: 7px; vertical-align: 1px;
}
.m-note code { font-family: var(--mono); font-size: 11.5px; background: var(--paper); border: 1px solid var(--g200); border-radius: 4px; padding: 0 4px; }
/* ── UX flow strip ── */
.flow-strip { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0; margin-bottom: 8px; }
@media (max-width: 720px) { .flow-strip { grid-template-columns: 1fr 1fr; gap: 18px 0; } }
@media (max-width: 420px) { .flow-strip { grid-template-columns: 1fr; } }
.flow-step { position: relative; padding: 0 18px 0 0; }
.flow-step + .flow-step { padding-left: 18px; }
.flow-step:not(:last-child)::after {
content: "→";
position: absolute; right: -1px; top: 34px;
color: var(--g500); font-size: 15px;
transform: translateX(50%);
}
@media (max-width: 720px) {
.flow-step:nth-child(2)::after { display: none; }
.flow-step { padding: 0 14px 0 0; }
}
@media (max-width: 420px) { .flow-step::after { display: none !important; } .flow-step { padding: 0; } }
.fs-num {
font-family: var(--mono); font-size: 11px; color: var(--g500);
margin-bottom: 6px;
}
.fs-card {
border: 1.5px solid var(--g300); border-radius: 10px;
background: var(--ivory); padding: 12px 13px;
min-height: 108px;
}
.fs-card.weak { border: 1.5px dashed var(--clay); background: #FBF1EC; }
.fs-card .fs-t { font-weight: 600; font-size: 13.5px; margin-bottom: 4px; }
.fs-card .fs-d { font-size: 12px; color: var(--g500); line-height: 1.45; }
.weak-tag {
display: inline-block; font-family: var(--mono); font-size: 10px;
letter-spacing: 0.08em; text-transform: uppercase;
background: var(--clay); color: #fff; border-radius: 5px;
padding: 2px 7px; margin-bottom: 6px;
}
.flow-note { font-size: 13px; color: var(--g700); background: var(--g100); border-left: 3px solid var(--clay); border-radius: 0 8px 8px 0; padding: 10px 14px; margin-top: 16px; max-width: 64ch; }
/* ── sequencing timeline ── */
.seq { list-style: none; margin: 0; padding: 0; }
.seq li {
display: grid; grid-template-columns: 30px 1fr auto; gap: 12px;
align-items: baseline;
padding: 9px 0;
border-bottom: 1px solid var(--g200);
font-size: 14px;
}
.seq li:last-child { border-bottom: none; }
.seq .s-num {
font-family: var(--mono); font-size: 12px; color: var(--paper);
background: var(--g500); border-radius: 6px;
width: 22px; height: 22px; display: inline-flex;
align-items: center; justify-content: center;
position: relative; top: 3px;
}
.seq .s-body strong { font-weight: 600; }
.seq .s-body .dep { color: var(--g500); font-size: 12.5px; }
.seq .s-est { font-family: var(--mono); font-size: 12px; color: var(--g500); white-space: nowrap; }
@media (max-width: 480px) { .seq li { grid-template-columns: 30px 1fr; } .seq .s-est { grid-column: 2; } }
/* ── mechanical details ── */
details.mech {
border: 1.5px solid var(--g300);
border-radius: 12px;
background: var(--g100);
}
details.mech summary {
cursor: pointer;
padding: 14px 18px;
font-size: 14px; font-weight: 600;
list-style: none;
display: flex; align-items: center; gap: 10px;
}
details.mech summary::-webkit-details-marker { display: none; }
details.mech summary::before {
content: "▸"; color: var(--g500); font-size: 13px;
transition: transform .15s; display: inline-block;
}
details.mech[open] summary::before { transform: rotate(90deg); }
details.mech summary .count {
font-family: var(--mono); font-size: 11px; color: var(--g500);
font-weight: 400; margin-left: auto;
}
.mech-list { list-style: none; margin: 0; padding: 4px 18px 16px 40px; }
.mech-list li {
font-size: 13.5px; color: var(--g700);
padding: 6px 0; border-top: 1px solid var(--g200);
}
.mech-list li:first-child { border-top: none; }
.mech-list code { font-family: var(--mono); font-size: 12px; background: var(--paper); border: 1px solid var(--g200); border-radius: 4px; padding: 0 5px; }
.mech-list .why { color: var(--g500); font-size: 12px; }
/* ── quick-reply card ── */
.tweak-card {
margin-top: 44px;
border: 1.5px solid var(--slate);
border-radius: 14px;
background: var(--ivory);
padding: 22px 24px 24px;
}
.tweak-card h3 {
font-family: var(--serif); font-weight: 500; font-size: 21px;
letter-spacing: -0.01em; margin: 0 0 4px;
}
.tweak-card > p { font-size: 13.5px; color: var(--g500); margin: 0 0 16px; }
.tweak-row {
display: flex; gap: 12px; align-items: flex-start;
background: var(--paper); border: 1.5px solid var(--g300);
border-radius: 10px; padding: 12px 14px; margin-bottom: 10px;
}
.tweak-row:last-child { margin-bottom: 0; }
.tweak-row .t-num {
font-family: var(--mono); font-size: 11px; font-weight: 700;
background: var(--oat); color: var(--g700); border-radius: 6px;
padding: 2px 8px; flex-shrink: 0; margin-top: 2px;
}
.tweak-row .t-body { font-size: 13.5px; color: var(--g700); }
.tweak-row .t-body .t-q { font-family: var(--serif); font-style: italic; color: var(--slate); }
.tweak-row button.copy-btn { position: static; flex-shrink: 0; margin: 0 0 0 auto; }
@media (max-width: 560px) { .tweak-row { flex-wrap: wrap; } .tweak-row button.copy-btn { margin-left: 40px; } }
/* footer */
footer.site-foot {
margin-top: 56px; padding-top: 24px;
border-top: 1.5px solid var(--g300);
font-size: 13.5px; color: var(--g500);
}
footer.site-foot a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
footer.site-foot a:hover { text-decoration-color: var(--clay); }
</style>
</head>
<body>
<div class="wrap">
<header class="page-head">
<a class="back" href="index.html">← All examples</a>
<div class="eyebrow">Know your unknowns · Pre-implementation</div>
<h1>The tweakable plan</h1>
<p class="lede">Instead of a plan sorted by execution order, ask for one sorted by how likely <em>you</em> are to change each part — so the decisions worth your attention surface first and the mechanical work sinks to the bottom.</p>
<div class="prompt-card">
<div class="p-label">The prompt</div>
<blockquote id="the-prompt">Write an implementation plan for annotation export as HTML, but lead with the decisions I'm most likely to tweak: data model changes, new type interfaces, and anything user-facing. Bury the mechanical refactoring at the bottom — I trust you on that part.</blockquote>
<button class="copy-btn" onclick="copyText(this, document.getElementById('the-prompt').textContent)">Copy prompt</button>
</div>
</header>
<div class="produced">What Claude produced</div>
<!-- ═══════════ THE ARTIFACT ═══════════ -->
<div class="artifact">
<h2 class="plan-title">Implementation plan: Annotation export</h2>
<p class="plan-sub">Acme · export a review's annotations as a shareable PDF or CSV · branch <code>feat/annotation-export</code></p>
<div class="chips">
<div class="chip"><span class="k">Effort</span><span class="v">~2.5 dev-days</span></div>
<div class="chip"><span class="k">Files touched</span><span class="v">13 (3 new)</span></div>
<div class="chip"><span class="k">Risk</span><span class="v risk">Lowmedium</span></div>
<div class="chip"><span class="k">Migrations</span><span class="v">1, additive</span></div>
</div>
<div class="sort-banner">
<div class="glyph">↑ tweak
↓ trust</div>
<div><strong>This plan is sorted by likelihood-of-tweaking, not execution order.</strong> Read top to bottom = most worth your attention first. Section A holds every judgment call; Section B is the build order; Section C is mechanical and safe to skip entirely.</div>
</div>
<!-- ── SECTION A ─────────────────────── -->
<section class="plan-sec">
<div class="sec-head">
<span class="sec-letter">A</span>
<h3>Decisions you'll probably want to change</h3>
<span class="likelihood hi">High tweak likelihood</span>
</div>
<p class="sec-intro">Three areas where I made a judgment call you might disagree with. Each flagged choice shows the alternative I considered — toggle to compare.</p>
<h4 class="sub">A1 · Data model — new <span style="text-transform:none">annotation_exports</span> table</h4>
<div class="schema-wrap">
<div class="table-box" id="schema" data-variant="main">
<div class="t-name">annotation_exports <span class="new-badge">NEW</span></div>
<div class="col-row"><span class="cn">id</span><span class="ct">uuid pk</span></div>
<div class="col-row"><span class="cn">review_id</span><span class="ct">uuid <span class="fk">→ reviews</span></span></div>
<div class="col-row"><span class="cn">requested_by</span><span class="ct">uuid <span class="fk">→ users</span></span></div>
<div class="col-row"><span class="cn">status</span><span class="ct">enum(queued, rendering, ready, failed)</span></div>
<div class="col-row"><span class="cn">options</span><span class="ct">jsonb</span></div>
<div class="col-row flag v-main"><span class="cn">snapshot ①</span><span class="ct">jsonb</span></div>
<div class="col-row alt-note v-alt">① no snapshot column — renderer JOINs <b>annotations</b> live at render time</div>
<div class="col-row flag v-main2"><span class="cn">artifact_url ②</span><span class="ct">text null</span></div>
<div class="col-row flag v-main2"><span class="cn">rendered_at</span><span class="ct">timestamptz null</span></div>
<div class="col-row alt-note v-alt2">② no stored artifact — <b>GET /exports/:id/download</b> renders on demand</div>
<div class="col-row"><span class="cn">created_at</span><span class="ct">timestamptz</span></div>
</div>
<div>
<div class="choice-card" data-variant="main" id="c1">
<div class="choice-head">
<span class="choice-flag">Choice ①</span>
<span class="ch-title">Snapshot vs. live join</span>
<button class="alt-toggle" onclick="toggleChoice(1)">View alternative →</button>
</div>
<div class="choice-body">
<div class="v-main">
<div class="pick-label">Plan's pick — denormalized snapshot</div>
Copy the annotation payloads into <b>snapshot jsonb</b> when the export is requested. The export is a record of the review <i>at that moment</i>: later edits, resolutions, and deletions don't rewrite history.
<ul>
<li>Export stays valid even if the review is archived.</li>
<li>Renderer reads one row — no N+1 across <b>annotations</b> + <b>annotation_replies</b>.</li>
</ul>
<p class="tradeoff"><strong>Cost:</strong> ~40&nbsp;KB/row for a heavy review; snapshots can go stale relative to the live thread.</p>
</div>
<div class="v-alt">
<div class="pick-label alt-l">Alternative — live join at render time</div>
Skip the snapshot; the renderer joins <b>annotations</b> when the file is generated. Exports always reflect current state.
<ul>
<li>No duplicate data, no staleness question.</li>
<li>Re-downloading the same export can produce a <i>different</i> file — surprising for audit use.</li>
</ul>
<p class="tradeoff"><strong>Pick this if:</strong> exports are working documents, not records. One line to tell me: <i>“use live join.”</i></p>
</div>
</div>
</div>
<div class="choice-card" data-variant="main" id="c2">
<div class="choice-head">
<span class="choice-flag">Choice ②</span>
<span class="ch-title">Stored artifact vs. render-on-demand</span>
<button class="alt-toggle" onclick="toggleChoice(2)">View alternative →</button>
</div>
<div class="choice-body">
<div class="v-main">
<div class="pick-label">Plan's pick — render once, store in blob storage</div>
The worker renders the PDF/CSV once and writes <b>artifact_url</b>. Downloads are a signed-URL redirect — cheap, cacheable, and shareable with reviewers who lack Acme accounts.
<p class="tradeoff"><strong>Cost:</strong> blob lifecycle to manage; I'd add a 30-day TTL sweep (Section C, item 6).</p>
</div>
<div class="v-alt">
<div class="pick-label alt-l">Alternative — render on every download</div>
No stored file: <b>GET /exports/:id/download</b> streams a fresh render. Zero storage, but ~24&nbsp;s per download on large reviews, and no shareable public link.
<p class="tradeoff"><strong>Pick this if:</strong> storage/compliance rules make stored copies awkward.</p>
</div>
</div>
</div>
</div>
</div>
<h4 class="sub">A2 · New type interfaces</h4>
<div class="code-grid">
<div class="code-block"><!--
--><div class="cl-line"><span class="cm">// src/types/exports.ts (new)</span></div><!--
--><div class="cl-line"><span class="kw">export interface</span> <span class="ty">ExportRequest</span> {</div><!--
--><div class="cl-line"> reviewId: <span class="ty">ReviewId</span>;</div><!--
--><div class="cl-line flagged"> format: <span class="st">'pdf'</span> | <span class="st">'csv'</span>;<span class="fnum">1</span></div><!--
--><div class="cl-line flagged"> range: <span class="st">'all'</span> | <span class="st">'unresolved'</span> | <span class="ty">TimecodeRange</span>;<span class="fnum">2</span></div><!--
--><div class="cl-line"> includeReplies: <span class="kw">boolean</span>; <span class="cm">// default true</span></div><!--
--><div class="cl-line flagged"> includeDrawings: <span class="kw">boolean</span>; <span class="cm">// default false</span><span class="fnum">3</span></div><!--
--><div class="cl-line">}</div><!--
--><div class="cl-line">&nbsp;</div><!--
--><div class="cl-line"><span class="kw">export interface</span> <span class="ty">AnnotationSnapshot</span> {</div><!--
--><div class="cl-line"> id: <span class="ty">AnnotationId</span>;</div><!--
--><div class="cl-line"> timecode: <span class="kw">string</span>; <span class="cm">// "00:04:12.08"</span></div><!--
--><div class="cl-line"> author: { id: <span class="ty">UserId</span>; name: <span class="kw">string</span> };</div><!--
--><div class="cl-line"> body: <span class="kw">string</span>;</div><!--
--><div class="cl-line flagged"> state: <span class="st">'open'</span> | <span class="st">'resolved'</span>;<span class="fnum">4</span></div><!--
--><div class="cl-line"> replies: <span class="ty">ReplySnapshot</span>[];</div><!--
--><div class="cl-line">}</div><!--
--></div>
<div class="margin-notes">
<div class="m-note"><span class="n-num">1</span>Started with PDF + CSV. Adding <code>'xlsx'</code> or <code>'srt'</code> later is one union member + one renderer.</div>
<div class="m-note"><span class="n-num">2</span><code>TimecodeRange</code> lets producers export just the notes for one scene. If nobody asked for this, cutting it removes ~½ day.</div>
<div class="m-note"><span class="n-num">3</span>Drawings (frame markups) rasterize into the PDF. Off by default because it triples render time.</div>
<div class="m-note"><span class="n-num">4</span>Deliberately drops the internal 5-state machine to 2 states in the export. Tell me if reviewers need to see <code>'wont_fix'</code>.</div>
</div>
</div>
<h4 class="sub">A3 · UX flow</h4>
<div class="flow-strip">
<div class="flow-step">
<div class="fs-num">Step 1</div>
<div class="fs-card">
<div class="fs-t">Export button</div>
<div class="fs-d">New item in the review toolbar overflow menu, next to “Copy share link”.</div>
</div>
</div>
<div class="flow-step">
<div class="fs-num">Step 2</div>
<div class="fs-card">
<div class="fs-t">Options modal</div>
<div class="fs-d">Format, range, include-replies, include-drawings. Reuses <b>ModalForm</b> from settings.</div>
</div>
</div>
<div class="flow-step">
<div class="fs-num">Step 3</div>
<div class="fs-card weak">
<span class="weak-tag">Weakest part of this plan</span>
<div class="fs-t">Background job + toast</div>
<div class="fs-d">Export renders in a worker; user gets a “we'll notify you” toast and can navigate away.</div>
</div>
</div>
<div class="flow-step">
<div class="fs-num">Step 4</div>
<div class="fs-card">
<div class="fs-t">Ready notification</div>
<div class="fs-d">Bell notification + download link; also listed under review → Exports tab.</div>
</div>
</div>
</div>
<p class="flow-note"><b>Why step 3 is weak:</b> most exports will finish in under 3 seconds, so a fire-and-forget toast may feel like overkill — but a 400-annotation review with drawings takes ~20&nbsp;s, which is too long to block. A hybrid (wait up to 4&nbsp;s, then fall back to notify) is nicer and adds ~half a day. Your call.</p>
</section>
<!-- ── SECTION B ─────────────────────── -->
<section class="plan-sec">
<div class="sec-head">
<span class="sec-letter">B</span>
<h3>Sequencing</h3>
<span class="likelihood lo">Execution order</span>
</div>
<p class="sec-intro">The order I'd actually build it in. Every step lands green on CI; nothing user-visible ships until step 5 flips the flag.</p>
<ol class="seq">
<li><span class="s-num">1</span><span class="s-body"><strong>Migration + model.</strong> <span class="dep">Create <b>annotation_exports</b>, wire the Sequelize model, factory, fixtures.</span></span><span class="s-est">0.5d</span></li>
<li><span class="s-num">2</span><span class="s-body"><strong>Types + request validation.</strong> <span class="dep">Everything in A2, plus zod schemas on <b>POST /api/reviews/:id/exports</b>.</span></span><span class="s-est">0.25d</span></li>
<li><span class="s-num">3</span><span class="s-body"><strong>Renderers.</strong> <span class="dep">CSV first (trivial), then PDF via the existing <b>@acme/render</b> service.</span></span><span class="s-est">1d</span></li>
<li><span class="s-num">4</span><span class="s-body"><strong>Worker job + blob upload.</strong> <span class="dep">Queue on <b>exports.render</b>; retries ×3 then <b>status = failed</b>.</span></span><span class="s-est">0.25d</span></li>
<li><span class="s-num">5</span><span class="s-body"><strong>UI: menu item, modal, toast, Exports tab.</strong> <span class="dep">Behind flag <b>export_annotations</b>.</span></span><span class="s-est">0.5d</span></li>
<li><span class="s-num">6</span><span class="s-body"><strong>Tests, docs, flag rollout to the Acme team.</strong></span><span class="s-est">0.25d</span></li>
</ol>
</section>
<!-- ── SECTION C ─────────────────────── -->
<section class="plan-sec">
<div class="sec-head">
<span class="sec-letter soft">C</span>
<h3>Mechanical work (trust me)</h3>
<span class="likelihood lo">Low tweak likelihood</span>
</div>
<p class="sec-intro">Refactors and plumbing the feature needs. No judgment calls here — collapsed on purpose.</p>
<details class="mech">
<summary>Boring but necessary <span class="count">8 tasks · ~0.5d total, folded into the estimates above</span></summary>
<ul class="mech-list">
<li>Extract <code>AnnotationSerializer</code> from <code>api/annotations.ts</code> into <code>lib/serializers/</code> so the export worker can reuse it. <span class="why">Pure move, no behavior change.</span></li>
<li>Finish the <code>CommentMarker</code><code>AnnotationMarker</code> rename — 3 files still import the deprecated alias.</li>
<li>Add <code>annotation_exports</code> to the fixture loader and the CI database-reset script.</li>
<li>Register <code>exports.render</code> in <code>workers/index.ts</code> and add it to the dead-letter alert list.</li>
<li>Create feature flag <code>export_annotations</code> in <code>flags.yaml</code>, default off.</li>
<li>Add a nightly TTL sweep for expired export blobs to <code>jobs/cleanup.ts</code> (only if Choice ② stays as-is).</li>
<li>Extend <code>openapi.yaml</code> with the two new endpoints and regenerate the client.</li>
<li>Move the timecode-formatting helper from <code>player/utils.ts</code> to <code>lib/time.ts</code> — the PDF renderer needs it and shouldn't import from the player bundle.</li>
</ul>
</details>
</section>
<!-- ── quick replies ─────────────────── -->
<div class="tweak-card">
<h3>Tweak these three things</h3>
<p>The highest-leverage replies you could send. Copy one, edit, send — I'll revise the plan.</p>
<div class="tweak-row">
<span class="t-num"></span>
<div class="t-body"><span class="t-q">“Switch Choice ① to live join — exports here are working docs, not records.”</span></div>
<button class="copy-btn" onclick="copyText(this, 'Switch Choice ① to live join — exports here are working docs, not records.')">Copy</button>
</div>
<div class="tweak-row">
<span class="t-num"></span>
<div class="t-body"><span class="t-q">“Cut TimecodeRange from ExportRequest; nobody has asked for per-scene export.”</span></div>
<button class="copy-btn" onclick="copyText(this, 'Cut TimecodeRange from ExportRequest; nobody has asked for per-scene export.')">Copy</button>
</div>
<div class="tweak-row">
<span class="t-num"></span>
<div class="t-body"><span class="t-q">“Do the hybrid for step 3: wait up to 4s inline, then fall back to notify.”</span></div>
<button class="copy-btn" onclick="copyText(this, 'Do the hybrid for step 3: wait up to 4s inline, then fall back to notify.')">Copy</button>
</div>
</div>
</div><!-- /artifact -->
<footer class="site-foot">
Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.
</footer>
</div>
<script>
function copyText(btn, text) {
var done = function () {
var orig = btn.dataset.orig || btn.textContent;
btn.dataset.orig = orig;
btn.textContent = 'Copied';
btn.classList.add('copied');
setTimeout(function () {
btn.textContent = orig;
btn.classList.remove('copied');
}, 1600);
};
var fallback = function () {
var ta = document.createElement('textarea');
ta.value = text;
ta.setAttribute('readonly', '');
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); done(); } catch (e) {}
document.body.removeChild(ta);
};
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, fallback);
} else {
fallback();
}
}
// Choice toggles: swap both the choice card body and the matching schema rows.
function toggleChoice(n) {
var card = document.getElementById('c' + n);
var toAlt = card.getAttribute('data-variant') === 'main';
var next = toAlt ? 'alt' : 'main';
card.setAttribute('data-variant', next);
card.querySelector('.alt-toggle').textContent = toAlt ? '← Back to plans pick' : 'View alternative →';
// schema rows for choice n use classes v-main/v-alt (n=1) or v-main2/v-alt2 (n=2)
var suffix = n === 1 ? '' : '2';
var schema = document.getElementById('schema');
schema.querySelectorAll('.v-main' + suffix).forEach(function (el) {
el.style.display = toAlt ? 'none' : '';
});
schema.querySelectorAll('.v-alt' + suffix).forEach(function (el) {
el.style.display = toAlt ? 'block' : 'none';
});
}
// Initialize schema alt rows hidden (choice 2 rows aren't covered by the
// [data-variant] CSS on #schema since both choices share one diagram).
document.querySelectorAll('#schema .v-alt, #schema .v-alt2').forEach(function (el) {
el.style.display = 'none';
});
document.querySelectorAll('#schema .v-main2').forEach(function (el) {
el.style.display = '';
});
</script>
</body>
</html>
+609
View File
@@ -0,0 +1,609 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Implementation notes — Know your unknowns</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 0 24px 100px; }
/* ── header ── */
header.page { padding: 56px 0 8px; position: relative; }
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 18px;
}
.eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
a.back {
position: absolute;
top: 60px; right: 0;
font-family: var(--mono);
font-size: 12px;
color: var(--g500);
text-decoration: none;
}
a.back:hover { color: var(--clay-d); }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(32px, 5vw, 46px);
line-height: 1.08;
letter-spacing: -0.015em;
margin: 0 0 10px;
}
.lede { font-size: 16.5px; color: var(--g700); max-width: 640px; margin: 0 0 30px; }
/* ── prompt box ── */
.promptcard {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 22px 26px 24px;
position: relative;
}
.promptcard .label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--clay);
margin-bottom: 12px;
}
.promptcard blockquote {
margin: 0;
font-family: var(--serif);
font-style: italic;
font-size: 17.5px;
line-height: 1.6;
color: var(--slate);
max-width: 62ch;
}
button.copybtn {
position: absolute;
top: 18px; right: 18px;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.06em;
color: var(--g700);
background: var(--g100);
border: 1.5px solid var(--g300);
border-radius: 8px;
padding: 6px 12px;
cursor: pointer;
}
button.copybtn:hover { border-color: var(--g500); }
button.copybtn.done { color: var(--olive); border-color: var(--olive); background: var(--paper); }
/* ── divider ── */
.divider {
display: flex;
align-items: center;
gap: 16px;
margin: 48px 0 30px;
font-family: var(--mono);
font-size: 11.5px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--g500);
white-space: nowrap;
}
.divider::before, .divider::after { content: ""; height: 1.5px; background: var(--g300); flex: 1; }
/* ── artifact: notes document ── */
.doc {
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
overflow: hidden;
}
.doc-head {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px 14px;
padding: 14px 22px;
border-bottom: 1.5px solid var(--g200);
background: var(--g100);
font-family: var(--mono);
font-size: 12px;
color: var(--g700);
}
.doc-head .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--olive); flex: none; }
.doc-head .path { overflow-wrap: anywhere; }
.doc-head .meta { margin-left: auto; color: var(--g500); }
.doc-summary {
display: flex;
flex-wrap: wrap;
gap: 12px;
padding: 22px 22px 0;
}
.stat {
flex: 1 1 130px;
background: var(--ivory);
border: 1.5px solid var(--g200);
border-radius: 10px;
padding: 12px 16px 10px;
}
.stat .n { font-family: var(--serif); font-weight: 500; font-size: 30px; line-height: 1.1; letter-spacing: -0.02em; }
.stat .t { font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--g500); margin-top: 3px; }
.stat.dev .n { color: var(--clay-d); }
.stat.you .n { color: var(--slate); }
.stat.you { border-color: var(--oat); background: #F6F0E4; }
/* filter chips */
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 18px 22px 4px;
}
.chip {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.05em;
padding: 6px 14px;
border-radius: 999px;
border: 1.5px solid var(--g300);
background: var(--paper);
color: var(--g700);
cursor: pointer;
}
.chip:hover { border-color: var(--g500); }
.chip.on { background: var(--slate); border-color: var(--slate); color: var(--ivory); }
.chip .ct { color: var(--g500); }
.chip.on .ct { color: var(--g300); }
/* timeline */
.timeline { padding: 16px 22px 26px; }
.entry {
display: grid;
grid-template-columns: 52px 14px 1fr;
gap: 0 14px;
position: relative;
}
.entry .ts {
font-family: var(--mono);
font-size: 12px;
color: var(--g500);
padding-top: 4px;
text-align: right;
}
.entry .rail { position: relative; }
.entry .rail::before {
content: "";
position: absolute;
left: 50%; top: 0; bottom: 0;
width: 1.5px;
background: var(--g200);
transform: translateX(-50%);
}
.entry.rail-first .rail::before { top: 10px; }
.entry.rail-last .rail::before { bottom: auto; height: 10px; }
.entry.rail-first.rail-last .rail::before { display: none; }
.entry .node {
position: absolute;
left: 50%; top: 8px;
width: 10px; height: 10px;
border-radius: 50%;
transform: translateX(-50%);
background: var(--g300);
border: 2px solid var(--paper);
box-shadow: 0 0 0 1.5px var(--g300);
}
.entry.t-plan .node { background: var(--olive); box-shadow: 0 0 0 1.5px var(--olive); }
.entry.t-dev .node { background: var(--clay); box-shadow: 0 0 0 1.5px var(--clay); }
.entry.t-disc .node { background: var(--oat); box-shadow: 0 0 0 1.5px var(--g500); }
.entry.t-human .node { background: var(--slate); box-shadow: 0 0 0 1.5px var(--slate); }
.entry .body { padding: 0 0 22px; min-width: 0; }
.badge {
display: inline-block;
font-family: var(--mono);
font-size: 10px;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 2.5px 9px;
border-radius: 6px;
margin-bottom: 6px;
}
.t-plan .badge { color: var(--olive); background: #EEF1E8; border: 1px solid #C9D4B8; }
.t-dev .badge { color: #fff; background: var(--clay); border: 1px solid var(--clay); }
.t-disc .badge { color: var(--g700); background: var(--g100); border: 1px solid var(--g300); }
.t-human .badge { color: var(--ivory); background: var(--slate); border: 1px solid var(--slate); }
.entry h3 {
font-family: var(--sans);
font-weight: 600;
font-size: 15px;
margin: 0 0 4px;
letter-spacing: -0.005em;
}
.entry p { margin: 0; font-size: 14px; color: var(--g700); max-width: 68ch; }
.entry code, .devgrid code, .n2 code {
font-family: var(--mono);
font-size: 12.5px;
background: var(--g100);
border: 1px solid var(--g200);
border-radius: 5px;
padding: 1px 5px;
overflow-wrap: anywhere;
}
.ts-mobile { display: none; }
/* deviation sub-fields */
.devgrid {
margin-top: 10px;
border: 1.5px solid var(--oat);
border-left: 3px solid var(--clay);
border-radius: 10px;
background: #FBF6EC;
overflow: hidden;
}
.devgrid .row {
display: grid;
grid-template-columns: 158px 1fr;
gap: 12px;
padding: 9px 14px;
font-size: 13.5px;
color: var(--g700);
}
.devgrid .row + .row { border-top: 1px solid #EFE5D2; }
.devgrid .k {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--clay-d);
padding-top: 2px;
}
.devgrid .row.chosen .k { color: var(--olive); }
.devgrid .row.chosen .v { color: var(--slate); font-weight: 500; }
@media (max-width: 560px) {
.devgrid .row { grid-template-columns: 1fr; gap: 2px; }
.entry { grid-template-columns: 14px 1fr; }
.entry .ts { display: none; }
.ts-mobile { display: inline; font-family: var(--mono); font-size: 11px; color: var(--g500); margin-left: 8px; }
}
/* attempt #2 block */
.next {
margin-top: 26px;
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
padding: 24px 26px 22px;
position: relative;
}
.next .label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.14em;
text-transform: uppercase;
color: var(--clay);
margin-bottom: 8px;
}
.next h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 24px;
letter-spacing: -0.01em;
margin: 0 0 6px;
}
.next .sub { font-size: 14px; color: var(--g500); margin: 0 0 14px; max-width: 60ch; }
.next ol { margin: 0; padding-left: 22px; }
.next li { font-size: 14.5px; color: var(--g700); margin-bottom: 10px; max-width: 70ch; }
.next li strong { color: var(--slate); font-weight: 600; }
footer {
margin-top: 64px;
padding-top: 22px;
border-top: 1.5px solid var(--g300);
font-size: 13.5px;
color: var(--g500);
}
footer a { color: var(--clay-d); text-decoration-color: var(--oat); text-underline-offset: 3px; }
.hidden { display: none; }
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<div class="eyebrow">Know your unknowns &middot; During implementation</div>
<a class="back" href="index.html">&larr; All examples</a>
<h1>Implementation notes</h1>
<p class="lede">Instead of letting mid-build surprises vanish into the scrollback, ask the agent to log every deviation from the plan as it happens — so the surprises become inputs to your next attempt.</p>
<div class="promptcard">
<div class="label">The prompt</div>
<button class="copybtn" id="copyPrompt">copy prompt</button>
<blockquote id="promptText">&ldquo;Keep an implementation-notes file as you build the export feature. If you hit an edge case that forces you to deviate from the plan, pick the conservative option, log it under &lsquo;Deviations&rsquo;, and keep going.&rdquo;</blockquote>
</div>
</header>
<div class="divider">What Claude produced</div>
<div class="doc">
<div class="doc-head">
<span class="dot"></span>
<span class="path">docs/notes/export-feature-implementation.md</span>
<span class="meta">run started 14:02 &middot; last write 16:51 &middot; branch feat/review-export</span>
</div>
<div class="doc-summary">
<div class="stat"><div class="n">11</div><div class="t">entries</div></div>
<div class="stat dev"><div class="n">4</div><div class="t">deviations</div></div>
<div class="stat you"><div class="n">2</div><div class="t">need your judgment</div></div>
</div>
<div class="chips" role="tablist" aria-label="Filter entries">
<button class="chip on" data-f="all">All <span class="ct">11</span></button>
<button class="chip" data-f="dev">Deviations <span class="ct">4</span></button>
<button class="chip" data-f="human">For you <span class="ct">2</span></button>
</div>
<div class="timeline" id="timeline">
<div class="entry t-plan" data-t="plan">
<div class="ts">14:02</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Plan-confirmed</span><span class="ts-mobile">14:02</span>
<h3>Step 1 — export job model and queue wiring, as planned</h3>
<p>Added <code>ExportJob</code> in <code>apps/api/src/exports/exportJob.ts</code>, enqueued on the existing BullMQ <code>media-tasks</code> queue with a dedicated <code>export</code> job name. Migration <code>0121_export_jobs</code> matches the plan's schema exactly (status enum, <code>requested_by</code>, <code>review_id</code>, <code>format</code>).</p>
</div>
</div>
<div class="entry t-plan" data-t="plan">
<div class="ts">14:18</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Plan-confirmed</span><span class="ts-mobile">14:18</span>
<h3>Step 2 — <code>POST /api/v1/reviews/:id/export</code> endpoint</h3>
<p>Request validation reuses the zod schemas in <code>apps/api/src/validation/</code> as the plan specified. <code>format</code> accepts <code>mp4-burnin | csv | bundle</code>. Returns <code>202</code> with a job id; polling endpoint deferred to step 6 per plan.</p>
</div>
</div>
<div class="entry t-disc" data-t="disc">
<div class="ts">14:29</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Discovery</span><span class="ts-mobile">14:29</span>
<h3><code>Review.duration_ms</code> is denormalized and occasionally stale</h3>
<p>Three fixture reviews have <code>duration_ms</code> that disagrees with the source asset's probe data (looks like re-uploads never refresh it). Export reads duration from <code>MediaAsset.probe.duration</code> instead — same approach <code>playbackManifest.ts</code> already takes. No plan change, just noting the trap for anyone who touches this later.</p>
</div>
</div>
<div class="entry t-dev" data-t="dev">
<div class="ts">14:41</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Deviation 1 of 4</span><span class="ts-mobile">14:41</span>
<h3>Legacy annotations don't always have frame timestamps</h3>
<div class="devgrid">
<div class="row"><div class="k">What the plan said</div><div class="v">Every annotation has a <code>frame_ts</code>; burn-in renderer can sort and place them all on the timeline.</div></div>
<div class="row"><div class="k">What the code revealed</div><div class="v">~12% of rows in <code>annotations</code> predate migration <code>0087</code> and have <code>frame_ts = NULL</code> — they were general review comments before per-frame annotations existed.</div></div>
<div class="row chosen"><div class="k">Conservative choice</div><div class="v">Exclude null-timestamp annotations from the video burn-in; include them in the CSV sidecar with <code>frame_ts</code> left blank and a <code>legacy_comment</code> flag. Nothing is silently dropped from the export bundle.</div></div>
<div class="row"><div class="k">Revisit</div><div class="v">Could interpolate a timestamp from <code>created_at</code> offset against the review session — decide whether that's honest enough to show in the video.</div></div>
</div>
</div>
</div>
<div class="entry t-plan" data-t="plan">
<div class="ts">15:05</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Plan-confirmed</span><span class="ts-mobile">15:05</span>
<h3>Step 3 — annotation burn-in via ffmpeg <code>drawtext</code>/<code>drawbox</code>, as planned</h3>
<p>Filtergraph builder in <code>apps/worker/src/exports/burnin.ts</code>. The plan's per-annotation overlay windows (<code>enable='between(t,a,b)'</code>) work; verified against the 40-annotation stress fixture — render time 1.4x realtime on the worker instance class, inside the plan's 2x budget.</p>
</div>
</div>
<div class="entry t-dev" data-t="dev">
<div class="ts">15:22</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Deviation 2 of 4</span><span class="ts-mobile">15:22</span>
<h3>The queue round-trips <code>Date</code> objects as strings</h3>
<div class="devgrid">
<div class="row"><div class="k">What the plan said</div><div class="v"><code>ExportJobPayload.requestedAt: Date</code>, passed through to the worker and compared against the review's <code>updated_at</code> to detect stale exports.</div></div>
<div class="row"><div class="k">What the code revealed</div><div class="v">BullMQ JSON-serializes payloads into Redis, so the worker receives an ISO string, not a <code>Date</code>. The staleness comparison silently coerced and passed in unit tests, but failed on a redelivered job in the integration run.</div></div>
<div class="row chosen"><div class="k">Conservative choice</div><div class="v">Typed the payload as wire format (<code>requestedAt: string</code> ISO-8601), parse once at the worker boundary in <code>exportConsumer.ts</code>. No custom serializer.</div></div>
<div class="row"><div class="k">Revisit</div><div class="v">Other <code>media-tasks</code> payloads likely have the same latent bug — a shared zod codec for job payloads would fix the class of problem.</div></div>
</div>
</div>
</div>
<div class="entry t-dev" data-t="dev">
<div class="ts">15:48</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Deviation 3 of 4</span><span class="ts-mobile">15:48</span>
<h3>Zip streaming already exists — dropped the planned <code>archiver</code> dependency</h3>
<div class="devgrid">
<div class="row"><div class="k">What the plan said</div><div class="v">Add <code>archiver</code> to build the export bundle (video + CSV + thumbnails) as a zip stream to S3.</div></div>
<div class="row"><div class="k">What the code revealed</div><div class="v"><code>packages/media/src/zipStream.ts</code> already does streaming zip with backpressure handling — it powers the bulk asset download feature and has been in production since March.</div></div>
<div class="row chosen"><div class="k">Conservative choice</div><div class="v">Reused <code>zipStream.ts</code> as-is. One fewer dependency, and bundle exports inherit the same tested backpressure behavior as bulk downloads.</div></div>
<div class="row"><div class="k">Revisit</div><div class="v"><code>zipStream</code> lives in <code>packages/media</code> but is now used by two features — worth promoting to <code>packages/shared</code> in a cleanup pass.</div></div>
</div>
</div>
</div>
<div class="entry t-disc" data-t="disc">
<div class="ts">16:07</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Discovery</span><span class="ts-mobile">16:07</span>
<h3>A websocket progress channel convention already exists</h3>
<p>The transcode pipeline emits <code>media:progress:{assetId}</code> events. Followed the same convention — exports emit <code>export:progress:{jobId}</code> — so the frontend can reuse <code>useProgressChannel()</code> from <code>apps/web/src/hooks/</code> instead of the polling UI the plan sketched. Kept the polling endpoint too, as the fallback.</p>
</div>
</div>
<div class="entry t-dev" data-t="dev">
<div class="ts">16:20</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Deviation 4 of 4</span><span class="ts-mobile">16:20</span>
<h3>Guest reviewers can view reviews but can't download assets</h3>
<div class="devgrid">
<div class="row"><div class="k">What the plan said</div><div class="v">Export permission = review-level ACL: anyone who can open the review can export it.</div></div>
<div class="row"><div class="k">What the code revealed</div><div class="v">Workspace-level <code>guest_reviewer</code> role grants review access with <code>can_download_assets = false</code> — clients use it to let external stakeholders comment without taking the media offsite. A review-ACL-only check would let guests exfiltrate source video via export.</div></div>
<div class="row chosen"><div class="k">Conservative choice</div><div class="v">Export returns <code>403</code> for <code>guest_reviewer</code> across all formats, mirroring the existing asset-download check in <code>assetAccess.ts</code>. Strictest defensible reading.</div></div>
<div class="row"><div class="k">Revisit</div><div class="v">Arguably guests should still get the CSV (annotations only, no media). That's a product call, not mine — logged below.</div></div>
</div>
</div>
</div>
<div class="entry t-human" data-t="human">
<div class="ts">16:33</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Todo for human</span><span class="ts-mobile">16:33</span>
<h3>Decide the guest-reviewer export policy</h3>
<p>Current behavior (from Deviation 4): guests get <code>403</code> on every export format. If you want guests to export the annotation CSV without media, it's a ~10-line change in <code>exportPolicy.ts</code> plus one test — but it changes what &ldquo;can't download assets&rdquo; means to customers, so it should be your call. Blocking neither merge nor QA.</p>
</div>
</div>
<div class="entry t-human" data-t="human">
<div class="ts">16:51</div>
<div class="rail"><span class="node"></span></div>
<div class="body">
<span class="badge">Todo for human</span><span class="ts-mobile">16:51</span>
<h3>Confirm the retention window for export bundles</h3>
<p>The plan didn't specify a TTL for generated bundles. I put them under the <code>exports/</code> S3 prefix with a 7-day lifecycle rule, matching share-link expiry — conservative, and nothing customer-visible promises longer. But if contracts or the data-retention policy say otherwise, the lifecycle rule in <code>infra/s3.tf</code> is the one line to change.</p>
</div>
</div>
</div>
</div>
<div class="next">
<div class="label">Fold back into the plan</div>
<h2>What this changes about attempt #2</h2>
<p class="sub">Three lines to paste into the next plan or prompt, so the next run doesn't rediscover today's surprises.</p>
<button class="copybtn" id="copyNext">copy bullets</button>
<ol id="nextList">
<li><strong>State the legacy-data caveat up front:</strong> ~12% of annotations (pre-migration-0087) have null <code>frame_ts</code>. Decide interpolate-vs-sidecar before implementation starts, not at 14:41 mid-build.</li>
<li><strong>Spec queue payloads as wire types:</strong> BullMQ round-trips through JSON, so payload interfaces should use ISO strings, never <code>Date</code> — and a shared zod codec for <code>media-tasks</code> payloads would retire the whole bug class.</li>
<li><strong>Inventory existing utilities before adding dependencies:</strong> <code>zipStream.ts</code> and <code>useProgressChannel()</code> both existed and both beat the plan's version. Add a &ldquo;search for prior art&rdquo; step to the plan template, and settle the guest-reviewer export policy before the next feature touches ACLs.</li>
</ol>
</div>
<footer>
Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.
</footer>
</div>
<script>
(function () {
// ── copy buttons ──
function wireCopy(btnId, getText) {
var btn = document.getElementById(btnId);
var original = btn.textContent;
btn.addEventListener('click', function () {
var text = getText();
function done() {
btn.textContent = 'Copied';
btn.classList.add('done');
setTimeout(function () {
btn.textContent = original;
btn.classList.remove('done');
}, 1600);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(done, function () { fallback(text); done(); });
} else {
fallback(text); done();
}
});
}
function fallback(text) {
var ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.opacity = '0';
document.body.appendChild(ta);
ta.select();
try { document.execCommand('copy'); } catch (e) {}
document.body.removeChild(ta);
}
wireCopy('copyPrompt', function () {
return "Keep an implementation-notes file as you build the export feature. If you hit an edge case that forces you to deviate from the plan, pick the conservative option, log it under 'Deviations', and keep going.";
});
wireCopy('copyNext', function () {
var items = document.querySelectorAll('#nextList li');
var out = [];
items.forEach(function (li, i) { out.push((i + 1) + '. ' + li.textContent.replace(/\s+/g, ' ').trim()); });
return out.join('\n');
});
// ── filter chips ──
var chips = document.querySelectorAll('.chip');
var entries = document.querySelectorAll('.entry');
function markRailEnds() {
var visible = Array.prototype.filter.call(entries, function (e) {
return !e.classList.contains('hidden');
});
entries.forEach(function (e) {
e.classList.remove('rail-first', 'rail-last');
});
if (visible.length) {
visible[0].classList.add('rail-first');
visible[visible.length - 1].classList.add('rail-last');
}
}
chips.forEach(function (chip) {
chip.addEventListener('click', function () {
chips.forEach(function (c) { c.classList.remove('on'); });
chip.classList.add('on');
var f = chip.getAttribute('data-f');
entries.forEach(function (e) {
var t = e.getAttribute('data-t');
var show = (f === 'all') || (f === 'dev' && t === 'dev') || (f === 'human' && t === 'human');
e.classList.toggle('hidden', !show);
});
markRailEnds();
});
});
markRailEnds();
})();
</script>
</body>
</html>
+573
View File
@@ -0,0 +1,573 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>The buy-in doc — Know your unknowns</title>
<style>
:root {
--ivory:#FAF9F5; --paper:#FFFFFF; --slate:#141413; --clay:#D97757; --clay-d:#B85C3E;
--oat:#E3DACC; --olive:#788C5D; --g100:#F0EEE6; --g200:#E6E3DA; --g300:#D1CFC5;
--g500:#87867F; --g700:#3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background: var(--ivory); color: var(--slate);
font-family: var(--sans); line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 980px; margin: 0 auto; padding: 0 20px; }
h1, h2, h3 { font-family: var(--serif); font-weight: 500; letter-spacing: -0.015em; }
/* ---------- page header ---------- */
header.page { padding: 56px 0 40px; border-bottom: 1.5px solid var(--g300); position: relative; }
.eyebrow {
font-family: var(--mono); font-size: 12px; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--g500); margin-bottom: 18px;
display: flex; align-items: center; gap: 12px;
}
.eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
.backlink {
position: absolute; top: 60px; right: 0;
font-family: var(--mono); font-size: 12px; color: var(--g500); text-decoration: none;
}
.backlink:hover { color: var(--clay-d); }
header.page h1 { font-size: clamp(34px, 5vw, 52px); line-height: 1.08; margin: 0 0 10px; }
.lede { font-size: 16.5px; color: var(--g700); max-width: 640px; }
/* ---------- prompt box ---------- */
.promptcard {
background: var(--paper); border: 1.5px solid var(--g300); border-radius: 14px;
padding: 22px 26px; margin: 30px 0 0; position: relative;
}
.promptcard .label {
font-family: var(--mono); font-size: 12px; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--clay); margin-bottom: 12px;
}
.promptcard blockquote {
font-family: var(--serif); font-style: italic; font-size: 17.5px;
line-height: 1.5; color: var(--slate); max-width: 720px;
}
.copybtn {
position: absolute; top: 18px; right: 18px;
font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.04em;
background: var(--g100); color: var(--g700); border: 1.5px solid var(--g300);
border-radius: 8px; padding: 6px 12px; cursor: pointer;
}
.copybtn:hover { border-color: var(--clay); color: var(--clay-d); }
.copybtn.done { background: var(--olive); border-color: var(--olive); color: #fff; }
/* ---------- divider ---------- */
.divider {
display: flex; align-items: center; gap: 16px; margin: 44px 0 36px;
font-family: var(--mono); font-size: 12px; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--g500);
}
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--g300); }
/* ================= ARTIFACT ================= */
.artifact {
background: var(--paper); border: 1.5px solid var(--g300); border-radius: 14px;
padding: clamp(22px, 4vw, 44px); margin-bottom: 48px;
}
.doc-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 6px; }
.doc-eyebrow {
font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.12em;
text-transform: uppercase; color: var(--g500);
display: flex; align-items: center; gap: 10px;
}
.doc-eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
.timechip {
font-family: var(--mono); font-size: 11.5px; color: var(--g700);
background: var(--oat); border-radius: 999px; padding: 5px 13px; white-space: nowrap;
display: inline-flex; align-items: center; gap: 7px;
}
.timechip::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--clay); }
.artifact h2.doc-title { font-size: clamp(26px, 3.6vw, 36px); line-height: 1.12; margin: 10px 0 4px; }
.doc-sub { color: var(--g500); font-size: 14px; margin-bottom: 26px; }
.doc-sub strong { color: var(--g700); font-weight: 600; }
.artifact h3.sec {
font-size: 21px; margin: 38px 0 14px; padding-top: 26px;
border-top: 1.5px solid var(--g200);
display: flex; align-items: baseline; gap: 10px;
}
.artifact h3.sec .n { font-family: var(--mono); font-size: 12px; color: var(--clay); font-weight: 400; }
/* ---------- demo player ---------- */
.demo-shell { position: relative; }
.demo {
background: var(--g100); border: 1.5px solid var(--g300); border-radius: 12px;
overflow: hidden; user-select: none;
}
.bl-titlebar {
display: flex; align-items: center; gap: 8px;
background: var(--paper); border-bottom: 1.5px solid var(--g200);
padding: 9px 14px;
}
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--g300); flex: none; }
.bl-name { font-family: var(--mono); font-size: 11px; color: var(--g500); margin-left: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.bl-body { display: grid; grid-template-columns: 1fr 232px; min-height: 268px; }
@media (max-width: 620px) { .bl-body { grid-template-columns: 1fr; } .bl-panel { border-left: none !important; border-top: 1.5px solid var(--g200); } }
/* video pane */
.bl-stage { position: relative; padding: 16px 16px 12px; display: flex; flex-direction: column; }
.bl-video {
flex: 1; min-height: 148px; border-radius: 8px; position: relative;
background:
radial-gradient(120px 80px at 68% 34%, #4a4b45 0%, transparent 70%),
linear-gradient(160deg, #3D3D3A 0%, #232322 100%);
border: 1.5px solid var(--g300);
}
.bl-play {
position: absolute; left: 12px; bottom: 10px; width: 0; height: 0;
border-left: 11px solid rgba(250,249,245,.85); border-top: 7px solid transparent; border-bottom: 7px solid transparent;
}
.bl-tc { position: absolute; right: 12px; bottom: 8px; font-family: var(--mono); font-size: 10.5px; color: rgba(250,249,245,.7); }
.bl-pin {
position: absolute; top: -7px; width: 14px; height: 14px; border-radius: 50% 50% 50% 2px;
background: var(--g500); transform: rotate(45deg); transition: background .25s;
}
.bl-pin.on { background: var(--clay); }
.bl-timeline { position: relative; height: 26px; margin-top: 14px; }
.bl-track { position: absolute; left: 0; right: 0; top: 11px; height: 4px; border-radius: 2px; background: var(--g300); }
.bl-track .fill { width: 34%; height: 100%; border-radius: 2px; background: var(--g500); }
/* annotation panel */
.bl-panel { background: var(--paper); border-left: 1.5px solid var(--g200); padding: 12px 12px 10px; display: flex; flex-direction: column; }
.bl-panel-head {
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--g500); display: flex; justify-content: space-between; align-items: center; margin-bottom: 9px;
}
.bl-count { color: var(--clay-d); }
.bl-ann {
display: flex; gap: 9px; align-items: flex-start; padding: 8px 8px;
border-radius: 8px; border: 1.5px solid transparent; transition: background .25s, border-color .25s;
}
.bl-ann.sel { background: var(--g100); border-color: var(--g300); }
.bl-check {
width: 15px; height: 15px; border-radius: 4px; border: 1.5px solid var(--g300);
background: var(--paper); flex: none; margin-top: 2px; position: relative; transition: background .2s, border-color .2s;
}
.bl-ann.sel .bl-check { background: var(--clay); border-color: var(--clay); }
.bl-ann.sel .bl-check::after {
content: ""; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px;
border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.bl-ann .who { font-size: 11.5px; font-weight: 600; line-height: 1.35; }
.bl-ann .who .t { font-family: var(--mono); font-weight: 400; font-size: 10px; color: var(--g500); margin-left: 5px; }
.bl-ann .txt { font-size: 11px; color: var(--g700); line-height: 1.4; }
.bl-export {
margin-top: auto; align-self: stretch; text-align: center;
font-family: var(--sans); font-size: 12.5px; font-weight: 600; color: #fff;
background: var(--slate); border: none; border-radius: 8px; padding: 9px 0;
transition: background .2s, transform .12s; pointer-events: none;
}
.bl-export.armed { background: var(--clay); }
.bl-export.pressed { background: var(--clay-d); transform: scale(.965); }
/* progress overlay */
.bl-progress {
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
background: rgba(20,20,19,.38); opacity: 0; pointer-events: none; transition: opacity .3s;
}
.bl-progress.show { opacity: 1; }
.bl-prog-card {
background: var(--paper); border-radius: 12px; padding: 20px 24px; width: min(300px, 84%);
box-shadow: 0 12px 32px rgba(20,20,19,.22);
}
.bl-prog-card .pt { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
.bl-prog-card .ps { font-family: var(--mono); font-size: 10.5px; color: var(--g500); margin-bottom: 12px; min-height: 14px; }
.bl-bar { height: 6px; border-radius: 3px; background: var(--g200); overflow: hidden; }
.bl-bar .b { width: 0%; height: 100%; background: var(--clay); border-radius: 3px; transition: width 1.7s cubic-bezier(.3,.7,.3,1); }
/* toast */
.bl-toast {
position: absolute; left: 50%; bottom: 14px; transform: translate(-50%, 16px);
background: var(--slate); color: var(--ivory); border-radius: 10px;
padding: 10px 14px; display: flex; align-items: center; gap: 10px;
opacity: 0; transition: opacity .35s, transform .35s; max-width: 92%;
box-shadow: 0 10px 26px rgba(20,20,19,.3);
}
.bl-toast.show { opacity: 1; transform: translate(-50%, 0); }
.bl-tick {
width: 18px; height: 18px; border-radius: 50%; background: var(--olive); flex: none; position: relative;
}
.bl-tick::after {
content: ""; position: absolute; left: 6px; top: 3.5px; width: 4px; height: 8px;
border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);
}
.bl-toast .m { font-size: 12px; white-space: nowrap; }
.bl-chip {
font-family: var(--mono); font-size: 10.5px; background: rgba(250,249,245,.14);
border: 1px solid rgba(250,249,245,.25); border-radius: 6px; padding: 3px 8px; white-space: nowrap;
}
.demo-caption {
display: flex; justify-content: space-between; align-items: center; gap: 12px;
margin-top: 10px; font-family: var(--mono); font-size: 11px; color: var(--g500);
}
.replay {
font-family: var(--mono); font-size: 11px; background: none; border: 1.5px solid var(--g300);
border-radius: 8px; color: var(--g700); padding: 5px 12px; cursor: pointer;
}
.replay:hover { border-color: var(--clay); color: var(--clay-d); }
/* ---------- pitch ---------- */
.pitch { font-family: var(--serif); font-size: 17.5px; line-height: 1.6; max-width: 46em; }
.pitch strong { font-weight: 600; }
/* ---------- objections ---------- */
.obj { border: 1.5px solid var(--g200); border-radius: 10px; margin-bottom: 10px; overflow: hidden; }
.obj summary {
list-style: none; cursor: pointer; padding: 12px 16px; display: flex; gap: 12px; align-items: baseline;
font-weight: 600; font-size: 14.5px;
}
.obj summary::-webkit-details-marker { display: none; }
.obj summary .q { font-family: var(--mono); font-size: 11px; color: var(--clay); flex: none; }
.obj summary::after { content: "+"; margin-left: auto; font-family: var(--mono); color: var(--g500); flex: none; }
.obj[open] summary::after { content: ""; }
.obj[open] summary { border-bottom: 1.5px solid var(--g200); background: var(--g100); }
.obj .a { padding: 13px 16px 14px 16px; font-size: 14px; color: var(--g700); }
.obj .ref {
display: inline-block; margin-top: 8px; font-family: var(--mono); font-size: 11.5px;
color: var(--clay-d); text-decoration: none; border-bottom: 1px dashed var(--g300);
}
.obj .ref:hover { border-bottom-color: var(--clay-d); }
/* ---------- spec table ---------- */
.tblwrap { overflow-x: auto; border: 1.5px solid var(--g200); border-radius: 10px; }
table.spec { width: 100%; border-collapse: collapse; font-size: 13.5px; min-width: 560px; }
table.spec th, table.spec td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--g200); vertical-align: top; }
table.spec tr:last-child td { border-bottom: none; }
table.spec th {
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
color: var(--g500); font-weight: 400; background: var(--g100);
}
table.spec td:first-child { font-weight: 600; white-space: nowrap; width: 140px; }
table.spec code, .a code, .risk code, .need code {
font-family: var(--mono); font-size: 0.88em; background: var(--g100);
border: 1px solid var(--g200); border-radius: 4px; padding: 1px 5px; white-space: nowrap;
}
.specref { font-family: var(--mono); font-size: 11px; color: var(--g500); }
/* ---------- risk + needs cards ---------- */
.cardgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 720px) { .cardgrid { grid-template-columns: 1fr; } }
.risk, .need {
border: 1.5px solid var(--g300); border-radius: 12px; padding: 18px 20px; background: var(--ivory);
}
.risk { border-left: 4px solid var(--clay); }
.need { border-left: 4px solid var(--olive); }
.risk h4, .need h4 {
font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase;
font-weight: 400; margin-bottom: 12px;
}
.risk h4 { color: var(--clay-d); }
.need h4 { color: var(--olive); }
.risk ul { list-style: none; }
.risk li { font-size: 13.5px; color: var(--g700); padding-left: 18px; position: relative; margin-bottom: 9px; }
.risk li::before { content: ""; position: absolute; left: 2px; top: 8px; width: 7px; height: 2px; background: var(--clay); }
.risk li strong { color: var(--slate); }
.approver { display: flex; gap: 11px; align-items: flex-start; padding: 9px 0; border-bottom: 1px solid var(--g200); }
.approver:last-child { border-bottom: none; }
.avatar {
width: 30px; height: 30px; border-radius: 50%; flex: none;
background: var(--oat); color: var(--clay-d);
font-family: var(--mono); font-size: 11px; display: flex; align-items: center; justify-content: center;
}
.approver .info { font-size: 13px; line-height: 1.4; }
.approver .info b { display: block; font-size: 13.5px; }
.approver .info .role { font-family: var(--mono); font-size: 10.5px; color: var(--g500); }
.approver .info .asks { color: var(--g700); margin-top: 2px; }
.bottomline {
margin-top: 34px; padding: 16px 20px; background: var(--oat); border-radius: 10px;
font-family: var(--serif); font-size: 16px;
}
footer {
padding: 26px 0 56px; font-size: 13px; color: var(--g500);
border-top: 1.5px solid var(--g300);
}
footer a { color: var(--clay-d); }
</style>
</head>
<body>
<div class="wrap">
<header class="page">
<a class="backlink" href="index.html">&larr; All examples</a>
<div class="eyebrow">Know your unknowns &middot; Post-implementation</div>
<h1>The buy-in doc</h1>
<p class="lede">After the work is done, the last unknown is other people — ask Claude to package the prototype, spec, and notes into one skimmable doc that answers objections before they're raised.</p>
<div class="promptcard">
<div class="label">The prompt</div>
<button class="copybtn" id="copyBtn">copy prompt</button>
<blockquote id="promptText">&ldquo;Package the prototype, the spec, and the implementation notes into a single doc I can drop in Slack to get buy-in on shipping annotation export. Lead with the demo.&rdquo;</blockquote>
</div>
</header>
<div class="divider">What Claude produced</div>
<!-- ================= ARTIFACT ================= -->
<article class="artifact">
<div class="doc-top">
<div class="doc-eyebrow">Ship proposal &middot; Acme</div>
<span class="timechip">90-second read</span>
</div>
<h2 class="doc-title">Annotation export is ready to ship</h2>
<p class="doc-sub">Prototype, spec, and rollout plan in one place. <strong>Ask:</strong> four sign-offs by Friday, flag ramp starts Monday.</p>
<!-- 1 · DEMO -->
<h3 class="sec"><span class="n">01</span> Watch it work</h3>
<div class="demo-shell">
<div class="demo" id="demo" aria-label="Animated mock of the annotation export flow in Acme">
<div class="bl-titlebar">
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
<span class="bl-name">Acme — q3_launch_teaser_v4.mp4 · Review</span>
</div>
<div class="bl-body">
<div class="bl-stage">
<div class="bl-video">
<span class="bl-play"></span>
<span class="bl-tc">00:42 / 02:05</span>
</div>
<div class="bl-timeline">
<div class="bl-track"><div class="fill"></div></div>
<span class="bl-pin" id="pin1" style="left: 18%;"></span>
<span class="bl-pin" id="pin2" style="left: 41%;"></span>
<span class="bl-pin" id="pin3" style="left: 73%;"></span>
</div>
</div>
<div class="bl-panel">
<div class="bl-panel-head"><span>Annotations</span><span class="bl-count" id="selCount">0 selected</span></div>
<div class="bl-ann" id="ann1">
<span class="bl-check"></span>
<div>
<div class="who">Maya R. <span class="t">00:23</span></div>
<div class="txt">Logo lands too late — pull in 8 frames?</div>
</div>
</div>
<div class="bl-ann" id="ann2">
<span class="bl-check"></span>
<div>
<div class="who">Devon P. <span class="t">00:51</span></div>
<div class="txt">Color shift here vs. the master grade</div>
</div>
</div>
<div class="bl-ann" id="ann3">
<span class="bl-check"></span>
<div>
<div class="who">Maya R. <span class="t">01:30</span></div>
<div class="txt">Approved — final CTA reads great</div>
</div>
</div>
<button class="bl-export" id="exportBtn" tabindex="-1">Export selected</button>
</div>
</div>
<div class="bl-progress" id="progress">
<div class="bl-prog-card">
<div class="pt">Exporting 3 annotations</div>
<div class="ps" id="progStatus">Collecting annotations…</div>
<div class="bl-bar"><div class="b" id="progBar"></div></div>
</div>
</div>
<div class="bl-toast" id="toast">
<span class="bl-tick"></span>
<span class="m">Export ready</span>
<span class="bl-chip">q3_teaser_notes.srt · 3 cues</span>
</div>
</div>
<div class="demo-caption">
<span id="demoState">select &rarr; export &rarr; done · loops automatically</span>
<button class="replay" id="replayBtn">&#8635; replay</button>
</div>
</div>
<!-- 2 · PITCH -->
<h3 class="sec"><span class="n">02</span> The pitch</h3>
<p class="pitch">Reviewers leave feedback in Acme, then retype it into email, Notion, and edit-bay software — <strong>the single most-cited gap in churn interviews (41% of departed teams)</strong>. Annotation export turns any selection of comments and drawings into SRT, CSV, or PDF in one click, using infrastructure we already run. It's built, flagged, load-tested to 12k annotations per session, and reversible in one toggle. The only thing between here and shipped is the four sign-offs below.</p>
<!-- 3 · OBJECTIONS -->
<h3 class="sec"><span class="n">03</span> What reviewers will ask</h3>
<details class="obj" open>
<summary><span class="q">Q1</span>Can this leak internal-only comments to a client?</summary>
<div class="a">No. Export runs through the same visibility resolver as the review page: annotations marked <code>internal</code> are excluded unless the exporter holds <code>workspace:admin</code> and explicitly opts in via a labeled checkbox. Covered by 14 permission-matrix tests.
<br><a class="ref" href="#spec">&#8599; spec &sect;4.2 — Visibility &amp; permissions</a></div>
</details>
<details class="obj">
<summary><span class="q">Q2</span>What happens on a monster session — thousands of annotations?</summary>
<div class="a">Export paginates at 500 annotations per batch and streams the file. Stress run against the largest real session in prod (a 12,400-annotation agency review) finished in 3.1s; p95 for typical sessions is 340ms.
<br><a class="ref" href="#spec">&#8599; implementation notes, 06-24 — load test results</a></div>
</details>
<details class="obj">
<summary><span class="q">Q3</span>Why file formats instead of a public API?</summary>
<div class="a">SRT + CSV + PDF cover 83% of the 212 support tickets tagged <code>export-request</code> over the last two quarters. An API is the right phase 2 — it needs auth scoping work this doesn't. Files unblock users now without foreclosing the API design.
<br><a class="ref" href="#spec">&#8599; metric — support tag analysis, Q1&ndash;Q2</a></div>
</details>
<details class="obj">
<summary><span class="q">Q4</span>Does this add new infrastructure we have to operate?</summary>
<div class="a">Zero new services. SRT/CSV render inline in the existing worker pool (<code>services/render-workers</code>); PDF reuses the report service already generating weekly digests. No new queues, no new datastore, no schema migrations.
<br><a class="ref" href="#spec">&#8599; spec &sect;5.1 — Architecture</a></div>
</details>
<details class="obj">
<summary><span class="q">Q5</span>What's the compliance story for exported customer content?</summary>
<div class="a">Every export is stamped with workspace ID, requesting user, and timestamp in file metadata, and written to the audit log (<code>audit.annotation_export</code>). Files are served via expiring signed URLs (24h) and never stored beyond that window.
<br><a class="ref" href="#spec">&#8599; spec &sect;6 — Audit &amp; retention</a></div>
</details>
<!-- 4 · SPEC SUMMARY -->
<h3 class="sec" id="spec"><span class="n">04</span> Spec at a glance</h3>
<div class="tblwrap">
<table class="spec">
<thead><tr><th>Area</th><th>Decision</th><th>Ref</th></tr></thead>
<tbody>
<tr><td>Formats</td><td>SRT (timecoded cues), CSV (flat, incl. drawing bounding boxes), PDF (frame thumbnails + comments)</td><td class="specref">&sect;2</td></tr>
<tr><td>Entry point</td><td>Annotation panel &rarr; multi-select &rarr; <b>Export selected</b>; also &ldquo;Export all&rdquo; in session menu</td><td class="specref">&sect;3.1</td></tr>
<tr><td>Permissions</td><td>Requires <code>session:comment</code> or above; <code>internal</code> annotations excluded by default</td><td class="specref">&sect;4.2</td></tr>
<tr><td>Limits</td><td>500 annotations/batch, streamed; 25k hard cap per export with clear error state</td><td class="specref">&sect;5.3</td></tr>
<tr><td>Telemetry</td><td><code>export_started</code>, <code>export_completed</code> (format, count, duration), <code>export_failed</code></td><td class="specref">&sect;7</td></tr>
<tr><td>Feature flag</td><td><code>annotation_export</code> — off &rarr; 5% &rarr; 50% &rarr; 100% over two weeks</td><td class="specref">&sect;8</td></tr>
</tbody>
</table>
</div>
<!-- 5 · RISK + NEEDS -->
<h3 class="sec"><span class="n">05</span> Risk, rollback, and what I need from you</h3>
<div class="cardgrid">
<div class="risk">
<h4>Risk &amp; rollback</h4>
<ul>
<li><strong>Rollback is one toggle.</strong> Flip <code>annotation_export</code> off — no schema migrations shipped, nothing to unwind.</li>
<li><strong>Blast radius is bounded.</strong> Export runs in the shared worker pool with a 30s per-job timeout; a runaway job cannot starve review playback.</li>
<li><strong>Worst credible failure:</strong> orphaned export files if the signed-URL cleanup lags — they self-expire in 24h regardless.</li>
<li><strong>Known gap:</strong> drawing annotations flatten to bounding boxes in CSV (full vectors are PDF-only). Documented in-product; phase 2 item.</li>
</ul>
</div>
<div class="need">
<h4>What I need from you</h4>
<div class="approver">
<span class="avatar">PN</span>
<div class="info"><b>Priya Natarajan</b><span class="role">Eng lead, Playback &amp; Review</span>
<div class="asks">Sign off on reusing the render worker pool (spec &sect;5.1) — the only shared-infra dependency.</div></div>
</div>
<div class="approver">
<span class="avatar">ES</span>
<div class="info"><b>Elena Sorokin</b><span class="role">Security</span>
<div class="asks">Approve the visibility rules and audit-log shape (spec &sect;4.2, &sect;6).</div></div>
</div>
<div class="approver">
<span class="avatar">MW</span>
<div class="info"><b>Marcus Webb</b><span class="role">Design</span>
<div class="asks">Final pass on the export modal and error states — prototype above is current.</div></div>
</div>
<div class="approver">
<span class="avatar">DK</span>
<div class="info"><b>Dana Kim</b><span class="role">PM, Collaboration</span>
<div class="asks">Approve the flag ramp schedule (spec &sect;8) and own the changelog entry.</div></div>
</div>
</div>
</div>
<div class="bottomline">Everything above is built and tested behind the flag. Reply with a &#10003; on your piece, or grab 15 minutes on my calendar — <b>target: ramp starts Monday.</b></div>
</article>
<footer>Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.</footer>
</div>
<script>
(function () {
// ---------- copy prompt ----------
var PROMPT = "Package the prototype, the spec, and the implementation notes into a single doc I can drop in Slack to get buy-in on shipping annotation export. Lead with the demo.";
var copyBtn = document.getElementById("copyBtn");
copyBtn.addEventListener("click", function () {
function done() {
copyBtn.textContent = "copied";
copyBtn.classList.add("done");
setTimeout(function () {
copyBtn.textContent = "copy prompt";
copyBtn.classList.remove("done");
}, 1600);
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(PROMPT).then(done, fallback);
} else { fallback(); }
function fallback() {
var ta = document.createElement("textarea");
ta.value = PROMPT;
ta.style.position = "fixed"; ta.style.opacity = "0";
document.body.appendChild(ta);
ta.select();
try { document.execCommand("copy"); } catch (e) {}
document.body.removeChild(ta);
done();
}
});
// ---------- demo animation ----------
var anns = [document.getElementById("ann1"), document.getElementById("ann2"), document.getElementById("ann3")];
var pins = [document.getElementById("pin1"), document.getElementById("pin2"), document.getElementById("pin3")];
var exportBtn = document.getElementById("exportBtn");
var progress = document.getElementById("progress");
var progBar = document.getElementById("progBar");
var progStatus = document.getElementById("progStatus");
var toast = document.getElementById("toast");
var selCount = document.getElementById("selCount");
var timers = [];
function at(ms, fn) { timers.push(setTimeout(fn, ms)); }
function reset() {
timers.forEach(clearTimeout);
timers = [];
anns.forEach(function (a) { a.classList.remove("sel"); });
pins.forEach(function (p) { p.classList.remove("on"); });
exportBtn.classList.remove("armed", "pressed");
progress.classList.remove("show");
progBar.style.transition = "none";
progBar.style.width = "0%";
// force reflow so the next width change animates
void progBar.offsetWidth;
progBar.style.transition = "";
progStatus.textContent = "Collecting annotations…";
toast.classList.remove("show");
selCount.textContent = "0 selected";
}
function play() {
reset();
[0, 1, 2].forEach(function (i) {
at(500 + i * 450, function () {
anns[i].classList.add("sel");
pins[i].classList.add("on");
selCount.textContent = (i + 1) + " selected";
if (i === 2) exportBtn.classList.add("armed");
});
});
at(2100, function () { exportBtn.classList.add("pressed"); });
at(2300, function () {
exportBtn.classList.remove("pressed");
progress.classList.add("show");
});
at(2450, function () { progBar.style.width = "100%"; });
at(3100, function () { progStatus.textContent = "Rendering SRT cues…"; });
at(3800, function () { progStatus.textContent = "Signing download URL…"; });
at(4300, function () {
progress.classList.remove("show");
toast.classList.add("show");
});
at(7000, play); // loop
}
document.getElementById("replayBtn").addEventListener("click", play);
play();
})();
</script>
</body>
</html>
+543
View File
@@ -0,0 +1,543 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quiz me before I merge — Know your unknowns</title>
<style>
:root{
--ivory:#FAF9F5; --paper:#FFFFFF; --slate:#141413; --clay:#D97757; --clay-d:#B85C3E;
--oat:#E3DACC; --olive:#788C5D; --g100:#F0EEE6; --g200:#E6E3DA; --g300:#D1CFC5;
--g500:#87867F; --g700:#3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{
background:var(--ivory); color:var(--slate); font-family:var(--sans);
line-height:1.55; font-size:15px; padding:40px 20px 64px;
}
.page{max-width:980px;margin:0 auto}
h1,h2,h3{font-family:var(--serif);font-weight:500;letter-spacing:-0.015em;color:var(--slate)}
/* ---- page header ---- */
.topline{display:flex;align-items:baseline;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:14px}
.eyebrow{
font-family:var(--mono);font-size:12px;text-transform:uppercase;letter-spacing:0.12em;
color:var(--g500);display:flex;align-items:center;gap:10px;
}
.eyebrow::before{content:"";display:inline-block;width:24px;height:2px;background:var(--clay);flex:none}
.backlink{font-family:var(--mono);font-size:12px;color:var(--g500);text-decoration:none;white-space:nowrap}
.backlink:hover{color:var(--clay-d)}
h1.pagetitle{font-size:34px;line-height:1.15;margin-bottom:10px}
.lede{color:var(--g700);font-size:16px;max-width:64ch;margin-bottom:26px}
/* ---- prompt box ---- */
.promptcard{
background:var(--paper);border:1.5px solid var(--g300);border-radius:14px;
padding:22px 26px;margin-bottom:36px;position:relative;
}
.promptcard .label{
font-family:var(--mono);font-size:12px;text-transform:uppercase;letter-spacing:0.12em;
color:var(--clay);margin-bottom:12px;
}
.promptcard blockquote{
font-family:var(--serif);font-style:italic;font-size:17.5px;line-height:1.55;
color:var(--slate);border:none;
}
.copybtn{
position:absolute;top:18px;right:18px;font-family:var(--mono);font-size:11.5px;
padding:6px 12px;border:1.5px solid var(--g300);border-radius:8px;background:var(--g100);
color:var(--g700);cursor:pointer;transition:all .15s;
}
.copybtn:hover{border-color:var(--clay);color:var(--clay-d)}
.copybtn.copied{background:var(--olive);border-color:var(--olive);color:#fff}
/* ---- divider ---- */
.divider{display:flex;align-items:center;gap:16px;margin:0 0 34px;
font-family:var(--mono);font-size:12px;letter-spacing:0.12em;color:var(--g500);text-transform:uppercase;white-space:nowrap}
.divider::before,.divider::after{content:"";flex:1;height:1px;background:var(--g300)}
/* =================== ARTIFACT =================== */
.artifact{background:var(--paper);border:1.5px solid var(--g300);border-radius:14px;overflow:hidden}
.art-head{padding:28px 34px 24px;border-bottom:1.5px solid var(--g200)}
.art-eyebrow{font-family:var(--mono);font-size:11.5px;text-transform:uppercase;letter-spacing:0.12em;color:var(--g500);margin-bottom:8px;display:flex;align-items:center;gap:10px}
.art-eyebrow::before{content:"";display:inline-block;width:24px;height:2px;background:var(--clay);flex:none}
.art-head h2{font-size:26px;margin-bottom:6px}
.art-head .sub{color:var(--g700);font-size:14.5px;max-width:66ch}
.branchline{font-family:var(--mono);font-size:12.5px;color:var(--g700);background:var(--g100);
border:1.5px solid var(--g300);border-radius:8px;padding:6px 10px;display:inline-block;margin-top:14px;overflow-x:auto;max-width:100%}
.branchline .arr{color:var(--g500);margin:0 6px}
.art-body{padding:30px 34px 38px}
@media (max-width:560px){
.art-head{padding:22px 18px 18px}
.art-body{padding:22px 18px 30px}
h1.pagetitle{font-size:27px}
}
/* stat chips */
.stats{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:34px}
.statchip{flex:1;min-width:130px;background:var(--g100);border:1.5px solid var(--g300);border-radius:12px;padding:14px 18px}
.statchip .num{font-family:var(--serif);font-size:28px;font-weight:500;letter-spacing:-0.02em;line-height:1.1}
.statchip .num.add{color:var(--olive)}
.statchip .num.del{color:var(--clay-d)}
.statchip .cap{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:0.1em;color:var(--g500);margin-top:4px}
.art-section{margin-bottom:38px}
.art-section h3{font-size:20px;margin-bottom:6px}
.art-section .secnote{color:var(--g500);font-size:13.5px;margin-bottom:16px}
.art-section p{color:var(--g700);margin-bottom:12px;max-width:70ch}
.art-section p strong{color:var(--slate)}
/* mental model diagram */
.flow-pair{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
@media (max-width:680px){.flow-pair{grid-template-columns:1fr}}
.flowcard{border:1.5px solid var(--g300);border-radius:12px;padding:16px 16px 18px;background:var(--ivory)}
.flowcard.after{border-color:var(--clay)}
.flowcard .fc-label{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:0.1em;margin-bottom:12px}
.flowcard.before .fc-label{color:var(--g500)}
.flowcard.after .fc-label{color:var(--clay-d)}
.fnode{border:1.5px solid var(--g300);background:var(--paper);border-radius:8px;padding:7px 12px;
font-family:var(--mono);font-size:12px;color:var(--g700);text-align:center}
.fnode.new{border-color:var(--clay);background:#FBF1EC;color:var(--clay-d)}
.fnode.oatf{background:var(--oat)}
.farrow{display:flex;justify-content:center;align-items:center;height:22px;color:var(--g500);font-size:13px;line-height:1}
.farrow.lab{gap:8px;font-family:var(--mono);font-size:10.5px}
.fsplit{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.fsplit .farrow{height:20px}
/* behavior blocks */
.beh{border:1.5px solid var(--g300);border-radius:12px;margin-bottom:14px;overflow:hidden}
.beh-head{display:flex;align-items:baseline;gap:12px;padding:14px 18px;background:var(--g100)}
.beh-num{font-family:var(--mono);font-size:12px;color:var(--clay-d);font-weight:600;flex:none}
.beh-head h4{font-family:var(--serif);font-weight:500;font-size:16.5px;letter-spacing:-0.01em}
.beh-body{padding:14px 18px 16px}
.beh-row{display:grid;grid-template-columns:58px 1fr;gap:10px;margin-bottom:9px;font-size:14px}
.beh-row:last-child{margin-bottom:0}
.beh-row .k{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:0.1em;color:var(--g500);padding-top:3px}
.beh-row .v{color:var(--g700)}
.fchip{display:inline-block;font-family:var(--mono);font-size:11.5px;background:var(--g100);
border:1px solid var(--g300);border-radius:6px;padding:1.5px 7px;color:var(--g700);white-space:nowrap;margin:1px 2px 1px 0}
/* leans-on callout */
.leans{border:1.5px solid var(--g300);border-left:4px solid var(--olive);border-radius:12px;
background:var(--ivory);padding:16px 20px}
.leans .lab{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:0.1em;color:var(--olive);margin-bottom:6px}
.leans p{margin-bottom:0;font-size:14px}
/* ---- quiz ---- */
.quiz-rule{display:flex;align-items:center;gap:16px;margin:44px 0 8px;
font-family:var(--mono);font-size:11.5px;letter-spacing:0.12em;color:var(--g500);text-transform:uppercase}
.quiz-rule::before,.quiz-rule::after{content:"";flex:1;height:1px;background:var(--g300)}
.quiz-intro{display:flex;justify-content:space-between;align-items:flex-end;gap:16px;flex-wrap:wrap;margin-bottom:20px}
.quiz-intro h3{font-size:22px}
.quiz-intro .qsub{color:var(--g500);font-size:13.5px}
.scorebox{font-family:var(--mono);font-size:12.5px;color:var(--g700);background:var(--g100);
border:1.5px solid var(--g300);border-radius:10px;padding:8px 14px;white-space:nowrap}
.scorebox b{color:var(--slate)}
.q{border:1.5px solid var(--g300);border-radius:12px;padding:18px 20px;margin-bottom:16px;background:var(--paper)}
.q.answered-right{border-color:var(--olive)}
.q.answered-wrong{border-color:var(--clay)}
.q .qnum{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:0.1em;color:var(--g500);margin-bottom:6px}
.q .qtext{font-family:var(--serif);font-size:17px;line-height:1.45;margin-bottom:14px;letter-spacing:-0.01em}
.q .qtext code{font-family:var(--mono);font-size:14px;background:var(--g100);border:1px solid var(--g300);border-radius:5px;padding:1px 5px}
.opt{display:flex;gap:10px;align-items:flex-start;width:100%;text-align:left;
border:1.5px solid var(--g300);border-radius:9px;background:var(--ivory);padding:10px 14px;
margin-bottom:8px;cursor:pointer;font:inherit;font-size:14px;color:var(--g700);transition:border-color .12s, background .12s}
.opt:hover{border-color:var(--g500)}
.opt .key{font-family:var(--mono);font-size:11.5px;color:var(--g500);flex:none;padding-top:2px}
.opt.sel-right{border-color:var(--olive);background:#F1F4EB;color:var(--slate)}
.opt.sel-right .key{color:var(--olive)}
.opt.sel-wrong{border-color:var(--clay);background:#FBF1EC;color:var(--slate)}
.opt.sel-wrong .key{color:var(--clay-d)}
.opt.reveal-right{border-color:var(--olive)}
.opt:disabled{cursor:default;opacity:1}
.opt.dim{opacity:.55}
.fb{border-radius:9px;padding:12px 15px;font-size:13.5px;margin-top:4px;display:none}
.fb.show{display:block}
.fb.good{background:#F1F4EB;border:1.5px solid var(--olive);color:var(--g700)}
.fb.good .fbtag{color:var(--olive)}
.fb.bad{background:#FBF1EC;border:1.5px solid var(--clay);color:var(--g700)}
.fb.bad .fbtag{color:var(--clay-d)}
.fbtag{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:0.1em;font-weight:600;margin-bottom:5px}
.fb .excerpt{margin-top:8px;border-left:3px solid var(--g300);padding:6px 12px;background:var(--paper);
border-radius:0 7px 7px 0;font-size:13px;color:var(--g700);font-style:italic;font-family:var(--serif)}
.fb .exlab{font-family:var(--mono);font-style:normal;font-size:10.5px;text-transform:uppercase;letter-spacing:0.09em;color:var(--g500);display:block;margin-bottom:3px}
/* result cards */
.result{display:none;margin-top:26px}
.result.show{display:block}
.cleared{border:1.5px solid var(--olive);border-radius:14px;background:#F1F4EB;padding:24px 26px}
.cleared .clr-head{display:flex;align-items:center;gap:12px;margin-bottom:6px}
.clr-badge{width:34px;height:34px;border-radius:50%;background:var(--olive);color:#fff;display:flex;
align-items:center;justify-content:center;font-size:17px;flex:none}
.cleared h3{font-size:21px}
.cleared .clr-sub{color:var(--g700);font-size:14px;margin-bottom:16px}
.mergelist{list-style:none}
.mergelist li{display:flex;gap:10px;align-items:baseline;font-family:var(--mono);font-size:12.5px;
color:var(--g700);padding:7px 0;border-bottom:1px dashed var(--g300)}
.mergelist li:last-child{border-bottom:none}
.mergelist .tick{color:var(--olive);font-weight:700;flex:none}
.mergelist .pend{color:var(--g500);flex:none}
.reread{border:1.5px solid var(--clay);border-radius:14px;background:#FBF1EC;padding:22px 26px}
.reread h3{font-size:19px;margin-bottom:4px}
.reread p{color:var(--g700);font-size:14px;margin-bottom:12px}
.reread ul{list-style:none}
.reread li{margin-bottom:7px}
.reread a{color:var(--clay-d);font-size:14px;text-decoration:none;border-bottom:1px solid var(--clay)}
.reread a:hover{color:var(--slate)}
.retry{margin-top:14px;font-family:var(--mono);font-size:12px;padding:8px 16px;border:1.5px solid var(--g300);
border-radius:8px;background:var(--paper);color:var(--g700);cursor:pointer}
.retry:hover{border-color:var(--clay);color:var(--clay-d)}
footer{margin-top:44px;text-align:center;font-size:13px;color:var(--g500)}
footer a{color:var(--clay-d);text-decoration:none}
footer a:hover{text-decoration:underline}
</style>
</head>
<body>
<div class="page">
<!-- ============ PAGE HEADER ============ -->
<div class="topline">
<div class="eyebrow">Know your unknowns &middot; Post-implementation</div>
<a class="backlink" href="index.html">&larr; All examples</a>
</div>
<h1 class="pagetitle">Quiz me before I merge</h1>
<p class="lede">Asking for a report <em>with a quiz you must pass</em> turns "I skimmed the diff" into verified understanding — the artifact won't let you feel done until you actually are.</p>
<div class="promptcard">
<div class="label">The Prompt</div>
<button class="copybtn" id="copyBtn">Copy prompt</button>
<blockquote id="promptText">I want to make sure I understand everything that happened in this change before I merge. Give me an HTML report on the export-feature diff — context, intuition, what was done — with a quiz at the bottom that I must pass.</blockquote>
</div>
<div class="divider">What Claude Produced</div>
<!-- ============ ARTIFACT ============ -->
<div class="artifact">
<div class="art-head">
<div class="art-eyebrow">Acme &middot; Merge readiness report</div>
<h2>Clip export: what changed and why</h2>
<p class="sub">A 14-file diff that adds server-side clip export to review threads. Read the three sections below, then pass the quiz. If you can't, you're not ready to merge — and that's the point.</p>
<div class="branchline">feature/clip-export<span class="arr">&rarr;</span>main &middot; 14 files &middot; 9 commits</div>
</div>
<div class="art-body">
<!-- stats -->
<div class="stats">
<div class="statchip"><div class="num">14</div><div class="cap">files changed</div></div>
<div class="statchip"><div class="num add">+612</div><div class="cap">lines added</div></div>
<div class="statchip"><div class="num del">&minus;148</div><div class="cap">lines removed</div></div>
</div>
<!-- mental model -->
<div class="art-section" id="sec-model">
<h3>The mental model</h3>
<p class="secnote">One diagram is worth 14 files: where an export request goes now vs. before.</p>
<p>Before this change, "export a clip" meant the browser stitched frames client-side with <code>MediaRecorder</code> and uploaded the result — slow, tab-locked, and broken on Safari. Now the client only <strong>requests</strong> an export; a new worker renders it server-side from the original media and the client polls a job until a signed download URL appears.</p>
<div class="flow-pair">
<div class="flowcard before">
<div class="fc-label">Before &middot; client-side render</div>
<div class="fnode">ReviewPlayer.tsx</div>
<div class="farrow">&darr;</div>
<div class="fnode">MediaRecorder capture (in-tab)</div>
<div class="farrow lab"><span>&darr;</span><span>~40&ndash;90s, tab locked</span></div>
<div class="fnode">PUT /uploads (whole file)</div>
<div class="farrow">&darr;</div>
<div class="fnode oatf">S3 exports/ bucket</div>
</div>
<div class="flowcard after">
<div class="fc-label">After &middot; server-side job</div>
<div class="fnode">ReviewPlayer.tsx</div>
<div class="farrow lab"><span>&darr;</span><span>POST /api/exports (returns job id)</span></div>
<div class="fnode new">export-worker (new, ffmpeg)</div>
<div class="fsplit">
<div><div class="farrow">&darr;</div><div class="fnode oatf">S3 exports/ bucket</div></div>
<div><div class="farrow">&darr;</div><div class="fnode new">jobs table (new)</div></div>
</div>
<div class="farrow lab"><span>&darr;</span><span>client polls every 2s</span></div>
<div class="fnode">GET /api/exports/:id &rarr; signed URL</div>
</div>
</div>
</div>
<!-- non-obvious behaviors -->
<div class="art-section" id="sec-behaviors">
<h3>Three non-obvious behaviors this introduces</h3>
<p class="secnote">The parts a diff skim won't tell you. Each one is deliberate — here's the reasoning.</p>
<div class="beh" id="beh-1">
<div class="beh-head"><span class="beh-num">01</span><h4>Exports render from the original upload, not the proxy the reviewer watched</h4></div>
<div class="beh-body">
<div class="beh-row"><span class="k">What</span><span class="v">The worker pulls <code class="fchip">media/originals/</code>, never the 720p review proxy. An exported clip can therefore look <em>sharper</em> than what the reviewer saw when they drew their annotations.</span></div>
<div class="beh-row"><span class="k">Why</span><span class="v">Editors export clips to hand off to clients; shipping the compressed proxy would embarrass them. Annotation coordinates are stored normalized (0&ndash;1), so they re-project onto the full-res frame correctly.</span></div>
<div class="beh-row"><span class="k">Where</span><span class="v"><span class="fchip">worker/export/render.ts:41</span> <span class="fchip">worker/export/burn_in.ts:88</span></span></div>
</div>
</div>
<div class="beh" id="beh-2">
<div class="beh-head"><span class="beh-num">02</span><h4>Export jobs survive a worker crash — via a visibility timeout, not retries</h4></div>
<div class="beh-body">
<div class="beh-row"><span class="k">What</span><span class="v">A job row is claimed by setting <code class="fchip">locked_until = now() + 10min</code>. If the worker dies mid-render, no retry fires; the lock simply expires and the next idle worker picks the job up from scratch.</span></div>
<div class="beh-row"><span class="k">Why</span><span class="v">ffmpeg renders aren't idempotent mid-flight, and a retry queue would need dead-letter handling we don't want yet. Lock expiry gives at-least-once semantics with one column. Consequence: a crashed job looks "processing" for up to 10 minutes before recovering.</span></div>
<div class="beh-row"><span class="k">Where</span><span class="v"><span class="fchip">db/migrations/0142_export_jobs.sql</span> <span class="fchip">worker/export/claim.ts:19</span></span></div>
</div>
</div>
<div class="beh" id="beh-3">
<div class="beh-head"><span class="beh-num">03</span><h4>Download URLs expire after 24 hours — the export itself lives 7 days</h4></div>
<div class="beh-body">
<div class="beh-row"><span class="k">What</span><span class="v">The signed S3 URL returned by <code class="fchip">GET /api/exports/:id</code> is valid for 24h, but the underlying object isn't deleted until a 7-day lifecycle rule fires. Re-hitting the endpoint mints a fresh URL with no re-render.</span></div>
<div class="beh-row"><span class="k">Why</span><span class="v">Short URLs limit link-forwarding outside the workspace (exports can contain unreleased footage); the longer object TTL means a stale Slack link costs one API call to revive, not a 90-second re-render.</span></div>
<div class="beh-row"><span class="k">Where</span><span class="v"><span class="fchip">api/exports/get.ts:57</span> <span class="fchip">infra/s3_lifecycle.tf:23</span></span></div>
</div>
</div>
</div>
<!-- leans on -->
<div class="art-section" id="sec-leans">
<div class="leans">
<div class="lab">Existing behavior this leans on</div>
<p><strong>Workspace-scoped signed-URL auth.</strong> <span class="fchip">GET /api/exports/:id</span> does no permission check of its own — it reuses the <span class="fchip">requireWorkspaceMember</span> middleware that already guards every media route. If that middleware's session handling ever changes (there's an open ticket, BL-2214, about guest reviewer sessions), export downloads change with it. Nothing in this diff would flag that.</p>
</div>
</div>
<!-- ============ QUIZ ============ -->
<div class="quiz-rule">Part 2 &middot; Prove it</div>
<div class="quiz-intro">
<div>
<h3>Six questions before you merge</h3>
<div class="qsub">Not trivia — each one is a decision you'd have to make correctly during an incident or a review.</div>
</div>
<div class="scorebox">Score: <b id="scoreNow">0</b> / <b>6</b> &middot; <span id="answeredNow">0</span> answered</div>
</div>
<div id="quiz"></div>
<!-- results -->
<div class="result" id="resPass">
<div class="cleared">
<div class="clr-head"><div class="clr-badge">&#10003;</div><h3>Cleared to merge</h3></div>
<p class="clr-sub">6/6 — you can explain this change to whoever gets paged for it. Standard checklist below.</p>
<ul class="mergelist">
<li><span class="tick">&#10003;</span>Understanding verified (this quiz, 6/6)</li>
<li><span class="tick">&#10003;</span>CI green on feature/clip-export &middot; 412 tests</li>
<li><span class="tick">&#10003;</span>Migration 0142 reviewed &mdash; additive, no backfill</li>
<li><span class="pend">&#9675;</span>Squash-merge with the summary above as the commit body</li>
<li><span class="pend">&#9675;</span>Watch export-worker dashboards for 30 min post-deploy</li>
<li><span class="pend">&#9675;</span>Note BL-2214 dependency in the merge comment</li>
</ul>
</div>
</div>
<div class="result" id="resFail">
<div class="reread">
<h3 id="failTitle">Not yet — re-read these sections</h3>
<p>You missed the questions below. The gaps map straight back to the report:</p>
<ul id="failLinks"></ul>
<button class="retry" id="retryBtn">Reset quiz &amp; try again</button>
</div>
</div>
</div><!-- /art-body -->
</div><!-- /artifact -->
<footer>Part of <a href="index.html">Know your unknowns</a> &mdash; companion examples to the blog post.</footer>
</div>
<script>
(function(){
// ---- copy prompt ----
var copyBtn = document.getElementById('copyBtn');
copyBtn.addEventListener('click', function(){
var t = document.getElementById('promptText').textContent;
function done(){ copyBtn.textContent='Copied'; copyBtn.classList.add('copied');
setTimeout(function(){copyBtn.textContent='Copy prompt';copyBtn.classList.remove('copied');},1600); }
if (navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(t).then(done, function(){ fallback(); });
} else { fallback(); }
function fallback(){
var ta=document.createElement('textarea'); ta.value=t; document.body.appendChild(ta);
ta.select(); try{document.execCommand('copy');}catch(e){} document.body.removeChild(ta); done();
}
});
// ---- quiz data ----
var QS = [
{
q:'A reviewer says their exported clip "looks different from what I annotated." Assuming no bug, why?',
opts:[
'The export re-encodes at a lower bitrate to keep files small',
'The export renders from the full-res original, while they reviewed the 720p proxy',
'Annotations are dropped when the source is above 1080p',
'Safari renders exports differently from Chrome'
],
correct:1,
reinforce:'Right — exports pull media/originals/, so the clip can be sharper than the proxy the reviewer watched. Expected, not a bug.',
excerptFrom:'Behavior 01', anchor:'beh-1', anchorLabel:'Behavior 01 — exports render from the original',
excerpt:'The worker pulls media/originals/, never the 720p review proxy. An exported clip can therefore look sharper than what the reviewer saw when they drew their annotations.'
},
{
q:'An export-worker pod is OOM-killed halfway through a render. What happens to that job?',
opts:[
'It moves to a dead-letter queue for manual replay',
'It is retried immediately on another worker',
'It shows "processing" until its 10-minute lock expires, then another worker restarts it from scratch',
'It fails permanently and the user must click Export again'
],
correct:2,
reinforce:'Exactly — no retry queue exists. Recovery is lock expiry: up to 10 minutes of apparent "processing", then a fresh render.',
excerptFrom:'Behavior 02', anchor:'beh-2', anchorLabel:'Behavior 02 — crash recovery via lock expiry',
excerpt:'If the worker dies mid-render, no retry fires; the lock simply expires and the next idle worker picks the job up from scratch… a crashed job looks "processing" for up to 10 minutes before recovering.'
},
{
q:'A client pastes an export link into Slack; a teammate clicks it 3 days later and gets an S3 "expired" error. Cheapest fix?',
opts:[
'Re-run the export — the file was deleted after 24 hours',
'Hit GET /api/exports/:id again — the object still exists for 7 days, so it just mints a fresh signed URL',
'Bump the lifecycle rule in s3_lifecycle.tf to 30 days',
'Ask an admin to make the exports/ bucket public-read'
],
correct:1,
reinforce:'Yes — only the URL expired (24h). The object lives 7 days, so a fresh GET returns a new link with zero re-render cost.',
excerptFrom:'Behavior 03', anchor:'beh-3', anchorLabel:'Behavior 03 — URL TTL vs object TTL',
excerpt:'The signed S3 URL is valid for 24h, but the underlying object isnt deleted until a 7-day lifecycle rule fires. Re-hitting the endpoint mints a fresh URL with no re-render.'
},
{
q:'Ticket BL-2214 changes how guest reviewer sessions are validated. Why does this diff make that ticket riskier?',
opts:[
'Guests can now trigger unlimited exports and exhaust the worker pool',
'Export downloads have no auth check of their own — they inherit whatever requireWorkspaceMember does, so a session change silently changes who can download',
'The jobs table stores guest session tokens in plaintext',
'It doesnt — exports are unrelated to session handling'
],
correct:1,
reinforce:'Correct — GET /api/exports/:id borrows requireWorkspaceMember wholesale. Change the middleware, change export access, with nothing in this diff to flag it.',
excerptFrom:'Existing behavior this leans on', anchor:'sec-leans', anchorLabel:'Callout — leans on workspace auth middleware',
excerpt:'GET /api/exports/:id does no permission check of its own — it reuses the requireWorkspaceMember middleware… If that middlewares session handling ever changes (BL-2214), export downloads change with it.'
},
{
q:'After deploy, a user reports "export stuck at processing for 15 minutes." Given the design, what does 15 minutes tell you?',
opts:[
'Nothing — long renders are normal for big clips',
'The clients 2-second polling loop probably disconnected',
'Something is wrong beyond a single crash: one crash self-heals within the 10-minute lock window, so 15+ minutes means repeated failures or a stuck worker',
'The S3 lifecycle rule deleted the file mid-render'
],
correct:2,
reinforce:'Right — the 10-minute lock is the self-heal budget. Anything past it means the job is failing repeatedly or a worker is wedged: page-worthy, not wait-worthy.',
excerptFrom:'Behavior 02', anchor:'beh-2', anchorLabel:'Behavior 02 — the 10-minute recovery window',
excerpt:'Lock expiry gives at-least-once semantics with one column. Consequence: a crashed job looks "processing" for up to 10 minutes before recovering.'
},
{
q:'Why do annotations still land in the right place even though the export renders at a different resolution than the review player?',
opts:[
'The worker scales the export down to match the proxy resolution first',
'Annotation coordinates are stored normalized (01) and re-projected onto whatever frame size is rendered',
'The client re-sends annotation pixels at export time',
'burn_in.ts snaps annotations to a fixed 1080p grid'
],
correct:1,
reinforce:'Yes — normalized coordinates are what make "render from the original" safe. Pixel-space storage would have broken every annotation in this diff.',
excerptFrom:'Behavior 01', anchor:'beh-1', anchorLabel:'Behavior 01 — normalized annotation coordinates',
excerpt:'Annotation coordinates are stored normalized (01), so they re-project onto the full-res frame correctly.'
}
];
var KEYS=['A','B','C','D'];
var answers=new Array(QS.length).fill(null); // true/false/null
var quizEl=document.getElementById('quiz');
function build(){
quizEl.innerHTML='';
answers=new Array(QS.length).fill(null);
QS.forEach(function(item,qi){
var qd=document.createElement('div'); qd.className='q'; qd.id='q'+qi;
var h='<div class="qnum">Question '+(qi+1)+' of '+QS.length+'</div>'
+'<div class="qtext">'+item.q+'</div>';
item.opts.forEach(function(o,oi){
h+='<button class="opt" data-q="'+qi+'" data-o="'+oi+'"><span class="key">'+KEYS[oi]+'</span><span>'+o+'</span></button>';
});
h+='<div class="fb" id="fb'+qi+'"></div>';
qd.innerHTML=h;
quizEl.appendChild(qd);
});
updateScore();
document.getElementById('resPass').classList.remove('show');
document.getElementById('resFail').classList.remove('show');
}
quizEl.addEventListener('click', function(e){
var btn=e.target.closest('.opt'); if(!btn||btn.disabled) return;
var qi=+btn.dataset.q, oi=+btn.dataset.o, item=QS[qi];
if(answers[qi]!==null) return;
var right=(oi===item.correct);
answers[qi]=right;
var qd=document.getElementById('q'+qi);
qd.classList.add(right?'answered-right':'answered-wrong');
qd.querySelectorAll('.opt').forEach(function(b){
b.disabled=true;
var boi=+b.dataset.o;
if(boi===oi) b.classList.add(right?'sel-right':'sel-wrong');
else if(boi===item.correct && !right) b.classList.add('reveal-right');
else b.classList.add('dim');
});
var fb=document.getElementById('fb'+qi);
if(right){
fb.className='fb good show';
fb.innerHTML='<div class="fbtag">&#10003; Correct</div>'+item.reinforce;
} else {
fb.className='fb bad show';
fb.innerHTML='<div class="fbtag">Not quite &mdash; the correct answer is '+KEYS[item.correct]+'</div>'
+'From the report:'
+'<div class="excerpt"><span class="exlab">'+item.excerptFrom+'</span>'+item.excerpt+'</div>';
}
updateScore();
if(answers.every(function(a){return a!==null;})) finish();
});
function updateScore(){
var score=answers.filter(function(a){return a===true;}).length;
var done=answers.filter(function(a){return a!==null;}).length;
document.getElementById('scoreNow').textContent=score;
document.getElementById('answeredNow').textContent=done;
}
function finish(){
var score=answers.filter(function(a){return a===true;}).length;
if(score===QS.length){
document.getElementById('resPass').classList.add('show');
document.getElementById('resPass').scrollIntoView({behavior:'smooth',block:'nearest'});
} else {
var missed=[]; answers.forEach(function(a,i){ if(a!==true) missed.push(i); });
document.getElementById('failTitle').textContent='Not yet: '+score+'/'+QS.length+' — re-read these sections';
var seen={}, ul=document.getElementById('failLinks'); ul.innerHTML='';
missed.forEach(function(i){
var it=QS[i];
if(seen[it.anchor]) return; seen[it.anchor]=true;
var li=document.createElement('li');
li.innerHTML='<a href="#'+it.anchor+'">'+it.anchorLabel+'</a>';
ul.appendChild(li);
});
document.getElementById('resFail').classList.add('show');
document.getElementById('resFail').scrollIntoView({behavior:'smooth',block:'nearest'});
}
}
document.getElementById('retryBtn').addEventListener('click', function(){
build();
document.getElementById('quiz').scrollIntoView({behavior:'smooth',block:'start'});
});
build();
})();
</script>
</body>
</html>
+486
View File
@@ -0,0 +1,486 @@
<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Know your unknowns — examples</title>
<style>
:root {
--ivory: #FAF9F5;
--paper: #FFFFFF;
--slate: #141413;
--clay: #D97757;
--clay-d: #B85C3E;
--oat: #E3DACC;
--olive: #788C5D;
--g100: #F0EEE6;
--g200: #E6E3DA;
--g300: #D1CFC5;
--g500: #87867F;
--g700: #3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--ivory);
color: var(--slate);
font-family: var(--sans);
line-height: 1.55;
-webkit-font-smoothing: antialiased;
}
.wrap { max-width: 1120px; margin: 0 auto; padding: 0 32px 140px; }
/* ── masthead ─────────────────────────── */
header.masthead {
padding: 80px 0 56px;
border-bottom: 1.5px solid var(--g300);
margin-bottom: 12px;
position: relative;
}
.eyebrow {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--g500);
margin-bottom: 18px;
display: flex;
align-items: center;
gap: 12px;
}
.eyebrow::before {
content: "";
width: 24px; height: 1.5px;
background: var(--clay);
}
.backlink {
position: absolute;
top: 34px; right: 0;
font-family: var(--mono);
font-size: 12px;
color: var(--g500);
text-decoration: none;
}
.backlink:hover { color: var(--clay); }
h1 {
font-family: var(--serif);
font-weight: 500;
font-size: clamp(38px, 5.4vw, 62px);
line-height: 1.06;
letter-spacing: -0.018em;
margin: 0 0 8px;
max-width: 17ch;
}
h1 em {
font-style: italic;
color: var(--clay);
}
.intro {
font-size: 16.5px;
color: var(--g700);
margin: 22px 0 0;
max-width: 640px;
}
.intro a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
.intro a:hover { text-decoration-color: var(--clay); }
.hero-grid {
display: grid;
grid-template-columns: 1fr 340px;
gap: 48px;
align-items: end;
}
@media (max-width: 880px) { .hero-grid { grid-template-columns: 1fr; } }
/* hero figure — map vs territory quadrants */
.hero-fig {
display: grid;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr 1fr;
gap: 12px;
}
@media (max-width: 880px) { .hero-fig { max-width: 360px; margin-top: 28px; } }
.hero-fig .q {
border-radius: 10px;
border: 1.5px solid var(--g300);
background: var(--paper);
padding: 12px;
aspect-ratio: 1;
position: relative;
display: flex;
flex-direction: column;
justify-content: flex-end;
overflow: hidden;
}
.hero-fig .q .tag {
font-family: var(--mono);
font-size: 8.5px;
letter-spacing: 0.07em;
text-transform: uppercase;
color: var(--g500);
}
.hero-fig .q.kk { background: var(--g100); }
.hero-fig .q.kk::before {
content: ""; position: absolute; inset: 12px 12px 34px;
border-radius: 6px;
background:
linear-gradient(var(--g300) 0 0) 0 0 / 80% 5px,
linear-gradient(var(--g300) 0 0) 0 12px / 60% 5px,
linear-gradient(var(--g300) 0 0) 0 24px / 70% 5px;
background-repeat: no-repeat;
}
.hero-fig .q.ku::before {
content: "?"; position: absolute; top: 2px; right: 12px;
font-family: var(--serif); font-size: 44px; color: var(--oat);
}
.hero-fig .q.uk::before {
content: ""; position: absolute; top: 16px; left: 16px;
width: 34px; height: 34px; border-radius: 50%;
border: 2.5px solid var(--olive); opacity: .55;
}
.hero-fig .q.uk::after {
content: ""; position: absolute; top: 29px; left: 29px;
width: 8px; height: 8px; border-radius: 50%;
background: var(--olive);
}
.hero-fig .q.uu {
border-color: var(--slate);
box-shadow: 0 10px 26px rgba(20,20,19,.10);
}
.hero-fig .q.uu::before {
content: ""; position: absolute; top: 14px; left: 14px; right: 14px; height: 36px;
border-radius: 6px;
border: 2px dashed var(--clay);
opacity: .75;
}
.hero-fig .q.uu .tag { color: var(--slate); }
/* ── toc pills ────────────────────────── */
nav.toc {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 26px 0 0;
}
nav.toc a {
font-size: 12.5px;
padding: 7px 14px;
border: 1.5px solid var(--g300);
border-radius: 999px;
text-decoration: none;
color: var(--g700);
background: var(--paper);
transition: border-color 120ms, color 120ms, background 120ms;
display: inline-flex;
align-items: center;
gap: 7px;
}
nav.toc a .n {
font-family: var(--mono);
font-size: 10px;
color: var(--g500);
}
nav.toc a:hover { border-color: var(--slate); color: var(--slate); }
nav.toc a:hover .n { color: var(--clay); }
/* ── section ──────────────────────────── */
section {
margin-top: 72px;
scroll-margin-top: 28px;
}
.sec-head {
display: flex;
align-items: baseline;
gap: 16px;
margin-bottom: 10px;
}
.sec-head .idx {
font-family: var(--mono);
font-size: 13px;
color: var(--clay);
font-weight: 600;
width: 34px;
flex-shrink: 0;
}
.sec-head h2 {
font-family: var(--serif);
font-weight: 500;
font-size: 27px;
margin: 0;
letter-spacing: -0.012em;
}
.sec-head .count {
font-family: var(--mono);
font-size: 11px;
color: var(--g500);
background: var(--g100);
padding: 2px 8px;
border-radius: 999px;
}
.sec-intro {
font-size: 14.5px;
color: var(--g700);
max-width: 700px;
margin: 0 0 24px 50px;
}
@media (max-width: 640px) { .sec-intro { margin-left: 0; } }
/* ── cards ────────────────────────────── */
.grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(316px, 1fr));
gap: 20px;
margin-left: 50px;
}
@media (max-width: 640px) { .grid { margin-left: 0; } }
a.card {
display: flex;
flex-direction: column;
background: var(--paper);
border: 1.5px solid var(--g300);
border-radius: 14px;
text-decoration: none;
color: inherit;
transition: transform 150ms ease, box-shadow 150ms ease, border-color 150ms ease;
overflow: hidden;
}
a.card:hover {
transform: translateY(-3px);
box-shadow: 0 10px 30px rgba(20, 20, 19, 0.10);
border-color: var(--slate);
}
.thumb {
height: 132px;
background: var(--g100);
border-bottom: 1.5px solid var(--g200);
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
transition: background 150ms ease;
}
a.card:hover .thumb { background: var(--oat); }
.thumb svg {
width: 100%; height: 100%;
overflow: visible;
}
.thumb svg .st { stroke: var(--g500); fill: none; stroke-width: 2.5; }
.thumb svg .fl { fill: var(--g300); }
.thumb svg .cl { fill: var(--clay); }
.thumb svg .ol { fill: var(--olive); }
.thumb svg .oa { fill: var(--oat); stroke: var(--g500); stroke-width: 2.5; }
.thumb svg .sl { fill: var(--slate); }
.thumb svg .wh { fill: var(--paper); stroke: var(--g500); stroke-width: 2.5; }
.thumb svg .ln { stroke: var(--g500); stroke-width: 2.5; fill: none; stroke-linecap: round; }
.thumb svg .lc { stroke: var(--clay); stroke-width: 2.5; fill: none; stroke-linecap: round; }
.thumb svg .da { stroke-dasharray: 4 4; }
a.card:hover .thumb svg .fl { fill: var(--g500); }
a.card:hover .thumb svg .oa { fill: var(--paper); }
.body { padding: 18px 20px 16px; display: flex; flex-direction: column; flex: 1; }
.title {
font-family: var(--serif);
font-size: 19px;
font-weight: 500;
line-height: 1.22;
color: var(--slate);
margin-bottom: 7px;
letter-spacing: -0.008em;
}
.desc {
font-size: 13.5px;
color: var(--g700);
line-height: 1.5;
margin-bottom: 16px;
flex: 1;
}
.file {
font-family: var(--mono);
font-size: 11px;
color: var(--g500);
border-top: 1px solid var(--g100);
padding-top: 11px;
display: flex;
align-items: center;
justify-content: space-between;
}
.file .arrow { transition: transform 150ms ease; color: var(--g300); }
a.card:hover .file { color: var(--clay); }
a.card:hover .file .arrow { transform: translateX(3px); color: var(--clay); }
/* ── footer ───────────────────────────── */
footer {
margin-top: 100px;
border-top: 1.5px solid var(--g300);
padding-top: 36px;
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 20px;
flex-wrap: wrap;
font-size: 13px;
color: var(--g500);
}
footer .k { font-family: var(--serif); font-style: italic; color: var(--g700); font-size: 15px; }
footer a { color: var(--clay); text-decoration-color: var(--oat); text-underline-offset: 3px; }
</style>
</head>
<body>
<div class="wrap">
<header class="masthead">
<a class="backlink" href="../index.html">← html-effectiveness</a>
<div class="hero-grid">
<div>
<div class="eyebrow">Companion to the blog post</div>
<h1>Know your <em>unknowns</em></h1>
<p class="intro">
The map is not the territory — the gap between them is your unknowns. Eleven
self-contained <code>.html</code> artifacts for discovering them before, during, and
after implementation. Each page shows the exact prompt at the top and the artifact
Claude produced below it: paste the prompt, get something like the page.
</p>
<nav class="toc">
<a href="#pre">Pre-implementation <span class="n">8</span></a>
<a href="#during">During implementation <span class="n">1</span></a>
<a href="#post">Post-implementation <span class="n">2</span></a>
</nav>
</div>
<div class="hero-fig" aria-hidden="true">
<div class="q kk"><span class="tag">Known knowns</span></div>
<div class="q ku"><span class="tag">Known unknowns</span></div>
<div class="q uk"><span class="tag">Unknown knowns</span></div>
<div class="q uu"><span class="tag">Unknown unknowns</span></div>
</div>
</div>
</header>
<section id="pre">
<div class="sec-head"><span class="idx">01</span><h2>Pre-implementation</h2><span class="count">8 demos</span></div>
<p class="sec-intro">Before any code is written is the cheapest place to find an unknown. Ask for a blindspot pass when the territory is unfamiliar, brainstorm and prototype when you'll only know it when you see it, let Claude interview you about the rest, and hand it references when words run out.</p>
<div class="grid">
<a class="card" href="01-blindspot-pass.html">
<div class="thumb"><svg viewBox="0 0 120 80"><rect class="wh" x="12" y="8" width="68" height="64" rx="6"/><line class="ln" x1="22" y1="20" x2="62" y2="20"/><line class="ln" x1="22" y1="30" x2="70" y2="30"/><line class="ln" x1="22" y1="40" x2="56" y2="40"/><line class="ln" x1="22" y1="50" x2="66" y2="50"/><line class="ln" x1="22" y1="60" x2="48" y2="60"/><circle class="cl" cx="70" cy="40" r="3.5"/><circle class="cl" cx="56" cy="60" r="3.5"/><circle class="lc da" cx="96" cy="32" r="15" fill="none"/><line class="lc" x1="106" y1="43" x2="114" y2="52"/><rect class="oa" x="88" y="58" width="24" height="14" rx="4"/><line class="ln" x1="93" y1="65" x2="107" y2="65"/></svg></div>
<div class="body">
<div class="title">Blindspot pass</div>
<div class="desc">Claude scans an unfamiliar auth module and reports your unknown unknowns — seven blindspot cards, each with a copyable prompt fix, assembled into one better implementation prompt.</div>
<div class="file"><span>01-blindspot-pass.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="02-color-grading-explainer.html">
<div class="thumb"><svg viewBox="0 0 120 80"><rect class="wh" x="8" y="8" width="104" height="46" rx="4"/><rect class="oa" x="8" y="8" width="104" height="20" rx="4"/><circle class="cl" cx="90" cy="20" r="6"/><path class="ol" d="M14 52 L38 28 L60 52 Z"/><path class="ol" d="M48 52 L74 32 L100 52 Z"/><line class="lc da" x1="60" y1="8" x2="60" y2="54"/><line class="ln" x1="16" y1="64" x2="56" y2="64"/><circle class="cl" cx="40" cy="64" r="4"/><line class="ln" x1="66" y1="64" x2="106" y2="64"/><circle class="cl" cx="88" cy="64" r="4"/><line class="ln" x1="16" y1="73" x2="56" y2="73"/><circle class="sl" cx="28" cy="73" r="4"/><line class="ln" x1="66" y1="73" x2="106" y2="73"/><circle class="sl" cx="98" cy="73" r="4"/></svg></div>
<div class="body">
<div class="title">Teach me my unknowns</div>
<div class="desc">An interactive color-grading explainer — vocabulary ladder, live before/after frame with sliders and presets — that turns "make the video nicer" into precise professional prompts.</div>
<div class="file"><span>02-color-grading-explainer.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="03-design-directions.html">
<div class="thumb"><svg viewBox="0 0 120 80"><rect class="sl" x="6" y="8" width="50" height="30" rx="4"/><rect class="cl" x="12" y="14" width="12" height="4" rx="2"/><rect class="fl" x="12" y="23" width="38" height="3" rx="1.5"/><rect class="fl" x="12" y="30" width="30" height="3" rx="1.5"/><rect class="wh" x="64" y="8" width="50" height="30" rx="4"/><circle class="cl" cx="77" cy="23" r="7"/><rect class="fl" x="90" y="17" width="18" height="4" rx="2"/><rect class="fl" x="90" y="25" width="13" height="4" rx="2"/><rect class="wh" x="6" y="44" width="50" height="30" rx="4"/><rect class="fl" x="11" y="50" width="12" height="18" rx="2"/><rect class="oa" x="25" y="50" width="12" height="18" rx="2"/><rect class="ol" x="39" y="50" width="12" height="18" rx="2"/><rect class="sl" x="64" y="44" width="50" height="30" rx="4"/><line class="lc" x1="70" y1="52" x2="96" y2="52"/><line class="ln" x1="70" y1="59" x2="104" y2="59"/><line class="ln" x1="70" y1="66" x2="88" y2="66"/></svg></div>
<div class="body">
<div class="title">Four design directions</div>
<div class="desc">The same review queue rendered four wildly different ways — ops console, editorial, kanban, terminal — with steal/skip chips that write your reply for you.</div>
<div class="file"><span>03-design-directions.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="04-toolbar-mock.html">
<div class="thumb"><svg viewBox="0 0 120 80"><rect class="oa" x="8" y="10" width="104" height="50" rx="5"/><circle class="cl" cx="90" cy="24" r="7"/><path class="ol" d="M10 48 Q 40 34 68 46 T 110 42 L 110 55 Q 110 58 105 58 L 15 58 Q 10 58 10 55 Z"/><rect class="st da" x="18" y="18" width="28" height="15" rx="2"/><path class="lc" d="M98 46 L 86 34"/><rect class="wh" x="28" y="48" width="64" height="13" rx="6.5"/><circle class="cl" cx="38" cy="54.5" r="2.5"/><circle class="fl" cx="47" cy="54.5" r="2.5"/><circle class="fl" cx="56" cy="54.5" r="2.5"/><circle class="ol" cx="68" cy="54.5" r="2.5"/><circle class="sl" cx="77" cy="54.5" r="2.5"/><line class="ln" x1="14" y1="70" x2="106" y2="70"/><line class="lc" x1="14" y1="70" x2="46" y2="70"/><circle class="cl" cx="46" cy="70" r="3"/></svg></div>
<div class="body">
<div class="title">Mock before you wire</div>
<div class="desc">A clickable throwaway mock of Acme's frame-annotation toolbar — three toggleable placements, A/B questions, and a self-filling reply template — before any real code is touched.</div>
<div class="file"><span>04-toolbar-mock.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="05-churn-brainstorm.html">
<div class="thumb"><svg viewBox="0 0 120 80"><line class="ln" x1="10" y1="16" x2="110" y2="16"/><circle class="wh" cx="20" cy="16" r="5"/><circle class="cl" cx="38" cy="16" r="5"/><circle class="wh" cx="56" cy="16" r="5"/><circle class="cl" cx="76" cy="16" r="5"/><circle class="st" cx="100" cy="16" r="5"/><rect class="wh" x="10" y="30" width="100" height="20" rx="4"/><rect class="cl" x="16" y="36" width="8" height="8" rx="2"/><line class="ln" x1="30" y1="38" x2="86" y2="38"/><line class="ln" x1="30" y1="44" x2="70" y2="44"/><rect class="oa" x="92" y="35" width="12" height="10" rx="3"/><rect class="wh" x="10" y="56" width="100" height="20" rx="4"/><rect class="wh" x="16" y="62" width="8" height="8" rx="2"/><line class="ln" x1="30" y1="64" x2="82" y2="64"/><line class="lc da" x1="30" y1="70" x2="62" y2="70"/></svg></div>
<div class="body">
<div class="title">Brainstorm the intervention</div>
<div class="desc">Ten codebase-grounded churn interventions plotted from ship-this-afternoon to quarter-long bet, with resonate checkboxes that assemble your reply.</div>
<div class="file"><span>05-churn-brainstorm.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="06-interview.html">
<div class="thumb"><svg viewBox="0 0 120 80"><circle class="cl" cx="12" cy="16" r="4"/><circle class="ol" cx="12" cy="30" r="4"/><circle class="st" cx="12" cy="44" r="4"/><circle class="st da" cx="12" cy="58" r="4"/><line class="ln" x1="12" y1="20" x2="12" y2="26"/><line class="ln da" x1="12" y1="34" x2="12" y2="54"/><rect class="wh" x="26" y="10" width="88" height="60" rx="6"/><rect class="cl" x="34" y="18" width="24" height="6" rx="3"/><line class="ln" x1="34" y1="32" x2="104" y2="32"/><rect class="oa" x="34" y="40" width="34" height="22" rx="4"/><rect class="wh" x="72" y="40" width="34" height="22" rx="4"/><line class="lc" x1="78" y1="51" x2="88" y2="51"/><line class="ln" x1="40" y1="51" x2="60" y2="51"/></svg></div>
<div class="body">
<div class="title">The interview</div>
<div class="desc">Claude interviews you one question at a time about an ambiguous feature, ordered by architectural blast radius, then hands back a decisions table and a ready-to-paste implementation prompt.</div>
<div class="file"><span>06-interview.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="07-reference-port.html">
<div class="thumb"><svg viewBox="0 0 120 80"><rect class="wh" x="6" y="10" width="48" height="60" rx="6"/><rect class="wh" x="66" y="10" width="48" height="60" rx="6"/><rect class="fl" x="13" y="19" width="34" height="4" rx="2"/><rect class="fl" x="13" y="29" width="26" height="4" rx="2"/><rect class="cl" x="13" y="39" width="32" height="5" rx="2.5"/><rect class="fl" x="13" y="50" width="28" height="4" rx="2"/><rect class="fl" x="73" y="19" width="30" height="4" rx="2"/><rect class="fl" x="73" y="29" width="34" height="4" rx="2"/><rect class="cl" x="73" y="39" width="30" height="5" rx="2.5"/><rect class="fl" x="73" y="50" width="24" height="4" rx="2"/><line class="lc da" x1="45" y1="41.5" x2="73" y2="41.5"/><circle class="ol" cx="60" cy="62" r="6"/><path class="ln" d="M57.5 62 l2 2 l3.5 -4" style="stroke:#FAF9F5"/></svg></div>
<div class="body">
<div class="title">Point at a reference</div>
<div class="desc">A semantics map that proves Claude understood a Rust reference implementation — matched code excerpts, gotcha notes, and edge-case tables — before porting it to TypeScript.</div>
<div class="file"><span>07-reference-port.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="08-implementation-plan.html">
<div class="thumb"><svg viewBox="0 0 120 80"><rect class="wh" x="8" y="6" width="104" height="68" rx="6"/><rect class="cl" x="16" y="14" width="5" height="12" rx="2"/><rect class="oa" x="27" y="14" width="60" height="12" rx="3"/><rect class="wh" x="93" y="14" width="12" height="12" rx="3"/><rect class="cl" x="16" y="32" width="5" height="10" rx="2"/><rect class="fl" x="27" y="32" width="48" height="10" rx="3"/><line class="lc" x1="16" y1="50" x2="104" y2="50"/><line class="ln da" x1="16" y1="58" x2="96" y2="58"/><line class="ln da" x1="16" y1="66" x2="72" y2="66"/></svg></div>
<div class="body">
<div class="title">The tweakable plan</div>
<div class="desc">An implementation plan sorted by likelihood-of-tweaking instead of execution order — flagged schema choices with toggleable alternatives, annotated type interfaces, and the mechanical work collapsed at the bottom.</div>
<div class="file"><span>08-implementation-plan.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<section id="during">
<div class="sec-head"><span class="idx">02</span><h2>During implementation</h2><span class="count">1 demo</span></div>
<p class="sec-intro">No matter how much planning you do, unknowns lurk in the territory. Have Claude keep a running log of every place the code forced a deviation from the plan, so the next attempt starts smarter.</p>
<div class="grid">
<a class="card" href="09-implementation-notes.html">
<div class="thumb"><svg viewBox="0 0 120 80"><rect class="wh" x="12" y="4" width="96" height="72" rx="5"/><line class="ln" x1="27" y1="14" x2="27" y2="68"/><circle class="ol" cx="27" cy="14" r="3.5"/><line class="ln" x1="37" y1="14" x2="98" y2="14"/><circle class="cl" cx="27" cy="28" r="3.5"/><line class="lc" x1="37" y1="28" x2="88" y2="28"/><rect class="oa" x="37" y="34" width="52" height="10" rx="2"/><circle class="fl" cx="27" cy="54" r="3.5"/><line class="ln" x1="37" y1="54" x2="94" y2="54"/><circle class="sl" cx="27" cy="66" r="3.5"/><line class="ln" x1="37" y1="66" x2="78" y2="66"/></svg></div>
<div class="body">
<div class="title">Implementation notes</div>
<div class="desc">The running log Claude kept during a 3-hour build — every deviation from the plan captured with the conservative call it made, and three bullets to fold into attempt #2.</div>
<div class="file"><span>09-implementation-notes.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<section id="post">
<div class="sec-head"><span class="idx">03</span><h2>Post-implementation</h2><span class="count">2 demos</span></div>
<p class="sec-intro">Shipping means other people inheriting your unknowns. A pitch doc answers the objections reviewers were about to raise; a quiz proves you actually understand what changed before you merge it.</p>
<div class="grid">
<a class="card" href="10-pitch-doc.html">
<div class="thumb"><svg viewBox="0 0 120 80"><rect class="wh" x="6" y="6" width="108" height="68" rx="5"/><rect class="oa" x="14" y="14" width="58" height="32" rx="4"/><path class="cl" d="M38 22 L52 30 L38 38 Z"/><line class="ln" x1="80" y1="20" x2="106" y2="20"/><line class="ln" x1="80" y1="28" x2="106" y2="28"/><line class="lc" x1="80" y1="36" x2="98" y2="36"/><line class="ln" x1="14" y1="54" x2="76" y2="54"/><line class="ln" x1="14" y1="62" x2="60" y2="62"/><rect class="ol" x="88" y="52" width="22" height="14" rx="7"/></svg></div>
<div class="body">
<div class="title">The buy-in doc</div>
<div class="desc">A ship-it pitch that leads with an animated demo of the export flow, then pre-answers every reviewer objection with evidence and names exactly who needs to sign off on what.</div>
<div class="file"><span>10-pitch-doc.html</span><span class="arrow"></span></div>
</div>
</a>
<a class="card" href="11-change-quiz.html">
<div class="thumb"><svg viewBox="0 0 120 80"><rect class="wh" x="6" y="8" width="108" height="30" rx="5"/><line class="ln" x1="14" y1="16" x2="70" y2="16"/><line class="ln" x1="14" y1="23" x2="92" y2="23"/><line class="ln" x1="14" y1="30" x2="58" y2="30"/><rect class="oa" x="80" y="12" width="26" height="10" rx="3"/><rect class="wh" x="6" y="44" width="108" height="12" rx="4"/><circle class="ol" cx="14" cy="50" r="4"/><line class="ln" x1="24" y1="50" x2="80" y2="50"/><rect class="wh" x="6" y="60" width="108" height="12" rx="4"/><circle class="cl" cx="14" cy="66" r="4"/><line class="ln" x1="24" y1="66" x2="66" y2="66"/><path class="lc" d="M96 62 l4 5 l7 -8" fill="none" stroke-width="2.5"/></svg></div>
<div class="body">
<div class="title">Quiz me before I merge</div>
<div class="desc">A merge-readiness report on a 14-file diff that ends in a six-question quiz you must pass — wrong answers point you back to the exact section you skimmed.</div>
<div class="file"><span>11-change-quiz.html</span><span class="arrow"></span></div>
</div>
</a>
</div>
</section>
<footer>
<span class="k">Every explainer, brainstorm, interview, and prototype is a cheap way to find out what you didn't know.</span>
<span><a href="../index.html">All HTML-effectiveness examples</a></span>
</footer>
</div>
</body>
</html>