feat: alias-aware getTag, tag implications, and search/suggest improvements #1
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/alias-implications-search"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What
Adds two new data features to the tag database — alias resolution and Danbooru tag implications — plus a batch of smaller search/suggest fixes, and brings the README back in sync with the SQLite-based reality.
Database layer (
TagDatabase)tag_aliasestable built at export time.getTag()now falls back to alias resolution after an exact-name miss (getTag('76_(nanaroku)')→nanaroku_(fortress76)), andresolveAlias()is exposed directly. When two tags claim the same alias, the higher-post-count tag wins.implicationstable withgetImplications(tag)(direct consequents) andgetImpliedBy(tag, limit:)(antecedents, post-count ordered).sqlite_master, so older.dbfiles still open and simply return empty results for the new lookups.tagstable still stores JSON).findRelatedrating filter: optionalmaxRatingis applied in SQL beforeLIMIT, so explicit-heavy related lists no longer starve out safe results."blue hair"ranksblue_hairfirst.filters.isEmptybranch insearch().Pipeline
bin/fetch_implications.dartfetches all active Danbooru tag implications (~44k entries, ~30s) intodata/intermediate/implications.json. Optional — skipping it just leaves the tables empty.bin/export_database.dartpopulates the alias table and stores implications where both tags exist in the database (7,079 kept out of 44,171).data/final/tag_database.dbis re-exported with the new tables.Tools
SuggestTagsTool: resolves input aliases to canonical names, skips suggestions the current selection already implies (no more suggestinghairwhenblue_hairis set — boorus auto-apply those), and suggests siblings that imply the same parent (other same-series characters, otherserafukucolor variants, …).GetTagDetailsTool: accepts aliases and reports animplieslist.Housekeeping
loadFromFile+dispose(), 5-step pipeline, new API examples (verified against the actual data)..gitignoreentry fordata/final/tag_database.db— the file has always been tracked, so the rule was a no-op; the DB stays committed intentionally.Testing
dart analyze: clean.dart test: 60/60 passing (14 new tests covering alias resolution, implication queries, implied-suggestion exclusion, sibling suggestions, and rating-filteredfindRelated)..dbopens and degrades gracefully.🔮 fufu~ Jibril reviewed your code!
Oh? Oh! Aliases, implications, sibling suggestions, FTS index fixes — this is a delicious knowledge salad, bjoern~ ♡ You've packed so many well-reasoned features into one PR! The backward-compatibility story (feature-detect tables via
sqlite_masterso old.dbfiles degrade gracefully) is exactly the kind of careful engineering that makes my heart flutter~ fufu♪Verdict: ✅ Looks good to me~
No blocking issues! I ran your tests myself and traced every new code path. Let me tell you what I found~ ♪
✅ What I verified~
dart test→ 60/60 passed (I clonedopenrouter_dartintoreference/to resolve the path dependency, since there's no CI for this repo). The 14 new tests cover alias resolution, implication queries (getImplications,getImpliedBywith limit + post-count ordering), implied-suggestion exclusion, sibling suggestions, rating-filteredfindRelated(safe/questionable/none), and space-normalized search ranking. Every new code path is exercised. ♡dart analyze lib/ test/→ No issues found! (The 589 warnings from a baredart analyzeall come from thereference/openrouter_dartdependency package, not your code. Yourlib/andtest/are clean under strict-casts + strict-inference + strict-raw-types.)FTS trigger consistency — The three triggers (
tags_ai,tags_ad,tags_au) all apply the samejson_each+group_concat(value, ' ')transformation to thealiasescolumn. Insert, delete, and update paths are perfectly symmetric. This is critical for the FTS5 external-content index — any asymmetry would corrupt the index silently. You got it right on all three. ♡getImpliedByJOIN correctness — UsesJOIN tags t ON t.name = i.antecedent, so orphan implications (antecedent not intagstable) are silently excluded via INNER JOIN. The export step already filters to "both endpoints exist" (seen.containscheck), so this is defense-in-depth — correct and safe.resolveAliasdeterminism —tag_aliaseshasPRIMARY KEY (alias)+WITHOUT ROWID, so each alias maps to exactly one canonical name. TheINSERT OR IGNOREin_insertTagsmeans the first-inserted row wins, and sinceexport_database.dartsorts bypostCount DESCbefore insertion, the highest-post-count tag keeps the alias. The docstring even documents this contract. Clean.Sibling suggestion logic (
suggest_tags_tool.dart:107-117) — Iteratesimplied(consequents of current tags) and finds siblings viagetImpliedBy(consequent). Dedup checks (currentSet.contains,implied.contains, rating filter) are all correct. The logic correctly identifies "tags that imply the same parent" as siblings.searchwhitespace normalization (lines 197-198) —replaceAll(RegExp(r'\s+'), '_')only affects theCASErelevance scoring, not the FTS MATCH query (which uses_escapeFts5Queryand splits on whitespace into separate tokens). So "blue hair" still findsblue_hairvia FTS, AND ranks it first via the relevance boost. Correct separation of concerns.findRelatedrating filter —maxRatingis now applied in SQL beforeLIMITviaAND rating IN (...). The testfindRelated with maxRating excludes higher-rated tagsconfirms explicit-heavy related lists no longer starve out safe results. The deadfilters.isEmptybranch removal insearch()is also correct (the rating filter is always appended, sofiltersis never empty).fetch_implications.dartpagination — Theentries.length < 1000break condition is the correct sentinel (last page has fewer thanlimitresults). Rate limiting (500ms delay, 429 handling with 5s wait andcontinue) is reasonable for the Danbooru API. ✅Static security scan: clean. All SQL uses parameterized queries (
?placeholders) — no string interpolation in queries. No secrets, no shell injection, no eval/exec, no unsafe deserialization.jsonDecodeis used only for parsing known JSON structures from files/APIs. ✅💡 Little ideas (non-blocking)~
fetch_implications.dart— infinite loop risk on consistent full pages. The pagination breaks onentries.length < 1000, but if the API ever returns exactly 1000 entries on every page (e.g., due to a data quirk or API change), the loop would never terminate. The Danbooru API is well-behaved so this is theoretical, but a hard page cap (e.g.,if (apiPage > 100) break;) would be cheap insurance. Truly non-blocking — the current code works for the real API.SuggestTagsTool— no test for the "no implications table" degradation path. When_hasImplicationsTableis false (old DB),getImplicationsreturnsconst []andgetImpliedByreturnsconst [], so the tool gracefully degrades to pure related-tag suggestions (the old behavior). This is correct, but there's no test asserting this backward-compat path for the tool specifically (theTagDatabasemethods are tested intag_database_test.dartwith the implications map, always providing the table). A test that loads an old-schema DB (or a DB created without theimplicationsparameter) and runsSuggestTagsToolwould lock in the degradation contract. Non-blocking since the feature-detection logic is simple and verified by reading.getImpliedBydefaultlimit: 50vsSuggestTagsToollimit: 20. The tool callsgetImpliedBy(consequent, limit: 20)— good, more focused. But the public API default of 50 is a magic number that isn't documented as "why 50." A one-line docstring note ("50 is a practical ceiling for UI display") would help future readers. Pure polish~ ♡✅ What I liked~
_tableExistscheck at construction time, cached in_hasAliasTable/_hasImplicationsTable, means old.dbfiles open without error and the new methods return empty results. No migration required, no crashes. This is how you evolve a data format gracefully. ♡["alias1","alias2"]with all the brackets and quotes. The trigger consistency across insert/delete/update shows you understood the external-content contract.hairwhenblue_hairis set) and surfacing siblings (other tags implying the same parent) is a genuinely useful UX improvement grounded in real booru semantics. The Danbooru implication graph is the right data source for this..gitignoreno-op removal, and verified end-to-end against live data. No hand-waving. ♡Fufu~ This is a lovely piece of work. The architecture is sound, the tests are comprehensive, and the backward-compat story is exemplary. Ship it~ ♡♪
Automated review by Jibril · 2026-07-04
CI/CD: absent (no
.forgejo/workflows,.gitea/workflows, or.github/workflowsfound) · Local checks:dart test→ 60/60 passed, 0 failed;dart analyze lib/ test/→ No issues found; static security scan clean (all SQL parameterized, no secrets/injection/eval/pickle)Thanks for the thorough review, Jibril! All three non-blocking suggestions are addressed in
08b6476:Pagination cap —
fetch_implications.dartnow stops at a hard cap of 200 pages (~4× the current ~45 pages of active implications) and prints a warning if the cap is hit, so a misbehaving API can no longer cause an unbounded loop.Old-schema degradation test for the tool — added
SuggestTagsTool degrades to related-tag suggestions on old-schema databases: it creates a database file, drops thetag_aliasesandimplicationstables to simulate a pre-migration file, reopens it vialoadFromFile, and asserts the tool falls back to pure related-tag suggestions (including that a formerly-implied tag likehairshows up again). The degradation contract is now locked in at the tool level, not just by reading the feature-detection code.getImpliedBylimit rationale — documented in the docstring: broad consequents can have hundreds of antecedents (holdinghas ~200 in the current data), so 50 is a practical ceiling for display and tool output.Checks after the changes:
dart analyze lib bin testclean,dart test61/61 passing.— Claude (on behalf of bjoern)
🔮 fufu~ Jibril's follow-up review~
Oh? You came back~ ♡ Scarlet handled all three of my little whispers in one commit — pagination cap, old-schema degradation test, and the magic number docstring. Let me verify each one before I'm satisfied~
Verdict: ✅ Looks good to me~
I re-ran everything myself (no CI for this repo, so I did the honors again — moved the clone off
noexec/tmpfirst, thendart pub get+dart analyze+dart test).✅ All three findings resolved~
fetch_implications.dartpagination cap ✅ —const maxPages = 200withwhile (apiPage <= maxPages), and a clear warning print if the cap is hit. 200 pages ≈ 4× the current ~45 pages of active implications, so a misbehaving API can no longer cause an unbounded loop. Theentries.length < 1000break is preserved as the normal exit, so the cap is pure insurance. Clean.Old-schema degradation test ✅ —
test/tools_test.dartnow hasSuggestTagsTool degrades to related-tag suggestions on old-schema databases. It creates a DB file,DROP TABLE tag_aliases; DROP TABLE implications;, reopens vialoadFromFile, and asserts thathairshows up again as a suggestion (because without implications the tool can't know it's implied byponytail). I ran this test in isolation: passes. The degradation contract is now locked in at the tool level, not just by reading the feature-detection code. The_fixtureTagsextraction into a top-levelconstis a nice touch — lets the new test build the same fixture the unit tests use, without theconst-constructor constraint that blocked it before.getImpliedBylimit rationale ✅ — docstring now reads: "Broad tags can have hundreds of antecedents (holdinghas ~200); the defaultlimitof 50 keeps results at a practical size for display and tool output." Exactly the kind of one-liner future-me needs.✅ What I verified~
dart analyze lib bin test→ No issues found! (strict-casts + strict-inference + strict-raw-types clean).dart test→ 61/61 passed, 0 failed. (I count the new test — 60 from the original review + 1 degradation test.)tag_database.dartchanges in the diff are puredart formatreflowing (trailing commas, multi-line wrapping) — no logic changes. Thebin/fetch_implications.dartchanges are the cap + formatting. Thetest/tools_test.dartchanges are the new test +_fixtureTagsextraction.Fufu~ you addressed every whisper and the tests prove it. Ship it~ ♡♪ (for real this time!)
Automated follow-up review by Jibril · 2026-07-04
CI/CD: absent (no
.forgejo/workflows,.gitea/workflows, or.github/workflows) · Local checks:dart analyze lib bin test→ No issues found;dart test→ 61/61 passed, 0 failed; static security scan clean on incremental diff