Commits
uriPatterns was read as a single literal string matched with `uri = ?`, so the
spec's wildcard (`uriPatterns=*`) looked for a subject named "*" and every
generic client got `{"labels":[]}`. Reported by @hipstersmoothie.com.
Now parses uriPatterns as an array (repeated params, comma-joined values, or
both), supports `*` and `prefix*` patterns, and adds `sources`, `limit`
(1-250, default 50) and `cursor` with a response cursor on full pages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
the fly-era builder-guard job died with the fly builder; symptom-level
staleness alerts (watchdog 180m + logfire) survived the cutover but a
failing prefect flow stayed silent for up to 3h. the 15-min watchdog now
also checks the pub-search-snapshot deployment's last terminal run and
pages on FAILED/CRASHED. skipped silently when PREFECT_API_* secrets are
absent; a transient prefect-API blip prints but does not page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
builds now come from the pub-search-snapshot prefect deployment on
heavypad every 2h (docs/builder-offbox-plan.md); the serving side adopts
from R2 unchanged. first heavypad prod snapshot adopted 2026-07-25
(b1784954615-a767), fly machine snapshot-builder-hourly destroyed.
deleted: scripts/reconcile-snapshot-builder, scripts/guard-snapshot-builder,
scripts/fly_machines.py, the CI reconcile step, and the builder-guard
watchdog job. snapshot-age alerting stays (threshold 120m -> 180m to fit
the 2h cadence). runbooks updated to prefect deployment run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
the deploy-time reconcile script re-provisions the builder machine env,
which silently undid the 2026-07-24 BUILDER_TIMEOUT_SECS=5400 bump (builds
run 13-40min depending on turso weather; 2700s left too little headroom
and watchdog kills produced stale-freshness alerts twice).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
search responses carry snippets only, which forces agents to re-fetch
records from the author's PDS and re-flatten block structures themselves
(and older leaflet shapes defeat naive extractors). the indexer already
stores extracted full text; serve it. up to 25 comma-separated uris per
request, read entirely from the local replica, same visibility policy as
search (banned/labeled/bridgy/dead-url excluded → listed under missing).
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>
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>
uriPatterns was read as a single literal string matched with `uri = ?`, so the
spec's wildcard (`uriPatterns=*`) looked for a subject named "*" and every
generic client got `{"labels":[]}`. Reported by @hipstersmoothie.com.
Now parses uriPatterns as an array (repeated params, comma-joined values, or
both), supports `*` and `prefix*` patterns, and adds `sources`, `limit`
(1-250, default 50) and `cursor` with a response cursor on full pages.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
the fly-era builder-guard job died with the fly builder; symptom-level
staleness alerts (watchdog 180m + logfire) survived the cutover but a
failing prefect flow stayed silent for up to 3h. the 15-min watchdog now
also checks the pub-search-snapshot deployment's last terminal run and
pages on FAILED/CRASHED. skipped silently when PREFECT_API_* secrets are
absent; a transient prefect-API blip prints but does not page.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
builds now come from the pub-search-snapshot prefect deployment on
heavypad every 2h (docs/builder-offbox-plan.md); the serving side adopts
from R2 unchanged. first heavypad prod snapshot adopted 2026-07-25
(b1784954615-a767), fly machine snapshot-builder-hourly destroyed.
deleted: scripts/reconcile-snapshot-builder, scripts/guard-snapshot-builder,
scripts/fly_machines.py, the CI reconcile step, and the builder-guard
watchdog job. snapshot-age alerting stays (threshold 120m -> 180m to fit
the 2h cadence). runbooks updated to prefect deployment run.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
the deploy-time reconcile script re-provisions the builder machine env,
which silently undid the 2026-07-24 BUILDER_TIMEOUT_SECS=5400 bump (builds
run 13-40min depending on turso weather; 2700s left too little headroom
and watchdog kills produced stale-freshness alerts twice).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
search responses carry snippets only, which forces agents to re-fetch
records from the author's PDS and re-flatten block structures themselves
(and older leaflet shapes defeat naive extractors). the indexer already
stores extracted full text; serve it. up to 25 comma-separated uris per
request, read entirely from the local replica, same visibility policy as
search (banned/labeled/bridgy/dead-url excluded → listed under missing).
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>