Spec
Intent
The validation-side twin of the velocity meter. Validation is the bottleneck DoCoDeGo's DE pillar exists to widen. Render as a mirror-image gauge: when this needle lags the velocity needle, drift accumulates.
Acceptance Criteria
[ ] Same scale as velocity meter for comparability
[ ] Indicates "lag vs velocity" if validation < generation
[ ] Coloured bands match velocity gauge convention
Constraints
Pure SVG, no runtime
Theme-aware via tokens
// validation-meter.astro copy
---
import type { CompendiumMeta } from '../../../data/types';
export const meta: CompendiumMeta = {
id: 33,
slug: 'validation-meter',
name: 'Validation Meter',
category: 'maturity-process',
subcategory: 'instruments',
pillar: 'de',
description: 'Mirror dial showing human validation throughput — the constraint that gates the velocity meter.',
spec: `## Intent
The validation-side twin of the velocity meter. Validation is the bottleneck DoCoDeGo's DE pillar exists to widen. Render as a mirror-image gauge: when this needle lags the velocity needle, drift accumulates.
## Acceptance Criteria
- [ ] Same scale as velocity meter for comparability
- [ ] Indicates "lag vs velocity" if validation < generation
- [ ] Coloured bands match velocity gauge convention
## Constraints
- Pure SVG, no runtime
- Theme-aware via tokens`,
tags: ['svg', 'gauge', 'animated', 'reduced-motion', 'real-content', 'dark-mode-tested'],
status: 'stable',
health: { ics: 90, a11y: 'aa', themed: true, animated: true },
};
const value = 6;
const max = 20;
const velocity = 14;
const lag = velocity - value;
const pct = Math.min(value / max, 1);
const angle = 120 - pct * 240;
const ticks = [0, 25, 50, 75, 100].map((p) => ({ a: 120 - (p / 100) * 240, n: Math.round((p / 100) * max) }));
---
<div data-this-component class="w-full bg-paper text-ink border-4 border-ink overflow-hidden">
<header class="flex items-baseline justify-between px-3 py-2 border-b-2 border-ink">
<span class="font-mono text-[10px] uppercase tracking-widest opacity-60">// validation</span>
<span class="font-mono text-[10px] uppercase tracking-widest text-attention">lag −{lag}</span>
</header>
<div class="p-3 flex flex-col items-center">
<svg viewBox="0 0 200 140" class="w-full max-w-[240px] block" role="img" aria-label={`Validation ${value} per day, lag ${lag} behind velocity`}>
<path d="M 170 110 A 70 70 0 1 0 30 110" fill="none" stroke="var(--color-ink)" stroke-width="3" />
<path d="M 170 110 A 70 70 0 0 0 100 40" fill="none" stroke="var(--color-success)" stroke-width="6" opacity="0.85" />
<path d="M 100 40 A 70 70 0 0 0 60 52" fill="none" stroke="var(--color-do)" stroke-width="6" opacity="0.85" />
<path d="M 60 52 A 70 70 0 0 0 30 110" fill="none" stroke="var(--color-danger)" stroke-width="6" opacity="0.85" />
{ticks.map((t) => {
const rad = ((t.a - 90) * Math.PI) / 180;
const x1 = 100 + Math.cos(rad) * 60, y1 = 100 + Math.sin(rad) * 60;
const x2 = 100 + Math.cos(rad) * 72, y2 = 100 + Math.sin(rad) * 72;
const lx = 100 + Math.cos(rad) * 50, ly = 100 + Math.sin(rad) * 50;
return (
<>
<line x1={x1} y1={y1} x2={x2} y2={y2} stroke="var(--color-ink)" stroke-width="2" />
<text x={lx} y={ly + 3} text-anchor="middle" font-family="JetBrains Mono, monospace" font-size="7" fill="var(--color-ink)" opacity="0.7">{t.n}</text>
</>
);
})}
<g transform={`rotate(${angle} 100 100)`} class="needle">
<polygon points="100,38 96,104 104,104" fill="var(--color-ink)" />
<circle cx="100" cy="100" r="6" fill="var(--color-co)" stroke="var(--color-ink)" stroke-width="2" />
</g>
<text x="100" y="130" text-anchor="middle" font-family="Inter, sans-serif" font-weight="900" font-size="20" fill="var(--color-ink)">{value}</text>
</svg>
<span class="font-mono text-[10px] uppercase tracking-widest opacity-60 -mt-1">validated / day</span>
</div>
</div>
<style>
[data-this-component] .needle { transition: transform 800ms cubic-bezier(0.2,0,0.13,1.4); }
@media (prefers-reduced-motion: reduce) {
[data-this-component] .needle { transition: none; }
}
</style>