Skip to content

feat(textnorm): MeaningPreset + HygienePreset (meaning-preserving normalization) - #34

Merged
alessiosavi merged 5 commits into
masterfrom
feat/meaning-preset
Jul 15, 2026
Merged

feat(textnorm): MeaningPreset + HygienePreset (meaning-preserving normalization)#34
alessiosavi merged 5 commits into
masterfrom
feat/meaning-preset

Conversation

@alessiosavi

Copy link
Copy Markdown
Owner

What

Four new pipeline stages and two presets for meaning-preserving text normalization:

  • PreserveMeaningPunct — rejected runes become spaces (never deleted, so tokens don't merge: 2.52 5 under old FilterRunes deletion became 25). Keeps ./, between digits (4.5, 1,000 — separators NOT unified), token-final + (s22+, c++), % after digit (100%). Combining marks (Mn/Mc/Me) are kept — Devanagari matras and Arabic harakat are vowels, not punctuation.
  • NormalizeUnicodeLatin — strips diacritics ONLY from Latin base characters. cafécafe, but किताब and مَكتَب survive intact (the existing NormalizeUnicode removes ALL Mn marks, destroying Indic/Arabic vowel signs).
  • DecodeHTMLEntities / RemoveFormatChars — byte hygiene: entity decoding, Cf/Cc removal (except \n/\t).
  • MeaningPreset = SanitizeUTF8 → DecodeHTMLEntities → NormalizeUnicodeLatin → [FoldWidth] → FoldCase → PreserveMeaningPunct → TrimSpace → CollapseWhitespace. No token dedup. For content hashes where Galaxy S22+ must not collide with Galaxy S22.
  • HygienePreset = SanitizeUTF8 → DecodeHTMLEntities → RemoveFormatChars → [FoldWidth] → TrimSpace → CollapseWhitespace. For model payloads — case/punctuation/diacritics preserved.

Why

Downstream consumers (framework-golang textpipe → veliu.com embeddings) need a fingerprint preset that doesn't destroy meaning and a payload-grade hygiene preset. Existing presets are intentionally untouched (byte-identical behavior).

Testing

  • Golden tests pin the distinctions: s22+s22, 4.545, 1,0001 000, no dedup, Indic/Arabic preserved.
  • FuzzMeaningPreset (20s run clean): output alphabet restricted to letters/numbers/marks/space/.,+%, no uncollapsed whitespace.
  • Full module: 683 tests green, go vet clean.

Release: will tag v1.3.0 after merge.

MeaningPreset: fingerprints that must preserve meaning (s22+ != s22,
4.5 != 45, 1,000 != 1 000; no token merging/dedup; Latin-only diacritic
stripping). PreserveMeaningPunct now also keeps combining marks (Mn/Mc/Me)
so Devanagari matras / Arabic harakat survive the stage standalone.
HygienePreset: payload-grade byte hygiene, case/punct/diacritics untouched.
@git-code-reviewer-v1

Copy link
Copy Markdown

👀 Starting code review...

@git-code-reviewer-v1

Copy link
Copy Markdown

🔄 Cloning repository...

@git-code-reviewer-v1

Copy link
Copy Markdown

🤖 Analyzing code with opencode...

@git-code-reviewer-v1

Copy link
Copy Markdown

✅ Review complete!

@git-code-reviewer-v1 git-code-reviewer-v1 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-thought-out addition to the textnorm package. The two new presets (MeaningPreset, HygienePreset) and supporting stages (PreserveMeaningPunct, DecodeHTMLEntities, RemoveFormatChars, NormalizeUnicodeLatin) follow existing patterns, are thoroughly unit-tested, and include fuzz coverage. No blocking correctness, security, or performance issues were found.

Non-blocking findings

  • hygiene.go and meaning.go contain package-level doc comments (// Package textnorm …) in addition to doc.go. Go tooling shows whichever package comment it encounters first; having multiple package comments is non-standard and can cause the wrong synopsis to surface in godoc/pkgsite depending on file ordering. Convert them to plain file-level comments (drop the Package textnorm prefix) or fold the text into doc.go.
  • fuzz_test.go gained a fuzz target for MeaningPreset but not for HygienePreset. Adding FuzzHygienePreset would keep parity with the other presets and guard idempotency regressions.

Comment thread textnorm/hygiene.go Outdated
@@ -0,0 +1,34 @@
// Package textnorm — hygiene.go: byte-hygiene stages for text that will be

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package comment duplicates (and could shadow) the one in doc.go. Convert to a plain file-level comment or merge into doc.go.

Suggested change
// Package textnorm — hygiene.go: byte-hygiene stages for text that will be
// hygiene.go: byte-hygiene stages for text that will be
// sent to an external model or stored verbatim. These stages never touch
// case, punctuation, or diacritics.
package textnorm

Comment thread textnorm/meaning.go Outdated
@@ -0,0 +1,62 @@
// Package textnorm — meaning.go implements meaning-preserving punctuation

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This package comment duplicates (and could shadow) the one in doc.go. Convert to a plain file-level comment or merge into doc.go.

Suggested change
// Package textnorm — meaning.go implements meaning-preserving punctuation
// meaning.go implements meaning-preserving punctuation filtering: instead
// of deleting non-alphanumeric runes (which merges tokens, e.g. "2.5"→"25"),
// every rejected rune becomes a single space, and the small set of
// punctuation that carries meaning in product/search text is kept:
//
// - '.' and ',' when BOTH neighbours are digits ("4.5", "1,000").
// Separators are NOT unified: "1,000" and "1.000" stay distinct because
// the same string means different numbers in different locales.
// - '+' when it terminates an alphanumeric token ("s22+", "c++").
// - '%' immediately after a digit ("100%").
package textnorm

Comment thread textnorm/fuzz_test.go
t.Fatalf("uncollapsed whitespace in %q", out)
}
})
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding an idempotency fuzz target for HygienePreset to match the coverage the other presets already have.

