项目文件夹

文件
2026-07-13 12:33:58 +08:00

544 行
32 KiB
HTML

<!-- Copyright 2026 Anthropic PBC · SPDX-License-Identifier: Apache-2.0 -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Quiz me before I merge — Know your unknowns</title>
<style>
:root{
--ivory:#FAF9F5; --paper:#FFFFFF; --slate:#141413; --clay:#D97757; --clay-d:#B85C3E;
--oat:#E3DACC; --olive:#788C5D; --g100:#F0EEE6; --g200:#E6E3DA; --g300:#D1CFC5;
--g500:#87867F; --g700:#3D3D3A;
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
}
*{box-sizing:border-box;margin:0;padding:0}
html{scroll-behavior:smooth}
body{
background:var(--ivory); color:var(--slate); font-family:var(--sans);
line-height:1.55; font-size:15px; padding:40px 20px 64px;
}
.page{max-width:980px;margin:0 auto}
h1,h2,h3{font-family:var(--serif);font-weight:500;letter-spacing:-0.015em;color:var(--slate)}
/* ---- page header ---- */
.topline{display:flex;align-items:baseline;justify-content:space-between;gap:16px;flex-wrap:wrap;margin-bottom:14px}
.eyebrow{
font-family:var(--mono);font-size:12px;text-transform:uppercase;letter-spacing:0.12em;
color:var(--g500);display:flex;align-items:center;gap:10px;
}
.eyebrow::before{content:"";display:inline-block;width:24px;height:2px;background:var(--clay);flex:none}
.backlink{font-family:var(--mono);font-size:12px;color:var(--g500);text-decoration:none;white-space:nowrap}
.backlink:hover{color:var(--clay-d)}
h1.pagetitle{font-size:34px;line-height:1.15;margin-bottom:10px}
.lede{color:var(--g700);font-size:16px;max-width:64ch;margin-bottom:26px}
/* ---- prompt box ---- */
.promptcard{
background:var(--paper);border:1.5px solid var(--g300);border-radius:14px;
padding:22px 26px;margin-bottom:36px;position:relative;
}
.promptcard .label{
font-family:var(--mono);font-size:12px;text-transform:uppercase;letter-spacing:0.12em;
color:var(--clay);margin-bottom:12px;
}
.promptcard blockquote{
font-family:var(--serif);font-style:italic;font-size:17.5px;line-height:1.55;
color:var(--slate);border:none;
}
.copybtn{
position:absolute;top:18px;right:18px;font-family:var(--mono);font-size:11.5px;
padding:6px 12px;border:1.5px solid var(--g300);border-radius:8px;background:var(--g100);
color:var(--g700);cursor:pointer;transition:all .15s;
}
.copybtn:hover{border-color:var(--clay);color:var(--clay-d)}
.copybtn.copied{background:var(--olive);border-color:var(--olive);color:#fff}
/* ---- divider ---- */
.divider{display:flex;align-items:center;gap:16px;margin:0 0 34px;
font-family:var(--mono);font-size:12px;letter-spacing:0.12em;color:var(--g500);text-transform:uppercase;white-space:nowrap}
.divider::before,.divider::after{content:"";flex:1;height:1px;background:var(--g300)}
/* =================== ARTIFACT =================== */
.artifact{background:var(--paper);border:1.5px solid var(--g300);border-radius:14px;overflow:hidden}
.art-head{padding:28px 34px 24px;border-bottom:1.5px solid var(--g200)}
.art-eyebrow{font-family:var(--mono);font-size:11.5px;text-transform:uppercase;letter-spacing:0.12em;color:var(--g500);margin-bottom:8px;display:flex;align-items:center;gap:10px}
.art-eyebrow::before{content:"";display:inline-block;width:24px;height:2px;background:var(--clay);flex:none}
.art-head h2{font-size:26px;margin-bottom:6px}
.art-head .sub{color:var(--g700);font-size:14.5px;max-width:66ch}
.branchline{font-family:var(--mono);font-size:12.5px;color:var(--g700);background:var(--g100);
border:1.5px solid var(--g300);border-radius:8px;padding:6px 10px;display:inline-block;margin-top:14px;overflow-x:auto;max-width:100%}
.branchline .arr{color:var(--g500);margin:0 6px}
.art-body{padding:30px 34px 38px}
@media (max-width:560px){
.art-head{padding:22px 18px 18px}
.art-body{padding:22px 18px 30px}
h1.pagetitle{font-size:27px}
}
/* stat chips */
.stats{display:flex;gap:12px;flex-wrap:wrap;margin-bottom:34px}
.statchip{flex:1;min-width:130px;background:var(--g100);border:1.5px solid var(--g300);border-radius:12px;padding:14px 18px}
.statchip .num{font-family:var(--serif);font-size:28px;font-weight:500;letter-spacing:-0.02em;line-height:1.1}
.statchip .num.add{color:var(--olive)}
.statchip .num.del{color:var(--clay-d)}
.statchip .cap{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:0.1em;color:var(--g500);margin-top:4px}
.art-section{margin-bottom:38px}
.art-section h3{font-size:20px;margin-bottom:6px}
.art-section .secnote{color:var(--g500);font-size:13.5px;margin-bottom:16px}
.art-section p{color:var(--g700);margin-bottom:12px;max-width:70ch}
.art-section p strong{color:var(--slate)}
/* mental model diagram */
.flow-pair{display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-top:16px}
@media (max-width:680px){.flow-pair{grid-template-columns:1fr}}
.flowcard{border:1.5px solid var(--g300);border-radius:12px;padding:16px 16px 18px;background:var(--ivory)}
.flowcard.after{border-color:var(--clay)}
.flowcard .fc-label{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:0.1em;margin-bottom:12px}
.flowcard.before .fc-label{color:var(--g500)}
.flowcard.after .fc-label{color:var(--clay-d)}
.fnode{border:1.5px solid var(--g300);background:var(--paper);border-radius:8px;padding:7px 12px;
font-family:var(--mono);font-size:12px;color:var(--g700);text-align:center}
.fnode.new{border-color:var(--clay);background:#FBF1EC;color:var(--clay-d)}
.fnode.oatf{background:var(--oat)}
.farrow{display:flex;justify-content:center;align-items:center;height:22px;color:var(--g500);font-size:13px;line-height:1}
.farrow.lab{gap:8px;font-family:var(--mono);font-size:10.5px}
.fsplit{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.fsplit .farrow{height:20px}
/* behavior blocks */
.beh{border:1.5px solid var(--g300);border-radius:12px;margin-bottom:14px;overflow:hidden}
.beh-head{display:flex;align-items:baseline;gap:12px;padding:14px 18px;background:var(--g100)}
.beh-num{font-family:var(--mono);font-size:12px;color:var(--clay-d);font-weight:600;flex:none}
.beh-head h4{font-family:var(--serif);font-weight:500;font-size:16.5px;letter-spacing:-0.01em}
.beh-body{padding:14px 18px 16px}
.beh-row{display:grid;grid-template-columns:58px 1fr;gap:10px;margin-bottom:9px;font-size:14px}
.beh-row:last-child{margin-bottom:0}
.beh-row .k{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:0.1em;color:var(--g500);padding-top:3px}
.beh-row .v{color:var(--g700)}
.fchip{display:inline-block;font-family:var(--mono);font-size:11.5px;background:var(--g100);
border:1px solid var(--g300);border-radius:6px;padding:1.5px 7px;color:var(--g700);white-space:nowrap;margin:1px 2px 1px 0}
/* leans-on callout */
.leans{border:1.5px solid var(--g300);border-left:4px solid var(--olive);border-radius:12px;
background:var(--ivory);padding:16px 20px}
.leans .lab{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:0.1em;color:var(--olive);margin-bottom:6px}
.leans p{margin-bottom:0;font-size:14px}
/* ---- quiz ---- */
.quiz-rule{display:flex;align-items:center;gap:16px;margin:44px 0 8px;
font-family:var(--mono);font-size:11.5px;letter-spacing:0.12em;color:var(--g500);text-transform:uppercase}
.quiz-rule::before,.quiz-rule::after{content:"";flex:1;height:1px;background:var(--g300)}
.quiz-intro{display:flex;justify-content:space-between;align-items:flex-end;gap:16px;flex-wrap:wrap;margin-bottom:20px}
.quiz-intro h3{font-size:22px}
.quiz-intro .qsub{color:var(--g500);font-size:13.5px}
.scorebox{font-family:var(--mono);font-size:12.5px;color:var(--g700);background:var(--g100);
border:1.5px solid var(--g300);border-radius:10px;padding:8px 14px;white-space:nowrap}
.scorebox b{color:var(--slate)}
.q{border:1.5px solid var(--g300);border-radius:12px;padding:18px 20px;margin-bottom:16px;background:var(--paper)}
.q.answered-right{border-color:var(--olive)}
.q.answered-wrong{border-color:var(--clay)}
.q .qnum{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:0.1em;color:var(--g500);margin-bottom:6px}
.q .qtext{font-family:var(--serif);font-size:17px;line-height:1.45;margin-bottom:14px;letter-spacing:-0.01em}
.q .qtext code{font-family:var(--mono);font-size:14px;background:var(--g100);border:1px solid var(--g300);border-radius:5px;padding:1px 5px}
.opt{display:flex;gap:10px;align-items:flex-start;width:100%;text-align:left;
border:1.5px solid var(--g300);border-radius:9px;background:var(--ivory);padding:10px 14px;
margin-bottom:8px;cursor:pointer;font:inherit;font-size:14px;color:var(--g700);transition:border-color .12s, background .12s}
.opt:hover{border-color:var(--g500)}
.opt .key{font-family:var(--mono);font-size:11.5px;color:var(--g500);flex:none;padding-top:2px}
.opt.sel-right{border-color:var(--olive);background:#F1F4EB;color:var(--slate)}
.opt.sel-right .key{color:var(--olive)}
.opt.sel-wrong{border-color:var(--clay);background:#FBF1EC;color:var(--slate)}
.opt.sel-wrong .key{color:var(--clay-d)}
.opt.reveal-right{border-color:var(--olive)}
.opt:disabled{cursor:default;opacity:1}
.opt.dim{opacity:.55}
.fb{border-radius:9px;padding:12px 15px;font-size:13.5px;margin-top:4px;display:none}
.fb.show{display:block}
.fb.good{background:#F1F4EB;border:1.5px solid var(--olive);color:var(--g700)}
.fb.good .fbtag{color:var(--olive)}
.fb.bad{background:#FBF1EC;border:1.5px solid var(--clay);color:var(--g700)}
.fb.bad .fbtag{color:var(--clay-d)}
.fbtag{font-family:var(--mono);font-size:11px;text-transform:uppercase;letter-spacing:0.1em;font-weight:600;margin-bottom:5px}
.fb .excerpt{margin-top:8px;border-left:3px solid var(--g300);padding:6px 12px;background:var(--paper);
border-radius:0 7px 7px 0;font-size:13px;color:var(--g700);font-style:italic;font-family:var(--serif)}
.fb .exlab{font-family:var(--mono);font-style:normal;font-size:10.5px;text-transform:uppercase;letter-spacing:0.09em;color:var(--g500);display:block;margin-bottom:3px}
/* result cards */
.result{display:none;margin-top:26px}
.result.show{display:block}
.cleared{border:1.5px solid var(--olive);border-radius:14px;background:#F1F4EB;padding:24px 26px}
.cleared .clr-head{display:flex;align-items:center;gap:12px;margin-bottom:6px}
.clr-badge{width:34px;height:34px;border-radius:50%;background:var(--olive);color:#fff;display:flex;
align-items:center;justify-content:center;font-size:17px;flex:none}
.cleared h3{font-size:21px}
.cleared .clr-sub{color:var(--g700);font-size:14px;margin-bottom:16px}
.mergelist{list-style:none}
.mergelist li{display:flex;gap:10px;align-items:baseline;font-family:var(--mono);font-size:12.5px;
color:var(--g700);padding:7px 0;border-bottom:1px dashed var(--g300)}
.mergelist li:last-child{border-bottom:none}
.mergelist .tick{color:var(--olive);font-weight:700;flex:none}
.mergelist .pend{color:var(--g500);flex:none}
.reread{border:1.5px solid var(--clay);border-radius:14px;background:#FBF1EC;padding:22px 26px}
.reread h3{font-size:19px;margin-bottom:4px}
.reread p{color:var(--g700);font-size:14px;margin-bottom:12px}
.reread ul{list-style:none}
.reread li{margin-bottom:7px}
.reread a{color:var(--clay-d);font-size:14px;text-decoration:none;border-bottom:1px solid var(--clay)}
.reread a:hover{color:var(--slate)}
.retry{margin-top:14px;font-family:var(--mono);font-size:12px;padding:8px 16px;border:1.5px solid var(--g300);
border-radius:8px;background:var(--paper);color:var(--g700);cursor:pointer}
.retry:hover{border-color:var(--clay);color:var(--clay-d)}
footer{margin-top:44px;text-align:center;font-size:13px;color:var(--g500)}
footer a{color:var(--clay-d);text-decoration:none}
footer a:hover{text-decoration:underline}
</style>
</head>
<body>
<div class="page">
<!-- ============ PAGE HEADER ============ -->
<div class="topline">
<div class="eyebrow">Know your unknowns &middot; Post-implementation</div>
<a class="backlink" href="index.html">&larr; All examples</a>
</div>
<h1 class="pagetitle">Quiz me before I merge</h1>
<p class="lede">Asking for a report <em>with a quiz you must pass</em> turns "I skimmed the diff" into verified understanding — the artifact won't let you feel done until you actually are.</p>
<div class="promptcard">
<div class="label">The Prompt</div>
<button class="copybtn" id="copyBtn">Copy prompt</button>
<blockquote id="promptText">I want to make sure I understand everything that happened in this change before I merge. Give me an HTML report on the export-feature diff — context, intuition, what was done — with a quiz at the bottom that I must pass.</blockquote>
</div>
<div class="divider">What Claude Produced</div>
<!-- ============ ARTIFACT ============ -->
<div class="artifact">
<div class="art-head">
<div class="art-eyebrow">Acme &middot; Merge readiness report</div>
<h2>Clip export: what changed and why</h2>
<p class="sub">A 14-file diff that adds server-side clip export to review threads. Read the three sections below, then pass the quiz. If you can't, you're not ready to merge — and that's the point.</p>
<div class="branchline">feature/clip-export<span class="arr">&rarr;</span>main &middot; 14 files &middot; 9 commits</div>
</div>
<div class="art-body">
<!-- stats -->
<div class="stats">
<div class="statchip"><div class="num">14</div><div class="cap">files changed</div></div>
<div class="statchip"><div class="num add">+612</div><div class="cap">lines added</div></div>
<div class="statchip"><div class="num del">&minus;148</div><div class="cap">lines removed</div></div>
</div>
<!-- mental model -->
<div class="art-section" id="sec-model">
<h3>The mental model</h3>
<p class="secnote">One diagram is worth 14 files: where an export request goes now vs. before.</p>
<p>Before this change, "export a clip" meant the browser stitched frames client-side with <code>MediaRecorder</code> and uploaded the result — slow, tab-locked, and broken on Safari. Now the client only <strong>requests</strong> an export; a new worker renders it server-side from the original media and the client polls a job until a signed download URL appears.</p>
<div class="flow-pair">
<div class="flowcard before">
<div class="fc-label">Before &middot; client-side render</div>
<div class="fnode">ReviewPlayer.tsx</div>
<div class="farrow">&darr;</div>
<div class="fnode">MediaRecorder capture (in-tab)</div>
<div class="farrow lab"><span>&darr;</span><span>~40&ndash;90s, tab locked</span></div>
<div class="fnode">PUT /uploads (whole file)</div>
<div class="farrow">&darr;</div>
<div class="fnode oatf">S3 exports/ bucket</div>
</div>
<div class="flowcard after">
<div class="fc-label">After &middot; server-side job</div>
<div class="fnode">ReviewPlayer.tsx</div>
<div class="farrow lab"><span>&darr;</span><span>POST /api/exports (returns job id)</span></div>
<div class="fnode new">export-worker (new, ffmpeg)</div>
<div class="fsplit">
<div><div class="farrow">&darr;</div><div class="fnode oatf">S3 exports/ bucket</div></div>
<div><div class="farrow">&darr;</div><div class="fnode new">jobs table (new)</div></div>
</div>
<div class="farrow lab"><span>&darr;</span><span>client polls every 2s</span></div>
<div class="fnode">GET /api/exports/:id &rarr; signed URL</div>
</div>
</div>
</div>
<!-- non-obvious behaviors -->
<div class="art-section" id="sec-behaviors">
<h3>Three non-obvious behaviors this introduces</h3>
<p class="secnote">The parts a diff skim won't tell you. Each one is deliberate — here's the reasoning.</p>
<div class="beh" id="beh-1">
<div class="beh-head"><span class="beh-num">01</span><h4>Exports render from the original upload, not the proxy the reviewer watched</h4></div>
<div class="beh-body">
<div class="beh-row"><span class="k">What</span><span class="v">The worker pulls <code class="fchip">media/originals/</code>, never the 720p review proxy. An exported clip can therefore look <em>sharper</em> than what the reviewer saw when they drew their annotations.</span></div>
<div class="beh-row"><span class="k">Why</span><span class="v">Editors export clips to hand off to clients; shipping the compressed proxy would embarrass them. Annotation coordinates are stored normalized (0&ndash;1), so they re-project onto the full-res frame correctly.</span></div>
<div class="beh-row"><span class="k">Where</span><span class="v"><span class="fchip">worker/export/render.ts:41</span> <span class="fchip">worker/export/burn_in.ts:88</span></span></div>
</div>
</div>
<div class="beh" id="beh-2">
<div class="beh-head"><span class="beh-num">02</span><h4>Export jobs survive a worker crash — via a visibility timeout, not retries</h4></div>
<div class="beh-body">
<div class="beh-row"><span class="k">What</span><span class="v">A job row is claimed by setting <code class="fchip">locked_until = now() + 10min</code>. If the worker dies mid-render, no retry fires; the lock simply expires and the next idle worker picks the job up from scratch.</span></div>
<div class="beh-row"><span class="k">Why</span><span class="v">ffmpeg renders aren't idempotent mid-flight, and a retry queue would need dead-letter handling we don't want yet. Lock expiry gives at-least-once semantics with one column. Consequence: a crashed job looks "processing" for up to 10 minutes before recovering.</span></div>
<div class="beh-row"><span class="k">Where</span><span class="v"><span class="fchip">db/migrations/0142_export_jobs.sql</span> <span class="fchip">worker/export/claim.ts:19</span></span></div>
</div>
</div>
<div class="beh" id="beh-3">
<div class="beh-head"><span class="beh-num">03</span><h4>Download URLs expire after 24 hours — the export itself lives 7 days</h4></div>
<div class="beh-body">
<div class="beh-row"><span class="k">What</span><span class="v">The signed S3 URL returned by <code class="fchip">GET /api/exports/:id</code> is valid for 24h, but the underlying object isn't deleted until a 7-day lifecycle rule fires. Re-hitting the endpoint mints a fresh URL with no re-render.</span></div>
<div class="beh-row"><span class="k">Why</span><span class="v">Short URLs limit link-forwarding outside the workspace (exports can contain unreleased footage); the longer object TTL means a stale Slack link costs one API call to revive, not a 90-second re-render.</span></div>
<div class="beh-row"><span class="k">Where</span><span class="v"><span class="fchip">api/exports/get.ts:57</span> <span class="fchip">infra/s3_lifecycle.tf:23</span></span></div>
</div>
</div>
</div>
<!-- leans on -->
<div class="art-section" id="sec-leans">
<div class="leans">
<div class="lab">Existing behavior this leans on</div>
<p><strong>Workspace-scoped signed-URL auth.</strong> <span class="fchip">GET /api/exports/:id</span> does no permission check of its own — it reuses the <span class="fchip">requireWorkspaceMember</span> middleware that already guards every media route. If that middleware's session handling ever changes (there's an open ticket, BL-2214, about guest reviewer sessions), export downloads change with it. Nothing in this diff would flag that.</p>
</div>
</div>
<!-- ============ QUIZ ============ -->
<div class="quiz-rule">Part 2 &middot; Prove it</div>
<div class="quiz-intro">
<div>
<h3>Six questions before you merge</h3>
<div class="qsub">Not trivia — each one is a decision you'd have to make correctly during an incident or a review.</div>
</div>
<div class="scorebox">Score: <b id="scoreNow">0</b> / <b>6</b> &middot; <span id="answeredNow">0</span> answered</div>
</div>
<div id="quiz"></div>
<!-- results -->
<div class="result" id="resPass">
<div class="cleared">
<div class="clr-head"><div class="clr-badge">&#10003;</div><h3>Cleared to merge</h3></div>
<p class="clr-sub">6/6 — you can explain this change to whoever gets paged for it. Standard checklist below.</p>
<ul class="mergelist">
<li><span class="tick">&#10003;</span>Understanding verified (this quiz, 6/6)</li>
<li><span class="tick">&#10003;</span>CI green on feature/clip-export &middot; 412 tests</li>
<li><span class="tick">&#10003;</span>Migration 0142 reviewed &mdash; additive, no backfill</li>
<li><span class="pend">&#9675;</span>Squash-merge with the summary above as the commit body</li>
<li><span class="pend">&#9675;</span>Watch export-worker dashboards for 30 min post-deploy</li>
<li><span class="pend">&#9675;</span>Note BL-2214 dependency in the merge comment</li>
</ul>
</div>
</div>
<div class="result" id="resFail">
<div class="reread">
<h3 id="failTitle">Not yet — re-read these sections</h3>
<p>You missed the questions below. The gaps map straight back to the report:</p>
<ul id="failLinks"></ul>
<button class="retry" id="retryBtn">Reset quiz &amp; try again</button>
</div>
</div>
</div><!-- /art-body -->
</div><!-- /artifact -->
<footer>Part of <a href="index.html">Know your unknowns</a> &mdash; companion examples to the blog post.</footer>
</div>
<script>
(function(){
// ---- copy prompt ----
var copyBtn = document.getElementById('copyBtn');
copyBtn.addEventListener('click', function(){
var t = document.getElementById('promptText').textContent;
function done(){ copyBtn.textContent='Copied'; copyBtn.classList.add('copied');
setTimeout(function(){copyBtn.textContent='Copy prompt';copyBtn.classList.remove('copied');},1600); }
if (navigator.clipboard && navigator.clipboard.writeText){
navigator.clipboard.writeText(t).then(done, function(){ fallback(); });
} else { fallback(); }
function fallback(){
var ta=document.createElement('textarea'); ta.value=t; document.body.appendChild(ta);
ta.select(); try{document.execCommand('copy');}catch(e){} document.body.removeChild(ta); done();
}
});
// ---- quiz data ----
var QS = [
{
q:'A reviewer says their exported clip "looks different from what I annotated." Assuming no bug, why?',
opts:[
'The export re-encodes at a lower bitrate to keep files small',
'The export renders from the full-res original, while they reviewed the 720p proxy',
'Annotations are dropped when the source is above 1080p',
'Safari renders exports differently from Chrome'
],
correct:1,
reinforce:'Right — exports pull media/originals/, so the clip can be sharper than the proxy the reviewer watched. Expected, not a bug.',
excerptFrom:'Behavior 01', anchor:'beh-1', anchorLabel:'Behavior 01 — exports render from the original',
excerpt:'The worker pulls media/originals/, never the 720p review proxy. An exported clip can therefore look sharper than what the reviewer saw when they drew their annotations.'
},
{
q:'An export-worker pod is OOM-killed halfway through a render. What happens to that job?',
opts:[
'It moves to a dead-letter queue for manual replay',
'It is retried immediately on another worker',
'It shows "processing" until its 10-minute lock expires, then another worker restarts it from scratch',
'It fails permanently and the user must click Export again'
],
correct:2,
reinforce:'Exactly — no retry queue exists. Recovery is lock expiry: up to 10 minutes of apparent "processing", then a fresh render.',
excerptFrom:'Behavior 02', anchor:'beh-2', anchorLabel:'Behavior 02 — crash recovery via lock expiry',
excerpt:'If the worker dies mid-render, no retry fires; the lock simply expires and the next idle worker picks the job up from scratch… a crashed job looks "processing" for up to 10 minutes before recovering.'
},
{
q:'A client pastes an export link into Slack; a teammate clicks it 3 days later and gets an S3 "expired" error. Cheapest fix?',
opts:[
'Re-run the export — the file was deleted after 24 hours',
'Hit GET /api/exports/:id again — the object still exists for 7 days, so it just mints a fresh signed URL',
'Bump the lifecycle rule in s3_lifecycle.tf to 30 days',
'Ask an admin to make the exports/ bucket public-read'
],
correct:1,
reinforce:'Yes — only the URL expired (24h). The object lives 7 days, so a fresh GET returns a new link with zero re-render cost.',
excerptFrom:'Behavior 03', anchor:'beh-3', anchorLabel:'Behavior 03 — URL TTL vs object TTL',
excerpt:'The signed S3 URL is valid for 24h, but the underlying object isn’t deleted until a 7-day lifecycle rule fires. Re-hitting the endpoint mints a fresh URL with no re-render.'
},
{
q:'Ticket BL-2214 changes how guest reviewer sessions are validated. Why does this diff make that ticket riskier?',
opts:[
'Guests can now trigger unlimited exports and exhaust the worker pool',
'Export downloads have no auth check of their own — they inherit whatever requireWorkspaceMember does, so a session change silently changes who can download',
'The jobs table stores guest session tokens in plaintext',
'It doesn’t — exports are unrelated to session handling'
],
correct:1,
reinforce:'Correct — GET /api/exports/:id borrows requireWorkspaceMember wholesale. Change the middleware, change export access, with nothing in this diff to flag it.',
excerptFrom:'Existing behavior this leans on', anchor:'sec-leans', anchorLabel:'Callout — leans on workspace auth middleware',
excerpt:'GET /api/exports/:id does no permission check of its own — it reuses the requireWorkspaceMember middleware… If that middleware’s session handling ever changes (BL-2214), export downloads change with it.'
},
{
q:'After deploy, a user reports "export stuck at processing for 15 minutes." Given the design, what does 15 minutes tell you?',
opts:[
'Nothing — long renders are normal for big clips',
'The client’s 2-second polling loop probably disconnected',
'Something is wrong beyond a single crash: one crash self-heals within the 10-minute lock window, so 15+ minutes means repeated failures or a stuck worker',
'The S3 lifecycle rule deleted the file mid-render'
],
correct:2,
reinforce:'Right — the 10-minute lock is the self-heal budget. Anything past it means the job is failing repeatedly or a worker is wedged: page-worthy, not wait-worthy.',
excerptFrom:'Behavior 02', anchor:'beh-2', anchorLabel:'Behavior 02 — the 10-minute recovery window',
excerpt:'Lock expiry gives at-least-once semantics with one column. Consequence: a crashed job looks "processing" for up to 10 minutes before recovering.'
},
{
q:'Why do annotations still land in the right place even though the export renders at a different resolution than the review player?',
opts:[
'The worker scales the export down to match the proxy resolution first',
'Annotation coordinates are stored normalized (0–1) and re-projected onto whatever frame size is rendered',
'The client re-sends annotation pixels at export time',
'burn_in.ts snaps annotations to a fixed 1080p grid'
],
correct:1,
reinforce:'Yes — normalized coordinates are what make "render from the original" safe. Pixel-space storage would have broken every annotation in this diff.',
excerptFrom:'Behavior 01', anchor:'beh-1', anchorLabel:'Behavior 01 — normalized annotation coordinates',
excerpt:'Annotation coordinates are stored normalized (0–1), so they re-project onto the full-res frame correctly.'
}
];
var KEYS=['A','B','C','D'];
var answers=new Array(QS.length).fill(null); // true/false/null
var quizEl=document.getElementById('quiz');
function build(){
quizEl.innerHTML='';
answers=new Array(QS.length).fill(null);
QS.forEach(function(item,qi){
var qd=document.createElement('div'); qd.className='q'; qd.id='q'+qi;
var h='<div class="qnum">Question '+(qi+1)+' of '+QS.length+'</div>'
+'<div class="qtext">'+item.q+'</div>';
item.opts.forEach(function(o,oi){
h+='<button class="opt" data-q="'+qi+'" data-o="'+oi+'"><span class="key">'+KEYS[oi]+'</span><span>'+o+'</span></button>';
});
h+='<div class="fb" id="fb'+qi+'"></div>';
qd.innerHTML=h;
quizEl.appendChild(qd);
});
updateScore();
document.getElementById('resPass').classList.remove('show');
document.getElementById('resFail').classList.remove('show');
}
quizEl.addEventListener('click', function(e){
var btn=e.target.closest('.opt'); if(!btn||btn.disabled) return;
var qi=+btn.dataset.q, oi=+btn.dataset.o, item=QS[qi];
if(answers[qi]!==null) return;
var right=(oi===item.correct);
answers[qi]=right;
var qd=document.getElementById('q'+qi);
qd.classList.add(right?'answered-right':'answered-wrong');
qd.querySelectorAll('.opt').forEach(function(b){
b.disabled=true;
var boi=+b.dataset.o;
if(boi===oi) b.classList.add(right?'sel-right':'sel-wrong');
else if(boi===item.correct && !right) b.classList.add('reveal-right');
else b.classList.add('dim');
});
var fb=document.getElementById('fb'+qi);
if(right){
fb.className='fb good show';
fb.innerHTML='<div class="fbtag">&#10003; Correct</div>'+item.reinforce;
} else {
fb.className='fb bad show';
fb.innerHTML='<div class="fbtag">Not quite &mdash; the correct answer is '+KEYS[item.correct]+'</div>'
+'From the report:'
+'<div class="excerpt"><span class="exlab">'+item.excerptFrom+'</span>'+item.excerpt+'</div>';
}
updateScore();
if(answers.every(function(a){return a!==null;})) finish();
});
function updateScore(){
var score=answers.filter(function(a){return a===true;}).length;
var done=answers.filter(function(a){return a!==null;}).length;
document.getElementById('scoreNow').textContent=score;
document.getElementById('answeredNow').textContent=done;
}
function finish(){
var score=answers.filter(function(a){return a===true;}).length;
if(score===QS.length){
document.getElementById('resPass').classList.add('show');
document.getElementById('resPass').scrollIntoView({behavior:'smooth',block:'nearest'});
} else {
var missed=[]; answers.forEach(function(a,i){ if(a!==true) missed.push(i); });
document.getElementById('failTitle').textContent='Not yet: '+score+'/'+QS.length+' — re-read these sections';
var seen={}, ul=document.getElementById('failLinks'); ul.innerHTML='';
missed.forEach(function(i){
var it=QS[i];
if(seen[it.anchor]) return; seen[it.anchor]=true;
var li=document.createElement('li');
li.innerHTML='<a href="#'+it.anchor+'">'+it.anchorLabel+'</a>';
ul.appendChild(li);
});
document.getElementById('resFail').classList.add('show');
document.getElementById('resFail').scrollIntoView({behavior:'smooth',block:'nearest'});
}
}
document.getElementById('retryBtn').addEventListener('click', function(){
build();
document.getElementById('quiz').scrollIntoView({behavior:'smooth',block:'start'});
});
build();
})();
</script>
</body>
</html>