Tag discovery and dual audiences: v0.0.28.0
Behavior
This release changes server and storage logic. The existing search and tag controls stay unchanged.
| Case | Result |
|---|---|
| Filter by a tag on your own, saved, favorited, or reviewed card | The matching card remains eligible, including its language-group representative. |
A card has both 男性向 and 女性向 | Either tag alone finds it; selecting both also finds it. Its target_audience is all. |
| Select several tags | A card must contain every selected tag (AND). |
| Male or female discovery preference | Only cards exclusively tagged for the opposite audience are excluded. Dual-tagged cards remain eligible for both. |
| Search a translated tag label or known alias | Search and filtering use the shared tag vocabulary, including legacy stored spellings. |
| Search an unknown custom tag | Its original text remains searchable. Popularity is not an eligibility requirement. |
Publication, visibility, blocks, language, and content restrictions still apply. Unfiltered recommendations retain their existing exclusions for own and saved cards.
Tag catalog API
GET /api/worlds/hub/tags returns popular shortcuts when q is absent. With q, it searches eligible published tags before applying the result limit and ranks exact matches ahead of more popular substring matches. For example, ?q=全性向&lang=zh&limit=20 can return that tag even if it is absent from the popular shortcuts.
Raw custom-tag text was already searchable; this release fixes exclusion and alias gaps and improves the tag catalog query. It does not expand the visible popular-tag shelf.
The catalog accepts contentLevel=sensitive as well as the older r18 and r18g values. Guests remain restricted to safe content. Tag names and counts follow viewer access rules; authenticated responses are private and are not cached across users.
Saving and publishing
Adding the second audience tag preserves both, including when publishing sends an older single-audience form value. An intentional audience-setting edit can still switch an existing dual-tagged card to one audience. Owner and administrator edits resolve audience settings against locked current tags; publishing uses the resolved audience in the review snapshot.
The storage guard preserves both audience tags within the 50-tag cap. It does not reconstruct tags removed by earlier versions; creators can add those tags again.
Database rollout
Install the new guard before application traffic reaches this release. The previous worlds_audience_consistent constraint forbids dual tags. The idempotent installer replaces it with validated worlds_audience_consistent_v2, updates the trigger, and reconciles inconsistent records without changing updated_at.
The startup self-heal is a fallback: production schedules its first attempt after 90 seconds. It is not the rollout migration path.
Run from packages/server, with credentials supplied through the environment. The production script requires PRODUCTION_DATABASE_URL; development uses DATABASE_URL.
pnpm exec tsx scripts/repair-world-audience.ts --production
pnpm exec tsx scripts/repair-world-audience.ts --production --apply --backup <new-backup-file>The first command audits in a read-only transaction. Applying requires a new backup file, takes a transaction-scoped lock, validates the constraint, and checks that repaired records retain their timestamps. Backups contain card metadata and must remain outside Git.
Production verification, 2026-09-14
- The v2 constraint is installed and validated; the old constraint was removed.
- All 17,747 existing cards retained their tags, audience values, and timestamps; zero records violate the new constraint.
- A temporary-table trigger probe verified dual-tag insertion resolves to
all, either/both tag queries match, and an explicit switch to female-only succeeds. No persistent test cards were created. - Application rollout and live smoke checks are separate deployment steps.
Validation
- 153 focused tests passed, covering recommendation exclusions, aliases, catalog access rules, audience persistence, SQL constraints, v1-to-v2 upgrades, and both serialized save/publish orders.
- 27 existing publish-route guards passed with CRLF normalized in the verification harness. No route-guard source changes were needed.
- The release checkout passed all 5 build tasks and all 8 typecheck tasks, run sequentially.
- No frontend files changed and no browser session was opened for verification.
