Skip to content

fix(slug): never slugify to an empty string [UPL-53] - #517

Merged
chiptus merged 6 commits into
mainfrom
claude/focused-hopper-xcy330
Oct 4, 2026
Merged

chiptus merged 6 commits into
mainfrom
claude/focused-hopper-xcy330

Conversation

@chiptus

@chiptus chiptus commented Oct 2, 2026

Copy link
Copy Markdown
Owner

UPL-53: public.slugify() and its JS/Deno mirrors collapsed names with no ASCII alphanumerics (non-Latin, punctuation-only) to "", which broke the dedupe triggers and slug-based lookups. All three now fall back to a deterministic n-<md5 prefix> slug, and the CSV-import guard that rejected such names outright (a stopgap for this exact bug) is removed.

Verification

  • SELECT public.slugify('サカナクション') returns a stable n-xxxxxxxx slug, not ''.
  • Create an artist/stage/group/set named e.g. !!! or כנסיית השכל; it gets a non-empty, stable slug.
  • Import a schedule CSV with a Hebrew/non-Latin artist name; it imports instead of being rejected.
  • pnpm run typecheck, pnpm run lint, pnpm exec vitest run, and pnpm run build all pass.
  • Latin names are unaffected (Hello World → hello-world).

🤖 Generated with Claude Code

https://claude.ai/code/session_016w93iLcniKNQm3TfiBZp7S


Generated by Claude Code

claude added 2 commits October 2, 2026 05:18
…nly names

public.slugify() and its generateSlug/toSlug mirrors collapsed names with no
ASCII alphanumerics to "", which the dedupe triggers treat as "no slug
supplied" and the slug-based lookups/unique constraints can't handle. Fall
back to a deterministic md5-derived slug (UPL-53). Also drops the CSV-import
guard that rejected such names outright, since the empty-slug case it was
guarding against no longer exists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w93iLcniKNQm3TfiBZp7S
…ed md5

The fallback test compared generateSlug's output to its own md5 helper,
which can't catch a divergence from Postgres' md5() output. Hardcode the
expected hex digests instead, verified independently against Node's crypto
md5 (same RFC 1321 algorithm).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w93iLcniKNQm3TfiBZp7S
Copilot AI balanced review requested due to automatic review settings October 2, 2026 05:24
@vercel

vercel Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
upline Ready Ready Preview Oct 4, 2026 8:19am UTC

@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Deploy → staging — workflow run
Last updated: 2026-10-04 14:11:01 UTC

  • ✅ DB migrations succeeded
  • ✅ Edge functions succeeded
  • ⏭️ Type generation skipped (no changes)

@chiptus

chiptus commented Oct 2, 2026

Copy link
Copy Markdown
Owner Author

CI: "Require staging label for migrations" is failing, not fixable from this PR.

This PR ships a new migration (supabase/migrations/20261002050800_slugify_non_empty_fallback.sql), which trips the repo's "Require staging label for migrations" check — it needs the staging label applied to the PR. That label is off-limits for the agent pipeline (docs/agents/autonomic-issues.md guardrails), so I can't self-resolve this one; it needs a maintainer call: add the staging label to route the migration through the staging DB flow, or merge with this check red if it isn't a hard gate here. No code change needed for this check specifically — everything else on this PR is independent of it.


Generated by Claude Code

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Existing malformed slugs remain uncorrected, and clearing editable slug fields now produces an unintended hash.

Review effort: Balanced
Findings: 1 High severity · 1 Medium severity

Open (2)
What changed in this PR

Adds deterministic hash fallbacks so non-ASCII and punctuation-only names always receive valid slugs.

Changes:

  • Adds matching SQL, browser, and Deno slug fallbacks.
  • Removes CSV rejection of non-sluggable names.
  • Adds MD5 and slug behavior tests.
File Description
supabase/​migrations/​20261002050800_slugify_non_empty_fallback.sql Adds the database fallback.
supabase/​functions/​diff-schedule/​helpers.ts Mirrors fallback in Deno.
supabase/​functions/​diff-schedule/​helpers.test.ts Tests Deno fallback behavior.
src/​services/​scheduleImport/​parseCsv.ts Removes obsolete import guard.
src/​services/​scheduleImport/​parseCsv.test.ts Covers newly accepted names.
src/​lib/​slug.ts Adds browser-side fallback.
src/​lib/​slug.test.ts Tests deterministic fallback slugs.
src/​lib/​md5.ts Implements synchronous MD5.
src/​lib/​md5.test.ts Validates MD5 vectors.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread supabase/migrations/20261002050800_slugify_non_empty_fallback.sql
Comment thread src/lib/slug.ts Outdated
@github-actions

github-actions Bot commented Oct 2, 2026 •

Copy link
Copy Markdown

Playwright test results

passed  68 passed

Details

stats  68 tests across 22 suites
duration  1 minute, 21 seconds
commit  b56d11e

…e hash fallback

Addresses Copilot review on #517:
- Rows written before this fix could have slug = '' (or, for artists/sets,
  a '-2'/'-3' collision suffix on an empty base). Recompute those with the
  fixed public.slugify() and re-resolve any new collision by id, the same
  way the existing artists/sets unique-constraint migrations already do.
- sanitizeSlug() delegated to generateSlug(), so clearing a manually-
  controlled slug field (festival/edition dialogs) silently filled it with
  a hash of the empty string and bypassed the "slug is required"
  validation. sanitizeSlug() now preserves blank input; only generateSlug()
  (deriving a slug from a name) applies the non-empty fallback.

