From 6a2f6934bbafe3700a461294f5b1ad98e8d2436a Mon Sep 17 00:00:00 2001 From: wehub-resource-sync Date: Mon, 13 Jul 2026 12:33:58 +0800 Subject: [PATCH] chore: import upstream snapshot with attribution --- 01-exploration-code-approaches.html | 454 ++++++++++ 02-exploration-visual-designs.html | 516 +++++++++++ 03-code-review-pr.html | 639 +++++++++++++ 04-code-understanding.html | 492 ++++++++++ 05-design-system.html | 630 +++++++++++++ 06-component-variants.html | 606 +++++++++++++ 07-prototype-animation.html | 456 ++++++++++ 08-prototype-interaction.html | 397 ++++++++ 09-slide-deck.html | 593 ++++++++++++ 10-svg-illustrations.html | 493 ++++++++++ 11-status-report.html | 529 +++++++++++ 12-incident-report.html | 597 ++++++++++++ 13-flowchart-diagram.html | 396 ++++++++ 14-research-feature-explainer.html | 382 ++++++++ 15-research-concept-explainer.html | 369 ++++++++ 16-implementation-plan.html | 703 +++++++++++++++ 17-pr-writeup.html | 596 ++++++++++++ 18-editor-triage-board.html | 574 ++++++++++++ 19-editor-feature-flags.html | 664 ++++++++++++++ 20-editor-prompt-tuner.html | 723 +++++++++++++++ CODE_OF_CONDUCT.md | 49 + LICENSE | 201 +++++ README.md | 41 + README.wehub.md | 7 + SECURITY.md | 22 + index.html | 824 +++++++++++++++++ unknowns/01-blindspot-pass.html | 679 ++++++++++++++ unknowns/02-color-grading-explainer.html | 582 ++++++++++++ unknowns/03-design-directions.html | 1050 ++++++++++++++++++++++ unknowns/04-toolbar-mock.html | 798 ++++++++++++++++ unknowns/05-churn-brainstorm.html | 716 +++++++++++++++ unknowns/06-interview.html | 644 +++++++++++++ unknowns/07-reference-port.html | 651 ++++++++++++++ unknowns/08-implementation-plan.html | 765 ++++++++++++++++ unknowns/09-implementation-notes.html | 609 +++++++++++++ unknowns/10-pitch-doc.html | 573 ++++++++++++ unknowns/11-change-quiz.html | 543 +++++++++++ unknowns/index.html | 486 ++++++++++ 38 files changed, 20049 insertions(+) create mode 100644 01-exploration-code-approaches.html create mode 100644 02-exploration-visual-designs.html create mode 100644 03-code-review-pr.html create mode 100644 04-code-understanding.html create mode 100644 05-design-system.html create mode 100644 06-component-variants.html create mode 100644 07-prototype-animation.html create mode 100644 08-prototype-interaction.html create mode 100644 09-slide-deck.html create mode 100644 10-svg-illustrations.html create mode 100644 11-status-report.html create mode 100644 12-incident-report.html create mode 100644 13-flowchart-diagram.html create mode 100644 14-research-feature-explainer.html create mode 100644 15-research-concept-explainer.html create mode 100644 16-implementation-plan.html create mode 100644 17-pr-writeup.html create mode 100644 18-editor-triage-board.html create mode 100644 19-editor-feature-flags.html create mode 100644 20-editor-prompt-tuner.html create mode 100644 CODE_OF_CONDUCT.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 README.wehub.md create mode 100644 SECURITY.md create mode 100644 index.html create mode 100644 unknowns/01-blindspot-pass.html create mode 100644 unknowns/02-color-grading-explainer.html create mode 100644 unknowns/03-design-directions.html create mode 100644 unknowns/04-toolbar-mock.html create mode 100644 unknowns/05-churn-brainstorm.html create mode 100644 unknowns/06-interview.html create mode 100644 unknowns/07-reference-port.html create mode 100644 unknowns/08-implementation-plan.html create mode 100644 unknowns/09-implementation-notes.html create mode 100644 unknowns/10-pitch-doc.html create mode 100644 unknowns/11-change-quiz.html create mode 100644 unknowns/index.html diff --git a/01-exploration-code-approaches.html b/01-exploration-code-approaches.html new file mode 100644 index 0000000..fc0c648 --- /dev/null +++ b/01-exploration-code-approaches.html @@ -0,0 +1,454 @@ + + + + + + + Debounced search — three approaches + + + +
+ +
+
Exploration · Acme web client
+

Three ways to implement debounced search

+
+ Prompt + Show me three different ways to implement debounced search for the task + filter input in our React codebase, with tradeoffs for each. +
+
+ + + +
+ + +
+
+

01Inline useEffect + setTimeout

+

Debounce logic lives directly inside the component that owns the input.

+
+ +
export function TaskSearch() {
+  const [draft, setDraft] = useState('');
+  const [query, setQuery] = useState('');
+
+  useEffect(() => {
+    const id = setTimeout(() => setQuery(draft), 300);
+    return () => clearTimeout(id);
+  }, [draft]);
+
+  const { data } = useTasks({ search: query });
+
+  return (
+    <input
+      value={draft}
+      onChange={(e) => setDraft(e.target.value)}
+      placeholder="Filter tasks…"
+    />
+  );
+}
+ +
+
+
Pro
+
Con
+
+
+
Zero new abstractions to learn
+
Logic duplicated everywhere search exists
+
+
+
Easy to step through in devtools
+
Two pieces of state for one conceptual value
+
+
+
No dependency or bundle change
+
Delay constant is buried in component body
+
+
+ +
+ Bundle impact: +0 kb + Testability: medium + Reuse: low + SSR safe: yes +
+
+ + +
+
+

02Custom useDebounce hook

+

Extract the timer into a shared hook under src/hooks/.

+
+ +
// src/hooks/useDebounce.ts
+export function useDebounce<T>(value: T, ms = 300): T {
+  const [debounced, setDebounced] = useState(value);
+  useEffect(() => {
+    const id = setTimeout(() => setDebounced(value), ms);
+    return () => clearTimeout(id);
+  }, [value, ms]);
+  return debounced;
+}
+
+// TaskSearch.tsx
+const [draft, setDraft] = useState('');
+const query = useDebounce(draft, 300);
+const { data } = useTasks({ search: query });
+ +
+
+
Pro
+
Con
+
+
+
Single import reused across filter, command bar, board search
+
One more file to maintain and document
+
+
+
Trivial to unit test with fake timers
+
Generic T hides intent slightly
+
+
+
Delay is a visible, tunable argument
+
Still re-renders on every keystroke
+
+
+ +
+ Bundle impact: +0.2 kb + Testability: high + Reuse: high + SSR safe: yes +
+
+ + +
+
+

03Tiny external library

+

Adopt use-debounce for both values and callbacks.

+
+ +
import { useDebouncedCallback }
+  from 'use-debounce';
+
+export function TaskSearch() {
+  const [query, setQuery] = useState('');
+
+  const onChange = useDebouncedCallback(
+    (next: string) => setQuery(next),
+    300,
+    { leading: false, maxWait: 1000 },
+  );
+
+  const { data } = useTasks({ search: query });
+
+  return (
+    <input
+      defaultValue=""
+      onChange={(e) => onChange(e.target.value)}
+    />
+  );
+}
+ +
+
+
Pro
+
Con
+
+
+
leading / trailing / maxWait handled for us
+
New runtime dependency to audit and update
+
+
+
Callback form skips intermediate re-renders
+
Uncontrolled input diverges from Acme form patterns
+
+
+
Well-tested edge cases (unmount, flush, cancel)
+
~1.4 kb gzipped for something we could own
+
+
+ +
+ Bundle impact: +1.4 kb + Testability: high + Reuse: high + SSR safe: yes +
+
+ +
+ + + + + +
+ + diff --git a/02-exploration-visual-designs.html b/02-exploration-visual-designs.html new file mode 100644 index 0000000..817fcc3 --- /dev/null +++ b/02-exploration-visual-designs.html @@ -0,0 +1,516 @@ + + + + + + + Empty state — four visual directions + + + + +
+
Acme · design exploration
+
+ Background: +
+ + +
+
+
+ +
+ +
+
Exploration · Empty states
+

Four visual directions for the “no tasks yet” state

+
+ Prompt + 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. +
+
+ +
+ + +
+ A — Minimal +
+
+

No tasks yet

+

When you create a task it will show up here.

+ New task +
+
+

+ Pure typography, single quiet action. Reads as calm and confident; + assumes the surrounding UI already carries enough personality. +

+
+ + +
+ B — Illustrated +
+
+ +

Start your first list

+

Group related work and watch progress roll up automatically.

+ Create a task +
+
+

+ A small geometric spot illustration anchors the eye and explains the + object model (lists contain tasks) without a wall of copy. +

+
+ + +
+ C — Playful +
+
+
+
+
+
+
+
+

Nothing on your plate

+

Enjoy the quiet, or add something to get moving.

+
+
+

+ A gently bobbing stack adds life to an otherwise static screen. Motion + is subtle enough to loop indefinitely without drawing complaints. +

+
+ + +
+ D — Instructional +
+
+

Set up this project

+
    +
  1. + Create your first task + Give it a name and an owner. +
  2. +
  3. + Add a due date + Acme will surface it on the timeline. +
  4. +
  5. + Invite a teammate + Shared projects stay in sync automatically. +
  6. +
+
+
+

+ 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. +

+
+ +
+
+ + + + diff --git a/03-code-review-pr.html b/03-code-review-pr.html new file mode 100644 index 0000000..bfb344b --- /dev/null +++ b/03-code-review-pr.html @@ -0,0 +1,639 @@ + + + + + + +PR #247 — Review Summary + + + +
+ +
+
acme/web · Pull Request #247
+

Add optimistic updates to task list mutations

+
+
+
MO
+
+
Mira Okafor
+
opened 2 days ago
+
+
+
+ mo/optimistic-tasks main +
+
+ +142 / −38 + 6 files changed +
+
+
+ +
+

What this PR does

+
    +
  • Replaces the await-then-refetch pattern in TaskList with optimistic cache writes, so toggling or reordering a task feels instant instead of waiting ~300ms for the round-trip.
  • +
  • Introduces a small useOptimisticTasks hook that wraps the mutation, snapshots the previous list, and rolls back on error.
  • +
  • Extends the API client to accept an idempotency key per mutation and adds a toast when a rollback fires.
  • +
+
+ +
+

Risk map

+ +
+ safe + worth a look + needs attention +
+
+ +
+

Files

+ + +
+
+
+
src/hooks/useOptimisticTasks.ts
+
+
+ needs attention + +58 −0 +
+
+
+
@@ -0,0 +1,58 @@
+
1+import { useMutation, useQueryClient } from '@tanstack/react-query';
+
2+import { updateTask, TaskPatch } from '../api/tasks';
+
3+import type { Task } from '../types/task';
+
4+
+
5+export function useOptimisticTasks(boardId: string) {
+
6+ const qc = useQueryClient();
+
7+ const key = ['tasks', boardId];
+
8+
+
9+ return useMutation({
+
10+ mutationFn: (patch: TaskPatch) => updateTask(patch),
+
11+ onMutate: async (patch) => {
+
12+ const prev = qc.getQueryData<Task[]>(key);
+
13+ qc.setQueryData<Task[]>(key, (old = []) =>
+
14+ old.map(t => t.id === patch.id ? { ...t, ...patch } : t)
+
15+ );
+
16+ return { prev };
+
17+ },
+
18+ onError: (_e, _p, ctx) => qc.setQueryData(key, ctx?.prev),
+
19+ });
+
20+}
+
+
+
+
line 11
+

BlockingonMutate doesn't call qc.cancelQueries(key) 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.

+
+
+
line 18
+

NitRollback restores the list but never surfaces the error. Consider wiring the existing pushToast here so users know the toggle didn't stick.