Suggested change
}
func FuzzHygienePreset(f *testing.F) {
for _, seed := range []string{
"Café \u200bCrème!",
"A & B",
" spaced\n\nout\t ",
} {
f.Add(seed)
}
f.Fuzz(func(t *testing.T, in string) {
pipe := HygienePreset()
out1, err := pipe.Run(in)
if err != nil {
t.Fatalf("Run() error = %v", err)
}
out2, err := pipe.Run(out1)
if err != nil {
t.Fatalf("second Run() error = %v", err)
}
if out1 != out2 {
t.Fatalf("HygienePreset not idempotent: %q != %q", out1, out2)
}
})
}

Addresses PR #34 review: meaning.go/hygiene.go no longer carry competing
package comments (detail moved to stage docs); HygienePreset gets a fuzz
target checking UTF-8 validity, no Cf/Cc runes, collapsed whitespace —
deliberately no idempotency invariant since entity decoding is not
idempotent.
@git-code-reviewer-v1

Copy link
Copy Markdown

👀 Starting code review...

@git-code-reviewer-v1

Copy link
Copy Markdown

🔄 Cloning repository...

@git-code-reviewer-v1

Copy link
Copy Markdown

🤖 Analyzing code with opencode...

@alessiosavi
alessiosavi merged commit 21a2358 into master Jul 15, 2026
4 checks passed
@alessiosavi
alessiosavi deleted the feat/meaning-preset branch July 15, 2026 00:22
@git-code-reviewer-v1

Copy link
Copy Markdown

✅ Review complete!

@git-code-reviewer-v1 git-code-reviewer-v1 Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All prior review feedback has been addressed in commit 4bb16f0: the duplicate package comments in hygiene.go and meaning.go were removed, and a FuzzHygienePreset target was added with a clear explanation of why idempotency is intentionally skipped (HTML-entity decoding is legitimately non-idempotent).

A fresh pass over the full change set finds no blocking issues:

  • Correctness: PreserveMeaningPunct correctly keeps ./, only when both neighbors are ASCII digits, + only when terminating an alphanumeric token, and % only after a digit, avoiding token-merging regressions. NormalizeUnicodeLatin strips Mn marks only when the preceding base is Latin, preserving non-Latin combining marks. RemoveFormatChars correctly excludes \n and \t from removal.
  • Error handling: No swallowed errors or silent failures; html.UnescapeString is appropriately wrapped.
  • Security: No injection, auth, or trust-boundary concerns introduced.
  • Tests: Thorough unit-test coverage for each new stage and preset, explicit collision-avoidance assertions in TestMeaningPresetGoldens, and fuzz targets for both new presets.
  • API consistency: MeaningPreset and HygienePreset follow the same option-pattern and stage-chaining conventions as existing presets.

Mergeable.

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.

1 participant