Verified the migration against a local Postgres 16 instance (seeded with
legacy '', '-2', '-3' rows across artists/sets/groups/stages, including a
same-name collision and a cross-edition case) before relying on it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w93iLcniKNQm3TfiBZp7S
Comment thread src/lib/md5.ts Outdated
Comment thread src/lib/slug.test.ts
Comment thread src/lib/slug.ts Outdated
Comment thread supabase/functions/diff-schedule/helpers.ts Outdated
Comment thread supabase/migrations/20261002051500_backfill_legacy_empty_slugs.sql Outdated
Addresses review on #517: replaces the duplicated ~90-line hand-rolled MD5
in src/lib/md5.ts and the private copy in the diff-schedule edge function
with the blueimp-md5 package (zero deps, works in both the browser bundle
and Deno via an npm: specifier) — same output, far less custom crypto-ish
code to maintain. Also applies a review suggestion: generateSlug() now
calls sanitizeSlug() instead of duplicating its stripping logic inline.

Verified the Deno side against the real edge runtime (Deno 2.9.7, matching
CI's deno-version: v2.x) rather than assuming the npm: import resolves.

Adds explicit Hebrew/Chinese fallback test cases per review discussion —
Hebrew is the actual name that surfaced this bug (see UPL-53's Linear
discussion).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w93iLcniKNQm3TfiBZp7S
Addresses review on #517: neither migration had reached staging/prod yet
(every deploy run so far was blocked on the "Require staging label"
check), so there's no reason to ship the slugify() redefinition and its
backfill as two separate migrations. Folded
20261002051500_backfill_legacy_empty_slugs.sql into
20261002050800_slugify_non_empty_fallback.sql, keeping the function
redefinition first so the backfill's UPDATEs call the fixed slugify().

Re-verified end-to-end as a single transaction against a local Postgres 16
instance (same legacy-row fixtures as before, including the same-name
collision and cross-edition case) before pushing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w93iLcniKNQm3TfiBZp7S
Comment thread supabase/migrations/20261002051500_backfill_legacy_empty_slugs.sql Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

The legacy-slug backfill can violate existing unique constraints before collision repair runs, aborting the migration.

Review effort: Balanced
Findings: 1 High severity · 2 Medium severity

Open (3)
Resolved since last review (2)
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

Comment thread supabase/migrations/20261002050800_slugify_non_empty_fallback.sql Outdated
Comment thread src/services/scheduleImport/parseCsv.test.ts
Comment thread supabase/migrations/20261002050800_slugify_non_empty_fallback.sql Outdated
…ce trim

Addresses Copilot review on #517, three confirmed bugs:

- The two-pass backfill (recompute, then separately re-suffix collisions)
  aborted the whole migration whenever two legacy rows shared a name: e.g.
  two artists both named "!!!" (slugs '' and '-2' from the old buggy
  trigger) both recompute to the same hash in one UPDATE, which violates
  artists_slug_unique before the re-suffix pass ever runs. Replaced with a
  per-row PL/pgSQL loop that resolves each row's collision (against
  existing rows and already-repaired siblings) before writing it, mirroring
  the dedupe triggers' own algorithm. Reproduced the abort and verified the
  fix against a local Postgres 16 instance seeded with this exact shape,
  plus a hash colliding with an unrelated existing group's slug.

- public.slugify() used plain TRIM(), which only strips ASCII spaces,
  while the JS/Deno mirrors' .trim() also strips tabs and newlines. A
  tab-padded name hashed differently in the DB than in resolveArtists()
  during CSV import, which would miss the existing row and create a
  duplicate. Switched to a \s-based regex trim in both places the function
  uses TRIM(); verified the SQL and JS outputs now agree on a tab-padded
  Hebrew name.

- resolveStage() treated two non-Latin stage names as a "close match"
  whenever both stripped to "" under the ASCII-only filter, now that such
  names can reach this function (the CSV-import guard that used to block
  them is gone). Added an explicit check: an empty stripped string never
  matches another empty stripped string.

Note: no automated migration-level regression test accompanies the backfill
fix — this sandbox has no Docker, so the repo's integration-test path
(`pnpm run test:integration`, which needs a local Supabase/Postgres via
Docker) isn't runnable here. Verified by hand instead with a close
approximation: a plain Postgres 16 instance with the same tables/
constraints, seeded with each adversarial case from the review.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016w93iLcniKNQm3TfiBZp7S
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown

Playwright test results

passed  372 passed
flaky  1 flaky

Details

stats  373 tests across 27 suites
duration  4 minutes, 45 seconds
commit  b56d11e

Flaky tests

webkit › groups-flow.spec.ts › group lifecycle: create, invite, join, isolate, leave

@chiptus
chiptus merged commit f4fedca into main Oct 4, 2026
51 of 53 checks passed
@chiptus
chiptus deleted the claude/focused-hopper-xcy330 branch October 4, 2026 14:20
@chiptus chiptus changed the title fix(slug): never slugify to an empty string fix(slug): never slugify to an empty string [UPL-53] Oct 4, 2026

This branch was successfully deployed

2 active deployments
staging — b56d11e1 Deployed Oct 4, 2026 by chiptus via types / Regenerate types (staging) #124
Preview — b56d11e1 Deployed Oct 4, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants