Skip to content

Stop a single-character key being containment-matched onto unrelated properties - #129

Merged
matt-edmondson merged 1 commit into
mainfrom
claude/exciting-albattani-zq715h
Sep 26, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
claude/exciting-albattani-zq715h

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #128

Filed and fixed per the recommendation left on #113: "The single-character looseness noted and deliberately left alone (meta_x_field → x → next) is the same class of bug as the empty-string one, one step less severe. Worth its own issue rather than leaving it in a comment — it is main's behaviour today and will outlive this thread."

The defect

PropertyNameNormalizer.Normalize strips one decorative prefix and one decorative suffix, so meta_x_field normalizes to the single character x. The containment tests in both matching classes are bidirectional, so a one-character fragment is contained in any candidate that happens to use that letter — "next" contains "x" — and the key is admitted on an incidental letter rather than a shared word.

Both classes already guarded the degenerate Length == 0 case, each with a comment describing exactly this failure mode. A length of 1 fell through that guard.

Measured at main (6ed61c5), .NET SDK 10.0.401, Linux

NameStandardizer.StandardizePropertyNames silently renames the key:

key normalizes to before after
meta_x_field x next meta_x_field
page_a_value a area page_a_value
meta_e_field e editor meta_e_field
custom_s_data s slug custom_s_data
page_x_text x next page_x_text
page_t_data t toc page_t_data
meta_z_info z meta_z_info meta_z_info

meta_z_info was already preserved, and only because no standard property happens to contain the letter "z" — which is the clearest statement of how incidental the old match was.

PropertyMerger.MergeSimilarProperties under FrontmatterMergeStrategy.Maximum is worse than reattribution — it drops a property outright:

IN   [meta_x_field=A, next=B, text=C]
before  [meta_x_field=A, next=B]        // text=C gone
after   [meta_x_field=A, next=B, text=C]

The cause is the same bidirectionality: meta_x_field resolved to next (as "next" contains "x") while next resolved back to meta_x_field at the same time, and the third key that also contained the letter was lost in the crossfire.

This is the failure mode #113 and #124 set out to close — a value silently attributed to an unrelated property, in a library whose job is round-tripping frontmatter — reached by a fragment of length 1 rather than length 0.

The change

The rule lives in one place, as PropertyNameNormalizer.MayMatchByContainment, following the consolidation #124 established rather than adding a third copy of the same reasoning. It is applied at the three containment sites:

  • NameStandardizer.FindStandardPropertyMatch — the partial-match loop
  • PropertyMerger.FindBasicCanonicalName — the partial-match loop
  • PropertyMerger.CalculateWordMatchScore — the containment branch scoring +1

The third one matters and is easy to miss: without it, FindSemanticCanonicalName's word-score path re-admits the same key after the other two are guarded, since a one-character key word scores +1 against any word containing it and .Where(match => match.Score > 0) then accepts it.

Containment only. Exact matching on a one-character normalized form is untouched, so the fix is not "ignore short keys".

The floor is two, not more. The shortest names anywhere in StandardOrder.PropertyNames or PropertyMappings are by, tag and url, and nothing in either normalizes to fewer than two characters — so no legitimate name was being matched by containment on a single character, and none stops matching now.

Tests

Four added in SingleCharacterKeyTests, alongside the existing DecorationOnlyKeyTests it parallels.

test covers
StandardizePropertyNames_PreservesKeysNormalizingToOneCharacter all six renaming cases in the table
MergeSimilarProperties_DoesNotMergeOrDropOnASingleCharacterFragment the dropped third property
MayMatchByContainment_RejectsAOneCharacterNameOnEitherSide the rule itself, both argument positions
StandardizePropertyNames_StillMatchesATwoCharacterFragment no regression at the floor (ag is in tags, stage, image)

Proved failing without the fix. Reverting only the two call-site files and keeping the new helper — so it still compiles — the two behavioural tests fail and the other two pass. That is the intended split: the rule test exercises the helper, and the floor test is a no-regression test that should pass both before and after.

Verification

  • dotnet build Frontmatter.sln -c Release — succeeded, 0 warnings (this repo runs analyzers as errors)
  • dotnet test Frontmatter.sln -c Release — 145 total, 145 passed, 0 failed
  • Same in Debug — 145 passed
  • Baseline before this change was 141, so the 141 pre-existing tests are unchanged

Not in this change

The ktsu.FuzzySearch adoption in PropertyMerger that #113 still carries is untouched — that one waits on the ranking decision recorded there, and this is an independent bug fix.

The adjacent looseness one step further out is also left alone: containment on a two-character fragment is admitted, so meta_ag_field still lands on a standard property via tags/stage/image. That is main's behaviour, it is pinned here as a no-regression test rather than changed, and narrowing it further is a behaviour judgement that wants its own issue — the same reasoning by which the single-character case was split out of #113 into #128.

🤖 Generated with Claude Code

https://claude.ai/code/session_013qMsnuVCPrfVynzgpbKQTD


Generated by Claude Code

…related properties

A key like meta_x_field normalizes to the single character x, and the
containment tests in NameStandardizer and PropertyMerger are bidirectional,
so x is admitted against any candidate that happens to use that letter:
"next" contains "x". The key is then renamed, or merged, onto an unrelated
property.

Both classes already guarded the empty normalized form, with comments
describing exactly this failure. A length of 1 fell through that guard.

Measured on main: StandardizePropertyNames turned meta_x_field into next,
page_a_value into area, meta_e_field into editor, custom_s_data into slug
and page_t_data into toc. MergeSimilarProperties was worse than
reattribution -- {meta_x_field, next, text} came back as
{meta_x_field, next}, dropping text entirely, because containment matched
mutually in both directions.

The rule now lives once, as PropertyNameNormalizer.MayMatchByContainment,
and applies to the three containment sites: the standardizer's partial-match
loop, the merger's partial-match loop, and CalculateWordMatchScore's
containment branch, which could otherwise re-admit the same key through the
word-score path.

Containment only. Exact matching on a one-character normalized form is
untouched, and the floor is two characters rather than more, so the shortest
real names anywhere in StandardOrder.PropertyNames or PropertyMappings --
by, tag, url -- still match as before.

Fixes #128

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

Copy link
Copy Markdown

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.

A key normalizing to a single character is containment-matched against unrelated properties, reattributing and losing values

2 participants