Skip to content

Latest commit

 

History

History
147 lines (129 loc) · 50.6 KB

File metadata and controls

147 lines (129 loc) · 50.6 KB

site/CLAUDE.md

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.

Storage Schema

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.

Site (Astro + Cloudflare Pages)

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 uses avg_editorial (content lens) not avg_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/articles and /rights/network. /rights/observatory redirects 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 in pt_flags_json), ?jargon=low|medium|high (jargon_density), ?temporal=retrospective|present|prospective|mixed (tf_primary_focus). These require a JOIN to rater_evals on 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, from sys:homepage KV blob — PsqAggregates.psq_stddev), Discourse Character (merged tone + scope with Wilson CIs, Art. 25 framing, GS geo enrichment annotation from geo-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 from computeRemClusters() 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 from methodology-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 /about pipeline 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 /rights hub card and /about supplementary 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 from sys:homepage KV 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 from getDomainSignalProfiles(db) (KV-cached). CPU computation extracted to computeSourceMetrics(), 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 /signals anchors, KV-cached sys:signalCoverage 120s). Sub-pages: /status/models (model registry + performance + measurement integrity — multi-model comparison sections read pre-computed KV blob sys:models:comparison built by cron every 10 min at minute%10===5; lightweight queries run inline), /status/events (activity log + diagnostics). All pages use cachedQuery with KV (keys sys:*, 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 to document.body.dataset.persona; CSS hides/shows .persona-reader/.persona-researcher spans 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 (localStorage hrcb_donated, 7-day TTL).
  • Search (/search): Algolia passthrough FTS — ?q= → story results (with HRCB scores/queued badges) + domain + user D1 matches. Eager consumer runs SLEEPER_RULES (from coverage-crawl.ts) on every search to mirror promising stories as pending. Play button (►) donor-gated (localStorage hrcb_donated 7-day TTL) → calls /api/trigger/[id] for SSE eval. New stories auto-inserted via insertAlgoliaHits().
  • 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> in Base.astro. WebSub: feed declares rel="hub" (Superfeedr); cron pings hub when stories_new > 0.
  • Badges (/api/v1/badge/[domain].svg): Shields.io-style SVG badge showing domain HRCB score. Embeddable in Markdown/HTML. Colors match scoreToColor() HSL scale. CORS enabled, 1h cache. ?signal=psq returns 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

Lib File Inventory

  • site/src/lib/db.ts — Barrel re-export from db-stories.ts, db-entities.ts, db-analytics.ts, db-multi-model.ts
  • site/src/lib/db-stories.ts — Story types, feed queries, dashboard stats, queue/failed stories, getStory() (reads from rater_scores), getFairWitnessForStory() (reads from rater_witness), getArticleRanking() (supports optional sortDir: 'asc' | 'desc' for bottom-ranked queries). Exports ContentTypeOption = 'all' | 'ED' | 'PO' | 'LP' | 'PR' | 'AC' | 'MI' for the ?ctype= feed filter (distinct from TypeOption which handles HN post type ask/show/job). getFilteredStoriesWithScores accepts optional signal drill-through params: pt? (propaganda technique — LIKE match on pt_flags_json), jargon? (jargon_density = ?), temporal? (tf_primary_focus = ?). When any suppl param is set and model is not alt-model, adds INNER 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) — simple SELECT * FROM user_aggregates WHERE stories >= ? (~5ms, replaces slow CTE). getUserAggregate(db, username) — single-user lookup. UserIntelligence interface uses full_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() includes PsqAggregates (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 stories
  • site/src/lib/db-utils.tsSETL_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. Use readDb on read-only Pages routes, writeDb on write paths (ingest, consumers, cron).
  • site/src/lib/shared-eval.ts — Barrel re-export from eval-types.ts, models.ts, prompts.ts, eval-parse.ts, eval-write.ts, rater-health.ts
  • site/src/lib/eval-types.ts — Type definitions, interfaces, ALL_SECTIONS constant. PSQ types: PSQ_DIMENSIONS, PsqDimensionScore, PSQ_THREAT_DIMENSIONS/PSQ_PROTECTIVE_DIMENSIONS role sets, LiteEvalResponseV2.
  • site/src/lib/models.ts — Model registry, provider types, queue bindings, QUEUE_CONFIG export, 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. validateSlimEvalResponse enforces evidence-level score caps (H=1.0, M=0.7, L=0.4 max absolute value) and checks full eval schema_version against 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-exports fetchUrlContent, writeEvalResult, PRIMARY_MODEL_ID from shared-eval for the trigger endpoint.
  • site/src/lib/eval-write.ts — D1 write functions. writeEvalResult() updates stories only. writeRaterEvalResult() writes rater_evals + rater_scores + rater_witness + eval_history, then calls writeEvalResult(). writeLiteRaterEvalResult() does COALESCE fill-in UPDATE to stories; does NOT promote eval_status. writePsqRaterEvalResult() writes PSQ signal (psq_score, psq_dimensions_json, psq_confidence) via COALESCE fill-in; calls updatePsqConsensus() (separate from HRCB consensus). Detects and stores editorial_uncertain=1 when editorial_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 by model_registry.enabled = 1 so disabled model scores don't skew ensemble scores; excludes prompt_mode='lite-v2' (PSQ evals). updatePsqConsensus() computes separate PSQ consensus from lite-v2 evals only. Applies neutral discount (×0.5) to lite evals with editorial_uncertain=1 OR score=0.0 with 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-enable
  • site/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 taxonomy
  • site/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; returns RemCluster[] sorted by size desc. Used by /rights/network.astro.
  • site/src/lib/psq-external.ts — External PSQ scoring client. Calls DistilBERT at psq.unratified.org/score (10-dim, validated r=0.680). scoreExternalPsq(text, timeoutMs)ExternalPsqResult | null, writeExternalPsqScore(db, hnId, result) writes to psq_external + mirrors to stories.psq_score, checkPsqHealth(). Scale: external 0-100 ÷ 10 → 0-10 (matches convention).
  • site/src/lib/calibration.ts — Full-model CALIBRATION_SET (hn_ids -1001..-1015) + lite-model LITE_CALIBRATION_SET (-2001..-2015) + per-model thresholds + parameterized runCalibrationCheck()
  • site/src/lib/content-gate.ts — Pre-eval content classification (paywall, captcha, bot protection, etc.)
  • site/src/lib/content-drift.tscomputeContentHash() + checkContentDrift() for re-evaluating stories whose content changed since last eval
  • site/src/lib/colors.ts — Score/SETL/confidence/gate color mapping. scoreToColor(score, lightMode?), correlationToColor(r, lightMode?), evidenceColor(ev, lightMode?), directionalityColor(d, lightMode?) accept optional lightMode param — dark bg: L=0.58→0.52, light bg: L=0.43→0.37. SSR callers omit param (defaults dark); client window.scoreColor() auto-detects html[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) and db-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 7807 application/problem+json{ type, title, status }), cache header helpers
  • site/src/pages/api/v0/ — HN Firebase API-compatible endpoints: topstories.json.ts, beststories.json.ts, newstories.json.ts, item/[id].json.ts
  • site/src/pages/api/v1/domain/[domain]/history.tsGET /api/v1/domain/{domain}/history?days=30 (max 365). From domain_profile_snapshots.
  • site/src/components/ — Reusable Astro components (Breadcrumb, SubNav, EvalCard, DcpTable, etc.). SubNav.astro renders 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

