SPB Git forge

spb/market-atlas

Public
12commits 1branches 0releases
1.1 MBsize
maindefault branch
10 days agolast push
TypeScript 96.7% SQL 1.6% CSS 0.8% JavaScript 0.5%

fix: 'coverage/' ignore/sync pattern swallowed apps/web/src/app/coverage (route 404 in prod); anchor to repo root

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 11 days ago (Sep 13, 2026) parent 17ead54

3 changed files +262 −2

modified .gitignore +1 −1
@@ -16,6 +16,6 @@ deploy/.admin-token
16 16 deploy/.hfmd-key
17 17 deploy/rendered/
18 18 apps/web/qa/screens/
19 −coverage/
19 +/coverage/
20 20 apps/web/AGENTS.md
21 21 apps/web/CLAUDE.md
added apps/web/src/app/coverage/page.tsx +260 −0
@@ -0,0 +1,260 @@
1 +import type { Metadata } from "next";
2 +import Link from "next/link";
3 +import { RedundancyPanel } from "@/components/market/redundancy-panel";
4 +import { Empty, Page, PageHeader, Pill, Section, Stat } from "@/components/ui/section";
5 +import { api } from "@/lib/api";
6 +import { COVERAGE_STATUS_LABEL, COVERAGE_STATUS_TONE, TIER_LABEL, TIER_TARGET } from "@/lib/coverage";
7 +import { ASSET_CLASS_LABEL, cx, instrumentHref } from "@/lib/format";
8 +import type { CoverageReport, CoverageTier } from "@/lib/types";
9 +
10 +export const metadata: Metadata = {
11 + title: "Source coverage",
12 + description: "How many independent source families observe each instrument, redundancy targets by tier, the source-expansion queue and statistically inferred shared upstreams.",
13 + alternates: { canonical: "/coverage" },
14 +};
15 +export const dynamic = "force-dynamic";
16 +
17 +const TIERS: CoverageTier[] = ["A", "B", "C", "D"];
18 +const CLASSES = ["EQUITY", "ETF", "INDEX", "CRYPTO", "FOREX", "COMMODITY", "FUTURE", "TREASURY", "BOND", "INTEREST_RATE"];
19 +
20 +export default async function CoveragePage({ searchParams }: { searchParams: Promise<Record<string, string | string[] | undefined>> }) {
21 + const sp = await searchParams;
22 + const tier = typeof sp.tier === "string" && TIERS.includes(sp.tier as CoverageTier) ? (sp.tier as CoverageTier) : undefined;
23 + const cls = typeof sp.asset_class === "string" && CLASSES.includes(sp.asset_class) ? sp.asset_class : undefined;
24 + const qs = new URLSearchParams({ limit: "200" });
25 + if (tier) qs.set("tier", tier);
26 + if (cls) qs.set("asset_class", cls);
27 + const [report, global] = await Promise.all([api<CoverageReport>(`/v1/coverage?${qs}`), tier || cls ? api<CoverageReport>("/v1/coverage?limit=1") : null]);
28 + const s = report.summary;
29 + const g = (global ?? report).summary;
30 + const href = (t?: CoverageTier, c?: string) => {
31 + const p = new URLSearchParams();
32 + if (t) p.set("tier", t);
33 + if (c) p.set("asset_class", c);
34 + const q = p.toString();
35 + return `/coverage${q ? `?${q}` : ""}`;
36 + };
37 + const byClass = Object.entries(s.byAssetClass).sort((a, b) => b[1].instruments - a[1].instruments);
38 + return (
39 + <Page wide>
40 + <PageHeader
41 + kicker="Source Mesh"
42 + title="Source coverage"
43 + lead="One instrument → many observers → one canonical market state. Market Atlas does not trust a quote: it observes the market through independent paths, counts source families (not URLs), and publishes the consensus with the evidence. This page measures that redundancy and lists where it is still thin."
44 + actions={
45 + <Link href="/methodology#source-mesh" className="text-sm text-accent hover:underline">
46 + How it is computed →
47 + </Link>
48 + }
49 + />
50 + <div className="grid grid-cols-1 gap-6 lg:grid-cols-[1.2fr_1fr]">
51 + <RedundancyPanel coverage={g} prominent />
52 + <div className="rounded-md border border-rule bg-surface p-4">
53 + <h3 className="text-[11px] font-medium uppercase tracking-wide text-ink-3">Redundancy targets by tier</h3>
54 + <div className="mt-2 divide-y divide-rule">
55 + {TIERS.map((t) => {
56 + const tt = g.tiers[t];
57 + return (
58 + <div key={t} className="grid grid-cols-[auto_1fr_auto] items-center gap-3 py-2 text-sm">
59 + <Link href={href(t, cls)} className={cx("mono inline-flex h-7 w-7 items-center justify-center rounded border text-xs font-semibold", tier === t ? "border-ink bg-ink text-canvas" : "border-rule text-ink-2 hover:border-rule-strong")}>
60 + {t}
61 + </Link>
62 + <div className="min-w-0">
63 + <div className="font-medium">{TIER_LABEL[t].replace(/^Tier [A-D] · /, "")}</div>
64 + <div className="truncate text-[11.5px] text-ink-3">{report.targets[t] ?? TIER_TARGET[t]}</div>
65 + <div className="mt-1 h-1 overflow-hidden rounded-full bg-surface-3">
66 + <span className={cx("block h-full rounded-full", tt.attainment >= 90 ? "bg-positive" : tt.attainment >= 50 ? "bg-accent" : "bg-warning")} style={{ width: `${Math.min(100, tt.attainment)}%` }} />
67 + </div>
68 + </div>
69 + <div className="text-right">
70 + <div className="mono tnum">
71 + {tt.covered} / {tt.instruments}
72 + </div>
73 + <div className="text-[11px] text-ink-3">{tt.attainment.toFixed(1)}% covered</div>
74 + </div>
75 + </div>
76 + );
77 + })}
78 + </div>
79 + </div>
80 + </div>
81 +
82 + <Section title="By asset class" hint="quoted instruments · share observed by ≥ 2 families · mean independent families">
83 + <div className="overflow-x-auto rounded-md border border-rule bg-surface">
84 + <table className="table-dense">
85 + <thead>
86 + <tr>
87 + <th>Asset class</th>
88 + <th className="text-right">Quoted</th>
89 + <th className="text-right">Multi-source</th>
90 + <th className="text-right">Share</th>
91 + <th className="text-right">Mean families</th>
92 + <th />
93 + </tr>
94 + </thead>
95 + <tbody>
96 + {byClass.map(([k, v]) => (
97 + <tr key={k}>
98 + <td className="font-medium">{ASSET_CLASS_LABEL[k] ?? k}</td>
99 + <td className="num">{v.instruments}</td>
100 + <td className="num">{v.multiSource}</td>
101 + <td className="num">
102 + <span className="inline-flex items-center gap-2">
103 + <span className="inline-block h-1.5 w-16 overflow-hidden rounded-full bg-surface-3">
104 + <span className={cx("block h-full rounded-full", v.instruments && v.multiSource / v.instruments >= 0.9 ? "bg-positive" : v.multiSource ? "bg-accent" : "bg-stale")} style={{ width: `${v.instruments ? (v.multiSource / v.instruments) * 100 : 0}%` }} />
105 + </span>
106 + {v.instruments ? `${Math.round((v.multiSource / v.instruments) * 100)}%` : "—"}
107 + </span>
108 + </td>
109 + <td className="num">{v.meanFamilies.toFixed(2)}</td>
110 + <td className="text-right">
111 + <Link href={href(tier, k)} className="text-xs text-accent hover:underline">
112 + queue →
113 + </Link>
114 + </td>
115 + </tr>
116 + ))}
117 + </tbody>
118 + </table>
119 + </div>
120 + </Section>
121 +
122 + <Section
123 + title="Source expansion queue"
124 + hint="instruments below their redundancy target, most valuable gaps first (tier weight × missing families)"
125 + badge={
126 + <span className="mono rounded bg-surface-3 px-1.5 py-0.5 text-[10.5px] text-ink-2 tnum">
127 + {report.queue.length}
128 + {report.queue.length >= 200 ? "+" : ""}
129 + </span>
130 + }
131 + >
132 + <div className="mb-3 flex flex-wrap items-center gap-2">
133 + <span className="text-xs text-ink-3">Tier</span>
134 + <Pill href={href(undefined, cls)} active={!tier}>
135 + all
136 + </Pill>
137 + {TIERS.map((t) => (
138 + <Pill key={t} href={href(t, cls)} active={tier === t}>
139 + {t}
140 + </Pill>
141 + ))}
142 + <span className="ml-2 text-xs text-ink-3">Class</span>
143 + <Pill href={href(tier)} active={!cls}>
144 + all
145 + </Pill>
146 + {CLASSES.filter((c) => s.byAssetClass[c] || c === cls).map((c) => (
147 + <Pill key={c} href={href(tier, c)} active={cls === c}>
148 + {ASSET_CLASS_LABEL[c] ?? c}
149 + </Pill>
150 + ))}
151 + </div>
152 + {report.queue.length ? (
153 + <div className="overflow-x-auto rounded-md border border-rule bg-surface">
154 + <table className="table-dense">
155 + <thead>
156 + <tr>
157 + <th>#</th>
158 + <th>Instrument</th>
159 + <th>Tier</th>
160 + <th className="text-right">Families / target</th>
161 + <th className="text-right">Missing</th>
162 + <th className="text-right">Observations</th>
163 + <th className="text-right">Proxies · validators</th>
164 + <th>Class</th>
165 + <th>Status</th>
166 + <th className="text-right">Score</th>
167 + </tr>
168 + </thead>
169 + <tbody>
170 + {report.queue.map((q, i) => (
171 + <tr key={q.instrumentId}>
172 + <td className="mono text-ink-3">{i + 1}</td>
173 + <td>
174 + <Link href={instrumentHref(q.instrumentId)} className="hover:underline">
175 + <span className="mono font-medium">{q.symbol}</span>
176 + <span className="block max-w-[260px] truncate text-[11.5px] text-ink-3">{q.name}</span>
177 + </Link>
178 + </td>
179 + <td>
180 + <span className="mono rounded border border-rule px-1.5 text-xs" title={TIER_TARGET[q.tier]}>
181 + {q.tier}
182 + </span>
183 + </td>
184 + <td className="num">
185 + {q.families} / {q.target.families}
186 + </td>
187 + <td className="num text-warning">{q.gap > 0 ? `−${q.gap}` : "—"}</td>
188 + <td className="num">{q.observations}</td>
189 + <td className="num text-ink-2">
190 + {q.proxies} · {q.validators}
191 + </td>
192 + <td className="text-xs text-ink-2">
193 + {ASSET_CLASS_LABEL[q.assetClass] ?? q.assetClass}
194 + {q.comparability ? <span className="block text-[10.5px] text-ink-3">{q.comparability.toLowerCase()} class</span> : null}
195 + </td>
196 + <td>
197 + <span className={cx("inline-flex h-5 items-center rounded-[3px] px-1.5 text-[10.5px] font-medium uppercase tracking-wide", COVERAGE_STATUS_TONE[q.status])}>{COVERAGE_STATUS_LABEL[q.status]}</span>
198 + </td>
199 + <td className="num">{Math.round(q.score * 100)}</td>
200 + </tr>
201 + ))}
202 + </tbody>
203 + </table>
204 + </div>
205 + ) : (
206 + <Empty>Every instrument in this selection meets its redundancy target.</Empty>
207 + )}
208 + <p className="mt-2 text-xs text-ink-3">
209 + A <em>family</em> is a group of sources believed to share an upstream (declared, or inferred statistically). Proxies (stablecoin markets, crosses) and validators (restricted-rights sources) confirm a value but never count as independent families. Score = 65 % family attainment + 35 % observation attainment.
210 + </p>
211 + </Section>
212 +
213 + <Section title="Likely shared upstreams" hint="pairs of sources whose values coincide within 2 s on ≥ 92 % of ≥ 120 aligned samples — counted as one family">
214 + {report.lineage.length ? (
215 + <div className="overflow-x-auto rounded-md border border-rule bg-surface">
216 + <table className="table-dense">
217 + <thead>
218 + <tr>
219 + <th>Source A</th>
220 + <th>Source B</th>
221 + <th className="text-right">Similarity</th>
222 + <th className="text-right">Aligned samples</th>
223 + <th className="text-right">Instruments</th>
224 + </tr>
225 + </thead>
226 + <tbody>
227 + {report.lineage.map((p) => (
228 + <tr key={`${p.sourceA}|${p.sourceB}`}>
229 + <td>
230 + <Link href={`/sources#${p.sourceA}`} className="mono hover:underline">
231 + {p.sourceA}
232 + </Link>
233 + </td>
234 + <td>
235 + <Link href={`/sources#${p.sourceB}`} className="mono hover:underline">
236 + {p.sourceB}
237 + </Link>
238 + </td>
239 + <td className="num">{(p.similarity * 100).toFixed(1)}%</td>
240 + <td className="num">{p.samples.toLocaleString("en-US")}</td>
241 + <td className="num">{p.instruments}</td>
242 + </tr>
243 + ))}
244 + </tbody>
245 + </table>
246 + </div>
247 + ) : (
248 + <Empty>No pair of sources currently looks like a shared upstream. The lineage engine recomputes every five minutes from live observations.</Empty>
249 + )}
250 + </Section>
251 +
252 + <div className="mt-8 grid grid-cols-2 gap-4 border-t border-rule pt-6 sm:grid-cols-4">
253 + <Stat label="Quoted instruments" value={g.quoted.toLocaleString("en-US")} />
254 + <Stat label="≥ 3 families" value={g.byFamilies[">=3"]} sub="Tier-A target" />
255 + <Stat label="≥ 5 families" value={g.byFamilies[">=5"]} sub="most observed" />
256 + <Stat label="No fresh source" value={g.byFamilies["0"]} tone={g.byFamilies["0"] ? "warn" : "muted"} sub="last known value shown as stale" />
257 + </div>
258 + </Page>
259 + );
260 +}
modified deploy/market-atlas.mld.json +1 −1
@@ -21,7 +21,7 @@
21 21 "data/",
22 22 "/tmp/",
23 23 "logs/",
24 − "coverage/",
24 + "/coverage/",
25 25 ".DS_Store",
26 26 ".claude/",
27 27 "deploy/.admin-token",
28 28