Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
9272af2
feat(admin): entail.dev tag suggestion client (SONA-220)
sparkyfen Sep 8, 2026
c7c59dc
fix(admin): let the entail.dev poll outlast the server's wait hold (S…
sparkyfen Sep 8, 2026
5e0efde
feat(admin): tag suggestion endpoint (SONA-220)
sparkyfen Sep 8, 2026
e33f4f4
fix(admin): empty entail.dev classification is a success with no tags…
sparkyfen Sep 8, 2026
d91c21b
docs(architecture): add entail.dev tag classifier (SONA-220)
sparkyfen Sep 8, 2026
a1b673a
fix(admin): review round 1 for the entail.dev tag suggestion client (…
sparkyfen Sep 8, 2026
97579b0
fix(admin): review round 2 for the entail.dev tag suggestion client (…
sparkyfen Sep 8, 2026
2520a6c
fix(admin): review round 3 for the entail.dev tag suggestion client (…
sparkyfen Sep 8, 2026
8d247e9
fix(admin): review round 4 for the entail.dev tag suggestion client (…
sparkyfen Sep 8, 2026
fbcc1d4
fix(admin): review round 5 for the entail.dev tag suggestion client (…
sparkyfen Sep 8, 2026
9c3b99d
fix(admin): review round 6 polish for the entail.dev tag suggestion c…
sparkyfen Sep 8, 2026
c1345db
fix(admin): lookup deadline clears one full first attempt (SONA-220)
sparkyfen Sep 8, 2026
40fc749
fix(admin): final review polish for the tag suggestion endpoint (SONA…
sparkyfen Sep 8, 2026
2194aeb
fix(admin): keep expected lookup outcomes out of the error metric (SO…
sparkyfen Sep 8, 2026
a29e901
fix(admin): answer 404 and 202 for declined or pending lookups (SONA-…
sparkyfen Sep 8, 2026
3419ae6
fix(admin): only input-rejection codes count as the classifier declin…
sparkyfen Sep 8, 2026
b05a742
docs(admin): describe not_found by the declined status set (SONA-220)
sparkyfen Sep 8, 2026
d84dbe0
fix(admin): clamp the Bluesky image count and state the 202 caller co…
sparkyfen Sep 8, 2026
4e51be9
fix(admin): terminal jobs, X error envelopes, and slashed tags (SONA-…
sparkyfen Sep 8, 2026
d48a416
fix(admin): read the request body up to the cap instead of buffering …
sparkyfen Sep 8, 2026
145519d
fix(admin): send no User-Agent on X guest-token requests (SONA-220)
sparkyfen Sep 8, 2026
2fbe43e
Let the lookup deadline clear both polls of a first attempt
sparkyfen Sep 14, 2026
cdc39b3
Merge origin/main into sparky/sona-220-entail-tag-suggestions
sparkyfen Sep 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions AI_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ second AI model reads the first one's work, and CodeRabbit, a third-party
service, reviews the diff. A human approves every merge and clicks every
deploy. There is no review team here, just one maintainer and a set of tools.

The running site makes one AI call of its own. When the operator asks for tag
suggestions on a piece of artwork, the site sends entail.dev, an image
classifier, a public link to the artwork's source post or to the picture in
that post. In normal operation, nothing a visitor does reaches an AI service.
The one exception is the diagnostic case the /ai page describes: when the
maintainer is debugging, logs that can include visitor IP addresses and page
URLs may pass through the development tools.

## Rules the agents work under

These rules predate this file, and every agent session on this project works
Expand Down
25 changes: 25 additions & 0 deletions UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,31 @@ you sync — or to catch up on what shipped since you last did — read the
the merged changes since the previous one. `git log --oneline <last-tag>..upstream/main`
after a fetch gives the same view from a clone.

## Read before upgrading: tag suggestions call entail.dev (SONA-220)

This release adds an admin-only tag suggestion lookup for artwork. Only you can
start it, and nothing runs on its own. When you do, the site sends a public
link to entail.dev, an image classifier, and shows you the tags it returns. For a
Bluesky post that link is the post itself. For an X post the site first asks X's
own API which picture the post carries, then sends the picture link X hands back.
Nothing from either reply is stored; the suggestions are yours to accept or drop.

The built-in privacy policy and the `/ai` page describe this call already. If
you pasted your own privacy text or your own `/ai` text in Settings, **neither
was updated**, and your pages will not mention services your site now contacts.
In your privacy policy's list of feature integrations, replace your X and
Bluesky entry with the first line below and add the second:

- `X (formerly Twitter) and Bluesky (fetching the profile pictures shown on this site; X is also asked which picture a post carries)`
- `entail.dev (an image classifier that suggests tags for artwork from its source post or the picture in it)`

And add this to your `/ai` text: "The site calls an AI service in one place. When
the site owner asks for tag suggestions on a piece of artwork, the site sends a
public link to entail.dev, an image classifier. That link points at the artwork's
source post, or at the picture in that post. For a post on X, the site first asks
X's own service which picture the post carries. Only the site owner can start
that, so nothing you do is sent to an AI service as you browse."

## One-time backfill: sticker animation flags (SONA-123)

The release that adds the per-sticker download-format menu also adds a
Expand Down
11 changes: 9 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ graph TB
Resend[✉️ Resend]
Turnstile[🧩 Cloudflare Turnstile]
ConsFYI[📅 cons.fyi]
Avatars[🖼️ Bluesky + X — profile picture sources]
Avatars[🖼️ Bluesky + X — profile pictures, tweet media]
Entail[🏷️ entail.dev — image tag classifier]
UT[☁️ UploadThing — optional]
end

Expand Down Expand Up @@ -91,14 +92,15 @@ graph TB
Auth -->|reset email| Resend
RateLimit --> Turnstile
Public -->|convention dates| ConsFYI
API -->|tag suggestions for a source post| Entail

RegClient -->|search / pull / submit| RegWorker
RegWorker --> RegD1

CI --> Deploy
Deploy -->|wrangler pages deploy| Hooks
CronWF -->|POST /api/cron/* with CRON_SECRET| API
API -->|fetch profile pictures to re-host| Avatars
API -->|fetch profile pictures to re-host, resolve tweet media| Avatars
Admin -->|fetch profile pictures to re-host| Avatars
Release -.->|pull tagged releases| Forks
```
Expand Down Expand Up @@ -133,6 +135,11 @@ graph TB
zone rather than the reader's or UTC.
- Telegram, FurTrack, FuzzySearch, Resend, and Turnstile are optional
integrations, keyed off secrets or settings (see `wrangler.toml.example` for the full list).
- entail.dev needs no key or secret. The app calls it only when an operator
asks for tag suggestions on an image whose source post is on Bluesky or X,
and never on a render path or a schedule. An X post takes one extra hop:
X's own API resolves the post to its image, and that image URL is what
entail.dev classifies.
- GitHub Actions is part of the runtime, not just delivery: the scheduled
workflows (`sticker-resync` daily 06:00 UTC, `artist-sync` 06:30,
`avatar-refresh` 07:00, `cleanup-orphans` weekly, `backfill-animated`
Expand Down
9 changes: 8 additions & 1 deletion src/lib/ai-disclosure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,14 @@ describe('defaultAiDisclosure', () => {
});

it('states the runtime boundary and the dev-time access plainly', () => {
expect(all).toMatch(/never calls an AI service, so nothing you do is sent to one as you browse/);
// SONA-220 added the one runtime call; the disclosure names it and keeps
// the browsing-time claim scoped so it stays true.
expect(all).toMatch(/calls an AI service in one place/);
expect(all).toContain('entail.dev');
// Both paths: the post link (Bluesky) and the picture link X hands back.
expect(all).toMatch(/points at the artwork's source post, or at the picture in that post/);
expect(all).toMatch(/the site first asks X's own service which picture the post carries/);
expect(all).toMatch(/Only the site owner can start that, so nothing you do is sent to an AI service as you browse/);
expect(all).toMatch(/logs and database/);
expect(all).toContain('CodeRabbit');
// Honesty about what dev-time log access can expose: no "your data never
Expand Down
7 changes: 4 additions & 3 deletions src/lib/ai-disclosure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@
//
// Wording approved by the operator 2026-08-12 after a peer review that set the
// register: short, disclosure-only, no persuasion. Facts only — every claim
// here is backed by the codebase (no runtime AI calls anywhere), the repo
// policy (AI_POLICY.md), or the operator's own practice. An owner whose
// here is backed by the codebase (the one runtime AI call is the operator's
// tag-suggestion lookup against entail.dev, SONA-220), the repo policy
// (AI_POLICY.md), or the operator's own practice. An owner whose
// practice differs (or who wants their own words) overrides the text via
// Settings, or turns the page off entirely with the aiPageEnabled toggle.
//
Expand Down Expand Up @@ -70,7 +71,7 @@ export function defaultAiDisclosure(): AiDisclosure {
},
{
lead: 'Your data.',
body: "The running site never calls an AI service, so nothing you do is sent to one as you browse. When the software is being worked on, the developer's tools can read this site's logs and database, as any developer's could, and those logs can include visitors' IP addresses and the pages they requested. Code goes to Anthropic and to CodeRabbit, a review service. Model training is switched off on the accounts used, and CodeRabbit states that the data from its reviews is never used for training. The privacy policy has the details."
body: "The site calls an AI service in one place. When the site owner asks for tag suggestions on a piece of artwork, the site sends a public link to entail.dev, an image classifier. That link points at the artwork's source post, or at the picture in that post. For a post on X, the site first asks X's own service which picture the post carries. Only the site owner can start that, so nothing you do is sent to an AI service as you browse. When the software is being worked on, the developer's tools can read this site's logs and database, as any developer's could, and those logs can include visitors' IP addresses and the pages they requested. Code goes to Anthropic and to CodeRabbit, a review service. Model training is switched off on the accounts used, and CodeRabbit states that the data from its reviews is never used for training. The privacy policy has the details."
},
{
lead: 'The model.',
Expand Down
11 changes: 7 additions & 4 deletions src/lib/legal.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ describe('defaultPrivacyPolicy', () => {
// The runtime boundary, honestly scoped: nothing browsing-time goes to the
// tools, but shared diagnostic logs can carry request data — both halves
// must stay, or the paragraph overclaims again.
expect(text).toMatch(/nothing you do here is sent to them as you browse/);
expect(text).toMatch(/nothing you do here is sent to those tools as you browse/);
expect(text).toMatch(/can contain request data such as IP addresses/);
});

Expand Down Expand Up @@ -180,7 +180,10 @@ describe('defaultPrivacyPolicy', () => {
expect(text).toContain('cons.fyi');
// The integrations list reads exhaustive, so it must actually be: every
// remote service a feature calls out to is named (SONA-167 round 1).
expect(text).toContain('Bluesky');
// Picture lookup is an X-only ask; Bluesky posts go to entail.dev whole.
expect(text).toMatch(/X \(formerly Twitter\) and Bluesky \(fetching the profile pictures shown on this site; X is also asked which picture a post carries\)/);
// SONA-220: the tag-suggestion lookup sends a post or picture URL to entail.dev.
expect(text).toMatch(/entail.dev \(an image classifier that suggests tags for artwork from its source post or the picture in it\)/);
expect(text).toContain('FurTrack');
expect(text).toContain('FuzzySearch');
// The lookup sends a copy of the image, and an unpublished one too; naming
Expand Down Expand Up @@ -297,8 +300,8 @@ describe('LEGAL_DEFAULTS_UPDATED tracks the default text', () => {
// privacy page would show a "Last updated" line older than its own text.
// Deliberately two assertions, not a diff — the point is to force the date
// bump, not to review the prose.
const RECORDED_TEXT_HASH = '3a1187cb2970888de509560a9e0c028b3340d5ba39f393da83e0c943b407a780';
const RECORDED_UPDATED = '2026-09-14';
const RECORDED_TEXT_HASH = 'b2c656867b76ec288a85b230ccce8279bdff6cd963edea4e90e1b31f0c7351fa';
const RECORDED_UPDATED = '2026-09-15';

function defaultsText(): string {
// Fixed opts so the hash depends on the prose alone, not the caller. Both
Expand Down
6 changes: 3 additions & 3 deletions src/lib/legal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface LegalSection {
// on every fork by construction (a build/deploy date would falsely advance on a
// redeploy that didn't touch the text). Bump this whenever you edit
// defaultPrivacyPolicy or defaultTerms.
export const LEGAL_DEFAULTS_UPDATED = '2026-09-14';
export const LEGAL_DEFAULTS_UPDATED = '2026-09-15';

/**
* Resolve the "Last updated" date to show on a legal page from a *stable* source
Expand Down Expand Up @@ -150,13 +150,13 @@ export function defaultPrivacyPolicy(opts: LegalOptions): LegalSection[] {
// paragraph with the toggle would delete a real processor
// disclosure. Only the vendor NAMES follow the affirmation, since
// those are the part a declining owner has not stood behind.
'Sites running this software are typically built and maintained with AI development tools, which do not run as part of the site itself, so nothing you do here is sent to them as you browse. When the site owner or their developer is diagnosing a problem, the operational data they share with development or code-review tools can include server logs and database records, and those logs can contain request data such as IP addresses, page URLs, and browser user-agent strings.',
'Sites running this software are typically built and maintained with AI development tools, which do not run as part of the site itself, so nothing you do here is sent to those tools as you browse. When the site owner or their developer is diagnosing a problem, the operational data they share with development or code-review tools can include server logs and database records, and those logs can contain request data such as IP addresses, page URLs, and browser user-agent strings.',
...(opts.aiToolsDisclosed === false
? []
: [
"For this site those tools are Anthropic's Claude, which writes and debugs code under the developer's direction, and CodeRabbit, a code review service that reads proposed changes."
]),
"For specific features the site also talks to Cloudflare Turnstile (bot protection on the sign-in page), Telegram (importing sticker packs), cons.fyi (convention listings), X (formerly Twitter) and Bluesky (fetching the profile pictures shown on this site), FurTrack (importing fursuit photos), FuzzySearch (finding where a piece was posted; when the site owner asks for a lookup, a copy of that image is sent, including images not published on this site), and the shared artist registry (syncing artist credits; the registry receives this site's name and hostname as part of the sync). The site contacts these services to run the feature; they are not used to track visitors."
"For specific features the site also talks to Cloudflare Turnstile (bot protection on the sign-in page), Telegram (importing sticker packs), cons.fyi (convention listings), X (formerly Twitter) and Bluesky (fetching the profile pictures shown on this site; X is also asked which picture a post carries), entail.dev (an image classifier that suggests tags for artwork from its source post or the picture in it), FurTrack (importing fursuit photos), FuzzySearch (finding where a piece was posted; when the site owner asks for a lookup, a copy of that image is sent, including images not published on this site), and the shared artist registry (syncing artist credits; the registry receives this site's name and hostname as part of the sync). The site contacts these services to run the feature; they are not used to track visitors."
]
},
{
Expand Down
Loading
Loading