574 lines
29 KiB
HTML
574 lines
29 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>The buy-in doc — Know your unknowns</title>
|
||
<style>
|
||
:root {
|
||
--ivory:#FAF9F5; --paper:#FFFFFF; --slate:#141413; --clay:#D97757; --clay-d:#B85C3E;
|
||
--oat:#E3DACC; --olive:#788C5D; --g100:#F0EEE6; --g200:#E6E3DA; --g300:#D1CFC5;
|
||
--g500:#87867F; --g700:#3D3D3A;
|
||
--serif: ui-serif, Georgia, "Times New Roman", Times, serif;
|
||
--sans: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||
--mono: ui-monospace, "SF Mono", Menlo, Monaco, Consolas, monospace;
|
||
}
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
background: var(--ivory); color: var(--slate);
|
||
font-family: var(--sans); line-height: 1.55;
|
||
-webkit-font-smoothing: antialiased;
|
||
}
|
||
.wrap { max-width: 980px; margin: 0 auto; padding: 0 20px; }
|
||
h1, h2, h3 { font-family: var(--serif); font-weight: 500; letter-spacing: -0.015em; }
|
||
|
||
/* ---------- page header ---------- */
|
||
header.page { padding: 56px 0 40px; border-bottom: 1.5px solid var(--g300); position: relative; }
|
||
.eyebrow {
|
||
font-family: var(--mono); font-size: 12px; letter-spacing: 0.12em;
|
||
text-transform: uppercase; color: var(--g500); margin-bottom: 18px;
|
||
display: flex; align-items: center; gap: 12px;
|
||
}
|
||
.eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
|
||
.backlink {
|
||
position: absolute; top: 60px; right: 0;
|
||
font-family: var(--mono); font-size: 12px; color: var(--g500); text-decoration: none;
|
||
}
|
||
.backlink:hover { color: var(--clay-d); }
|
||
header.page h1 { font-size: clamp(34px, 5vw, 52px); line-height: 1.08; margin: 0 0 10px; }
|
||
.lede { font-size: 16.5px; color: var(--g700); max-width: 640px; }
|
||
|
||
/* ---------- prompt box ---------- */
|
||
.promptcard {
|
||
background: var(--paper); border: 1.5px solid var(--g300); border-radius: 14px;
|
||
padding: 22px 26px; margin: 30px 0 0; position: relative;
|
||
}
|
||
.promptcard .label {
|
||
font-family: var(--mono); font-size: 12px; letter-spacing: 0.12em;
|
||
text-transform: uppercase; color: var(--clay); margin-bottom: 12px;
|
||
}
|
||
.promptcard blockquote {
|
||
font-family: var(--serif); font-style: italic; font-size: 17.5px;
|
||
line-height: 1.5; color: var(--slate); max-width: 720px;
|
||
}
|
||
.copybtn {
|
||
position: absolute; top: 18px; right: 18px;
|
||
font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.04em;
|
||
background: var(--g100); color: var(--g700); border: 1.5px solid var(--g300);
|
||
border-radius: 8px; padding: 6px 12px; cursor: pointer;
|
||
}
|
||
.copybtn:hover { border-color: var(--clay); color: var(--clay-d); }
|
||
.copybtn.done { background: var(--olive); border-color: var(--olive); color: #fff; }
|
||
|
||
/* ---------- divider ---------- */
|
||
.divider {
|
||
display: flex; align-items: center; gap: 16px; margin: 44px 0 36px;
|
||
font-family: var(--mono); font-size: 12px; letter-spacing: 0.12em;
|
||
text-transform: uppercase; color: var(--g500);
|
||
}
|
||
.divider::before, .divider::after { content: ""; flex: 1; height: 1px; background: var(--g300); }
|
||
|
||
/* ================= ARTIFACT ================= */
|
||
.artifact {
|
||
background: var(--paper); border: 1.5px solid var(--g300); border-radius: 14px;
|
||
padding: clamp(22px, 4vw, 44px); margin-bottom: 48px;
|
||
}
|
||
.doc-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; flex-wrap: wrap; margin-bottom: 6px; }
|
||
.doc-eyebrow {
|
||
font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.12em;
|
||
text-transform: uppercase; color: var(--g500);
|
||
display: flex; align-items: center; gap: 10px;
|
||
}
|
||
.doc-eyebrow::before { content: ""; width: 24px; height: 1.5px; background: var(--clay); flex: none; }
|
||
.timechip {
|
||
font-family: var(--mono); font-size: 11.5px; color: var(--g700);
|
||
background: var(--oat); border-radius: 999px; padding: 5px 13px; white-space: nowrap;
|
||
display: inline-flex; align-items: center; gap: 7px;
|
||
}
|
||
.timechip::before { content: ""; width: 7px; height: 7px; border-radius: 50%; background: var(--clay); }
|
||
.artifact h2.doc-title { font-size: clamp(26px, 3.6vw, 36px); line-height: 1.12; margin: 10px 0 4px; }
|
||
.doc-sub { color: var(--g500); font-size: 14px; margin-bottom: 26px; }
|
||
.doc-sub strong { color: var(--g700); font-weight: 600; }
|
||
|
||
.artifact h3.sec {
|
||
font-size: 21px; margin: 38px 0 14px; padding-top: 26px;
|
||
border-top: 1.5px solid var(--g200);
|
||
display: flex; align-items: baseline; gap: 10px;
|
||
}
|
||
.artifact h3.sec .n { font-family: var(--mono); font-size: 12px; color: var(--clay); font-weight: 400; }
|
||
|
||
/* ---------- demo player ---------- */
|
||
.demo-shell { position: relative; }
|
||
.demo {
|
||
background: var(--g100); border: 1.5px solid var(--g300); border-radius: 12px;
|
||
overflow: hidden; user-select: none;
|
||
}
|
||
.bl-titlebar {
|
||
display: flex; align-items: center; gap: 8px;
|
||
background: var(--paper); border-bottom: 1.5px solid var(--g200);
|
||
padding: 9px 14px;
|
||
}
|
||
.dot { width: 10px; height: 10px; border-radius: 50%; background: var(--g300); flex: none; }
|
||
.bl-name { font-family: var(--mono); font-size: 11px; color: var(--g500); margin-left: 8px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.bl-body { display: grid; grid-template-columns: 1fr 232px; min-height: 268px; }
|
||
@media (max-width: 620px) { .bl-body { grid-template-columns: 1fr; } .bl-panel { border-left: none !important; border-top: 1.5px solid var(--g200); } }
|
||
|
||
/* video pane */
|
||
.bl-stage { position: relative; padding: 16px 16px 12px; display: flex; flex-direction: column; }
|
||
.bl-video {
|
||
flex: 1; min-height: 148px; border-radius: 8px; position: relative;
|
||
background:
|
||
radial-gradient(120px 80px at 68% 34%, #4a4b45 0%, transparent 70%),
|
||
linear-gradient(160deg, #3D3D3A 0%, #232322 100%);
|
||
border: 1.5px solid var(--g300);
|
||
}
|
||
.bl-play {
|
||
position: absolute; left: 12px; bottom: 10px; width: 0; height: 0;
|
||
border-left: 11px solid rgba(250,249,245,.85); border-top: 7px solid transparent; border-bottom: 7px solid transparent;
|
||
}
|
||
.bl-tc { position: absolute; right: 12px; bottom: 8px; font-family: var(--mono); font-size: 10.5px; color: rgba(250,249,245,.7); }
|
||
.bl-pin {
|
||
position: absolute; top: -7px; width: 14px; height: 14px; border-radius: 50% 50% 50% 2px;
|
||
background: var(--g500); transform: rotate(45deg); transition: background .25s;
|
||
}
|
||
.bl-pin.on { background: var(--clay); }
|
||
.bl-timeline { position: relative; height: 26px; margin-top: 14px; }
|
||
.bl-track { position: absolute; left: 0; right: 0; top: 11px; height: 4px; border-radius: 2px; background: var(--g300); }
|
||
.bl-track .fill { width: 34%; height: 100%; border-radius: 2px; background: var(--g500); }
|
||
|
||
/* annotation panel */
|
||
.bl-panel { background: var(--paper); border-left: 1.5px solid var(--g200); padding: 12px 12px 10px; display: flex; flex-direction: column; }
|
||
.bl-panel-head {
|
||
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
|
||
color: var(--g500); display: flex; justify-content: space-between; align-items: center; margin-bottom: 9px;
|
||
}
|
||
.bl-count { color: var(--clay-d); }
|
||
.bl-ann {
|
||
display: flex; gap: 9px; align-items: flex-start; padding: 8px 8px;
|
||
border-radius: 8px; border: 1.5px solid transparent; transition: background .25s, border-color .25s;
|
||
}
|
||
.bl-ann.sel { background: var(--g100); border-color: var(--g300); }
|
||
.bl-check {
|
||
width: 15px; height: 15px; border-radius: 4px; border: 1.5px solid var(--g300);
|
||
background: var(--paper); flex: none; margin-top: 2px; position: relative; transition: background .2s, border-color .2s;
|
||
}
|
||
.bl-ann.sel .bl-check { background: var(--clay); border-color: var(--clay); }
|
||
.bl-ann.sel .bl-check::after {
|
||
content: ""; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px;
|
||
border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);
|
||
}
|
||
.bl-ann .who { font-size: 11.5px; font-weight: 600; line-height: 1.35; }
|
||
.bl-ann .who .t { font-family: var(--mono); font-weight: 400; font-size: 10px; color: var(--g500); margin-left: 5px; }
|
||
.bl-ann .txt { font-size: 11px; color: var(--g700); line-height: 1.4; }
|
||
.bl-export {
|
||
margin-top: auto; align-self: stretch; text-align: center;
|
||
font-family: var(--sans); font-size: 12.5px; font-weight: 600; color: #fff;
|
||
background: var(--slate); border: none; border-radius: 8px; padding: 9px 0;
|
||
transition: background .2s, transform .12s; pointer-events: none;
|
||
}
|
||
.bl-export.armed { background: var(--clay); }
|
||
.bl-export.pressed { background: var(--clay-d); transform: scale(.965); }
|
||
|
||
/* progress overlay */
|
||
.bl-progress {
|
||
position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
||
background: rgba(20,20,19,.38); opacity: 0; pointer-events: none; transition: opacity .3s;
|
||
}
|
||
.bl-progress.show { opacity: 1; }
|
||
.bl-prog-card {
|
||
background: var(--paper); border-radius: 12px; padding: 20px 24px; width: min(300px, 84%);
|
||
box-shadow: 0 12px 32px rgba(20,20,19,.22);
|
||
}
|
||
.bl-prog-card .pt { font-size: 13px; font-weight: 600; margin-bottom: 4px; }
|
||
.bl-prog-card .ps { font-family: var(--mono); font-size: 10.5px; color: var(--g500); margin-bottom: 12px; min-height: 14px; }
|
||
.bl-bar { height: 6px; border-radius: 3px; background: var(--g200); overflow: hidden; }
|
||
.bl-bar .b { width: 0%; height: 100%; background: var(--clay); border-radius: 3px; transition: width 1.7s cubic-bezier(.3,.7,.3,1); }
|
||
|
||
/* toast */
|
||
.bl-toast {
|
||
position: absolute; left: 50%; bottom: 14px; transform: translate(-50%, 16px);
|
||
background: var(--slate); color: var(--ivory); border-radius: 10px;
|
||
padding: 10px 14px; display: flex; align-items: center; gap: 10px;
|
||
opacity: 0; transition: opacity .35s, transform .35s; max-width: 92%;
|
||
box-shadow: 0 10px 26px rgba(20,20,19,.3);
|
||
}
|
||
.bl-toast.show { opacity: 1; transform: translate(-50%, 0); }
|
||
.bl-tick {
|
||
width: 18px; height: 18px; border-radius: 50%; background: var(--olive); flex: none; position: relative;
|
||
}
|
||
.bl-tick::after {
|
||
content: ""; position: absolute; left: 6px; top: 3.5px; width: 4px; height: 8px;
|
||
border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg);
|
||
}
|
||
.bl-toast .m { font-size: 12px; white-space: nowrap; }
|
||
.bl-chip {
|
||
font-family: var(--mono); font-size: 10.5px; background: rgba(250,249,245,.14);
|
||
border: 1px solid rgba(250,249,245,.25); border-radius: 6px; padding: 3px 8px; white-space: nowrap;
|
||
}
|
||
.demo-caption {
|
||
display: flex; justify-content: space-between; align-items: center; gap: 12px;
|
||
margin-top: 10px; font-family: var(--mono); font-size: 11px; color: var(--g500);
|
||
}
|
||
.replay {
|
||
font-family: var(--mono); font-size: 11px; background: none; border: 1.5px solid var(--g300);
|
||
border-radius: 8px; color: var(--g700); padding: 5px 12px; cursor: pointer;
|
||
}
|
||
.replay:hover { border-color: var(--clay); color: var(--clay-d); }
|
||
|
||
/* ---------- pitch ---------- */
|
||
.pitch { font-family: var(--serif); font-size: 17.5px; line-height: 1.6; max-width: 46em; }
|
||
.pitch strong { font-weight: 600; }
|
||
|
||
/* ---------- objections ---------- */
|
||
.obj { border: 1.5px solid var(--g200); border-radius: 10px; margin-bottom: 10px; overflow: hidden; }
|
||
.obj summary {
|
||
list-style: none; cursor: pointer; padding: 12px 16px; display: flex; gap: 12px; align-items: baseline;
|
||
font-weight: 600; font-size: 14.5px;
|
||
}
|
||
.obj summary::-webkit-details-marker { display: none; }
|
||
.obj summary .q { font-family: var(--mono); font-size: 11px; color: var(--clay); flex: none; }
|
||
.obj summary::after { content: "+"; margin-left: auto; font-family: var(--mono); color: var(--g500); flex: none; }
|
||
.obj[open] summary::after { content: "–"; }
|
||
.obj[open] summary { border-bottom: 1.5px solid var(--g200); background: var(--g100); }
|
||
.obj .a { padding: 13px 16px 14px 16px; font-size: 14px; color: var(--g700); }
|
||
.obj .ref {
|
||
display: inline-block; margin-top: 8px; font-family: var(--mono); font-size: 11.5px;
|
||
color: var(--clay-d); text-decoration: none; border-bottom: 1px dashed var(--g300);
|
||
}
|
||
.obj .ref:hover { border-bottom-color: var(--clay-d); }
|
||
|
||
/* ---------- spec table ---------- */
|
||
.tblwrap { overflow-x: auto; border: 1.5px solid var(--g200); border-radius: 10px; }
|
||
table.spec { width: 100%; border-collapse: collapse; font-size: 13.5px; min-width: 560px; }
|
||
table.spec th, table.spec td { text-align: left; padding: 10px 14px; border-bottom: 1px solid var(--g200); vertical-align: top; }
|
||
table.spec tr:last-child td { border-bottom: none; }
|
||
table.spec th {
|
||
font-family: var(--mono); font-size: 10.5px; letter-spacing: 0.1em; text-transform: uppercase;
|
||
color: var(--g500); font-weight: 400; background: var(--g100);
|
||
}
|
||
table.spec td:first-child { font-weight: 600; white-space: nowrap; width: 140px; }
|
||
table.spec code, .a code, .risk code, .need code {
|
||
font-family: var(--mono); font-size: 0.88em; background: var(--g100);
|
||
border: 1px solid var(--g200); border-radius: 4px; padding: 1px 5px; white-space: nowrap;
|
||
}
|
||
.specref { font-family: var(--mono); font-size: 11px; color: var(--g500); }
|
||
|
||
/* ---------- risk + needs cards ---------- */
|
||
.cardgrid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||
@media (max-width: 720px) { .cardgrid { grid-template-columns: 1fr; } }
|
||
.risk, .need {
|
||
border: 1.5px solid var(--g300); border-radius: 12px; padding: 18px 20px; background: var(--ivory);
|
||
}
|
||
.risk { border-left: 4px solid var(--clay); }
|
||
.need { border-left: 4px solid var(--olive); }
|
||
.risk h4, .need h4 {
|
||
font-family: var(--mono); font-size: 11.5px; letter-spacing: 0.1em; text-transform: uppercase;
|
||
font-weight: 400; margin-bottom: 12px;
|
||
}
|
||
.risk h4 { color: var(--clay-d); }
|
||
.need h4 { color: var(--olive); }
|
||
.risk ul { list-style: none; }
|
||
.risk li { font-size: 13.5px; color: var(--g700); padding-left: 18px; position: relative; margin-bottom: 9px; }
|
||
.risk li::before { content: ""; position: absolute; left: 2px; top: 8px; width: 7px; height: 2px; background: var(--clay); }
|
||
.risk li strong { color: var(--slate); }
|
||
|
||
.approver { display: flex; gap: 11px; align-items: flex-start; padding: 9px 0; border-bottom: 1px solid var(--g200); }
|
||
.approver:last-child { border-bottom: none; }
|
||
.avatar {
|
||
width: 30px; height: 30px; border-radius: 50%; flex: none;
|
||
background: var(--oat); color: var(--clay-d);
|
||
font-family: var(--mono); font-size: 11px; display: flex; align-items: center; justify-content: center;
|
||
}
|
||
.approver .info { font-size: 13px; line-height: 1.4; }
|
||
.approver .info b { display: block; font-size: 13.5px; }
|
||
.approver .info .role { font-family: var(--mono); font-size: 10.5px; color: var(--g500); }
|
||
.approver .info .asks { color: var(--g700); margin-top: 2px; }
|
||
|
||
.bottomline {
|
||
margin-top: 34px; padding: 16px 20px; background: var(--oat); border-radius: 10px;
|
||
font-family: var(--serif); font-size: 16px;
|
||
}
|
||
|
||
footer {
|
||
padding: 26px 0 56px; font-size: 13px; color: var(--g500);
|
||
border-top: 1.5px solid var(--g300);
|
||
}
|
||
footer a { color: var(--clay-d); }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="wrap">
|
||
|
||
<header class="page">
|
||
<a class="backlink" href="index.html">← All examples</a>
|
||
<div class="eyebrow">Know your unknowns · Post-implementation</div>
|
||
<h1>The buy-in doc</h1>
|
||
<p class="lede">After the work is done, the last unknown is other people — ask Claude to package the prototype, spec, and notes into one skimmable doc that answers objections before they're raised.</p>
|
||
|
||
<div class="promptcard">
|
||
<div class="label">The prompt</div>
|
||
<button class="copybtn" id="copyBtn">copy prompt</button>
|
||
<blockquote id="promptText">“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.”</blockquote>
|
||
</div>
|
||
</header>
|
||
|
||
<div class="divider">What Claude produced</div>
|
||
|
||
<!-- ================= ARTIFACT ================= -->
|
||
<article class="artifact">
|
||
<div class="doc-top">
|
||
<div class="doc-eyebrow">Ship proposal · Acme</div>
|
||
<span class="timechip">90-second read</span>
|
||
</div>
|
||
<h2 class="doc-title">Annotation export is ready to ship</h2>
|
||
<p class="doc-sub">Prototype, spec, and rollout plan in one place. <strong>Ask:</strong> four sign-offs by Friday, flag ramp starts Monday.</p>
|
||
|
||
<!-- 1 · DEMO -->
|
||
<h3 class="sec"><span class="n">01</span> Watch it work</h3>
|
||
<div class="demo-shell">
|
||
<div class="demo" id="demo" aria-label="Animated mock of the annotation export flow in Acme">
|
||
<div class="bl-titlebar">
|
||
<span class="dot"></span><span class="dot"></span><span class="dot"></span>
|
||
<span class="bl-name">Acme — q3_launch_teaser_v4.mp4 · Review</span>
|
||
</div>
|
||
<div class="bl-body">
|
||
<div class="bl-stage">
|
||
<div class="bl-video">
|
||
<span class="bl-play"></span>
|
||
<span class="bl-tc">00:42 / 02:05</span>
|
||
</div>
|
||
<div class="bl-timeline">
|
||
<div class="bl-track"><div class="fill"></div></div>
|
||
<span class="bl-pin" id="pin1" style="left: 18%;"></span>
|
||
<span class="bl-pin" id="pin2" style="left: 41%;"></span>
|
||
<span class="bl-pin" id="pin3" style="left: 73%;"></span>
|
||
</div>
|
||
</div>
|
||
<div class="bl-panel">
|
||
<div class="bl-panel-head"><span>Annotations</span><span class="bl-count" id="selCount">0 selected</span></div>
|
||
<div class="bl-ann" id="ann1">
|
||
<span class="bl-check"></span>
|
||
<div>
|
||
<div class="who">Maya R. <span class="t">00:23</span></div>
|
||
<div class="txt">Logo lands too late — pull in 8 frames?</div>
|
||
</div>
|
||
</div>
|
||
<div class="bl-ann" id="ann2">
|
||
<span class="bl-check"></span>
|
||
<div>
|
||
<div class="who">Devon P. <span class="t">00:51</span></div>
|
||
<div class="txt">Color shift here vs. the master grade</div>
|
||
</div>
|
||
</div>
|
||
<div class="bl-ann" id="ann3">
|
||
<span class="bl-check"></span>
|
||
<div>
|
||
<div class="who">Maya R. <span class="t">01:30</span></div>
|
||
<div class="txt">Approved — final CTA reads great</div>
|
||
</div>
|
||
</div>
|
||
<button class="bl-export" id="exportBtn" tabindex="-1">Export selected</button>
|
||
</div>
|
||
</div>
|
||
<div class="bl-progress" id="progress">
|
||
<div class="bl-prog-card">
|
||
<div class="pt">Exporting 3 annotations</div>
|
||
<div class="ps" id="progStatus">Collecting annotations…</div>
|
||
<div class="bl-bar"><div class="b" id="progBar"></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="bl-toast" id="toast">
|
||
<span class="bl-tick"></span>
|
||
<span class="m">Export ready</span>
|
||
<span class="bl-chip">q3_teaser_notes.srt · 3 cues</span>
|
||
</div>
|
||
</div>
|
||
<div class="demo-caption">
|
||
<span id="demoState">select → export → done · loops automatically</span>
|
||
<button class="replay" id="replayBtn">↻ replay</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 2 · PITCH -->
|
||
<h3 class="sec"><span class="n">02</span> The pitch</h3>
|
||
<p class="pitch">Reviewers leave feedback in Acme, then retype it into email, Notion, and edit-bay software — <strong>the single most-cited gap in churn interviews (41% of departed teams)</strong>. Annotation export turns any selection of comments and drawings into SRT, CSV, or PDF in one click, using infrastructure we already run. It's built, flagged, load-tested to 12k annotations per session, and reversible in one toggle. The only thing between here and shipped is the four sign-offs below.</p>
|
||
|
||
<!-- 3 · OBJECTIONS -->
|
||
<h3 class="sec"><span class="n">03</span> What reviewers will ask</h3>
|
||
<details class="obj" open>
|
||
<summary><span class="q">Q1</span>Can this leak internal-only comments to a client?</summary>
|
||
<div class="a">No. Export runs through the same visibility resolver as the review page: annotations marked <code>internal</code> are excluded unless the exporter holds <code>workspace:admin</code> and explicitly opts in via a labeled checkbox. Covered by 14 permission-matrix tests.
|
||
<br><a class="ref" href="#spec">↗ spec §4.2 — Visibility & permissions</a></div>
|
||
</details>
|
||
<details class="obj">
|
||
<summary><span class="q">Q2</span>What happens on a monster session — thousands of annotations?</summary>
|
||
<div class="a">Export paginates at 500 annotations per batch and streams the file. Stress run against the largest real session in prod (a 12,400-annotation agency review) finished in 3.1s; p95 for typical sessions is 340ms.
|
||
<br><a class="ref" href="#spec">↗ implementation notes, 06-24 — load test results</a></div>
|
||
</details>
|
||
<details class="obj">
|
||
<summary><span class="q">Q3</span>Why file formats instead of a public API?</summary>
|
||
<div class="a">SRT + CSV + PDF cover 83% of the 212 support tickets tagged <code>export-request</code> over the last two quarters. An API is the right phase 2 — it needs auth scoping work this doesn't. Files unblock users now without foreclosing the API design.
|
||
<br><a class="ref" href="#spec">↗ metric — support tag analysis, Q1–Q2</a></div>
|
||
</details>
|
||
<details class="obj">
|
||
<summary><span class="q">Q4</span>Does this add new infrastructure we have to operate?</summary>
|
||
<div class="a">Zero new services. SRT/CSV render inline in the existing worker pool (<code>services/render-workers</code>); PDF reuses the report service already generating weekly digests. No new queues, no new datastore, no schema migrations.
|
||
<br><a class="ref" href="#spec">↗ spec §5.1 — Architecture</a></div>
|
||
</details>
|
||
<details class="obj">
|
||
<summary><span class="q">Q5</span>What's the compliance story for exported customer content?</summary>
|
||
<div class="a">Every export is stamped with workspace ID, requesting user, and timestamp in file metadata, and written to the audit log (<code>audit.annotation_export</code>). Files are served via expiring signed URLs (24h) and never stored beyond that window.
|
||
<br><a class="ref" href="#spec">↗ spec §6 — Audit & retention</a></div>
|
||
</details>
|
||
|
||
<!-- 4 · SPEC SUMMARY -->
|
||
<h3 class="sec" id="spec"><span class="n">04</span> Spec at a glance</h3>
|
||
<div class="tblwrap">
|
||
<table class="spec">
|
||
<thead><tr><th>Area</th><th>Decision</th><th>Ref</th></tr></thead>
|
||
<tbody>
|
||
<tr><td>Formats</td><td>SRT (timecoded cues), CSV (flat, incl. drawing bounding boxes), PDF (frame thumbnails + comments)</td><td class="specref">§2</td></tr>
|
||
<tr><td>Entry point</td><td>Annotation panel → multi-select → <b>Export selected</b>; also “Export all” in session menu</td><td class="specref">§3.1</td></tr>
|
||
<tr><td>Permissions</td><td>Requires <code>session:comment</code> or above; <code>internal</code> annotations excluded by default</td><td class="specref">§4.2</td></tr>
|
||
<tr><td>Limits</td><td>500 annotations/batch, streamed; 25k hard cap per export with clear error state</td><td class="specref">§5.3</td></tr>
|
||
<tr><td>Telemetry</td><td><code>export_started</code>, <code>export_completed</code> (format, count, duration), <code>export_failed</code></td><td class="specref">§7</td></tr>
|
||
<tr><td>Feature flag</td><td><code>annotation_export</code> — off → 5% → 50% → 100% over two weeks</td><td class="specref">§8</td></tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 5 · RISK + NEEDS -->
|
||
<h3 class="sec"><span class="n">05</span> Risk, rollback, and what I need from you</h3>
|
||
<div class="cardgrid">
|
||
<div class="risk">
|
||
<h4>Risk & rollback</h4>
|
||
<ul>
|
||
<li><strong>Rollback is one toggle.</strong> Flip <code>annotation_export</code> off — no schema migrations shipped, nothing to unwind.</li>
|
||
<li><strong>Blast radius is bounded.</strong> Export runs in the shared worker pool with a 30s per-job timeout; a runaway job cannot starve review playback.</li>
|
||
<li><strong>Worst credible failure:</strong> orphaned export files if the signed-URL cleanup lags — they self-expire in 24h regardless.</li>
|
||
<li><strong>Known gap:</strong> drawing annotations flatten to bounding boxes in CSV (full vectors are PDF-only). Documented in-product; phase 2 item.</li>
|
||
</ul>
|
||
</div>
|
||
<div class="need">
|
||
<h4>What I need from you</h4>
|
||
<div class="approver">
|
||
<span class="avatar">PN</span>
|
||
<div class="info"><b>Priya Natarajan</b><span class="role">Eng lead, Playback & Review</span>
|
||
<div class="asks">Sign off on reusing the render worker pool (spec §5.1) — the only shared-infra dependency.</div></div>
|
||
</div>
|
||
<div class="approver">
|
||
<span class="avatar">ES</span>
|
||
<div class="info"><b>Elena Sorokin</b><span class="role">Security</span>
|
||
<div class="asks">Approve the visibility rules and audit-log shape (spec §4.2, §6).</div></div>
|
||
</div>
|
||
<div class="approver">
|
||
<span class="avatar">MW</span>
|
||
<div class="info"><b>Marcus Webb</b><span class="role">Design</span>
|
||
<div class="asks">Final pass on the export modal and error states — prototype above is current.</div></div>
|
||
</div>
|
||
<div class="approver">
|
||
<span class="avatar">DK</span>
|
||
<div class="info"><b>Dana Kim</b><span class="role">PM, Collaboration</span>
|
||
<div class="asks">Approve the flag ramp schedule (spec §8) and own the changelog entry.</div></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bottomline">Everything above is built and tested behind the flag. Reply with a ✓ on your piece, or grab 15 minutes on my calendar — <b>target: ramp starts Monday.</b></div>
|
||
</article>
|
||
|
||
<footer>Part of <a href="index.html">Know your unknowns</a> — companion examples to the blog post.</footer>
|
||
</div>
|
||
|
||
<script>
|
||
(function () {
|
||
// ---------- copy prompt ----------
|
||
var PROMPT = "Package the prototype, the spec, and the implementation notes into a single doc I can drop in Slack to get buy-in on shipping annotation export. Lead with the demo.";
|
||
var copyBtn = document.getElementById("copyBtn");
|
||
copyBtn.addEventListener("click", function () {
|
||
function done() {
|
||
copyBtn.textContent = "copied";
|
||
copyBtn.classList.add("done");
|
||
setTimeout(function () {
|
||
copyBtn.textContent = "copy prompt";
|
||
copyBtn.classList.remove("done");
|
||
}, 1600);
|
||
}
|
||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||
navigator.clipboard.writeText(PROMPT).then(done, fallback);
|
||
} else { fallback(); }
|
||
function fallback() {
|
||
var ta = document.createElement("textarea");
|
||
ta.value = PROMPT;
|
||
ta.style.position = "fixed"; ta.style.opacity = "0";
|
||
document.body.appendChild(ta);
|
||
ta.select();
|
||
try { document.execCommand("copy"); } catch (e) {}
|
||
document.body.removeChild(ta);
|
||
done();
|
||
}
|
||
});
|
||
|
||
// ---------- demo animation ----------
|
||
var anns = [document.getElementById("ann1"), document.getElementById("ann2"), document.getElementById("ann3")];
|
||
var pins = [document.getElementById("pin1"), document.getElementById("pin2"), document.getElementById("pin3")];
|
||
var exportBtn = document.getElementById("exportBtn");
|
||
var progress = document.getElementById("progress");
|
||
var progBar = document.getElementById("progBar");
|
||
var progStatus = document.getElementById("progStatus");
|
||
var toast = document.getElementById("toast");
|
||
var selCount = document.getElementById("selCount");
|
||
var timers = [];
|
||
|
||
function at(ms, fn) { timers.push(setTimeout(fn, ms)); }
|
||
|
||
function reset() {
|
||
timers.forEach(clearTimeout);
|
||
timers = [];
|
||
anns.forEach(function (a) { a.classList.remove("sel"); });
|
||
pins.forEach(function (p) { p.classList.remove("on"); });
|
||
exportBtn.classList.remove("armed", "pressed");
|
||
progress.classList.remove("show");
|
||
progBar.style.transition = "none";
|
||
progBar.style.width = "0%";
|
||
// force reflow so the next width change animates
|
||
void progBar.offsetWidth;
|
||
progBar.style.transition = "";
|
||
progStatus.textContent = "Collecting annotations…";
|
||
toast.classList.remove("show");
|
||
selCount.textContent = "0 selected";
|
||
}
|
||
|
||
function play() {
|
||
reset();
|
||
[0, 1, 2].forEach(function (i) {
|
||
at(500 + i * 450, function () {
|
||
anns[i].classList.add("sel");
|
||
pins[i].classList.add("on");
|
||
selCount.textContent = (i + 1) + " selected";
|
||
if (i === 2) exportBtn.classList.add("armed");
|
||
});
|
||
});
|
||
at(2100, function () { exportBtn.classList.add("pressed"); });
|
||
at(2300, function () {
|
||
exportBtn.classList.remove("pressed");
|
||
progress.classList.add("show");
|
||
});
|
||
at(2450, function () { progBar.style.width = "100%"; });
|
||
at(3100, function () { progStatus.textContent = "Rendering SRT cues…"; });
|
||
at(3800, function () { progStatus.textContent = "Signing download URL…"; });
|
||
at(4300, function () {
|
||
progress.classList.remove("show");
|
||
toast.classList.add("show");
|
||
});
|
||
at(7000, play); // loop
|
||
}
|
||
|
||
document.getElementById("replayBtn").addEventListener("click", play);
|
||
play();
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|