Factions Page

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.

Key Patterns

  • compatibility_date must stay 2024-09-23 in site/wrangler.toml. Bumping breaks Astro SSR — every page returns [object Object] due to incompatible Response handling.
  • @astrojs/cloudflare version: npm audit reports 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 .astro components (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 say number — always add typeof val !== 'number' guards before pushing to typed arrays. Binary search method: comment out components, deploy, check curl | wc -c — 4.9KB = truncated. Pages refactored: homepage (11 components), signals (8), models (16), factions (7+SignalSpace).
  • D1 null vs undefined — use == null everywhere: D1 returns null for NULL columns but undefined for 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.ts score checks), parse validators (eval-parse.ts), and template conditionals. Never use === null for values that might originate from D1 query results or parsed JSON.
  • Astro .json.ts routing: Astro strips only the final .ts extension. Dynamic .json routes capture 123.json as params.id — strip .json suffix: 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 use scoreToColor() HSL interpolation in colors.ts. All .astro CSS declarations use var(--) — ~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> in Base.astro <head>. Toggle in Nav.astro via .theme-btn[data-theme] buttons; JS in Base.astro <script> handles clicks + syncs button states. Tailwind surface-* colors are hardcoded hex in tailwind.config.mjs — they don't follow CSS var overrides. Override pattern: html[data-theme="light"] .bg-surface-2 { background-color: var(--bg-surface); } etc. in global.css after @layer base. Light palette: OkSolar Light (base3 #fdf6e3 bg). 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.css applies filter: brightness(0.74) to .tripartite-score .score-value, .hm-score, .hm-data .score-font in 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 in ArticleDetail.astro (.ev-badges .badge) and item/[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-cards handle layout. Nav separators use plain ' | ' text (wrapping spans break flex). word-break: break-word scoped to .titleline, .sitebit only. Breakpoints: 640px, 400px.
  • Progressive disclosure: .collapsible-section styles <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_SECONDS env 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_gate regex 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: pendingqueuedevaluating (manual SSE only) → done | failed | skipped | rescoring. Feed/query filters group pending+queued+evaluating together. markFailed/markSkipped guards: NOT IN ('done', 'rescoring').
  • domain_aggregates column names: avg_hrcb (not avg_hcb), evaluated_count (not eval_count), story_count, avg_pt_count, avg_pt_score (migration 0053). avg_hrcb is null for domains with no evaluated stories — self-corrects on re-eval.
  • D1 remote query complexity: ORDER BY on domain_aggregates times out when combined with JOINs. Workaround: WHERE evaluated_count >= N first (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 with prompt_mode='lite').
  • DCP caching: 7-day TTL in KV per domain, also persisted to domain_dcp table. Browser audit br_* elements merged into DCP on audit completion.
  • Browser audit: domain_browser_audit table (migration 0061) stores per-domain headless Chromium results. CF Browser Rendering worker (wrangler.browser-audit.toml) dispatched via hrcb-browser-audit queue from cron (every 6h, 20 domains/cycle) or sweep=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_compat flag.
  • Lite prompt mode: Workers AI models use METHODOLOGY_SYSTEM_PROMPT_LITE (schema lite-1.6): editorial (0-100) + TQ binary indicators (tq_author/date/sources/corrections/conflicts, each 0 or 1). tq_score = sum/5. Validator injects ev.structural = tq_score * 2 - 1 as proxy so computeLiteAggregates() works unchanged. SETL computed. No per-section scores, no PT (pt_flag_count/pt_score = null). writeLiteRaterEvalResult COALESCE fill-in to stories (nulls only); lite evals do NOT promote eval_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: validateLiteEvalResponse repairs 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() in consumer-shared.ts looks up cached DCP for the domain (lookupCachedDcp), computes average modifier across all non-null DCP elements, caps at ±0.30, and applies to ev.structural before computeLiteAggregates(). Non-fatal on error. applyDcpToLiteStructural() in compute-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-wai HRCB + llama-4-scout-wai-psq PSQ). llama-3.3-70b-wai + llama-3.3-70b-wai-psq disabled (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 KV wai: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) use METHODOLOGY_SYSTEM_PROMPT_LITE_V2 (schema lite-2.0): 3-dim PSQ (threat_exposure, trust_conditions, resilience_baseline). writePsqRaterEvalResult writes to psq_lite_archive table (preserved for comparison). Does NOT write hcb_* columns — PSQ is an independent construct. Separate consensus: updatePsqConsensus() writes consensus to psq_lite_archive. Clean-cut model pattern: prompt mode change = new model ID (-psq suffix). 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.ts calls DistilBERT at psq.unratified.org/score (10-dim, held-out r=0.680). Inline in consumer workers (non-fatal). Writes to psq_external table only — no longer mirrors to stories.psq_score (reverted: external scores lack breadth, 86% in one bucket). stories.psq_score now sourced from LLM PSQ consensus via updatePsqConsensus(). D1 100-column limit: stories table at exactly 100 columns — all new PSQ data uses separate tables (migration 0070).
  • Per-model content truncation: ModelDefinition.max_input_chars limits content for small models (llama-3.3-70b-wai-psq=6000, llama-4-scout-wai-psq=12000). Truncation pct in rater_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 with editorial_uncertain=1 OR score=0.0 with 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.astro editorialUncertainDisplay triggers for ALL zero-score stories (not just lite-only). Shows ND in --color-nd gray with tooltip "Score indeterminate: content lacked sufficient signal for meaningful evaluation". db-stories.ts getFilteredStoriesWithScores demotes 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) via claimFromEvalQueue(). UNIQUE(hn_id, target_provider, target_model) + INSERT OR IGNORE = idempotent. Stale claims (>5 min) auto-recovered. batch_id flows dispatch → rater_evals.eval_batch_id for 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/queue ORDER BY COALESCE(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 include prompt_mode: model.prompt_mode (set at dispatch). Consumer uses as fallback in isLiteMode detection.
  • 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 /calibrate deletes eval_history + rater_* for cal IDs before re-enqueue. POST /calibrate?mode=lite deletes 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=lite tests actual Workers AI models. evaluate-standalone.mjs --mode lite tests prompt structure only (uses local claude-haiku). CF Workers egress IPs get Cloudflare Bot Management 157-char blocks → triggers age_gate. Sites without cf-ray header are safe. Current EX-3 = pypi.org (Fastly CDN).
  • calibration_evals longitudinal flow: POST /calibrate?mode=lite stores run timestamp in KV (calibration:lite:current_run). ingest.ts reads this when hn_id is a cal ID and calls writeCalibrationEval(). INSERT OR IGNORE deduplicates concurrent writes.
  • Unified read/write path: All per-section data in rater_scores/rater_witness (legacy scores/fair_witness dropped — migration 0047). writeEvalResult() updates stories only; writeRaterEvalResult() writes rater tables + calls writeEvalResult(). Primary model: model_registry.is_primary (migration 0045), queried via getPrimaryModelId(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 by model_registry.enabled=1; epoch is semantic documentation for ad-hoc queries.
  • Item page materialized columns: /item/[id] reads supplementary signals, labels, metadata from stories columns. Aggregates from rater_scores via computeAggregates(). hcb_json excluded (~12-15KB savings). Default tab uses story.eval_model. Shows archive_url link ("archived") and archive_used note ("from archive") when present. Displays "Contested" badge when consensus_spread > 0.3 with 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: writeRaterEvalResult checks hcb_structural_mean === null (editorial-only response) — rater data still written but story NOT promoted to done. Logs eval_skip.
  • Consumer provider guards: openrouter + workers-ai consumers check prep.modelDef.provider matches their expected provider — acks and skips if misrouted.
  • Llama + numeric prefix: Llama models emit "+0.5" instead of "0.5". extractJsonFromResponse strips 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 by markSkipped(). Surfaced on domain/domains/sources/status pages.
  • Re-promotion guard: The story upsert in enqueueForEvaluation() (hn-bot.ts) must include AND gate_category IS NULL — otherwise permanently-gated stories get re-promoted every cron cycle (infinite skip loop). autoEvalIds is a local Set in enqueueForEvaluation() (not a standalone function) determining auto-eval vs skip. url IS NOT NULL is 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 + hardcoded siteOrigin. Pass explicit canonicalUrl prop to override (e.g., /stories query normalization). All pages get <link rel="canonical"> and <meta property="og:url"> by default.
  • Middleware (src/middleware.ts): Security headers + RFC 8288 Link header (feed autodiscovery on text/html responses) applied to all SSR responses. caches.default broken in CF Pages SSR: Returning a cached Response causes TypeError: Can't modify immutable headers — reverted 2026-03-03. Use CF Cache Rules for edge caching instead. Static files in public/ bypass middleware entirely.
  • public/_headers (CF Pages custom headers): sets Content-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 to agent-card.json; a2a/agent-card.json → 301 redirect to agent-card.json (A2A canonical discovery path). Redirects in public/_redirects), agent-inbox.json (inter-agent proposals inbox), agent-manifest.json (construction provenance + subject_matter/architecture/not_about fields — 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 from public/.
  • 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; backs query-udhr-article-rankings agent-card skill), domains, domain/[domain], domain/[domain]/history, signals (4 aggregates in parallel: getSignalOverview + getTdSignalAggregates + getComplexityAggregates + getTemporalFramingAggregates; response includes transparency, accessibility, temporal sections + 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 in api-v1.ts.
  • Model registry D1 overlay: model_registry table (migration 0037) — toggle models via wrangler d1 execute without a deploy. PK column is model_id (not id). getEnabledModelsFromDb(db) intersects DB with MODEL_REGISTRY. Cron uses this at dispatch time. Every rater_evals analytics query must filter disabled models: use INNER JOIN model_registry mr ON mr.model_id = re.eval_model AND mr.enabled = 1. For coverage metrics, also add AND re.hn_id > 0 to all CASE WHEN status counts and WHERE hn_id > 0 as 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 via sweep=content_drift. Self-posts excluded.
  • Item page D1 batch: /item/[id] uses a single rawDb.batch() call for all independent queries (raterEvals, comments, storyEvents, evalHistory, storyTimeline, DCP) — reduces CPU overhead vs sequential awaits. Uses rawDb (not session-wrapped db) because batch() is not available on the D1 Sessions proxy. getFairWitnessForStory and getRaterScores run 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> from rater_evals.reasoning, matched by model). Filter/sort: type chips (all/eval/pipeline/drift via data-category attrs), 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 in db-stories.ts (not db-analytics).
  • Algolia backfill sweep: sweep=algolia_backfill uses searchAlgolia() + insertAlgoliaHits() (from coverage-crawl.ts). Parameters: min_score (default 500), limit (max 200), days_back (default 365).
  • SLEEPER_RULES (coverage-crawl.ts): Pluggable array of SleeperRule { label, minScore?, maxScore?, minComments?, maxAgeHours? }. Each rule generates a separate Algolia numericFilters query that inserts matching stories as pending — runs on every /search page load. Current rules: high_engagement (≥100 pts, ≤7d) + sleeper (≤10 pts, ≥5 comments, ≤12h). Add/remove rules without touching search.astro logic.
  • Credit pause fallback: When credit_pause:anthropic KV key is set, enqueueForEvaluation skips 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.astro injects body[data-score-mode] JS + global.css .score-mode-btn styles remain but are unused since stories.astro columnar refactor. No pages currently render .score-mode-btn elements. Safe to remove in future cleanup.
  • CF Web Analytics beacon: Base.astro <head> loads static.cloudflareinsights.com/beacon.min.js (token 0d5a83edc72a4a1282eee4a50ae8879a). Bot-filtered RUM: sessions, referrers, top pages, countries, devices. CSP in src/middleware.ts explicitly allows script-src https://static.cloudflareinsights.com and connect-src https://cloudflareinsights.com — both required or beacon is silently blocked. Analytics dashboard: CF Dashboard → Analytics → Web Analytics → observatory.unratified.org.