Skip to content

[patch] Rank standardized property names by fuzzy similarity - #120

Merged
matt-edmondson merged 2 commits into
mainfrom
claude/fuzzy-ranking-113
Sep 22, 2026
Merged

matt-edmondson merged 2 commits into
mainfrom
claude/fuzzy-ranking-113

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Refs #113.

The defect

NameStandardizer.FindStandardPropertyMatch admits a standard property by substring containment and returns the first one that qualifies. Containment says a candidate is plausible; it does not say how good it is. So the winner is decided by the order StandardOrder.PropertyNames happens to list them in:

key today this PR
subtitles_track title subtitle
review_status_flag status review_status

In both cases the shorter, less specific name wins only because it is listed earlier (title before subtitle, status before review_status).

The change

Candidates are ranked by ktsu.FuzzySearch, which was already a PackageReference of Frontmatter.csproj but had no call site anywhere in the repo — the repo's own CLAUDE.md flags it as referenced-but-unused. Its score rewards consecutive characters, separator boundaries and camelCase boundaries, and penalizes characters the pattern never matched: the ranking this matcher was approximating by hand.

FuzzyRanking.Score scores both directions and keeps the better one, because the caller's gate is itself bidirectional — a candidate qualifies whether it contains the key or is contained by it, and a one-directional score would leave half of the admitted candidates unranked.

What is deliberately not changed

The containment gate. The issue's sketch replaces it with Fuzzy.Contains, but that matches a subsequence, not a substring, so it admits strictly more candidates. Measuring it against the real StandardOrder.PropertyNames list, the extra admissions are all low- or negative-scoring (video_subtitles also matches video/doi; author_editorial_notes also matches audio/area) and every one of them loses on ranking anyway. Since ranking already discards them, widening the gate buys nothing and risks renaming frontmatter a caller meant to preserve — the failure mode here is silent data loss, so the conservative gate is the right default. Ties still fall back to the standard order, so the existing ordering still decides when the scores cannot.

PropertyMerger's word-overlap scorer, the other call site named in the issue. Its candidates only have to share a word, and related pairs like review_notes/notes_reviewed are neither a substring nor a subsequence of one another — so a whole-name fuzzy score returns nothing for them and hands the decision straight back to iteration order, which is worse than what is there today. Scoring per word pair instead does work, but I could not construct a case that pins the resulting ranking change through the public API, and an unpinned behaviour change on a merge path is not worth landing unattended. Detail left on #113.

Testing

  • Full suite: 129/129 pass (124 pre-existing, unchanged, + 5 new).
  • Both behavioural tests were verified to fail without the fix, producing exactly the wrong answers named above:
    failed StandardizePropertyNames_CandidateSharesAPrefixWithAnEarlierProperty_PrefersTheCloserMatch
      Expected 'subtitles_track' to standardize to 'subtitle'; got 'title'.
    failed StandardizePropertyNames_CandidateContainsACompoundProperty_PrefersTheMoreSpecificMatch
      Expected 'review_status_flag' to standardize to 'review_status'; got 'status'.
    
  • Three unit tests pin FuzzyRanking.Score's contract: symmetry, relative ranking, and that it reports no score when neither name is a subsequence of the other (the property that decides the PropertyMerger question above).

🤖 Generated with Claude Code

https://claude.ai/code/session_01S9FaM3MJE7n987yxvtxUZD


Generated by Claude Code

NameStandardizer.FindStandardPropertyMatch admitted a standard property by
substring containment and returned the first one that qualified, so the winner
was decided by the order StandardOrder.PropertyNames happens to list them in
rather than by how well it matched. "subtitles_track" standardized to "title"
instead of "subtitle", and "review_status_flag" to "status" instead of
"review_status" -- in both cases the shorter, less specific name won only
because it is listed earlier.

Candidates are now ranked by ktsu.FuzzySearch, which was already a
PackageReference of this project but had no call site anywhere in the repo.
Its score rewards consecutive characters, separator boundaries and camelCase
boundaries, and penalizes characters the pattern never matched -- the ranking
this matcher was approximating by hand.

The containment gate is deliberately unchanged. Fuzzy.Contains matches a
subsequence rather than a substring, so using it to admit candidates as well as
rank them would map keys that are preserved today; since ranking already
discards those weaker candidates, widening the gate buys nothing and risks
renaming frontmatter a caller meant to keep. Ties still fall back to the
standard order.

PropertyMerger's word-overlap scorer is the other call site named in the issue
and is left alone for now: its candidates only have to share a word, so pairs
like "review_notes"/"notes_reviewed" are related without either being a
subsequence of the other, and a whole-name score cannot rank them at all.

Refs #113

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S9FaM3MJE7n987yxvtxUZD
Comment thread Frontmatter.Test/FuzzyRankingTests.cs Fixed
The Standardize helper asserted the extracted frontmatter was not null and then
returned it, which leaves the return flowing a nullable value as far as static
analysis is concerned. Throwing AssertFailedException on the null branch makes
the contract explicit and carries a message naming the key that produced no
frontmatter.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S9FaM3MJE7n987yxvtxUZD
@sonarqubecloud

Copy link
Copy Markdown

@matt-edmondson
matt-edmondson merged commit cb603f1 into main Sep 22, 2026
12 checks passed
@matt-edmondson
matt-edmondson deleted the claude/fuzzy-ranking-113 branch September 22, 2026 00:30
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.

2 participants