headroom/tests/test_transforms/test_code_compressor_cjk.py

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

59 lines
2.5 KiB
Python
Raw Permalink Normal View History

fix(code-compressor): CJK-aware relevance-query symbol matching (#1747) ## Description `CodeAwareCompressor` gives a code symbol a relevance "context boost" when the query names it. The query tokenizer in `_analyze_symbol_importance` used an ASCII-only delimiter class, so a CJK query (no spaces, CJK punctuation) collapsed into one blob and never matched an ASCII symbol name; the substring fallback was also gated behind `len(name) > 3`, dropping short ASCII names glued to CJK. This extracts the query tokenization + matching into two pure helpers, adds CJK/full-width punctuation as delimiters, and relaxes the `len>3` guard only for CJK queries. ASCII/English behavior is byte-identical. `code_compressor` is pure-Python (no Rust twin, no parity fixtures). ## Type of Change - [x] Bug fix (non-breaking change that fixes an issue) ## Changes Made - `headroom/transforms/code_compressor.py`: add `_query_context_tokens` (CJK/full-width punctuation + ideographic space as delimiters) and `_symbol_in_context` (substring `len>3` guard relaxed only for CJK queries), used by `_analyze_symbol_importance`. - `tests/test_transforms/test_code_compressor_cjk.py`: pure-function tests (CJK isolation, short-name relaxation, English-unchanged, empty). ## Testing - [x] Unit tests pass (`pytest`) - [x] Linting passes (`ruff`) - [x] Type checking passes (`mypy`) - [x] New tests added for new functionality - [x] Manual testing performed (see Real Behavior Proof) ### Test Output ```text $ .venv/bin/python -m pytest tests/test_transforms/test_code_compressor_cjk.py 6 passed $ .venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py 35 passed, 39 skipped # no regression (skips need the [code] tree-sitter extra) $ ruff check / mypy headroom/transforms/code_compressor.py # clean ``` ## Real Behavior Proof - Environment: macOS (Darwin), Python in a uv venv, branch `feat/code-compressor-cjk-relevance` off `main`. - Exact command / steps: called the extracted helpers directly on CJK and ASCII queries. - Observed result: `_query_context_tokens("请重点保留(parse_config)的解析配置")` isolates `parse_config` as its own token (before: the whole query was one blob, so the exact-match boost never fired); `_symbol_in_context("db", ...)` now matches a short ASCII name glued to a CJK query (before: dropped by the `len>3` guard). English is unchanged — for `"keep the database helper"`, `_symbol_in_context("db", ...)` still returns `False` (no spurious short substring match). All 6 new tests pass; the existing 35 `code_compressor` tests are unchanged. - Not tested: end-to-end `compress()` (needs the `[code]` tree-sitter extra); the fix is at the pure query-matching layer and is verified there. ## Review Readiness - [x] I have performed a self-review - [x] This PR is ready for human review ## Checklist - [x] My code follows the project's style guidelines - [x] I have performed a self-review of my code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation — N/A (internal compressor behavior) - [x] My changes generate no new warnings - [x] I have added tests that prove my fix is effective - [x] New and existing unit tests pass locally with my changes - [ ] I have updated the CHANGELOG.md — N/A: internal relevance-scoring fix, no user-facing surface change ## Additional Notes - Scope note: a pure-CJK query that names the function only by a Chinese description (no ASCII token anywhere) still cannot match an ASCII symbol name — cross-script query matching remains out of scope.
2026-07-08 01:49:26 +08:00
"""CJK-aware relevance-query matching in the code compressor.
The symbol-importance context boost tokenized the query with an ASCII-only
delimiter class, so a CJK query (no spaces, CJK punctuation) collapsed into one
blob and never isolated/matched an ASCII symbol name the user asked to keep.
These exercise the extracted pure helpers (no tree-sitter needed).
"""
from headroom.transforms.code_compressor import (
_query_context_tokens,
_symbol_in_context,
)
def test_cjk_query_isolates_wrapped_ascii_symbol():
# full-width parens around the name must still tokenize parse_config out
words, lowered, has_cjk = _query_context_tokens("请重点保留parse_config的解析配置")
assert has_cjk
assert "parse_config" in words
assert _symbol_in_context("parse_config", words, lowered, has_cjk)
def test_cjk_query_matches_short_ascii_name_glued_to_cjk():
# 'db' (len 2) glued to CJK has no delimiter to isolate it; the len>3 guard is
# relaxed for CJK so the substring fallback still matches.
words, lowered, has_cjk = _query_context_tokens("请保留db相关的逻辑")
assert has_cjk
assert _symbol_in_context("db", words, lowered, has_cjk)
def test_english_short_name_substring_still_gated():
# ASCII query unchanged: a short name that is only a substring (not a token)
# of an English query must NOT match (avoids spurious boosts).
words, lowered, has_cjk = _query_context_tokens("keep the database helper")
assert not has_cjk
assert not _symbol_in_context("db", words, lowered, has_cjk)
def test_english_exact_token_match_unchanged():
words, lowered, has_cjk = _query_context_tokens("keep parse_config and helper")
assert not has_cjk
assert _symbol_in_context("parse_config", words, lowered, has_cjk)
assert _symbol_in_context("helper", words, lowered, has_cjk)
def test_english_long_name_substring_fallback_unchanged():
# ASCII path, len>3 substring fallback: 'parse_config' is not a standalone
# token but is a substring of 'parse_configs' -> must still match (unchanged).
words, lowered, has_cjk = _query_context_tokens("parse_configs and related helpers")
assert not has_cjk
assert "parse_config" not in words
assert _symbol_in_context("parse_config", words, lowered, has_cjk)
def test_empty_context_matches_nothing():
words, lowered, has_cjk = _query_context_tokens("")
assert words == set() and lowered == "" and has_cjk is False
assert not _symbol_in_context("foo", words, lowered, has_cjk)