Archilyzer · Source

archilyzer

Archilyzer
git clone https://archilyzer.pages.dev/source/archilyzer.git
Log | Files | Refs | README | LICENSE

commit cac99e26d8c7c8917c911623256d21c593bff02c
parent a232026885547ba8768a12824a830bd44c26815e
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 26 Sep 2026 02:24:53 -0400

common: officialInstances — the hub's official instances in the homepage's order, one colour each

hubSummary.ts orders the members by the summary's sites (the homepage's
order, transcripts first) and gives each its accent, else the summary's,
else seriesColor() at its summary index, the colour its homepage card and
chart layer wear. A member the summary does not name follows, in colours
after the summary's. Four unit tests.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

Diffstat:
Mcommon/lib/hubSummary.test.ts | 104+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/hubSummary.ts | 53++++++++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 156 insertions(+), 1 deletion(-)

diff --git a/common/lib/hubSummary.test.ts b/common/lib/hubSummary.test.ts @@ -3,9 +3,12 @@ import assert from "node:assert/strict"; import { HUB_SUMMARY_VERSION, hubSummarySiteFor, + officialInstances, parseHubSummary, toHubSummary, + type HubSummary, } from "./hubSummary"; +import { seriesColor } from "./homepageChart"; import type { HomepageSummary } from "./homepageSummary"; // Run with: @@ -139,3 +142,104 @@ test("hubSummarySiteFor matches by siteId, then by origin", () => { assert.equal(hubSummarySiteFor(hub, { siteId: "gamma", origin: "https://gamma.example" }), null); assert.equal(hubSummarySiteFor(null, { siteId: "alpha" }), null); }); + +// The hub's official instances, three of them, as hub-sites.json lists them +// (alphabetically), none with an accent of its own. +function pool() { + return [ + { siteId: "anilyzer", origin: "https://anilyzer.example" }, + { siteId: "hasanalyzer", origin: "https://hasanalyzer.example" }, + { siteId: "jeralyzer", origin: "https://jeralyzer.example" }, + ]; +} + +// The homepage's order: transcripts, most first. +function homepageOrder(): HubSummary { + const site = (siteId: string, transcripts: number) => ({ + siteId, + siteTitle: siteId, + siteUrl: `https://${siteId}.example`, + transcripts, + }); + return { + version: HUB_SUMMARY_VERSION, + generatedAt: "2026-09-26T00:00:00.000Z", + official: null, + sites: [site("jeralyzer", 300), site("anilyzer", 200), site("hasanalyzer", 100)], + }; +} + +test("officialInstances lists the members in the summary's order, each in its homepage colour", () => { + const out = officialInstances(pool(), homepageOrder()); + assert.deepEqual( + out.map((o) => [o.site.siteId, o.figures?.transcripts, o.accent]), + [ + ["jeralyzer", 300, seriesColor(0)], + ["anilyzer", 200, seriesColor(1)], + ["hasanalyzer", 100, seriesColor(2)], + ], + ); +}); + +test("officialInstances: with no summary the given order stands, coloured by place", () => { + const out = officialInstances(pool(), null); + assert.deepEqual( + out.map((o) => [o.site.siteId, o.figures, o.accent]), + [ + ["anilyzer", null, seriesColor(0)], + ["hasanalyzer", null, seriesColor(1)], + ["jeralyzer", null, seriesColor(2)], + ], + ); +}); + +test("officialInstances: an accent the site sets wins; its place still follows the summary", () => { + const members = pool().map((m) => + m.siteId === "anilyzer" ? { ...m, accent: "#e6a1c0" } : m, + ); + const summary = homepageOrder(); + summary.sites[2] = { ...summary.sites[2], accent: "#b49cf2" }; // hasanalyzer + const out = officialInstances(members, summary); + assert.deepEqual( + out.map((o) => [o.site.siteId, o.accent]), + [ + ["jeralyzer", seriesColor(0)], + ["anilyzer", "#e6a1c0"], // hub-sites.json's own + ["hasanalyzer", "#b49cf2"], // the summary's, when hub-sites.json has none + ], + ); +}); + +test("officialInstances: a member the summary does not name follows, in colours no named one wears", () => { + const members = [ + { siteId: "zeta", origin: "https://zeta.example" }, + ...pool(), + { siteId: "alpha", origin: "https://alpha.example" }, + ]; + // The summary also names a site the hub does not list (index 1): its colour + // stays its own, and is not handed to an unnamed member. + const summary = homepageOrder(); + summary.sites.splice(1, 0, { + siteId: "gone", + siteTitle: "gone", + siteUrl: "https://gone.example", + }); + const out = officialInstances(members, summary); + assert.deepEqual( + out.map((o) => [o.site.siteId, o.accent]), + [ + ["jeralyzer", seriesColor(0)], + ["anilyzer", seriesColor(2)], + ["hasanalyzer", seriesColor(3)], + ["zeta", seriesColor(4)], + ["alpha", seriesColor(5)], + ], + ); + // Matched by origin when the id changed between builds. + const renamed = officialInstances( + [{ siteId: "jer", origin: "https://jeralyzer.example" }], + homepageOrder(), + ); + assert.equal(renamed[0].figures?.siteId, "jeralyzer"); + assert.equal(renamed[0].accent, seriesColor(0)); +}); diff --git a/common/lib/hubSummary.ts b/common/lib/hubSummary.ts @@ -13,12 +13,14 @@ // never an error. It is read same-origin, so it is not in HUB_CORS_PATHS, and // the deploy guard (builtHubProblem) does not require it. // -// Pure and client-safe: the hub page parses it in the browser. +// Pure and client-safe: the hub page parses it in the browser, and lists and +// colours its official instances by it (officialInstances, below). import type { HomepageOfficialTotals, HomepageSummary, } from "./homepageSummary"; +import { seriesColor } from "./homepageChart"; export const HUB_SUMMARY_FILE = "hub-summary.json"; export const HUB_SUMMARY_VERSION = 1; @@ -162,3 +164,52 @@ export function hubSummarySiteFor( }) ?? null ); } + +// One official instance as every hub surface shows it: its figures, and the +// colour it wears on its card's stripe, its scope chip's dot, its results' left +// edge and its channel group. +export type OfficialInstance<T> = { + site: T; + figures: HubSummarySite | null; + accent: string; +}; + +// The official instances in the homepage's order, each with one colour. +// +// ORDER: the summary's `sites` order — the homepage's (transcripts, most first: +// homepageSummary.ts), the order its ArchiveCards and growth chart draw in, so +// the hub and the homepage list the instances on the same rows. A member the +// summary does not name follows, in the order given. With no summary the order +// given (hub-sites.json's) stands. +// +// COLOUR: the site's own accent, else the summary's, else seriesColor() at its +// index in the summary — the colour its homepage card and chart layer wear. A +// member the summary does not name takes the colours after the summary's, so it +// never wears one a named instance wears. With no summary that is its place in +// the list. +export function officialInstances< + T extends { siteId: string; origin?: string; accent?: string }, +>(members: readonly T[], summary: HubSummary | null): OfficialInstance<T>[] { + const placed = members.map((site, given) => { + const figures = hubSummarySiteFor(summary, site); + const at = figures && summary ? summary.sites.indexOf(figures) : -1; + return { site, figures, at, given }; + }); + const named = placed + .filter((p) => p.at >= 0) + .sort((a, b) => a.at - b.at || a.given - b.given); + const unnamed = placed.filter((p) => p.at < 0); + const listed = summary?.sites.length ?? 0; + return [ + ...named.map((p) => ({ + site: p.site, + figures: p.figures, + accent: p.site.accent || p.figures?.accent || seriesColor(p.at), + })), + ...unnamed.map((p, k) => ({ + site: p.site, + figures: null, + accent: p.site.accent || seriesColor(listed + k), + })), + ]; +}