Repository navigation
feat(24): artist tags and notes - #9
Merged
Merged
Conversation
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)
…focus, and textarea size (G-24-9..11)
… 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
…and textarea fixes
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).
Coverage
head 69f49c8 · 2026-10-06T22:00:26Z |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.