Archilyzer · Source

archilyzer

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

commit 4e09aa50f8ff5e62df1db2037e1e873ae7a8e6ac
parent 7544c6c10d4fa873579b4484f7d21e4c4060fee0
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date:   Sat, 22 Aug 2026 19:39:29 -0400

pipelines: keep the pure band readings off the client side

FlowStation is a SERVER component and was calling bandSentence() out of
StateBand.tsx, which is `"use client"`. A plain function exported from a client
module cannot be CALLED from the server — only rendered — so every channel page
500'd at request time with "attempted to call bandSentence() from the server".

`pnpm build` passes clean on this: the route is force-dynamic, so nothing
prerenders it and the error only appears on a request. e2e found it; the build
could not. band.ts's header already documented this hazard in the OTHER
direction (a client component importing the server-only builder) and the same
file is the right home for the fix.

So band.ts now carries the type AND every pure reading — SEGMENTS,
segmentValue, bandDenominator, bandSentence, bandHeadline — and StateBand.tsx
exports only components.

Guarded, because the failure is invisible to tsc and to build: a unit test
asserts band.ts takes no value import and carries no directive, and that
StateBand.tsx exports nothing callable. Verified to actually fail by
reintroducing a lowercase export, not just observed green.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Diffstat:
Meditor/app/auto-queue/components/OperationRail.tsx | 4+++-
Meditor/app/channels/[slug]/components/flow/FlowStation.tsx | 9+++++++--
Meditor/app/channels/components/ChannelsTable.tsx | 4+++-
Meditor/app/components/pipelines/StateBand.tsx | 121++++---------------------------------------------------------------------------
Meditor/app/components/pipelines/band.ts | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Meditor/app/components/pipelines/buildBands.test.ts | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
6 files changed, 197 insertions(+), 119 deletions(-)

