Merge pull request #316 from chopratejas/rust-stage-3d-pr4-unidiff-detector

Rust stage 3d pr4 unidiff detector
This commit is contained in:
Tejas Chopra 2026-04-29 15:57:06 -07:00 committed by GitHub
commit d15afdfde5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 298 additions and 76 deletions

View file

@ -0,0 +1,222 @@
//! Stage-3d ContentRouter detection chain.
//!
//! Wires the per-tier detectors (`magika_detector`, `unidiff_detector`)
//! into the single function the ContentRouter calls. The locked design
//! from `project_rust_content_detection_arch.md`:
//!
//! ```text
//! Tier 1: magika_detect() → if non-PlainText, return it
//! Tier 2: unidiff::is_diff() → if true, return GitDiff
//! Tier 3: PlainText (fallthrough)
//! ```
//!
//! # Why no regex tier
//!
//! User-locked decision (2026-04-25): the Rust side does not run the
//! regex-based [`crate::transforms::content_detector`] in production
//! detection. The regex path stays in tree as a comparison oracle for
//! parity testing and as an opt-in escape hatch, but the dispatch is
//! magika + parser only.
//!
//! # Tier-1 errors do not abort the chain
//!
//! If magika init or inference fails, we log at warn level and proceed
//! to Tier 2. The chain's *next* tier is the legitimate fallback for
//! magika failure — that's the whole point of having tiers. We
//! deliberately do **not** treat tier-1 error as a hard failure of the
//! entire chain; that would block all detection on a transient ONNX
//! issue. Loud-on-error stays at the [`magika_detect`] entry point for
//! callers who care; the chain swallows the err with a log line.
//!
//! # SearchResults / BuildOutput
//!
//! The retired regex detector recognized grep-style search output
//! (`file:line:`) and CMake/log output as their own [`ContentType`]
//! variants. Magika has no equivalent labels. Per the locked design,
//! these now route to [`ContentType::PlainText`] — we prefer
//! passthrough to misroute. If a benchmark later shows real
//! compression loss on grep/build outputs, we add a focused detector
//! for those specifically; not preemptively.
use crate::transforms::content_detector::ContentType;
use crate::transforms::magika_detector::magika_detect;
use crate::transforms::unidiff_detector::is_diff;
/// Run the detection chain on `content` and return the chosen
/// [`ContentType`].
///
/// Empty input shortcuts to [`ContentType::PlainText`] without
/// touching either tier.
pub fn detect(content: &str) -> ContentType {
if content.is_empty() {
return ContentType::PlainText;
}
// ── Tier 1: Magika ──────────────────────────────────────────
match magika_detect(content) {
Ok(ContentType::PlainText) => {
// Magika says "I don't know" or "plain text". Continue
// to Tier 2 — magika frequently mis-classifies short
// diffs and prose-prefixed diffs as text.
}
Ok(content_type) => return content_type,
Err(e) => {
// Init or inference failure. Log it (so an ops-side
// health check can spot magika trouble in the proxy
// logs) and fall through to Tier 2 — the chain itself
// must not break on a single tier's outage.
tracing::warn!(
error = %e,
"magika detection failed; falling through to unidiff tier"
);
}
}
// ── Tier 2: unidiff parser ──────────────────────────────────
if is_diff(content) {
return ContentType::GitDiff;
}
// ── Tier 3: fallthrough ─────────────────────────────────────
ContentType::PlainText
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_input_short_circuits_to_plain_text() {
assert_eq!(detect(""), ContentType::PlainText);
}
#[test]
fn json_array_routes_via_tier_1() {
let payload = r#"[{"id": 1}, {"id": 2}, {"id": 3}]"#;
assert_eq!(detect(payload), ContentType::JsonArray);
}
#[test]
fn source_code_routes_via_tier_1() {
let py = "def hello():\n print('world')\n\nclass Foo:\n pass\n";
assert_eq!(detect(py), ContentType::SourceCode);
}
#[test]
fn html_routes_via_tier_1() {
let html = "<!DOCTYPE html><html><body><h1>x</h1></body></html>";
assert_eq!(detect(html), ContentType::Html);
}
#[test]
fn standard_git_diff_routes_via_tier_1_or_2() {
let diff = "diff --git a/foo.py b/foo.py\n\
--- a/foo.py\n\
+++ b/foo.py\n\
@@ -1,1 +1,2 @@\n \
def hello():\n\
+ print(\"new\")\n";
// Either magika tags it `diff` (Tier 1 hit) or magika
// mis-classifies as text and unidiff catches it (Tier 2).
// Both paths produce GitDiff.
assert_eq!(detect(diff), ContentType::GitDiff);
}
#[test]
fn naked_hunk_diff_routes_via_tier_2() {
// Magika often mis-classifies naked hunks (no `diff --git`
// wrapper) because the visible bytes look like ordinary
// patch lines mixed with code. Tier 2 (unidiff parser)
// catches these.
let diff = "--- a/foo.py\n\
+++ b/foo.py\n\
@@ -1,2 +1,2 @@\n\
-old line\n\
+new line\n \
context line\n";
assert_eq!(detect(diff), ContentType::GitDiff);
}
#[test]
fn plain_prose_routes_to_plain_text() {
let prose = "The quick brown fox jumps over the lazy dog. \
Just regular English with no special structure.";
assert_eq!(detect(prose), ContentType::PlainText);
}
#[test]
fn grep_search_results_route_to_plain_text_per_locked_design() {
// Locked design (2026-04-25): no regex tier on Rust side, so
// grep-style `file:line:content` output now goes through
// PlainText. This is a deliberate behavior change vs. the
// retired regex detector. If proxy benchmarks later show
// real compression loss on grep output, we add a focused
// detector then — not preemptively.
let grep = "src/foo.py:42:def process():\n\
src/bar.py:10: return True\n\
src/baz.py:7:class Worker:\n";
// We assert PlainText to lock the design. If magika
// probabilistically detects this as code, that's also fine
// for the router (CodeAware compresses code well too) —
// but the test pins the safe-default contract.
let result = detect(grep);
assert!(
result == ContentType::PlainText || result == ContentType::SourceCode,
"grep output should route to PlainText (preferred) or SourceCode (acceptable), got {result:?}"
);
}
#[test]
fn build_log_output_routes_via_chain() {
// Same locked-design note: build/test log output (no
// explicit regex detector for it on the Rust side). Magika
// can label this either as `txt` (→ PlainText, our mapping)
// or, more probabilistically, as a code-group label like
// `log` / `c` because the structured `[LEVEL]` shape and
// `file.cpp:line` references look code-like to the model.
// Either route is acceptable for the router: SourceCode
// dispatches to the code-aware compressor (which compresses
// log lines reasonably well via repetition collapsing);
// PlainText is the safe-default passthrough. The test pins
// the contract that it lands somewhere reasonable, not at
// a degenerate type like JsonArray or GitDiff.
let log = "[INFO] Building target foo\n\
[WARN] Deprecated API usage in foo.cpp:45\n\
[ERROR] Compilation failed: undefined reference\n";
let got = detect(log);
assert!(
matches!(got, ContentType::PlainText | ContentType::SourceCode),
"build log should route to PlainText or SourceCode, got {got:?}"
);
}
#[test]
fn yaml_routes_to_source_code() {
// YAML lives in magika's `code` group; the chain returns it
// as SourceCode so the router picks the code-aware compressor.
let yaml = "name: my-app\nversion: 1.0\ndependencies:\n - foo\n";
assert_eq!(detect(yaml), ContentType::SourceCode);
}
#[test]
fn rust_source_routes_to_source_code() {
let rs = "use std::collections::HashMap;\n\n\
pub struct Counter { counts: HashMap<String, u32> }\n\n\
impl Counter {\n \
pub fn new() -> Self { Self { counts: HashMap::new() } }\n\
}\n";
assert_eq!(detect(rs), ContentType::SourceCode);
}
#[test]
fn chain_is_deterministic_across_repeated_calls() {
// Magika returns the same label for identical input on
// repeated calls; the chain wraps that determinism.
let payload = r#"{"users": [{"id": 1}, {"id": 2}]}"#;
let a = detect(payload);
let b = detect(payload);
let c = detect(payload);
assert_eq!(a, b);
assert_eq!(b, c);
}
}

