headroom/tests/test_transforms
chopratejas c9aaba3f5b feat(rust): port tag_protector to Rust + 5 bug fixes (Phase 3e.4)
`headroom/transforms/tag_protector.py` was a regex-driven scan-and-
replace loop that ran on every kompress call from ContentRouter
(`content_router.py:1089`). The Python implementation had five real
bugs we now fix in the port — the most consequential being a
`str.replace(.., .., 1)` first-occurrence-replace bug that silently
collapsed two identical custom-tag blocks in the same input to a
single placeholder + a stray duplicate of the second block.

# Bug fixes (each pinned by a `fixed_in_3e4` test)

* **#1: O(n²) on nested custom tags.** Python's `while changed` loop
  restarted a full regex scan after every replacement. Rust walks
  once in linear time on input length.
* **#2: First-occurrence replace bug.** `result.replace(orig, ph, 1)`
  replaces the FIRST textual match, not the matched offset. Two
  identical custom-tag blocks collapsed to one placeholder + a stray
  duplicate of the second block. The Rust walker stitches output by
  offset so distinct blocks always get distinct placeholders.
* **#3: Silent 50-iteration cap.** Python had a hard `max_iterations
  = 50` safety limit that quietly truncated tag protection on deeply
  nested input. The Rust walker is bounded by input length only.
* **#4: Self-closing pass duplicate-replace risk.** Python ran a
  second loop with the same `replace_first` bug for self-closers.
  Rust handles self-closers in the same single pass.
* **#5: Placeholder collision.** If the input contained a literal
  `{{HEADROOM_TAG_…}}` substring, Python silently let the collision
  break restoration. Rust salts the prefix and reports it in stats.

# Architecture

Two-phase walker:
* Phase 1 (`identify_spans`): linear scan over input bytes, hand-
  rolled tag-open / tag-close lexer (no regex). Maintains a stack of
  open custom tags; on a matching close, collapses the inner span
  into a single `Span { start, end, Block }`. Self-closing custom
  tags become `Span { ..., SelfClosing }` immediately. Marker-only
  mode (`compress_tagged_content=true`) emits Open/CloseMarker spans
  instead. Orphan opens stay un-protected (matches Python behavior).
  Orphan closes are emitted verbatim and counted in stats.
* Phase 2 (`emit_output`): walks `text` once, splicing placeholders
  for span ranges and copying everything else verbatim. Offset-based,
  never `str.replace`.

PyO3 surface: `protect_tags`, `restore_tags`, `is_html_tag`,
`known_html_tag_names`. The Python shim retires the regex internals
and re-exports `KNOWN_HTML_TAGS` (rebuilt from the Rust list) +
`_is_html_tag` for backwards compat with `content_router.py` and the
existing test surface.

# Test plan

* 25 Rust unit tests including 4 `fixed_in_3e4_*` bug-fix tests
* 27 Python tests (23 existing + 4 new `fixed_in_3e4` parity tests)
* 5 integration tests in `test_tag_protection_integration.py` pass
* `make ci-precheck` clean
2026-04-29 23:18:09 -07:00
..
__init__.py Initial commit: Headroom SDK - LLM context optimization toolkit 2026-01-06 23:16:58 -08:00
test_cache_aligner.py fix: dashboard metrics, TTFB tracking, eager LLMLingua loading, and multi-provider consistency 2026-03-07 23:33:45 -08:00
test_code_compressor.py Fix CI test, bump to 0.5.7 2026-03-26 12:04:30 -07:00
test_content_router.py Fix tests: update default assertions for disabled CodeCompressor 2026-04-11 10:40:44 -07:00
test_diff_compressor.py feat(rust): retire python diff_compressor, ship rust-only via pyo3 2026-04-26 09:15:37 -07:00
test_diff_compressor_rust_parity.py feat(rust): retire python diff_compressor, ship rust-only via pyo3 2026-04-26 09:15:37 -07:00
test_html_extractor.py fix(tests): skip HTML extractor tests when trafilatura not installed 2026-01-31 15:39:55 -08:00
test_intelligent_context.py fix: Extend Anthropic format tool protection to IntelligentContextManager 2026-02-01 12:27:17 -08:00
test_kompress_compressor.py Make KompressCompressor model-configurable: model_id, chunk_words, score_threshold 2026-04-14 13:19:35 -07:00
test_progressive_summarizer.py Add quality retention eval and fix linting for Python 3.12 2026-01-19 21:52:18 -08:00
test_read_lifecycle.py fix: dashboard metrics, TTFB tracking, eager LLMLingua loading, and multi-provider consistency 2026-03-07 23:33:45 -08:00
test_rolling_window.py fix: Extend Anthropic format tool protection to IntelligentContextManager 2026-02-01 12:27:17 -08:00
test_scoring.py Add IntelligentContextManager for semantic-aware context management 2026-01-18 22:22:48 -08:00
test_smart_crusher_bugs.py feat(rust): retire python smart_crusher, ship rust-only via pyo3 2026-04-27 00:52:21 -07:00
test_smart_crusher_ccr_roundtrip.py chore(rust): SmartCrusher CCR marker injection + walker unification 2026-04-27 20:25:22 -07:00
test_smart_crusher_lossless_default.py feat(rust): SmartCrusher PR4 — lossless-first default + CCR-Dropped restoration 2026-04-27 16:30:22 -07:00
test_smart_crusher_rust_parity.py feat(rust): SmartCrusher PR4 — lossless-first default + CCR-Dropped restoration 2026-04-27 16:30:22 -07:00
test_tag_protector.py feat(rust): port tag_protector to Rust + 5 bug fixes (Phase 3e.4) 2026-04-29 23:18:09 -07:00
test_tool_crusher.py Fix all ruff lint and format errors for CI 2026-01-10 15:33:44 -08:00