Site-specific detail for the Astro + Cloudflare Pages frontend and Workers pipeline. See root CLAUDE.md for project overview, build/deploy commands, and local scripts.
D1 (hrcb-db) tables: stories, events, eval_history, domain_dcp, dlq_messages, calibration_runs, ratelimit_snapshots, domain_aggregates (materialized per-domain signal averages — avg_hrcb, evaluated_count, story_count, avg_pt_count, avg_pt_score), daily_section_stats, calibration_evals (UNIQUE on calibration_run+hn_id+eval_model+eval_provider; INSERT OR IGNORE deduplicates), model_registry (migration 0037 — enabled, disabled_reason, updated_at, is_primary migration 0045), domain_profile_snapshots (migration 0039 — daily snapshots, PK (domain, snapshot_date), INSERT OR IGNORE idempotent; migration 0055 added 9 fields: avg_confidence, avg_sr, avg_pt_count, avg_pt_score, avg_dominance, avg_fw_ratio, dominant_scope, dominant_reading_level, dominant_sentiment — historical rows pre-2026-02-28 have NULLs for these), eval_queue (migration 0041 — pull-model dispatch, UNIQUE(hn_id, target_provider, target_model), stale claims auto-recovered after 5 min), user_aggregates (migration 0054 — materialized per-user signal summary, same upsert pattern as domain_aggregates; avg_editorial_full = section-aggregate full eval, avg_editorial_lite = holistic lite eval — empirically distinct constructs r=0.44; 4 refresh triggers: writeEvalResult + writeRaterEvalResult + writeLiteRaterEvalResult + crawl-path in hn-bot.ts story INSERT). hn_users.submitted_count (INTEGER) added in migration 0054 — populated from HN API data.submitted.length in crawlUserProfiles(). stories.source (TEXT DEFAULT 'hn'), stories.source_country (TEXT), stories.source_language (TEXT DEFAULT 'en') — migration 0066. Future-proofs multi-source ingestion. source_country backfillable from domain TLD via sweep=backfill_country. psq_external (migration 0070 — canonical external DistilBERT PSQ scores, PK hn_id; psq_score 0-10, psq_dimensions_json, psq_factors_json, psq_confidence, elapsed_ms, model_version; mirrored to stories.psq_score for display). psq_lite_archive (migration 0070 — preserved LLM-based PSQ data for comparison research; psq_consensus_* columns updated by updatePsqConsensus()). stories table at 100 columns (D1/SQLite limit) — cannot add more; new PSQ data uses separate tables.
KV (CONTENT_CACHE) keys: content cache, DCP cache (7d TTL), rate limit state per model, query result cache (q:*, TTL 300-600s), queue in-flight reservation (queue:inflight:<provider>:<hn_id>, TTL 300s), v1 API rate limit counters (ratelimit:v1:<ip>, TTL 3600s), daily domain snapshot guard (snapshot:domain:${today}, TTL 25h), cron distributed lock (cron:lock, TTL 120s), calibration run ID (calibration:lite:current_run, TTL 30d), credit pause flag (credit_pause:anthropic, TTL 600s), user aggregates page cache (sys:users:${sort}:${minStories}, TTL 60s), refresh_user_aggregates sweep lock (sweep:refresh_user_aggregates:running, TTL 600s), TD signal aggregates (sys:tdSignalAggregates, TTL 300s), complexity aggregates (sys:complexityAggregates, TTL 600s), temporal framing aggregates (sys:temporalFramingAggregates, TTL 600s), signal coverage (sys:signalCoverage, TTL 120s — corpus-level per-signal measured/total/pct for /status), homepage blob (sys:homepage, TTL 300s — cron pre-computed every 5 min at minute%5===0 via computeHomepageBlob() in db-analytics.ts; index.astro reads KV first, D1 fallback on miss; includes rightsBalance positive/neutral/negative counts), Kagi API backoff (kagi:backoff, TTL 1800s — set on 429, checked by all 4 Kagi sweeps before API calls), AP publish dedup (ap:pub:<hn_id>, TTL 30d — prevents duplicate Fediverse posts), Workers AI daily neuron budget (wai:neurons:YYYY-MM-DD, TTL 48h — cron gates dispatch at 8K, consumer increments ~50/eval).
R2 (hrcb-content-snapshots): content snapshots for audit trail.
Navigation: stories | signals | sources | rights | about | search (6 items). Title "HRO" links home. /status, /trends, /data, /reference accessible by URL but not in nav.
Page taxonomy:
- Observatory (
/): Human Rights Observatory (homepage) — 11 extracted components (HeroSection, NarrativeFindings, UdhrHeatmap, RightsUnderPressure, EvidenceTransparency, PropagandaLandscape, StakeholderVoice, GeographicCoverage, TemporalTrends, PsqLens, DomainProfiles). UDHR Rights Heatmap (editorial/content scores with bipolar diverging bars, UDHR generation taxonomy colors, per-row SETL tension dots, grouped by article category with sparklines; client-side sort controls: article # / most negative / most positive / most triggered / highest tension — pure JS, no new queries; default is article order grouped view, sorted view is a flat<div id="hm-sorted">with cloned rows), Rights Under Pressure (most negative, most triggered, lowest coverage), Evidence Transparency (weakest Fair Witness ratios), Propaganda Landscape (technique distribution + top flagged stories), Stakeholder Voice (who speaks / who is spoken about), Geographic Coverage (scope + region distribution), Temporal Trends (60-day HRCB chart), PSQ Reader Safety (conditional, experimental), Domain Rights Profiles (top 10 with fingerprints). Heatmap usesavg_editorial(content lens) notavg_final(combined) — structural channel surfaced via SETL dots only ("says ≠ does"). Data:getArticleDetailedStats+getArticleSparklines+getSignalOverview+getDomainStats+getDomainFingerprints+getDailyHrcb+getStatusCounts+getTopPropagandaStories+getStakeholderOverview+getRegionDistribution. SubNav links to/rights/articlesand/rights/network./rights/observatoryredirects to/(301). - Stories (
/stories): columnar table — 5 columns (#, Title, Rights, Safety, Points) with clickable sortable headers. Filter bar — sort (top/new/score/psq/points/conf/velocity), show (all/evaluated/positive/negative/neutral/safe/mixed/threat/pending/failed), content (?ctype=ED|PO|LP|PR|AC|MI), model. Signal drill-through params (from/signals):?pt=<technique>(propaganda technique inpt_flags_json),?jargon=low|medium|high(jargon_density),?temporal=retrospective|present|prospective|mixed(tf_primary_focus). These require a JOIN torater_evalson primary model path. Active filter shown as "signal filter:" banner with "× clear" link. Subtext (author, comments, badges, heatmap) in title cell. Also:/past(archive by date),/velocity,/dynamics,/item/[id](merged audit trail: eval_history + events) - Signals (
/signals): findings-first page (redesigned 2026-03-05). Sections: Key Findings (computed one-liners linking to sections below), Transparency (TD disclosure rates with Wilson 95% CIs, Article 19 framing), Accessibility (jargon + knowledge with Wilson 95% CIs, Article 26 framing → jargon counts link to/stories?jargon=low|medium|high), Persuasion Techniques (PTC-18 tier breakdown — high-severity/moderate/low-severity with Wilson CIs on occurrence proportions, not top-5 — differentiates from homepage), Temporal Framing (retro/present/prospective with Wilson CIs + time horizon with Wilson CIs → pct values link to/stories?temporal=<focus>), Reader Safety (PSQ — threat/protective dimension split, Art. 3+12 framing, t-dist meanCI on avg PSQ, fromsys:homepageKV blob —PsqAggregates.psq_stddev), Discourse Character (merged tone + scope with Wilson CIs, Art. 25 framing, GS geo enrichment annotation fromgeo-reference.ts— Wolfram Alpha demographics for 22 countries, US dominance %, underrepresented countries), Signal Tensions (cross-signal contradiction patterns: knowledge gatekeeping, retrospective bias, high-severity PT at scale, opaque discourse), Methodology notes. Data:getStatusCounts+getSignalOverview(KV 120s) +getTdSignalAggregates(KV 300s) +getComplexityAggregates(KV 600s) +getTemporalFramingAggregates(KV 600s) + homepage KV blob for PSQ. - Rights (
/rights): hub →/rights/articles,/article/[n],/reference(network removed from nav — D1 self-join timeout). (UDHR provision rankings + Deep Dive: score stats, top/bottom 3 highlights, evidence bars, directionality markers, theme tags; section labels link to/article/[n]). Network (/rights/network): MST backbone graph + Tension/Harmony tables + Rights Entanglement Map (REM — single-linkage clusters fromcomputeRemClusters()at r≥0.35, cluster cards, most/least entangled pairs, corpus-level narrative) + E/S Channel bars + Power Rankings (sortable). All 496 provision pairs positive (min r=+0.187). - Methodology (
/methodology): prerendered static page (prerender = true) — renders the exact LLM evaluation prompt frommethodology-content.ts(CC BY-SA 4.0). Full methodology (METHODOLOGY_PREAMBLE) + Lite methodology (METHODOLOGY_LITE) with collapsible sections, markdown-to-HTML tables/lists, section navigation, JSON-LD TechArticle. URL-only, not in main nav. Linked from/aboutpipeline description. - Reference (
/reference): prerendered static page (prerender = true) — "never leave without a solution" routing table. Three sections: (1) Scoring Concepts (HRCB, E/S channels, SETL, Fair Witness, DCP, evidence strength, confidence, content types, directionality, consensus, volatility — each links to/about#anchor); (2) Supplementary Signals (EQ/PT/SO/ET/SR/TF/GS/CL/TD/RTS — each badge links to/about#anchor); (3) UDHR Provisions (all 31 articles in 9 groups, each links to/article/n). URL-only, not in main nav. Reachable from/rightshub card and/aboutsupplementary section. - Sources (
/sources): live source intelligence dashboard — Key Findings (computed one-liners), Source Universe one-liner, Source Metrics 5-card grid (HRCB Spread, Avg SETL, Fair Witness, Avg Confidence, Avg PSQ fromsys:homepageKV blob), Signal Leaders 8-card grid (UDHR Art. 19 framing), Editorial Character 4 distribution charts with UDHR framing (Art. 19 tone, Art. 25 scope, Art. 26 reading level, sentiment), Source HRCB Distribution 7-band bar chart, Deep Dive hub cards. Uses<div role="heading">instead of<h2>in conditional blocks (Astro SSR truncation fix). Data fromgetDomainSignalProfiles(db)(KV-cached). CPU computation extracted tocomputeSourceMetrics(), KV-cached (sys:sourceMetrics, 120s TTL). Sub-pages:/domains,/domain/[domain],/users,/user/[username],/factions - Trends (
/trends): hub →/seldon,/velocity,/dynamics - Status (
/status): pipeline health — Coverage Spectrum funnel, Workers Health, Queue Breakdown, Eval Velocity stacked bar, Operations, Signal Coverage (per-signal measured/total/pct with deep links to/signalsanchors, KV-cachedsys:signalCoverage120s). Sub-pages:/status/models(model registry + performance + measurement integrity — multi-model comparison sections read pre-computed KV blobsys:models:comparisonbuilt by cron every 10 min atminute%10===5; lightweight queries run inline),/status/events(activity log + diagnostics). All pages usecachedQuerywith KV (keyssys:*, TTLs 60-600s); ops-critical data stays uncached. - About (
/about): 3-tier progressive disclosure — Tier 1 always visible, Tier 2<details open>, Tier 3<details>collapsed. Reference sections have anchor IDs (e.g.,#classification,#setl) for deep linking from the homepage (/). Persona toggle (Reader | Researcher) at top of page —localStorage['hro_persona'], synced todocument.body.dataset.persona; CSS hides/shows.persona-reader/.persona-researcherspans and blocks. Reader default (grade 8, ~62% verbosity); Researcher shows full technical prose. Tier 2 methodology prose only — tables, formulas, gradient bars untouched. Anthropic/Claude Code section in Tier 1 (always visible): built-with disclosure + Pentagon contract dispute + UDHR article mapping (Art 3, 12, Preamble) + Fair Witness disclosure footer. Frameworks cited: CRAAP Test (Wikipedia), PTC-18 (ACL D19-1565), Russell's Circumplex (Wikipedia), Heinlein ref (Wikipedia) — all linked in researcher view. UDHR historical claims linked to UN source. - Data (
/data): prerendered — live API endpoints table + greyed-out 501 export table. JSON-LD Dataset schema (measurementTechnique, variableMeasured, distribution, CC BY-SA 4.0 license). Links to OpenAPI spec. - OpenAPI (
/api/v1/openapi.json): prerendered static OpenAPI 3.1.0 spec — 16 endpoints (10 v1 incl. articles + methodology, 4 v0, badge, exports stub-free), 7 component schemas (StorySummary, DomainAggregate, DomainSnapshot, UserAggregate, RaterEval, ArticleStats, Error), RFC 7807 error responses. 24h cache. Linked from/data. - Support (
/support): two-track funding page — (1) $200/mo GitHub Sponsors goal → Safety Quotient Lab → full Claude Code usage tier; (2) API token donations via PayPal (one-time or recurring) → evaluation pipeline credits. "mark as donated" bypass (localStoragehrcb_donated, 7-day TTL). - Search (
/search): Algolia passthrough FTS —?q=→ story results (with HRCB scores/queued badges) + domain + user D1 matches. Eager consumer runsSLEEPER_RULES(fromcoverage-crawl.ts) on every search to mirror promising stories as pending. Play button (►) donor-gated (localStoragehrcb_donated7-day TTL) → calls/api/trigger/[id]for SSE eval. New stories auto-inserted viainsertAlgoliaHits(). - Feeds (
/feed.xml): Atom feed with query params?filter=positive|negative|neutral,?article=0-30(UDHR provision),?domain=...,?limit=1-100. Combine freely./feed/opml.xml= OPML subscription list (all 31 provision feeds). Autodiscovery<link>inBase.astro. WebSub: feed declaresrel="hub"(Superfeedr); cron pings hub whenstories_new > 0. - Badges (
/api/v1/badge/[domain].svg): Shields.io-style SVG badge showing domain HRCB score. Embeddable in Markdown/HTML. Colors matchscoreToColor()HSL scale. CORS enabled, 1h cache.?signal=psqreturns PSQ badge instead of HRCB. - Redirects (301):
/rights/observatory→/,/dashboard→/status,/system→/status,/models→/status/models,/front→/past,/articles→/rights/articles,/network→/rights/network,/user-intel→/users,/domain-intel→/domains
site/src/lib/db.ts— Barrel re-export fromdb-stories.ts,db-entities.ts,db-analytics.ts,db-multi-model.tssite/src/lib/db-stories.ts— Story types, feed queries, dashboard stats, queue/failed stories,getStory()(reads fromrater_scores),getFairWitnessForStory()(reads fromrater_witness),getArticleRanking()(supports optionalsortDir: 'asc' | 'desc'for bottom-ranked queries). ExportsContentTypeOption = 'all' | 'ED' | 'PO' | 'LP' | 'PR' | 'AC' | 'MI'for the?ctype=feed filter (distinct fromTypeOptionwhich handles HN post typeask/show/job).getFilteredStoriesWithScoresaccepts optional signal drill-through params:pt?(propaganda technique — LIKE match onpt_flags_json),jargon?(jargon_density = ?),temporal?(tf_primary_focus = ?). When any suppl param is set and model is not alt-model, addsINNER JOIN rater_evals re ON re.hn_id = s.hn_id AND re.eval_model = s.eval_model AND re.eval_status = 'done' AND re.prompt_mode = 'full'.site/src/lib/db-entities.ts— Domain/user queries, signal profiles, DCP, pipeline health, content gate stats, events re-exports.getUserIntelligence(db, sort, minStories, limit)— simpleSELECT * FROM user_aggregates WHERE stories >= ?(~5ms, replaces slow CTE).getUserAggregate(db, username)— single-user lookup.UserIntelligenceinterface usesfull_evaluated/lite_evaluated+avg_editorial_full/avg_editorial_lite.getMeanSetl(db)— corpus avg SETL across all rater_scores; wired to /signals Derived Metrics SETL card (previously@internal).site/src/lib/db-analytics.ts— Sparklines, histograms, scatter, velocity, daily HRCB, temporal patterns, observatory,getProviderStats,getModelQueueStats,getDlqTrend,getSelfThrottleImpact,getEvalLatencyStats,getSignalCompleteness,getModelTrustHistory+groupTrustByModel,getDomainKarmaMap,getKarmaHrcbCorrelation,getContentTypeValidation+getContentTypeDisagreement+getMisclassificationSummary,getVelocityStats,getDailyEvalVelocity,getModelChannelAverages,getCoverageProgression,getContentTypeEvalMix,getTruncationImpact,getCostStats,getDailyCostStats,getArticleDetailStats,getArticleDirectionalityBreakdown,getArticleThemeBreakdown,getTdSignalAggregates(corpus-wide transparency disclosure rates, wired to /signals Transparency Observatory),getComplexityAggregates(corpus-wide jargon_density + assumed_knowledge distributions; Article 26 accessibility framing; wired to /signals Content Accessibility — high_jargon_pct, expert_pct, accessible_pct derived),getTemporalFramingAggregates(corpus-wide tf_primary_focus distribution: retrospective/present/prospective/mixed with pcts + time_horizon sub-distribution; wired to /signals Temporal Framing).getCorpusSignalCoverage(corpus-level per-signal measured/total/pct from stories table; wired to /status Signal Coverage).computeHomepageBlob()includesPsqAggregates(avg_psq, per-dim averages, story count) in KV blob.site/src/lib/db-multi-model.ts— Rater evals/scores/witness, model agreement, multi-model storiessite/src/lib/db-utils.ts—SETL_CASE_SQL(alias)SQL fragment helper,cachedQuery<T>(kv, key, fn, ttl)KV-backed query cache,safeBatch()D1 batch chunker,D1_BATCH_SIZE = 100(single source of truth). D1 Read Replication helpers:readDb(db)— routes to nearest replica (withSession('first-unconstrained'));writeDb(db)— ensures read-after-write consistency (withSession('first-primary')). Both fall back to raw db if Sessions API unavailable. UsereadDbon read-only Pages routes,writeDbon write paths (ingest, consumers, cron).site/src/lib/shared-eval.ts— Barrel re-export fromeval-types.ts,models.ts,prompts.ts,eval-parse.ts,eval-write.ts,rater-health.tssite/src/lib/eval-types.ts— Type definitions, interfaces, ALL_SECTIONS constant. PSQ types:PSQ_DIMENSIONS,PsqDimensionScore,PSQ_THREAT_DIMENSIONS/PSQ_PROTECTIVE_DIMENSIONSrole sets,LiteEvalResponseV2.site/src/lib/models.ts— Model registry, provider types, queue bindings,QUEUE_CONFIGexport,getEnabledModelsFromDb(db)(D1 overlay — intersects DB-enabled list with MODEL_REGISTRY, falls back to static on error).PromptMode = 'full' | 'lite' | 'lite-v2'.isLiteMode()returns true for both lite and lite-v2;isLiteV2Mode()for PSQ-specific branching.site/src/lib/prompts.ts— System prompts (full, slim, lite, lite-v2).buildLiteV2Prompt(dims)generates a complete PSQ prompt + output schema for any dimension subset.site/src/lib/methodology-content.ts— Methodology text (CC BY-SA 4.0).PSQ_DIMENSION_RUBRICS(10 dims from instruments.json),PSQ_DIM_VARIANTS(1/2/3/5/10 presets),buildPsqDimensionRubric(dims),buildLiteV2SystemPrompt(dims).site/src/lib/eval-parse.ts— Response parsing, validation, content fetching.validateSlimEvalResponseenforces evidence-level score caps (H=1.0, M=0.7, L=0.4 max absolute value) and checks full evalschema_versionagainst pattern/^\d+\.\d+$/(current DB version: '3.7') — future-proof; any MAJOR.MINOR version passes. Lite versions go through a separate validator. Lite validator flags suspect lazy-neutral (editorial=0.0 with confidence ≥ 0.7) as a warning — the known Llama failure mode of defaulting to center instead of evaluating UDHR signals.validateLiteEvalResponseV2()+computeLiteAggregatesV2()for PSQ-based evals.site/src/lib/evaluate.ts— SSE trigger path + cron evaluator.callClaude()makes an Anthropic fetch with 90s AbortController timeout — no indefinite hang on API stalls. Re-exportsfetchUrlContent,writeEvalResult,PRIMARY_MODEL_IDfromshared-evalfor the trigger endpoint.site/src/lib/eval-write.ts— D1 write functions.writeEvalResult()updatesstoriesonly.writeRaterEvalResult()writes rater_evals + rater_scores + rater_witness + eval_history, then callswriteEvalResult().writeLiteRaterEvalResult()does COALESCE fill-in UPDATE tostories; does NOT promoteeval_status.writePsqRaterEvalResult()writes PSQ signal (psq_score,psq_dimensions_json,psq_confidence) via COALESCE fill-in; callsupdatePsqConsensus()(separate from HRCB consensus). Detects and storeseditorial_uncertain=1wheneditorial_mean=0.0 && confidence≥0.7(Llama lazy-neutral flag — preserves original score, no fabrication).updateConsensusScore()called at end of full+lite write paths — filters bymodel_registry.enabled = 1so disabled model scores don't skew ensemble scores; excludesprompt_mode='lite-v2'(PSQ evals).updatePsqConsensus()computes separate PSQ consensus from lite-v2 evals only. Applies neutral discount (×0.5) to lite evals witheditorial_uncertain=1ORscore=0.0with high confidence — prevents lazy-neutral from pulling consensus toward zero.requestArchive()KV-rate-limited Wayback Machine preservation.PT_TECHNIQUE_WEIGHTS+computePtScore()— Tier A=3, B=2, C=1; cumulative; unknown techniques contribute 0; NULL=not measured (lite), 0=measured+clean (full).site/src/lib/rater-health.ts— Per-model health tracking, auto-disable/re-enablesite/src/lib/hn-bot.ts— HN Firebase API crawling and queue dispatch.enqueueForEvaluation()(content pre-fetch, gate check, dispatch to eval_queue),checkFlaggedStories(),refreshFromUpdates(),crawlComments(),crawlUserProfiles(),triggerReEvals(),dispatchFreeModelEvals(),dispatchFrontPageFreeEvals()(every-minute front-page dispatch to all 6 free WAI models — lite HRCB + PSQ — regardless of enabled flag; priority 100),preloadContentCache(). Domain circuit breaker lives here. Core types:HNItem,QueueMessage,CrawlResult.site/src/lib/events.ts— Structured event logger with typed event taxonomysite/src/lib/compute-aggregates.ts— Deterministic aggregate computation (CPU-side). Volatility thresholds: stdDev < 0.10 = Low, < 0.25 = Medium, else High (per methodology spec).computeRemClusters(correlation, threshold=0.35)— single-linkage hierarchical clustering on provision correlation Map; returnsRemCluster[]sorted by size desc. Used by/rights/network.astro.site/src/lib/psq-external.ts— External PSQ scoring client. Calls DistilBERT atpsq.unratified.org/score(10-dim, validated r=0.680).scoreExternalPsq(text, timeoutMs)→ExternalPsqResult | null,writeExternalPsqScore(db, hnId, result)writes topsq_external+ mirrors tostories.psq_score,checkPsqHealth(). Scale: external 0-100 ÷ 10 → 0-10 (matches convention).site/src/lib/calibration.ts— Full-modelCALIBRATION_SET(hn_ids -1001..-1015) + lite-modelLITE_CALIBRATION_SET(-2001..-2015) + per-model thresholds + parameterizedrunCalibrationCheck()site/src/lib/content-gate.ts— Pre-eval content classification (paywall, captcha, bot protection, etc.)site/src/lib/content-drift.ts—computeContentHash()+checkContentDrift()for re-evaluating stories whose content changed since last evalsite/src/lib/colors.ts— Score/SETL/confidence/gate color mapping.scoreToColor(score, lightMode?),correlationToColor(r, lightMode?),evidenceColor(ev, lightMode?),directionalityColor(d, lightMode?)accept optionallightModeparam — dark bg: L=0.58→0.52, light bg: L=0.43→0.37. SSR callers omit param (defaults dark); clientwindow.scoreColor()auto-detectshtml[data-theme].normalizePsq(psq)converts 0-10 to [-1,+1];formatPsqScore(psq)formats for display.site/src/lib/geo-reference.ts— Static Wolfram Alpha demographic data (22 countries: population, internet penetration, HDI, corpus mentions) +UNDERREPRESENTED(5 zero-mention countries) +computeGeoInsights()for GS signal enrichment on/signals. Source: Wolfram Alpha 2023 estimates, generated 2026-03-05. Refresh annually.site/src/lib/stats.ts— Statistical utilities:wilsonCI(successes, n, z?)(Wilson score interval for proportions),tCritical(df)(t-distribution LUT, Wolfram-verified 2026-03-05, 9 queries),meanCI(mean, stdDev, n)(t-distribution CI for means). Used by/signals(proportion CIs) anddb-multi-model.ts(model score CIs).site/src/lib/api-v1.ts— Shared helpers for v0 and v1 public API routes:corsHeaders(),checkRateLimit()(200 req/hour),jsonResponse(),errorResponse()(RFC 7807application/problem+json—{ type, title, status }), cache header helperssite/src/pages/api/v0/— HN Firebase API-compatible endpoints:topstories.json.ts,beststories.json.ts,newstories.json.ts,item/[id].json.tssite/src/pages/api/v1/domain/[domain]/history.ts—GET /api/v1/domain/{domain}/history?days=30(max 365). Fromdomain_profile_snapshots.site/src/components/— Reusable Astro components (Breadcrumb, SubNav, EvalCard, DcpTable, etc.).SubNav.astrorenders pipe-separated sibling nav links.site/functions/rate-limit.ts— Rate limit state, capacity checks, credit pause (KV TTL: 600s)site/functions/providers.ts— API call adapters (Anthropic, OpenRouter, Workers AI) with 15s AbortController timeout
site/src/pages/factions.astro clusters domains by editorial character using 8 supplementary signal dimensions (EQ, SO, SR, TD, PT inverted, AR, VA, FW).
Algorithm: Z-normalize → cosine similarity → agglomerative hierarchical clustering at 1/φ threshold (fallback to 1/φ² if single giant cluster).
Page sections: Signal Landscape → Parallel Coordinates → Signal Space (2D PCA scatter + 3D Three.js orbit) → Differentiation → Cluster Cards (radar charts, members, distributions, liminal flags) → Affinity Matrix → Interesting Pairs → Outliers → Methodology Notes.
Data flow: getDomainSignalProfiles(db) → computeFactionsData() (z-normalize → cluster → enrich) → render. DB query KV-cached (q:domainSignalProfiles, 5-min TTL). Full computation result KV-cached (sys:factions, 120s TTL — bypasses 25-35ms CPU cost). Map<string, DomainSignalProfile> is not JSON-serializable — cache arrays, reconstruct Map.
Signal Space (site/src/components/SignalSpace.astro): Server-side PCA (power iteration, 3 components). 2D SVG scatter + 3D Three.js orbit (CDN lazy import via <script is:inline define:vars>). Toggle buttons for 2D/3D.
compatibility_datemust stay2024-09-23insite/wrangler.toml. Bumping breaks Astro SSR — every page returns[object Object]due to incompatible Response handling.@astrojs/cloudflareversion:npm auditreports 4 vulnerabilities (undici decompression DoS) in@astrojs/cloudflare@12.6.6+via wrangler/miniflare. These affect local dev tooling only — not the Cloudflare Workers production runtime, which uses the native CF fetch API. Do not upgrade to v13 until a stable non-alpha release exists. Current: 12.6.12 (accepted risk). Fix: wait for v13 stable.- Astro template gotcha: Cannot use TypeScript generics with angle brackets (
Record<string, string>) inside JSX template expressions — extract to frontmatter constants instead. - Astro SSR silent truncation: Runtime errors during Astro SSR template rendering on CF Pages produce truncated HTML (200 status, no error message). Two fixes required: (1) extract large inline template sections into separate
.astrocomponents (moves expressions into separate tagged template literals), (2) guard all.toFixed(),.toLocaleString(),.length, and property access on values that could be null from KV/D1 — a single null dereference in any component silently truncates the entire page. KV blob null trap:Object.entries()on KV JSON data can yield null values even when TypeScript types saynumber— always addtypeof val !== 'number'guards before pushing to typed arrays. Binary search method: comment out components, deploy, checkcurl | wc -c— 4.9KB = truncated. Pages refactored: homepage (11 components), signals (8), models (16), factions (7+SignalSpace). - D1 null vs undefined — use
== nulleverywhere: D1 returnsnullfor NULL columns butundefinedfor absent columns. All functions that guard nullable values must use== null(loose equality, catches both). This applies to display functions (formatScore,scoreToColor, etc.), compute functions (computeSetl,computeConfidence), write guards (eval-write.tsscore checks), parse validators (eval-parse.ts), and template conditionals. Never use=== nullfor values that might originate from D1 query results or parsed JSON. - Astro
.json.tsrouting: Astro strips only the final.tsextension. Dynamic.jsonroutes capture123.jsonasparams.id— strip.jsonsuffix:parseInt(params.id.replace(/\.json$/, ''), 10). - Semantic color system (migration complete): 38+ CSS custom properties in
global.css:root:--bg-*,--fg-*, channel colors (--channel-hrcb/editorial/structural), status colors (--color-negative/warning/positive+ vivid/faint variants), supplementary (--color-inference/amber/hn-score/disabled). Badge/text utility classes (.badge-positive,.text-muted, etc.). Dynamic score colors usescoreToColor()HSL interpolation incolors.ts. All .astro CSS declarations usevar(--)— ~62 hex values remain in JS/canvas contexts only (correct). - Light/dark theme toggle (Mar 2026):
html[data-theme]drives theme ("light"default,"dark"= original OkSolar dark).localStorage['hro_theme']. FOUC prevention:<script is:inline>inBase.astro<head>. Toggle inNav.astrovia.theme-btn[data-theme]buttons; JS inBase.astro<script>handles clicks + syncs button states. Tailwindsurface-*colors are hardcoded hex intailwind.config.mjs— they don't follow CSS var overrides. Override pattern:html[data-theme="light"] .bg-surface-2 { background-color: var(--bg-surface); }etc. inglobal.cssafter@layer base. Light palette: OkSolar Light (base3#fdf6e3bg). fg palette shifted one step darker (2026-03-03) for contrast on--bg-surface(#dbd2b9):--fg-primary: #586e75(base01),--fg-secondary: #657b83(base00),--fg-emphasis: #4a5d64. Original base00 (#657b83) on bg-surface gave only ~2.77:1; base01 gives ~3.58:1 on surface, ~4.9:1 on cream. Score color contrast: SSR-rendered score colors use dark-bg lightness (L≈0.52-0.58);global.cssappliesfilter: brightness(0.74)to.tripartite-score .score-value,.hm-score,.hm-data .score-fontin light mode — brings neutrals to WCAG AA (≥4.5:1 on cream).window.scoreColor()auto-detects theme; new SSE-triggered scores render at correct lightness client-side. Evidence/directionality badge colors inArticleDetail.astro(.ev-badges .badge) anditem/[id].astro(.ev-bar > div,.ev-labels > span) also use CSS brightness filter — no cookie-based SSR detection needed. - Filter bar classes (
global.css):.filter-bar(container),.filter-row(line grouping),.filter-label(sort:/show:/model: text),.filter-sep(pipe separators),.filter-active(selected option — orange bold),.filter-link(unselected options),.filter-stats(count/page text). Used on index, domains, users, dynamics, past pages. Filter bars use<div>containers (not<table>). - Mobile responsiveness:
.insight-grid,.two-col,.stat-cardshandle layout. Nav separators use plain' | 'text (wrapping spans break flex).word-break: break-wordscoped to.titleline, .sitebitonly. Breakpoints: 640px, 400px. - Progressive disclosure:
.collapsible-sectionstyles<details>/<summary>for 3-tier content. Used on About (Tier 1 always visible, Tier 2<details open>, Tier 3<details>) and Status sub-pages. - Hub pages as navigation gateways: Rights and Trends hubs are lean nav gateways — minimal inline data. Sources (
/sources) is a live dashboard like/signals. Don't duplicate sub-page data on hub pages. - Consumer hash functions:
hashString()= SHA-256 first 16 bytes as hex (32 chars). Used for methodology_hash (system prompt only) and prompt_hash (system + user). - Rate limiting: Both Anthropic and OpenRouter consumers read provider-specific rate limit headers proactively (
anthropic-ratelimit-*/x-ratelimit-*), self-throttle via KV before hitting 429s. Circuit breaker at 3+ consecutive 429s.RATE_LIMIT_MAX_BACKOFF_SECONDSenv var (default 120) caps delay. - Content gate dual placement: Runs in cron pre-fetch (primary — blocks before queueing, writes
gate_category/gate_confidence) AND consumer (safety net for KV cache misses). Pure regex, no LLM calls. - age_gate false positive:
age_gateregex triggers on articles that discuss age verification, not just actual gates. Fix: require form elements or "enter your date of birth" / "are you 18?" phrases rather than topic keywords. - eval_status lifecycle:
pending→queued→evaluating(manual SSE only) →done|failed|skipped|rescoring. Feed/query filters grouppending+queued+evaluatingtogether.markFailed/markSkippedguards:NOT IN ('done', 'rescoring'). - domain_aggregates column names:
avg_hrcb(notavg_hcb),evaluated_count(noteval_count),story_count,avg_pt_count,avg_pt_score(migration 0053).avg_hrcbis null for domains with no evaluated stories — self-corrects on re-eval. - D1 remote query complexity:
ORDER BYondomain_aggregatestimes out when combined with JOINs. Workaround:WHERE evaluated_count >= Nfirst (uses index), then ORDER BY; or sort in application. - Calibration IDs: Full: -1001..-1015 (
CALIBRATION_SET). Lite: -2001..-2015 (LITE_CALIBRATION_SET). Lite cal:POST /calibrate?mode=lite(inserts pending),POST /calibrate/check?mode=lite(reads rater_evals withprompt_mode='lite'). - DCP caching: 7-day TTL in KV per domain, also persisted to
domain_dcptable. Browser auditbr_*elements merged into DCP on audit completion. - Browser audit:
domain_browser_audittable (migration 0061) stores per-domain headless Chromium results. CF Browser Rendering worker (wrangler.browser-audit.toml) dispatched viahrcb-browser-auditqueue from cron (every 6h, 20 domains/cycle) orsweep=browser_audit. Derives 4 DCP elements:br_tracking(3rd-party tracker count),br_security(HTTPS/HSTS/CSP),br_accessibility(lang/skip-nav/alt),br_consent(cookie banner quality + dark patterns). Requires@cloudflare/puppeteer+nodejs_compatflag. - Lite prompt mode: Workers AI models use
METHODOLOGY_SYSTEM_PROMPT_LITE(schemalite-1.6): editorial (0-100) + TQ binary indicators (tq_author/date/sources/corrections/conflicts, each 0 or 1).tq_score = sum/5. Validator injectsev.structural = tq_score * 2 - 1as proxy socomputeLiteAggregates()works unchanged. SETL computed. No per-section scores, no PT (pt_flag_count/pt_score = null).writeLiteRaterEvalResultCOALESCE fill-in tostories(nulls only); lite evals do NOT promoteeval_status.EvalCard.astro:hasEval = hcb_weighted_mean !== null || hcb_editorial_mean !== null;isLiteOnly = hcb_weighted_mean === null || hcb_structural_mean === null;displayScore = hcb_weighted_mean ?? hcb_editorial_mean. Absence-as-negative repair:validateLiteEvalResponserepairs editorial ≤ -0.60 (raw ≤ 10, "dehumanizing propaganda" tier) to 0.0 (ND-equivalent) — Workers AI models score tech content with zero rights discussion far below 50 despite prompt instruction. Repaired scores trigger lazy-neutral flag → consensus neutral-discount (×0.5). DCP injection (ES-R2):processLiteResult()inconsumer-shared.tslooks up cached DCP for the domain (lookupCachedDcp), computes average modifier across all non-null DCP elements, caps at ±0.30, and applies toev.structuralbeforecomputeLiteAggregates(). Non-fatal on error.applyDcpToLiteStructural()incompute-aggregates.ts. Backward compat: lite-1.5 (structural holistic) — isV16 block skipped, tq_score=null. lite-1.4 (missing structural) — editorial-only aggregation. WAI models consolidated to Scout only (llama-4-scout-waiHRCB +llama-4-scout-wai-psqPSQ).llama-3.3-70b-wai+llama-3.3-70b-wai-psqdisabled (free tier budget). HRCB consensus: claude-haiku + Scout = 2 models. PSQ consensus: single model (no new consensus, existing backfill preserved). Daily neuron budget: 8K/day cap via KVwai:neurons:YYYY-MM-DD— cron gates dispatch, consumer increments ~50/eval. 3 OpenRouter lite models disabled — externally blocked (404/429). - PSQ prompt mode (lite-v2): PSQ models (
llama-3.3-70b-wai-psq,llama-4-scout-wai-psq,qwen3-30b-a3b-wai-psq) useMETHODOLOGY_SYSTEM_PROMPT_LITE_V2(schemalite-2.0): 3-dim PSQ (threat_exposure, trust_conditions, resilience_baseline).writePsqRaterEvalResultwrites topsq_lite_archivetable (preserved for comparison). Does NOT writehcb_*columns — PSQ is an independent construct. Separate consensus:updatePsqConsensus()writes consensus topsq_lite_archive. Clean-cut model pattern: prompt mode change = new model ID (-psqsuffix).UNIQUE(hn_id, eval_model)preserves both old lite and new PSQ rows per story. Prevents hybrid-row contamination. Future prompt mode changes should follow same pattern. - External PSQ (research only):
psq-external.tscalls DistilBERT atpsq.unratified.org/score(10-dim, held-out r=0.680). Inline in consumer workers (non-fatal). Writes topsq_externaltable only — no longer mirrors tostories.psq_score(reverted: external scores lack breadth, 86% in one bucket).stories.psq_scorenow sourced from LLM PSQ consensus viaupdatePsqConsensus(). D1 100-column limit:storiestable at exactly 100 columns — all new PSQ data uses separate tables (migration 0070). - Per-model content truncation:
ModelDefinition.max_input_charslimits content for small models (llama-3.3-70b-wai-psq=6000, llama-4-scout-wai-psq=12000). Truncation pct inrater_evals.content_truncation_pct. - Consensus weighting:
updateConsensusScore()weight =baseWeight × confidenceFactor × truncDiscount × neutralDiscount.baseWeight: full=1.0, lite=0.5.confidenceFactor:max(0.2, COALESCE(hcb_confidence, 0.5)).truncDiscount:1 - truncPct × 0.5.neutralDiscount: 0.5 for lite evals witheditorial_uncertain=1ORscore=0.0with confidence≥0.7 (Llama lazy-neutral mitigation); 1.0 otherwise. Confidence is incomparable across prompt modes (lite avg 0.85, full avg 0.17) — it differentiates within mode only; baseWeight handles cross-mode trust. - Zero-score ND display:
EvalCard.astroeditorialUncertainDisplaytriggers for ALL zero-score stories (not just lite-only). ShowsNDin--color-ndgray with tooltip "Score indeterminate: content lacked sufficient signal for meaningful evaluation".db-stories.tsgetFilteredStoriesWithScoresdemotes zero-score stories in time/top sort (CASE WHEN ABS < 0.005 THEN 1 ELSE 0 sort prefix). - eval_queue pull model: Consumers claim from
eval_queue(migration 0041) viaclaimFromEvalQueue(). UNIQUE(hn_id, target_provider, target_model) + INSERT OR IGNORE = idempotent. Stale claims (>5 min) auto-recovered.batch_idflows dispatch →rater_evals.eval_batch_idfor regression isolation. - eval_priority_score: Time-decayed dispatch priority (migration 0044):
(hn_score * decay) + (hn_comments * 0.5 * decay) + log10(karma) * 10 + feed_count * 5,decay = exp(-hoursOld/24). Both dispatch and/api/queueORDER BYCOALESCE(eval_priority_score, hn_score, 0) DESC. - Workers AI response format:
ai.run()may return{ response: "string" }or{ response: { ...object } }— consumer handles both. - QueueMessage
prompt_mode: Non-primary model messages includeprompt_mode: model.prompt_mode(set at dispatch). Consumer uses as fallback inisLiteModedetection. - Cron KV distributed lock: Scheduled handler acquires
cron:lock(120s TTL) before running. Lock present → skip cycle. Lock check failure is non-fatal. - Calibration cleanup:
POST /calibratedeletes eval_history + rater_* for cal IDs before re-enqueue.POST /calibrate?mode=litedeletes lite rater_evals for -2001..-2015 first — without this, NOT EXISTS filter skips already-evaluated IDs. - Lite calibration cloud vs standalone gap:
POST /calibrate?mode=litetests actual Workers AI models.evaluate-standalone.mjs --mode litetests prompt structure only (uses local claude-haiku). CF Workers egress IPs get Cloudflare Bot Management 157-char blocks → triggersage_gate. Sites withoutcf-rayheader are safe. Current EX-3 = pypi.org (Fastly CDN). - calibration_evals longitudinal flow:
POST /calibrate?mode=litestores run timestamp in KV (calibration:lite:current_run).ingest.tsreads this when hn_id is a cal ID and callswriteCalibrationEval().INSERT OR IGNOREdeduplicates concurrent writes. - Unified read/write path: All per-section data in
rater_scores/rater_witness(legacyscores/fair_witnessdropped — migration 0047).writeEvalResult()updatesstoriesonly;writeRaterEvalResult()writes rater tables + callswriteEvalResult(). Primary model:model_registry.is_primary(migration 0045), queried viagetPrimaryModelId(db).rater_evals.data_epoch(migration 0065):'current'(default) or'legacy-lite-1.x'(pre-PSQ lite HRCB evals from disabled WAI models). Consensus already filters bymodel_registry.enabled=1; epoch is semantic documentation for ad-hoc queries. - Item page materialized columns:
/item/[id]reads supplementary signals, labels, metadata fromstoriescolumns. Aggregates fromrater_scoresviacomputeAggregates().hcb_jsonexcluded (~12-15KB savings). Default tab usesstory.eval_model. Showsarchive_urllink ("archived") andarchive_usednote ("from archive") when present. Displays "Contested" badge whenconsensus_spread > 0.3with 2+ models. - eval-write FK guards: All 3 write functions do
SELECT 1 FROM stories WHERE hn_id = ?at entry — throws if story doesn't exist, preventing orphaned eval rows. - Structural channel guard:
writeRaterEvalResultcheckshcb_structural_mean === null(editorial-only response) — rater data still written but story NOT promoted todone. Logseval_skip. - Consumer provider guards: openrouter + workers-ai consumers check
prep.modelDef.providermatches their expected provider — acks and skips if misrouted. - Llama
+numeric prefix: Llama models emit"+0.5"instead of"0.5".extractJsonFromResponsestrips leading+via/:\s*\+(\d)/g. - Consumer batch-level API key check: Anthropic/OpenRouter check API key at batch level. Missing key →
msg.retry()all messages and return. - DLQ consumer ack placement:
msg.ack()only fires after successful DB write + event log. If write fails, message is NOT acked. - Content gate columns:
stories.gate_category(TEXT) +stories.gate_confidence(REAL) — migration 0024. NULL = accessible or pending. Written bymarkSkipped(). Surfaced on domain/domains/sources/status pages. - Re-promotion guard: The story upsert in
enqueueForEvaluation()(hn-bot.ts) must includeAND gate_category IS NULL— otherwise permanently-gated stories get re-promoted every cron cycle (infinite skip loop).autoEvalIdsis a localSetinenqueueForEvaluation()(not a standalone function) determining auto-eval vs skip.url IS NOT NULLis the wrong guard — blocks valid Ask HN posts. - gate_category taxonomy: Regex-based (content-gate.ts): paywall, bot_protection, captcha, login_wall, cookie_wall, geo_restriction, age_gate, app_gate, rate_limited, error_page, redirect_or_js_required. Pipeline-level (hn-bot.ts + consumer-shared.ts): binary_content, js_rendered, no_content, hn_removed.
- JSON-LD embedding:
<script type="application/ld+json" set:html={JSON.stringify(jsonLd).replace(/</g, '\\u003c')} />. Applied to: index (ItemList), item/[id] (Review + Rating), article/[n], domain/[domain] (Organization + AggregateRating), domains (ItemList), about (AboutPage + FAQPage), data (Dataset), methodology (TechArticle), sources (CollectionPage). Multiple JSON-LD blocks per page are valid (about has 2). - Base.astro canonical/og:url: Auto-derived from
Astro.url.pathname+ hardcodedsiteOrigin. Pass explicitcanonicalUrlprop to override (e.g.,/storiesquery normalization). All pages get<link rel="canonical">and<meta property="og:url">by default. - Middleware (
src/middleware.ts): Security headers + RFC 8288Linkheader (feed autodiscovery ontext/htmlresponses) applied to all SSR responses. caches.default broken in CF Pages SSR: Returning a cached Response causesTypeError: Can't modify immutable headers— reverted 2026-03-03. Use CF Cache Rules for edge caching instead. Static files inpublic/bypass middleware entirely. public/_headers(CF Pages custom headers): setsContent-Type: application/jrd+json+ CORS on/.well-known/webfinger. Use this for static files that need non-default content-type headers — middleware only runs for SSR routes..well-known/inventory:security.txt,agent-card.json+agent.json(A2A capability card — 8 skills incl. get-methodology, query-psq-signals;agent.json→ 301 redirect toagent-card.json;a2a/agent-card.json→ 301 redirect toagent-card.json(A2A canonical discovery path). Redirects inpublic/_redirects),agent-inbox.json(inter-agent proposals inbox),agent-manifest.json(construction provenance +subject_matter/architecture/not_aboutfields — prevents domain-name confabulation),ai-instructions.txt(human-readable AI agent orientation — identity, purpose, discouraged associations, endpoint directory),methodology.json(machine-readable scoring spec — weights, SETL formula, evidence caps, PTC-18 tiers),webfinger(RFC 7033 identity). All served as static files frompublic/.- Public REST API:
/api/v1/Astro routes: stories, story/[id], articles (per-UDHR-provision aggregate scores — editorial/structural averages, story counts, trigger counts, evidence distribution, SETL; backsquery-udhr-article-rankingsagent-card skill), domains, domain/[domain], domain/[domain]/history, signals (4 aggregates in parallel:getSignalOverview+getTdSignalAggregates+getComplexityAggregates+getTemporalFramingAggregates; response includestransparency,accessibility,temporalsections +generated_at), users (?sort=stories|score|hrcb|karma|..., ?min_stories=3, ?limit=50), user/[username]. Export stubs → 501. Public read-only, IP rate limit (200 req/hr), CORS*. Helpers inapi-v1.ts. - Model registry D1 overlay:
model_registrytable (migration 0037) — toggle models viawrangler d1 executewithout a deploy. PK column ismodel_id(notid).getEnabledModelsFromDb(db)intersects DB with MODEL_REGISTRY. Cron uses this at dispatch time. Everyrater_evalsanalytics query must filter disabled models: useINNER JOIN model_registry mr ON mr.model_id = re.eval_model AND mr.enabled = 1. For coverage metrics, also addAND re.hn_id > 0to all CASE WHEN status counts andWHERE hn_id > 0as denominator (excludes calibration stories -2001..-1001 from coverage ratios). - checkFlaggedStories: Runs every 10th minute (
minute % 10 === 3). Three eval_error strings: "Story removed from HN", "Story flagged/killed on HN", "Story deleted on HN" — all gate_category='hn_removed'. CrawlResult field:flagged_check. - Content drift detection:
content_hash(SHA-256 first 16 bytes hex) written on primary eval.checkContentDrift()re-fetches stories >7 days old, re-queues if hash changed. Triggered viasweep=content_drift. Self-posts excluded. - Item page D1 batch:
/item/[id]uses a singlerawDb.batch()call for all independent queries (raterEvals, comments, storyEvents, evalHistory, storyTimeline, DCP) — reduces CPU overhead vs sequential awaits. UsesrawDb(not session-wrappeddb) becausebatch()is not available on the D1 Sessions proxy.getFairWitnessForStoryandgetRaterScoresrun after batch (depend on batch results for model name / loop). - Audit trail on item page:
/item/[id]merges eval_history + events into unified chronological trail. Eval entries show model, score, token count, score drift delta badge on same-model re-evals, and lite reasoning (expandable<details>fromrater_evals.reasoning, matched by model). Filter/sort: type chips (all/eval/pipeline/drift viadata-categoryattrs), model dropdown, newest/oldest toggle — all client-side JS. Longitudinal sparkline: shown when ≥2 eval_history entries OR ≥5 story_snapshots; dual 560×64 SVG — HN score area (bottom, blue) + HRCB dots (top, score-colored) on shared time axis, computed server-side.getStoryTimeline()is indb-stories.ts(not db-analytics). - Algolia backfill sweep:
sweep=algolia_backfillusessearchAlgolia()+insertAlgoliaHits()(fromcoverage-crawl.ts). Parameters:min_score(default 500),limit(max 200),days_back(default 365). - SLEEPER_RULES (
coverage-crawl.ts): Pluggable array ofSleeperRule { label, minScore?, maxScore?, minComments?, maxAgeHours? }. Each rule generates a separate Algolia numericFilters query that inserts matching stories as pending — runs on every/searchpage load. Current rules:high_engagement(≥100 pts, ≤7d) +sleeper(≤10 pts, ≥5 comments, ≤12h). Add/remove rules without touchingsearch.astrologic. - Credit pause fallback: When
credit_pause:anthropicKV key is set,enqueueForEvaluationskips Anthropic queue and dispatches to free model queues instead. Lite queue always fires regardless of credit state. - Feed filter/sort COALESCE: Positive/negative/neutral filters and score sorts use
COALESCE(hcb_weighted_mean, hcb_editorial_mean)so lite-only stories surface correctly. - Score mode toggle (legacy):
Base.astroinjectsbody[data-score-mode]JS + global.css.score-mode-btnstyles remain but are unused since stories.astro columnar refactor. No pages currently render.score-mode-btnelements. Safe to remove in future cleanup. - CF Web Analytics beacon:
Base.astro<head>loadsstatic.cloudflareinsights.com/beacon.min.js(token0d5a83edc72a4a1282eee4a50ae8879a). Bot-filtered RUM: sessions, referrers, top pages, countries, devices. CSP insrc/middleware.tsexplicitly allowsscript-src https://static.cloudflareinsights.comandconnect-src https://cloudflareinsights.com— both required or beacon is silently blocked. Analytics dashboard: CF Dashboard → Analytics → Web Analytics → observatory.unratified.org.