Context
The scanner currently uses shared input hints only for the empty-input fast path and then calls the filter with input.text, causing the filter to prepare and normalize the input again. Composed profiles also evaluate the original source independently, so profiles that share a normalization strategy can repeat equivalent preparation.
Filter construction is intentionally benchmarked separately because creating a filter for every message can cost more than the lookup itself. Consumers should reuse the shared filter or a factory-created filter instead of recompiling dictionaries in the message path.
Proposed Implementation
- Define an internal prepared-input capability that can carry same-length normalized views, scan facts, and safe scanner hints through one top-level scan.
- Reuse a normalized view across child profiles only when they declare the same normalization strategy.
- Let the package scanner consume compatible prepared data without rebuilding code-point or normalization facts.
- Keep a conservative fallback for custom filters or callers that do not support the capability.
- Treat hints as optional optimization evidence; missing or incomplete hints must never change matching behavior.
- Coordinate any shared public input-contract change with
@textfilters/core in a separate reviewed change rather than leaking package internals.
- Expand usage documentation and benchmark guidance to state that filter instances should be constructed once and reused.
Acceptance Criteria
- A composed scan prepares at most one normalized view per distinct normalization strategy.
- Scanner
check() and sink-based scan() reuse compatible prepared data instead of silently repeating full input preparation.
- Shared and factory-created filters keep existing isolation and mutation semantics.
- Custom filters without the internal capability remain fully compatible through the fallback.
check(), analyze(), censor(), scanner ranges, streaming order, profile provenance, taxonomy metadata, and UTF-16 offsets remain compatible.
- Tests cover same-strategy profiles, mixed-strategy profiles, missing hints, partial hints, astral input, custom filter fallback, and early stop.
- Benchmarks compare reused versus repeated preparation for single-profile and composed-profile long-clean, match, analysis, censor, and scanner cases.
- Filter-construction cost remains visible, and documentation explicitly warns against per-message filter creation.
Non-Goals
- Do not add a global or cross-message normalized-text cache.
- Do not trust caller hints as correctness guarantees.
- Do not force all language profiles to use the same normalization strategy.
- Do not expose compiled dictionary or matcher internals.
- Do not change another package's runtime contract from this repository without a separately reviewed dependency.
Dependencies
Use the prepared representation from #170. Coordinate candidate facts with #168 and open a focused @textfilters/core dependency only if a shared scanner contract change is required.
Context
The scanner currently uses shared input hints only for the empty-input fast path and then calls the filter with
input.text, causing the filter to prepare and normalize the input again. Composed profiles also evaluate the original source independently, so profiles that share a normalization strategy can repeat equivalent preparation.Filter construction is intentionally benchmarked separately because creating a filter for every message can cost more than the lookup itself. Consumers should reuse the shared filter or a factory-created filter instead of recompiling dictionaries in the message path.
Proposed Implementation
@textfilters/corein a separate reviewed change rather than leaking package internals.Acceptance Criteria
check()and sink-basedscan()reuse compatible prepared data instead of silently repeating full input preparation.check(),analyze(),censor(), scanner ranges, streaming order, profile provenance, taxonomy metadata, and UTF-16 offsets remain compatible.Non-Goals
Dependencies
Use the prepared representation from #170. Coordinate candidate facts with #168 and open a focused
@textfilters/coredependency only if a shared scanner contract change is required.