Alliance Credibility — preliminary, not for citation
Preliminary model output — not for citation. This table is for finding and comparing alliances; the credibility estimate and its credible intervals live on each alliance’s page. Tiers reflect expert coverage, not precision — gold/silver have expert ratings on file; bronze is LLM-only and provisional. Vintage: 2026-06-25.
median = vals => {
const s = [...vals].sort((a, b) => a - b), n = s.length;
return n % 2 ? s[(n - 1) / 2] : (s[n / 2 - 1] + s[n / 2]) / 2;
}
tierRank = ({gold: 0, silver: 1, bronze: 2})
// Best tier per alliance (D3: roll up to the best member tier for the home view).
bestTierByAlliance = {
const m = new Map();
for (const r of estimates) {
const cur = m.get(r.alliance_id);
if (cur === undefined || tierRank[r.confidence_tier] < tierRank[cur]) m.set(r.alliance_id, r.confidence_tier);
}
return m;
}
// Coarse alliance-level trajectory for the sparkline: median of the actor medians per year.
trajByAlliance = {
const byA = new Map();
for (const r of estimates) {
if (!byA.has(r.alliance_id)) byA.set(r.alliance_id, new Map());
const ym = byA.get(r.alliance_id);
if (!ym.has(r.year)) ym.set(r.year, []);
ym.get(r.year).push(r.irt_median);
}
const out = new Map();
for (const [aid, ym] of byA)
out.set(aid, [...ym.entries()].map(([year, v]) => ({year, val: median(v)})).sort((a, b) => a.year - b.year));
return out;
}
// Shared y-domain so sparkline shapes are comparable across rows.
yDomain = estimates.reduce((a, d) => [Math.min(a[0], d.irt_median), Math.max(a[1], d.irt_median)], [Infinity, -Infinity])
allianceSummary = meta.map(d => ({
atopid: d.atopid, name: d.name, members: d.members, n_members: d.n_members,
type: d.type, region: d.region, era: d.era, begyr: d.begyr, endyr: d.endyr,
tier: bestTierByAlliance.get(d.atopid) ?? "bronze",
traj: trajByAlliance.get(d.atopid) ?? []
}))filtered = allianceSummary.filter(d =>
tierFilter.includes(d.tier) &&
(regionFilter === "All" || d.region === regionFilter) &&
(search.trim() === "" ||
d.name.toLowerCase().includes(search.toLowerCase()) ||
d.members.some(m => m.toLowerCase().includes(search.toLowerCase())))
).sort((a, b) => a.name.localeCompare(b.name))sparkline = traj => traj.length < 2
? html`<span style="color:#ccc">—</span>`
: Plot.plot({
width: 130, height: 26, margin: 0,
x: {axis: null}, y: {domain: yDomain, axis: null},
marks: [Plot.line(traj, {x: "year", y: "val", stroke: "#1f5fa8", strokeWidth: 1, strokeOpacity: 0.85})]
})
tierBadge = t => {
const c = {gold: "#9a7400", silver: "#6b6b6b", bronze: "#9a9a9a"};
return html`<span style="background:${c[t]}22; color:${c[t]}; padding:1px 8px; border-radius:10px; font-size:0.76rem; font-weight:600">${t}</span>`;
}html`<table style="border-collapse:collapse; width:100%; font-size:0.9rem">
<thead>
<tr style="border-bottom:2px solid #ccc; text-align:left">
<th style="padding:5px 8px">Alliance</th>
<th style="padding:5px 8px">Trajectory</th>
<th style="padding:5px 8px">Tier</th>
<th style="padding:5px 8px">Type</th>
<th style="padding:5px 8px">Region</th>
<th style="padding:5px 8px">Years</th>
<th style="padding:5px 8px; text-align:right">Members</th>
</tr>
</thead>
<tbody>
${filtered.map(d => html`<tr style="border-bottom:1px solid #eee; ${d.tier === 'bronze' ? 'opacity:0.6' : ''}">
<td style="padding:5px 8px"><a href="alliance.html?atopid=${d.atopid}">${d.name}</a></td>
<td style="padding:3px 8px">${sparkline(d.traj)}</td>
<td style="padding:5px 8px">${tierBadge(d.tier)}${d.tier === 'bronze' ? html` <span style="font-size:0.7rem; color:#a15c00">LLM-only</span>` : ''}</td>
<td style="padding:5px 8px">${d.type}</td>
<td style="padding:5px 8px">${d.region}</td>
<td style="padding:5px 8px; white-space:nowrap">${d.begyr}–${d.endyr}</td>
<td style="padding:5px 8px; text-align:right">${d.n_members}</td>
</tr>`)}
</tbody>
</table>`