Anilist Chart
Read-only render layer for the AniList chat statistics site: turns the
anilist_metadata and user_scores ClickHouse tables (written by the
@anilist/api ingest model) into the six static pages — the «Доска почёта»
board, the /anime landing, and the four genre charts (chart, current, fresh,
bayes). This is a faithful port of the legacy Python (generate_board.py,
generate_landing.py, anilist_chart*.py); every ranking rule, formatter, and
award is reproduced from the oracle. The render boundary is strictly
read-only — the worst failure is a stale page for a week, fixable by re-run.
Methods render the pages from whatever data is present and publish each page that rendered (fan-out; one failing page never suppresses the rest).
Global Arguments
| Argument | Type | Description |
|---|---|---|
| clickhouseUrl? | string | ClickHouse HTTP endpoint for the read-only render user, e.g. https://host:8443. |
| clickhouseUser? | string | Read-only ClickHouse user (sent as the X-ClickHouse-User header). |
| clickhouseKey? | string | Password for the read-only ClickHouse user (X-ClickHouse-Key header). |
| clickhouseDatabase | string | ClickHouse database holding anilist_metadata and user_scores. |
| userNames | array | AniList usernames whose scores feed the four genre charts. |
| topK | number | Rows per genre in each chart; also advertised on the /anime landing. |
| bayesMinVotes | number | Bayesian prior weight m (minimum votes for confidence) on the /bayes chart. |
| penaltyRate | number | Per-season age penalty applied on the /fresh chart. |
| nodeHost? | string | SSH host of the node serving the pages. Required for `publish`. |
| nodeUser | string | SSH user for the serving node. |
| outputDir? | string | Absolute path of the nginx-served output directory on the node. Required for `publish`. |
| sshTimeoutMs | number | Timeout for the ssh publish fallback spawn (per page). A hung ssh connection |
| clickhouseMaxResponseBytes | number | Cap on the total bytes read from a single ClickHouse query response body |
| Argument | Type | Description |
|---|---|---|
| topK | number | Rows per genre; threaded into BOTH the chart compute and the landing copy. |
| bayesMinVotes | number | Bayesian prior weight m; threaded into the /bayes chart. |
Resources
2026.08.02.1
Real-fix (not byte-frozen) for all 7 latent bugs tracked in the LOCAL
anilist-chart-latent-bugs issue-lifecycle model (3 MED, 4 LOW; 0 CRITICAL/HIGH
— see the 2026.08.01.1 entry below for the original per-bug writeup, written
when all 7 were accepted-but-unfixed). model.version / manifest.yaml bump
2026.08.01.1 -> 2026.08.02.1, with a single upgrades[] entry
(upgradeAttributes: (old) => old — the two new global args are DEFAULTED, so
an old attribute record lacking both keys still reads identically once the zod
defaults apply).
- LB1 (MED) The 11 ClickHouse reads (board, chartScores, distinctIds,
chartMeta, six landing aggregates) plus the freshness read ran with no
try/catch, so any read throw escaped
execute()with NO diagnostic marker left behind. They are now wrapped in one try/catch: on any throw, arenderRunmarker is written (ok:false,refuseReason: "read failed: <message>",anomalies: [<message>]) THEN the error is re-thrown (write-then-rethrow — the same fail-loud shape aspublish()'s existing guard), so the workflow step still fails AND the marker now exists forswamp report get/swamp data getto inspect. - LB2 (MED) The ssh publish
Deno.Commandspawn carried noAbortSignal/timeout, so a hung ssh connection blockedpublish()forever. A new DEFAULTED global argsshTimeoutMs(default 30000) now bounds the whole spawn/write/output round-trip viaAbortController+setTimeout+clearTimeout(NOTAbortSignal.timeout, so a fast success cancels the pending timer instead of leaving it to fire after the fact). On abort the child is killed,output()rejects, and the existing per-page try/catch inpublishPagesmarks just that page failed — the rest still publish. - LB3 (MED)
ClickHouseClient.query()buffered the entire response body viares.text()with no cap. It now streams the body throughres.body.getReader(), counting bytes as chunks accumulate, and throwsClickHouse response exceeds N bytesthe moment the running total passes the cap — freeing the partial buffer early rather than fully buffering an unbounded/misbehaving upstream first. The cap comes from a new DEFAULTED global argclickhouseMaxResponseBytes(default 67108864 = 64MiB), threaded throughconfigFromintoClickHouseConfig.maxResponseBytes; a body under the cap still returns every row (a ceiling, not a silent truncator). - LB4 (LOW) A malformed freshness timestamp (
Date.parse->NaN) coercednewestDataAgeMstonull, which silently disabled the staleness anomaly (evaluateFreshnessonly fires staleness whennewestDataAgeMs !== null).render()now distinguishes "raw present but unparseable" from "genuinely absent" and passes a new optionalnewestTimestampMalformedflag intoFreshnessInput; when true,evaluateFreshnesspushes an explicit unparseable-timestamp anomaly (staleness check skipped). Stillok:true, still no false "publishing last-known-good" — the gap is now SIGNALLED, not silent. - LB5 (LOW) A non-numeric
media_idfromdistinctMediaIdsQuerybecameNaN, whicharrayIntParamtruncated to the literal string"NaN"— an invalidArray(Int64)wire value that a real ClickHouse rejects, aborting the whole render.render()now filtersidsthrough.filter(Number.isFinite)before ever building the array param, so a corrupt id is skipped, never sent (emptyidsstill routes through the existing[]branch).arrayIntParamalso now throws loud on a non-finite input as defense-in-depth, so the poisoned literal can never be constructed even by a future caller — though filtering means that throw never fires in practice. - LB6 (LOW) A ClickHouse error response's body was echoed verbatim (up to
500 chars, including newlines) into the thrown error. The body is now trimmed
to 200 chars, whitespace runs collapsed to a single space, and defensively
.replaceAll'd for the configured key (belt-and-braces — a CH error body never legitimately contains it). Still no credential leak in any thrown error or written resource; confirmed by a new pure adversarial test that stubs a body literally containing a sentinel "key" and asserts it comes back[redacted]. - LB7 (LOW)
arrayStringParam's hand-rolled escaping left an embedded NUL byte completely unescaped. A third.replace()pass now encodes it as the two-character\0escape, running AFTER the backslash-doubling and quote passes (order matters: the new escape's own backslash must not be doubled by
Notes trimmed at a line boundary to fit the registry's 4900-byte per-version cap. Full section: https://github.com/umag/swamp-workspace/blob/d850d8e8372d4c0008c9245959a090b37095de7a/anilist-chart/CHANGELOG.md
Merge pull request #60 from umag/feat/anilist-chart-extension
feat(anilist-chart): add @magistr/anilist-chart render+publish extension
- Has README or module doc2/2earned
- README has a code example1/1earned
- README is substantive1/1earned
- Most symbols documented1/1earned
- No slow types (deprecated)1/1earned
- Dependencies pass trust audit2/2earned
- Has description1/1earned
- Platform support declared (or universal)2/2earned
- License declared1/1earned
- Verified public repository2/2earned