Commits
platforms mass re-put whole archives over the firehose (4,951 docs
re-indexed 2026-07-21 alone, in per-author bursts). every re-put bumped
indexed_at, which (a) churned the below-watermark set the snapshot
builder's count gate assumes immutable — built vs expected drifted ~450
per build, 8x the ±0.1% tolerance, so the builder crash-looped and its
hourly full-table scans degraded search latency — and (b) burned a
turso FTS delete+insert pair per unchanged doc.
indexed_at now means "content last changed": the upsert preserves it
when content_hash matches, and the FTS rewrite (title+content, exactly
what the hash covers) is skipped. metadata-only updates still apply and
reach the replica via full-row snapshot copies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The cascade ranks by endorsements in a rolling week, so one doc can stay
#1 for days; the 20h dedup let it repost daily. Now fetch 10 candidates
and post the first URL not already posted in the last 7 days.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zig's std.http.Client re-pools a connection whose send failed, so once
turso/NAT kills an idle keep-alive conn every query fails with
WriteFailed until restart (2026-07-06 wedge). doRequest now evicts idle
pool connections and retries once on WriteFailed/HttpConnectionClosing.
MRE for the upstream bug in repros/, regression test in Client.zig.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'I felt rad/good/meh/bad' scored 0.47 — under the 0.50 flag threshold —
because the rotating mood word splits unigram doc-frequency: no slot word
reaches the 50% scaffold bar, so every 2-word title counted as half
templated. Two fixes, either of which catches it:
- short titles (≤3 content words) that are at least half scaffold now
count fully templated → composite 0.65
- thin-volume promotion: prolific authors with near-empty bodies
(thinness >0.9, ~400+ docs) go to model review regardless of title
shape — the model gate is the precision layer, the pre-score only
decides who's worth tokens
SCORING_VERSION 10→11 so bootstrap re-scores the corpus cleanly.
Precision regressions (date-titled journal, diverse-short-title human)
still pass; new regression test covers both directions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
repo URLs (github + tangled), docs, comments, binary name, and the site's
source links. deliberately unchanged: live identifiers — turbopuffer
namespace, logfire service_name, APP_NAME default, and all fly/CF/R2
resource names (leaflet-search-backend etc.), which are infra renames to
take separately if ever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
generateSW from workbox-config.cjs (no build step introduced): 30 URLs /
~600kB precached with content-hash revisions, atlas*.json runtime-cached
stale-while-revalidate, /api and functions untouched (network-only).
registration lives in ui.js so every page gets it.
site/deploy.sh chains regenerate → wrangler --branch=main; the generated
sw.js is committed so the 6h atlas prefect flow (which deploys from a
fresh clone) keeps serving it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the manifest, icon set (192/512 + maskable, magnifier glyph on a dark
tile, generated by icons/render.py), and the <head> tags — manifest link,
theme-color, apple-touch-icon — across all seven pages. _headers serves the
manifest as application/manifest+json.
This is the installable-metadata half of PWA support. Offline/caching still
needs a service worker (deliberately not hand-rolled yet — researching the
right generated-SW approach for this no-build static site).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two independent contrast fixes on the atlas, in both themes.
- Particles: dark-mode star cores were mixed 60-70% toward pure white,
blowing out to white-hot glow. Pull the mix back (0.42 star / 0.38 dot)
and trim glow alpha so each dot reads as its own colour — black-body-ward,
not extinguished. Light mode already used the publication hue, so it's
untouched.
- Cluster/title labels: brighter fill (0.95 dark / 0.88 light) and a
heavier text halo (width 3->4, opacity 0.7->0.88) so names stay readable
over a noisy particle field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stats-page traffic chart summed every endpoint's hourly count, including
/activity — which dashboard.js polls once a second per open tab. A single
stats page left open therefore pins the chart at ~3,500 req/hr (a flat 1/s),
99% of it that self-poll, while real request volume sits at ~20-40/hr. That's
a UI implementation detail masquerading as product traffic.
getTrafficSeries() now skips endpoints where countsAsTraffic() is false
(currently just /activity). Per-endpoint latency stats are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Funnel icon (matching the site's feather-style settings glyph) + a chevron
that flips on expand, in place of the bare 'filters' word. When filters are
actually applied the whole control tints to the accent blue and shows a count
badge, so an engaged state is legible at a glance — not just via the panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tag / platform / date filter stack consumed the whole mobile viewport,
pushing results and footer below the fold. On mobile (<=600px) the three
filters now collapse behind a 'filters' toggle with an active-count badge;
tapping expands them, and an already-applied filter (e.g. from a ?tag= URL)
auto-expands so state is never hidden. Desktop is unchanged. Also fixes the
'date:' label to 'filter by date:' for consistency with the other two.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
API + MCP + labeler XRPC + the on-PDS corpus, indexing into the deep docs.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lemma publishes site.standard.document records with content.$type
pub.lemma.blog.entry (its only own collection is drafts). The basePath
rule missed a custom-domain doc; the content-type fallback (same
mechanism as leaflet's) is authoritative. Network-wide (per UFOs):
lemma is ~a dozen published docs from ~5 authors — we index all 12.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
byId entries have no .label — use PubPlatforms.labelOf (domain, or
'other'), which matches the old PLATFORM_CONFIG labels exactly.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lemma.pub has 11 docs in the corpus (classified 'other' until now).
The add is exactly the shape the registry promised: one detection rule
in indexer.zig (base_path contains lemma.pub), one entry in
platforms.js (purple, lemma.pub domain), doc mentions. Existing turso
rows reclassified with indexed_at bumped so the next snapshot carries
it to the replica.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Platform identity was hard-coded in seven structures across two files
(PLATFORM_CONFIG, the filter list, and the marquee in index.html;
PLATFORMS, both color tables, and domains in atlas.js). All of it now
derives from window.PubPlatforms in platforms.js: order, home domains,
dark/light colors, favicon URLs. Adding a platform (lemma next) is one
entry here plus a backend detection rule if it still classifies as
'other'.
Also in this pass: homepage empty state reads 'search writing on
standard.site' with a slow logo marquee of the indexed platforms
(pause on hover, reduced-motion falls back to a static wrap) and a
'your site could be here' link to wil.to's step-by-step standard.site
guide — found and vetted with our own search engine. Atlas legend keeps
its color dots and gains platform logos.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The h1 theme toggle is gone everywhere; the settings sheet (ui.js,
shared) gains a theme group above the client picker with proper icons
(moon/sun/monitor for dark/light/system). stats and labels — which had
no settings at all — get the gear, components.css, and ui.js wiring.
Theme changes dispatch lf-theme-change so canvas surfaces repaint:
atlas re-marks dirty, wrapped re-reads its star-field palette (both
previously piggybacked on the removed button). Per-page THEME_ICONS
cycle code deleted; the flash-prevention head snippet stays.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every page now ends its h1 breadcrumb with the theme toggle at the
right edge (recommended/subscribed/wrapped already did): index moved it
up from below the search box, stats out of the footer, labels gained
one (it had none). dashboard.css h1 goes flex to match; cache-bust
bumped. atlas keeps its own header (deliberate oddball).
The homepage ? popover now leads with what this IS — a search engine
for writing on the open social web, the standard.site platforms — and
the surfaces built on top (atlas/recommended/subscribed/wrapped/stats/
labels, each with a plain-words gloss) before the search syntax.
'search atproto publishing platforms' → standard.site phrasing in the
empty state and meta descriptions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 'indexes' → 'is a search engine for'; 'marked' → the protocol's own
word (label/labeled); firehose tooltip actually says what the
firehose is
- honest epistemics: we don't know accounts' mechanisms, we observe
output shape — 'publish pages that look stamped out by a system',
not 'generate documents from a database'
- publisher favicons (duckduckgo icon service, no backend) give each
account a face; normalized pattern shapes demoted to hover — they
were illegible inline
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'policy is allowed to have taste' was eye-roll material. The tooltip
now says what's true: the label is accurate, we chose to keep this
account for now, and per-label experience controls (bsky-moderation
style) are something we're considering but haven't built.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-user toggle asked visitors a question they can't reason about
('do you want bulk-generated accounts in results?'). Punt on the whole
bsky-style selector UX: labeled accounts are hidden by default, kept
accounts always show with an informational badge linking to /labels,
and the labeled=show API param remains for deliberate consumers. No
localStorage, no setting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Local test runs masked it via a stale cached test binary; the exe
compile in CI caught it. zig build (not just zig build test) is now
part of the pre-push ritual.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bsky-labeler subscription model, for our own surface: search
results from bulk-generated-labeled accounts are hidden by default;
/labels gains a per-browser preference (hidden / shown-with-badge) that
the search page honors via &labeled=show. Kept accounts always show.
Shown results wear a dashed 'bulk-generated' badge linking to /labels.
One choke point in handleSearch covers every mode and format:
applyLabelPolicy reparses the ≤tens-of-rows result array (zat.json
accessors + Stringify.valueAlloc), drops or annotates by
classifier.isLabeledDid + policy.isKept.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two prior backend deploys failed in CI: build.zig embeds
../kept-dids.txt, which doesn't exist inside the backend/ docker
context — same staging requirement as banned-dids.txt (workflow cp +
Dockerfile COPY + path trigger + gitignore for the staged copy).
/labels reads for people, not protocol folks: the right-hand chip now
answers the visitor's actual question ('kept in search' / 'not in
search') instead of doc counts and indexing trivia; doc count moved
into the evidence line; the footer says labels are published openly
(protocol detail in the tooltip) and 'reach out' links to the
@pub-search.waow.tech profile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
queryLabels matches subjects exactly, but the query param was used raw:
did%3Aplc%3A… (what encodeURIComponent and any standards-following
client produces — including our own /labels page links) matched nothing
and returned an empty label set, silently. Decode %XX (and '+') before
the store lookup; malformed escapes fall back to the raw value.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
thefestivusproject.com is genuinely bulk-generated (one doc per Seinfeld
episode — the model's verdict is correct) and also delightful. Negating
the label would make the public stream lie; carving 'unless it's funny'
into the judge would put taste in the one place it must never live. The
architecture already has the seam: the label states what a thing IS,
the consumer decides what to DO — and consumer policy is allowed taste.
kept-dids.txt (repo root, embedded like banned-dids.txt) lists accounts
pub-search keeps despite a true label. policy.isKept gates the future
hard-drop; /labels shows a 'kept' badge with the reasoning in its
tooltip. kept ∩ banned = ∅ enforced by test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seeded banned entries have zero replica presence, so authorSite could
never resolve them — the page fell back to raw DIDs ('handles not
resolving'). The registry note's first token IS the domain: store it in
a new author_stats.site column at seed time and prefer it in the
summary. Seed now refreshes the row every boot (site/reason wording)
while still emitting the stream label only once. The page drops the
meaningless score/'0 docs' chrome for hand-banned rows ('excluded
before indexing' instead).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hand-banned DIDs (banned-dids.txt) are dropped at the firehose and
excluded from the replica, so the classifier could never observe them —
our three best-evidenced bulk-generated cases were invisible on the
label stream that exists to share exactly those findings. The two
systems were fully disjoint.
bootstrap now seeds every banned-registry entry as an operator-attested
LABELED author (no model review — the evidence is docs/exclusions.md)
and emits its bulk-generated label. Idempotent: re-seeds after each
scoring version's negate+wipe, and picks up newly-banned DIDs on any
boot without a version bump. policy.zig gains BANNED_ENTRIES so the
inline '# domain' notes from banned-dids.txt reach the /labels page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'machine-generated' collides with what people mean by AI-written in
2026 — and the label explicitly does NOT mean that (original AI writing
is welcome; the line is composed vs generated). 'bulk-generated' names
what we actually exclude: documents generated in bulk from a dataset.
Rename playbook (same as bulk-mirror → machine-generated at v6):
LABEL_BULK_GENERATED constant, SCORING_VERSION 9→10 (bootstrap negates
prior labels — under BOTH values, since negation matches on val — and
re-judges), service declaration + /labels page + docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- README: TAP_* env vars → INGESTER_*, labeler added to 'how it works'
- exclusions.md: the 'planned end-state' labeler note replaced with the
as-built reality (autonomous, negation-corrected, no review queue)
- spam-detection-plan.md: status plan → BUILT, with the deltas from plan
(label renamed, composed-vs-generated line, majority-of-3 gate)
- hydrant.md: marked HISTORICAL (tap is gone; our ingester covers it)
- api.md: /api/labeler + /admin/label documented
- docs/README: labeler in further-reading; stats footer links /labels
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
the readout is now one face of a wheel on a horizontal axle: the old face
tips away over the top as the new one rotates up from below, arriving
fully garbled, then every character snaps into focus at once (random-order
resolution, ~0.6s) instead of the drawn-out left-to-right decode.
facts about a person carry their did; the frontend resolves it through the
viewer's preferred client for the profile link. linked facts show a dotted
underline + ↗ once decoded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scattered fixed-position captions overlapped on narrow viewports and bare
text fought the star field for contrast. now one glass panel in normal
flow beneath the entry form (adapts to any width, can never overlap),
facts decode in character-by-character — resolved head, churning glyph
frontier — like a transmission resolving out of the noise. ~35 lines of
vanilla js, no library; reduced-motion gets a plain swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
per-platform firsts now use verified arrival only — a claimed date and a
record-key timestamp can be co-minted by the same import, so their mutual
agreement proves nothing (the 'leaflet in 2022' fact was exactly that).
generator emits 24 facts (per-tag counts, busiest day, largest publication,
cover art, corpus size, new authors); the entry screen floats three at a
time around the content column, each on its own fade clock.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The old line ('written by a person') was wrong in both directions: most
banned content WAS human-written at the source (patents by attorneys,
recall notices by regulators, transcripts of humans speaking), and
original writing by an AI belongs in the index. What actually unites
every ban is the generating process: one document per dataset record,
template + data determining the text — even when the dataset is the
account's own (prideraiser enumerates its own campaigns).
Everything now states that line consistently: the judge prompt (kept in
lockstep with scripts/judge-eval), the /labels page, the exclusions.md
policy, and the label-declaration description.
Judge → gemma-4-12B-it-8bit: 19/19 conclusive votes correct across two
validation runs on the new prompt (all misses were 503s/timeouts, never
wrong verdicts), and it's the model our own provider machine serves.
judge-eval gains coryd.dev (942-doc human) as a 4th ground-truth case,
JUDGE_TIMEOUT/JUDGE_CONCURRENCY knobs, creds validation, and reasoning-
model parsing. SCORING_VERSION 8→9 re-judges all decided authors under
the new line on deploy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scripts/build-facts precomputes display-ready factoids into site/facts.json
(verified firsts via the three-clock method — createdAt vs rkey TID vs
indexed_at — plus extremes, tags, and network-shape stats) so the fun
full-scan queries never touch the live db. the entry screen rotates them
as a dim text line beneath the form, complementing the atlas backdrop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cocore dispatch is fail-fast by design (503 no_providers_connected,
'capacity-shaped and retryable') — there is no server-side queue, the
requester is the queue. Ours is author_stats (PENDING is durable), but a
provider nap burned review attempts like garbled replies: 5 fast 503s
exhausted the budget in ~a minute, then the candidate waited for the
next boot's attempt reset. Now a 503 surfaces as error.NoCapacity, the
worker pauses 10 minutes, and the candidate keeps its attempt count —
offline capacity and bad verdicts are different failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
websocket.zig's handshake defaults (1024-byte request head, 10 headers
max) rejected any browser request forwarded by the Cloudflare proxy —
the browser's own headers plus CF's additions (cf-ray, x-forwarded-for,
sec-fetch-*) exceeded both limits, and the dropped connection surfaced
as a 502. Minimal curl probes stayed under the limits, which is why the
endpoint looked healthy from the CLI while every browser got Bad
Gateway. Raise to 8192 bytes / 64 headers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'writing by people' overclaimed: AI authors write genuine long-form
(e.g. agents publishing essays). The excluded thing is mechanical
emission — one record per database row — not non-human authorship.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prose 'how a label is decided' accordion is gone; the decision path
is now the page's structure: firehose → pattern filter → model review →
label, each stage showing its live number (authors watched, threshold,
votes/in-review, active labels) with depth in hover tooltips. Proper
nouns link on first reference (label spec, co/core); each account's DID
links to its signed label history on the queryLabels endpoint, so the
page's claims are one click from the cryptographic record.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 7B judge wasn't noisy — it was systematically inverted: 0/3 on the
known-truth set (called both bots human 3-0, the human illustrator
machine). scripts/judge-eval replicates the exact vote material +
prompt offline and runs candidate models through the full 3-vote
protocol against ground truth. Qwen3.6-35B-A3B scored 3/3 with every
completed vote correct (8/8); gemma-4-12B 2/3 (miss was pure
timeouts); both small Qwens 0/3.
The 35B is reasoning-style, so: max_tokens 150→2000 (150 truncated the
thought stream and every reply parsed as inconclusive), and
parseVerdict now takes the LAST "machine" verdict in the reply (thought
streams draft verdicts before the final answer) with the reason pulled
from that final JSON object.
SCORING_VERSION 7→8: re-judges the three decided authors on deploy.
Latency is fine: the review worker polls a state table — a slow judge
means candidates sit PENDING longer, nothing upstream blocks or queues.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
platforms mass re-put whole archives over the firehose (4,951 docs
re-indexed 2026-07-21 alone, in per-author bursts). every re-put bumped
indexed_at, which (a) churned the below-watermark set the snapshot
builder's count gate assumes immutable — built vs expected drifted ~450
per build, 8x the ±0.1% tolerance, so the builder crash-looped and its
hourly full-table scans degraded search latency — and (b) burned a
turso FTS delete+insert pair per unchanged doc.
indexed_at now means "content last changed": the upsert preserves it
when content_hash matches, and the FTS rewrite (title+content, exactly
what the hash covers) is skipped. metadata-only updates still apply and
reach the replica via full-row snapshot copies.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
zig's std.http.Client re-pools a connection whose send failed, so once
turso/NAT kills an idle keep-alive conn every query fails with
WriteFailed until restart (2026-07-06 wedge). doRequest now evicts idle
pool connections and retries once on WriteFailed/HttpConnectionClosing.
MRE for the upstream bug in repros/, regression test in Client.zig.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'I felt rad/good/meh/bad' scored 0.47 — under the 0.50 flag threshold —
because the rotating mood word splits unigram doc-frequency: no slot word
reaches the 50% scaffold bar, so every 2-word title counted as half
templated. Two fixes, either of which catches it:
- short titles (≤3 content words) that are at least half scaffold now
count fully templated → composite 0.65
- thin-volume promotion: prolific authors with near-empty bodies
(thinness >0.9, ~400+ docs) go to model review regardless of title
shape — the model gate is the precision layer, the pre-score only
decides who's worth tokens
SCORING_VERSION 10→11 so bootstrap re-scores the corpus cleanly.
Precision regressions (date-titled journal, diverse-short-title human)
still pass; new regression test covers both directions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
repo URLs (github + tangled), docs, comments, binary name, and the site's
source links. deliberately unchanged: live identifiers — turbopuffer
namespace, logfire service_name, APP_NAME default, and all fly/CF/R2
resource names (leaflet-search-backend etc.), which are infra renames to
take separately if ever.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
generateSW from workbox-config.cjs (no build step introduced): 30 URLs /
~600kB precached with content-hash revisions, atlas*.json runtime-cached
stale-while-revalidate, /api and functions untouched (network-only).
registration lives in ui.js so every page gets it.
site/deploy.sh chains regenerate → wrangler --branch=main; the generated
sw.js is committed so the 6h atlas prefect flow (which deploys from a
fresh clone) keeps serving it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds the manifest, icon set (192/512 + maskable, magnifier glyph on a dark
tile, generated by icons/render.py), and the <head> tags — manifest link,
theme-color, apple-touch-icon — across all seven pages. _headers serves the
manifest as application/manifest+json.
This is the installable-metadata half of PWA support. Offline/caching still
needs a service worker (deliberately not hand-rolled yet — researching the
right generated-SW approach for this no-build static site).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two independent contrast fixes on the atlas, in both themes.
- Particles: dark-mode star cores were mixed 60-70% toward pure white,
blowing out to white-hot glow. Pull the mix back (0.42 star / 0.38 dot)
and trim glow alpha so each dot reads as its own colour — black-body-ward,
not extinguished. Light mode already used the publication hue, so it's
untouched.
- Cluster/title labels: brighter fill (0.95 dark / 0.88 light) and a
heavier text halo (width 3->4, opacity 0.7->0.88) so names stay readable
over a noisy particle field.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The stats-page traffic chart summed every endpoint's hourly count, including
/activity — which dashboard.js polls once a second per open tab. A single
stats page left open therefore pins the chart at ~3,500 req/hr (a flat 1/s),
99% of it that self-poll, while real request volume sits at ~20-40/hr. That's
a UI implementation detail masquerading as product traffic.
getTrafficSeries() now skips endpoints where countsAsTraffic() is false
(currently just /activity). Per-endpoint latency stats are unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Funnel icon (matching the site's feather-style settings glyph) + a chevron
that flips on expand, in place of the bare 'filters' word. When filters are
actually applied the whole control tints to the accent blue and shows a count
badge, so an engaged state is legible at a glance — not just via the panel.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The tag / platform / date filter stack consumed the whole mobile viewport,
pushing results and footer below the fold. On mobile (<=600px) the three
filters now collapse behind a 'filters' toggle with an active-count badge;
tapping expands them, and an already-applied filter (e.g. from a ?tag= URL)
auto-expands so state is never hidden. Desktop is unchanged. Also fixes the
'date:' label to 'filter by date:' for consistency with the other two.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
lemma publishes site.standard.document records with content.$type
pub.lemma.blog.entry (its only own collection is drafts). The basePath
rule missed a custom-domain doc; the content-type fallback (same
mechanism as leaflet's) is authoritative. Network-wide (per UFOs):
lemma is ~a dozen published docs from ~5 authors — we index all 12.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
lemma.pub has 11 docs in the corpus (classified 'other' until now).
The add is exactly the shape the registry promised: one detection rule
in indexer.zig (base_path contains lemma.pub), one entry in
platforms.js (purple, lemma.pub domain), doc mentions. Existing turso
rows reclassified with indexed_at bumped so the next snapshot carries
it to the replica.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Platform identity was hard-coded in seven structures across two files
(PLATFORM_CONFIG, the filter list, and the marquee in index.html;
PLATFORMS, both color tables, and domains in atlas.js). All of it now
derives from window.PubPlatforms in platforms.js: order, home domains,
dark/light colors, favicon URLs. Adding a platform (lemma next) is one
entry here plus a backend detection rule if it still classifies as
'other'.
Also in this pass: homepage empty state reads 'search writing on
standard.site' with a slow logo marquee of the indexed platforms
(pause on hover, reduced-motion falls back to a static wrap) and a
'your site could be here' link to wil.to's step-by-step standard.site
guide — found and vetted with our own search engine. Atlas legend keeps
its color dots and gains platform logos.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The h1 theme toggle is gone everywhere; the settings sheet (ui.js,
shared) gains a theme group above the client picker with proper icons
(moon/sun/monitor for dark/light/system). stats and labels — which had
no settings at all — get the gear, components.css, and ui.js wiring.
Theme changes dispatch lf-theme-change so canvas surfaces repaint:
atlas re-marks dirty, wrapped re-reads its star-field palette (both
previously piggybacked on the removed button). Per-page THEME_ICONS
cycle code deleted; the flash-prevention head snippet stays.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every page now ends its h1 breadcrumb with the theme toggle at the
right edge (recommended/subscribed/wrapped already did): index moved it
up from below the search box, stats out of the footer, labels gained
one (it had none). dashboard.css h1 goes flex to match; cache-bust
bumped. atlas keeps its own header (deliberate oddball).
The homepage ? popover now leads with what this IS — a search engine
for writing on the open social web, the standard.site platforms — and
the surfaces built on top (atlas/recommended/subscribed/wrapped/stats/
labels, each with a plain-words gloss) before the search syntax.
'search atproto publishing platforms' → standard.site phrasing in the
empty state and meta descriptions.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- 'indexes' → 'is a search engine for'; 'marked' → the protocol's own
word (label/labeled); firehose tooltip actually says what the
firehose is
- honest epistemics: we don't know accounts' mechanisms, we observe
output shape — 'publish pages that look stamped out by a system',
not 'generate documents from a database'
- publisher favicons (duckduckgo icon service, no backend) give each
account a face; normalized pattern shapes demoted to hover — they
were illegible inline
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'policy is allowed to have taste' was eye-roll material. The tooltip
now says what's true: the label is accurate, we chose to keep this
account for now, and per-label experience controls (bsky-moderation
style) are something we're considering but haven't built.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The per-user toggle asked visitors a question they can't reason about
('do you want bulk-generated accounts in results?'). Punt on the whole
bsky-style selector UX: labeled accounts are hidden by default, kept
accounts always show with an informational badge linking to /labels,
and the labeled=show API param remains for deliberate consumers. No
localStorage, no setting.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bsky-labeler subscription model, for our own surface: search
results from bulk-generated-labeled accounts are hidden by default;
/labels gains a per-browser preference (hidden / shown-with-badge) that
the search page honors via &labeled=show. Kept accounts always show.
Shown results wear a dashed 'bulk-generated' badge linking to /labels.
One choke point in handleSearch covers every mode and format:
applyLabelPolicy reparses the ≤tens-of-rows result array (zat.json
accessors + Stringify.valueAlloc), drops or annotates by
classifier.isLabeledDid + policy.isKept.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The two prior backend deploys failed in CI: build.zig embeds
../kept-dids.txt, which doesn't exist inside the backend/ docker
context — same staging requirement as banned-dids.txt (workflow cp +
Dockerfile COPY + path trigger + gitignore for the staged copy).
/labels reads for people, not protocol folks: the right-hand chip now
answers the visitor's actual question ('kept in search' / 'not in
search') instead of doc counts and indexing trivia; doc count moved
into the evidence line; the footer says labels are published openly
(protocol detail in the tooltip) and 'reach out' links to the
@pub-search.waow.tech profile.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
queryLabels matches subjects exactly, but the query param was used raw:
did%3Aplc%3A… (what encodeURIComponent and any standards-following
client produces — including our own /labels page links) matched nothing
and returned an empty label set, silently. Decode %XX (and '+') before
the store lookup; malformed escapes fall back to the raw value.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
thefestivusproject.com is genuinely bulk-generated (one doc per Seinfeld
episode — the model's verdict is correct) and also delightful. Negating
the label would make the public stream lie; carving 'unless it's funny'
into the judge would put taste in the one place it must never live. The
architecture already has the seam: the label states what a thing IS,
the consumer decides what to DO — and consumer policy is allowed taste.
kept-dids.txt (repo root, embedded like banned-dids.txt) lists accounts
pub-search keeps despite a true label. policy.isKept gates the future
hard-drop; /labels shows a 'kept' badge with the reasoning in its
tooltip. kept ∩ banned = ∅ enforced by test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Seeded banned entries have zero replica presence, so authorSite could
never resolve them — the page fell back to raw DIDs ('handles not
resolving'). The registry note's first token IS the domain: store it in
a new author_stats.site column at seed time and prefer it in the
summary. Seed now refreshes the row every boot (site/reason wording)
while still emitting the stream label only once. The page drops the
meaningless score/'0 docs' chrome for hand-banned rows ('excluded
before indexing' instead).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The hand-banned DIDs (banned-dids.txt) are dropped at the firehose and
excluded from the replica, so the classifier could never observe them —
our three best-evidenced bulk-generated cases were invisible on the
label stream that exists to share exactly those findings. The two
systems were fully disjoint.
bootstrap now seeds every banned-registry entry as an operator-attested
LABELED author (no model review — the evidence is docs/exclusions.md)
and emits its bulk-generated label. Idempotent: re-seeds after each
scoring version's negate+wipe, and picks up newly-banned DIDs on any
boot without a version bump. policy.zig gains BANNED_ENTRIES so the
inline '# domain' notes from banned-dids.txt reach the /labels page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
'machine-generated' collides with what people mean by AI-written in
2026 — and the label explicitly does NOT mean that (original AI writing
is welcome; the line is composed vs generated). 'bulk-generated' names
what we actually exclude: documents generated in bulk from a dataset.
Rename playbook (same as bulk-mirror → machine-generated at v6):
LABEL_BULK_GENERATED constant, SCORING_VERSION 9→10 (bootstrap negates
prior labels — under BOTH values, since negation matches on val — and
re-judges), service declaration + /labels page + docs updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- README: TAP_* env vars → INGESTER_*, labeler added to 'how it works'
- exclusions.md: the 'planned end-state' labeler note replaced with the
as-built reality (autonomous, negation-corrected, no review queue)
- spam-detection-plan.md: status plan → BUILT, with the deltas from plan
(label renamed, composed-vs-generated line, majority-of-3 gate)
- hydrant.md: marked HISTORICAL (tap is gone; our ingester covers it)
- api.md: /api/labeler + /admin/label documented
- docs/README: labeler in further-reading; stats footer links /labels
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
the readout is now one face of a wheel on a horizontal axle: the old face
tips away over the top as the new one rotates up from below, arriving
fully garbled, then every character snaps into focus at once (random-order
resolution, ~0.6s) instead of the drawn-out left-to-right decode.
facts about a person carry their did; the frontend resolves it through the
viewer's preferred client for the profile link. linked facts show a dotted
underline + ↗ once decoded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scattered fixed-position captions overlapped on narrow viewports and bare
text fought the star field for contrast. now one glass panel in normal
flow beneath the entry form (adapts to any width, can never overlap),
facts decode in character-by-character — resolved head, churning glyph
frontier — like a transmission resolving out of the noise. ~35 lines of
vanilla js, no library; reduced-motion gets a plain swap.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
per-platform firsts now use verified arrival only — a claimed date and a
record-key timestamp can be co-minted by the same import, so their mutual
agreement proves nothing (the 'leaflet in 2022' fact was exactly that).
generator emits 24 facts (per-tag counts, busiest day, largest publication,
cover art, corpus size, new authors); the entry screen floats three at a
time around the content column, each on its own fade clock.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The old line ('written by a person') was wrong in both directions: most
banned content WAS human-written at the source (patents by attorneys,
recall notices by regulators, transcripts of humans speaking), and
original writing by an AI belongs in the index. What actually unites
every ban is the generating process: one document per dataset record,
template + data determining the text — even when the dataset is the
account's own (prideraiser enumerates its own campaigns).
Everything now states that line consistently: the judge prompt (kept in
lockstep with scripts/judge-eval), the /labels page, the exclusions.md
policy, and the label-declaration description.
Judge → gemma-4-12B-it-8bit: 19/19 conclusive votes correct across two
validation runs on the new prompt (all misses were 503s/timeouts, never
wrong verdicts), and it's the model our own provider machine serves.
judge-eval gains coryd.dev (942-doc human) as a 4th ground-truth case,
JUDGE_TIMEOUT/JUDGE_CONCURRENCY knobs, creds validation, and reasoning-
model parsing. SCORING_VERSION 8→9 re-judges all decided authors under
the new line on deploy.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
scripts/build-facts precomputes display-ready factoids into site/facts.json
(verified firsts via the three-clock method — createdAt vs rkey TID vs
indexed_at — plus extremes, tags, and network-shape stats) so the fun
full-scan queries never touch the live db. the entry screen rotates them
as a dim text line beneath the form, complementing the atlas backdrop.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
cocore dispatch is fail-fast by design (503 no_providers_connected,
'capacity-shaped and retryable') — there is no server-side queue, the
requester is the queue. Ours is author_stats (PENDING is durable), but a
provider nap burned review attempts like garbled replies: 5 fast 503s
exhausted the budget in ~a minute, then the candidate waited for the
next boot's attempt reset. Now a 503 surfaces as error.NoCapacity, the
worker pauses 10 minutes, and the candidate keeps its attempt count —
offline capacity and bad verdicts are different failures.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
websocket.zig's handshake defaults (1024-byte request head, 10 headers
max) rejected any browser request forwarded by the Cloudflare proxy —
the browser's own headers plus CF's additions (cf-ray, x-forwarded-for,
sec-fetch-*) exceeded both limits, and the dropped connection surfaced
as a 502. Minimal curl probes stayed under the limits, which is why the
endpoint looked healthy from the CLI while every browser got Bad
Gateway. Raise to 8192 bytes / 64 headers.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The prose 'how a label is decided' accordion is gone; the decision path
is now the page's structure: firehose → pattern filter → model review →
label, each stage showing its live number (authors watched, threshold,
votes/in-review, active labels) with depth in hover tooltips. Proper
nouns link on first reference (label spec, co/core); each account's DID
links to its signed label history on the queryLabels endpoint, so the
page's claims are one click from the cryptographic record.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The 7B judge wasn't noisy — it was systematically inverted: 0/3 on the
known-truth set (called both bots human 3-0, the human illustrator
machine). scripts/judge-eval replicates the exact vote material +
prompt offline and runs candidate models through the full 3-vote
protocol against ground truth. Qwen3.6-35B-A3B scored 3/3 with every
completed vote correct (8/8); gemma-4-12B 2/3 (miss was pure
timeouts); both small Qwens 0/3.
The 35B is reasoning-style, so: max_tokens 150→2000 (150 truncated the
thought stream and every reply parsed as inconclusive), and
parseVerdict now takes the LAST "machine" verdict in the reply (thought
streams draft verdicts before the final answer) with the reason pulled
from that final JSON object.
SCORING_VERSION 7→8: re-judges the three decided authors on deploy.
Latency is fine: the review worker polls a state table — a slow judge
means candidates sit PENDING longer, nothing upstream blocks or queues.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>