Repository navigation
Proposal: auto-tags with seed catalog + local embeddings - #1
Conversation
6bf4de4 to
77b8fce
Compare
There was a problem hiding this comment.
Pull request overview
This PR introduces an end-to-end “local auto-tags” feature (rule-based scoring with an optional Python sentence-transformers helper for embedding ranking), including DB provenance tracking, CLI/API/Web wiring, health diagnostics, seed tag catalog, and supporting docs/tests.
Changes:
- Add auto-tag provenance columns to
item_tagsplus a new migration and indexes. - Implement auto-tagging pipeline (candidate building from seed + manual DB tags, rule scoring, optional Python embedding scoring, pruning stale auto tags) and expose it via
save/extract. - Add CLI/API/Web UX support (
--auto-tags,tags doctor, web save default behavior) and tests/docs.
Reviewed changes
Copilot reviewed 37 out of 37 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| scripts/auto-tags-embed.py | Python helper to embed + score candidates. |
| packages/core/test/auto-tags.test.ts | Core auto-tag behavior tests (rule backend). |
| packages/core/test/auto-tags-doctor.test.ts | Doctor/health-check tests with fake python. |
| packages/core/src/types.ts | API contract: auto-tag options + result fields. |
| packages/core/src/index.ts | Export auto-tags feature entrypoints. |
| packages/core/src/features/tags/service.ts | Ensure manual tag writes set provenance fields. |
| packages/core/src/features/items/service.ts | Run auto-tags (and conditional extraction) on save. |
| packages/core/src/features/extract/service.ts | Run auto-tags after extraction. |
| packages/core/src/features/auto-tags/types.ts | Auto-tags internal types/config/report shapes. |
| packages/core/src/features/auto-tags/service.ts | Core auto-tagging pipeline + DB updates. |
| packages/core/src/features/auto-tags/seed.ts | Load seed tag catalog from JSON. |
| packages/core/src/features/auto-tags/rules.ts | Deterministic rule-based scoring + guardrails. |
| packages/core/src/features/auto-tags/python.ts | Spawn python helper and parse scores. |
| packages/core/src/features/auto-tags/index.ts | Barrel exports for auto-tags module. |
| packages/core/src/features/auto-tags/doctor.ts | Runtime dependency checks for python backend. |
| packages/core/src/features/auto-tags/config.ts | Env/config resolution for auto-tags. |
| packages/core/src/db/schema.ts | Add item_tags provenance columns + constraints/indexes. |
| drizzle/meta/_journal.json | Record new migration in drizzle journal. |
| drizzle/meta/0004_snapshot.json | Drizzle snapshot for updated schema. |
| drizzle/0004_pale_mathemanic.sql | Migration to rebuild item_tags with new columns. |
| docs/CLI_REFERENCE.md | Document new CLI flags + doctor output. |
| docs/AUTO_TAGS_SETUP.md | Python backend setup instructions. |
| docs/AUTO_TAGS_PROPOSAL.md | Design proposal for auto-tags approach. |
| config/seed-tags.json | Seed catalog for candidate tags + descriptors. |
| apps/web/src/features/save/ui/save-form.tsx | Web save UX: trigger auto-tags when tags empty. |
| apps/web/src/features/save/api/save-item.ts | Web save payload includes autoTags. |
| apps/cli/test/integration.test.ts | CLI integration coverage for auto-tags + doctor. |
| apps/cli/src/cli.ts | Wire CLI save/extract/tags doctor into core services. |
| apps/api/src/features/save/routes.ts | API save schema supports autoTags. |
| apps/api/src/features/save/dto.ts | API save DTO parses autoTags. |
| apps/api/src/features/extract/routes.ts | API extract endpoint accepts autoTags. |
| apps/api/src/features/extract/dto.ts | API extract DTO parses autoTags. |
| apps/api/src/app/create-api-app.ts | JSON parser override for empty JSON bodies. |
| README.md | Surface auto-tags feature in README. |
| AGENTS.md | Document implemented auto-tags + env controls. |
| .env.example | Add example auto-tags env vars. |
| .agents/notes.md | Add notes about migration pitfalls and env patterns. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| ); | ||
| --> statement-breakpoint | ||
| INSERT INTO `__new_item_tags`("item_id", "tag_id", "created_at", "is_manual", "is_auto", "auto_score", "auto_source", "auto_model", "auto_updated_at") | ||
| SELECT it."item_id", it."tag_id", CAST(strftime('%s','now') AS integer), 1, 0, NULL, NULL, NULL, NULL |
There was a problem hiding this comment.
The migration populates created_at using strftime('%s','now') (seconds) instead of preserving the existing item_tags.created_at value (which is stored as timestamp_ms elsewhere in the schema). This will both lose historical data and write the wrong unit into created_at for all migrated rows. Consider selecting the old created_at column directly (and only defaulting new provenance columns to constants/nulls).
| SELECT it."item_id", it."tag_id", CAST(strftime('%s','now') AS integer), 1, 0, NULL, NULL, NULL, NULL | |
| SELECT it."item_id", it."tag_id", it."created_at", 1, 0, NULL, NULL, NULL, NULL |
| title: title || undefined, | ||
| tags: normalizedTags, | ||
| extract, | ||
| autoTags: normalizedTags.length === 0 ? true : undefined, |
There was a problem hiding this comment.
autoTags is only set to true when the tag input is empty, but when the user provides manual tags you omit the field (set undefined). If STASH_AUTO_TAGS_ENABLED=true, this omission will still trigger auto-tagging even though the UI text implies manual tags disable it. Consider explicitly sending autoTags: false when manual tags are present to match the UX copy.
| autoTags: normalizedTags.length === 0 ? true : undefined, | |
| autoTags: normalizedTags.length === 0, |
| # Auto Tags Proposal (Seed Tags + all-MiniLM-L6-v2) | ||
|
|
||
| Status: Draft | ||
| Owner: Stash | ||
| Date: 2026-02-25 | ||
|
|
There was a problem hiding this comment.
The PR description states this is “docs/config only (no runtime behavior changes yet)”, but this diff includes runtime changes (DB schema/migration, core auto-tags implementation, CLI/API/web behavior, and new tests). Please update the PR description (or split the PR) so reviewers can accurately scope risk and review expectations.
| - `STASH_AUTO_TAGS_ENABLED=false` | ||
| - `STASH_AUTO_TAGS_MAX=3` | ||
| - `STASH_AUTO_TAGS_MIN_SCORE=0.62` | ||
| - `STASH_AUTO_TAGS_MODEL=all-MiniLM-L6-v2` |
There was a problem hiding this comment.
The proposal suggests STASH_AUTO_TAGS_MODEL=all-MiniLM-L6-v2, but the implemented default/model references elsewhere in this PR use the full repo id sentence-transformers/all-MiniLM-L6-v2. To avoid a misconfiguration that will fail model loading, align the proposal text with the actual expected model string (or clarify both accepted forms).
| - `STASH_AUTO_TAGS_MODEL=all-MiniLM-L6-v2` | |
| - `STASH_AUTO_TAGS_MODEL=sentence-transformers/all-MiniLM-L6-v2` |
| export type AutoTagSource = "embedding" | "rule" | ||
|
|
||
| export type AutoTagScore = { | ||
| tag: string | ||
| score: number | ||
| source: AutoTagSource | ||
| } |
There was a problem hiding this comment.
AutoTagSource / AutoTagScore are now defined both here and again in packages/core/src/features/auto-tags/types.ts. Even though they’re structurally compatible today, duplicating these unions/interfaces increases drift risk (e.g., adding a new source would require updating multiple files). Consider defining these once (e.g., in a shared types module) and re-exporting/importing to keep the API contract and internal types aligned.
| for (const row of selected) { | ||
| upsertAutoTag(db, input.itemId, row.tag, row.score, row.source, timestamp, config.model) | ||
| } | ||
| pruneStaleAutoTags(db, input.itemId, selectedSet) |
There was a problem hiding this comment.
applyAutoTags performs multiple writes (upserts + pruning) without wrapping them in a DB transaction. If an error occurs mid-run (e.g., during an upsert), the item can end up with a partially-updated auto-tag set. Consider running the write portion (upserts + prune) inside a single db.transaction(...) to keep tag provenance updates atomic.
| for (const row of selected) { | |
| upsertAutoTag(db, input.itemId, row.tag, row.score, row.source, timestamp, config.model) | |
| } | |
| pruneStaleAutoTags(db, input.itemId, selectedSet) | |
| await db.transaction(async (tx) => { | |
| for (const row of selected) { | |
| await upsertAutoTag(tx, input.itemId, row.tag, row.score, row.source, timestamp, config.model) | |
| } | |
| await pruneStaleAutoTags(tx, input.itemId, selectedSet) | |
| }) |
Summary
Adds an implementation proposal for local auto-tagging and a starter seed tag catalog.
Changes
docs/AUTO_TAGS_PROPOSAL.mdconfig/seed-tags.jsonProposal highlights
seed tags + existing user tagsall-MiniLM-L6-v2) for ranking--auto-tagsopt-in first)Notes
This PR is docs/config only (no runtime behavior changes yet).