Repository navigation
fix(search): compare frontmatter tags ignoring letter case and store them by Obsidian's tag rule - #667
Conversation
…them by Obsidian's tag rule Tag comparisons across the index now fold both sides with toLowerCase(), the rule Obsidian's Tags view groups spellings by: vault_search_by_tag in both modes, vault_search's tags filter, vault_list_tasks's tag filter, and vault_list_tags, which merges spellings under the most-used one. A leading "#" on any tag input is accepted; an input left empty after it is rejected. vault_search's tags filter matches nested tags like vault_search_by_tag. Frontmatter tags are stored by Obsidian 1.14.4's own rule, in a new pure parser (obsidian-markdown/tags.ts): the first key spelled tags in any letter case, a text value as one tag, a list's text entries, each without a leading "#" or a trailing "/", and only names Obsidian accepts as tags. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
List obsidian-markdown/tags in the AGENTS.md structure tree, say in ARCHITECTURE.md that a leading # on a tag input is dropped and a lone # rejected, describe how vault_list_tags merges letter-case spellings, and name the tag comparison helpers in the search-helpers row. Ship-Check: pr-review · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comments state why the tags parser keeps repeats, what an empty tag input means, and how the tag listing picks a spelling. searchByTag picks its predicate and bound value in one branch. The tag tool texts say a leading # is optional, show a nested match, and say tags come back in the note's letter case; the tool-surface snapshots are regenerated. Ship-Check: code-quality · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… the response formatter tags.ts exports isTagsKey, the one rule for a property key spelled tags in any letter case, and tool-helpers.ts uses it for the promoted-key strip instead of restating the rule with toLowerCase(). Ship-Check: triage · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- listAllTags: the merge fixture now separates ranking by occurrences from ranking by notes and from the binary tie-break, and both merge tests assert the whole tag list - searchByTag: the Projects and my-project decoys sit in their own note, so a match on either one shows up - new tests: a stored non-ASCII tag in prefix mode, a leading # in exact mode, the enriched chunk prefix's tags, a trimmed text tags value, and a Supplemental Punctuation name - exact error and fold assertions; it.each labels within vitest's 40-character title limit Ship-Check: test-audit · claude-opus-5-5 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
formatNoteMetadata strips only the tags key the index read (the first key spelled tags in any letter case), so a later key such as TAGS with its own values stays in additional_properties instead of vanishing from the response. Ship-Check: triage · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Add tags.ts to the Obsidian Markdown Parsing module list
The page's purpose enumerates the obsidian-markdown/ parser modules but omits the new tags.ts this PR adds to that layer, so the page now describes an incomplete parser inventory. AGENTS.md's parallel tree comment was updated in this PR, leaving the two inventories inconsistent. Failure scenario: A contributor or a DeepWiki index reads the Obsidian Markdown Parsing page to learn which parsers the layer holds; tags.ts — now the home of frontmatter tag parsing consumed by the search index and formatNoteMetadata — is absent from the list, so tag-parsing work gets attributed to frontmatter.ts or overlooked. Suggested fixExtend the enumeration in the Obsidian Markdown Parsing purpose with tags.ts (frontmatter tag parsing per Obsidian's Tags view rules).umm-actually · z-ai/glm-5.3-flash |
|
umm-actually re-reviewed at No new findings (6 tracked finding(s) across all runs). Context notes
umm-actually · z-ai/glm-5.3-flash |
Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Fixed (issue comment 6087399066, 🔍 ship-check · pr-monitor · claude-fable-5-1 |
|
Update the additional_properties claim for the per-note tags key
The PR makes formatNoteMetadata promote the first tags-spelled key in any letter case and keep a later one under additional_properties, but vault_search_by_property's Returns bullet still claims additional_properties holds only keys outside title, tags, type, created, and related. A capitalized Tags key no longer lands there and a second tags-spelled key now does, so the documented contract and the response shape disagree. Failure scenario: A note whose frontmatter is Suggested fixReword the bullet to the new rule, e.g. "- additional_properties holds every frontmatter key whose value was not promoted — title, type, created, related, and the first key spelled tags in any letter case; a later key spelled tags stays here too."umm-actually · z-ai/glm-5.3-flash |
… per-note tags key ARCHITECTURE.md scopes the nested-tag sentence to non-exact searches; vault_search_by_tag's Returns says the tags array holds the frontmatter tags Obsidian accepts, as written; vault_search_by_property's Returns names the tags key the index read among the keys kept out of additional_properties. Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
|
Fixed (issue comment 6087801512, 🔍 ship-check · pr-monitor · claude-fable-5-1 |
…once The vault_search tags bullet read "all required"; it now says every listed frontmatter tag must match. The empty-tag Errors bullets in the three tools no longer wrap a message that contains quotes in a second pair of quotes. Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
vault_search_by_tag's Returns names the tags array as every frontmatter tag Obsidian accepts, in the note's letter case; vault_list_tags says only tags Obsidian accepts are counted and names the secondary sort; the vault_search_by_property bullet names the first key spelled tags in any letter case instead of "the tags key read"; vault_list_tasks's path error keeps its remedy. Ship-Check: pr-monitor · claude-fable-5-1 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
FIXED_PROMOTED_KEYS holds the four keys with one spelling; promotedKeysOfNote adds the tags key the note's frontmatter spells, and the comment says why that key varies per note. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Summary
Tag comparisons in the search index now ignore letter case, and frontmatter tags are stored by the rule Obsidian's Tags view applies, so
vault_search_by_tag({ tag: "Project" })finds notes taggedproject,vault_list_tagslistsProjectandprojectas one tag, and a leading#on any tag input is accepted.What changed
fold_tagand its JavaScript mirrorfoldTagboth applytoLowerCase(), the rule Obsidian groups tag spellings by.vault_search_by_tag(both modes),vault_search'stagsfilter (SQL and the vector-only mirror),vault_list_tasks'stagfilter, andvault_list_tagsall compare folded values.NESTED_TAG_PREDICATE(fold_tag(value) || '/' LIKE ? ESCAPE '\'bound to<tag>/%), so a tag matches itself or any tag nested under it with one fold per stored value.vault_search'stagsfilter now nests likevault_search_by_tag.#. Every tag input drops one leading#. An input that is empty after it ("#") is rejected withtag must not be empty after its leading "#", listed in the three tools' Errors and asserted over HTTP.obsidian-markdown/tags.tsreproduces Obsidian 1.14.4'sparseFrontMatterTags+getTagsname test: the first key spelledtagsin any letter case, a text value as one tag, a list's text entries, each trimmed, without a leading#or a trailing/, kept only when Obsidian would list it as a tag (no whitespace, none of Obsidian's rejected punctuation, not digits only).coerceToArraystays forrelated.vault_list_tagsmerges spellings. Spellings that differ only in letter case are one entry, counted as distinct notes and shown as the spelling with the most occurrences across notes (ties in binary order).Tags:reported once.formatNoteMetadatastrips the tags key the index read, in any letter case, fromadditional_properties; a later key spelledtagswith its own values stays there.vault_search,vault_search_by_tag,vault_list_tags, andvault_list_tasksstate the case rule, the nested match, and the#error; ARCHITECTURE.md, AGENTS.md, and the wiki inventory updated; tool-surface snapshot regenerated.Tool-surface size:
default136,295 → 136,588 (+293),embedding-off135,079 → 135,372 (+293); both under the cap.Tool-definition review: the
tool-definition-revieweragent compared main's captured tool list with this branch's. Only the five intended tools changed and no fact was dropped. Its four defects in the new text are applied (an unnamed referent invault_search_by_property,vault_search_by_tag's Returns claiming tags come back "as written",vault_list_tagsnot saying only accepted names are counted, and two optional remedies: the secondary sort invault_list_tagsand the.mdremedy invault_list_tasks). Dismissed: re-quoting the empty-tag error message, which wraps a message containing quotes in a second pair and read as confusing on review; and listing the index-corruption error of the row mappers, which a client cannot act on.Not a breaking change: the
tagsarray was documented as "every frontmatter tag" andvault_list_tagsas omitting#, which the stored values now satisfy; nested matches invault_search's filter and the accepted#are additive; the values that no longer index (2024,alpha, beta,true) were never documented as tags and Obsidian does not show them.Tests
tags.test.ts: the name rule (accepted and rejected characters, digits only, the Punctuation blocks), key lookup in any letter case, first-key-wins, text and list values, trailing-slash and leading-#removal.search-helpers.test.ts:foldTag(ẞ/ß merge, ß/ss apart),normalizeTagQuery,tagMatchesQuery,nestedTagLikePattern, and the vector-only mirror with decoys.search-index.test.ts: stored tags per key form;searchByTagcase folding in both modes withProjects/my-projectdecoys, non-ASCII folding,#and/inputs,foo//;fullTextSearchnested tags filter;listAllTagsmerge, occurrence ranking, tie-break; the chunk metadata prefix.task-queries.test.ts,hybrid-search.test.ts: case-insensitive task tag filter; vector-only tag filter with an FTS miss.tool-helpers.test.ts:formatNoteMetadatawith aTags:key and a second tags-spelled key.server-error-contracts.test.ts: the"#"error over HTTP for all three tools.Full suite: 5,288 tests passing; eslint, tsc, knip, markdownlint clean.
Live Test Results
Head: e71536d
Image:
ghcr.io/aliasunder/vault-cortex:testbuilt from that head (org.opencontainers.image.revisionmatches); the container was re-created, started, and reported healthy.This run re-verifies the head after a rename in
tool-helpers.ts. The first run, onbd39ecc16eb9d98c30d5b3425cfcbe51fd7f0a86, gave the same results.Four probe notes, each with the body token
prprobetoken667:probe-upper(tagPrProbe667),probe-lower(tagprprobe667),prprobe667-child(tagprprobe667/child), and the decoyprprobe667s(tagprprobe667s). Before = the server before the test deploy; after = the test build.vault_search_by_tag({ tag: "prprobe667" }): before lower, child; after lower, upper, child.vault_search_by_tag({ tag: "PrProbe667" }): before upper, child; after lower, upper, child.vault_search_by_tag({ tag: "#prprobe667" }): before empty; after lower, upper, child.vault_search_by_tag({ tag: "PRPROBE667", exact: true }): before empty; after lower, upper.vault_search({ query: "prprobetoken667", filters: { tags: ["prprobe667"] } }): before lower; after child, lower, upper.vault_search_by_tag({ tag: "#" }): after returns the errortag must not be empty after its leading "#".vault_list_tags: before one entry per spelling; afterPrProbe667with count 2 (the two spellings merged, the tie in binary order),prprobe667/child1,prprobe667s1.The probe notes were removed after the test.
🤖 Generated with Claude Code