Archilyzer · Source

archilyzer

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

commit ce63f663c012fd7a3e572011fd5a9bedebbe508b
parent 89c1f7ffff64802e662dfa179be3a0cb245f7244
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Mon, 28 Sep 2026 18:00:50 -0400

common: the homepage fold counts a transcript with no transcribedDate

The fold required `hasTranscript && transcribedDate` for everything, so a
stats page whose transcripts carry no date showed a site serving 1,889 videos
as 0 transcripts, 0 channels, 0 hours. A transcript now counts for totals,
channels, hours, the site card's total and upload-month placement; only the
transcribed time series, "this month" and the recent rail need the date.
buildStats guarantees one since stats schema 6; this is for pages from before.
HOMEPAGE_SUMMARY_VERSION stays 5: no field is added.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

Diffstat:
Mcommon/lib/homepageSummary.test.ts | 40++++++++++++++++++++++++++++++++++++++++
Mcommon/lib/homepageSummary.ts | 39+++++++++++++++++++++++++++++++++------
2 files changed, 73 insertions(+), 6 deletions(-)

diff --git a/common/lib/homepageSummary.test.ts b/common/lib/homepageSummary.test.ts @@ -198,6 +198,46 @@ test("a site's accent is published as a hex: an id becomes its on-dark value", ( assert.ok(plain.sites.every((x) => !("accent" in x))); }); +// Stats schema 6 guarantees a transcript a date, but a stats page written +// before it could carry a whole channel of transcripts with none — and the fold +// used to require the date for everything, so a site serving 1,889 videos +// showed 0 transcripts, 0 channels, 0 hours. The date is only for the time axis. +test("a transcript with no transcribedDate (a pre-schema-6 page) is counted, just not charted", () => { + const sites = [...SITES, site("gamma", ["g1"], "https://gamma.example")]; + const undated = [ + stat({ channelSlug: "g1", id: "u1", uploadDate: "20251101", duration: 3600, transcribedDate: null }), + stat({ channelSlug: "g1", id: "u2", uploadDate: "20260210", duration: 3600, transcribedDate: null }), + ]; + const base = buildHomepageSummary(STATS, CHANNEL_SITES, sites, NOW); + const s = buildHomepageSummary([...STATS, ...undated], { ...CHANNEL_SITES, g1: ["gamma"] }, sites, NOW); + + const gamma = s.sites.find((x) => x.siteId === "gamma"); + assert.ok(gamma, "a site whose transcripts are all undated is still on the family page"); + assert.deepEqual( + [gamma.channels, gamma.transcribed.total, gamma.hoursArchived, gamma.recordings], + [1, 2, 2, 2], + ); + // No month to put them in: not this month, not the sparkline, not a series. + assert.equal(gamma.transcribed.thisMonth, 0); + assert.deepEqual(gamma.transcribed.last12, new Array(12).fill(0)); + assert.equal(s.series.transcribed.month.bySite.gamma, undefined); + assert.deepEqual(s.series.transcribed.month.total, base.series.transcribed.month.total); + assert.equal(s.totals.transcribedThisMonth, base.totals.transcribedThisMonth); + assert.ok(!s.recent.some((r) => r.siteId === "gamma"), "the recent rail needs a date"); + // Counted everywhere a count lives, and placed by UPLOAD month like any other. + assert.equal(s.totals.transcripts, base.totals.transcripts + 2); + assert.equal(s.totals.channels, base.totals.channels + 1); + assert.equal(s.official!.transcripts, base.official!.transcripts + 2); + assert.equal(s.official!.channels, base.official!.channels + 1); + assert.equal(s.monthly!.find((m) => m.month === "2025-11")!.bySite.gamma, 1); + assert.equal(s.monthly!.find((m) => m.month === "2026-02")!.bySite.gamma, 1); + const placed = s.monthly!.reduce( + (a, m) => a + Object.values(m.bySite).reduce((x, y) => x + y, 0), + 0, + ); + assert.equal(placed + s.monthlyUnplaced!, s.official!.transcripts); +}); + test("a named accent also travels as its id; a custom hex and no accent carry none", () => { const sites = [ { ...site("alpha", ["a1", "a2"], "https://alpha.example"), accent: " Brass " }, diff --git a/common/lib/homepageSummary.ts b/common/lib/homepageSummary.ts @@ -61,6 +61,9 @@ export type MetricSeries = { }; export type SiteMetricStat = { + // For `transcribed`, also counts transcripts with no transcribedDate (a stats + // page from before stats schema 6) — they have no month, so they are in + // `total` and in neither `thisMonth` nor `last12`. total: number; thisMonth: number; last12: number[]; // last 12 month buckets (zero-padded left), for the sparkline @@ -322,6 +325,12 @@ function siteStatFrom( return { total, thisMonth, last12 }; } +// A site's transcribed card counts its undated transcripts too. They have no +// month, so they join `total` only — never `thisMonth` or the sparkline. +function withUndated(stat: SiteMetricStat, undated: number): SiteMetricStat { + return undated > 0 ? { ...stat, total: stat.total + undated } : stat; +} + export function buildHomepageSummary( stats: readonly VideoStat[], channelSites: ChannelSitesMap, @@ -362,11 +371,14 @@ export function buildHomepageSummary( // seconds, gone. `transcripts` is counted HERE, in the same pass that places // or leaves unplaced each transcript, so `placed + unplaced = // official.transcripts` holds even for a `transcribedDate` that bucketize - // drops (a future or malformed month). + // drops (a future or malformed month). `undated` counts the transcripts with + // no `transcribedDate` at all (a stats page from before stats schema 6): no + // transcribed bucket can hold them, so they join the card's total directly. type SiteAcc = { channels: Set<string>; recordings: number; transcripts: number; + undated: number; seconds: number; gone: number; }; @@ -376,7 +388,7 @@ export function buildHomepageSummary( if (!a) siteAcc.set( id, - (a = { channels: new Set(), recordings: 0, transcripts: 0, seconds: 0, gone: 0 }), + (a = { channels: new Set(), recordings: 0, transcripts: 0, undated: 0, seconds: 0, gone: 0 }), ); return a; }; @@ -385,12 +397,20 @@ export function buildHomepageSummary( let monthlyUnplaced = 0; for (const s of stats) { - const hasTx = s.hasTranscript && !!s.transcribedDate; + // A transcript COUNTS whether or not it carries a date: transcripts, + // channels, hours, upload-month placement. Only the transcribed time series + // (and "this month", and the recent rail) need `transcribedDate`. buildStats + // guarantees one since stats schema 6, but a page written before that could + // hold a whole channel of transcripts with none — and requiring the date + // here made a site serving 1,889 videos show 0 transcripts, 0 channels and + // 0 hours. + const hasTx = s.hasTranscript; + const dated = hasTx && !!s.transcribedDate; if (hasTx) { transcripts += 1; channelSet.add(s.channelSlug); hoursSeconds += s.duration > 0 ? s.duration : 0; - if (monthOf(s.transcribedDate) === nowMonth) transcribedThisMonth += 1; + if (dated && monthOf(s.transcribedDate) === nowMonth) transcribedThisMonth += 1; } if (s.downloadedDate) { downloads += 1; @@ -410,7 +430,11 @@ export function buildHomepageSummary( const um = uploadMonthOf(s.uploadDate); if (um && um < nowMonth) uploadItems.push({ month: um, siteId }); else monthlyUnplaced += 1; - transcribedItems.push({ date: s.transcribedDate as string, channelSlug: s.channelSlug, siteId }); + if (dated) { + transcribedItems.push({ date: s.transcribedDate as string, channelSlug: s.channelSlug, siteId }); + } else { + acc.undated += 1; + } } if (s.downloadedDate) { downloadedItems.push({ date: s.downloadedDate, channelSlug: s.channelSlug, siteId }); @@ -433,7 +457,10 @@ export function buildHomepageSummary( siteTitle: s.siteTitle, siteDescription: s.siteDescription, siteUrl: s.siteUrl, - transcribed: siteStatFrom(series.transcribed.month, s.siteId, nowMonth), + transcribed: withUndated( + siteStatFrom(series.transcribed.month, s.siteId, nowMonth), + siteAcc.get(s.siteId)?.undated ?? 0, + ), downloaded: siteStatFrom(series.downloaded.month, s.siteId, nowMonth), channels: siteAcc.get(s.siteId)?.channels.size ?? 0, recordings: siteAcc.get(s.siteId)?.recordings ?? 0,