Skip to content

docs: add coding agent guidance from tag management implementation#62

Draft
andykais-claude wants to merge 53 commits into
mainfrom
cursor/tag-management-features-65fa
Draft

docs: add coding agent guidance from tag management implementation#62
andykais-claude wants to merge 53 commits into
mainfrom
cursor/tag-management-features-65fa

Conversation

@andykais-claude

Copy link
Copy Markdown
Collaborator

Lessons learned from the tag management feature PR (#56) and its follow-up fix PR (#61), documented for future coding agents.

Changes

AGENTS.md

  • Fix outdated migration example (was old export const pattern, now uses the decorator class pattern actually used in the codebase)
  • Add SQLite Migration Pitfalls section covering: DROP COLUMN with FK constraints, PRAGMA foreign_keys in transactions, trigger rebuilds when renaming tables, seed schema matching for the migration diff test
  • Add semantic field naming guideline (avoid generic source/target)
  • Add cleanup side-effects consideration (delete_unreferenced + rule references)
  • Add COUNT query performance tip
  • Clarify migration editing policy on feature branches

packages/core/.cursor/rules/core-patterns.md (new)

  • Table rebuild pattern step-by-step
  • media_add_tag helper documentation (alias resolution + parent propagation)
  • Tag rule undo mechanism (tag_alias_id/tag_parent_id tracking)
  • Count query pattern with PaginationVars
  • Slug-based rule references rationale

packages/web/.cursor/rules/web-patterns.md (new)

  • .svelte.ts extension requirement for runes (the rune_outside_svelte gotcha)
  • {#key} pattern for page param reactivity (vs onMount vs $effect)
  • display: contents CSS pattern for grid-through-slot layouts
  • URL encoding: don't encodeURIComponent tag slugs
Open in Web Open in Cursor 

cursoragent and others added 30 commits March 24, 2026 09:39
Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
- Use DROP COLUMN instead of table-rebuild for alias_tag_id removal
- tag_rule uses slug-based references (source_tag_slug/target_tag_slug)
- Add slug field to Tag model for fast lookups
- Alias tags have zero media_reference_tag rows; add_rule migrates existing rows
- Rename remove_rule to delete_rule for consistency
- Add aliases and parents columns to tag search results table
- Use slug instead of id for tag detail page route (/tags/[slug])
- Update all sections for slug-based design throughout

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
The polymorphic tag_rule table provided no real benefit since aliases
and parents have fundamentally different logic, side-effects, validation,
UI, and action methods. Separate tables and models keep each concept
self-contained.

Action interface updated to:
  Forager::tag::alias_create / alias_delete
  Forager::tag::parent_create / parent_delete

Input schemas, models, migration, RPC, and web pages updated throughout.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
…op alias_tag_id

- Add slug column to tag table (populated from tag_group.name:tag.name)
- Create tag_alias table with source_tag_slug/target_tag_slug (slug-based)
- Create tag_parent table with source_tag_slug/target_tag_slug (slug-based)
- Rebuild tag table to remove alias_tag_id (FK constraint requires full rebuild)
- Drop and recreate all triggers referencing the tag table
- Update seed migration to version 9 with new schema
- Update Tag model schema: add slug, remove alias_tag_id
- Remove alias_tag_id usage from tag_create() and series_actions
- Update migration test expectations for v9

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Replace inline UNIQUE constraints with named indexes for both tables:
- tag_alias_pair (unique), tag_alias_source, tag_alias_target
- tag_parent_pair (unique), tag_parent_source, tag_parent_target

These indexes will be needed for efficient slug-based lookups in the
TagAlias and TagParent models.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
- TagAlias: create, delete, select_by_source, select_all_by_target,
  update_source_slug, update_target_slug
- TagParent: create, delete, select_children, select_parents,
  update_source_slug, update_target_slug
- Register both in models/mod.ts and add result types

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Add #select_by_slug query and wire it into #select_one_impl so tags
can be looked up by slug (e.g. Tag.select_one({slug: 'genre:adventure'})).

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
New Zod schemas in tag_management_inputs.ts:
- TagGet, TagUpdate
- TagAliasCreate, TagAliasDelete
- TagParentCreate, TagParentDelete

Exported via inputs_parsers.ts and inputs_types.ts.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
…hods

New action methods on Forager::tag:
- get(slug): fetch tag detail with aliases, parents, children
- update(slug, name?, group?, description?): edit tag properties,
  recompute slug, update tag_alias/tag_parent references
- alias_create(source, target): create alias with media_reference_tag
  migration from source to target
- alias_delete(id): remove alias
- parent_create(source, target): create parent/child with cycle detection
- parent_delete(id): remove parent/child

Supporting changes:
- Tag model: add #update query
- MediaReferenceTag model: add select_all_by_tag_id query

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
- Replace TagRuleRef with result_types.Tag in TagDetail interface
- Move tag_slug_format to private #format_slug method on TagActions
- TagAliasCreate/TagParentCreate inputs now accept Tag structure
  (name+group or shorthand) instead of raw slug strings
- alias_create wraps create + migration in a transaction
- parent_create wraps create + media propagation in a transaction
- parent_create adds parent tag to all existing media with child tag
- tag.update throws if tag rules exist when renaming (name/group change)
- tag.update collision error includes other tag's id
- TagAlias model: field comments explaining source=alias, target=canonical
- TagParent model: field comments explaining source=child, target=parent

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
- Rename #migrate_alias_tags to #apply_alias_to_existing_media
- Remove #format_slug indirection, use tag_slug_format directly
- alias_create now returns TagDetail (for the target/canonical tag)
- parent_create now returns TagDetail (for the target/parent tag)
- Extract #build_tag_detail helper shared by get/alias_create/parent_create

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
…rns TagParentResponse

Both methods now return full response structures with TagDetail for
both sides of the relationship plus the rule record itself:
- TagAliasResponse: { alias, alias_target, rule }
- TagParentResponse: { parent, child, rule }

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Three top-level tests covering new tag management functionality:

- tag alias: create with migration, get relationships, duplicate/self/chain
  validation, delete, and alias-with-parent overlap
- tag parent: create with propagation, get relationships, self/circular
  validation, delete
- tag update: rename name/group/description, slug collision, rename
  blocked by existing rules, nonexistent tag

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
- Move all tests into tag.test.ts (delete tag_management.test.ts)
- Use ctx.assert.list_partial for alias/parent/children assertions
- Fix alias_create/parent_create to handle missing source tags
  (TagAliasResponse.alias and TagParentResponse.child are now nullable)
- Add test: alias migration when source+target share a media reference
- Add test: multi-level circular parent detection (A->B->C, then C->A)
- Add test: auto cleanup interaction with aliases
- Add test: alias_create when source tag doesn't exist yet
- Add test: parent_create when source tag doesn't exist yet

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
…agDetail

Rename alias_target -> alias_for throughout TagDetail, TagAliasResponse,
tag_actions.ts, tests, and design doc for clarity.

Add docstring comments to all TagDetail, TagAliasResponse, and
TagParentResponse fields describing their purpose.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Add get, update, alias_create, alias_delete, parent_create, and
parent_delete to ForagerTagApi.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
New route with controller, queryparams rune, and components:
- /tags/+page.svelte: page with header, search controls, results table
- TagsController: extends BaseController with TagQueryParams rune
- TagQueryParams: manages search state, URL sync, and RPC calls
- TagSearchParams: search input, sort dropdown, order toggle
- TagSearchResults: table with tag name, group, media/unread counts,
  created/updated dates; each tag links to /tags/[slug]

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
New /components/Datetime.svelte renders a <time> element with
toLocaleDateString() display and full ISO datetime in the title
attribute. Replace inline format_date helper in TagSearchResults.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
- tag_create now resolves aliases: if a tag is an alias, the canonical
  tag is returned instead
- manage_media_tags now applies parent tags: after all tags are applied,
  parent rules are walked and parent tags are added to the media
- series_actions uses tag_create for alias resolution
- Add test suite: 'tag rules respected during media operations' with
  subtests for alias resolution and parent propagation during both
  media.create and media.update

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
- Create shared BaseQueryParams rune in $lib/runes, TagQueryParams extends it
- Fix order param: wire through tag_actions.ts to Tag.select_paginated,
  use dynamic ASC/DESC in SQL ORDER BY clauses
- Tag component: add hide_count prop to suppress media_reference_count
- TagSearchResults: drop transparent on Tag chips, use hide_count,
  make table headers clickable to change sort column and toggle order
  (with sort direction indicator arrows)

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
…ination

- Restore show_group={false} on Tag chip in search results (group
  column makes it redundant)
- Add Scroller-based infinite scroll to TagSearchResults
- TagQueryParams: add load_more() method that increases limit by
  PAGE_SIZE and re-fetches, tracks has_more state
- Sticky table header within scroll container

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
- TagActions.search now returns TagSearchResultItem with alias_count
  and parent_count fields, computed per tag from tag_alias/tag_parent
- TagSearchResults table: add Aliases and Parents columns showing
  counts (blank when zero for cleaner display)

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Move tag count from inline text in TagSearchResults to a dedicated
Footer component matching the /browse page footer style. Shows
'Total tags N' with human-readable number formatting.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Add a nav section to the left side of the Header with Browse and Tags
links. The current page is highlighted (bright text, darker background),
inactive pages are subtle with hover states. Uses SvelteKit page state
for active detection via pathname prefix matching.

Simple inline nav links chosen over hamburger/dropdown since there are
only two pages — no hidden state or extra clicks needed.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Header now uses CSS grid (auto 1fr columns). SearchParams forms use
display:contents so their children become direct grid items. The search
bar row sits next to the nav (column 2), while the advanced filters
row spans the full width (grid-column: 1 / -1). The nav stays aligned
with the top row in both collapsed and expanded states.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
New route for viewing and editing a single tag's properties and
relationships.

Backend:
- TagDetail now includes rule_id on each related tag (TagWithRuleId)
  so the UI can delete specific alias/parent rules

Frontend (/tags/[slug]):
- TagDetailController: load, save, alias_create/delete, child_create,
  parent_add/delete methods with auto-reload after mutations
- Section 1 (Tag Info): editable name, group, description with save
  button. Navigates to new slug URL on rename.
- Section 2 (Aliases): shows alias_for target if this tag is an alias,
  lists tags aliased to this one, add alias input via TagAutoCompleteInput
- Section 3 (Parent/Child): lists children and parents with add/remove
  controls via TagAutoCompleteInput

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
…RL encoding

- Rename controller.ts -> controller.svelte.ts so Svelte compiler
  processes $state runes (fixes rune_outside_svelte error)
- Remove encodeURIComponent from all tag slug URLs — slugs only contain
  [a-z0-9_:] which are valid URL characters. URLs now show as
  /tags/medium:painting instead of /tags/medium%3Apainting

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
cursoragent and others added 23 commits March 24, 2026 09:39
- Show created_at date in the Tag Info section using Datetime component
- Add 'Browse media with this tag' link that navigates to
  /browse?tags=<slug>

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Replace generic source_tag_slug/target_tag_slug with semantically
meaningful names:

tag_alias table:
  source_tag_slug -> alias_tag_slug
  target_tag_slug -> alias_for_tag_slug

tag_parent table:
  source_tag_slug -> child_tag_slug
  target_tag_slug -> parent_tag_slug

Input schemas:
  source_tag/target_tag -> alias_tag/alias_for_tag (aliases)
  source_tag/target_tag -> child_tag/parent_tag (parents)

Updated migration, seed, models, actions, and all tests.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
alias_create: source_tag/target_tag -> alias_tag/alias_for_tag
parent_create: source_tag/target_tag -> child_tag/parent_tag

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
The logic was right but the descriptions were backwards. Fixed:
- TagParent model: child_tag_slug is the tag that triggers parent inclusion
- TagDetail: children = when a child is applied, this parent is also applied
- TagDetail: parents = when this tag is applied, the parent is also applied
- UI labels updated to match the correct semantics

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Migration v11: rebuild media_reference_tag with tag_alias_id and
tag_parent_id nullable FK columns referencing tag_alias and tag_parent.

Model changes:
- MediaReferenceTag: add tag_alias_id/tag_parent_id to schema and create
- Add delete_by_tag_alias_id and delete_by_tag_parent_id queries

New media_add_tag helper in base.ts:
- Replaces tag_create + MediaReferenceTag.get_or_create pattern everywhere
- Resolves aliases (sets tag_alias_id on the created row)
- Walks parent chain and adds parent tags (sets tag_parent_id on each)

Action changes:
- alias_create: passes alias rule ID to #apply_alias_to_existing_media
- alias_delete: deletes media_reference_tag rows by tag_alias_id first
- parent_create: passes parent rule ID to #apply_parent_to_existing_media
- parent_delete: deletes media_reference_tag rows by tag_parent_id first

Tests:
- alias_delete undoes media_reference_tag changes
- parent_delete undoes media_reference_tag changes
- alias_delete does not remove manually-added tags
- media.create with alias tracks tag_alias_id for undo
- media.update with parent tracks tag_parent_id for undo

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
- Add comment that parent tag resolution does not resolve aliases
- Wrap alias_create, alias_delete, parent_create, parent_delete in
  transactions for atomicity
- Pass whole tag_alias/tag_parent records to #apply_alias_to_existing_media
  and #apply_parent_to_existing_media instead of positional args
- Add PRAGMA foreign_key_check before COMMIT in migration v11

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
…ngth is needed

Add count_by_alias, count_by_alias_for to TagAlias model.
Add count_children, count_parents to TagParent model.

Replace list-then-count patterns in TagActions:
- tag.search: use count queries for alias_count and parent_count
- tag.update: use count queries to check for existing rules

Remove unused listing methods:
- TagAlias.select_all_by_alias_for (replaced by count_by_alias_for)
- TagParent.select_children (replaced by count_children)

Retain listing methods still needed for iteration:
- TagParent.select_parents (used in BFS parent chain walking)
- TagAlias.select_by_alias (used for alias resolution and error messages)

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
…leting

alias_delete now reverses the alias migration: media_reference_tag rows
that were created by the alias rule are moved back to point at the
original alias tag (with tag_alias_id cleared), rather than being
deleted entirely. This properly undoes the alias — media that was
tagged with the alias before the rule was created gets its original
tag association restored.

Added select_all_by_tag_alias_id query to MediaReferenceTag model.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Replace onMount with $effect watching page.params.slug so the tag
detail reloads when clicking tag links within the detail page.
onMount only fires once on initial mount, so client-side navigations
to a different /tags/[slug] were changing the URL but not reloading
the data.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
Use {#key slug} to destroy and recreate the entire page content when
the slug param changes. The controller is created fresh inside the
keyed block via {@const}, so all state (detail, draft, inputs) resets.

Moved input state (alias_input, child_input, parent_input) into the
controller so they're part of the fresh instance. Controller now
accepts slug in the constructor and loads immediately.

Co-authored-by: andykais-claude <andykais-claude@users.noreply.github.com>
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.

3 participants