Skip to content

fix(engine): entity-name hygiene at resolution intake — whitespace normalization and tag-shaped category-label skip - #3277

Open
JoshFunnell wants to merge 2 commits into
vectorize-io:mainfrom
JoshFunnell:upstream/entity-name-hygiene
Open

fix(engine): entity-name hygiene at resolution intake — whitespace normalization and tag-shaped category-label skip#3277
JoshFunnell wants to merge 2 commits into
vectorize-io:mainfrom
JoshFunnell:upstream/entity-name-hygiene

Conversation

@JoshFunnell

Copy link
Copy Markdown

Two small data-quality fixes at the entity-resolution intake choke point, each motivated by a measured production defect.

Fixes #3275. Fixes #3276.

1. Whitespace normalization of candidate entity names

Candidate names now have internal whitespace runs (including \n, \r, \t) collapsed to a single space and ends stripped, before matching or creation. Production banks were measured (2026-08-08) storing canonical_name values with embedded newlines — extraction artifacts that shear any line-oriented consumer (psql -A, logs, exports). Case handling is deliberately untouched (the registry already matches on LOWER(canonical_name)).

Existing rows are not migrated by this PR: renormalizing stored names can collide with the (bank_id, LOWER(canonical_name)) uniqueness, so cleanup of old rows is a merge operation and left to operators/a follow-up.

2. Skip tag-shaped category labels at intake

Extraction was measured minting category labels as entities (domain:lens, domain:host, domain:memory). They behave like broad tags and measurably poison entity-based workflows: an entity-scoped retrieval selector that picked domain:lens plateaued at 61.5% precision; excluding category-shaped names, 94.0% on the same corpus.

Names matching a conservative shape — ^[a-z][a-z0-9_-]{1,15}:[a-z][a-z0-9_-]{1,24}$ after normalization, checked on a lowercased copy — are skipped with a debug log and a counter line. The shape deliberately does not match URLs (//), Windows paths, times (digits before the colon), or names containing spaces/dots.

Design note discovered in testing: configured entity labels are deliberately key:value-shaped (e.g. use:use-001), so they match the same regex. They are exempted via the existing label lookup (is_label_entity), keeping the exact-match label path intact. Consequence: an unwanted artifact that happens to collide with a configured label key survives the skip — that seemed the right trade, and it is test-pinned.

Where the fixes live, and why there

_prepare_entities_for_resolution() in hindsight-api-slim/hindsight_api/engine/retain/link_utils.py — the single choke point both resolution entry paths funnel through, and before the flat-list/entity_to_unit mapping is derived, so the resolver's positional invariant (output index-aligned with input) is untouched. Dropping names deeper inside the resolver would have broken that alignment.

Tests

hindsight-api-slim/tests/test_entity_name_hygiene.py, 27 tests, non-matching names asserted first (URLs, Windows paths, times, re: subject, plain names, configured labels). Regression across the entity suites: 208 passed; the small set of pre-existing failures on main (a Turkish-İ lowercasing case and label-extraction tests needing an API key) was verified unchanged in count and identity before/after this diff.

Production banks were measured (2026-08-08) with entity canonical_name
values containing embedded newlines -- extraction artifacts -- which
shears any line-oriented consumer (psql -A output, logs, exports).
Add _normalize_entity_name() and apply it at candidate-entity intake
in _prepare_entities_for_resolution(), collapsing all internal
whitespace runs (including \n, \r, \t) to a single space and
stripping ends. Case handling is unchanged (the registry matches on
LOWER(canonical_name) separately).

Tested by tests/test_entity_name_hygiene.py.
Measured (2026-08-08): extraction minted entities named domain:lens,
domain:host, domain:memory -- category labels, not entities. They
behave like broad tags and measurably poisoned entity-based scoping
(61.5% vs 94.0% precision on one subject). Add a conservative
tag-shape regex (_is_tag_shaped_name) and apply it in
_prepare_entities_for_resolution(), after whitespace normalization
and against a lowercased copy of the name, silently skipping matches
(logged at debug level).

Configured entity labels are deliberately "key:value" shaped
(vectorize-ioGH-1558, e.g. tag-type label values like use:use-001), so
_prepare_entities_for_resolution() now also accepts entity_labels and
exempts any name that resolves as a configured label from the skip --
without this exemption, test_intrabatch_dedup_leaves_label_values_separate
and test_entity_resolution_does_not_merge_distinct_label_values
regressed.

Tested by tests/test_entity_name_hygiene.py (attacking cases for URLs,
Windows paths, times, and configured labels come first, per the
false-positive risk).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant