Fold the two NormalizePropertyName helpers, and stop decoration-only keys being silently renamed - #124
Merged
Conversation
…n-only keys being renamed NameStandardizer and PropertyMerger each carried their own copy of NormalizePropertyName, and the copies had drifted. The standardizer lower-cased without trimming, compared its prefix and suffix culture-sensitively, and replaced only the space character, so a key carrying leading or trailing whitespace - a tab especially - kept the padding and missed the affix strip entirely. Measured over a corpus of 30 keys, the two implementations disagreed on 15. Both now call a single PropertyNameNormalizer, which takes the merger's behaviour: trim first so a padded key normalizes like its bare form, ordinal affix comparisons, and one separator collapse. Folding them surfaced a latent bug that was already reachable on main without any padding. A key made only of decoration - "page_", "_value", "_", "page__value" - normalizes to the empty string, and every string contains the empty string, so the containment tests in both classes admitted it against the whole candidate set. The standardizer renamed such a key to "title" and the merger folded it into an unrelated property, losing the value either way, which is the one thing a round-tripping frontmatter library must not do. Both call sites now treat an empty normalized form as "nothing to match on" and leave the key alone. Padding previously masked some of this by accident: " page_ " survived only because the missing trim blocked the prefix strip, while "page_" did not. That inconsistency is gone. Three regression tests cover the two guards and the padded/bare invariant; all three fail against the previous implementation. The existing 129 tests are unchanged and still pass. Refs #113 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJa1Uvm6PDR74cLBoE4PMY
All five land in code this PR introduces. csharpsquid:S3267 x2 on the affix loops. Sonar suggests Where, but the loop body reassigns the captured key between the prefix and suffix steps, so a deferred LINQ filter over it would be a hazard rather than a simplification. Array.Find says what the loop actually does - take the first match, then act - and removes the finding on its own terms. MSTEST0068 x2: CollectionAssert.AreEqual -> Assert.AreSequenceEqual. MSTEST0037: Assert.AreEqual(1, x.Count) -> Assert.HasCount(1, x). No behaviour change; Array.Find returns the first match exactly as the foreach/break did. Suite still 137 green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UJa1Uvm6PDR74cLBoE4PMY
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Picks up the last outstanding item noted on #113 — "the two near-duplicate
NormalizePropertyNamehelpers are untouched" — and, in doing it, turns up a data-loss bug that is already reachable onmain.Refs #113. This does not close it: the
PropertyMergerfuzzy-ranking half is still open and still waiting on the judgement call recorded there. Only the helper fold is done here.The fold
NameStandardizerandPropertyMergereach carried their ownNormalizePropertyName, and the copies had drifted. The standardizer's:StartsWith(string)/EndsWith(string))Measured over a 30-key corpus, the two disagreed on 15. A sample:
NameStandardizerPropertyMerger\ttitle\ttitletitle" meta_description "meta_descriptiondescription" user_name_value "user_name_valuename" post_headline "post_headlineheadline" page_ "pageBoth now call one
PropertyNameNormalizer, which takes the merger's behaviour on every point of difference — it is the more robust of the two:Trim()covers all whitespace rather than the space alone, ordinal comparison is correct for fixed ASCII affix tokens, andSplit/Joincollapses separator runs in one pass.The bug the fold surfaced
A key made only of decoration —
page_,_value,_,page__value,meta_— normalizes to the empty string. Every string contains the empty string, so the containment tests in both classes admit such a key against the entire candidate set, and it is then renamed to, or merged into, whichever candidate ranks first.Verified on
main, before any change here:The value is silently attributed to an unrelated property — the one thing a round-tripping frontmatter library must not do.
Padding was masking part of it by accident:
" page_ "survived only because the standardizer's missing trim blocked the prefix strip, while bare"page_"did not. Folding the helpers without fixing this would have made the failure uniform rather than removing it, so both call sites now treat an empty normalized form as "nothing to match on" and leave the key alone.Tests
Three regression tests, each of which fails against the previous implementation (verified by reverting the two source files and re-running — 3 failed, 134 passed):
StandardizePropertyNames_PreservesDecorationOnlyKeysMergeSimilarProperties_DoesNotMergeDecorationOnlyKeyIntoAnotherStandardizePropertyNames_TreatsPaddedAndUnpaddedKeysAlikePlus five characterization tests pinning the shared normalizer's semantics.
The padded/bare test asserts over the decision rather than the literal spelling, because an unmatched key is preserved verbatim — padding included — which is correct for a round-tripping library.
Full suite: 137 passed, 0 failed (129 pre-existing, unchanged).
Scope notes
internal.meta_x_field→x→next. That ismain's behaviour for the unpadded form already; this PR only makes the padded form agree with it. Narrowing that heuristic is a separate behaviour judgement.🤖 Generated with Claude Code
https://claude.ai/code/session_01UJa1Uvm6PDR74cLBoE4PMY
Generated by Claude Code