+
+
+
+ + +
+
+
+
src/components/TaskList.tsx
+
+
+ worth a look + +31 −24 +
+
+
+
@@ -42,14 +42,17 @@ export function TaskList({ boardId }: Props) {
+
42 const { data: tasks } = useTasks(boardId);
+
43- const [pending, setPending] = useState<string | null>(null);
+
44-
+
45- async function toggle(task: Task) {
+
46- setPending(task.id);
+
47- await updateTask({ id: task.id, done: !task.done });
+
48- await refetch();
+
49- setPending(null);
+
50- }
+
43+ const { mutate, isPending } = useOptimisticTasks(boardId);
+
44+
+
45+ const toggle = (task: Task) =>
+
46+ mutate({ id: task.id, done: !task.done });
+
47
+
48 return (
+
49 <ul className="tasks">
+
50- {tasks?.map(t => <TaskRow key={t.id} task={t} busy={pending === t.id} />)}
+
50+ {tasks?.map(t => <TaskRow key={t.id} task={t} onToggle={toggle} />)}
+
51 </ul>
+
+
+
+
line 43
+

NitisPending is destructured but never read. Either drop it or pass it to TaskRow so the checkbox can dim while the request is in flight.

+
+
+
+ + +
+
+
+
src/api/tasks.ts
+
+
+ worth a look + +19 −6 +
+
+
+
@@ -12,10 +12,15 @@ export type TaskPatch = Partial<Task> & { id: string };
+
12
+
13-export async function updateTask(patch: TaskPatch) {
+
14- return http.patch(`/tasks/${patch.id}`, patch);
+
13+export async function updateTask(
+
14+ patch: TaskPatch,
+
15+ key = crypto.randomUUID(),
+
16+) {
+
17+ return http.patch(`/tasks/${patch.id}`, patch, {
+
18+ headers: { 'Idempotency-Key': key },
+
19+ });
+
20 }
+
+
+
+
line 15
+

BlockingGenerating the idempotency key as a default parameter means retries from the mutation layer get a new key each time, which defeats the purpose. The key should be minted once in onMutate and threaded through.

+
+
+
+ + +
+ + src/components/Toast.tsx + + safe + +14 −2 + + +
Adds a variant="warning" style and exports pushToast. Purely additive, no behaviour change for existing call sites.
+
+ +
+ + src/types/task.ts + + safe + +6 −2 + + +
Widens Task.status to include "archived" and adds an optional updatedAt timestamp. Type-only change.
+
+ +
+ + src/components/__tests__/TaskList.test.tsx + + safe + +14 −4 + + +
Adds a test asserting the row updates synchronously after click, and one asserting rollback when the mocked request rejects. Both pass locally.
+
+
+ +
+

Suggested next steps

+
    +
  • + + +
  • +
  • + + +
  • +
  • + + +
  • +
+
+ +
+ + + + diff --git a/04-code-understanding.html b/04-code-understanding.html new file mode 100644 index 0000000..e36505b --- /dev/null +++ b/04-code-understanding.html @@ -0,0 +1,492 @@ + + + + + + +How authentication flows through acme/web + + + +
+ +
+
acme/web · architecture note
+

How authentication flows through the codebase

+

+ Acme uses cookie-based sessions: the browser never holds a bearer token directly. Every authenticated request hits /api/*, passes through a single verifyToken() middleware, and resolves to a Session row that downstream handlers read off req.ctx. The middleware is the only place that talks to the session store, which is the only place that talks to the sessions table — so there's exactly one trust boundary to reason about. +

+
+ +
+

Request path

+
+ + + + + + + + + + + Browser + acme.app + + + + /api/session + route handler + + + + verifyToken() + middleware/auth.ts + + + + + + SessionStore + lib/sessionStore.ts + + + + Postgres + sessions table + + + + + + + + + cookie + lookup + +
+ +

Callstack walkthrough

+ + +
+
1
+
+
src/app/providers/AuthProvider.tsx :22-48
+

On mount, the React provider issues a GET /api/session with credentials: 'include' so the fw_sid cookie rides along. The response either hydrates currentUser into context or leaves it null, which the router treats as "show the sign-in screen".

+
+ show source +
// src/app/providers/AuthProvider.tsx
+export function AuthProvider({ children }: Props) {
+  const [user, setUser] = useState<User | null>(null);
+
+  useEffect(() => {
+    fetch('/api/session', { credentials: 'include' })
+      .then(r => r.ok ? r.json() : null)
+      .then(setUser);
+  }, []);
+
+  return <AuthCtx.Provider value={{ user }}>{children}</AuthCtx.Provider>;
+}
+
+
+
+ + +
+
2
+
+
src/server/routes/session.ts :9-27
+

The route itself is thin: it just returns whatever req.ctx.session the middleware attached. If the middleware short-circuited with a 401, this handler never runs — so there's no auth logic duplicated here.

+
+ show source +
// src/server/routes/session.ts
+router.get('/session', verifyToken, (req, res) => {
+  const { session } = req.ctx;
+  res.json({
+    id:    session.userId,
+    email: session.email,
+    role:  session.role,
+    exp:   session.expiresAt,
+  });
+});
+
+
+
+ + +
+
3
+
+
src/middleware/auth.ts :14-31
+

This is the trust boundary. verifyToken reads the signed fw_sid cookie, asks SessionStore to resolve it, and either populates req.ctx.session or responds 401. Every protected route in the app is mounted behind this function, so changing its behaviour changes auth globally.

+
+ show source +
// src/middleware/auth.ts
+export async function verifyToken(req, res, next) {
+  const raw = req.signedCookies['fw_sid'];
+  if (!raw) return res.status(401).end();
+
+  const session = await SessionStore.get(raw);
+  if (!session || session.expiresAt < Date.now()) {
+    return res.status(401).end();
+  }
+
+  req.ctx = { session };
+  next();
+}
+
+
+
+ + +
+
4
+
+
src/lib/sessionStore.ts :8-52
+

SessionStore is a small read-through cache: it checks an in-process LRU first, then falls back to Postgres. Writes (create, revoke) always go straight to the DB and invalidate the cache entry so other workers don't serve a stale session.

+
+ show source +
// src/lib/sessionStore.ts
+const cache = new LRU<string, Session>({ max: 5000, ttl: 60_000 });
+
+export const SessionStore = {
+  async get(id: string) {
+    const hit = cache.get(id);
+    if (hit) return hit;
+    const row = await db.one(SELECT_SESSION, [id]);
+    if (row) cache.set(id, row);
+    return row ?? null;
+  },
+  /* create, revoke, touch ... */
+};
+
+
+
+ + +
+
5
+
+
db/migrations/004_sessions.sql :1-18
+

The sessions table is keyed on a random 32-byte id (the cookie value) with a covering index on user_id for "sign out everywhere". Expiry is enforced both here (expires_at) and again in the middleware as defence in depth.

+
+ show source +
-- db/migrations/004_sessions.sql
+create table sessions (
+  id          text primary key,
+  user_id     uuid not null references users(id),
+  created_at  timestamptz default now(),
+  expires_at  timestamptz not null,
+  ip          inet,
+  user_agent  text
+);
+create index sessions_user_id_idx on sessions(user_id);
+
+
+
+
+ + + +
+ + + + diff --git a/05-design-system.html b/05-design-system.html new file mode 100644 index 0000000..da5460e --- /dev/null +++ b/05-design-system.html @@ -0,0 +1,630 @@ + + + + + + +Acme — Design System Reference + + + +
+ +
+

Acme design system

+

+ Generated from src/styles/tokens.ts and src/components/ — use as a portable reference when prompting. +

+
+ + +
+

Color

+
+ +
+
Primary
+
+
+
+ #D97757 + --clay +
+
+
+ #141413 + --slate +
+
+
+ #FAF9F5 + --ivory +
+
+
+ #E3DACC + --oat +
+
+
+ +
+
Neutral
+
+
+
+ #FFFFFF + --white +
+
+
+ #F0EEE6 + --gray-100 +
+
+
+ #D1CFC5 + --gray-300 +
+
+
+ #87867F + --gray-500 +
+
+
+ #3D3D3A + --gray-700 +
+
+
+ +
+
Semantic
+
+
+
+ #788C5D + --success +
+
+
+ #C78E3F + --warning +
+
+
+ #B04A4A + --danger +
+
+
+ #5C7CA3 + --info +
+
+
+
+ + +
+

Typography

+
+ +
+
+
Plan the week ahead
+
+ Display + 48 / 1.1 / 500 +
+
+
+
Plan the week ahead
+
+ Heading 1 + 32 / 1.2 / 500 +
+
+
+
Plan the week ahead
+
+ Heading 2 + 24 / 1.3 / 500 +
+
+
+
Review milestones, assign owners, and surface blockers before they cascade.
+
+ Body + 16 / 1.55 / 430 +
+
+
+
Review milestones, assign owners, and surface blockers before they cascade.
+
+ Small + 14 / 1.5 / 430 +
+
+
+
UPDATED 2 HOURS AGO
+
+ Caption + 12 / 1.4 / 500 +
+
+
+
+ + +
+

Spacing

+
+ +
+
+
+
4--sp-1
+
+
+
+
8--sp-2
+
+
+
+
12--sp-3
+
+
+
+
16--sp-4
+
+
+
+
24--sp-5
+
+
+
+
32--sp-6
+
+
+
+
48--sp-7
+
+
+
+
64--sp-8
+
+
+
+ + +
+

Radius & Elevation

+
+ +
+
+
4px--r-xs
+
+
+
8px--r-sm
+
+
+
12px--r-md
+
+
+
20px--r-lg
+
+
+ +
+
+
--shadow-sm0 1px 2px / 6%
+
+
+
--shadow-md0 4px 10px / 8%
+
+
+
--shadow-lg0 12px 28px / 12%
+
+
+
+ + +
+

Core components

+
+ +
+
<Button />
+
+ + + + +
+
+ +
+
<Input />
+
+ + +
+
+ +
+
<Checkbox />
+
+ + +
+
+ +
+
<Badge />
+
+ Draft + In review + Done + Overdue +
+
+
+ +
+ + diff --git a/06-component-variants.html b/06-component-variants.html new file mode 100644 index 0000000..75debcf --- /dev/null +++ b/06-component-variants.html @@ -0,0 +1,606 @@ + + + + + + +Acme — Card Variant Matrix + + + +
+ +
+

Card variant matrix

+

Six structural treatments of the Acme <Card /> component. Adjust density and emphasis with the controls, hover a variant to see its prop combo.

+
+ + +
+
+ Padding + + 20px +
+ +
+ Border +
+ + + +
+
+ +
+ +
+
+ + +
+ + +
+ A · Flat +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: dense lists on tinted backgrounds

+
+ + +
+ B · Outlined +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: default content cards on ivory

+
+ + +
+ C · Elevated +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: draggable items, popovers

+
+ + +
+ D · Accent stripe +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: pinned or priority items

+
+ + +
+ E · Inset +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: nested cards inside white panels

+
+ + +
+ F · Horizontal +
+
+
WP
+
+

Weekly planning

+

12 tasks · due Friday

+
+
+
+ Q2 + Roadmap +
+ +
+

best for: compact row lists, sidebars

+
+ +
+ + +
+
JSX — hover a variant above
+
// hover a card to preview its props
+
+ +
+ + + + diff --git a/07-prototype-animation.html b/07-prototype-animation.html new file mode 100644 index 0000000..b63929e --- /dev/null +++ b/07-prototype-animation.html @@ -0,0 +1,456 @@ + + + + + + +Acme — Task completed micro-interaction + + + +
+ +
+
Acme / prototype / micro-interaction
+

Task completed

+

+ 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. +

+
+ +
+ +
+
+
+ +
+ Send weekly digest + Fri +
+
+
+
+
+
+
+
click to toggle
+
+ + + +
+ +
+

Keyframes

+
+
fill0ms
+
check80ms
+
strike120ms
+
confetti200ms
+
collapse600ms
+
+
+ +
+

Copy-paste CSS

+
/* circle: clay flash, settle to olive with spring overshoot */
+.task.done .check {
+  animation: settle 380ms cubic-bezier(.34,1.56,.64,1) forwards;
+}
+@keyframes settle {
+  0%   { transform: scale(.8);  background: #D97757; }
+  55%  { transform: scale(1.18); }
+  100% { transform: scale(1);   background: #788C5D; }
+}
+
+/* checkmark draws via stroke-dashoffset, 80ms delay */
+.check path        { stroke-dasharray: 20; stroke-dashoffset: 20;
+                     transition: stroke-dashoffset 220ms var(--ease) 80ms; }
+.task.done path    { stroke-dashoffset: 0; }
+
+/* strikethrough grows left → right */
+.label::after      { width: 0; transition: width 240ms var(--ease) 120ms; }
+.task.done .label::after { width: 100%; }
+
+/* row steps back after the celebration */
+.task.done         { max-height: 44px; opacity: .6;
+                     transition-delay: 600ms; }
+
+ +
+ + + + diff --git a/08-prototype-interaction.html b/08-prototype-interaction.html new file mode 100644 index 0000000..fb8fde0 --- /dev/null +++ b/08-prototype-interaction.html @@ -0,0 +1,397 @@ + + + + + + +Acme — Sidebar drag-to-reorder + + + +
+ +
+
Acme / prototype / interaction
+

Sidebar drag-to-reorder

+

+ Throwaway HTML so we can feel the reorder before porting it to + React. Native dragstart / dragover / drop, + ~40 lines of JS, no libraries. Grab a row by the dots and move it. +

+
+ +
+ +
+ +

Order persists in the DOM only — refresh to reset.

+
+ +
+
+

What you're feeling

+

Design decisions baked into this prototype, so you can push back on them.

+
    +
  • + Drop indicator snaps to the nearest gap, not the raw cursor Y. + It only moves when you cross a row's midpoint — feels more decisive, + less jittery. +
  • +
  • + Dragged row stays in place at 35% opacity with a 2° tilt. + Keeping the ghost in the list preserves your sense of where you + started; the tilt reads as "lifted." +
  • +
  • + Grip dots are the affordance, but the whole row is draggable. + Dots darken on hover to teach the gesture without forcing a + tiny hit target. +
  • +
  • + No auto-scroll, no drop animation. Left out on purpose so the + core feel is easy to judge — say the word and we add them next. +
  • +
+
+ +
+

Open questions

+
    +
  1. Should Trash (and maybe Archive) be pinned to the bottom and excluded from reordering?
  2. +
  3. Do we want rows to slide to their new slot on drop, or is the instant snap acceptable?
  4. +
  5. Keyboard path: is Alt + Arrow to move the focused row enough for the first ship?
  6. +
+
+
+ +
+
+ + + + diff --git a/09-slide-deck.html b/09-slide-deck.html new file mode 100644 index 0000000..61fd981 --- /dev/null +++ b/09-slide-deck.html @@ -0,0 +1,593 @@ + + + + + + +Platform Eng — Week of Mar 10 + + + + + +
+
+ +

Platform Eng
— Week of Mar 10

+

+ What shipped, what's moving, and one decision we need from the + room before the Acme 2.4 cut. +

+ +
+
+ + +
+
+
Shipped this week
+

Three things out the door

+
    +
  • + +
    +
    Bulk task import
    +
    + CSV and JSON uploads now land straight into a board with + column mapping — no more paste-and-pray. +
    +
    + #4211 +
  • +
  • + +
    +
    Webhook retries v2
    +
    + Exponential backoff with jitter; dead-letter queue surfaces + in the workspace admin panel. +
    +
    + #4188 #4203 +
  • +
  • + +
    +
    Postgres 16 migration
    +
    + All read replicas cut over Tuesday night; zero customer-facing + downtime, ~9% faster aggregate queries. +
    +
    + #4179 +
  • +
+
+
+ + +
+
+
In progress
+

Carrying into next week

+
    +
  • +
    + Recurring tasks engine + ~70% +
    +
    +

    + Scheduler and RRULE parsing are done; remaining work is the + timezone edge cases and the "skip holidays" toggle. +

    +
  • +
  • +
    + Audit log export + ~35% +
    +
    +

    + Streaming NDJSON endpoint is up behind a flag; still wiring + the S3 destination picker and retention policy UI. +

    +
  • +
+
+
+ + +
+
+
Metrics
+

Numbers we watch

+ +
+
+
API p95 latency
+
184ms
+
↓ 12% wk/wk
+
+
+
Background job error rate
+
0.21%
+
↓ 0.08pp
+
+
+ +
+ +
+ p95 latency, trailing 8 days — lower is better +
+
+
+
+ + +
+
+
Decision needed
+

One call to make

+ +
+

+ Do we ship recurring tasks behind a workspace flag in 2.4, or + hold one more week for the timezone fixes? +

+

+ 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. +

+
+ +
+ A — Flag it, ship Friday + B — Hold for 2.5 +
+
+
+ + +
+
+
Next week
+

On deck

+
    +
  • Finish recurring tasks — timezone matrix tests, then dogfood on the internal ops board.
  • +
  • Audit log export to private beta — three workspaces lined up.
  • +
  • Start scoping rate-limit headers for the public API; RFC draft by Thursday.
  • +
+
+ Questions → drop them in the platform channel or grab anyone after standup. +
+
+
+ + +
1 / 6
+ + + + diff --git a/10-svg-illustrations.html b/10-svg-illustrations.html new file mode 100644 index 0000000..c408408 --- /dev/null +++ b/10-svg-illustrations.html @@ -0,0 +1,493 @@ + + + + + + +Background jobs — header illustrations + + + +
+ +
+

Background jobs — header illustrations

+

+ Three 720×320 hand-drawn SVGs for the Acme docs section on + background jobs. Flat fills, 1.5–2px strokes, palette-locked. Each + exports standalone via the button below it. +

+
720 × 320 · inline SVG · no external assets
+
+ + +
+
+ + + + + + + + + + + + queue + + + + + job 5 + + + job 4 + + + job 3 + + + job 2 + + + + job 1 + + + + + + + + worker + pool=4 + + + next to run + Jobs are pulled FIFO; the worker leases one at a time. + +
+
+
+
Queue
+
For "How jobs are picked up" — intro page header.
+
+ +
+
+ + +
+
+ + + + + + + + + + t = 0 + time → + + + + + + + try 1 + + + + + + try 2 + + + + + + try 3 + + + + + try 4 + + + + +1s + + + +2s + + + +4s + + + Each failure waits twice as long before re-queuing; jitter not pictured. + +
+
+
+
Retry with backoff
+
For "Handling failures" — retry policy header.
+
+ +
+
+ + +
+
+ + + + + + + + + + + + + enqueue + batch() + + + + + + + + + + + shard 0 + + + shard 1 + + + shard 2 + + + shard 3 + + + + + + + + + + + merge + await all + + + fan-out + fan-in + Parent job spawns N children, then blocks on a completion barrier. + +
+
+
+
Fan-out / fan-in
+
For "Batch and parallel work" — fan-out pattern header.
+
+ +
+
+ + +
+

Palette & rules

+ +
+
+
+
+
ivory
+
#FAF9F5
+
+
+
+
+
+
slate
+
#141413
+
+
+
+
+
+
clay
+
#D97757
+
+
+
+
+
+
olive
+
#788C5D
+
+
+
+
+
+
oat
+
#E3DACC
+
+
+
+
+
+
gray-150
+
#F0EEE6
+
+
+
+
+
+
gray-300
+
#D1CFC5
+
+
+
+
+
+
gray-500
+
#87867F
+
+
+
+ +
    +
  • Strokes are 1.5px for neutral boxes, 2px for emphasised containers.
  • +
  • All rectangles use rx="10"; no drop shadows or gradients.
  • +
  • Labels inside boxes are 11px mono; annotations outside are 12px sans, gray-500.
  • +
  • Clay marks "the thing in focus"; olive marks "success / done".
  • +
  • Each SVG carries its own <style> block so the download stands alone.
  • +
+
+ +
+ + + + diff --git a/11-status-report.html b/11-status-report.html new file mode 100644 index 0000000..7f5ed34 --- /dev/null +++ b/11-status-report.html @@ -0,0 +1,529 @@ + + + + + + +Acme — Engineering Status — Week 11 + + + +
+ +
+
+

Engineering Status — Week 11

+ auto-generated +
+
+ Mar 10 – Mar 16, 2025  ·  + acme/app @ main +
+
+ + +
+
+
+
14
+
PRs merged
+
+3 vs wk10
+
+
+
6
+
Deploys
+
±0
+
+
+
1
+
Incidents
+
SEV-2 · 47m
+
+
+
3
+
Flaky tests fixed
+
suite now 99.1%
+
+
+
+ + +
+

Highlights

+
+
    +
  • + Bulk task editing shipped to 100%. The multi-select + toolbar landed behind a flag on Monday and ramped to all workspaces by + Thursday with no error-rate regression. +
  • +
  • + Sync API p95 down 38%. Replacing per-task auth checks + with a scoped batch lookup cut the hot path from 410ms to 255ms on the + staging load test. +
  • +
  • + One SEV-2 on Wednesday — a config rollout pushed + a bad connection-pool limit to the sync workers. Mitigated in 47 minutes; + full postmortem linked below. +
  • +
+
+ + +
+

Shipped

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PRTitleAuthorRisk
#4871Bulk edit toolbar: selection model + keyboard shortcutsMira OkaforMed
#4874Batch auth lookup for /v2/sync hot pathDevon ParkMed
#4878Fix race in attachment uploader retry loopSam ReyesLow
#4879Migrate reminder scheduler to idempotent job keysPriya AnandHigh
#4882Board view: collapse empty swimlanes by defaultMira OkaforLow
#4885Quarantine 3 flaky webhook integration testsJules TanLow
#4888Connection pool limits configurable per worker tierDevon ParkMed
#4891Dark mode pass on settings & billing panelsNoor HalabiLow
+
+ + +
+

Velocity

+
+
+ + + + + + + + + 0 + 1 + 2 + 3 + + + + + + + + + + + + + + + + + + + 2 + 3 + 1 + 4 + 2 + 1 + 1 + + + Mon + Tue + Wed + Thu + Fri + Sat + Sun + +
+ PRs merged per day. Thursday spike is the bulk-edit feature train (4 PRs landed together). +
+
+
+ + +
+

Carryover

+
+
+
+ In review +
+ Workspace export to CSV — waiting on pagination review. + · Sam Reyes +
+
+
+ Blocked +
+ SSO group mapping — blocked on staging IdP credentials from IT. + · Priya Anand +
+
+
+ Slipped +
+ Mobile push reliability dashboard — deprioritized for incident follow-up. + · Devon Park +
+
+
+
+ +
+ Sources: git log main..HEAD · CI dashboard · deploy log +  —  generated Mar 16 2025 18:02 +
+ +
+ + diff --git a/12-incident-report.html b/12-incident-report.html new file mode 100644 index 0000000..ff06f1d --- /dev/null +++ b/12-incident-report.html @@ -0,0 +1,597 @@ + + + + + + +INC-2025-0412 — Elevated 502s on task sync + + + + + + +
+ +
+
INC-2025-0412
+

Elevated 502s on task sync

+
+ SEV-2 + Resolved + Duration 47 min + Detected Apr 12 · 14:07 + Owner Devon Park +
+
+ +
+
TL;DR
+

+ A config rollout lowered the database connection-pool limit on the + sync-worker 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. +

+
+ + +
+

Timeline

+
+
+ +
+ + 14:02 +
+ Config change cfg-9a12 promoted to + production via the standard rollout pipeline. +
+
+ +
+ + 14:06 +
+ Impact starts. Sync workers begin queueing on pool + checkout; p95 latency climbs past 4s and the load balancer starts + returning 502s. +
+
+ +
+ + 14:07 +
+ Alert fires: sync_5xx_rate > 2% for 60s. + On-call (Devon) acknowledges. +
+
+ +
+ + 14:18 +
+ Initial hypothesis is a bad deploy of the API service; last two + application deploys are rolled back with no effect. +
+
+ +
+ + 14:31 +
+ Mira joins and notices pool-wait saturation in the worker dashboard. + Investigation pivots to infra config rather than app code. +
+
+ +
+ + 14:44 +
+ Mitigated. cfg-9a12 + reverted; worker fleet cycled. 5xx rate drops below 0.2% within + three minutes. +
+
+ +
+ + 14:49 +
+ Monitors green for 5 minutes. Incident declared resolved; status + page updated. +
+
+ +
+
+ + +
+

Root cause

+
+

+ PR #4888 made connection-pool limits + configurable per worker tier. The default for the new + sync-worker 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. +

+

+ Because config rollouts and code deploys go through separate pipelines, + the on-call's first instinct — rolling back the most recent + application deploys — had no effect and cost roughly 13 minutes of + diagnosis time. +

+ +
+ infra/config/workers.yaml +
pool:
+
global_max_connections: 64
+
tiers:
+
- sync-worker: { max_connections: 64 }
+
+ sync-worker: { max_connections: 8 } # debug value, do not ship
+
webhook-worker: { max_connections: 32 }
+
+
+ + +
+

Impact

+
+ + + + + + + + + + + + + + + + + + + + + +
Requests failed (502)~41,200
Peak error rate21.4%
Users affected~2,300 workspaces
Data lossNone — clients retried
SLA breachNo (within monthly budget)
+
+ + +
+

Action items

+
+
+ +
+ + DP + Revert cfg-9a12 and restore pool limit to 64 + Apr 12 +
+ +
+ + MO + Add config-linter range check for max_connections (warn < 32) + Apr 18 +
+ +
+ + SR + Surface “recent config rollouts” alongside deploys in the on-call dashboard + Apr 25 +
+ +
+ + PA + Canary config changes to one worker AZ for 10 min before fleet-wide promote + May 02 +
+ +
+
+ +
+ Authored from on-call notes + alert history · reviewed by Devon Park, Mira Okafor +
+ +
+ + diff --git a/13-flowchart-diagram.html b/13-flowchart-diagram.html new file mode 100644 index 0000000..8967f6e --- /dev/null +++ b/13-flowchart-diagram.html @@ -0,0 +1,396 @@ + + + + + + +Deploy pipeline — annotated flowchart + + + +
+ +
+
Illustrations & Diagrams · Flowchart
+

What happens when you git push

+

+ The deploy pipeline for acme/web, drawn from .github/workflows/ and the + Argo manifests. Click any step to see what runs, how long it usually takes, and where it can short-circuit. +

+
+ +
+ +
+
+ + + + + + + + + + + + + + + + + + + pass + + fail → status + + + + healthy + + canary fails + + + + + + + git push main + + + + + CI · lint + typecheck + ~2 min · ci.yml + + + + + Unit + integration tests + ~6 min · 3 shards + + + + + pass? + + + + + Post failure status + slack #deploys + + + + + Build + push image + ghcr.io/acme/web + + + + + Argo canary 5% + 10 min soak + + + + + SLO ok? + + + + + Promote 25 → 50 → 100% + ~8 min + + + + + Auto-rollback + revert image tag + + + + + Smoke tests in prod + playwright · 90s + + + + + ✅ Deploy complete + + +
+ +
+ process step + decision + terminal success + failure path +
+
+ + + +
+
+ + + + diff --git a/14-research-feature-explainer.html b/14-research-feature-explainer.html new file mode 100644 index 0000000..aea9deb --- /dev/null +++ b/14-research-feature-explainer.html @@ -0,0 +1,382 @@ + + + + + + +How rate limiting works in acme/api + + + +
+ + + + + +
+
+
Research & Learning · feature summary
+

How rate limiting works in acme/api

+
+ TL;DR — Every request passes through rateLimit() middleware, which resolves the + caller to a bucket key, fetches a token-bucket from Redis, and either consumes one token or + returns 429. Limits are declared per-route in config/limits.yaml; routes + without an entry inherit the default tier (100 req/min per API key). +
+
+ +

The request path, step by step

+

Expand each step to see what runs and where it lives. The whole path is ~40 lines and adds about + 0.4 ms p50 to every request.

+ +
+ 1 · Identify the caller middleware/ratelimit.ts:21 +
+

The middleware first reduces the request to a bucketKey: API key if an + Authorization header is present, otherwise the client IP (via the + x-forwarded-for chain, trusting only our own LB). Anonymous IP traffic gets a much + lower default tier.

+
+
+ +
+ 2 · Look up the bucket lib/tokenBucket.ts:9 +
+

The route name plus bucket key map to a Redis hash (rl:{route}:{key}) holding + tokens and updatedAt. If the key is missing it's created lazily at full + capacity — there's no warm-up.

+
+
+ +
+ 3 · Refill and consume lib/tokenBucket.ts:31 +
+

Refill is computed from elapsed time (rate × Δt, capped at burst), then + one token is subtracted. The whole read-modify-write runs as a single Lua script so concurrent + requests can't double-spend.

+
+
+ +
+ 4 · Reject when empty middleware/ratelimit.ts:48 +
+

If the script returns tokens < 0 the middleware short-circuits with + 429 Too Many Requests and sets Retry-After to the seconds until one token + refills. Successful responses always carry X-RateLimit-Remaining.

+
+
+ +

Configuring a limit on your route

+

You don't touch the middleware. Add an entry to config/limits.yaml keyed by route name, + and (optionally) tag the route so the middleware can find it.

+ +
+
+ + + +
+
# config/limits.yaml
+default:
+  rate: 100/min
+  burst: 120
+
+search.query:
+  rate: 20/min
+  burst: 40
+  key: api_key        # or: ip
+
// routes/search.ts
+router.post(
+  "/search",
+  rateLimit("search.query"),
+  handler,
+);
+
HTTP/1.1 429 Too Many Requests
+Retry-After: 17
+X-RateLimit-Limit: 20
+X-RateLimit-Remaining: 0
+
+{ "error": "rate_limited", "retry_after": 17 }
+
+ +
+ +
If you only need the default tier, you don't need a YAML entry at all — just wrap the handler in + rateLimit() with no argument. The route name is inferred from the path.
+
+ +

Gotchas worth knowing

+
    +
  • Limits are per-process in dev. The Redis client falls back to an + in-memory map when REDIS_URL is unset, so local testing won't reflect real cluster + behaviour.
  • +
  • Burst ≠ rate. burst is the bucket capacity; a caller + idle for a minute can fire burst requests instantly even if rate is low.
  • +
  • Streaming responses count once. The token is consumed at request start; a 30-second SSE + stream still costs one token.
  • +
+ +

FAQ

+
+
How do I exempt internal traffic?
+
Set x-acme-internal: 1 from the caller; the middleware checks it against the + mTLS peer name and skips the bucket entirely.
+ +
Where do I see who's getting limited?
+
Every 429 emits a ratelimit.rejected metric tagged with route and key + type. There's a Grafana panel under API → Health.
+ +
Can a single user have a higher limit?
+
Yes — add their API key under overrides: in the YAML. Overrides are reloaded without + a deploy.
+
+
+ +
+ + + + diff --git a/15-research-concept-explainer.html b/15-research-concept-explainer.html new file mode 100644 index 0000000..192869d --- /dev/null +++ b/15-research-concept-explainer.html @@ -0,0 +1,369 @@ + + + + + + +Consistent hashing — an interactive explainer + + + +
+ +
+
Research & Learning · concept explainer
+

Consistent hashing, in one ring

+

+ You have K keys spread across N cache servers. A server dies, or you add one. How many + keys have to move? With naive hash(key) mod N the answer is "almost all of them." Consistent + hashing gets it down to roughly K / N. Here's why. +

+ +

The trick: hash onto a circle, not a line

+

+ Map both nodes and keys onto the same + ring (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 arc reassign — to the next node round — and everything else + stays put. +

+ +
+
+ +
+
+ + + 4 +
+
+ + + 32 +
+
+ + + +
+
+ 4 nodes · 32 keys · moved on last change +
+
+
+
+

+ 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. +

+ +

Versus mod N

+ + + + + + + + +
hash mod Nconsistent hashing
Keys moved when N→N+1~ (N−1)/N of all keys~ 1/(N+1)
Hot-spot riskeven by constructionuneven — fix with virtual nodes
Lookup costO(1)O(log N) (binary search on ring)
Used byarray sharding, simple LBDynamo, Cassandra, Memcached clients, Envoy
+ +

Where you'll meet it

+

+ Any time you're spreading state across a pool that changes size: cache fleets, partitioned queues, object + storage, request routing with sticky sessions. The + virtual-node 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. +

+
+ + + + +
+ + + + diff --git a/16-implementation-plan.html b/16-implementation-plan.html new file mode 100644 index 0000000..483a826 --- /dev/null +++ b/16-implementation-plan.html @@ -0,0 +1,703 @@ + + + + + + + Implementation plan — Comment threads on task cards + + + +
+ +
+
Implementation plan · Acme web client
+

Comment threads on task cards

+
+ Prompt + 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. +
+
+ + + +
+
Effort
~2 weeks
+
Surfaces touched
3 packages
+
New tables
2
+
Feature flag
task_comments_v1
+
+ + + +
+
01

Milestones

+

Ship in four slices, each independently reviewable and each behind the flag. Nothing is user-visible until slice 4.

+ +
+
+
Week 1 · Mon–Tue
+
+
+

Schema & API contract

+

New comments and comment_reads tables, migrations, and the tRPC router stubs. No UI. Contract reviewed before anything else lands.

+
packages/dbpackages/apimigration 0042
+
+
+ +
+
Week 1 · Wed–Fri
+
+
+

Thread component & composer

+

Static <CommentThread> rendered from fixtures. Optimistic insert on submit, rollback on failure, one level of nesting only.

+
apps/webstorybook
+
+
+ +
+
Week 2 · Mon–Wed
+
+
+

Realtime fan-out & unread state

+

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.

+
packages/realtimeapps/web
+
+
+ +
+
Week 2 · Thu–Fri
+
+
+

Notifications, flag ramp, docs

+

Mention detection → notification row, email digest fallback, ramp task_comments_v1 to internal, then 10% → 100% over three days.

+
packages/notifygrowthbook
+
+
+
+
+ + + +
+
02

Data flow

+

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.

+ +
+ + + + + + + + + + + + + + <CommentComposer> + apps/web + + + React Query cache + optimistic insert + + + comments.create + tRPC · packages/api + + + comments table + postgres · packages/db + + + realtime channel + task:{id}:comments + + + Other viewers + subscribed cards + + + notify worker + @mentions → queue + + + + + + + + + + + + + + + + + + submit (id=temp) + mutate + INSERT + read cursor + enqueue + broadcast row + live append + reconcile temp id → real id + + +

Solid = request/response path. Dashed clay = realtime fan-out. The composer never waits on the dashed path.

+
+
+ + + +
+
03

Mockups

+

Not pixel-final — just enough that the reviewer and I agree on nesting depth, composer placement, and what the sidebar digest looks like.

+ +
+
+
A · Thread inside an open task card
+
+
+
Ship onboarding empty-state rewrite
+
BIR-1142 · Assigned to Priya · Due Fri
+
+
+
+
JM
+
+
Jonah M. 2h ago
+
Should the illustration swap when the workspace already has one project? Feels odd to show the "start here" art twice.
+ +
+
+
+
PS
+
+
Priya S. 40m ago
+
Good catch — I'll gate it on projects.count > 0 and fall back to the minimal variant.
+
+
+
+
Add a comment…
+
Post
+
+
+
+
+
+ +
+
B · Sidebar unread digest
+
+
+
+
JM
+
Jonah commented on BIR-1142 — "Should the illustration swap when…"
+
+
+
AK
+
Aiko mentioned you on BIR-1098 — "@priya can you confirm the copy here?"
+
+
+
RW
+
Rowan replied on BIR-0971 — "Merged, thanks for the quick turnaround."
+
+
+
+
+
+
+ + + +
+
04

Key code

+

The two pieces most likely to be done wrong: the migration (soft deletes, read cursors) and the optimistic mutation (temp-id reconciliation).

+ +
+
+
packages/db/migrations/0042_comments.sql
+
create table comments (
+  id          uuid primary key default gen_random_uuid(),
+  task_id     uuid not null references tasks(id),
+  parent_id   uuid references comments(id),   -- one level only,
+                                                -- enforced in API
+  author_id   uuid not null references users(id),
+  body        text not null,
+  created_at  timestamptz not null default now(),
+  deleted_at  timestamptz                       -- soft delete
+);
+
+create table comment_reads (
+  task_id    uuid not null references tasks(id),
+  user_id    uuid not null references users(id),
+  read_up_to timestamptz not null,
+  primary key (task_id, user_id)
+);
+
+create index comments_task_created
+  on comments (task_id, created_at);
+
+ +
+
apps/web/hooks/useAddComment.ts
+
export function useAddComment(taskId: string) {
+  const qc = useQueryClient();
+  return trpc.comments.create.useMutation({
+    onMutate: async (input) => {
+      const temp = { ...input, id: `temp-${nanoid()}`,
+                     createdAt: new Date(), pending: true };
+      qc.setQueryData(key(taskId), (prev) =>
+        [...(prev ?? []), temp]);
+      return { tempId: temp.id };
+    },
+    onSuccess: (row, _v, ctx) => {
+      // reconcile temp id → real id so the
+      // realtime append doesn't duplicate it
+      qc.setQueryData(key(taskId), (prev) =>
+        prev.map((c) => c.id === ctx.tempId ? row : c));
+    },
+    onError: (_e, _v, ctx) => {
+      qc.setQueryData(key(taskId), (prev) =>
+        prev.filter((c) => c.id !== ctx.tempId));
+    },
+  });
+}
+
+
+
+ + + +
+
05

Risks & mitigations

+
+
+
Risk
+
Sev
+
Mitigation
+
+
+
Realtime duplicate: socket append races with the HTTP response and the temp-id reconcile.
+
HIGH
+
Dedupe on server-assigned id in the cache updater; socket payload carries the real id, temp rows are filtered on reconcile.
+
+
+
Unread counts go stale when a user reads the thread on another device.
+
MED
+
Broadcast comment_reads upserts on the same channel; client treats its own cursor as max(local, remote).
+
+
+
Mention detection false-positives on pasted markdown (@media, @2x).
+
LOW
+
Resolve mentions against workspace members only, at write time, and store the resolved user ids — never re-parse on read.
+
+
+
+ + + +
+
06

Open questions

+
+
+
Do we allow editing, or only delete-and-repost?
+
Editing needs an edited_at column and an "edited" affordance. Delete-and-repost is simpler but loses the reply anchor. Leaning toward delete-only for v1.
+
Decide with · design, before slice 2
+
+
+
Email digest cadence when a user has the app closed
+
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.
+
Decide with · platform, before slice 4
+
+
+
+ +
+ + diff --git a/17-pr-writeup.html b/17-pr-writeup.html new file mode 100644 index 0000000..34c60d0 --- /dev/null +++ b/17-pr-writeup.html @@ -0,0 +1,596 @@ + + + + + + + PR #312 — Move notification delivery onto a queue + + + +
+ +
+
Pull request · Acme
+

#312 — Move notification delivery onto a queue

+
+ 9 files + +418 / −190 + branch notify-queuemain + author @priya +
+
+ Prompt + Write up PR #312 for my reviewers. Explain the motivation, walk them + through the change file by file with the why for each, show + before/after behavior, and tell them exactly where to focus. They + haven't touched the notification code in six months. +
+
+ +
+
+ +
+
TL;DR
+

Notification sends were happening inline in the request path. Under load they added 200–800 ms to mutation latency and silently dropped emails when the SMTP pool was exhausted. This PR moves delivery onto the existing pg-boss queue so the API returns immediately and failed sends retry with backoff.

+
+ + + +
+

Why

+

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.

+ +
+
+
Before
+
    +
  • Sends run inline in the mutation handler
  • +
  • SMTP timeout = 500 error for the comment
  • +
  • No retries — a dropped email is gone
  • +
  • p99 on comments.create: 1.4 s
  • +
+
+
+
After
+
    +
  • Handler enqueues one job per recipient, returns
  • +
  • Worker retries 3× with exponential backoff
  • +
  • Dead-letter table for inspection after exhaustion
  • +
  • p99 on comments.create: 180 ms (staging)
  • +
+
+
+
+ + + +
+

File-by-file

+

Ordered for reading, not alphabetically. Start at the worker — it's the new thing — then the enqueue call site, then the plumbing.

+ +
+ +
+ + + packages/notify/src/worker.ts + new + +126 + +
+

The heart of the PR. A pg-boss subscriber that pulls notify.deliver 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.

+
boss.work('notify.deliver', { batchSize: 20 }, async (jobs) => {
+  for (const job of jobs) {
+    const { userId, event, channel } = job.data;
+    const prefs = await getPrefs(userId);
+    if (!prefs[channel]) return;          // user muted this channel
+
+    try {
+      await adapters[channel].send(userId, event);
+    } catch (err) {
+      if (job.retryCount >= MAX_RETRY[channel]) {
+        await deadLetter(job, err);         // don't throw — ack & park
+        return;
+      }
+      throw err;                            // pg-boss reschedules
+    }
+  }
+});
+
+
+ +
+ + + packages/api/src/routers/comments.ts + mod + +14 −62 + +
+

Where the win shows up. The mutation used to call sendEmail, sendSlack, and createInApp directly. Now it inserts the comment, computes recipients, and enqueues. The try/catch soup is gone.

+
  const comment = await db.comments.insert(input);
+  const recipients = await resolveWatchers(input.taskId, input.mentions);
+
+  for (const r of recipients) {
+    await sendEmail(r, comment);       // blocked the response
+    await sendSlack(r, comment);
+  }
+  await boss.insert(recipients.flatMap((r) =>
+    CHANNELS.map((ch) => ({
+      name: 'notify.deliver',
+      data: { userId: r.id, channel: ch, event: toEvent(comment) },
+      singletonKey: `${comment.id}:${r.id}:${ch}`,  // idempotent
+    }))));
+  return comment;
+
+
+ +
+ + + packages/db/migrations/0051_dead_letter.sql + new + +22 + +
+

Table for jobs that exhaust their retries. Deliberately not 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.

+
+
+ +
+ + + packages/notify/src/adapters/{email,slack,inapp}.ts + mod + +88 −74 + +
+

Mostly moves. Each adapter now implements a shared Adapter interface and throws a typed RetryableError or PermanentError 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.

+
+
+ +
+ + + apps/worker/src/index.ts, infra/fly.toml + mod + +31 −4 + +
+

Registers the new subscriber in the existing worker process and bumps its concurrency from 5 → 20. No new deploy unit.

+
+
+ +
+ + + packages/notify/src/__tests__/worker.test.ts + new + +137 + +
+

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.

+
+
+ +
+
+ + + +
+

Where to focus your review

+
+
+
1
+
+
The retry / dead-letter boundary
+
worker.ts:31–44. I catch, check retryCount, 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.
+
+
+
+
2
+
+
The singleton key
+
comments.ts:28. ${commentId}:${userId}:${channel} should make re-enqueues idempotent if the API handler retries. Sanity-check that this can't collide across tasks.
+
+
+
+
3
+
+
What I deliberately did not do
+
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.
+
+
+
+
+ + + +
+

Test plan

+
+
Unit: retry → dead-letter path, channel mute, singleton dedupe
packages/notify — 14 cases, real pg-boss on test db
+
Integration: create comment with 3 watchers, assert 9 jobs enqueued and drained
+
Staging load: 500 rps on comments.create for 10 min, p99 = 180 ms
was 1.4 s before — dashboard linked in the PR description
+
Manual: kill SMTP mid-burst, confirm jobs land in dead-letter and nothing 500s
will do during the 10% ramp
+
+
+ + + +
+

Rollout

+

Behind notify_queue_v2. The old inline path stays in the codebase, dead but dormant, for one release in case we need to flip back.

+
+
Day 0
internal
Acme team only. Watch dead-letter table + worker error rate.
+
Day 2
10%
Random sample. Alert if dead-letter rate > 0.5% of sends.
+
Day 4
100%
Ramp fully, delete the inline path in a follow-up PR next week.
+
+
+ +
+ + +
+ +
+ + diff --git a/18-editor-triage-board.html b/18-editor-triage-board.html new file mode 100644 index 0000000..ae4dbb0 --- /dev/null +++ b/18-editor-triage-board.html @@ -0,0 +1,574 @@ + + + + + + +Acme — Cycle 14 triage + + + +
+ +
+
Acme / editor / triage
+

Cycle 14 triage

+

+ 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. +

+
drag tickets between columns  ·  click a tag to filter
+
+ +
+
+
+ + + +
+ +
+ +
+ + + + diff --git a/19-editor-feature-flags.html b/19-editor-feature-flags.html new file mode 100644 index 0000000..15be394 --- /dev/null +++ b/19-editor-feature-flags.html @@ -0,0 +1,664 @@ + + + + + + +Acme — flags.production.json + + + +
+ +
+
Acme / editor / feature-flags
+

flags.production.json

+

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.

+
+ +
+ + +
+ + +
+ +
+ + + + diff --git a/20-editor-prompt-tuner.html b/20-editor-prompt-tuner.html new file mode 100644 index 0000000..7a27d03 --- /dev/null +++ b/20-editor-prompt-tuner.html @@ -0,0 +1,723 @@ + + + + + + +Acme — Support reply prompt tuner + + + +
+ +
+
Acme / editor / prompt-tuner
+

Support reply draft prompt

+

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.

+
+ +
+ + +
+
slots use {{double_brace}} syntax
+
+ +
+ + +
+
+ Template +
+ 0 chars · ~0 tokens +
+
+
+
Available slots
+
+
+
+ + +
+
Live preview · 3 sample tickets
+
+
+ +
+ +
Highlighted slots fill from each sample's ticket fields. Anything underlined in dashed clay isn't a known field and will pass through unfilled.
+ +
+ + + + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..91bac7d --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -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 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..02fe842 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..aace658 --- /dev/null +++ b/README.md @@ -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). diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..c0781a2 --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`ThariqS/html-effectiveness` +- 原始仓库:https://github.com/ThariqS/html-effectiveness +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..70e008f --- /dev/null +++ b/SECURITY.md @@ -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. diff --git a/index.html b/index.html new file mode 100644 index 0000000..62f8a68 --- /dev/null +++ b/index.html @@ -0,0 +1,824 @@ + + + + + + +The unreasonable effectiveness of HTML — examples + + + +
+ +
+
+
+
Companion to the blog post
+

The unreasonable effectiveness of HTML

+

+ Twenty self-contained .html 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. +

+ +
+ +
+
+ + +
+
01

Exploration & Planning

3 demos
+

+ 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. +

+ +
+ + +
+
02

Code Review & Understanding

3 demos
+

+ 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. +

+ +
+ + +
+
03

Design

2 demos
+

+ HTML is 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. +

+ +
+ + +
+
04

Prototyping

2 demos
+

+ 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. +

+ +
+ + +
+
05

Illustrations & Diagrams

2 demos
+

+ 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. +

+ +
+ + +
+
06

Decks

1 demo
+

+ A handful of <section> 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. +

+ +
+ + +
+
07

Research & Learning

2 demos
+

+ 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. +

+ +
+ + +
+
08

Reports

2 demos
+

+ 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. +

+ +
+ + +
+
09

Custom Editing Interfaces

3 demos
+

+ 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. +

+ +
+ + + +
+ + diff --git a/unknowns/01-blindspot-pass.html b/unknowns/01-blindspot-pass.html new file mode 100644 index 0000000..e7c2d19 --- /dev/null +++ b/unknowns/01-blindspot-pass.html @@ -0,0 +1,679 @@ + + + + + + +Blindspot pass — Know your unknowns + + + +
+ +
+
+
Know your unknowns · Pre-implementation
+ ← All examples +
+

Blindspot pass

+

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.

+ +
+
+ The 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.
+
+
+ +
What Claude produced
+ +
+
+ + blindspot-pass · acme/services/auth · 47 files scanned +
+
+ +

Unknown unknowns: the Acme auth module

+

Scanned services/auth/ (47 files), 214 commits touching it since Jan 2025, 3 migrations in flight, 2 reverted attempts. Seven things you'd have learned the hard way.

+ +
+
+
What you asked for
+

"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.

+
+
+
What you're actually walking into
+

A module mid-migration on session storage, with one provider that quietly bypasses the middleware everyone assumes runs, a reverted previous attempt at exactly this task, and an identity-linking model that isn't visible from the provider interface at all.

+
+
+ +
+ 4 landmines + 2 unwritten conventions + 1 missing concept + 1 reverted attempt at this exact task (PR #2841) +
+ +
The blindspots
+ + +
+
+ Landmine + 01 +

Sessions are double-written — the Redis store you'll find first is the wrong one

+
+

Acme is mid-migration from Postgres-backed sessions to Redis (MIG-118, stalled since March). Every session write currently goes through SessionBridge.write(), which fans out to both RedisSessionStore and the legacy pg_sessions table. The migration was never finished, so reads still come from Postgres.

+
Why it bitesGrepping for "session" surfaces RedisSessionStore 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.
+
+
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.
+ +
+
+ + +
+
+ Landmine + 02 +

The SAML provider — your obvious template — bypasses the auth middleware

+
+

Every provider except SAML mounts under authPipeline, which runs rate limiting, audit logging, and the device-trust check. saml/handler.ts 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 // TODO(priya): fold back into pipeline from 14 months ago.

+
Why it bitesSAML 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.
+
+
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.
+ +
+
+ + +
+
+ History + 03 +

Someone already tried this — and it was reverted for a reason that still holds

+
+

PR #2841 ("Add Okta OIDC provider", Sept 2025) was merged and reverted nine days later. The revert commit explains why: Acme callback URLs must carry a tenant suffix (/auth/callback/:provider/:workspaceSlug) 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 AUTH_STATE_SECRET — was designed in the revert thread but never built.

+
Why it bitesNothing 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.
+
+
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.
+ +
+
+ + +
+
+ Missing concept + 04 +

Identities and accounts are separate things — and the provider interface hides it

+
+

Acme splits users from identities: one account can have several linked identities (email+password, Google, SAML), joined in the identities table. The provider interface only returns a profile; the linking decision happens in identity/linker.ts, keyed on verified email. Providers that can't guarantee verified emails (many SSO IdPs don't) must set requiresManualLink: true or the linker will happily merge two different people who share a corporate alias.

+
Why it bitesImplement 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.
+
+
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.
+ +
+
+ + +
+
+ Landmine + 05 +

Token refresh is behind a flag that's on in dev and off in prod

+
+

Refresh-token rotation is gated by auth.refresh_rotation in flags.yaml. It defaults to true in dev and staging, false in prod (a paused rollout after a clock-skew incident in May). With the flag off, prod sessions live exactly ACCESS_TOKEN_TTL (12h) and then hard-expire — there is no refresh path.

+
Why it bitesAny 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.
+
+
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.
+ +
+
+ + +
+
+ Convention + 06 +

Registering a provider takes three steps, and only two are discoverable

+
+

A provider isn't live when it compiles. It must be (1) registered in providers/registry.ts, (2) inserted into the auth_providers table via a migration — the admin UI reads the DB, not the registry — and (3) added to the fixture list in __tests__/provider-contract.spec.ts, 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.

+
Why it bitesSkip 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.
+
+
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.
+ +
+
+ + +
+
+ Landmine + 07 +

Logout isn't a function call — it's an event, and the review player listens

+
+

Session revocation publishes auth.session.revoked 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. logout.ts handles this for existing providers, but SSO single-logout (SLO) callbacks arrive on a separate path and must publish the event themselves.

+
Why it bitesMiss 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.
+
+
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.
+ +
+
+ + +
+
+
Your improved prompt
+ +
+

Everything above, folded into the prompt you should actually give me

+
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.
+

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.

+
+ +
+
+ + +
+ + + + diff --git a/unknowns/02-color-grading-explainer.html b/unknowns/02-color-grading-explainer.html new file mode 100644 index 0000000..4349a63 --- /dev/null +++ b/unknowns/02-color-grading-explainer.html @@ -0,0 +1,582 @@ + + + + + + +Teach me my unknowns — Know your unknowns + + + +
+ +
+
Know your unknowns · Pre-implementation
+ ← All examples +

Teach me my unknowns

+

Before doing unfamiliar work, ask Claude to teach you the domain's vocabulary — so your next prompt uses the words a professional would.

+ +
+
The prompt
+
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.
+ +
+
+ +
What Claude produced
+ +
+

Color grading in one sitting

+

Everything you need to grade acme-launch-v3.mp4 — 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.

+ + +
The mental model
+
+
1 · IngestFootage arrives "flat" (log) — grey and washed out, on purpose. It preserves detail.
+
2 · CorrectFix exposure & white balance so it looks real. Boring, essential, first.
+
3 · GradeAdd the look: contrast shape, color bias, mood. This is the creative part.
+
4 · MatchMake every shot in the cut agree, so the video feels like one piece.
+
+ + +
The vocabulary ladder
+
+
+
Exposureoverall brightness
+
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.
+
"the interview shots look a half-stop underexposed — bring them up"
+
+
+
White balancecolor temperature
+
Whether white things actually look white. Measured in Kelvin: low = warm/orange (tungsten), high = cool/blue (daylight). Mixed lighting is the classic gotcha.
+
"the office footage reads too warm — cool it toward neutral, protect skin tones"
+
+
+
Contrast curvetone response
+
The S-shaped curve mapping shadows→highlights. Steeper = punchier; flatter = softer, more "filmic". You bend the curve, you don't just crank a slider.
+
"add a gentle S-curve — richer blacks but don't crush shadow detail"
+
+
+
Lift / Gamma / Gain3-way correction
+
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".
+
"push the lift slightly teal and the gain slightly warm — subtle, not music-video"
+
+
+
Saturation vs vibrancecolor intensity
+
Saturation boosts all colors equally (skin goes orange fast). Vibrance boosts only the muted colors and protects skin. Almost always reach for vibrance first.
+
"use vibrance not saturation — the UI screenshots can pop but faces stay natural"
+
+
+
LUTlook-up table
+
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.
+
"apply the Rec.709 conversion LUT first, then a creative LUT at ~60% strength"
+
+
+ + +
Feel it — grade a frame
+
+
+ acme-launch-v3.mp4 · frame 01:47:12 · drag the split to compare +
+ + + +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+
+ + UNGRADED + GRADED +
+ +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+ Corporate clean: slight lift in exposure, near-neutral temperature, modest contrast — trustworthy and bright. The default for product launch videos. + · +
+
+ + +
What good looks like
+
+
    +
  • Skin tones stay believable. Every creative look bends around faces, never through them. If the founder looks seasick or sunburned, the grade is wrong.
  • +
  • Blacks are rich but not crushed. 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.
  • +
  • Whites don't clip. 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.
  • +
  • Shot-to-shot consistency. Cutting from the office interview to the screen capture shouldn't feel like changing channels. Shot matching is half the job.
  • +
  • The look serves the story. A launch video wants clean and confident, not moody thriller. If viewers notice the grade before the product, dial it back.
  • +
  • Check on more than one screen. 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.
  • +
+
+ + +
The payoff
+
+

Prompts you couldn't have written an hour ago

+

An hour ago the best you had was "make the video look nicer." Now you can say:

+
+
"The interview footage was shot in log — apply the camera's Rec.709 conversion LUT first, then grade on top of that, not on the raw flat image."
+
"Give the whole cut a restrained teal-orange look: push the lift slightly teal, gain slightly warm, keep gamma neutral so midtone skin stays natural."
+
"Shot 14 is about 2/3 of a stop underexposed and too warm versus shot 13 — match its exposure and white balance to shot 13, then A/B them at the cut point."
+
"Add a soft S-curve for contrast and lift color intensity with vibrance rather than saturation, so the product UI pops without turning faces orange."
+
+
+
+ + +
+ + + + diff --git a/unknowns/03-design-directions.html b/unknowns/03-design-directions.html new file mode 100644 index 0000000..57b20c4 --- /dev/null +++ b/unknowns/03-design-directions.html @@ -0,0 +1,1050 @@ + + + + + + +Four design directions — Know your unknowns + + + +
+ +
+ ← All examples +
Know your unknowns · Pre-implementation
+

Four design directions

+

+ When you can't describe the design you want, don't describe one — ask for four incompatible ones + rendered live, and let your gut do the specifying. Reacting is easier than imagining. +

+
+
The prompt
+
“I want a review-queue dashboard for Acme but I have no visual taste and don't know what's possible. Make me one HTML page with 4 wildly different design directions so I can react to them.”
+ +
+
+ +
What Claude produced
+ +

+ Same data, four philosophies. Every frame below renders the identical review queue — + five videos awaiting review in Acme — so the only variable is the design. Under each frame, + tap the chips for anything you'd steal or skip; your picks assemble a + reply at the bottom of the page. +

+
+ The shared dataset: 5 pending reviews · 4 reviewers (MK, DR, LT, JB) · statuses: + in review / needs changes / waiting on reviewer / approved w/ notes · + 23 open comments · 14 reviews cleared this week · avg turnaround 6.2h +
+ + +
+
DIRECTION 1 / 4

Dense ops console

+

“Your review queue is an operational system. Show everything, waste nothing, let the anomalies pop.”

+
+
acme.app/ops/review-queue
+
+
+
+ ACME/REVIEW-OPS + TEAM: LAUNCH-CONTENT · UPDATED 14:32 +
+
+
Pending
5 +2 today
+
Avg turnaround
6.2h −0.8h
+
Cleared / 7d
14 on pace
+
Oldest waiting
2d 4h SLA 48h
+
+ + + + + + + + + + + +
IDVideoLenReviewerCmtsStatusAge
VR-2214Q3 launch teaser — v40:47MK · Kowalski12NEEDS CHANGES2h
VR-2213Onboarding walkthrough — final3:12DR · Raman3IN REVIEW5h
VR-2209Customer story: Halvorsen Co.2:05LT · Torres0WAITING1d 6h
VR-2206Homepage hero loop — v90:15MK · Kowalski7APPROVED*1d 8h
VR-2201Podcast clip — ep. 321:28JB · Beck1IN REVIEW2d 4h
+
+
+
+
+ + + +
+
+ + +
+
DIRECTION 2 / 4

Airy editorial cards

+

“Reviewing creative work should feel calm. One number that matters, then the queue as a reading list.”

+
+
acme.app/studio/queue
+
+
+
+
+
5
+
videos waiting on a decision
+
+
14 cleared this week —
your fastest week since May.
+
+
+ MK +
+
Q3 launch teaser — v4
+
0:47 · Maya Kowalski · 12 comments · uploaded 2h ago
+
+
needs changes
+
+
+ DR +
+
Onboarding walkthrough — final
+
3:12 · Dev Raman · 3 comments · uploaded 5h ago
+
+
being reviewed now
+
+
+ LT +
+
Customer story: Halvorsen Co.
+
2:05 · Lena Torres · no comments yet · uploaded yesterday
+
+
waiting on Lena
+
+
+ MK +
+
Homepage hero loop — v9
+
0:15 · Maya Kowalski · 7 comments · uploaded yesterday
+
+
approved, with notes
+
+
+ JB +
+
Podcast clip — ep. 32
+
1:28 · Jonah Beck · 1 comment · uploaded two days ago
+
+
being reviewed now
+
+
+
+
+
+ + + +
+
+ + +
+
DIRECTION 3 / 4

Kanban / timeline hybrid

+

“A review is a thing that moves. Show where each video sits in the pipeline, and how long it's been sitting.”

+
+
acme.app/board/review-queue
+
+
+
+ Review pipeline — Launch content + 14 cleared this week · avg 6.2h to decision +
+
+
+
Waiting / in review3
+
+
Onboarding walkthrough — final
+
DR 3:12 · 5h in 3✎
+
+
+
Customer story: Halvorsen Co.
+
LT 2:05 · 1d 6h in 0✎
+
+
+
Podcast clip — ep. 32
+
JB 1:28 · 2d 4h in 1✎
+
+
+
+
Needs changes1
+
+
Q3 launch teaser — v4
+
MK 0:47 · 2h in 12✎
+
+
+
+
Approved w/ notes1
+
+
Homepage hero loop — v9
+
MK 0:15 · 1d 8h in 7✎
+
+
+
+
Time in queue — red-flagged past 24h
+
+ now + 12h + 24h + 36h + 48h+ + teaser v4 + onboarding + Halvorsen + hero v9 + ep. 32 +
+
+
+
+
+ + + +
+
+ + +
+
DIRECTION 4 / 4

Brutalist mono terminal

+

“No decoration, no chrome. A queue is a list; read it top to bottom and get out.”

+
+
acme.app/q — keyboard-first
+
+
+
acme :: review-queue :: launch-content
+
════════════════════════════════════════════════════════════════════
+
+ PENDING=5 + CLEARED_7D=14 + AVG_TURNAROUND=6.2h + OLDEST=2d4h !SLA +
+ + + + + + + + + + + +
#titlelenrevcmtstatusage
1q3-launch-teaser.v40:47MK12[NEEDS-CHANGES]2h
2onboarding-walkthrough.final3:12DR3[IN-REVIEW]5h
3customer-story.halvorsen2:05LT0[WAITING]1d6h
4homepage-hero-loop.v90:15MK7[APPROVED+NOTES]1d8h
5podcast-clip.ep321:28JB1[IN-REVIEW]2d4h
+
> open 3 — j/k move · o open · a approve · c comment
+
+
+
+
+ + + +
+
+ + +
+
How to react
+

Reply with a direction and your steals

+

+ You don't have to write a design brief — that's the whole point. Pick the direction whose + attitude is right, then name the two or three details worth carrying over from the others. + Tap a direction and the chips above; the reply writes itself. +

+
+ + + + +
+
+

+      
+    
+

+ Paste that back into the chat as-is. Claude will fold the steals into the winning direction and + come back with one refined design — which is a far shorter conversation than trying to + describe taste you didn't know you had. +

+
+ + + +
+ + + + diff --git a/unknowns/04-toolbar-mock.html b/unknowns/04-toolbar-mock.html new file mode 100644 index 0000000..2d2dd57 --- /dev/null +++ b/unknowns/04-toolbar-mock.html @@ -0,0 +1,798 @@ + + + + + + +Mock before you wire — Know your unknowns + + + +
+ +
+ ← All examples +
Know your unknowns · Pre-implementation
+

Mock before you wire

+

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.

+ +
+
The prompt
+ +
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.
+
+
+ +
What Claude produced
+ + +
+
+
Acme · frame-annotation toolbar · mock, no real code touched
+
+ + + +
+
+

Floating pill, bottom-center. Hovers above the seekbar; hides on playback. Closest to what Loom/Frame.io ship.

+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + Ridgeline '26 + TEASER · CUT 04 + + + + + + + 1 + + + + + 2 + + + + 3 + + + +
00:00:41:07
+
PAUSED
+ + +
+
+ + + +
+
+
+
+
+
+
+ + + +
+ + + +
+ +

Everything above is fake data in one static file — nothing reads from apps/player yet. Real wiring would live in apps/player/src/annotate/Toolbar.tsx behind the frame_annotations flag.

+
+ + +
+
Open questions for you
+

Four layout calls I'd rather not guess

+

Tap a chip to record your pick — the reply template below fills itself in.

+ +
+

Q1Which 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.

+
+ + + +
+
+ +
+

Q2Color + 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.

+
+ + +
+
+ +
+

Q3The 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?

+
+ + +
+
+ +
+

Q4Blur 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?

+
+ + +
+
+ +
+
My reactions — copy & paste back
+ +

+    
+
+ + +
+ + + + diff --git a/unknowns/05-churn-brainstorm.html b/unknowns/05-churn-brainstorm.html new file mode 100644 index 0000000..562f837 --- /dev/null +++ b/unknowns/05-churn-brainstorm.html @@ -0,0 +1,716 @@ + + + + + + +Brainstorm the intervention — Know your unknowns + + + +
+ +
+ ← All examples +
Know your unknowns · Pre-implementation
+

Brainstorm the intervention

+

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.

+ +
+
The 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.”
+ +
+
+ +
What Claude produced
+ +
+
Acme · post-onboarding churn
+

10 places to intervene, cheapest to most ambitious

+

I searched apps/web, services/, and packages/ for onboarding, invite, and first-session code paths. A pattern showed up: a lot of retention machinery already exists but is disconnected — the cheapest options below are mostly wiring, not building. Check the ones that resonate and copy the reply at the bottom.

+ +
+
+ Ship this afternoon + Quarter-long bet +
+
+
+ + + + + + + + + + +
+
wiringnew UInew lifecyclenew surface
+
+ +
+ +
+
+ #1 +

Fix the dead-end empty state

+
S
+
+ apps/web/src/onboarding/EmptyState.tsx +

Found in code — The zero-projects dashboard renders the illustration and the copy “No projects yet” — and nothing else. NewProjectButton exists two directories over but was never imported here; the only way forward is the tiny “+” in the top nav. Every churned user's last screen was probably this one.

+
Impact100% of new users hit this screen; a real CTA plus a “drop a video anywhere” hint removes the most literal dead end in the product.
+
+
+ +
+
+ #2 +

Turn on the sample project that already exists

+
S
+
+ apps/web/src/projects/sampleProject.ts +

Found in code — Someone built a complete seeded demo project — “Product teaser v3” with a 45s clip, four timestamped comments, and one resolved thread — behind the flag sample_project_v1. The flag is still false for everyone and the file hasn't been touched in 8 months. The feature is done; it was just never shipped.

+
ImpactNew users see what a live review looks like before they've uploaded anything. Cheapest possible “aha” — flip a flag, verify the seed script.
+
+
+ +
+
+ #3 +

Show pending invites inside the app

+
S
+
+ services/mailer/templates/invite.mjml · apps/web/src/team/TeamPanel.tsx +

Found in code — Invites go out as email only. TeamPanel queries team_members but ignores the invites table entirely — an inviter can't see that their teammate never accepted, and there's no resend. For a collaborative review tool, a solo user is a churn-in-waiting.

+
ImpactSurfacing “2 invites pending · resend” turns invisible invite decay into a visible, fixable state.
+
+
+ +
+
+ #4 +

Wire the milestones table to a dashboard checklist

+
M
+
+ services/api/src/models/userMilestones.ts · apps/web/src/dashboard/Dashboard.tsx +

Found in code — The backend already records first_upload, first_comment, first_invite_accepted, and first_approval in user_milestones — but no client code reads the table. It exists purely for an analytics dashboard. All the ingredients for a “3 of 4 steps done” checklist are sitting there.

+
ImpactChecklists reliably lift activation; this one needs one endpoint and one component because tracking already works.
+
+
+ +
+
+ #5 +

Engineer the first-comment moment

+
M
+
+ apps/web/src/review/CommentComposer.tsx · apps/web/src/review/Timeline.tsx +

Found in code — 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. Timeline.tsx has a TODO: first-run hint? from the original author. Add a ghost pin on first play — “click anywhere to comment at this moment”.

+
ImpactIf first-session comment rate is the activation metric (it should be), this is the most direct lever on it.
+
+
+ +
+
+ #6 +

Rescue silently failed first uploads

+
M
+
+ services/ingest/src/transcode/queue.ts +

Found in code — When transcode fails (unsupported codec, oversized ProRes, corrupt moov atom), the job moves to failed, logs to Sentry, and tells the user nothing — 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.

+
ImpactA 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.
+
+
+ +
+
+ #7 +

Watch-together first session for teams

+
L
+
+ apps/web/src/review/PresenceProvider.tsx · services/realtime/ +

Found in code — Live presence is fully built — shared playhead, live cursors, “Maya is watching” badges — but it only lights up if two people happen to open the same review. Nothing engineers that coincidence. Add a “review together” scheduling nudge when a teammate accepts an invite, deep-linking both into the same session.

+
ImpactTeams that experience one live session likely retain at a different rate than solo users; this makes the multiplayer moment deliberate instead of lucky.
+
+
+ +
+
+ #8 +

Ship five review-workflow templates

+
L
+
+ packages/workflows/src/definitions/ +

Found in code — Approval workflows (rough cut → client review → final sign-off) are fully configurable — and start completely blank. The definitions directory contains only __fixtures__ for tests. Judging by a comment in WorkflowEditor.tsx, 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.

+
ImpactTemplates convert “configure this abstraction” into “pick your job” — the classic fix for blank-canvas churn in workflow tools.
+
+
+ +
+
+ #9 +

Built-in quick screen & camera recording

+
XL
+
+ new surface · services/ingest already accepts chunked webm +

Found in code — Today Acme's onboarding has a hard external dependency: you need a finished video before the product is useful. But services/ingest already accepts chunked webm uploads (built for the mobile app), so a MediaRecorder-based “record your screen, get feedback” flow needs no pipeline work — “just” the capture UI. Removes the biggest reason a trial goes idle: nothing to review yet.

+
ImpactChanges who can activate — designers and PMs without an editor can create the first reviewable artifact themselves. Big build, big surface area.
+
+
+ +
+
+ #10 +

First-class client review portals

+
XL
+
+ services/auth/src/guestTokens.ts · apps/web/src/share/ +

Found in code — Guest share links exist but are deliberately crippled: tokens expire after 24h, guests can't comment, and there's a // TODO(sasha): revisit guest perms after launch from two years ago. The users who churn are often the ones whose clients 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).

+
ImpactAttacks the structural cause: Acme's value depends on people who refuse to create accounts. Quarter-long bet with auth, billing, and abuse-prevention implications.
+
+
+ +
+
+ + +
+ +
+
+ 0 resonate + Check the interventions that resonate — your reply builds here. + +
+
+ + + + diff --git a/unknowns/06-interview.html b/unknowns/06-interview.html new file mode 100644 index 0000000..b0f8c5e --- /dev/null +++ b/unknowns/06-interview.html @@ -0,0 +1,644 @@ + + + + + + +The interview — Know your unknowns + + + +
+ +
+
Know your unknowns · Pre-implementation
+ ← All examples +

The interview

+

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.

+ +
+
The prompt
+ +
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.
+
+
+ +
What Claude produced
+ +
+
+ Annotation export — spec interview + acme/web · 7 open questions · ordered by blast radius +
+
+ +
+
+
+ + +
+ + + + diff --git a/unknowns/07-reference-port.html b/unknowns/07-reference-port.html new file mode 100644 index 0000000..f6e9a36 --- /dev/null +++ b/unknowns/07-reference-port.html @@ -0,0 +1,651 @@ + + + + + + +Point at a reference — Know your unknowns + + + +
+ +
+
Know your unknowns · Pre-implementation
+ ← All examples +

Point at a reference

+

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.

+ +
+
The prompt
+ +
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 — but first show me a semantics map so I can confirm you understood it.
+
+
+ +
What Claude produced
+ +
+ +
+

Semantics map: rate-limiter → Acme API client

+ awaiting confirmation +
+

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.

+
+ source vendor/rate-limiter (Rust, 612 loc, 3 modules) + target src/api/retry/ in acme-web + read bucket.rs · backoff.rs · budget.rs · 14 tests +
+ + +
+
1 What the crate actually does
+
+
    +
  • Token bucket admission (bucket.rs): every outgoing request withdraws 1 token from a bucket that refills at rate_per_sec = 8 up to burst_capacity = 40. The bucket starts full, so a cold client can burst 40 requests instantly.
  • +
  • Refill is lazy and integer-truncated: tokens are minted only when a request arrives, from elapsed nanoseconds × rate ÷ 10⁹. Fractional tokens are never stored — instead, last_refill only advances when at least one whole token is minted, so sub-token elapsed time carries forward.
  • +
  • Decorrelated jitter backoff (backoff.rs): on retryable failure, the next delay is drawn uniformly from [base, min(prev × 3, cap)] with base = 250ms, cap = 30s. Each draw seeds the next — delays wander rather than doubling in lockstep, which desynchronizes clients after an outage.
  • +
  • Retry budget (budget.rs): a second, separate bucket. Every successful first-try request deposits 1; every retry attempt withdraws 10. When the balance can’t cover a withdrawal, retries are refused entirely (first-try requests still go out). This caps retry amplification at ~10% during a sustained outage.
  • +
  • Failure classification is the caller’s job: the crate never inspects errors. It exposes acquire(), next_delay(), try_withdraw() and the caller decides what counts as retryable. The TS port keeps that boundary — Acme’s isRetryable() in src/api/errors.ts stays the single source of truth.
  • +
+
+
+ + +
+
2 Side-by-side: Rust source → proposed TypeScript
+

Highlighted regions correspond across columns — hover one to light up its counterpart and the gotcha note it maps to.

+ + +
+
PAIR A / 3

Lazy refill — integer truncation must survive the float world

+
+
+
vendor/rate-limiter/src/bucket.rs:41Rust
+
fn refill(&mut self, now: Instant) {
+    let elapsed = now
+        .saturating_duration_since1(self.last_refill);
+    let new_tokens = elapsed.as_nanos() as u64
+        * self.rate_per_sec as u64
+        / 1_000_000_0002;
+    if new_tokens > 03 {
+        self.tokens = (self.tokens + new_tokens)
+            .min(self.burst_capacity);
+        self.last_refill = now;
+    }
+}
+
+
+
src/api/retry/tokenBucket.ts (proposed)TS
+
private refill(now: number): void {
+  // now comes from performance.now(), not Date.now()
+  const elapsedMs = Math.max(0, now - this.lastRefill)1;
+  const newTokens = Math.floor(
+    (elapsedMs * this.ratePerSec) / 1000
+  )2;
+  if (newTokens > 03) {
+    this.tokens = Math.min(
+      this.tokens + newTokens,
+      this.burstCapacity
+    );
+    this.lastRefill = now;
+  }
+}
+
+
+
+
1Clock going backwards. saturating_duration_since clamps negative elapsed time to zero. Date.now() can jump backwards under NTP correction; the port uses monotonic performance.now() and keeps Math.max(0, …) as a belt-and-suspenders match.
+
2Integer vs float math. Rust’s u64 division truncates; JS division doesn’t. Math.floor restores truncation. Millis instead of nanos is safe: at rate = 8, elapsedMs * 8 stays far below 2⁵³, so no precision loss.
+
3The load-bearing guard. last_refill only advances when a whole token is minted. Dropping this guard (an easy “simplification”) silently discards sub-token progress on every call — at low rates the bucket would never refill under frequent polling. Preserved exactly, plus a regression test.
+
+
+ + +
+
PAIR B / 3

Decorrelated jitter — inclusive range, wandering seed

+
+
+
vendor/rate-limiter/src/backoff.rs:27Rust
+
fn next_delay(&mut self) -> Duration {
+    let hi = (self.prev_delay_ms.saturating_mul(3)4)
+        .min(self.cap_ms);
+    let lo = self.base_ms;
+    let ms = self.rng
+        .gen_range(lo..=hi.max(lo))5;
+    self.prev_delay_ms = ms;6
+    Duration::from_millis(ms)
+}
+
+
+
src/api/retry/backoff.ts (proposed)TS
+
nextDelay(): number {
+  const hi = Math.min(this.prevDelayMs * 34, this.capMs);
+  const lo = this.baseMs;
+  const span = Math.max(hi, lo) - lo;
+  const ms = lo + Math.floor(
+    this.random() * (span + 1)
+  )5;
+  this.prevDelayMs = ms;6
+  return ms;
+}
+
+
+
+
4Saturating multiply. saturating_mul(3) guards u64 overflow. In JS this can’t overflow — capMs = 30_000 bounds the product long before 2⁵³ — so the guard is deliberately dropped (see §3, “dropped” column).
+
5Inclusive vs exclusive range. Rust’s lo..=hi includes both endpoints. Naive lo + random() * (hi - lo) never yields hi. The + 1 inside Math.floor restores inclusivity — a one-character bug magnet, called out so you can veto or bless it.
+
6Stateful seed. Each draw becomes the next iteration’s prev — this is what makes it decorrelated rather than plain expo-backoff-with-jitter. reset() restores prevDelayMs = baseMs on success, matching the crate’s Backoff::reset. this.random is injectable for deterministic tests (crate uses a seeded SmallRng in its tests).
+
+
+ + +
+
PAIR C / 3

Retry budget — a mutex the event loop doesn’t need (with one trap)

+
+
+
vendor/rate-limiter/src/budget.rs:58Rust
+
pub fn try_withdraw(&self) -> bool {
+    let mut b = self.inner.lock().unwrap()7;
+    b.deposit_drip(Instant::now());
+    if b.balance >= WITHDRAW_COST {
+        b.balance -= WITHDRAW_COST;8
+        true
+    } else {
+        false // refuse retry, don’t queue9
+    }
+}
+
+
+
src/api/retry/budget.ts (proposed)TS
+
tryWithdraw(): boolean {
+  // no lock: single-threaded event loop7
+  // but NO await between check and debit.
+  this.depositDrip(this.clock());
+  if (this.balance >= WITHDRAW_COST) {
+    this.balance -= WITHDRAW_COST;8
+    return true;
+  }
+  return false9;
+}
+
+
+
+
7Thread-safety → event loop. The crate’s Mutex exists because Rust callers withdraw from worker threads. Acme’s client runs on one event loop, so the lock disappears — but the atomicity it provided must be preserved by convention: tryWithdraw is synchronous end-to-end, and I’ll add an eslint no-await-in-budget boundary comment plus a test that the method never returns a Promise.
+
8Check-then-debit stays fused. Same shape both sides. The failure mode isn’t data races here — it’s a future refactor inserting an await (e.g., to log) between the check and the debit, letting two in-flight retries both pass the check.
+
9Refuse, never queue. On an empty budget the crate returns false immediately — the request either goes out as a first try or fails fast. The port must not “helpfully” enqueue the retry for later; that would rebuild the retry storm the budget exists to prevent.
+
+
+
+ + +
+
3 Behaviors: preserved / deliberately changed / dropped
+
+
+
Preserved exactly
+
    +
  • Refill truncation + guard — whole tokens only; lastRefill advances only on mint
  • +
  • Jitter formula — uniform over [base, min(prev×3, cap)], inclusive both ends
  • +
  • Budget economics — +1 per first-try success, −10 per retry, ceiling 1000
  • +
  • Bucket starts full — 40-request cold burst is intentional (matches crate test burst_at_t0)
  • +
  • Caller classifies errors — limiter never inspects failures
  • +
+
+
+
Deliberately changed
+
    +
  • Instantperformance.now() — both monotonic; ms resolution is sufficient at rate 8/s
  • +
  • u64 nanos → number ms — all products provably < 2⁵³; Math.floor replays integer division
  • +
  • Mutex<Budget> → plain fields — atomicity by sync-only convention + test
  • +
  • SmallRng → injected random() — defaults to Math.random, seeded stub in tests
  • +
+
+
+
Dropped (not needed)
+
    +
  • saturating_mul overflow guards — unreachable once cap is applied in ms range
  • +
  • Send + Sync impls, Arc cloning — no threads to share across
  • +
  • tokio/async-std feature flags — port is runtime-agnostic by construction
  • +
  • Prometheus counters — Acme emits via telemetry.track() instead; hook points kept
  • +
+
+
+
+ + +
+
4 Edge cases: expected behavior on both sides
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Edge caseRust crateTypeScript portMatch
Clock skew
system clock steps back 5s mid-session
Instant is monotonic — unaffected. saturating_duration_since is a second fence.performance.now() is monotonic — unaffected. Math.max(0,…) kept as the same second fence.identical
Burst at t=0
fresh client fires 45 requests at once
First 40 admitted instantly (bucket starts full); 41–45 rejected until refill. Crate test: burst_at_t0.Same: 40 admitted, 5 rejected with RateLimited. Port test replays the crate’s fixture numbers verbatim.identical
Budget exhaustion
sustained 100% failure for 60s
Balance drains to 0 in ~10 retries; further retries refused, first-tries continue. Retry rate settles at deposit÷cost = 10% of success rate (0 here).Same economics, same settle point. Difference: refusal surfaces as RetryBudgetExhausted error so Acme’s upload queue can show “reconnecting” instead of failing silently.equivalent*
Slow drip
rate 8/s but polled every 20ms (0.16 tokens/poll)
Guard carries sub-token time forward; a token mints every ~125ms regardless of poll cadence.Same, via the preserved newTokens > 0 guard (Pair A, note 3). Regression test asserts mint cadence at 20ms polling.identical
Delay at cap
10th consecutive failure
Draw range collapses toward [250ms, 30s]; delay stays ≤ 30s but still jittered — never a fixed 30s (avoids re-synchronizing clients).Same bounds, same non-degenerate jitter — verified with a seeded RNG replaying the crate’s cap_still_jitters test vector.identical
+
+

* “equivalent” = same decision, different surface. The crate returns bare false; the port wraps it in a typed error because Acme’s UI needs to distinguish “rate limited” from “offline”. Flag this row if you’d rather keep the bare boolean.

+
+ + +
+
5 Sign-off
+
+
+

Ready to port when you are

+

Reply “semantics confirmed” and I’ll implement tokenBucket.ts, backoff.ts, and budget.ts with the crate’s 14 tests translated first. Or correct any row above — quote its number (e.g. “note 5”, “budget exhaustion row”) and I’ll revise the map before writing code.

+
+
+ + +
+
+
+ +
+ + +
+ + + + diff --git a/unknowns/08-implementation-plan.html b/unknowns/08-implementation-plan.html new file mode 100644 index 0000000..0119b65 --- /dev/null +++ b/unknowns/08-implementation-plan.html @@ -0,0 +1,765 @@ + + + + + + +The tweakable plan — Know your unknowns + + + +
+ +
+ ← All examples +
Know your unknowns · Pre-implementation
+

The tweakable plan

+

Instead of a plan sorted by execution order, ask for one sorted by how likely you are to change each part — so the decisions worth your attention surface first and the mechanical work sinks to the bottom.

+ +
+
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.
+ +
+
+ +
What Claude produced
+ + +
+ +

Implementation plan: Annotation export

+

Acme · export a review's annotations as a shareable PDF or CSV · branch feat/annotation-export

+ +
+
Effort~2.5 dev-days
+
Files touched13 (3 new)
+
RiskLow–medium
+
Migrations1, additive
+
+ +
+
↑ tweak +↓ trust
+
This plan is sorted by likelihood-of-tweaking, not execution order. 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.
+
+ + +
+
+ A +

Decisions you'll probably want to change

+ High tweak likelihood +
+

Three areas where I made a judgment call you might disagree with. Each flagged choice shows the alternative I considered — toggle to compare.

+ +

A1 · Data model — new annotation_exports table

+ +
+
+
annotation_exports NEW
+
iduuid pk
+
review_iduuid → reviews
+
requested_byuuid → users
+
statusenum(queued, rendering, ready, failed)
+
optionsjsonb
+
snapshot ①jsonb
+
① no snapshot column — renderer JOINs annotations live at render time
+
artifact_url ②text null
+
rendered_attimestamptz null
+
② no stored artifact — GET /exports/:id/download renders on demand
+
created_attimestamptz
+
+ +
+
+
+ Choice ① + Snapshot vs. live join + +
+
+
+
Plan's pick — denormalized snapshot
+ Copy the annotation payloads into snapshot jsonb when the export is requested. The export is a record of the review at that moment: later edits, resolutions, and deletions don't rewrite history. +
    +
  • Export stays valid even if the review is archived.
  • +
  • Renderer reads one row — no N+1 across annotations + annotation_replies.
  • +
+

Cost: ~40 KB/row for a heavy review; snapshots can go stale relative to the live thread.

+
+
+
Alternative — live join at render time
+ Skip the snapshot; the renderer joins annotations when the file is generated. Exports always reflect current state. +
    +
  • No duplicate data, no staleness question.
  • +
  • Re-downloading the same export can produce a different file — surprising for audit use.
  • +
+

Pick this if: exports are working documents, not records. One line to tell me: “use live join.”

+
+
+
+ +
+
+ Choice ② + Stored artifact vs. render-on-demand + +
+
+
+
Plan's pick — render once, store in blob storage
+ The worker renders the PDF/CSV once and writes artifact_url. Downloads are a signed-URL redirect — cheap, cacheable, and shareable with reviewers who lack Acme accounts. +

Cost: blob lifecycle to manage; I'd add a 30-day TTL sweep (Section C, item 6).

+
+
+
Alternative — render on every download
+ No stored file: GET /exports/:id/download streams a fresh render. Zero storage, but ~2–4 s per download on large reviews, and no shareable public link. +

Pick this if: storage/compliance rules make stored copies awkward.

+
+
+
+
+
+ +

A2 · New type interfaces

+ +
+
// src/types/exports.ts (new)
export interface ExportRequest {
reviewId: ReviewId;
format: 'pdf' | 'csv';1
range: 'all' | 'unresolved' | TimecodeRange;2
includeReplies: boolean; // default true
includeDrawings: boolean; // default false3
}
 
export interface AnnotationSnapshot {
id: AnnotationId;
timecode: string; // "00:04:12.08"
author: { id: UserId; name: string };
body: string;
state: 'open' | 'resolved';4
replies: ReplySnapshot[];
}
+
+
1Started with PDF + CSV. Adding 'xlsx' or 'srt' later is one union member + one renderer.
+
2TimecodeRange lets producers export just the notes for one scene. If nobody asked for this, cutting it removes ~½ day.
+
3Drawings (frame markups) rasterize into the PDF. Off by default because it triples render time.
+
4Deliberately drops the internal 5-state machine to 2 states in the export. Tell me if reviewers need to see 'wont_fix'.
+
+
+ +

A3 · UX flow

+ +
+
+
Step 1
+
+
Export button
+
New item in the review toolbar overflow menu, next to “Copy share link”.
+
+
+
+
Step 2
+
+
Options modal
+
Format, range, include-replies, include-drawings. Reuses ModalForm from settings.
+
+
+
+
Step 3
+
+ Weakest part of this plan +
Background job + toast
+
Export renders in a worker; user gets a “we'll notify you” toast and can navigate away.
+
+
+
+
Step 4
+
+
Ready notification
+
Bell notification + download link; also listed under review → Exports tab.
+
+
+
+

Why step 3 is weak: 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 s, which is too long to block. A hybrid (wait up to 4 s, then fall back to notify) is nicer and adds ~half a day. Your call.

+
+ + +
+
+ B +

Sequencing

+ Execution order +
+

The order I'd actually build it in. Every step lands green on CI; nothing user-visible ships until step 5 flips the flag.

+
    +
  1. 1Migration + model. Create annotation_exports, wire the Sequelize model, factory, fixtures.0.5d
  2. +
  3. 2Types + request validation. Everything in A2, plus zod schemas on POST /api/reviews/:id/exports.0.25d
  4. +
  5. 3Renderers. CSV first (trivial), then PDF via the existing @acme/render service.1d
  6. +
  7. 4Worker job + blob upload. Queue on exports.render; retries ×3 then status = failed.0.25d
  8. +
  9. 5UI: menu item, modal, toast, Exports tab. Behind flag export_annotations.0.5d
  10. +
  11. 6Tests, docs, flag rollout to the Acme team.0.25d
  12. +
+
+ + +
+
+ C +

Mechanical work (trust me)

+ Low tweak likelihood +
+

Refactors and plumbing the feature needs. No judgment calls here — collapsed on purpose.

+
+ Boring but necessary 8 tasks · ~0.5d total, folded into the estimates above +
    +
  • Extract AnnotationSerializer from api/annotations.ts into lib/serializers/ so the export worker can reuse it. Pure move, no behavior change.
  • +
  • Finish the CommentMarkerAnnotationMarker rename — 3 files still import the deprecated alias.
  • +
  • Add annotation_exports to the fixture loader and the CI database-reset script.
  • +
  • Register exports.render in workers/index.ts and add it to the dead-letter alert list.
  • +
  • Create feature flag export_annotations in flags.yaml, default off.
  • +
  • Add a nightly TTL sweep for expired export blobs to jobs/cleanup.ts (only if Choice ② stays as-is).
  • +
  • Extend openapi.yaml with the two new endpoints and regenerate the client.
  • +
  • Move the timecode-formatting helper from player/utils.ts to lib/time.ts — the PDF renderer needs it and shouldn't import from the player bundle.
  • +
+
+
+ + +
+

Tweak these three things

+

The highest-leverage replies you could send. Copy one, edit, send — I'll revise the plan.

+
+ +
“Switch Choice ① to live join — exports here are working docs, not records.”
+ +
+
+ +
“Cut TimecodeRange from ExportRequest; nobody has asked for per-scene export.”
+ +
+
+ +
“Do the hybrid for step 3: wait up to 4s inline, then fall back to notify.”
+ +
+
+ +
+ + + +
+ + + + diff --git a/unknowns/09-implementation-notes.html b/unknowns/09-implementation-notes.html new file mode 100644 index 0000000..52bebf3 --- /dev/null +++ b/unknowns/09-implementation-notes.html @@ -0,0 +1,609 @@ + + + + + + +Implementation notes — Know your unknowns + + + +
+ +
+
Know your unknowns · During implementation
+ ← All examples +

Implementation notes

+

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.

+ +
+
The prompt
+ +
“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.”
+
+
+ +
What Claude produced
+ +
+
+ + docs/notes/export-feature-implementation.md + run started 14:02 · last write 16:51 · branch feat/review-export +
+ +
+
11
entries
+
4
deviations
+
2
need your judgment
+
+ +
+ + + +
+ +
+ +
+
14:02
+
+
+ Plan-confirmed14:02 +

Step 1 — export job model and queue wiring, as planned

+

Added ExportJob in apps/api/src/exports/exportJob.ts, enqueued on the existing BullMQ media-tasks queue with a dedicated export job name. Migration 0121_export_jobs matches the plan's schema exactly (status enum, requested_by, review_id, format).

+
+
+ +
+
14:18
+
+
+ Plan-confirmed14:18 +

Step 2 — POST /api/v1/reviews/:id/export endpoint

+

Request validation reuses the zod schemas in apps/api/src/validation/ as the plan specified. format accepts mp4-burnin | csv | bundle. Returns 202 with a job id; polling endpoint deferred to step 6 per plan.

+
+
+ +
+
14:29
+
+
+ Discovery14:29 +

Review.duration_ms is denormalized and occasionally stale

+

Three fixture reviews have duration_ms that disagrees with the source asset's probe data (looks like re-uploads never refresh it). Export reads duration from MediaAsset.probe.duration instead — same approach playbackManifest.ts already takes. No plan change, just noting the trap for anyone who touches this later.

+
+
+ +
+
14:41
+
+
+ Deviation 1 of 414:41 +

Legacy annotations don't always have frame timestamps

+
+
What the plan said
Every annotation has a frame_ts; burn-in renderer can sort and place them all on the timeline.
+
What the code revealed
~12% of rows in annotations predate migration 0087 and have frame_ts = NULL — they were general review comments before per-frame annotations existed.
+
Conservative choice
Exclude null-timestamp annotations from the video burn-in; include them in the CSV sidecar with frame_ts left blank and a legacy_comment flag. Nothing is silently dropped from the export bundle.
+
Revisit
Could interpolate a timestamp from created_at offset against the review session — decide whether that's honest enough to show in the video.
+
+
+
+ +
+
15:05
+
+
+ Plan-confirmed15:05 +

Step 3 — annotation burn-in via ffmpeg drawtext/drawbox, as planned

+

Filtergraph builder in apps/worker/src/exports/burnin.ts. The plan's per-annotation overlay windows (enable='between(t,a,b)') work; verified against the 40-annotation stress fixture — render time 1.4x realtime on the worker instance class, inside the plan's 2x budget.

+
+
+ +
+
15:22
+
+
+ Deviation 2 of 415:22 +

The queue round-trips Date objects as strings

+
+
What the plan said
ExportJobPayload.requestedAt: Date, passed through to the worker and compared against the review's updated_at to detect stale exports.
+
What the code revealed
BullMQ JSON-serializes payloads into Redis, so the worker receives an ISO string, not a Date. The staleness comparison silently coerced and passed in unit tests, but failed on a redelivered job in the integration run.
+
Conservative choice
Typed the payload as wire format (requestedAt: string ISO-8601), parse once at the worker boundary in exportConsumer.ts. No custom serializer.
+
Revisit
Other media-tasks payloads likely have the same latent bug — a shared zod codec for job payloads would fix the class of problem.
+
+
+
+ +
+
15:48
+
+
+ Deviation 3 of 415:48 +

Zip streaming already exists — dropped the planned archiver dependency

+
+
What the plan said
Add archiver to build the export bundle (video + CSV + thumbnails) as a zip stream to S3.
+
What the code revealed
packages/media/src/zipStream.ts already does streaming zip with backpressure handling — it powers the bulk asset download feature and has been in production since March.
+
Conservative choice
Reused zipStream.ts as-is. One fewer dependency, and bundle exports inherit the same tested backpressure behavior as bulk downloads.
+
Revisit
zipStream lives in packages/media but is now used by two features — worth promoting to packages/shared in a cleanup pass.
+
+
+
+ +
+
16:07
+
+
+ Discovery16:07 +

A websocket progress channel convention already exists

+

The transcode pipeline emits media:progress:{assetId} events. Followed the same convention — exports emit export:progress:{jobId} — so the frontend can reuse useProgressChannel() from apps/web/src/hooks/ instead of the polling UI the plan sketched. Kept the polling endpoint too, as the fallback.

+
+
+ +
+
16:20
+
+
+ Deviation 4 of 416:20 +

Guest reviewers can view reviews but can't download assets

+
+
What the plan said
Export permission = review-level ACL: anyone who can open the review can export it.
+
What the code revealed
Workspace-level guest_reviewer role grants review access with can_download_assets = false — 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.
+
Conservative choice
Export returns 403 for guest_reviewer across all formats, mirroring the existing asset-download check in assetAccess.ts. Strictest defensible reading.
+
Revisit
Arguably guests should still get the CSV (annotations only, no media). That's a product call, not mine — logged below.
+
+
+
+ +
+
16:33
+
+
+ Todo for human16:33 +

Decide the guest-reviewer export policy

+

Current behavior (from Deviation 4): guests get 403 on every export format. If you want guests to export the annotation CSV without media, it's a ~10-line change in exportPolicy.ts plus one test — but it changes what “can't download assets” means to customers, so it should be your call. Blocking neither merge nor QA.

+
+
+ +
+
16:51
+
+
+ Todo for human16:51 +

Confirm the retention window for export bundles

+

The plan didn't specify a TTL for generated bundles. I put them under the exports/ 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 infra/s3.tf is the one line to change.

+
+
+ +
+
+ + + + + +
+ + + + diff --git a/unknowns/10-pitch-doc.html b/unknowns/10-pitch-doc.html new file mode 100644 index 0000000..b908512 --- /dev/null +++ b/unknowns/10-pitch-doc.html @@ -0,0 +1,573 @@ + + + + + + +The buy-in doc — Know your unknowns + + + +
+ +
+ ← All examples +
Know your unknowns · Post-implementation
+

The buy-in doc

+

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.

+ +
+
The 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.”
+
+
+ +
What Claude produced
+ + +
+
+
Ship proposal · Acme
+ 90-second read +
+

Annotation export is ready to ship

+

Prototype, spec, and rollout plan in one place. Ask: four sign-offs by Friday, flag ramp starts Monday.

+ + +

01 Watch it work

+
+
+
+ + Acme — q3_launch_teaser_v4.mp4 · Review +
+
+
+
+ + 00:42 / 02:05 +
+
+
+ + + +
+
+
+
Annotations0 selected
+
+ +
+
Maya R. 00:23
+
Logo lands too late — pull in 8 frames?
+
+
+
+ +
+
Devon P. 00:51
+
Color shift here vs. the master grade
+
+
+
+ +
+
Maya R. 01:30
+
Approved — final CTA reads great
+
+
+ +
+
+
+
+
Exporting 3 annotations
+
Collecting annotations…
+
+
+
+
+ + Export ready + q3_teaser_notes.srt · 3 cues +
+
+
+ select → export → done · loops automatically + +
+
+ + +

02 The pitch

+

Reviewers leave feedback in Acme, then retype it into email, Notion, and edit-bay software — the single most-cited gap in churn interviews (41% of departed teams). 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.

+ + +

03 What reviewers will ask

+
+ Q1Can this leak internal-only comments to a client? +
No. Export runs through the same visibility resolver as the review page: annotations marked internal are excluded unless the exporter holds workspace:admin and explicitly opts in via a labeled checkbox. Covered by 14 permission-matrix tests. +
↗ spec §4.2 — Visibility & permissions
+
+
+ Q2What happens on a monster session — thousands of annotations? +
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. +
↗ implementation notes, 06-24 — load test results
+
+
+ Q3Why file formats instead of a public API? +
SRT + CSV + PDF cover 83% of the 212 support tickets tagged export-request 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. +
↗ metric — support tag analysis, Q1–Q2
+
+
+ Q4Does this add new infrastructure we have to operate? +
Zero new services. SRT/CSV render inline in the existing worker pool (services/render-workers); PDF reuses the report service already generating weekly digests. No new queues, no new datastore, no schema migrations. +
↗ spec §5.1 — Architecture
+
+
+ Q5What's the compliance story for exported customer content? +
Every export is stamped with workspace ID, requesting user, and timestamp in file metadata, and written to the audit log (audit.annotation_export). Files are served via expiring signed URLs (24h) and never stored beyond that window. +
↗ spec §6 — Audit & retention
+
+ + +

04 Spec at a glance

+
+ + + + + + + + + + +
AreaDecisionRef
FormatsSRT (timecoded cues), CSV (flat, incl. drawing bounding boxes), PDF (frame thumbnails + comments)§2
Entry pointAnnotation panel → multi-select → Export selected; also “Export all” in session menu§3.1
PermissionsRequires session:comment or above; internal annotations excluded by default§4.2
Limits500 annotations/batch, streamed; 25k hard cap per export with clear error state§5.3
Telemetryexport_started, export_completed (format, count, duration), export_failed§7
Feature flagannotation_export — off → 5% → 50% → 100% over two weeks§8
+
+ + +

05 Risk, rollback, and what I need from you

+
+
+

Risk & rollback

+
    +
  • Rollback is one toggle. Flip annotation_export off — no schema migrations shipped, nothing to unwind.
  • +
  • Blast radius is bounded. Export runs in the shared worker pool with a 30s per-job timeout; a runaway job cannot starve review playback.
  • +
  • Worst credible failure: orphaned export files if the signed-URL cleanup lags — they self-expire in 24h regardless.
  • +
  • Known gap: drawing annotations flatten to bounding boxes in CSV (full vectors are PDF-only). Documented in-product; phase 2 item.
  • +
+
+
+

What I need from you

+
+ PN +
Priya NatarajanEng lead, Playback & Review +
Sign off on reusing the render worker pool (spec §5.1) — the only shared-infra dependency.
+
+
+ ES +
Elena SorokinSecurity +
Approve the visibility rules and audit-log shape (spec §4.2, §6).
+
+
+ MW +
Marcus WebbDesign +
Final pass on the export modal and error states — prototype above is current.
+
+
+ DK +
Dana KimPM, Collaboration +
Approve the flag ramp schedule (spec §8) and own the changelog entry.
+
+
+
+ +
Everything above is built and tested behind the flag. Reply with a ✓ on your piece, or grab 15 minutes on my calendar — target: ramp starts Monday.
+
+ + +
+ + + + diff --git a/unknowns/11-change-quiz.html b/unknowns/11-change-quiz.html new file mode 100644 index 0000000..38c6d32 --- /dev/null +++ b/unknowns/11-change-quiz.html @@ -0,0 +1,543 @@ + + + + + + +Quiz me before I merge — Know your unknowns + + + +
+ + +
+
Know your unknowns · Post-implementation
+ ← All examples +
+

Quiz me before I merge

+

Asking for a report with a quiz you must pass turns "I skimmed the diff" into verified understanding — the artifact won't let you feel done until you actually are.

+ +
+
The Prompt
+ +
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.
+
+ +
What Claude Produced
+ + +
+
+
Acme · Merge readiness report
+

Clip export: what changed and why

+

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.

+
feature/clip-exportmain · 14 files · 9 commits
+
+ +
+ + +
+
14
files changed
+
+612
lines added
+
−148
lines removed
+
+ + +
+

The mental model

+

One diagram is worth 14 files: where an export request goes now vs. before.

+

Before this change, "export a clip" meant the browser stitched frames client-side with MediaRecorder and uploaded the result — slow, tab-locked, and broken on Safari. Now the client only requests 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.

+
+
+
Before · client-side render
+
ReviewPlayer.tsx
+
+
MediaRecorder capture (in-tab)
+
~40–90s, tab locked
+
PUT /uploads (whole file)
+
+
S3 exports/ bucket
+
+
+
After · server-side job
+
ReviewPlayer.tsx
+
POST /api/exports (returns job id)
+
export-worker (new, ffmpeg)
+
+
S3 exports/ bucket
+
jobs table (new)
+
+
client polls every 2s
+
GET /api/exports/:id → signed URL
+
+
+
+ + +
+

Three non-obvious behaviors this introduces

+

The parts a diff skim won't tell you. Each one is deliberate — here's the reasoning.

+ +
+
01

Exports render from the original upload, not the proxy the reviewer watched

+
+
WhatThe 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.
+
WhyEditors export clips to hand off to clients; shipping the compressed proxy would embarrass them. Annotation coordinates are stored normalized (0–1), so they re-project onto the full-res frame correctly.
+
Whereworker/export/render.ts:41 worker/export/burn_in.ts:88
+
+
+ +
+
02

Export jobs survive a worker crash — via a visibility timeout, not retries

+
+
WhatA job row is claimed by setting locked_until = now() + 10min. If the worker dies mid-render, no retry fires; the lock simply expires and the next idle worker picks the job up from scratch.
+
Whyffmpeg 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.
+
Wheredb/migrations/0142_export_jobs.sql worker/export/claim.ts:19
+
+
+ +
+
03

Download URLs expire after 24 hours — the export itself lives 7 days

+
+
WhatThe signed S3 URL returned by GET /api/exports/:id 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.
+
WhyShort 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.
+
Whereapi/exports/get.ts:57 infra/s3_lifecycle.tf:23
+
+
+
+ + +
+
+
Existing behavior this leans on
+

Workspace-scoped signed-URL auth. GET /api/exports/:id does no permission check of its own — it reuses the requireWorkspaceMember 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.

+
+
+ + +
Part 2 · Prove it
+
+
+

Six questions before you merge

+
Not trivia — each one is a decision you'd have to make correctly during an incident or a review.
+
+
Score: 0 / 6 · 0 answered
+
+ +
+ + +
+
+

Cleared to merge

+

6/6 — you can explain this change to whoever gets paged for it. Standard checklist below.

+
    +
  • Understanding verified (this quiz, 6/6)
  • +
  • CI green on feature/clip-export · 412 tests
  • +
  • Migration 0142 reviewed — additive, no backfill
  • +
  • Squash-merge with the summary above as the commit body
  • +
  • Watch export-worker dashboards for 30 min post-deploy
  • +
  • Note BL-2214 dependency in the merge comment
  • +
+
+
+ +
+
+

Not yet — re-read these sections

+

You missed the questions below. The gaps map straight back to the report:

+ + +
+
+ +
+
+ + +
+ + + + diff --git a/unknowns/index.html b/unknowns/index.html new file mode 100644 index 0000000..dacbd45 --- /dev/null +++ b/unknowns/index.html @@ -0,0 +1,486 @@ + + + + + + +Know your unknowns — examples + + + +
+ +
+ ← html-effectiveness +
+
+
Companion to the blog post
+

Know your unknowns

+

+ The map is not the territory — the gap between them is your unknowns. Eleven + self-contained .html 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. +

+ +
+ +
+
+ +
+
01

Pre-implementation

8 demos
+

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.

+
+ +
+
+
Blindspot pass
+
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.
+
01-blindspot-pass.html
+
+
+ +
+
+
Teach me my unknowns
+
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.
+
02-color-grading-explainer.html
+
+
+ +
+
+
Four design directions
+
The same review queue rendered four wildly different ways — ops console, editorial, kanban, terminal — with steal/skip chips that write your reply for you.
+
03-design-directions.html
+
+
+ +
+
+
Mock before you wire
+
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.
+
04-toolbar-mock.html
+
+
+ +
+
+
Brainstorm the intervention
+
Ten codebase-grounded churn interventions plotted from ship-this-afternoon to quarter-long bet, with resonate checkboxes that assemble your reply.
+
05-churn-brainstorm.html
+
+
+ +
+
+
The interview
+
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.
+
06-interview.html
+
+
+ +
+
+
Point at a reference
+
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.
+
07-reference-port.html
+
+
+ +
+
+
The tweakable plan
+
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.
+
08-implementation-plan.html
+
+
+
+
+ +
+
02

During implementation

1 demo
+

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.

+ +
+ +
+
03

Post-implementation

2 demos
+

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.

+ +
+ + + +
+ +