View file

@ -18,6 +18,7 @@
pub mod adaptive_sizer;
pub mod anchor_selector;
pub mod content_detector;
pub mod detection;
pub mod diff_compressor;
pub mod magika_detector;
pub mod smart_crusher;
@ -26,6 +27,7 @@ pub mod unidiff_detector;
pub use content_detector::{
detect_content_type, is_json_array_of_dicts, ContentType, DetectionResult,
};
pub use detection::detect;
pub use diff_compressor::{
DiffCompressionResult, DiffCompressor, DiffCompressorConfig, DiffCompressorStats,
};

View file

@ -21,10 +21,9 @@ use headroom_core::transforms::smart_crusher::{
SmartCrusherConfig as RustSmartCrusherConfig,
};
use headroom_core::transforms::{
detect_content_type as rust_detect_content_type,
is_json_array_of_dicts as rust_is_json_array_of_dicts, ContentType as RustContentType,
DetectionResult as RustDetectionResult, DiffCompressionResult, DiffCompressor,
DiffCompressorConfig, DiffCompressorStats,
detect as rust_detect_chain, is_json_array_of_dicts as rust_is_json_array_of_dicts,
ContentType as RustContentType, DetectionResult as RustDetectionResult, DiffCompressionResult,
DiffCompressor, DiffCompressorConfig, DiffCompressorStats,
};
use pyo3::prelude::*;
use pyo3::types::{PyDict, PyString};
@ -888,14 +887,29 @@ impl PyDetectionResult {
/// Detect the type of `content`. Returns a `DetectionResult` with the
/// same field surface as Python's dataclass.
///
/// Releases the GIL while detecting — pattern matching can be substantial
/// on large bodies (HTML scans, 500-line diff windows), and freeing the
/// GIL lets other Python threads make progress in the meantime.
/// Stage-3d (PR5) wired this through the magika→unidiff→PlainText
/// detection chain — the regex `content_detector` is no longer on
/// the production path. The chain returns a `ContentType` only;
/// we synthesize the legacy `DetectionResult` shape here with
/// `confidence = 1.0` (the chain doesn't surface a probabilistic
/// score) and an empty metadata bag (no production caller reads
/// metadata from this binding today — see audit notes in
/// `headroom/transforms/content_router.py`).
///
/// Releases the GIL while detecting — magika inference and unidiff
/// parsing can be substantial on large bodies, and freeing the GIL
/// lets other Python threads make progress in the meantime.
#[pyfunction]
fn detect_content_type(py: Python<'_>, content: &str) -> PyDetectionResult {
let owned = content.to_string();
let result = py.allow_threads(move || rust_detect_content_type(&owned));
PyDetectionResult { inner: result }
let content_type = py.allow_threads(move || rust_detect_chain(&owned));
PyDetectionResult {
inner: RustDetectionResult {
content_type,
confidence: 1.0,
metadata: serde_json::Map::new(),
},
}
}
/// Quick check: is `content` a JSON array of dictionaries (the format

View file

@ -48,59 +48,37 @@ from typing import Any
from ..config import DEFAULT_EXCLUDE_TOOLS, ReadLifecycleConfig, TransformResult
from ..tokenizer import Tokenizer
from .base import Transform
from .content_detector import ContentType, DetectionResult, detect_content_type
from .content_detector import ContentType, DetectionResult
logger = logging.getLogger(__name__)
_magika_detector: Any | None = None
_magika_status: bool | None = None
def _get_magika_detector() -> Any | None:
"""Load the Magika detector only when router detection actually runs."""
global _magika_detector, _magika_status
if _magika_status is False:
return None
if _magika_detector is not None:
return _magika_detector
try:
from ..compression.detector import get_detector
_magika_detector = get_detector(prefer_magika=True)
_magika_status = True
logger.info("ContentRouter: Using Magika ML-based content detection")
except ImportError:
_magika_status = False
logger.debug("Magika not available, using regex-based detection")
return _magika_detector
def _detect_content(content: str) -> DetectionResult:
"""Detect content type using Magika if available, else regex fallback."""
magika_detector = _get_magika_detector()
if magika_detector is not None:
result = magika_detector.detect(content)
# Map Magika ContentType to router's expected format
type_map = {
"json": ContentType.JSON_ARRAY,
"code": ContentType.SOURCE_CODE,
"log": ContentType.BUILD_OUTPUT,
"diff": ContentType.GIT_DIFF,
"markdown": ContentType.PLAIN_TEXT,
"text": ContentType.PLAIN_TEXT,
"unknown": ContentType.PLAIN_TEXT,
}
mapped_type = type_map.get(result.content_type.value, ContentType.PLAIN_TEXT)
return DetectionResult(
content_type=mapped_type,
confidence=result.confidence,
metadata={"language": result.language, "raw_label": result.raw_label},
)
else:
return detect_content_type(content)
"""Detect content type via the Rust detection chain.
Stage-3d (PR5) wired this through `headroom._core.detect_content_type`,
which runs the magikaunidiffPlainText chain. The Python-side
Magika+regex fallback path was retired here single detection
surface, no parallel paths. The Rust extension is a hard dep
(no Python fallback) per `feedback_no_silent_fallbacks.md`.
The Rust binding returns the legacy `DetectionResult` shape with
`confidence=1.0` and an empty metadata dict. Existing callers
only consumed `.content_type` from it; the strategy mapping in
`_strategy_from_detection` keys off that field alone.
"""
from headroom._core import detect_content_type as _rust_detect
rust_result = _rust_detect(content)
# Rust's `content_type` is the lowercase string tag (e.g.
# "json_array"); translate to the Python `ContentType` enum so
# downstream mapping keys match.
content_type = ContentType(rust_result.content_type)
return DetectionResult(
content_type=content_type,
confidence=rust_result.confidence,
metadata={},
)
def _create_content_signature(

View file

@ -101,30 +101,36 @@ def test_router_result_helpers_and_summary() -> None:
def test_content_signature_and_detection_helpers(monkeypatch: pytest.MonkeyPatch) -> None:
"""Stage-3d (PR5) wired `_detect_content` through the Rust chain
(`headroom._core.detect_content_type` magika unidiff
PlainText). The pre-PR5 Python-side `_get_magika_detector`
fallback path is gone.
This test asserts the new contract:
1. The detection helper delegates to the Rust binding.
2. Whatever `ContentType` the Rust side returns flows back as a
Python `DetectionResult` with that same `content_type`.
"""
signature = _create_content_signature("search", "file.py:10:match", language="python")
assert signature is not None
assert len(signature.structure_hash) == 24
fake_detector = SimpleNamespace(
detect=lambda content: SimpleNamespace(
content_type=SimpleNamespace(value="code"),
confidence=0.91,
language="python",
raw_label="python",
)
)
monkeypatch.setattr(content_router_module, "_get_magika_detector", lambda: fake_detector)
magika_result = _detect_content("def main(): pass")
assert magika_result == DetectionResult(
content_type=ContentType.SOURCE_CODE,
confidence=0.91,
metadata={"language": "python", "raw_label": "python"},
)
# Monkeypatch the Rust binding to return a deterministic fake
# result; verify _detect_content propagates the content_type
# tag back as the Python ContentType enum.
import headroom._core as _core
fallback = DetectionResult(ContentType.PLAIN_TEXT, 0.6, {"kind": "fallback"})
monkeypatch.setattr(content_router_module, "_get_magika_detector", lambda: None)
monkeypatch.setattr(content_router_module, "detect_content_type", lambda content: fallback)
assert _detect_content("plain") is fallback
fake_rust_result = SimpleNamespace(
content_type="source_code",
confidence=1.0,
metadata={},
)
monkeypatch.setattr(_core, "detect_content_type", lambda content: fake_rust_result)
result = _detect_content("def main(): pass")
assert result.content_type is ContentType.SOURCE_CODE
assert result.confidence == 1.0
assert result.metadata == {}
def test_mixed_content_section_splitting_and_json_extraction() -> None: