- apps/desktop: electron-vite + React + Tailwind mapped onto design tokens (preflight off; tokens/base.css owns the reset); boots to a Reginald placeholder proving fonts/tokens/core wiring - packages/core: pure TS; gitea label schema (est/*, p/*, deadline/hard) with pessimistic conflict resolution + 15 unit tests - docs/design: full design handoff (tokens, 16 component contracts, interactive 14-screen prototype, Reginald voice rules) - docs/PLAN.md: product plan (purity rule, pm-state repo, deterministic scheduler + Monte Carlo, directive log) - Deliberate deviation from novelpad stack: no ElectricSQL/PGlite — local store is a rebuildable cache over gitea REST/webhooks (better-sqlite3 in main process, arriving in P1) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
129 lines
7.2 KiB
Plaintext
129 lines
7.2 KiB
Plaintext
// Calibration report — estimate-vs-actual evidence behind the cones
|
||
function CalibrationScreen({ onBack }) {
|
||
const DS = window.CommiTeaDesignSystem_20e63b;
|
||
const { Card, Badge, Icon } = DS;
|
||
const c = window.CT_DATA.calibration;
|
||
|
||
// scatter chart geometry
|
||
const W = 420, H = 300, pad = { l: 36, r: 16, t: 14, b: 30 };
|
||
const maxD = 9;
|
||
const X = (d) => pad.l + (d / maxD) * (W - pad.l - pad.r);
|
||
const Y = (d) => H - pad.b - (d / maxD) * (H - pad.t - pad.b);
|
||
|
||
const BiasBar = ({ bias }) => {
|
||
if (bias == null) return <span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)' }}>n too small</span>;
|
||
return (
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 8, flex: 1 }}>
|
||
<div style={{ position: 'relative', flex: 1, height: 10, background: 'var(--paper-2)', borderRadius: 3 }}>
|
||
<div style={{ position: 'absolute', left: '30%', top: -2, bottom: -2, width: 1.5, background: 'var(--line-2)' }}></div>
|
||
<div style={{
|
||
position: 'absolute', left: '30%', top: 1.5, height: 7, width: `${Math.min(bias * 1.6, 66)}%`,
|
||
background: bias > 15 ? 'var(--warn)' : 'var(--ok)', borderRadius: '0 3px 3px 0', opacity: 0.75,
|
||
}}></div>
|
||
</div>
|
||
<span style={{ font: '500 11.5px var(--font-mono)', color: bias > 15 ? 'var(--warn)' : 'var(--ok)', width: 42, textAlign: 'right' }}>+{bias}%</span>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 16 }}>
|
||
<div>
|
||
<button type="button" onClick={onBack} style={{
|
||
display: 'inline-flex', alignItems: 'center', gap: 6, background: 'none', border: 'none',
|
||
font: '500 12.5px var(--font-sans)', color: 'var(--ink-2)', cursor: 'pointer', padding: '2px 0', marginBottom: 10,
|
||
}}>
|
||
<Icon name="arrow-left" size={14} /> Runway
|
||
</button>
|
||
<header style={{ borderBottom: 'var(--rule-double)', paddingBottom: 14, display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 12 }}>
|
||
<div>
|
||
<h1 style={{ font: 'var(--text-display)', color: 'var(--ink-1)', margin: 0 }}>Calibration</h1>
|
||
<p style={{ font: 'var(--text-data)', color: 'var(--ink-3)', margin: '6px 0 0', whiteSpace: 'nowrap' }}>{c.n} closed issues with estimates · evidence, not opinion</p>
|
||
</div>
|
||
<Badge tone="ok" dot>curve active · n ≥ 20</Badge>
|
||
</header>
|
||
</div>
|
||
|
||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 14, alignItems: 'start' }}>
|
||
{/* scatter */}
|
||
<Card overline="Estimate vs actual" title="The shape of hope" jade>
|
||
<svg width="100%" viewBox={`0 0 ${W} ${H}`} style={{ display: 'block' }}>
|
||
{[1, 3, 5, 8].map((d) => (
|
||
<React.Fragment key={d}>
|
||
<line x1={X(d)} x2={X(d)} y1={pad.t} y2={H - pad.b} stroke="var(--line-1)" strokeWidth="1" />
|
||
<text x={X(d)} y={H - 12} textAnchor="middle" style={{ font: '400 10px var(--font-mono)', fill: 'var(--ink-3)' }}>{d}d</text>
|
||
<line x1={pad.l} x2={W - pad.r} y1={Y(d)} y2={Y(d)} stroke="var(--line-1)" strokeWidth="1" />
|
||
<text x={pad.l - 6} y={Y(d) + 3} textAnchor="end" style={{ font: '400 10px var(--font-mono)', fill: 'var(--ink-3)' }}>{d}d</text>
|
||
</React.Fragment>
|
||
))}
|
||
{/* perfect line */}
|
||
<line x1={X(0)} y1={Y(0)} x2={X(maxD)} y2={Y(maxD)} stroke="var(--line-2)" strokeWidth="1.2" strokeDasharray="4 4" />
|
||
<text x={X(7.6)} y={Y(7.6) + 14} style={{ font: '400 10px var(--font-mono)', fill: 'var(--ink-3)' }}>honest</text>
|
||
{/* fit */}
|
||
<line x1={X(0)} y1={Y(0)} x2={X(maxD / c.fit)} y2={Y(maxD)} stroke="var(--cone-line)" strokeWidth="1.6" />
|
||
<text x={X(4.1)} y={Y(4.1 * c.fit) - 8} style={{ font: '500 10px var(--font-mono)', fill: 'var(--cone-line)' }}>you · ×{c.fit}</text>
|
||
{/* points */}
|
||
{c.scatter.map(([e, a], i) => (
|
||
<circle key={i} cx={X(e) + ((i % 5) - 2) * 3} cy={Y(a)} r="3" fill="var(--accent)" opacity="0.55" />
|
||
))}
|
||
</svg>
|
||
<p style={{ font: 'var(--text-caption)', color: 'var(--ink-3)', margin: '8px 0 0' }}>estimated (x) vs actual days (y) · actuals inferred from git events, never tracked</p>
|
||
</Card>
|
||
|
||
<div style={{ display: 'flex', flexDirection: 'column', gap: 14 }}>
|
||
{/* per-label bias */}
|
||
<Card overline="Bias by estimate label" flush>
|
||
<div>
|
||
{c.labels.map((r, i) => (
|
||
<div key={r.label} style={{
|
||
display: 'flex', alignItems: 'center', gap: 14, padding: '10px 20px',
|
||
borderTop: i === 0 ? 'none' : '1px solid var(--line-1)',
|
||
}}>
|
||
<span style={{ font: '500 11.5px var(--font-mono)', color: 'var(--ink-1)', width: 52 }}>{r.label}</span>
|
||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)', width: 66 }}>n={r.n} · {r.median}</span>
|
||
<BiasBar bias={r.bias} />
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* per-person */}
|
||
<Card overline="By person" flush>
|
||
<div>
|
||
{c.people.map((p, i) => (
|
||
<div key={p.who} style={{
|
||
display: 'flex', alignItems: 'center', gap: 12, padding: '11px 20px',
|
||
borderTop: i === 0 ? 'none' : '1px solid var(--line-1)',
|
||
}}>
|
||
<span style={{
|
||
width: 24, height: 24, borderRadius: '50%', background: 'var(--spruce-2)', color: 'var(--accent-text)',
|
||
font: '600 9px/24px var(--font-sans)', textAlign: 'center', flexShrink: 0,
|
||
}}>{p.who.split(' ').map((w) => w[0]).join('')}</span>
|
||
<div style={{ flex: 1, minWidth: 0 }}>
|
||
<span style={{ font: 'var(--text-body-strong)', color: 'var(--ink-1)' }}>{p.who}</span>
|
||
<span style={{ font: '400 11px var(--font-mono)', color: 'var(--ink-3)' }}> · n={p.n} · {p.note}</span>
|
||
</div>
|
||
<span style={{ font: '500 12px var(--font-mono)', color: p.bias > 15 ? 'var(--warn)' : 'var(--ok)' }}>+{p.bias}%</span>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</Card>
|
||
|
||
{/* effect on forecasts */}
|
||
<Card overline="What this does to your forecasts">
|
||
<div style={{ display: 'flex', alignItems: 'center', gap: 10, flexWrap: 'wrap' }}>
|
||
<span style={{ font: '400 12.5px var(--font-mono)', color: 'var(--ink-2)', whiteSpace: 'nowrap' }}>{c.effect.raw}</span>
|
||
<Icon name="arrow-right" size={14} style={{ color: 'var(--ink-3)' }} />
|
||
<span style={{ font: '500 12.5px var(--font-mono)', color: 'var(--ink-1)', whiteSpace: 'nowrap' }}>{c.effect.banded}</span>
|
||
</div>
|
||
<p style={{ font: 'var(--text-agent)', color: 'var(--ink-2)', margin: '10px 0 0' }}>
|
||
You are not bad at estimating; you are optimistic in a very stable way. Stable, I can work with.
|
||
</p>
|
||
</Card>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
Object.assign(window, { CalibrationScreen });
|