diff --git a/editor/app/auto-queue/components/OperationRail.tsx b/editor/app/auto-queue/components/OperationRail.tsx @@ -1,9 +1,11 @@ "use client"; -import type { OperationBand } from "../../components/pipelines/band"; import { bandCoverage, bandDenominator, + type OperationBand, +} from "../../components/pipelines/band"; +import { BandLegend, StateBand, } from "../../components/pipelines/StateBand"; diff --git a/editor/app/channels/[slug]/components/flow/FlowStation.tsx b/editor/app/channels/[slug]/components/flow/FlowStation.tsx @@ -4,11 +4,16 @@ import type { FlowStation as Station, StationOperation, } from "../../lib/channelFlow"; +// THE SPLIT MATTERS HERE. This is a SERVER component, and StateBand.tsx is +// `"use client"` — a plain function exported from a client module cannot be +// CALLED from the server ("attempted to call bandSentence() from the server"), +// only rendered as a component. So the readings come from band.ts, which +// carries no directive, and only the component itself comes from StateBand. import { bandHeadline, bandSentence, - StateBand, -} from "../../../../components/pipelines/StateBand"; +} from "../../../../components/pipelines/band"; +import { StateBand } from "../../../../components/pipelines/StateBand"; import { formatCount, formatCoverage, STATION_DOT } from "./tone"; // One station on the line, emitted as THREE siblings so the parent grid can put diff --git a/editor/app/channels/components/ChannelsTable.tsx b/editor/app/channels/components/ChannelsTable.tsx @@ -3,9 +3,11 @@ import Link from "next/link"; import { useMemo, useState } from "react"; import type { ChannelStat } from "yt-dlp-transcript-common/controller/channels"; -import type { OperationBand } from "../../components/pipelines/band"; import { bandSentence, + type OperationBand, +} from "../../components/pipelines/band"; +import { BandLegend, StateBand, } from "../../components/pipelines/StateBand"; diff --git a/editor/app/components/pipelines/StateBand.tsx b/editor/app/components/pipelines/StateBand.tsx @@ -1,6 +1,11 @@ "use client"; -import { bandCoverage, type OperationBand } from "./band"; +import { + bandDenominator, + SEGMENTS, + segmentValue, + type OperationBand, +} from "./band"; // THE INSTRUMENT. One component, three scales, one vocabulary. // @@ -24,65 +29,6 @@ import { bandCoverage, type OperationBand } from "./band"; // EVERY FILL IS A TOKEN from common/styles/tokens.css. No new colour ships with // this instrument, and that is load-bearing rather than frugal. -// The five populations, in the order they are laid down. `present` first so a -// band reads left-to-right as "done → can do → cannot do yet → cannot do at -// all", which is the same direction the channel transit line runs. -export const SEGMENTS = [ - { - key: "present", - label: "done", - // Solid, muted. The artifact exists; it is not where attention goes. - className: "bg-muted-foreground/45", - }, - { - key: "reachable", - label: "can run now", - // THE one saturated segment. - className: "bg-info", - }, - { - key: "blocked", - label: "blocked upstream", - // Hatched — waiting on an operation this system produces, so it will clear - // itself without anyone doing anything. - className: - "bg-warning/25 [background-image:repeating-linear-gradient(45deg,currentColor_0_1px,transparent_1px_4px)] text-warning/70", - }, - { - key: "deferred", - label: "held by a gate", - // Dotted — a gate (stale cues, a duration window) is holding it. - className: - "bg-transparent [background-image:radial-gradient(currentColor_0.5px,transparent_0.5px)] [background-size:3px_3px] text-muted-foreground", - }, - { - key: "missingInput", - label: "media gone", - // Hollow outline — there is nothing to do. An opt-in re-download is the - // only thing that would ever change it, and it must never look like work. - className: "bg-transparent ring-1 ring-inset ring-border", - }, -] as const; - -export type SegmentKey = (typeof SEGMENTS)[number]["key"]; - -export function segmentValue(band: OperationBand, key: SegmentKey): number { - if (key === "present") return band.present ?? 0; - return band[key]; -} - -// The denominator a band's segments are drawn against. -// -// `eligible` when it is known. When it is NOT — a snapshot can predate the -// field, and snapshots lapse — falling back to the sum of the work counts would -// silently redraw the band as "100% accounted for", so instead the band renders -// as an un-filled outline and says `coverage unknown`. Unknown is not zero, and -// it is not full either. -export function bandDenominator(band: OperationBand): number | null { - if (band.eligible != null && band.eligible > 0) return band.eligible; - return null; -} - // The three scales. Height and minimum width only — the fills, the order and // the meanings are identical, which is the entire point. // @@ -98,59 +44,6 @@ const SIZES = { export type BandSize = keyof typeof SIZES; -// The sentence a band carries in its tooltip and its accessible name. -// -// THE FIGURES ARE STATED SEPARATELY AND NEVER SUMMED — the same rule the rail's -// figure row holds, moved somewhere it applies to every scale at once. On the -// measured corpus `reachable` and `missingInput` are four orders of magnitude -// apart on diarization; one "remaining" number would say the same thing about a -// lane that is finished and a lane that cannot start. -export function bandSentence(band: OperationBand): string { - const parts: string[] = [`${band.reachable.toLocaleString()} can run now`]; - if (band.blocked > 0) { - parts.push(`${band.blocked.toLocaleString()} blocked upstream`); - } - if (band.deferred > 0) { - parts.push(`${band.deferred.toLocaleString()} held by a gate`); - } - if (band.missingInput > 0) { - parts.push(`${band.missingInput.toLocaleString()} have no media left`); - } - const denominator = bandDenominator(band); - parts.push( - band.present == null || denominator == null - ? "coverage unknown" - : `${band.present.toLocaleString()} done of ${denominator.toLocaleString()}`, - ); - return parts.join(" · "); -} - -// THE ONE POPULATION THAT DOMINATES WHAT IS LEFT, in three words. -// -// Not a total and not a percentage — the shape of the remainder is the thing -// that varies across this corpus, and naming its largest part is the shortest -// true sentence about a pipeline. "11,333 no media" and "11,329 to do" are the -// same size of number and mean opposite things; a single "remaining" figure -// would render them identically. -export function bandHeadline(band: OperationBand): string { - const candidates: Array<[number, string]> = [ - [band.reachable, "to do"], - [band.blocked, "blocked"], - [band.missingInput, "no media"], - [band.deferred, "held"], - ]; - let best: [number, string] | null = null; - for (const c of candidates) { - if (c[0] > (best?.[0] ?? 0)) best = c; - } - if (!best) { - // Nothing outstanding. Whether that is "finished" depends on a denominator - // we may not have, so say what is known and no more. - return bandDenominator(band) === null ? "coverage unknown" : "nothing to do"; - } - return `${best[0].toLocaleString()} ${best[1]}`; -} - export function StateBand({ band, size, @@ -224,5 +117,3 @@ export function BandLegend({ className = "" }: { className?: string }) { </ul> ); } - -export { bandCoverage }; diff --git a/editor/app/components/pipelines/band.ts b/editor/app/components/pipelines/band.ts @@ -83,3 +83,129 @@ export function bandCoverage(band: OperationBand): number | null { } return Math.min(1, band.present / band.eligible); } + +// ── THE FILL VOCABULARY, AND THE PURE READINGS OF A BAND ──────────────────── +// +// These live HERE, beside the type, and not beside the component that draws +// them — for the same client/server reason the file header gives, in the other +// direction. StateBand.tsx is `"use client"`, and a plain function exported +// from a client module CANNOT BE CALLED by a server component: Next throws +// "attempted to call bandSentence() from the server". The channel page's +// station foot is a server component and needs the sentence and the headline, +// so the pure functions belong on the shared, directive-free side. +// +// `pnpm build` does not catch this. The route is force-dynamic, so nothing +// prerenders it and the error only appears on a request — which is what e2e +// found and the build did not. + +// The five populations, in the order they are laid down. `present` first so a +// band reads left-to-right as "done → can do → cannot do yet → cannot do at +// all", which is the same direction the channel transit line runs. +export const SEGMENTS = [ + { + key: "present", + label: "done", + // Solid, muted. The artifact exists; it is not where attention goes. + className: "bg-muted-foreground/45", + }, + { + key: "reachable", + label: "can run now", + // THE one saturated segment. + className: "bg-info", + }, + { + key: "blocked", + label: "blocked upstream", + // Hatched — waiting on an operation this system produces, so it will clear + // itself without anyone doing anything. + className: + "bg-warning/25 [background-image:repeating-linear-gradient(45deg,currentColor_0_1px,transparent_1px_4px)] text-warning/70", + }, + { + key: "deferred", + label: "held by a gate", + // Dotted — a gate (stale cues, a duration window) is holding it. + className: + "bg-transparent [background-image:radial-gradient(currentColor_0.5px,transparent_0.5px)] [background-size:3px_3px] text-muted-foreground", + }, + { + key: "missingInput", + label: "media gone", + // Hollow outline — there is nothing to do. An opt-in re-download is the + // only thing that would ever change it, and it must never look like work. + className: "bg-transparent ring-1 ring-inset ring-border", + }, +] as const; + +export type SegmentKey = (typeof SEGMENTS)[number]["key"]; + +export function segmentValue(band: OperationBand, key: SegmentKey): number { + if (key === "present") return band.present ?? 0; + return band[key]; +} + +// The denominator a band's segments are drawn against. +// +// `eligible` when it is known. When it is NOT — a snapshot can predate the +// field, and snapshots lapse — falling back to the sum of the work counts would +// silently redraw the band as "100% accounted for", so instead the band renders +// as an un-filled outline and says `coverage unknown`. Unknown is not zero, and +// it is not full either. +export function bandDenominator(band: OperationBand): number | null { + if (band.eligible != null && band.eligible > 0) return band.eligible; + return null; +} + +// The sentence a band carries in its tooltip and its accessible name. +// +// THE FIGURES ARE STATED SEPARATELY AND NEVER SUMMED — the same rule the rail's +// figure row holds, moved somewhere it applies to every scale at once. On the +// measured corpus `reachable` and `missingInput` are four orders of magnitude +// apart on diarization; one "remaining" number would say the same thing about a +// lane that is finished and a lane that cannot start. +export function bandSentence(band: OperationBand): string { + const parts: string[] = [`${band.reachable.toLocaleString()} can run now`]; + if (band.blocked > 0) { + parts.push(`${band.blocked.toLocaleString()} blocked upstream`); + } + if (band.deferred > 0) { + parts.push(`${band.deferred.toLocaleString()} held by a gate`); + } + if (band.missingInput > 0) { + parts.push(`${band.missingInput.toLocaleString()} have no media left`); + } + const denominator = bandDenominator(band); + parts.push( + band.present == null || denominator == null + ? "coverage unknown" + : `${band.present.toLocaleString()} done of ${denominator.toLocaleString()}`, + ); + return parts.join(" · "); +} + +// THE ONE POPULATION THAT DOMINATES WHAT IS LEFT, in three words. +// +// Not a total and not a percentage — the shape of the remainder is the thing +// that varies across this corpus, and naming its largest part is the shortest +// true sentence about a pipeline. "11,333 no media" and "11,329 to do" are the +// same size of number and mean opposite things; a single "remaining" figure +// would render them identically. +export function bandHeadline(band: OperationBand): string { + const candidates: Array<[number, string]> = [ + [band.reachable, "to do"], + [band.blocked, "blocked"], + [band.missingInput, "no media"], + [band.deferred, "held"], + ]; + let best: [number, string] | null = null; + for (const c of candidates) { + if (c[0] > (best?.[0] ?? 0)) best = c; + } + if (!best) { + // Nothing outstanding. Whether that is "finished" depends on a denominator + // we may not have, so say what is known and no more. + return bandDenominator(band) === null ? "coverage unknown" : "nothing to do"; + } + return `${best[0].toLocaleString()} ${best[1]}`; +} diff --git a/editor/app/components/pipelines/buildBands.test.ts b/editor/app/components/pipelines/buildBands.test.ts @@ -1,5 +1,6 @@ import { test } from "node:test"; import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; import type { ChannelSnapshot } from "yt-dlp-transcript-common/controller/channelSnapshot"; import type { BackfillSnapshotEntry } from "yt-dlp-transcript-common/lib/backfillKinds"; import { @@ -326,3 +327,54 @@ test("a channel with no snapshot at all is every band empty", () => { assert.equal(bandCoverage(band), null); } }); + +// ── THE CLIENT/SERVER SPLIT, GUARDED ──────────────────────────────────────── + +test("band.ts stays directive-free, so a server component can call it", () => { + // band.ts carries the TYPE and every pure reading of a band. The channel + // page's station foot is a SERVER component and calls bandSentence() and + // bandHeadline() directly; adding "use client" here would break it at request + // time with "attempted to call bandSentence() from the server". + const src = readFileSync(new URL("./band.ts", import.meta.url), "utf8"); + // A DIRECTIVE, not the string — this file discusses "use client" in prose. + // A directive is a bare expression statement before any other code. + assert.ok( + !/^\s*(?:"use client"|'use client');?\s*$/m.test(src), + "band.ts must not be a client module", + ); + // And it must import nothing but types — buildBands.ts pulls in + // channelSnapshot → execa, which would put node:child_process in the browser + // bundle and fail `next build`. + const valueImports = [...src.matchAll(/^import\s+(?!type\b)/gm)]; + assert.equal( + valueImports.length, + 0, + "band.ts must not take a value import — see its header", + ); +}); + +test("StateBand.tsx exports only components, never callable helpers", () => { + // THE BUG THIS CAUGHT, ONCE. A plain function exported from a `"use client"` + // module cannot be CALLED by a server component — only rendered. bandSentence + // lived here, the server-rendered station foot called it, and every channel + // page 500'd at request time. + // + // `pnpm build` does NOT catch this: the route is force-dynamic, so nothing + // prerenders it and the error only appears on a request. e2e found it and the + // build did not, which is why this guard is a unit test and not a build step. + const src = readFileSync(new URL("./StateBand.tsx", import.meta.url), "utf8"); + assert.ok( + /^\s*(?:"use client"|'use client');?\s*$/m.test(src), + "StateBand.tsx is the client half", + ); + const exported = [...src.matchAll(/^export\s+(?:function|const)\s+(\w+)/gm)].map( + (m) => m[1], + ); + assert.ok(exported.length > 0, "found no exports to check — regex drifted"); + for (const name of exported) { + assert.ok( + /^[A-Z]/.test(name), + `${name} is exported from a client module but is not a component — a server component that calls it throws at request time. Move it to band.ts.`, + ); + } +});