fix(transforms/content-router): route grep/log output away from HTML extractor (#1719)

## Description

Follow-up to #1717 (envelope-aware detection). Even when the tool-output
envelope
is unwrapped, the native (magika) detector still tags dense `grep`/`rg`
output and
build logs as **HTML** — file paths and `</>`/brackets read as markup.
Those then
get routed to the HTML article-extractor, which is lossy for that
content (it
strips the code and identifiers the lines carry).

When the structural log/search detectors positively claim the payload,
override
the HTML verdict: build output / tracebacks → LOG (checked first),
`path:line`
grep output → SEARCH. It **reuses the existing `_try_detect_log` /
`_try_detect_search`
detectors**, so no new pattern or regex is introduced, and it only ever
reconsiders
an HTML verdict — every other detection is untouched.

Per-content and deterministic (no cross-turn state), so prefix caching
is
unaffected.

Closes #

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- `_detect_content()`: when the native detector returns `HTML`, re-check
with
`_try_detect_log` then `_try_detect_search` and return their verdict
when they
  claim the payload (`headroom/transforms/content_router.py`).
- Regression test.

## Testing

- [x] Unit tests pass
- [x] Linting passes (`ruff check`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality

### Test Output

```text
$ pytest tests/test_transforms_content_router.py tests/test_transforms_content_detection.py -q
46 passed in 0.94s

$ ruff check headroom/transforms/content_router.py tests/test_transforms_content_router.py
All checks passed!

$ mypy headroom/transforms/content_router.py
Success: no issues found in 1 source file
```

## Real Behavior Proof

- Environment: local, Python 3.12.6, native `headroom._core` detect
backend.
- With the native detector forced to `html`:
`grep`-over-`.html`-template output
detects as `SEARCH_RESULTS`, a build/error log as `BUILD_OUTPUT`, and a
genuine
  HTML article as `HTML` (override does not fire).
- Verified directly that raw magika returns `html` for realistic
`grep`-over-HTML
  output, and that this change reroutes it to `search`.
- Not tested: end-to-end proxy request replay.

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

## Additional Notes

- Builds on #1717; the two changes live in the same `_detect_content`
function
  (both prevent tool output from being misrouted to the HTML extractor).
- Pre-existing mypy findings in
`tests/test_transforms_content_router.py` are
unrelated and left as-is; `mypy headroom` is clean and the added test is
typed.
This commit is contained in:
Tejas Chopra 2026-07-02 16:22:31 -07:00 committed by GitHub
parent bec47a1898
commit 0d18ef26f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 58 additions and 1 deletions

View file

@ -58,7 +58,7 @@ from ..config import (
from ..parser import CCR_RETRIEVAL_MARKER_RE
from ..tokenizer import Tokenizer
from .base import Transform
from .content_detector import ContentType, DetectionResult
from .content_detector import ContentType, DetectionResult, _try_detect_log, _try_detect_search
from .content_detector import detect_content_type as _regex_detect_content_type
from .error_detection import content_has_strong_error_indicators
@ -302,6 +302,18 @@ def _detect_content(content: str) -> DetectionResult:
)
return _regex_detect_content_type(content)
# HTML misroute guard (native/magika path): dense punctuation in grep
# output and build logs (file paths, </>, brackets) can read as markup, so
# the native detector tags real search results / logs as HTML. Routing those
# to the HTML article-extractor is lossy — it strips code and identifiers.
# When the structural log/search detectors positively claim the payload,
# trust them over the HTML verdict: tracebacks/build output win as LOG
# (checked first), path:line grep output routes to SEARCH.
if content_type is ContentType.HTML:
override = _try_detect_log(content) or _try_detect_search(content)
if override is not None:
return override
if content_type is ContentType.PLAIN_TEXT:
regex_result = _regex_detect_content_type(content)
if regex_result.content_type is not ContentType.PLAIN_TEXT:

View file

@ -1024,3 +1024,48 @@ def test_detect_content_sees_through_tool_output_envelope() -> None:
wrapped = f"<returncode>0</returncode>\n<output>\n{code}\n</output>"
assert _detect_content(wrapped).content_type is ContentType.SOURCE_CODE
assert _detect_content(wrapped).content_type is _detect_content(code).content_type
def test_detect_content_overrides_html_misroute_for_grep_and_logs(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Regression: the native detector (magika) tags dense grep output and
build logs as HTML because file paths and </> read as markup. Routing those
to the HTML article-extractor is lossy (it strips code + identifiers). When
the structural log/search detectors positively claim the payload they
override the HTML verdict (log checked first so tracebacks win); genuine
HTML with no such structure is left as HTML."""
import headroom._core as _core
monkeypatch.setenv("HEADROOM_DETECT_BACKEND", "rust")
monkeypatch.setattr(
_core,
"detect_content_type",
lambda content: SimpleNamespace(content_type="html", confidence=1.0, metadata={}),
)
# grep over HTML template files: native says html, but it is search results.
grep = "\n".join(
f'templates/pages/dashboard_{i}.html:{10 + i}: <div class="card" data-id="{i}">'
for i in range(6)
)
assert _detect_content(grep).content_type is ContentType.SEARCH_RESULTS
# build/error log misread as html -> LOG wins (checked before search).
build_log = "\n".join(
[
"ERROR failed to compile module widget",
"WARNING deprecated call near <template>",
"Traceback (most recent call last):",
"ERROR build aborted after 2 retries",
]
)
assert _detect_content(build_log).content_type is ContentType.BUILD_OUTPUT
# genuine HTML article: no grep/log structure -> override does not fire.
html = (
"<!DOCTYPE html>\n<html><head><title>x</title></head>"
"<body><main><section><p>An article about widgets and gadgets.</p>"
"</section></main></body></html>"
)
assert _detect_content(html).content_type is ContentType.HTML