Skip to content

feat(24): artist tags and notes - #9

Merged
danielrpof merged 140 commits into
mainfrom
gsd/phase-24-artist-tags-notes
Oct 6, 2026
Merged

danielrpof merged 140 commits into
mainfrom
gsd/phase-24-artist-tags-notes

Conversation

@danielrpof

Copy link
Copy Markdown
Owner

No description provided.

Grill decisions D-18..D-31 amend the discuss-phase design and take
precedence over it: cap trigger (ADR 0004), merge-by-UPDATE, tag/note
API shape, singular watchlist.note, lazy vocabulary load. Adds the
domain glossary entries and the phase pattern map.
The duplicate models/model_overrides blocks meant only the later copy applied. Merge both so the opus planning/verification and sonnet execution routing takes effect.
- tags/artist_tags tables keyed on artists.id, watchlist.note column (D-28)
- BEFORE INSERT cap trigger with post-lock existence re-check (ADR 0004, D-18)
- schema tests prove the raw-insert refusal, set-based skip-existing, the
  forced concurrent-same-tag race, name/note CHECKs, and cascade behavior
- bump hardcoded schema version 9 -> 10; regenerate sqlc output
Trim/collapse-whitespace/NFC identity plus the required/too-long/invalid
sentinels (TAG-03, TAG-04 encoding probe). Placeholder passes bytes through
unchanged so the package compiles; every case but the boundary-accept one
fails on assertion.
NFC normalization, trim/collapse whitespace, then validate required,
control-character, and 32-rune length (TAG-03, TAG-04 encoding probe).
- tags.Service.Attach: get-or-create + link in one transaction (D-29),
  constraint-name error mapping to ErrTagCapReached/ErrEntryNotFound
- POST /watchlist/{id}/tags, registered inside registerDataRoutes so it
  inherits gate.Authenticate + RequireCSRFHeader (D-20, T-24-03/04)
- ListWatchlist enrichment via two parallel ARRAY(...) subqueries, zipped
  in watchlist.Service.List into non-nil Entry.Tags/Note (D-26, D-30)
- TestTags_AttachEndToEnd proves the full chain against real Postgres
…gate coverage

- Service.Detach and DELETE /watchlist/{id}/tags/{tag_id} (idempotent 204,
  D-21); registered inside registerDataRoutes next to attach
- internal/tags/service_test.go: identity (ASCII + non-ASCII case fold),
  cap-reached orphan-tag rollback, idempotent detach, unknown-entry mapping
- internal/httpserver/tags_test.go: 400/404/409 mapping, looped concurrent
  cap race (ADR 0004 required test 1), concurrent same-link detach, gated
  401/403 coverage with zero store calls
- TestService_Remove_LeavesArtistTagsIntact + TestZipTags close out TAG-07
  and the parallel-array zip contract
- ListTags query: LEFT JOINs to artist_tags/watchlist, count(w.id) counts
  watched carriers only (D-11), zero-link and removed-only tags still
  surface (D-12, D-13), ordered by lower(name) then id (TAG-05)
- tags.Service.List and internal/httpserver's handleListTags/TagStore.List
- GET /tags registered in registerDataRoutes, inheriting the gate/CSRF stack
- RenameTag/GetTagByName/CountCarriersForTags/DeleteTagCountingCarriers
  queries; RenameTag is always called before any GetTagByName lookup, so
  collision is detected from the tags_name_lower_idx violation, never a
  pre-check (D-09, D-22)
- Service.Rename returns *CollisionError (target's stored casing plus the
  post-merge union count, D-23) on collision, retries once if the collider
  vanished between statements; Service.Delete reports the watched-carrier
  count a delete removed (TAG-06) while leaving watchlist rows untouched
- PATCH /tags/{id} and DELETE /tags/{id} handlers/routes; parseTagID
  generalized to take the path-param name
…e for all vocabulary routes

- LockTagsForMerge (ORDER BY id FOR UPDATE), DeleteDuplicateSourceLinks,
  RepointSourceLinks, DeleteTag, CountCarriers queries -- merges delete then
  UPDATE, never INSERT, so a 10-tag artist merging in a tag it already
  carries never transiently holds 11 links (D-19, ADR 0004)
- Service.Merge: self-merge and not-found guards, one transaction, keeps the
  target's stored casing (D-23); ADR 0004 required test 3 (10-tag artist
  carrying only the source) and the both-source-and-target companion case
  both pass
- POST /tags/{id}/merge handler/route; TestTags_Vocabulary_Gated401NoCookie
  and TestTags_Vocabulary_GatedForbiddenWithoutCSRFHeader cover all four
  vocabulary routes (list/rename/merge/delete)
… G-24-11)

- fake ResizeObserver test: expanded note keeps its focused 'less' toggle
- textarea keeps text-base below md, md:text-label from md up
… below md (G-24-9, G-24-11)

- overflow effect skips measuring while expanded, so 'less' stays mounted
- drop the unprefixed text-label from the note textarea
… each open (G-24-10)

- armed by open, consumed by the first successful load
- only moves focus from the popup, its close button, the body, or outside
Exclude archived milestone docs, phase 24 plan/summary logs, and frontend
tooling config from the graph; bump sonnet model override to 5.5.
Pin @modelcontextprotocol/sdk, brace-expansion, proxy-addr, source-map-js and
undici past fixed versions; ignore unfixable braces CVE-2026-93687 until
2026-11-05 (dev-only via shadcn CLI).
@github-actions

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown

Coverage

Area Coverage Δ vs main Gate Status
Backend 89.21% — 80% ✅
Frontend 94.90% — 70% ✅

head 69f49c8 · 2026-10-06T22:00:26Z
Delta not available yet — no main baseline cached (first run or evicted). Absolute coverage shown.

@danielrpof danielrpof changed the title Gsd/phase 24 artist tags notes feat(24): artist tags and notes Oct 6, 2026
@danielrpof
danielrpof merged commit 7eaa989 into main Oct 6, 2026
38 of 39 checks passed
@danielrpof
danielrpof deleted the gsd/phase-24-artist-tags-notes branch October 6, 2026 22:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant