Skip to content

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.

CaseResult
Filter by a tag on your own, saved, favorited, or reviewed cardThe 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 tagsA card must contain every selected tag (AND).
Male or female discovery preferenceOnly cards exclusively tagged for the opposite audience are excluded. Dual-tagged cards remain eligible for both.
Search a translated tag label or known aliasSearch and filtering use the shared tag vocabulary, including legacy stored spellings.
Search an unknown custom tagIts 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.

sh
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.