Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
"""Tests for Code-Aware Compressor using tree-sitter AST parsing.
|
|
|
|
|
|
|
|
|
|
Comprehensive tests covering:
|
|
|
|
|
- CodeCompressorConfig: Configuration validation and defaults
|
|
|
|
|
- CodeAwareCompressor: Core AST-based compression functionality
|
|
|
|
|
- Language detection: Auto-detection from extensions and content
|
|
|
|
|
- Transform interface: apply(), should_apply() methods
|
|
|
|
|
- Syntax preservation: Guarantees valid output syntax
|
|
|
|
|
- Edge cases: Empty content, unavailable dependency, fallbacks
|
|
|
|
|
"""
|
|
|
|
|
|
fix(code): validate Python compressed syntax (#1302)
## Description
Fix a Python code-compression validity gap from #1233 where tree-sitter
parsing could mark compressed output as syntactically valid even when
Python compile-time syntax rules reject it.
This keeps `from __future__ import ...` statements in the
import-preservation bucket so they stay before executable definitions,
and adds Python `compile(..., "exec")` verification after `ast.parse`.
It also keeps the earlier conservative class-method decorator
indentation hardening from this branch.
Refs #1233.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Treat Python `future_import_statement` nodes as preserved imports.
- Verify Python compressed output with both `ast.parse` and
`compile(..., "exec")`.
- Preserve original source-line indentation for decorators attached to
class methods.
- Add a regression fixture covering `from __future__ import
annotations`, class decorators, property decorators, async methods, and
`match` statements.
- Add a direct regression assertion that future imports stay before
executable definitions.
- Document the user-visible fix in `CHANGELOG.md`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ /tmp/headroom-issue-1233-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py::TestTreeSitterIntegration::test_python_future_import_stays_at_module_start -q
1 passed, 1 warning
$ /tmp/headroom-issue-1233-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py -q
61 passed, 1 warning
$ uv run --extra dev ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
All checks passed!
$ uv run --extra dev ruff format --check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
2 files already formatted
$ git diff --check
# no output
```
## Real Behavior Proof
- Environment: macOS, Python 3.11.14, local checkout with `[code]`
dependencies installed in `/tmp/headroom-issue-1233-venv`.
- Exact command / steps: added
`test_python_future_import_stays_at_module_start`, ran it before the fix
to confirm the compressed output failure, then reran the focused test
and full `tests/test_transforms/test_code_compressor.py` after the
patch.
- Observed result: before this patch, the regression fixture produced
compressed Python with `from __future__ import annotations` after
class/function definitions. `result.syntax_valid` was `True`, but
`compile(result.compressed, "<test>", "exec")` failed with `SyntaxError:
from __future__ imports must occur at the beginning of the file`. After
this patch, the focused regression and full code-compressor test file
pass locally, and the regression now directly asserts that the future
import appears before executable definitions.
- Not tested: full repository pytest, `mypy headroom`, and a broad
corpus run over third-party source files.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
This PR is now scoped to the stable compile-time failure path in #1233.
The broader syntax-failure rate from the issue may still need
corpus-level follow-up.
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-06-24 03:41:14 +08:00
|
|
|
import textwrap
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
from unittest.mock import patch
|
|
|
|
|
|
|
|
|
|
import pytest
|
|
|
|
|
|
fix(code-compressor): recover valid Python rewrites after local syntax rejection (#2202)
## Description
A compile-invalid Python definition rewrite currently makes
`CodeAwareCompressor` discard every otherwise valid rewrite in the file
and return the original source at 0 percent reduction. The existing
whole-file safety guard stays in place, while a Python-only recovery
replay now preserves the rejected definition and keeps independent valid
compression.
The recovery reuses the current Python validation authority in
`ast.parse()` plus `compile()`, runs only after the first assembled
module already fails `_verify_syntax()`, and stays out of non-Python
paths.
Closes #1233
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added a Python-only recovery replay after the first assembled module
fails syntax validation.
- Preserved only the invalid function or class rewrite while allowing
independent valid definitions to remain compressed.
- Kept the existing whole-file syntax guard and original-source fallback
as the terminal safety check.
- Added focused invalid-node, valid-modern-syntax, and fail-safe
coverage.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid
tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python
-v`)
- [x] Linting passes (`uv run ruff check
headroom/transforms/code_compressor.py
tests/test_transforms/test_code_compressor.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v
5 passed in 0.34s
uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
All checks passed!
uv run ruff format headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py --check
2 files already formatted
```
## Real Behavior Proof
- Environment: Windows, synced `uv` environment with `dev` and `code`
extras installed
- Exact command / steps: run the focused invalid-node regression through
public `compress(..., language="python")`
- Observed result: `1 passed in 0.19s`; the invalid candidate stays
original, the neighboring valid candidate remains compressed, and
`headroom-PR-TARGET-1233-PROOF.md` records the base `ratio=1.0`
whole-file rollback against the fixed head behavior.
- Not tested: the stale future-import mismatch discussed in the old
issue comment, already covered on current main
## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- The stale future-import comment on #1233 is not the live slice here;
current main already validates Python with `compile()` and already
covers that ordering case.
- This fix keeps the existing whole-file fail-safe and does not broaden
into cross-language recovery or new syntax models.
- `CHANGELOG.md` remains unchanged because Headroom generates release
notes from conventional commits.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 23:19:46 -04:00
|
|
|
import headroom.transforms.code_compressor as cc
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
from headroom.transforms.code_compressor import (
|
|
|
|
|
CodeAwareCompressor,
|
|
|
|
|
CodeCompressionResult,
|
|
|
|
|
CodeCompressorConfig,
|
|
|
|
|
CodeLanguage,
|
|
|
|
|
DocstringMode,
|
feat(code): add PHP support to CodeAwareCompressor (#2423)
## Description
Adds PHP to `CodeAwareCompressor`, fixing #201. PHP was already
*detected* as code (Magika labels in `headroom/compression/detector.py`
include `php`, and the Rust `magika_detector.rs` lists it too) but there
was no PHP `LangConfig`, so PHP content silently passed through
uncompressed. This wires PHP through the tree-sitter compression path
following the C# pattern (the most recently added, fully functional
language — deliberately not the quarantined Perl path).
A secondary detection bug is fixed along the way: PHP's `$variables`
match Perl's prefilter regex, and the existing Perl-dominance guard in
`detect_language` returned `UNKNOWN` for PHP files. An explicit `<?php`
open tag — which no Perl source contains — now drops Perl from the
candidate set before that guard runs.
## Type of Change
- [ ] Bug fix
- [x] New feature
- [ ] Documentation update
- [ ] Refactor
- [ ] Other
## Changes Made
- `headroom/transforms/code_compressor.py`: `CodeLanguage.PHP` +
`phtml`/`php5`/`php7`/`php8` aliases; PHP `LangConfig` built from the
actual tree-sitter-php grammar (node names verified by parsing samples):
`namespace_use_declaration` imports,
`function_definition`/`method_declaration` functions,
`class_declaration`/`interface_declaration`/`trait_declaration` classes,
`enum_declaration` types, `declaration_list` class bodies,
`compound_statement` function bodies. `namespace_definition` maps to
`package_node` so statement-scoped `namespace App;` hoists ahead of the
`use` imports (required PHP ordering); the rare block-scoped `namespace
A { }` form takes the same path and is preserved verbatim — valid
output, just no compression inside the block. PHP prefilter regexes
added; supported-languages error message updated; `<?php`-tag Perl
disambiguation in `detect_language`.
- `headroom/transforms/content_detector.py`: `php` entry in
`_CODE_PATTERNS` so raw PHP classifies as `SOURCE_CODE` and reaches the
code-aware route.
- `tests/test_transforms/test_code_compressor.py`: new `TestPhpSupport`
mirroring `TestCSharpSupport` — signatures preserved / bodies elided,
`<?php` → `namespace` → `use` → declarations ordering, auto-detection
despite the Perl sigil overlap, alias coercion, malformed passthrough.
- `tests/test_code_compressor_language_alias.py`: `php` in the canonical
list, `phtml` in the alias table.
- `docs/content/docs/code-compression.mdx`: PHP added to the Tier 2
supported-languages row.
No new dependency: `tree-sitter-language-pack` (the existing `[code]`
extra) already ships the PHP grammar. No Rust changes needed.
## Testing
- [x] New unit tests added and passing
- [x] Full affected test suites pass locally
**Test Output**
```
$ python -m pytest tests/test_transforms/test_code_compressor.py tests/test_code_compressor_language_alias.py -q
============================= 120 passed in 7.81s =============================
$ python -m pytest tests/test_transforms/ -q
3 failed, 443 passed # the 3 failures (kompress ONNX thread caps, kompress size gate,
# text_crusher unicode parity) reproduce identically on a clean
# upstream/main checkout in this environment — pre-existing local
# ONNX runtime quirks, unrelated to this change
$ ruff check . (0.15.17, CI-pinned) → All checks passed! | ruff format --check → clean
$ mypy headroom/transforms/code_compressor.py headroom/transforms/content_detector.py --ignore-missing-imports
Success: no issues found in 2 source files
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.13, tree-sitter +
tree-sitter-language-pack (<1.0) installed, branch
`feat/201-php-code-compression` off `upstream/main`.
- Exact command / steps: parsed PHP samples (namespaced class w/
methods, block-scoped namespace, mixed HTML+PHP) with
`tree_sitter_language_pack.get_parser('php')` to verify every node name
used in the config; then ran `CodeAwareCompressor().compress(php_code,
language="php")` and `compress(php_code)` (auto-detection) on a 48-line
realistic service class.
- Observed result: explicit and auto-detected paths both return
`language=CodeLanguage.PHP`, `compression_ratio=0.64`,
`syntax_valid=True`; method bodies elided to `// [N lines omitted]`
while `<?php`, `namespace`, `use` lines, class header, and all
signatures are preserved verbatim in the original order. Before the
detection fix, auto-detection returned `UNKNOWN` (Perl prefilter
dominance) — reproduced and then verified fixed.
- Not tested: exotic PHP shapes (heredoc-heavy code, attributes `#[...]`
on methods, interleaved multi-`<?php ?>` HTML templates beyond the basic
mixed case); these fall back to verbatim preservation via the
uncaptured-node pass or malformed-passthrough, both of which are covered
by tests for the simple cases.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-01 00:54:13 +02:00
|
|
|
coerce_language,
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
detect_language,
|
|
|
|
|
is_tree_sitter_available,
|
|
|
|
|
is_tree_sitter_loaded,
|
|
|
|
|
unload_tree_sitter,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
# Try to import for availability check
|
|
|
|
|
try:
|
|
|
|
|
import tree_sitter_language_pack # noqa: F401
|
|
|
|
|
|
|
|
|
|
TREE_SITTER_INSTALLED = True
|
|
|
|
|
except ImportError:
|
|
|
|
|
TREE_SITTER_INSTALLED = False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Test Fixtures
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def default_config():
|
|
|
|
|
"""Default CodeCompressorConfig for testing."""
|
|
|
|
|
return CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10, # Low threshold for tests
|
|
|
|
|
enable_ccr=False, # Disable CCR for unit tests
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def compressor(default_config):
|
|
|
|
|
"""CodeAwareCompressor instance with default config."""
|
|
|
|
|
return CodeAwareCompressor(default_config)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture
|
|
|
|
|
def tokenizer():
|
|
|
|
|
"""Get a tokenizer for Transform interface tests."""
|
|
|
|
|
from headroom.providers import OpenAIProvider
|
|
|
|
|
from headroom.tokenizer import Tokenizer
|
|
|
|
|
|
|
|
|
|
provider = OpenAIProvider()
|
|
|
|
|
token_counter = provider.get_token_counter("gpt-4o")
|
|
|
|
|
return Tokenizer(token_counter, "gpt-4o")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Test Data Generators
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_python_code(n_functions: int = 5, n_classes: int = 1) -> str:
|
|
|
|
|
"""Generate Python code for testing."""
|
|
|
|
|
lines = [
|
|
|
|
|
'"""Module with classes and functions."""',
|
|
|
|
|
"",
|
|
|
|
|
"import os",
|
|
|
|
|
"import sys",
|
|
|
|
|
"from typing import Any, Optional, List",
|
|
|
|
|
"from dataclasses import dataclass",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for c in range(n_classes):
|
|
|
|
|
lines.extend(
|
|
|
|
|
[
|
|
|
|
|
"@dataclass",
|
|
|
|
|
f"class TestClass{c}:",
|
|
|
|
|
' """A test class with docstring."""',
|
|
|
|
|
" name: str",
|
|
|
|
|
" value: int = 0",
|
|
|
|
|
"",
|
|
|
|
|
" def method(self, arg: Any) -> str:",
|
|
|
|
|
' """Process the argument."""',
|
|
|
|
|
" result = str(arg)",
|
|
|
|
|
" for i in range(10):",
|
|
|
|
|
' result += f"iteration {i}"',
|
|
|
|
|
" return result",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
for i in range(n_functions):
|
|
|
|
|
lines.extend(
|
|
|
|
|
[
|
|
|
|
|
f"def function_{i}(arg: Any, optional: Optional[str] = None) -> str:",
|
|
|
|
|
f' """Process argument {i}.',
|
|
|
|
|
"",
|
|
|
|
|
" This is a longer docstring with multiple lines.",
|
|
|
|
|
" It explains what the function does in detail.",
|
|
|
|
|
"",
|
|
|
|
|
" Args:",
|
|
|
|
|
" arg: The argument to process.",
|
|
|
|
|
" optional: An optional parameter.",
|
|
|
|
|
"",
|
|
|
|
|
" Returns:",
|
|
|
|
|
" A string result.",
|
|
|
|
|
' """',
|
|
|
|
|
" result = str(arg)",
|
|
|
|
|
" if optional:",
|
|
|
|
|
" result += optional",
|
|
|
|
|
" for i in range(10):",
|
|
|
|
|
' result += f"iteration {i}"',
|
|
|
|
|
" try:",
|
|
|
|
|
" int(result)",
|
|
|
|
|
" except ValueError:",
|
|
|
|
|
' result = "0"',
|
|
|
|
|
" return result",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_javascript_code(n_functions: int = 5) -> str:
|
|
|
|
|
"""Generate JavaScript code for testing."""
|
|
|
|
|
lines = [
|
|
|
|
|
"// Module with various functions",
|
|
|
|
|
'import { something } from "module";',
|
|
|
|
|
'const config = require("./config");',
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for i in range(n_functions):
|
|
|
|
|
lines.extend(
|
|
|
|
|
[
|
|
|
|
|
"/**",
|
|
|
|
|
f" * Process function {i}",
|
|
|
|
|
" * @param {any} arg - The argument",
|
|
|
|
|
" * @returns {string} The result",
|
|
|
|
|
" */",
|
|
|
|
|
f"function processFunction{i}(arg) {{",
|
|
|
|
|
" let result = String(arg);",
|
|
|
|
|
" for (let j = 0; j < 10; j++) {",
|
|
|
|
|
" result += `iteration ${j}`;",
|
|
|
|
|
" }",
|
|
|
|
|
" try {",
|
|
|
|
|
" JSON.parse(result);",
|
|
|
|
|
" } catch (e) {",
|
|
|
|
|
" console.error(e);",
|
|
|
|
|
" }",
|
|
|
|
|
" return result;",
|
|
|
|
|
"}",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
lines.append("export { processFunction0 };")
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_go_code(n_functions: int = 3) -> str:
|
|
|
|
|
"""Generate Go code for testing."""
|
|
|
|
|
lines = [
|
|
|
|
|
"package main",
|
|
|
|
|
"",
|
|
|
|
|
'import "fmt"',
|
|
|
|
|
"",
|
|
|
|
|
"// Config holds configuration",
|
|
|
|
|
"type Config struct {",
|
|
|
|
|
" Name string",
|
|
|
|
|
" Value int",
|
|
|
|
|
"}",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
for i in range(n_functions):
|
|
|
|
|
lines.extend(
|
|
|
|
|
[
|
|
|
|
|
f"// Process{i} processes the input",
|
|
|
|
|
f"func Process{i}(input string) (string, error) {{",
|
|
|
|
|
" result := input",
|
|
|
|
|
" for i := 0; i < 10; i++ {",
|
|
|
|
|
' result = fmt.Sprintf("%s-%d", result, i)',
|
|
|
|
|
" }",
|
|
|
|
|
" if len(result) == 0 {",
|
|
|
|
|
' return "", fmt.Errorf("empty result")',
|
|
|
|
|
" }",
|
|
|
|
|
" return result, nil",
|
|
|
|
|
"}",
|
|
|
|
|
"",
|
|
|
|
|
]
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestCodeCompressorConfig
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCodeCompressorConfig:
|
|
|
|
|
"""Tests for CodeCompressorConfig dataclass."""
|
|
|
|
|
|
|
|
|
|
def test_default_values(self):
|
|
|
|
|
"""Default config values are sensible."""
|
|
|
|
|
config = CodeCompressorConfig()
|
|
|
|
|
|
|
|
|
|
assert config.preserve_imports is True
|
|
|
|
|
assert config.preserve_signatures is True
|
|
|
|
|
assert config.preserve_type_annotations is True
|
|
|
|
|
assert config.preserve_decorators is True
|
|
|
|
|
assert config.docstring_mode == DocstringMode.FIRST_LINE
|
|
|
|
|
assert config.target_compression_rate == 0.2
|
|
|
|
|
assert config.max_body_lines == 5
|
|
|
|
|
assert config.min_tokens_for_compression == 100
|
|
|
|
|
assert config.enable_ccr is True
|
|
|
|
|
|
|
|
|
|
def test_custom_values(self):
|
|
|
|
|
"""Custom config values are applied."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
preserve_imports=False,
|
|
|
|
|
preserve_signatures=True,
|
|
|
|
|
docstring_mode=DocstringMode.FULL,
|
|
|
|
|
target_compression_rate=0.3,
|
|
|
|
|
max_body_lines=10,
|
|
|
|
|
min_tokens_for_compression=50,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert config.preserve_imports is False
|
|
|
|
|
assert config.preserve_signatures is True
|
|
|
|
|
assert config.docstring_mode == DocstringMode.FULL
|
|
|
|
|
assert config.target_compression_rate == 0.3
|
|
|
|
|
assert config.max_body_lines == 10
|
|
|
|
|
assert config.min_tokens_for_compression == 50
|
|
|
|
|
|
|
|
|
|
def test_docstring_modes(self):
|
|
|
|
|
"""All docstring modes are valid."""
|
|
|
|
|
for mode in DocstringMode:
|
|
|
|
|
config = CodeCompressorConfig(docstring_mode=mode)
|
|
|
|
|
assert config.docstring_mode == mode
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestCodeCompressionResult
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCodeCompressionResult:
|
|
|
|
|
"""Tests for CodeCompressionResult dataclass."""
|
|
|
|
|
|
|
|
|
|
def test_tokens_saved(self):
|
|
|
|
|
"""tokens_saved property calculates correctly."""
|
|
|
|
|
result = CodeCompressionResult(
|
|
|
|
|
compressed="short",
|
|
|
|
|
original="long content here",
|
|
|
|
|
original_tokens=100,
|
|
|
|
|
compressed_tokens=30,
|
|
|
|
|
compression_ratio=0.3,
|
|
|
|
|
language=CodeLanguage.PYTHON,
|
|
|
|
|
syntax_valid=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result.tokens_saved == 70
|
|
|
|
|
|
|
|
|
|
def test_tokens_saved_no_negative(self):
|
|
|
|
|
"""tokens_saved never returns negative."""
|
|
|
|
|
result = CodeCompressionResult(
|
|
|
|
|
compressed="expanded",
|
|
|
|
|
original="short",
|
|
|
|
|
original_tokens=10,
|
|
|
|
|
compressed_tokens=20,
|
|
|
|
|
compression_ratio=2.0,
|
|
|
|
|
language=CodeLanguage.PYTHON,
|
|
|
|
|
syntax_valid=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result.tokens_saved == 0
|
|
|
|
|
|
|
|
|
|
def test_savings_percentage(self):
|
|
|
|
|
"""savings_percentage property calculates correctly."""
|
|
|
|
|
result = CodeCompressionResult(
|
|
|
|
|
compressed="short",
|
|
|
|
|
original="long content",
|
|
|
|
|
original_tokens=100,
|
|
|
|
|
compressed_tokens=25,
|
|
|
|
|
compression_ratio=0.25,
|
|
|
|
|
language=CodeLanguage.PYTHON,
|
|
|
|
|
syntax_valid=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result.savings_percentage == 75.0
|
|
|
|
|
|
|
|
|
|
def test_savings_percentage_zero_original(self):
|
|
|
|
|
"""savings_percentage handles zero original tokens."""
|
|
|
|
|
result = CodeCompressionResult(
|
|
|
|
|
compressed="",
|
|
|
|
|
original="",
|
|
|
|
|
original_tokens=0,
|
|
|
|
|
compressed_tokens=0,
|
|
|
|
|
compression_ratio=1.0,
|
|
|
|
|
language=CodeLanguage.UNKNOWN,
|
|
|
|
|
syntax_valid=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
assert result.savings_percentage == 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestCodeLanguage
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCodeLanguage:
|
|
|
|
|
"""Tests for CodeLanguage enum and detection."""
|
|
|
|
|
|
|
|
|
|
def test_all_language_values_are_unique(self):
|
|
|
|
|
"""All language enum values are unique."""
|
|
|
|
|
values = [lang.value for lang in CodeLanguage]
|
|
|
|
|
assert len(values) == len(set(values))
|
|
|
|
|
|
|
|
|
|
def test_detect_python_language(self):
|
|
|
|
|
"""Python language is detected from code patterns."""
|
|
|
|
|
|
|
|
|
|
code = """
|
|
|
|
|
import os
|
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
|
|
def function(arg: str) -> str:
|
|
|
|
|
return arg
|
|
|
|
|
|
|
|
|
|
class MyClass:
|
|
|
|
|
pass
|
|
|
|
|
"""
|
|
|
|
|
lang, confidence = detect_language(code)
|
|
|
|
|
assert lang == CodeLanguage.PYTHON
|
|
|
|
|
assert confidence > 0.5
|
|
|
|
|
|
|
|
|
|
def test_detect_javascript_language(self):
|
|
|
|
|
"""JavaScript language is detected from code patterns."""
|
|
|
|
|
|
|
|
|
|
code = """
|
|
|
|
|
const express = require('express');
|
|
|
|
|
import { something } from 'module';
|
|
|
|
|
|
|
|
|
|
function handler(req, res) {
|
|
|
|
|
return res.json({ status: 'ok' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default handler;
|
|
|
|
|
"""
|
|
|
|
|
lang, confidence = detect_language(code)
|
|
|
|
|
assert lang in (CodeLanguage.JAVASCRIPT, CodeLanguage.TYPESCRIPT)
|
|
|
|
|
assert confidence > 0.3
|
|
|
|
|
|
|
|
|
|
def test_detect_go_language(self):
|
|
|
|
|
"""Go language is detected from code patterns."""
|
|
|
|
|
|
|
|
|
|
code = """
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import "fmt"
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
fmt.Println("Hello")
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
lang, confidence = detect_language(code)
|
|
|
|
|
assert lang == CodeLanguage.GO
|
|
|
|
|
assert confidence > 0.3
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestCodeAwareCompressor
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestCodeAwareCompressor:
|
|
|
|
|
"""Tests for CodeAwareCompressor core functionality."""
|
|
|
|
|
|
|
|
|
|
def test_init_with_default_config(self):
|
|
|
|
|
"""Compressor initializes with default config."""
|
|
|
|
|
compressor = CodeAwareCompressor()
|
|
|
|
|
|
|
|
|
|
assert compressor.config is not None
|
|
|
|
|
assert compressor.config.preserve_imports is True
|
|
|
|
|
|
|
|
|
|
def test_init_with_custom_config(self, default_config):
|
|
|
|
|
"""Compressor initializes with custom config."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
|
|
|
|
|
assert compressor.config == default_config
|
|
|
|
|
|
|
|
|
|
def test_compress_skips_small_content(self, compressor):
|
|
|
|
|
"""Small content is not compressed."""
|
|
|
|
|
small_code = "def f(): pass"
|
|
|
|
|
result = compressor.compress(small_code)
|
|
|
|
|
|
|
|
|
|
assert result.compressed == small_code
|
|
|
|
|
assert result.compression_ratio == 1.0
|
|
|
|
|
|
|
|
|
|
def test_compress_handles_empty_content(self, compressor):
|
|
|
|
|
"""Empty content returns empty result."""
|
|
|
|
|
result = compressor.compress("")
|
|
|
|
|
|
|
|
|
|
assert result.compressed == ""
|
|
|
|
|
assert result.compression_ratio == 1.0
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
|
|
|
|
|
def test_compress_with_explicit_language(self, compressor):
|
|
|
|
|
"""Language can be specified explicitly."""
|
|
|
|
|
code = generate_python_code(2)
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Should detect or use the specified language
|
|
|
|
|
assert result.language == CodeLanguage.PYTHON or result.language == CodeLanguage.UNKNOWN
|
|
|
|
|
|
|
|
|
|
def test_compress_auto_detects_python(self, compressor):
|
|
|
|
|
"""Python code is auto-detected during compression."""
|
|
|
|
|
code = """
|
|
|
|
|
import os
|
|
|
|
|
from typing import List
|
|
|
|
|
|
|
|
|
|
def function(arg: str) -> List[str]:
|
|
|
|
|
return [arg]
|
|
|
|
|
|
|
|
|
|
class MyClass:
|
|
|
|
|
pass
|
|
|
|
|
"""
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
# Should detect Python (if tree-sitter available) or return UNKNOWN
|
|
|
|
|
assert result.language in (CodeLanguage.PYTHON, CodeLanguage.UNKNOWN)
|
|
|
|
|
|
|
|
|
|
def test_compress_auto_detects_javascript(self, compressor):
|
|
|
|
|
"""JavaScript code is auto-detected during compression."""
|
|
|
|
|
code = """
|
|
|
|
|
const express = require('express');
|
|
|
|
|
import { something } from 'module';
|
|
|
|
|
|
|
|
|
|
function handler(req, res) {
|
|
|
|
|
return res.json({ status: 'ok' });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default handler;
|
|
|
|
|
"""
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
assert result.language in (
|
|
|
|
|
CodeLanguage.JAVASCRIPT,
|
|
|
|
|
CodeLanguage.TYPESCRIPT,
|
|
|
|
|
CodeLanguage.UNKNOWN,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_compress_auto_detects_go(self, compressor):
|
|
|
|
|
"""Go code is auto-detected during compression."""
|
|
|
|
|
code = """
|
|
|
|
|
package main
|
|
|
|
|
|
|
|
|
|
import "fmt"
|
|
|
|
|
|
|
|
|
|
func main() {
|
|
|
|
|
fmt.Println("Hello")
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
assert result.language in (CodeLanguage.GO, CodeLanguage.UNKNOWN)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestFallbackCompression
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestFallbackCompression:
|
|
|
|
|
"""Tests for fallback compression when tree-sitter unavailable."""
|
|
|
|
|
|
|
|
|
|
def test_fallback_when_tree_sitter_unavailable(self, default_config):
|
|
|
|
|
"""Uses fallback compression when tree-sitter is not installed."""
|
|
|
|
|
with patch(
|
|
|
|
|
"headroom.transforms.code_compressor._check_tree_sitter_available",
|
|
|
|
|
return_value=False,
|
|
|
|
|
):
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = generate_python_code(5)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
|
|
|
|
|
# Should still return a result (fallback compression)
|
|
|
|
|
assert result is not None
|
2026-03-26 11:11:00 -07:00
|
|
|
# Kompress fallback does NOT guarantee syntax validity
|
|
|
|
|
# If Kompress is unavailable, returns original (valid)
|
|
|
|
|
# If Kompress IS available, syntax_valid=False (cannot guarantee)
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
|
|
|
|
|
def test_fallback_preserves_structure(self, default_config):
|
Integrate DynamicContentDetector into CacheAligner (Phase 1)
- Add DynamicContentDetector integration for comprehensive dynamic content
detection (20+ patterns vs previous 4 date patterns)
- New detection: UUIDs, API keys, JWT tokens, Unix timestamps, request/trace
IDs, hex hashes (MD5/SHA1/SHA256), version numbers, high-entropy strings
- Add CacheAlignerConfig options: use_dynamic_detector, detection_tiers,
extra_dynamic_labels, entropy_threshold
- Maintain backward compatibility with legacy date-only mode
- Add 25 new comprehensive tests for Phase 1 functionality
- Fix code compressor fallback test to properly mock LLMLingua availability
Expected cache hit improvement: 30-50% by extracting more dynamic content
2026-01-19 22:56:20 -08:00
|
|
|
"""Fallback compression preserves basic structure when no compressor available.
|
|
|
|
|
|
2026-03-26 11:11:00 -07:00
|
|
|
When both tree-sitter and Kompress are unavailable, the fallback
|
Integrate DynamicContentDetector into CacheAligner (Phase 1)
- Add DynamicContentDetector integration for comprehensive dynamic content
detection (20+ patterns vs previous 4 date patterns)
- New detection: UUIDs, API keys, JWT tokens, Unix timestamps, request/trace
IDs, hex hashes (MD5/SHA1/SHA256), version numbers, high-entropy strings
- Add CacheAlignerConfig options: use_dynamic_detector, detection_tiers,
extra_dynamic_labels, entropy_threshold
- Maintain backward compatibility with legacy date-only mode
- Add 25 new comprehensive tests for Phase 1 functionality
- Fix code compressor fallback test to properly mock LLMLingua availability
Expected cache hit improvement: 30-50% by extracting more dynamic content
2026-01-19 22:56:20 -08:00
|
|
|
returns the original code unchanged - preserving all structure.
|
|
|
|
|
"""
|
|
|
|
|
with (
|
|
|
|
|
patch(
|
|
|
|
|
"headroom.transforms.code_compressor._check_tree_sitter_available",
|
|
|
|
|
return_value=False,
|
|
|
|
|
),
|
|
|
|
|
patch(
|
2026-03-26 11:11:00 -07:00
|
|
|
"headroom.transforms.kompress_compressor.is_kompress_available",
|
Integrate DynamicContentDetector into CacheAligner (Phase 1)
- Add DynamicContentDetector integration for comprehensive dynamic content
detection (20+ patterns vs previous 4 date patterns)
- New detection: UUIDs, API keys, JWT tokens, Unix timestamps, request/trace
IDs, hex hashes (MD5/SHA1/SHA256), version numbers, high-entropy strings
- Add CacheAlignerConfig options: use_dynamic_detector, detection_tiers,
extra_dynamic_labels, entropy_threshold
- Maintain backward compatibility with legacy date-only mode
- Add 25 new comprehensive tests for Phase 1 functionality
- Fix code compressor fallback test to properly mock LLMLingua availability
Expected cache hit improvement: 30-50% by extracting more dynamic content
2026-01-19 22:56:20 -08:00
|
|
|
return_value=False,
|
|
|
|
|
),
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
):
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = generate_python_code(3)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
|
Integrate DynamicContentDetector into CacheAligner (Phase 1)
- Add DynamicContentDetector integration for comprehensive dynamic content
detection (20+ patterns vs previous 4 date patterns)
- New detection: UUIDs, API keys, JWT tokens, Unix timestamps, request/trace
IDs, hex hashes (MD5/SHA1/SHA256), version numbers, high-entropy strings
- Add CacheAlignerConfig options: use_dynamic_detector, detection_tiers,
extra_dynamic_labels, entropy_threshold
- Maintain backward compatibility with legacy date-only mode
- Add 25 new comprehensive tests for Phase 1 functionality
- Fix code compressor fallback test to properly mock LLMLingua availability
Expected cache hit improvement: 30-50% by extracting more dynamic content
2026-01-19 22:56:20 -08:00
|
|
|
# With no compressor available, original code is returned unchanged
|
|
|
|
|
# This preserves all imports and class/function signatures
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
assert "import os" in result.compressed
|
|
|
|
|
assert "def function_" in result.compressed
|
Integrate DynamicContentDetector into CacheAligner (Phase 1)
- Add DynamicContentDetector integration for comprehensive dynamic content
detection (20+ patterns vs previous 4 date patterns)
- New detection: UUIDs, API keys, JWT tokens, Unix timestamps, request/trace
IDs, hex hashes (MD5/SHA1/SHA256), version numbers, high-entropy strings
- Add CacheAlignerConfig options: use_dynamic_detector, detection_tiers,
extra_dynamic_labels, entropy_threshold
- Maintain backward compatibility with legacy date-only mode
- Add 25 new comprehensive tests for Phase 1 functionality
- Fix code compressor fallback test to properly mock LLMLingua availability
Expected cache hit improvement: 30-50% by extracting more dynamic content
2026-01-19 22:56:20 -08:00
|
|
|
# Compression ratio should be 1.0 (no compression)
|
|
|
|
|
assert result.compression_ratio == 1.0
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestTransformInterface
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestTransformInterface:
|
|
|
|
|
"""Tests for Transform interface (apply, should_apply)."""
|
|
|
|
|
|
|
|
|
|
def test_should_apply_returns_false_for_small_content(self, default_config, tokenizer):
|
|
|
|
|
"""should_apply returns False for small content."""
|
|
|
|
|
config = CodeCompressorConfig(min_tokens_for_compression=1000)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
messages = [{"role": "user", "content": "def f(): pass"}]
|
|
|
|
|
|
|
|
|
|
assert not compressor.should_apply(messages, tokenizer)
|
|
|
|
|
|
|
|
|
|
def test_should_apply_returns_bool_for_large_code(self, default_config, tokenizer):
|
|
|
|
|
"""should_apply returns boolean for large code content."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = generate_python_code(20)
|
|
|
|
|
messages = [{"role": "tool", "tool_call_id": "call_1", "content": code}]
|
|
|
|
|
|
|
|
|
|
# Should return True if there's code content to process
|
|
|
|
|
result = compressor.should_apply(messages, tokenizer)
|
|
|
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
|
|
|
|
def test_apply_returns_transform_result(self, default_config, tokenizer):
|
|
|
|
|
"""apply() returns proper TransformResult."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = generate_python_code(10)
|
|
|
|
|
messages = [{"role": "tool", "tool_call_id": "call_1", "content": code}]
|
|
|
|
|
|
|
|
|
|
result = compressor.apply(messages, tokenizer)
|
|
|
|
|
|
|
|
|
|
assert result is not None
|
|
|
|
|
assert result.tokens_before > 0
|
|
|
|
|
assert len(result.messages) == 1
|
|
|
|
|
|
|
|
|
|
def test_apply_passes_through_non_code_messages(self, default_config, tokenizer):
|
|
|
|
|
"""apply() passes through non-code messages unchanged."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
messages = [
|
|
|
|
|
{"role": "user", "content": "Hello"},
|
|
|
|
|
{"role": "assistant", "content": "Hi there!"},
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
result = compressor.apply(messages, tokenizer)
|
|
|
|
|
|
|
|
|
|
assert result.messages[0]["content"] == "Hello"
|
|
|
|
|
assert result.messages[1]["content"] == "Hi there!"
|
|
|
|
|
|
|
|
|
|
def test_name_property(self, compressor):
|
|
|
|
|
"""Compressor has correct name."""
|
|
|
|
|
assert compressor.name == "code_aware_compressor"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestEdgeCases
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestEdgeCases:
|
|
|
|
|
"""Edge case tests for CodeAwareCompressor."""
|
|
|
|
|
|
|
|
|
|
def test_whitespace_only_content(self, compressor):
|
|
|
|
|
"""Whitespace-only content is handled gracefully."""
|
|
|
|
|
result = compressor.compress(" \n\t\n ")
|
|
|
|
|
|
|
|
|
|
assert result.compression_ratio == 1.0
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
|
|
|
|
|
def test_unicode_content(self, default_config):
|
|
|
|
|
"""Unicode in code is handled correctly."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = '''
|
|
|
|
|
def greet(name: str) -> str:
|
|
|
|
|
"""Greet the user in multiple languages."""
|
|
|
|
|
return f"Hello, {name}! \u4f60\u597d! \u3053\u3093\u306b\u3061\u306f!"
|
|
|
|
|
'''
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
|
|
|
|
|
# Should handle unicode without crashing
|
|
|
|
|
assert result is not None
|
|
|
|
|
|
|
|
|
|
def test_very_long_function(self, default_config):
|
|
|
|
|
"""Very long functions are compressed."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
lines = ["def very_long_function():"]
|
|
|
|
|
lines.append(' """A very long function."""')
|
|
|
|
|
for i in range(100):
|
|
|
|
|
lines.append(f" x_{i} = {i}")
|
|
|
|
|
lines.append(" return x_99")
|
|
|
|
|
code = "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
|
|
|
|
|
# Should compress the long function body
|
|
|
|
|
assert result.compression_ratio < 1.0 or "tree_sitter" not in str(
|
|
|
|
|
is_tree_sitter_available()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_nested_functions(self, default_config):
|
|
|
|
|
"""Nested functions are handled."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
code = """
|
|
|
|
|
def outer():
|
|
|
|
|
def inner():
|
|
|
|
|
return "inner"
|
|
|
|
|
return inner()
|
|
|
|
|
"""
|
|
|
|
|
result = compressor.compress(code)
|
|
|
|
|
|
|
|
|
|
assert result is not None
|
2026-03-26 12:04:30 -07:00
|
|
|
# syntax_valid requires tree-sitter; without it, validation is skipped
|
|
|
|
|
if is_tree_sitter_available():
|
|
|
|
|
assert result.syntax_valid is True
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
|
|
|
|
|
def test_syntax_errors_in_input(self, default_config):
|
|
|
|
|
"""Syntax errors in input don't crash the compressor."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
# Invalid Python syntax
|
|
|
|
|
code = """
|
|
|
|
|
def broken(
|
|
|
|
|
# Missing closing paren
|
|
|
|
|
"""
|
|
|
|
|
# Should not raise
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
assert result is not None
|
|
|
|
|
|
|
|
|
|
def test_mixed_language_content(self, default_config):
|
|
|
|
|
"""Mixed language content (like markdown with code) is handled."""
|
|
|
|
|
compressor = CodeAwareCompressor(default_config)
|
|
|
|
|
content = """
|
|
|
|
|
# Documentation
|
|
|
|
|
|
|
|
|
|
Here is some code:
|
|
|
|
|
|
|
|
|
|
```python
|
|
|
|
|
def example():
|
|
|
|
|
pass
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
And some more text.
|
|
|
|
|
"""
|
|
|
|
|
# Should not crash
|
|
|
|
|
result = compressor.compress(content)
|
|
|
|
|
assert result is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestMemoryManagement
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestMemoryManagement:
|
|
|
|
|
"""Tests for memory management functions."""
|
|
|
|
|
|
|
|
|
|
def test_is_tree_sitter_available_returns_bool(self):
|
|
|
|
|
"""is_tree_sitter_available returns a boolean."""
|
|
|
|
|
result = is_tree_sitter_available()
|
|
|
|
|
assert isinstance(result, bool)
|
|
|
|
|
|
|
|
|
|
def test_is_tree_sitter_loaded_returns_false_initially(self):
|
|
|
|
|
"""is_tree_sitter_loaded returns False when no parsers loaded."""
|
|
|
|
|
# Clear any loaded parsers first
|
|
|
|
|
unload_tree_sitter()
|
|
|
|
|
assert is_tree_sitter_loaded() is False
|
|
|
|
|
|
|
|
|
|
def test_unload_returns_false_when_nothing_loaded(self):
|
|
|
|
|
"""unload_tree_sitter returns False when nothing to unload."""
|
|
|
|
|
# Ensure nothing is loaded
|
|
|
|
|
unload_tree_sitter()
|
|
|
|
|
result = unload_tree_sitter()
|
|
|
|
|
assert result is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Integration Tests (only run if tree-sitter is installed)
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter-languages not installed")
|
|
|
|
|
class TestTreeSitterIntegration:
|
|
|
|
|
"""Integration tests that require actual tree-sitter installation.
|
|
|
|
|
|
|
|
|
|
These tests verify actual AST parsing and compression behavior.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def test_actual_python_compression(self):
|
|
|
|
|
"""Test actual compression of Python code."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(5)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Should achieve compression
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.language == CodeLanguage.PYTHON
|
|
|
|
|
|
|
|
|
|
def test_actual_javascript_compression(self):
|
|
|
|
|
"""Test actual compression of JavaScript code."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_javascript_code(5)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="javascript")
|
|
|
|
|
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.language == CodeLanguage.JAVASCRIPT
|
|
|
|
|
|
|
|
|
|
def test_actual_go_compression(self):
|
fix(code): stop TS export duplication + comment displacement (#1906)
## Description
`CodeAwareCompressor` (AST-based code compression,
`headroom/transforms/code_compressor.py`) had two bugs in its
structure-reassembly path, found while investigating a reported Go
brace-duplication issue (the Go bug itself — `statement_list` row-range
swallowing a block's closing brace — was already fixed on `main` in
#1668; this PR fixes what was *actually* still broken):
1. **TS/JS `export` keyword duplication.** `export function foo() {}` /
`export class Foo {}` compressed to `export export function foo() {}` —
invalid syntax, silently discarded by `_verify_syntax`'s fallback (the
caller never sees an error, compression just quietly no-ops). Root
cause: `_compress_function_ast` / `_compress_class_ast` slice a node's
source by **line**, not by byte offset, deliberately — to preserve
leading indentation for definitions nested inside classes. But when a
node shares its *first* line with a preceding sibling (the `export`
keyword is a sibling of the function inside tree-sitter's
`export_statement` node, not part of the function node itself), that
line-based slice pulled the sibling's text in too. The
`export_statement` handler then re-prepended the same `export` text on
top, producing the duplicate.
2. **Doc-comment displacement (all languages).** A `/** ... */` or `//`
doc comment directly above a top-level function/class/type got detached
from its declaration during AST extraction and re-emitted in one cluster
at the very end of the compressed output, instead of staying attached to
what it documents. Root cause: doc comments are top-level *siblings* of
the declaration they document, not children of it — the extractor didn't
attach them to anything, so they fell through to a "leftover top-level
code" bucket that gets flushed as a single block after all functions.
Also tightens `test_actual_go_compression`, which — per its own comment
— was written to *tolerate* the Go bug (`compression_ratio may be 1.0 if
compression produces invalid syntax`) rather than catch it. Since the
underlying Go bug is already fixed on `main`, this now asserts real
compression (`compression_ratio < 1.0`), matching its JS/Python
siblings.
Closes #1905
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
Two commits: the fix itself, then the tests that prove it — bisectable
independently, both pass the full suite on their own.
**Commit 1 — `fix(code):`**
- `headroom/transforms/code_compressor.py`: add `_get_node_lines()` —
line-based node slicing that still preserves indentation, but trims a
preceding sibling's text from the first line when that prefix isn't pure
whitespace (i.e. an `export` keyword sharing the line), so callers that
re-add the sibling text themselves don't get a duplicate; used by
`_compress_function_ast` and `_compress_class_ast`.
- `headroom/transforms/code_compressor.py`: add
`_get_leading_comment_text()` — walks a node's `prev_sibling` chain to
collect contiguous doc-comment nodes immediately above it (no blank line
in between) and returns them for the caller to prepend, also marking
their byte ranges as captured so they aren't independently swept into
the leftover top-level-code bucket; wired into every capture branch in
`_extract_structure` (package, import, export statement, decorator,
function, class, type).
- `CHANGELOG.md`: added an entry under `### Fixed`.
**Commit 2 — `test(code):`**
- `tests/test_transforms/test_code_compressor.py`:
`test_actual_go_compression` now asserts `compression_ratio < 1.0`
instead of tolerating a 1.0 fallback.
- `tests/test_code_aware_brace_comment_regressions.py` (new): 4
regression tests — TS `export` not duplicated + valid syntax, TS doc
comments stay attached, Go doc comments stay attached, and a
real-TS-compression parity test matching the existing JS/Python/Go
"actual compression" tests.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py tests/test_code_aware_brace_comment_regressions.py
All checks passed!
$ ruff format --check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py tests/test_code_aware_brace_comment_regressions.py
3 files already formatted
$ pytest tests/test_transforms/test_code_compressor.py tests/test_code_aware_regressions.py tests/test_code_aware_brace_comment_regressions.py -q
83 passed in 6.23s
$ pytest -q # full suite
7912 passed, 5 failed, 442 skipped in 417.43s (0:06:57)
# The 5 failures are pre-existing and unrelated: confirmed to fail identically
# with this PR's changes stashed out (clean upstream/main checkout).
# - test_wrap_marker_is_stale_when_pid_reused (PID-reuse detection, env-specific)
# - test_read_cached_oauth_token_falls_back_to_gh_cli (leaks real local `gh` credentials)
# - test_rtk_reader_returns_none_on_nonzero_exit / test_lean_ctx_reader_returns_none_on_failure_and_logs
# (pass in isolation; fail only in full-suite order — pre-existing test-pollution, unrelated to code_compressor.py)
# - test_parser_usable_in_thread_pool (test itself passes a str to parser.parse(),
# which tree-sitter's binding has always required as bytes — a pre-existing test
# bug unrelated to this change; separate fix in progress on another branch)
$ mypy headroom
Success: no issues found in 408 source files
```
## Real Behavior Proof
- Environment: macOS (Darwin 24.6.0), Python 3.14.5, headroom-ai dev
checkout built via `uv sync --extra dev` + `maturin develop -m
crates/headroom-py/Cargo.toml` (real `headroom._core` build, not
mocked), `tree-sitter==0.25.2` / `tree-sitter-language-pack` per the
pinned `[code]` extra.
- Exact command / steps: ran
`CodeAwareCompressor(CodeCompressorConfig(min_tokens_for_compression=10,
enable_ccr=False)).compress(open("sdk/typescript/src/client.ts").read(),
language="typescript")` identically against `git stash`-ed (pre-fix) and
current (post-fix) trees; full snippet and additional samples below.
- Observed result: `client.ts` (real 20KB SDK file in this repo) went
from `compression_ratio=1.0` with a silent fallback (`export export
class HeadroomClient` in the raw AST attempt, invalid syntax) to
`compression_ratio=0.942`, `syntax_valid=True` — real compression, no
duplication; full before/after table below.
- Not tested: real-world repos beyond this repo's own SDK sample and the
bundled benchmark fixture — broader corpus testing may follow as a
comment on this PR.
**Exact command, full snippet:**
```python
from headroom.transforms.code_compressor import CodeAwareCompressor, CodeCompressorConfig
compressor = CodeAwareCompressor(CodeCompressorConfig(min_tokens_for_compression=10, enable_ccr=False))
with open("sdk/typescript/src/client.ts") as f:
code = f.read()
result = compressor.compress(code, language="typescript")
```
**Observed result, before vs. after, real code:**
| Sample | Before (main) | After (this fix) |
|---|---|---|
| `sdk/typescript/src/client.ts` (real 20KB SDK file, this repo) |
`compression_ratio=1.0`, silent fallback — `export export class
HeadroomClient` in the raw AST attempt, invalid syntax |
`compression_ratio=0.942`, `syntax_valid=True` — real compression, no
duplication |
| TS fixture exercising both bugs (exported fn/class + doc comments) |
`compression_ratio=1.0`, silent fallback | `compression_ratio=0.993`,
`syntax_valid=True` |
| `middleware/ratelimit.go` (bundled benchmark sample) |
`compression_ratio=0.862`, `syntax_valid=True` — unaffected (Go bug
already fixed on `main` by #1668) | `compression_ratio=0.862`,
`syntax_valid=True` — unchanged, confirms no regression |
| `generate_go_code(3)` (existing test fixture) |
`compression_ratio=0.498` | `compression_ratio=0.498` — unchanged,
confirms no regression |
On code shaped to actually exercise elision (function bodies long enough
to exceed `max_body_lines=5`), TypeScript compresses in line with other
languages once the correctness bug stops blocking it entirely:
| Language | Compression savings (synthetic fixture, ~10-line function
bodies) |
|---|---|
| Python | 64.4% |
| Go | 52.3% |
| TypeScript | 49.0% |
| JavaScript | 42.8% |
(`client.ts`'s real-world 5.8% savings is lower than the synthetic
TypeScript number above because most of its methods are ≤5 lines — under
the elision threshold regardless of language — not because of a
language-specific limitation.)
## 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
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The Go brace-duplication bug that motivated this investigation was
already fixed on `main` (#1668, merged before this branch was based) —
confirmed via the minimal repro and `ratelimit.go`, both compress
cleanly with no duplicated braces. This PR fixes what was still actually
broken: the TS/JS `export`-duplication bug and the doc-comment
displacement bug (both present across languages), found empirically
while verifying the original bug report against the current `main`.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-10 01:51:32 +08:00
|
|
|
"""Test actual compression of Go code."""
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_go_code(3)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="go")
|
|
|
|
|
|
fix(code): stop TS export duplication + comment displacement (#1906)
## Description
`CodeAwareCompressor` (AST-based code compression,
`headroom/transforms/code_compressor.py`) had two bugs in its
structure-reassembly path, found while investigating a reported Go
brace-duplication issue (the Go bug itself — `statement_list` row-range
swallowing a block's closing brace — was already fixed on `main` in
#1668; this PR fixes what was *actually* still broken):
1. **TS/JS `export` keyword duplication.** `export function foo() {}` /
`export class Foo {}` compressed to `export export function foo() {}` —
invalid syntax, silently discarded by `_verify_syntax`'s fallback (the
caller never sees an error, compression just quietly no-ops). Root
cause: `_compress_function_ast` / `_compress_class_ast` slice a node's
source by **line**, not by byte offset, deliberately — to preserve
leading indentation for definitions nested inside classes. But when a
node shares its *first* line with a preceding sibling (the `export`
keyword is a sibling of the function inside tree-sitter's
`export_statement` node, not part of the function node itself), that
line-based slice pulled the sibling's text in too. The
`export_statement` handler then re-prepended the same `export` text on
top, producing the duplicate.
2. **Doc-comment displacement (all languages).** A `/** ... */` or `//`
doc comment directly above a top-level function/class/type got detached
from its declaration during AST extraction and re-emitted in one cluster
at the very end of the compressed output, instead of staying attached to
what it documents. Root cause: doc comments are top-level *siblings* of
the declaration they document, not children of it — the extractor didn't
attach them to anything, so they fell through to a "leftover top-level
code" bucket that gets flushed as a single block after all functions.
Also tightens `test_actual_go_compression`, which — per its own comment
— was written to *tolerate* the Go bug (`compression_ratio may be 1.0 if
compression produces invalid syntax`) rather than catch it. Since the
underlying Go bug is already fixed on `main`, this now asserts real
compression (`compression_ratio < 1.0`), matching its JS/Python
siblings.
Closes #1905
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
Two commits: the fix itself, then the tests that prove it — bisectable
independently, both pass the full suite on their own.
**Commit 1 — `fix(code):`**
- `headroom/transforms/code_compressor.py`: add `_get_node_lines()` —
line-based node slicing that still preserves indentation, but trims a
preceding sibling's text from the first line when that prefix isn't pure
whitespace (i.e. an `export` keyword sharing the line), so callers that
re-add the sibling text themselves don't get a duplicate; used by
`_compress_function_ast` and `_compress_class_ast`.
- `headroom/transforms/code_compressor.py`: add
`_get_leading_comment_text()` — walks a node's `prev_sibling` chain to
collect contiguous doc-comment nodes immediately above it (no blank line
in between) and returns them for the caller to prepend, also marking
their byte ranges as captured so they aren't independently swept into
the leftover top-level-code bucket; wired into every capture branch in
`_extract_structure` (package, import, export statement, decorator,
function, class, type).
- `CHANGELOG.md`: added an entry under `### Fixed`.
**Commit 2 — `test(code):`**
- `tests/test_transforms/test_code_compressor.py`:
`test_actual_go_compression` now asserts `compression_ratio < 1.0`
instead of tolerating a 1.0 fallback.
- `tests/test_code_aware_brace_comment_regressions.py` (new): 4
regression tests — TS `export` not duplicated + valid syntax, TS doc
comments stay attached, Go doc comments stay attached, and a
real-TS-compression parity test matching the existing JS/Python/Go
"actual compression" tests.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py tests/test_code_aware_brace_comment_regressions.py
All checks passed!
$ ruff format --check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py tests/test_code_aware_brace_comment_regressions.py
3 files already formatted
$ pytest tests/test_transforms/test_code_compressor.py tests/test_code_aware_regressions.py tests/test_code_aware_brace_comment_regressions.py -q
83 passed in 6.23s
$ pytest -q # full suite
7912 passed, 5 failed, 442 skipped in 417.43s (0:06:57)
# The 5 failures are pre-existing and unrelated: confirmed to fail identically
# with this PR's changes stashed out (clean upstream/main checkout).
# - test_wrap_marker_is_stale_when_pid_reused (PID-reuse detection, env-specific)
# - test_read_cached_oauth_token_falls_back_to_gh_cli (leaks real local `gh` credentials)
# - test_rtk_reader_returns_none_on_nonzero_exit / test_lean_ctx_reader_returns_none_on_failure_and_logs
# (pass in isolation; fail only in full-suite order — pre-existing test-pollution, unrelated to code_compressor.py)
# - test_parser_usable_in_thread_pool (test itself passes a str to parser.parse(),
# which tree-sitter's binding has always required as bytes — a pre-existing test
# bug unrelated to this change; separate fix in progress on another branch)
$ mypy headroom
Success: no issues found in 408 source files
```
## Real Behavior Proof
- Environment: macOS (Darwin 24.6.0), Python 3.14.5, headroom-ai dev
checkout built via `uv sync --extra dev` + `maturin develop -m
crates/headroom-py/Cargo.toml` (real `headroom._core` build, not
mocked), `tree-sitter==0.25.2` / `tree-sitter-language-pack` per the
pinned `[code]` extra.
- Exact command / steps: ran
`CodeAwareCompressor(CodeCompressorConfig(min_tokens_for_compression=10,
enable_ccr=False)).compress(open("sdk/typescript/src/client.ts").read(),
language="typescript")` identically against `git stash`-ed (pre-fix) and
current (post-fix) trees; full snippet and additional samples below.
- Observed result: `client.ts` (real 20KB SDK file in this repo) went
from `compression_ratio=1.0` with a silent fallback (`export export
class HeadroomClient` in the raw AST attempt, invalid syntax) to
`compression_ratio=0.942`, `syntax_valid=True` — real compression, no
duplication; full before/after table below.
- Not tested: real-world repos beyond this repo's own SDK sample and the
bundled benchmark fixture — broader corpus testing may follow as a
comment on this PR.
**Exact command, full snippet:**
```python
from headroom.transforms.code_compressor import CodeAwareCompressor, CodeCompressorConfig
compressor = CodeAwareCompressor(CodeCompressorConfig(min_tokens_for_compression=10, enable_ccr=False))
with open("sdk/typescript/src/client.ts") as f:
code = f.read()
result = compressor.compress(code, language="typescript")
```
**Observed result, before vs. after, real code:**
| Sample | Before (main) | After (this fix) |
|---|---|---|
| `sdk/typescript/src/client.ts` (real 20KB SDK file, this repo) |
`compression_ratio=1.0`, silent fallback — `export export class
HeadroomClient` in the raw AST attempt, invalid syntax |
`compression_ratio=0.942`, `syntax_valid=True` — real compression, no
duplication |
| TS fixture exercising both bugs (exported fn/class + doc comments) |
`compression_ratio=1.0`, silent fallback | `compression_ratio=0.993`,
`syntax_valid=True` |
| `middleware/ratelimit.go` (bundled benchmark sample) |
`compression_ratio=0.862`, `syntax_valid=True` — unaffected (Go bug
already fixed on `main` by #1668) | `compression_ratio=0.862`,
`syntax_valid=True` — unchanged, confirms no regression |
| `generate_go_code(3)` (existing test fixture) |
`compression_ratio=0.498` | `compression_ratio=0.498` — unchanged,
confirms no regression |
On code shaped to actually exercise elision (function bodies long enough
to exceed `max_body_lines=5`), TypeScript compresses in line with other
languages once the correctness bug stops blocking it entirely:
| Language | Compression savings (synthetic fixture, ~10-line function
bodies) |
|---|---|
| Python | 64.4% |
| Go | 52.3% |
| TypeScript | 49.0% |
| JavaScript | 42.8% |
(`client.ts`'s real-world 5.8% savings is lower than the synthetic
TypeScript number above because most of its methods are ≤5 lines — under
the elision threshold regardless of language — not because of a
language-specific limitation.)
## 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
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
The Go brace-duplication bug that motivated this investigation was
already fixed on `main` (#1668, merged before this branch was based) —
confirmed via the minimal repro and `ratelimit.go`, both compress
cleanly with no duplicated braces. This PR fixes what was still actually
broken: the TS/JS `export`-duplication bug and the doc-comment
displacement bug (both present across languages), found empirically
while verifying the original bug report against the current `main`.
---------
Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-07-10 01:51:32 +08:00
|
|
|
assert result.compression_ratio < 1.0
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.language == CodeLanguage.GO
|
|
|
|
|
|
fix(code): compress class member containers (#1334)
## Description
CodeAwareCompressor used the same `body_node_types` config to find both
executable function bodies and class/impl member containers. That works
when those AST nodes happen to match, but it misses member containers
such as Java `class_body`, C++ `field_declaration_list`, and Rust
`declaration_list`, so class methods were returned essentially
uncompressed.
This adds an optional `class_body_node_types` override for class/impl
member containers and uses it only in class compression. It also skips
anonymous punctuation tokens while reconstructing class bodies and keeps
same-line C++ class semicolons attached to the compressed class
declaration.
Closes #1318
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added `LangConfig.class_body_node_types` for languages whose
class/impl member container differs from executable method-body nodes.
- Configured class member containers for JavaScript, TypeScript, Java,
C++, and Rust.
- Updated `_compress_class_ast` to use class-member containers, skip
anonymous punctuation children, and preserve C++ `};` output without
creating stray top-level semicolons.
- Added regression coverage proving class/impl methods compress for
JavaScript, TypeScript, Java, C++, and Rust.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ PYTHONPATH=. /tmp/headroom-1319-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py -q
collected 71 items
tests/test_transforms/test_code_compressor.py .......................... [ 36%]
............................................. [100%]
71 passed, 1 warning in 0.36s
$ /tmp/headroom-1319-venv/bin/python -m ruff check .
All checks passed!
$ /tmp/headroom-1319-venv/bin/python -m ruff format --check .
965 files already formatted
$ PYTHONPATH=. /tmp/headroom-1319-venv/bin/python -m mypy headroom
headroom/proxy/server.py:1186: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1257: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1261: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
pyproject.toml: note: unused section(s): module = ['mlx.*']
Success: no issues found in 394 source files
```
## Real Behavior Proof
- Environment: macOS arm64, Python 3.14.5, branch
`fix-code-compressor-class-members`, tree-sitter grammar pack installed
in `/tmp/headroom-1319-venv`, repo imported with `PYTHONPATH=.`.
- Exact command / steps: Reproduced class-method compression with
`CodeAwareCompressor(CodeCompressorConfig(enable_ccr=False,
min_tokens_for_compression=1, max_body_lines=1))` for Java/C++/Rust
before the fix, then reran the pytest/ruff/mypy commands listed above
after the patch.
- Observed result: Java/C++/Rust class methods now compress below 1.0
while `syntax_valid` remains true; C++ output preserves `};`; regression
coverage also verifies JavaScript/TypeScript class member containers.
- Not tested: Full repository pytest suite; local `uv run` editable
builds are blocked on this machine by native C++ header failures in
optional/native dependencies (`hnswlib` / Rust `esaxx-rs`), so
validation used a lightweight venv with `PYTHONPATH=.`.
## 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
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
Documentation and CHANGELOG updates are not applicable for this narrow
bug fix. The pytest warning shown above is from running without
`pytest-asyncio` in the lightweight verification venv (`asyncio_mode`
config is unknown there); it is unrelated to this change.
2026-06-23 15:41:36 -04:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
|
(
|
|
|
|
|
"language",
|
|
|
|
|
"code",
|
|
|
|
|
"expected_signature",
|
|
|
|
|
"expected_omitted_lines",
|
|
|
|
|
"expected_removed_line",
|
|
|
|
|
"expected_closing",
|
|
|
|
|
),
|
|
|
|
|
[
|
|
|
|
|
(
|
|
|
|
|
"javascript",
|
|
|
|
|
(
|
|
|
|
|
"class Calc {\n"
|
|
|
|
|
" compute(x) {\n"
|
|
|
|
|
" let a = x + 1;\n"
|
|
|
|
|
" let b = a * 2;\n"
|
|
|
|
|
" let c = b - 3;\n"
|
|
|
|
|
" return c;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
),
|
|
|
|
|
"compute(x) {",
|
|
|
|
|
3,
|
|
|
|
|
"return c;",
|
|
|
|
|
"}\n}",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"typescript",
|
|
|
|
|
(
|
|
|
|
|
"class Calc {\n"
|
|
|
|
|
" compute(x: number): number {\n"
|
|
|
|
|
" let a = x + 1;\n"
|
|
|
|
|
" let b = a * 2;\n"
|
|
|
|
|
" let c = b - 3;\n"
|
|
|
|
|
" return c;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
),
|
|
|
|
|
"compute(x: number): number {",
|
|
|
|
|
3,
|
|
|
|
|
"return c;",
|
|
|
|
|
"}\n}",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"java",
|
|
|
|
|
(
|
|
|
|
|
"public class Calc {\n"
|
|
|
|
|
" public int compute(int x) {\n"
|
|
|
|
|
" int a = x + 1;\n"
|
|
|
|
|
" int b = a * 2;\n"
|
|
|
|
|
" int c = b - 3;\n"
|
|
|
|
|
" int d = c / 4;\n"
|
|
|
|
|
" int e = d + 5;\n"
|
|
|
|
|
" return e;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
),
|
|
|
|
|
"public int compute(int x) {",
|
|
|
|
|
5,
|
|
|
|
|
"return e;",
|
|
|
|
|
"}\n}",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"cpp",
|
|
|
|
|
(
|
|
|
|
|
"class Calc {\n"
|
|
|
|
|
"public:\n"
|
|
|
|
|
" int compute(int x) {\n"
|
|
|
|
|
" int a = x + 1;\n"
|
|
|
|
|
" int b = a * 2;\n"
|
|
|
|
|
" int c = b - 3;\n"
|
|
|
|
|
" int d = c / 4;\n"
|
|
|
|
|
" int e = d + 5;\n"
|
|
|
|
|
" return e;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"};\n"
|
|
|
|
|
),
|
|
|
|
|
"int compute(int x) {",
|
|
|
|
|
5,
|
|
|
|
|
"return e;",
|
|
|
|
|
"};",
|
|
|
|
|
),
|
|
|
|
|
(
|
|
|
|
|
"rust",
|
|
|
|
|
(
|
|
|
|
|
"impl Calc {\n"
|
|
|
|
|
" pub fn compute(&self, x: i32) -> i32 {\n"
|
|
|
|
|
" let a = x + 1;\n"
|
|
|
|
|
" let b = a * 2;\n"
|
|
|
|
|
" let c = b - 3;\n"
|
|
|
|
|
" let d = c / 4;\n"
|
|
|
|
|
" let e = d + 5;\n"
|
|
|
|
|
" e\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
),
|
|
|
|
|
"pub fn compute(&self, x: i32) -> i32 {",
|
|
|
|
|
5,
|
|
|
|
|
" e\n",
|
|
|
|
|
"}\n}",
|
|
|
|
|
),
|
feat(transforms): first-class C# support in CodeAwareCompressor (#1926)
Refs #1664
## Description
First-class C# support in `CodeAwareCompressor` via the tree-sitter
`csharp` grammar, at parity with Java/C++/Rust: `using` directives,
namespace headers, and type/member signatures preserved verbatim;
method/constructor/destructor/operator/local-function bodies compressed;
malformed input passes through unchanged. **No new dependencies** — the
grammar ships inside the already-pinned
`tree-sitter-language-pack==0.13.0` (resolves only as `"csharp"`;
`c_sharp`/`cs` raise `LookupError`). Spec and maintainer go-ahead in the
issue.
Closes #1664
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `CodeLanguage.CSHARP` + full `_LANG_CONFIGS` entry;
`_LANGUAGE_PREFILTER` and `content_detector` patterns chosen to be
C#-distinctive (so Java doesn't mis-tag).
- New data-driven `LangConfig` fields (pattern of #1334's
`class_body_node_types`): `container_node_types` — block-scoped
`namespace { }` routed through class compression so members compress
without the wrapper being re-emitted verbatim; `opaque_node_types` —
`#if`…`#endif` wrappers preserved verbatim without recursion (recursing
+ wrapper re-emit duplicated whole files, up to ~1.9x input on real
repos); `#if` blocks wrapping only usings are emitted with the imports
so they stay ahead of type declarations.
- Shared-path fixes surfaced by real C# repos, each guarded and covered
by a fail-before test: keep an Allman `{` on its own line in class
reconstruction (K&R path byte-for-byte unchanged; Allman Java now
compresses instead of falling back); line-based child extraction no
longer swallows the following line for nodes ending at column 0 (C#
`#region`/`#endregion` span their trailing newline — the over-slice
duplicated the next member's signature or the closing brace); uncaptured
top-level nodes preceding the first captured node (license banners,
`#region License`) are emitted first instead of relocated below the code
(tree-sitter-c-sharp rejects top-level `#region` after a type
declaration, so relocation forfeited compression for the whole file).
- `TestCSharpSupport` (8 tests) + a C# case in the parametrized
member-container test; CHANGELOG entry.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_transforms/test_code_compressor.py -q
2 failed, 78 passed, 1 warning, 4 errors # the 2 failures / 4 errors reproduce
# identically on main in the same env
# (network-dependent tokenizer setup)
Fail-before: with both changed sources reverted to main, the new C#-scoped
selection reports "10 failed, 5 passed" (the 5 other languages keep passing);
on the branch: "15 passed".
$ ruff check headroom/transforms/code_compressor.py headroom/transforms/content_detector.py tests/test_transforms/test_code_compressor.py
All checks passed!
$ ruff format --check <same files>
3 files already formatted
```
## Real Behavior Proof
- Environment: Linux 6.8.0 aarch64, Python 3.10.12; `uv run --no-project
--with "tree-sitter-language-pack==0.13.0" --with
"tree-sitter>=0.25.2,<0.26" --with "pydantic>=2.0.0"`; real
`CodeAwareCompressor` (`CodeCompressorConfig(enable_ccr=False)`,
otherwise defaults), no mocks.
- Exact command / steps: cloned two real .NET repos at depth 1
(`github.com/JamesNK/Newtonsoft.Json @ 4f73e74`,
`github.com/App-vNext/Polly @ 7a1d10f`), ran `python proof_csharp.py
<repo>` over every `.cs` file (chars/4 token estimate; tiktoken BPE
download unavailable in my sandbox). Script in the collapsed section
below.
- Observed result: 16.1% tokens saved on Newtonsoft.Json (945/945
syntax-valid), 37.8% on Polly (797/797 syntax-valid), zero content
duplication; full output:
```text
repo: Newtonsoft.Json (945 .cs files)
tokens before: 1,777,691 after: 1,490,629 saved: 287,062 (16.1%)
files compressed: 479 pass-through: 466 inflated(>before): 19
syntax_valid: 945/945
latency ms P50: 0.7 P95: 18.7 P99: 44.1 max: 255.0 mean: 3.5
repo: Polly (797 .cs files)
tokens before: 1,100,523 after: 684,303 saved: 416,220 (37.8%)
files compressed: 693 pass-through: 104 inflated(>before): 15
syntax_valid: 797/797
latency ms P50: 0.8 P95: 11.6 P99: 28.9 max: 74.1 mean: 2.4
```
After rebasing onto current `main` (which touched the same transform
files via #1906/#1747/#1668) I re-ran the Polly proof on the rebased
tree: 37.8% saved, 797/797 syntax-valid, P99 28.5ms — unchanged.
Signatures/properties verbatim, bodies elided with call summaries,
`using` order and preproc balance intact; residual "inflated" files are
+2…+209 chars of assembly blank lines, not duplicated content.
Newtonsoft is the adversarial case (multi-targeting: heavy `#if`,
`#region`, Allman) — its conditional regions stay verbatim by design.
Latency at parity with Java (<50ms P99; max is the pre-existing
symbol-analysis cost on ~1800+-line files, shared with other languages).
- Not tested: proxy end-to-end path with C# through `ContentRouter`
(tested the `CodeAwareCompressor` API directly); CCR retrieval
round-trips (`enable_ccr=False` in proof runs); exact tiktoken counts
(chars/4 estimate — relative ratios are tokenizer-independent);
Windows/macOS; full native `uv run pytest` with the Rust extension (ran
the complete `test_code_compressor.py` in a lightweight venv; its 2
failures/4 errors reproduce identically on `main`); `mypy`.
<details>
<summary>proof_csharp.py (reproducible)</summary>
```python
"""Real behavior proof: run the real CodeAwareCompressor over a .NET repo."""
import pathlib
import statistics
import sys
import time
from headroom.transforms.code_compressor import (
CodeAwareCompressor,
CodeCompressorConfig,
)
try:
import tiktoken
ENC = tiktoken.get_encoding("cl100k_base")
def toks(s: str) -> int:
return len(ENC.encode(s, disallowed_special=()))
except Exception:
def toks(s: str) -> int:
return len(s) // 4
target = pathlib.Path(sys.argv[1])
comp = CodeAwareCompressor(CodeCompressorConfig(enable_ccr=False))
tot_before = tot_after = 0
n_files = n_compressed = n_valid = n_passthrough = n_inflated = 0
times_ms: list[float] = []
for f in sorted(target.rglob("*.cs")):
try:
code = f.read_text(encoding="utf-8-sig", errors="replace")
except OSError:
continue
t0 = time.perf_counter()
r = comp.compress(code, language="csharp")
times_ms.append((time.perf_counter() - t0) * 1000)
n_files += 1
b, a = toks(code), toks(r.compressed)
tot_before += b
tot_after += a
if r.compressed == code:
n_passthrough += 1
else:
n_compressed += 1
if r.syntax_valid:
n_valid += 1
if a > b:
n_inflated += 1
times_ms.sort()
p = lambda q: times_ms[min(int(len(times_ms) * q), len(times_ms) - 1)]
print(f"repo: {target.name} ({n_files} .cs files)")
print(f" tokens before: {tot_before:,} after: {tot_after:,} saved: {tot_before - tot_after:,} ({(1 - tot_after / tot_before) * 100:.1f}%)")
print(f" files compressed: {n_compressed} pass-through: {n_passthrough} inflated(>before): {n_inflated}")
print(f" syntax_valid: {n_valid}/{n_files}")
print(f" latency ms P50: {p(0.50):.1f} P95: {p(0.95):.1f} P99: {p(0.99):.1f} max: {times_ms[-1]:.1f} mean: {statistics.mean(times_ms):.1f}")
```
</details>
## 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
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A — terminal evidence above.
## Additional Notes
- Dependency justification: none added, none bumped; the `csharp`
grammar is inside the already-pinned `tree-sitter-language-pack==0.13.0`
wheel; `uv.lock` untouched.
- Architecture: malformed input passes through byte-identical; every
risky construct prefers the false negative (verbatim) over corruption;
invalid reassembly falls back to the original via the existing
validation gate (observed live); no new imports at module load; P99
<50ms on both proof repos.
- Known v1 limitations (deliberate false negatives, possible
follow-ups): expression-bodied members and property accessor bodies stay
verbatim; declarations inside `#if` regions stay verbatim.
- Related pre-existing finding, out of scope: C/C++ exhibit the same
`#if`-wrapper duplication on `main` (an `#if`-wrapped C++ class is
emitted twice, ratio 1.62). Happy to file separately.
- `mypy` unchecked above because I did not run it in my environment.
2026-07-12 19:54:38 +02:00
|
|
|
(
|
|
|
|
|
"csharp",
|
|
|
|
|
(
|
|
|
|
|
"namespace Acme\n"
|
|
|
|
|
"{\n"
|
|
|
|
|
" public class Calc\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" public int Compute(int x)\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" int a = x + 1;\n"
|
|
|
|
|
" int b = a * 2;\n"
|
|
|
|
|
" int c = b - 3;\n"
|
|
|
|
|
" int d = c / 4;\n"
|
|
|
|
|
" int e = d + 5;\n"
|
|
|
|
|
" return e;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
),
|
|
|
|
|
"public int Compute(int x)",
|
|
|
|
|
5,
|
|
|
|
|
"return e;",
|
|
|
|
|
"}\n}",
|
|
|
|
|
),
|
fix(code): compress class member containers (#1334)
## Description
CodeAwareCompressor used the same `body_node_types` config to find both
executable function bodies and class/impl member containers. That works
when those AST nodes happen to match, but it misses member containers
such as Java `class_body`, C++ `field_declaration_list`, and Rust
`declaration_list`, so class methods were returned essentially
uncompressed.
This adds an optional `class_body_node_types` override for class/impl
member containers and uses it only in class compression. It also skips
anonymous punctuation tokens while reconstructing class bodies and keeps
same-line C++ class semicolons attached to the compressed class
declaration.
Closes #1318
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added `LangConfig.class_body_node_types` for languages whose
class/impl member container differs from executable method-body nodes.
- Configured class member containers for JavaScript, TypeScript, Java,
C++, and Rust.
- Updated `_compress_class_ast` to use class-member containers, skip
anonymous punctuation children, and preserve C++ `};` output without
creating stray top-level semicolons.
- Added regression coverage proving class/impl methods compress for
JavaScript, TypeScript, Java, C++, and Rust.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ PYTHONPATH=. /tmp/headroom-1319-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py -q
collected 71 items
tests/test_transforms/test_code_compressor.py .......................... [ 36%]
............................................. [100%]
71 passed, 1 warning in 0.36s
$ /tmp/headroom-1319-venv/bin/python -m ruff check .
All checks passed!
$ /tmp/headroom-1319-venv/bin/python -m ruff format --check .
965 files already formatted
$ PYTHONPATH=. /tmp/headroom-1319-venv/bin/python -m mypy headroom
headroom/proxy/server.py:1186: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1257: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1261: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
pyproject.toml: note: unused section(s): module = ['mlx.*']
Success: no issues found in 394 source files
```
## Real Behavior Proof
- Environment: macOS arm64, Python 3.14.5, branch
`fix-code-compressor-class-members`, tree-sitter grammar pack installed
in `/tmp/headroom-1319-venv`, repo imported with `PYTHONPATH=.`.
- Exact command / steps: Reproduced class-method compression with
`CodeAwareCompressor(CodeCompressorConfig(enable_ccr=False,
min_tokens_for_compression=1, max_body_lines=1))` for Java/C++/Rust
before the fix, then reran the pytest/ruff/mypy commands listed above
after the patch.
- Observed result: Java/C++/Rust class methods now compress below 1.0
while `syntax_valid` remains true; C++ output preserves `};`; regression
coverage also verifies JavaScript/TypeScript class member containers.
- Not tested: Full repository pytest suite; local `uv run` editable
builds are blocked on this machine by native C++ header failures in
optional/native dependencies (`hnswlib` / Rust `esaxx-rs`), so
validation used a lightweight venv with `PYTHONPATH=.`.
## 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
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
Documentation and CHANGELOG updates are not applicable for this narrow
bug fix. The pytest warning shown above is from running without
`pytest-asyncio` in the lightweight verification venv (`asyncio_mode`
config is unknown there); it is unrelated to this change.
2026-06-23 15:41:36 -04:00
|
|
|
],
|
|
|
|
|
)
|
|
|
|
|
def test_compresses_methods_inside_class_member_containers(
|
|
|
|
|
self,
|
|
|
|
|
language,
|
|
|
|
|
code,
|
|
|
|
|
expected_signature,
|
|
|
|
|
expected_omitted_lines,
|
|
|
|
|
expected_removed_line,
|
|
|
|
|
expected_closing,
|
|
|
|
|
):
|
|
|
|
|
"""Class/impl member containers are distinct from executable method bodies."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=1,
|
|
|
|
|
max_body_lines=1,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language=language)
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage(language)
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert expected_signature in result.compressed
|
|
|
|
|
assert f"// [{expected_omitted_lines} lines omitted]" in result.compressed
|
|
|
|
|
assert expected_removed_line not in result.compressed
|
|
|
|
|
assert result.compressed.endswith(expected_closing)
|
|
|
|
|
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
def test_imports_preserved(self):
|
|
|
|
|
"""Imports are preserved in compressed output."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
preserve_imports=True,
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(5)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert "import os" in result.compressed
|
|
|
|
|
assert "from typing import" in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_signatures_preserved(self):
|
|
|
|
|
"""Function signatures are preserved."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
preserve_signatures=True,
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(3)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Should preserve function signatures
|
|
|
|
|
assert "def function_" in result.compressed
|
|
|
|
|
assert "arg:" in result.compressed or "(arg" in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_error_handlers_preserved(self):
|
|
|
|
|
"""Module-level try/except blocks are preserved."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
# Code with module-level try/except (not inside functions)
|
|
|
|
|
code = '''
|
|
|
|
|
import os
|
|
|
|
|
|
|
|
|
|
def setup():
|
|
|
|
|
"""Setup function."""
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
from optional_module import feature
|
|
|
|
|
except ImportError:
|
|
|
|
|
feature = None
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
|
"""Main function with long body."""
|
|
|
|
|
result = []
|
|
|
|
|
for i in range(100):
|
|
|
|
|
result.append(i)
|
|
|
|
|
return result
|
|
|
|
|
'''
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Module-level error handlers should be preserved
|
|
|
|
|
assert "try:" in result.compressed or "except" in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_syntax_verification(self):
|
|
|
|
|
"""Output syntax is verified as valid."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(5)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Verify the compressed output is valid Python
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
|
|
|
|
|
# Should be parseable
|
|
|
|
|
try:
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
except SyntaxError:
|
|
|
|
|
pytest.fail("Compressed output has invalid Python syntax")
|
|
|
|
|
|
fix(code): validate Python compressed syntax (#1302)
## Description
Fix a Python code-compression validity gap from #1233 where tree-sitter
parsing could mark compressed output as syntactically valid even when
Python compile-time syntax rules reject it.
This keeps `from __future__ import ...` statements in the
import-preservation bucket so they stay before executable definitions,
and adds Python `compile(..., "exec")` verification after `ast.parse`.
It also keeps the earlier conservative class-method decorator
indentation hardening from this branch.
Refs #1233.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Treat Python `future_import_statement` nodes as preserved imports.
- Verify Python compressed output with both `ast.parse` and
`compile(..., "exec")`.
- Preserve original source-line indentation for decorators attached to
class methods.
- Add a regression fixture covering `from __future__ import
annotations`, class decorators, property decorators, async methods, and
`match` statements.
- Add a direct regression assertion that future imports stay before
executable definitions.
- Document the user-visible fix in `CHANGELOG.md`.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ /tmp/headroom-issue-1233-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py::TestTreeSitterIntegration::test_python_future_import_stays_at_module_start -q
1 passed, 1 warning
$ /tmp/headroom-issue-1233-venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py -q
61 passed, 1 warning
$ uv run --extra dev ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
All checks passed!
$ uv run --extra dev ruff format --check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
2 files already formatted
$ git diff --check
# no output
```
## Real Behavior Proof
- Environment: macOS, Python 3.11.14, local checkout with `[code]`
dependencies installed in `/tmp/headroom-issue-1233-venv`.
- Exact command / steps: added
`test_python_future_import_stays_at_module_start`, ran it before the fix
to confirm the compressed output failure, then reran the focused test
and full `tests/test_transforms/test_code_compressor.py` after the
patch.
- Observed result: before this patch, the regression fixture produced
compressed Python with `from __future__ import annotations` after
class/function definitions. `result.syntax_valid` was `True`, but
`compile(result.compressed, "<test>", "exec")` failed with `SyntaxError:
from __future__ imports must occur at the beginning of the file`. After
this patch, the focused regression and full code-compressor test file
pass locally, and the regression now directly asserts that the future
import appears before executable definitions.
- Not tested: full repository pytest, `mypy headroom`, and a broad
corpus run over third-party source files.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
## Checklist
- [x] My code follows the project style guidelines
- [x] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
This PR is now scoped to the stable compile-time failure path in #1233.
The broader syntax-failure rate from the issue may still need
corpus-level follow-up.
Co-authored-by: JD Davis <mxjerrett@gmail.com>
2026-06-24 03:41:14 +08:00
|
|
|
def test_python_future_import_stays_at_module_start(self):
|
|
|
|
|
"""Compressed Python keeps future imports before executable statements."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
target_compression_rate=0.2,
|
|
|
|
|
max_body_lines=3,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = textwrap.dedent(
|
|
|
|
|
"""
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
from typing import Any, Callable, Iterable
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def traced(label: str) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
|
|
|
|
|
def decorate(fn: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
|
|
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
|
|
|
return await fn(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
return wrapper
|
|
|
|
|
|
|
|
|
|
return decorate
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@dataclass(slots=True)
|
|
|
|
|
class Event:
|
|
|
|
|
kind: str
|
|
|
|
|
payload: dict[str, Any]
|
|
|
|
|
retries: int = 0
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
def important(self) -> bool:
|
|
|
|
|
return self.kind in {"error", "retry"} or self.retries > 2
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class EventRouter:
|
|
|
|
|
def __init__(self, sinks: dict[str, Callable[[Event], Any]]) -> None:
|
|
|
|
|
self.sinks = sinks
|
|
|
|
|
self.history: list[tuple[str, bool]] = []
|
|
|
|
|
|
|
|
|
|
@traced("route")
|
|
|
|
|
async def route(self, events: Iterable[Event]) -> list[str]:
|
|
|
|
|
accepted: list[str] = []
|
|
|
|
|
for event in events:
|
|
|
|
|
match event:
|
|
|
|
|
case Event(kind="error", payload={"code": code, "message": msg}, retries=r) if r > 1:
|
|
|
|
|
destination = "pager"
|
|
|
|
|
accepted.append(f"{destination}:{code}:{msg}")
|
|
|
|
|
case Event(kind=kind, payload=payload) if (route := payload.get("route")):
|
|
|
|
|
destination = str(route)
|
|
|
|
|
accepted.append(f"{destination}:{kind}")
|
|
|
|
|
case _:
|
|
|
|
|
destination = "dead_letter"
|
|
|
|
|
accepted.append(destination)
|
|
|
|
|
|
|
|
|
|
self.history.append((destination, event.important))
|
|
|
|
|
|
|
|
|
|
return [item for item in accepted if item]
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
future_import_index = result.compressed.index("from __future__ import annotations")
|
|
|
|
|
first_executable_index = min(
|
|
|
|
|
result.compressed.index("@dataclass"),
|
|
|
|
|
result.compressed.index("def traced"),
|
|
|
|
|
result.compressed.index("class EventRouter"),
|
|
|
|
|
)
|
|
|
|
|
assert future_import_index < first_executable_index
|
|
|
|
|
try:
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
except SyntaxError as exc:
|
|
|
|
|
pytest.fail(f"Compressed output has invalid Python syntax: {exc}\n{result.compressed}")
|
|
|
|
|
|
Add AST-based code compression and custom model configuration
CodeAwareCompressor:
- Tree-sitter based AST parsing for Python, JS, TS, Go, Rust, Java, C, C++
- Preserves imports, signatures, type annotations, error handlers
- Guarantees syntactically valid output
- Uses tree-sitter-language-pack for broad language support
ContentRouter:
- Intelligent compression orchestrator
- Auto-routes content to optimal compressor based on type detection
- Source hint support for high-confidence routing
Custom Model Configuration:
- HEADROOM_MODEL_LIMITS env var and ~/.headroom/models.json support
- Pattern-based inference for unknown models (opus/sonnet/haiku tiers)
- Support for Claude 4.5, Claude 4, o3, o3-mini
- Graceful fallback - never crashes on unknown models
2026-01-14 13:46:55 -08:00
|
|
|
def test_tree_sitter_loaded_after_compression(self):
|
|
|
|
|
"""Parser is loaded after compression."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
|
|
|
|
|
# Ensure clean state
|
|
|
|
|
unload_tree_sitter()
|
|
|
|
|
assert is_tree_sitter_loaded() is False
|
|
|
|
|
|
|
|
|
|
# Compress should load parser
|
|
|
|
|
code = generate_python_code(3)
|
|
|
|
|
compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert is_tree_sitter_loaded() is True
|
|
|
|
|
|
|
|
|
|
def test_unload_clears_parsers(self):
|
|
|
|
|
"""unload_tree_sitter clears loaded parsers."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
|
|
|
|
|
# Load a parser
|
|
|
|
|
code = generate_python_code(3)
|
|
|
|
|
compressor.compress(code, language="python")
|
|
|
|
|
assert is_tree_sitter_loaded() is True
|
|
|
|
|
|
|
|
|
|
# Unload
|
|
|
|
|
result = unload_tree_sitter()
|
|
|
|
|
assert result is True
|
|
|
|
|
assert is_tree_sitter_loaded() is False
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestDocstringModes
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter-languages not installed")
|
|
|
|
|
class TestDocstringModes:
|
|
|
|
|
"""Tests for different docstring handling modes."""
|
|
|
|
|
|
|
|
|
|
def test_docstring_mode_full(self):
|
|
|
|
|
"""FULL mode preserves entire docstrings."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
docstring_mode=DocstringMode.FULL,
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(2)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Should preserve full docstrings
|
|
|
|
|
assert "Args:" in result.compressed or "Returns:" in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_docstring_mode_first_line(self):
|
|
|
|
|
"""FIRST_LINE mode keeps only first line of docstring."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
docstring_mode=DocstringMode.FIRST_LINE,
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
code = generate_python_code(2)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Multi-line docstring details should be removed
|
|
|
|
|
# This is implementation-dependent
|
|
|
|
|
assert result.compressed is not None
|
|
|
|
|
|
|
|
|
|
def test_docstring_mode_remove(self):
|
|
|
|
|
"""REMOVE mode removes all docstrings."""
|
|
|
|
|
config = CodeCompressorConfig(
|
|
|
|
|
docstring_mode=DocstringMode.REMOVE,
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
max_body_lines=2, # Low threshold to trigger compression
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
compressor = CodeAwareCompressor(config)
|
|
|
|
|
# Larger function to trigger body compression
|
|
|
|
|
code = '''
|
|
|
|
|
def example():
|
|
|
|
|
"""This docstring should be removed."""
|
|
|
|
|
x = 1
|
|
|
|
|
y = 2
|
|
|
|
|
z = 3
|
|
|
|
|
result = x + y + z
|
|
|
|
|
for i in range(10):
|
|
|
|
|
result += i
|
|
|
|
|
return result
|
|
|
|
|
'''
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Docstring should be removed when REMOVE mode is active
|
|
|
|
|
assert "This docstring should be removed" not in result.compressed
|
2026-03-03 14:18:04 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# TestSemanticSymbolImportance
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _payment_processing_code() -> str:
|
|
|
|
|
"""Python code with varying symbol importance for testing."""
|
|
|
|
|
return '''
|
|
|
|
|
import os
|
|
|
|
|
from typing import List, Optional
|
|
|
|
|
|
|
|
|
|
def process_payment(order, config):
|
|
|
|
|
"""Process a payment through the pipeline."""
|
|
|
|
|
validated = validate_order(order)
|
|
|
|
|
if not validated.is_valid:
|
|
|
|
|
return PaymentResult(status='failed')
|
|
|
|
|
charge = charge_customer(order.customer, order.total)
|
|
|
|
|
receipt = generate_receipt(charge)
|
|
|
|
|
send_confirmation(order.customer.email, receipt)
|
|
|
|
|
update_inventory(order.items)
|
|
|
|
|
log_transaction(charge.transaction_id)
|
|
|
|
|
notify_warehouse(order)
|
|
|
|
|
return PaymentResult(status='success', receipt=receipt)
|
|
|
|
|
|
|
|
|
|
def validate_order(order):
|
|
|
|
|
"""Validate an order before processing."""
|
|
|
|
|
if not order.items:
|
|
|
|
|
return ValidationResult(False, ['No items'])
|
|
|
|
|
total = sum(item.price for item in order.items)
|
|
|
|
|
if total <= 0:
|
|
|
|
|
return ValidationResult(False, ['Invalid total'])
|
|
|
|
|
if not order.customer:
|
|
|
|
|
return ValidationResult(False, ['No customer'])
|
|
|
|
|
return ValidationResult(True, [])
|
|
|
|
|
|
|
|
|
|
def charge_customer(customer, amount):
|
|
|
|
|
"""Charge the customer."""
|
|
|
|
|
gateway = get_payment_gateway()
|
|
|
|
|
response = gateway.charge(customer.card, amount)
|
|
|
|
|
if not response.success:
|
|
|
|
|
raise PaymentError(response.error)
|
|
|
|
|
return response
|
|
|
|
|
|
|
|
|
|
def generate_receipt(charge):
|
|
|
|
|
"""Generate a receipt for the charge."""
|
|
|
|
|
template = load_template('receipt')
|
|
|
|
|
return template.render(charge=charge)
|
|
|
|
|
|
|
|
|
|
def _format_log_entry(entry):
|
|
|
|
|
"""Format a log entry for internal use. Never called."""
|
|
|
|
|
timestamp = entry.get('ts', '')
|
|
|
|
|
level = entry.get('level', 'INFO')
|
|
|
|
|
message = entry.get('msg', '')
|
|
|
|
|
source = entry.get('source', 'unknown')
|
|
|
|
|
formatted = f'[{timestamp}] {level}: {message} ({source})'
|
|
|
|
|
return formatted.strip()
|
|
|
|
|
|
|
|
|
|
def _dead_helper():
|
|
|
|
|
"""Never called anywhere in this file."""
|
|
|
|
|
x = 1
|
|
|
|
|
y = 2
|
|
|
|
|
z = 3
|
|
|
|
|
result = x + y + z
|
|
|
|
|
for i in range(100):
|
|
|
|
|
result += i
|
|
|
|
|
return result
|
|
|
|
|
'''
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter-languages not installed")
|
|
|
|
|
class TestSemanticSymbolImportance:
|
|
|
|
|
"""Tests for semantic symbol importance analysis and variable compression."""
|
|
|
|
|
|
|
|
|
|
def _make_compressor(self, **overrides):
|
|
|
|
|
defaults = {
|
|
|
|
|
"min_tokens_for_compression": 10,
|
|
|
|
|
"max_body_lines": 3,
|
|
|
|
|
"enable_ccr": False,
|
|
|
|
|
"semantic_analysis": True,
|
|
|
|
|
}
|
|
|
|
|
defaults.update(overrides)
|
|
|
|
|
return CodeAwareCompressor(CodeCompressorConfig(**defaults))
|
|
|
|
|
|
|
|
|
|
def test_symbol_scores_populated(self):
|
|
|
|
|
"""Compression result includes symbol importance scores."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
assert result.symbol_scores
|
|
|
|
|
assert "process_payment" in result.symbol_scores
|
|
|
|
|
assert "validate_order" in result.symbol_scores
|
|
|
|
|
assert "_dead_helper" in result.symbol_scores
|
|
|
|
|
|
|
|
|
|
def test_called_functions_score_higher_than_dead_code(self):
|
|
|
|
|
"""Functions called by others score higher than unused functions."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
# validate_order is called by process_payment — should score higher
|
|
|
|
|
assert result.symbol_scores["validate_order"] > result.symbol_scores["_dead_helper"]
|
|
|
|
|
assert result.symbol_scores["charge_customer"] > result.symbol_scores["_dead_helper"]
|
|
|
|
|
|
|
|
|
|
def test_public_symbols_score_higher_than_private(self):
|
|
|
|
|
"""Public functions (no leading _) score higher than private ones."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
code = '''
|
|
|
|
|
def public_func():
|
|
|
|
|
"""A public function."""
|
|
|
|
|
x = 1
|
|
|
|
|
y = 2
|
|
|
|
|
z = 3
|
|
|
|
|
result = x + y + z
|
|
|
|
|
for i in range(10):
|
|
|
|
|
result += i
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
def _private_func():
|
|
|
|
|
"""A private function."""
|
|
|
|
|
x = 1
|
|
|
|
|
y = 2
|
|
|
|
|
z = 3
|
|
|
|
|
result = x + y + z
|
|
|
|
|
for i in range(10):
|
|
|
|
|
result += i
|
|
|
|
|
return result
|
|
|
|
|
'''
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.symbol_scores["public_func"] > result.symbol_scores["_private_func"]
|
|
|
|
|
|
|
|
|
|
def test_dead_code_compressed_to_signature_only(self):
|
|
|
|
|
"""Functions with score < 0.1 are compressed to signature + docstring only."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
# _dead_helper has 0 references, private → score 0.0
|
|
|
|
|
assert result.symbol_scores["_dead_helper"] < 0.1
|
|
|
|
|
|
|
|
|
|
# Body should be fully omitted
|
|
|
|
|
assert "_dead_helper" in result.compressed
|
|
|
|
|
# Should NOT contain body content
|
|
|
|
|
assert "range(100)" not in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_referenced_functions_keep_more_body(self):
|
|
|
|
|
"""Higher-scored functions get more body lines from the budget."""
|
|
|
|
|
# Use a generous target rate so there IS budget to distribute
|
|
|
|
|
compressor = self._make_compressor(target_compression_rate=0.7)
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
compressed = result.compressed
|
|
|
|
|
# With 70% target, high-scoring functions should retain body
|
|
|
|
|
# while low-scoring ones get less. validate_order is referenced
|
|
|
|
|
# and public (high score) so should keep some body.
|
|
|
|
|
# _dead_helper has lowest score so should get least body.
|
|
|
|
|
# Count body lines per function as a proxy for retention
|
|
|
|
|
lines = compressed.split("\n")
|
|
|
|
|
in_validate = False
|
|
|
|
|
in_dead = False
|
|
|
|
|
validate_body = 0
|
|
|
|
|
dead_body = 0
|
|
|
|
|
for line in lines:
|
|
|
|
|
if "def validate_order" in line:
|
|
|
|
|
in_validate = True
|
|
|
|
|
in_dead = False
|
|
|
|
|
continue
|
|
|
|
|
elif "def _dead_helper" in line:
|
|
|
|
|
in_dead = True
|
|
|
|
|
in_validate = False
|
|
|
|
|
continue
|
|
|
|
|
elif line.startswith("def ") or (line.startswith("class ") and ":" in line):
|
|
|
|
|
in_validate = False
|
|
|
|
|
in_dead = False
|
|
|
|
|
continue
|
|
|
|
|
if in_validate and line.strip() and not line.strip().startswith('"""'):
|
|
|
|
|
validate_body += 1
|
|
|
|
|
if in_dead and line.strip() and not line.strip().startswith('"""'):
|
|
|
|
|
dead_body += 1
|
|
|
|
|
|
|
|
|
|
assert validate_body >= dead_body
|
|
|
|
|
|
|
|
|
|
def test_omitted_comment_includes_calls(self):
|
|
|
|
|
"""Omitted comment includes call information when available."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
# process_payment calls validate_order, charge_customer, generate_receipt
|
|
|
|
|
# These should appear in the omitted comment
|
|
|
|
|
compressed = result.compressed
|
|
|
|
|
if "lines omitted" in compressed:
|
|
|
|
|
# Find omitted comments and check for calls info
|
|
|
|
|
for line in compressed.split("\n"):
|
|
|
|
|
if "process_payment" not in line and "lines omitted" in line:
|
|
|
|
|
continue
|
|
|
|
|
if "lines omitted; calls:" in line:
|
|
|
|
|
assert "validate_order" in line or "charge_customer" in line
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
def test_semantic_analysis_disabled(self):
|
|
|
|
|
"""When semantic_analysis=False, all functions get uniform compression."""
|
|
|
|
|
compressor_with = self._make_compressor(semantic_analysis=True)
|
|
|
|
|
compressor_without = self._make_compressor(semantic_analysis=False)
|
|
|
|
|
|
|
|
|
|
code = _payment_processing_code()
|
|
|
|
|
result_with = compressor_with.compress(code, language="python")
|
|
|
|
|
result_without = compressor_without.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# Without semantic analysis, no symbol scores
|
|
|
|
|
assert result_without.symbol_scores == {}
|
|
|
|
|
|
|
|
|
|
# With semantic analysis, dead code is compressed more aggressively
|
|
|
|
|
# _dead_helper body should NOT appear with semantic analysis
|
|
|
|
|
assert "range(100)" not in result_with.compressed
|
|
|
|
|
# But with uniform compression (no semantic), body lines ARE kept
|
|
|
|
|
assert "x = 1" in result_without.compressed
|
|
|
|
|
|
|
|
|
|
def test_summary_includes_semantic_info(self):
|
|
|
|
|
"""Summary includes semantic analysis information."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
summary = result.summary
|
|
|
|
|
if result.symbol_scores:
|
|
|
|
|
low_count = sum(1 for s in result.symbol_scores.values() if s < 0.1)
|
|
|
|
|
if low_count > 0:
|
|
|
|
|
assert "low-importance" in summary
|
|
|
|
|
|
|
|
|
|
def test_dunder_methods_get_boost(self):
|
|
|
|
|
"""Dunder methods (__init__, etc.) get importance boost."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
code = '''
|
|
|
|
|
class MyClass:
|
|
|
|
|
"""A class."""
|
|
|
|
|
def __init__(self, value):
|
|
|
|
|
"""Initialize."""
|
|
|
|
|
self.value = value
|
|
|
|
|
self.processed = False
|
|
|
|
|
self.results = []
|
|
|
|
|
self.cache = {}
|
|
|
|
|
self.errors = []
|
|
|
|
|
for i in range(10):
|
|
|
|
|
self.results.append(i)
|
|
|
|
|
|
|
|
|
|
def _setup_cache(self):
|
|
|
|
|
"""Internal setup."""
|
|
|
|
|
x = 1
|
|
|
|
|
y = 2
|
|
|
|
|
z = 3
|
|
|
|
|
result = x + y + z
|
|
|
|
|
for i in range(10):
|
|
|
|
|
result += i
|
|
|
|
|
return result
|
|
|
|
|
'''
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
# __init__ should score higher than _setup_cache
|
|
|
|
|
if "__init__" in result.symbol_scores and "_setup_cache" in result.symbol_scores:
|
|
|
|
|
assert result.symbol_scores["__init__"] > result.symbol_scores["_setup_cache"]
|
|
|
|
|
|
|
|
|
|
def test_javascript_importance(self):
|
|
|
|
|
"""Symbol importance works for JavaScript code."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
code = """
|
|
|
|
|
import { db } from './database';
|
|
|
|
|
|
|
|
|
|
function processUser(userId) {
|
|
|
|
|
const user = fetchUser(userId);
|
|
|
|
|
const profile = buildProfile(user);
|
|
|
|
|
sendNotification(user.email, profile);
|
|
|
|
|
logAction('process', userId);
|
|
|
|
|
updateMetrics('user_processed');
|
|
|
|
|
return { user, profile };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function fetchUser(id) {
|
|
|
|
|
const result = db.query('SELECT * FROM users WHERE id = ?', [id]);
|
|
|
|
|
if (!result) {
|
|
|
|
|
throw new Error('User not found');
|
|
|
|
|
}
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function buildProfile(user) {
|
|
|
|
|
const prefs = loadPreferences(user.id);
|
|
|
|
|
return { ...user, preferences: prefs };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _internalDebug(msg) {
|
|
|
|
|
const ts = Date.now();
|
|
|
|
|
const formatted = `[${ts}] DEBUG: ${msg}`;
|
|
|
|
|
console.log(formatted);
|
|
|
|
|
return formatted;
|
|
|
|
|
}
|
|
|
|
|
"""
|
|
|
|
|
result = compressor.compress(code, language="javascript")
|
|
|
|
|
|
|
|
|
|
assert result.symbol_scores
|
|
|
|
|
# fetchUser is called by processUser — should score higher than _internalDebug
|
|
|
|
|
if "fetchUser" in result.symbol_scores and "_internalDebug" in result.symbol_scores:
|
|
|
|
|
assert result.symbol_scores["fetchUser"] > result.symbol_scores["_internalDebug"]
|
|
|
|
|
|
|
|
|
|
def test_syntax_still_valid_with_importance(self):
|
|
|
|
|
"""Compressed output with importance remains syntactically valid."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
|
|
|
|
|
# Should be parseable as Python
|
|
|
|
|
try:
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
except SyntaxError:
|
|
|
|
|
pytest.fail("Semantic compression produced invalid Python syntax")
|
|
|
|
|
|
|
|
|
|
def test_empty_code_no_crash(self):
|
|
|
|
|
"""Importance analysis handles empty code gracefully."""
|
|
|
|
|
compressor = self._make_compressor()
|
|
|
|
|
result = compressor.compress("", language="python")
|
|
|
|
|
|
|
|
|
|
assert result.symbol_scores == {}
|
|
|
|
|
|
|
|
|
|
def test_config_default_semantic_analysis_enabled(self):
|
|
|
|
|
"""semantic_analysis is True by default in config."""
|
|
|
|
|
config = CodeCompressorConfig()
|
|
|
|
|
assert config.semantic_analysis is True
|
fix(code): verify a real parse in tree-sitter availability check (#1231) (#1299)
## Description
`is_tree_sitter_available()` / `_check_tree_sitter_available()` in
`headroom/transforms/code_compressor.py` return `True` based on
importing `tree_sitter_language_pack` alone, without ever constructing a
parser or attempting a parse. When the installed pack/parser combination
is ABI-incompatible, `get_parser`/`parse` raises at runtime; the caller
catches it and silently falls back to the lossy text compressor, while
the availability flag and startup banner still report code-aware as on.
This is the defensive half that the `<1.0` pin in #1234 does not cover:
if that cap is ever lifted, the availability signal silently lies again.
Follow-up to #1231.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Make `_check_tree_sitter_available()` construct a parser and parse a
tiny snippet, returning `True` only if it yields a real `module` AST
instead of trusting an import.
- Add `_tree_sitter_importable()` for the cheap import-only probe, and
use it to guard parser construction so the real-parse check cannot
recurse.
- Add tests asserting the check is `False` when parsing raises and
`True` on a real parse, plus that AST compression runs for python/rust
without falling back.
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
# pytest tests/test_transforms/test_code_compressor.py -> passed locally (tree-sitter-language-pack 0.13.0)
# ruff check . and ruff format --check . pass locally on the rebased branch.
# Full pytest suite / mypy not run locally; left to CI.
```
## Real Behavior Proof
- Environment: local repo on tree-sitter-language-pack 0.13.0,
tree-sitter 0.25.2, Python 3.12, Linux
- Exact command / steps: call `is_tree_sitter_available()`, then run
`pytest tests/test_transforms/test_code_compressor.py`
- Observed result: with a working pack the probe parses and returns
`True` (code-aware runs, strategy `CODE_AWARE` rather than the kompress
fallback); the new
`test_check_tree_sitter_available_false_when_parse_broken` confirms that
when parsing raises the check now returns `False` instead of the old
import-only `True`, so the lossy fallback is no longer entered silently.
- Not tested: reproducing the specific ABI-incompatible 1.x pack combo
against a live install (covered instead by a mocked broken parse in the
test)
## 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
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
2026-06-23 12:00:57 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
# Regression: tree-sitter ABI mismatch (real AST must run, no silent fallback)
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter grammar pack not installed")
|
|
|
|
|
class TestRealASTRuns:
|
|
|
|
|
"""Guards against the regression where the code-aware compressor silently
|
|
|
|
|
fell back to a lossy stripper because ``_get_parser`` built a stock
|
|
|
|
|
``tree_sitter.Parser`` and assigned it a foreign grammar-pack ``Language``
|
|
|
|
|
(raising ``TypeError`` that was swallowed into a fallback).
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def _compressor(self):
|
|
|
|
|
return CodeAwareCompressor(
|
|
|
|
|
CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=10,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
fix(code-compressor): recover valid Python rewrites after local syntax rejection (#2202)
## Description
A compile-invalid Python definition rewrite currently makes
`CodeAwareCompressor` discard every otherwise valid rewrite in the file
and return the original source at 0 percent reduction. The existing
whole-file safety guard stays in place, while a Python-only recovery
replay now preserves the rejected definition and keeps independent valid
compression.
The recovery reuses the current Python validation authority in
`ast.parse()` plus `compile()`, runs only after the first assembled
module already fails `_verify_syntax()`, and stays out of non-Python
paths.
Closes #1233
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added a Python-only recovery replay after the first assembled module
fails syntax validation.
- Preserved only the invalid function or class rewrite while allowing
independent valid definitions to remain compressed.
- Kept the existing whole-file syntax guard and original-source fallback
as the terminal safety check.
- Added focused invalid-node, valid-modern-syntax, and fail-safe
coverage.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid
tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python
-v`)
- [x] Linting passes (`uv run ruff check
headroom/transforms/code_compressor.py
tests/test_transforms/test_code_compressor.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v
5 passed in 0.34s
uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
All checks passed!
uv run ruff format headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py --check
2 files already formatted
```
## Real Behavior Proof
- Environment: Windows, synced `uv` environment with `dev` and `code`
extras installed
- Exact command / steps: run the focused invalid-node regression through
public `compress(..., language="python")`
- Observed result: `1 passed in 0.19s`; the invalid candidate stays
original, the neighboring valid candidate remains compressed, and
`headroom-PR-TARGET-1233-PROOF.md` records the base `ratio=1.0`
whole-file rollback against the fixed head behavior.
- Not tested: the stale future-import mismatch discussed in the old
issue comment, already covered on current main
## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- The stale future-import comment on #1233 is not the live slice here;
current main already validates Python with `compile()` and already
covers that ordering case.
- This fix keeps the existing whole-file fail-safe and does not broaden
into cross-language recovery or new syntax models.
- `CHANGELOG.md` remains unchanged because Headroom generates release
notes from conventional commits.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 23:19:46 -04:00
|
|
|
def _recovery_compressor(self, **overrides):
|
|
|
|
|
defaults = {
|
|
|
|
|
"min_tokens_for_compression": 1,
|
|
|
|
|
"max_body_lines": 2,
|
|
|
|
|
"enable_ccr": False,
|
|
|
|
|
"semantic_analysis": False,
|
|
|
|
|
}
|
|
|
|
|
defaults.update(overrides)
|
|
|
|
|
return CodeAwareCompressor(CodeCompressorConfig(**defaults))
|
|
|
|
|
|
|
|
|
|
def _python_recovery_fixture(self) -> str:
|
|
|
|
|
return textwrap.dedent(
|
|
|
|
|
"""\
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
|
|
|
|
def expand_search_roots(user_root: str) -> list[Path]:
|
|
|
|
|
root = Path(user_root)
|
|
|
|
|
candidates = [root]
|
|
|
|
|
for child in root.iterdir():
|
|
|
|
|
candidates.append(child.resolve())
|
|
|
|
|
return candidates
|
|
|
|
|
|
|
|
|
|
def load_user_overrides(config_path: str) -> dict[str, str]:
|
|
|
|
|
config: dict[str, str] = {}
|
|
|
|
|
for line in Path(config_path).read_text().splitlines():
|
|
|
|
|
if "=" in line:
|
|
|
|
|
key, value = line.split("=", 1)
|
|
|
|
|
config[key.strip()] = value.strip()
|
|
|
|
|
return config
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
fix(code): verify a real parse in tree-sitter availability check (#1231) (#1299)
## Description
`is_tree_sitter_available()` / `_check_tree_sitter_available()` in
`headroom/transforms/code_compressor.py` return `True` based on
importing `tree_sitter_language_pack` alone, without ever constructing a
parser or attempting a parse. When the installed pack/parser combination
is ABI-incompatible, `get_parser`/`parse` raises at runtime; the caller
catches it and silently falls back to the lossy text compressor, while
the availability flag and startup banner still report code-aware as on.
This is the defensive half that the `<1.0` pin in #1234 does not cover:
if that cap is ever lifted, the availability signal silently lies again.
Follow-up to #1231.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Make `_check_tree_sitter_available()` construct a parser and parse a
tiny snippet, returning `True` only if it yields a real `module` AST
instead of trusting an import.
- Add `_tree_sitter_importable()` for the cheap import-only probe, and
use it to guard parser construction so the real-parse check cannot
recurse.
- Add tests asserting the check is `False` when parsing raises and
`True` on a real parse, plus that AST compression runs for python/rust
without falling back.
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
# pytest tests/test_transforms/test_code_compressor.py -> passed locally (tree-sitter-language-pack 0.13.0)
# ruff check . and ruff format --check . pass locally on the rebased branch.
# Full pytest suite / mypy not run locally; left to CI.
```
## Real Behavior Proof
- Environment: local repo on tree-sitter-language-pack 0.13.0,
tree-sitter 0.25.2, Python 3.12, Linux
- Exact command / steps: call `is_tree_sitter_available()`, then run
`pytest tests/test_transforms/test_code_compressor.py`
- Observed result: with a working pack the probe parses and returns
`True` (code-aware runs, strategy `CODE_AWARE` rather than the kompress
fallback); the new
`test_check_tree_sitter_available_false_when_parse_broken` confirms that
when parsing raises the check now returns `False` instead of the old
import-only `True`, so the lossy fallback is no longer entered silently.
- Not tested: reproducing the specific ABI-incompatible 1.x pack combo
against a live install (covered instead by a mocked broken parse in the
test)
## 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
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
2026-06-23 12:00:57 +08:00
|
|
|
def test_get_parser_returns_stock_node_api(self):
|
|
|
|
|
"""The parser must yield nodes with the stock tree_sitter property API
|
|
|
|
|
that the tree-walking code relies on (``.type``/``.children``/...)."""
|
|
|
|
|
from headroom.transforms.code_compressor import _get_parser
|
|
|
|
|
|
|
|
|
|
parser = _get_parser("python")
|
|
|
|
|
tree = parser.parse(b"def foo(x):\n return x + 1\n")
|
|
|
|
|
root = tree.root_node
|
|
|
|
|
|
|
|
|
|
# Property access (NOT method calls) — the old pack binding exposed
|
|
|
|
|
# methods like ``.kind()`` which would break every call site.
|
|
|
|
|
assert root.type == "module"
|
|
|
|
|
assert root.child_count >= 1
|
|
|
|
|
assert isinstance(root.children, list)
|
|
|
|
|
|
|
|
|
|
func = root.children[0]
|
|
|
|
|
assert func.type == "function_definition"
|
|
|
|
|
assert isinstance(func.start_byte, int)
|
|
|
|
|
assert isinstance(func.end_byte, int)
|
|
|
|
|
# start_point must be index-able like a (row, col) tuple.
|
|
|
|
|
assert func.start_point[0] == 0
|
|
|
|
|
assert b"def foo" in func.text
|
|
|
|
|
|
|
|
|
|
def test_check_tree_sitter_available_verifies_real_parse(self):
|
|
|
|
|
"""``_check_tree_sitter_available`` must only return True when an actual
|
|
|
|
|
parse succeeds — not merely when the package imports."""
|
|
|
|
|
import headroom.transforms.code_compressor as cc
|
|
|
|
|
|
|
|
|
|
cc._tree_sitter_available = None # reset memoized result
|
|
|
|
|
assert cc._check_tree_sitter_available() is True
|
|
|
|
|
|
|
|
|
|
def test_check_tree_sitter_available_false_when_parse_broken(self):
|
|
|
|
|
"""If parsing raises (e.g. the old foreign-Language bug), availability
|
|
|
|
|
must report False instead of green-lighting the broken path."""
|
|
|
|
|
import headroom.transforms.code_compressor as cc
|
|
|
|
|
|
|
|
|
|
cc._tree_sitter_available = None
|
|
|
|
|
with patch.object(cc, "_get_parser", side_effect=TypeError("boom")):
|
|
|
|
|
assert cc._check_tree_sitter_available() is False
|
|
|
|
|
cc._tree_sitter_available = None # reset for other tests
|
|
|
|
|
|
|
|
|
|
def test_ast_runs_for_python_no_fallback(self):
|
|
|
|
|
"""A supported language must be compressed via real AST, not the
|
|
|
|
|
UNKNOWN-language Kompress fallback."""
|
|
|
|
|
result = self._compressor().compress(_payment_processing_code(), language="python")
|
|
|
|
|
|
|
|
|
|
# The fallback path forces language=UNKNOWN and syntax_valid=False.
|
|
|
|
|
# Real AST keeps the detected language and guarantees valid syntax.
|
|
|
|
|
assert result.language == CodeLanguage.PYTHON
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
|
|
|
|
|
def test_ast_preserves_structure_for_python(self):
|
|
|
|
|
"""AST output retains signatures/scopes/imports (unlike the old
|
|
|
|
|
whitespace garble)."""
|
|
|
|
|
code = (
|
|
|
|
|
"import math\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"def compute(values):\n"
|
|
|
|
|
" total = 0\n"
|
|
|
|
|
" for v in values:\n"
|
|
|
|
|
" total += v * v\n"
|
|
|
|
|
" total -= 1\n"
|
|
|
|
|
" total *= 2\n"
|
|
|
|
|
" total //= 3\n"
|
|
|
|
|
" return math.sqrt(total)\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.PYTHON
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
# Structure markers survive compression.
|
|
|
|
|
assert "import math" in result.compressed
|
|
|
|
|
assert "def compute(values):" in result.compressed
|
|
|
|
|
# Output is still valid Python.
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
|
fix(code-compressor): recover valid Python rewrites after local syntax rejection (#2202)
## Description
A compile-invalid Python definition rewrite currently makes
`CodeAwareCompressor` discard every otherwise valid rewrite in the file
and return the original source at 0 percent reduction. The existing
whole-file safety guard stays in place, while a Python-only recovery
replay now preserves the rejected definition and keeps independent valid
compression.
The recovery reuses the current Python validation authority in
`ast.parse()` plus `compile()`, runs only after the first assembled
module already fails `_verify_syntax()`, and stays out of non-Python
paths.
Closes #1233
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added a Python-only recovery replay after the first assembled module
fails syntax validation.
- Preserved only the invalid function or class rewrite while allowing
independent valid definitions to remain compressed.
- Kept the existing whole-file syntax guard and original-source fallback
as the terminal safety check.
- Added focused invalid-node, valid-modern-syntax, and fail-safe
coverage.
## Testing
- [x] Unit tests pass (`uv run pytest
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid
tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input
tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python
-v`)
- [x] Linting passes (`uv run ruff check
headroom/transforms/code_compressor.py
tests/test_transforms/test_code_compressor.py`)
- [ ] Type checking passes (`uv run mypy headroom`)
- [x] New tests added for new functionality when applicable
- [ ] Manual testing performed
### Test Output
```text
uv run pytest tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_invalid_node_falls_back_locally tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_does_not_block_valid_modern_syntax tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_python_recovery_still_returns_original_when_all_candidates_invalid tests/test_transforms/test_code_compressor.py::TestEdgeCases::test_syntax_errors_in_input tests/test_transforms/test_code_compressor.py::TestRealASTRuns::test_ast_preserves_structure_for_python -v
5 passed in 0.34s
uv run ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
All checks passed!
uv run ruff format headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py --check
2 files already formatted
```
## Real Behavior Proof
- Environment: Windows, synced `uv` environment with `dev` and `code`
extras installed
- Exact command / steps: run the focused invalid-node regression through
public `compress(..., language="python")`
- Observed result: `1 passed in 0.19s`; the invalid candidate stays
original, the neighboring valid candidate remains compressed, and
`headroom-PR-TARGET-1233-PROOF.md` records the base `ratio=1.0`
whole-file rollback against the fixed head behavior.
- Not tested: the stale future-import mismatch discussed in the old
issue comment, already covered on current main
## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Additional Notes
- The stale future-import comment on #1233 is not the live slice here;
current main already validates Python with `compile()` and already
covers that ordering case.
- This fix keeps the existing whole-file fail-safe and does not broaden
into cross-language recovery or new syntax models.
- `CHANGELOG.md` remains unchanged because Headroom generates release
notes from conventional commits.
---------
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-07-14 23:19:46 -04:00
|
|
|
def test_python_invalid_node_falls_back_locally(self):
|
|
|
|
|
"""One invalid Python rewrite must preserve only that definition."""
|
|
|
|
|
compressor = self._recovery_compressor()
|
|
|
|
|
code = self._python_recovery_fixture()
|
|
|
|
|
original_compress_function_ast = compressor._compress_function_ast
|
|
|
|
|
|
|
|
|
|
def _patched(node, code_text, language, lang_config, body_limits, analysis):
|
|
|
|
|
name = cc._get_definition_name(node)
|
|
|
|
|
if name == "expand_search_roots":
|
|
|
|
|
return "def expand_search_roots(user_root: str) -> list[Path]:\n if True\n"
|
|
|
|
|
if name == "load_user_overrides":
|
|
|
|
|
return (
|
|
|
|
|
"def load_user_overrides(config_path: str) -> dict[str, str]:\n"
|
|
|
|
|
" config: dict[str, str] = {}\n"
|
|
|
|
|
" # [4 lines omitted]\n"
|
|
|
|
|
" return config"
|
|
|
|
|
)
|
|
|
|
|
return original_compress_function_ast(
|
|
|
|
|
node,
|
|
|
|
|
code_text,
|
|
|
|
|
language,
|
|
|
|
|
lang_config,
|
|
|
|
|
body_limits,
|
|
|
|
|
analysis,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
with patch.object(compressor, "_compress_function_ast", side_effect=_patched):
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert result.compressed != code
|
|
|
|
|
assert "if True" not in result.compressed
|
|
|
|
|
assert "for child in root.iterdir():" in result.compressed
|
|
|
|
|
assert "# [4 lines omitted]" in result.compressed
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
|
|
|
|
|
def test_python_recovery_does_not_block_valid_modern_syntax(self):
|
|
|
|
|
"""Valid decorators, nested defs, and match syntax still compress."""
|
|
|
|
|
compressor = self._recovery_compressor(max_body_lines=4)
|
|
|
|
|
code = textwrap.dedent(
|
|
|
|
|
"""\
|
|
|
|
|
from dataclasses import dataclass
|
|
|
|
|
|
|
|
|
|
def traced(fn):
|
|
|
|
|
return fn
|
|
|
|
|
|
|
|
|
|
@dataclass
|
|
|
|
|
class Command:
|
|
|
|
|
name: str
|
|
|
|
|
payload: dict[str, int]
|
|
|
|
|
|
|
|
|
|
@traced
|
|
|
|
|
def route_command(command: Command) -> str:
|
|
|
|
|
def normalize(value: str) -> str:
|
|
|
|
|
return value.strip().lower()
|
|
|
|
|
|
|
|
|
|
match normalize(command.name):
|
|
|
|
|
case "ping":
|
|
|
|
|
return "pong"
|
|
|
|
|
case "echo":
|
|
|
|
|
return str(command.payload)
|
|
|
|
|
case _:
|
|
|
|
|
return "unknown"
|
|
|
|
|
"""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert "@traced" in result.compressed
|
|
|
|
|
assert "class Command:" in result.compressed
|
|
|
|
|
assert "def route_command(command: Command) -> str:" in result.compressed
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
|
|
|
|
|
def test_python_recovery_still_returns_original_when_all_candidates_invalid(self):
|
|
|
|
|
"""Recovery keeps the terminal whole-file fail-safe."""
|
|
|
|
|
compressor = self._recovery_compressor()
|
|
|
|
|
code = self._python_recovery_fixture()
|
|
|
|
|
|
|
|
|
|
def _patched(node, _code_text, _language, _lang_config, _body_limits, _analysis):
|
|
|
|
|
name = cc._get_definition_name(node) or "broken"
|
|
|
|
|
return f"def {name}(:\n pass"
|
|
|
|
|
|
|
|
|
|
with patch.object(compressor, "_compress_function_ast", side_effect=_patched):
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio == 1.0
|
|
|
|
|
assert "(:\n" not in result.compressed
|
|
|
|
|
assert "for child in root.iterdir():" in result.compressed
|
|
|
|
|
assert 'key, value = line.split("=", 1)' in result.compressed
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
|
fix(code): slice tree-sitter byte offsets as UTF-8 (#1332)
## Description
CodeAwareCompressor was slicing Python strings with tree-sitter
`start_byte` / `end_byte` offsets directly. That works for ASCII-only
files, but it corrupts slices after non-ASCII source text such as CJK
characters or emoji because tree-sitter offsets are UTF-8 byte offsets
while Python string indexes are character offsets.
This caused code-aware compression to produce invalid intermediate
Python and then safely fall back to the original file, resulting in 0%
compression on affected files.
Closes #1319
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Added `_slice_code_bytes()` in
`headroom/transforms/code_compressor.py` to slice source text using
UTF-8 byte offsets.
- Updated `_get_node_text()` to use byte-safe slicing.
- Routed the other direct tree-sitter byte-offset slices through the
same helper.
- Added regression tests in
`tests/test_transforms/test_code_compressor.py`:
- `test_get_node_text_uses_utf8_byte_offsets`
- `test_ast_compresses_python_after_non_ascii_source`
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [x] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ .venv/bin/python -m pytest tests/test_transforms/test_code_compressor.py
68 passed, 1 warning
$ .venv/bin/python -m ruff check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
All checks passed!
$ .venv/bin/python -m ruff format --check headroom/transforms/code_compressor.py tests/test_transforms/test_code_compressor.py
2 files already formatted
$ git diff --check
# no output
$ /tmp/headroom-1319-venv/bin/python -m mypy headroom
headroom/proxy/server.py:1186: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1257: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
headroom/proxy/server.py:1261: note: By default the bodies of untyped functions are not checked, consider using --check-untyped-defs [annotation-unchecked]
pyproject.toml: note: unused section(s): module = ['mlx.*']
Success: no issues found in 394 source files
```
## Real Behavior Proof
- Environment: macOS arm64, Python 3.14.5, tree-sitter 0.25.2,
tree-sitter-language-pack 0.13.0
- Exact command / steps: On `main`, ran a local reproducer with a Python
source string containing a CJK docstring before a second function;
called `_get_node_text()` on the second tree-sitter function node; ran a
full `CodeAwareCompressor.compress(...)` repro with non-ASCII module
text before an import and a compressible function; re-ran both repros on
this branch.
- Observed result: Before fix, `_get_node_text()` returned the wrong
slice (`'nd():\n return 2\n'` instead of `'def second():\n return 2'`)
and full compression fell back to the original file with
`compression_ratio: 1.0`; after fix, `_get_node_text()` returns the full
expected function slice and full compression succeeds with
`compression_ratio < 1.0`, `syntax_valid: True`, and does not return the
original.
- Not tested: Full repository test suite; live proxy/provider
integrations; Windows/Linux platform-specific behavior.
## 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
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A
## Additional Notes
- Documentation was not updated because this is an internal bug fix with
no user-facing API or behavior change beyond restoring intended
compression.
- `CHANGELOG.md` was not updated because the fix is narrow and
issue-scoped; maintainers can advise if they want a changelog entry.
- The fix is intentionally small and targeted: it only changes how
tree-sitter byte offsets are converted back into Python source text,
without changing compression heuristics or language behavior.
2026-06-23 16:03:44 -04:00
|
|
|
def test_get_node_text_uses_utf8_byte_offsets(self):
|
|
|
|
|
"""tree-sitter byte offsets must not be sliced as Python str indexes."""
|
|
|
|
|
from headroom.transforms.code_compressor import _get_node_text, _get_parser
|
|
|
|
|
|
|
|
|
|
code = 'def first():\n """中文占位"""\n return 1\n\ndef second():\n return 2\n'
|
|
|
|
|
root = _get_parser("python").parse(code.encode("utf-8")).root_node
|
|
|
|
|
functions = [node for node in root.children if node.type == "function_definition"]
|
|
|
|
|
|
|
|
|
|
assert _get_node_text(functions[1], code) == "def second():\n return 2"
|
|
|
|
|
|
|
|
|
|
def test_ast_compresses_python_after_non_ascii_source(self):
|
|
|
|
|
"""CJK/emoji before a later function must not corrupt downstream slices."""
|
|
|
|
|
compressor = CodeAwareCompressor(
|
|
|
|
|
CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=1,
|
|
|
|
|
max_body_lines=2,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
semantic_analysis=False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
code = (
|
|
|
|
|
"def first():\n"
|
|
|
|
|
' """中文占位 with emoji 🔥."""\n'
|
|
|
|
|
" return 1\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"def second():\n"
|
|
|
|
|
" values = []\n"
|
|
|
|
|
" for i in range(10):\n"
|
|
|
|
|
" values.append(i)\n"
|
|
|
|
|
" values.append(i * 2)\n"
|
|
|
|
|
" values.append(i * 3)\n"
|
|
|
|
|
" values.append(i * 4)\n"
|
|
|
|
|
" return sum(values)\n"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
result = compressor.compress(code, language="python")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.PYTHON
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert "def second():" in result.compressed
|
|
|
|
|
assert "中文占位" in result.compressed
|
|
|
|
|
compile(result.compressed, "<test>", "exec")
|
|
|
|
|
|
fix(code): verify a real parse in tree-sitter availability check (#1231) (#1299)
## Description
`is_tree_sitter_available()` / `_check_tree_sitter_available()` in
`headroom/transforms/code_compressor.py` return `True` based on
importing `tree_sitter_language_pack` alone, without ever constructing a
parser or attempting a parse. When the installed pack/parser combination
is ABI-incompatible, `get_parser`/`parse` raises at runtime; the caller
catches it and silently falls back to the lossy text compressor, while
the availability flag and startup banner still report code-aware as on.
This is the defensive half that the `<1.0` pin in #1234 does not cover:
if that cap is ever lifted, the availability signal silently lies again.
Follow-up to #1231.
## Type of Change
- [x] Bug fix (non-breaking change that fixes an issue)
- [ ] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- Make `_check_tree_sitter_available()` construct a parser and parse a
tiny snippet, returning `True` only if it yields a real `module` AST
instead of trusting an import.
- Add `_tree_sitter_importable()` for the cheap import-only probe, and
use it to guard parser construction so the real-parse check cannot
recurse.
- Add tests asserting the check is `False` when parsing raises and
`True` on a real parse, plus that AST compression runs for python/rust
without falling back.
## Testing
- [ ] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
# pytest tests/test_transforms/test_code_compressor.py -> passed locally (tree-sitter-language-pack 0.13.0)
# ruff check . and ruff format --check . pass locally on the rebased branch.
# Full pytest suite / mypy not run locally; left to CI.
```
## Real Behavior Proof
- Environment: local repo on tree-sitter-language-pack 0.13.0,
tree-sitter 0.25.2, Python 3.12, Linux
- Exact command / steps: call `is_tree_sitter_available()`, then run
`pytest tests/test_transforms/test_code_compressor.py`
- Observed result: with a working pack the probe parses and returns
`True` (code-aware runs, strategy `CODE_AWARE` rather than the kompress
fallback); the new
`test_check_tree_sitter_available_false_when_parse_broken` confirms that
when parsing raises the check now returns `False` instead of the old
import-only `True`, so the lossy fallback is no longer entered silently.
- Not tested: reproducing the specific ABI-incompatible 1.x pack combo
against a live install (covered instead by a mocked broken parse in the
test)
## 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
- [ ] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [ ] New and existing unit tests pass locally with my changes
- [ ] I have updated the CHANGELOG.md if applicable
2026-06-23 12:00:57 +08:00
|
|
|
def test_ast_runs_for_rust_no_fallback(self):
|
|
|
|
|
"""A second supported language (Rust) also runs through real AST."""
|
|
|
|
|
code = (
|
|
|
|
|
"pub fn add(a: i64, b: i64) -> i64 {\n"
|
|
|
|
|
" let mut acc = a;\n"
|
|
|
|
|
" acc += b;\n"
|
|
|
|
|
" acc -= 0;\n"
|
|
|
|
|
" acc\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="rust")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.RUST
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
# Signature is preserved verbatim.
|
|
|
|
|
assert "pub fn add(a: i64, b: i64) -> i64" in result.compressed
|
feat(transforms): first-class C# support in CodeAwareCompressor (#1926)
Refs #1664
## Description
First-class C# support in `CodeAwareCompressor` via the tree-sitter
`csharp` grammar, at parity with Java/C++/Rust: `using` directives,
namespace headers, and type/member signatures preserved verbatim;
method/constructor/destructor/operator/local-function bodies compressed;
malformed input passes through unchanged. **No new dependencies** — the
grammar ships inside the already-pinned
`tree-sitter-language-pack==0.13.0` (resolves only as `"csharp"`;
`c_sharp`/`cs` raise `LookupError`). Spec and maintainer go-ahead in the
issue.
Closes #1664
## Type of Change
- [ ] Bug fix (non-breaking change that fixes an issue)
- [x] New feature (non-breaking change that adds functionality)
- [ ] Breaking change (fix or feature that would cause existing
functionality to change)
- [ ] Documentation update
- [ ] Performance improvement
- [ ] Code refactoring (no functional changes)
## Changes Made
- `CodeLanguage.CSHARP` + full `_LANG_CONFIGS` entry;
`_LANGUAGE_PREFILTER` and `content_detector` patterns chosen to be
C#-distinctive (so Java doesn't mis-tag).
- New data-driven `LangConfig` fields (pattern of #1334's
`class_body_node_types`): `container_node_types` — block-scoped
`namespace { }` routed through class compression so members compress
without the wrapper being re-emitted verbatim; `opaque_node_types` —
`#if`…`#endif` wrappers preserved verbatim without recursion (recursing
+ wrapper re-emit duplicated whole files, up to ~1.9x input on real
repos); `#if` blocks wrapping only usings are emitted with the imports
so they stay ahead of type declarations.
- Shared-path fixes surfaced by real C# repos, each guarded and covered
by a fail-before test: keep an Allman `{` on its own line in class
reconstruction (K&R path byte-for-byte unchanged; Allman Java now
compresses instead of falling back); line-based child extraction no
longer swallows the following line for nodes ending at column 0 (C#
`#region`/`#endregion` span their trailing newline — the over-slice
duplicated the next member's signature or the closing brace); uncaptured
top-level nodes preceding the first captured node (license banners,
`#region License`) are emitted first instead of relocated below the code
(tree-sitter-c-sharp rejects top-level `#region` after a type
declaration, so relocation forfeited compression for the whole file).
- `TestCSharpSupport` (8 tests) + a C# case in the parametrized
member-container test; CHANGELOG entry.
## Testing
- [x] Unit tests pass (`pytest`)
- [x] Linting passes (`ruff check .`)
- [ ] Type checking passes (`mypy headroom`)
- [x] New tests added for new functionality
- [x] Manual testing performed
### Test Output
```text
$ python -m pytest tests/test_transforms/test_code_compressor.py -q
2 failed, 78 passed, 1 warning, 4 errors # the 2 failures / 4 errors reproduce
# identically on main in the same env
# (network-dependent tokenizer setup)
Fail-before: with both changed sources reverted to main, the new C#-scoped
selection reports "10 failed, 5 passed" (the 5 other languages keep passing);
on the branch: "15 passed".
$ ruff check headroom/transforms/code_compressor.py headroom/transforms/content_detector.py tests/test_transforms/test_code_compressor.py
All checks passed!
$ ruff format --check <same files>
3 files already formatted
```
## Real Behavior Proof
- Environment: Linux 6.8.0 aarch64, Python 3.10.12; `uv run --no-project
--with "tree-sitter-language-pack==0.13.0" --with
"tree-sitter>=0.25.2,<0.26" --with "pydantic>=2.0.0"`; real
`CodeAwareCompressor` (`CodeCompressorConfig(enable_ccr=False)`,
otherwise defaults), no mocks.
- Exact command / steps: cloned two real .NET repos at depth 1
(`github.com/JamesNK/Newtonsoft.Json @ 4f73e74`,
`github.com/App-vNext/Polly @ 7a1d10f`), ran `python proof_csharp.py
<repo>` over every `.cs` file (chars/4 token estimate; tiktoken BPE
download unavailable in my sandbox). Script in the collapsed section
below.
- Observed result: 16.1% tokens saved on Newtonsoft.Json (945/945
syntax-valid), 37.8% on Polly (797/797 syntax-valid), zero content
duplication; full output:
```text
repo: Newtonsoft.Json (945 .cs files)
tokens before: 1,777,691 after: 1,490,629 saved: 287,062 (16.1%)
files compressed: 479 pass-through: 466 inflated(>before): 19
syntax_valid: 945/945
latency ms P50: 0.7 P95: 18.7 P99: 44.1 max: 255.0 mean: 3.5
repo: Polly (797 .cs files)
tokens before: 1,100,523 after: 684,303 saved: 416,220 (37.8%)
files compressed: 693 pass-through: 104 inflated(>before): 15
syntax_valid: 797/797
latency ms P50: 0.8 P95: 11.6 P99: 28.9 max: 74.1 mean: 2.4
```
After rebasing onto current `main` (which touched the same transform
files via #1906/#1747/#1668) I re-ran the Polly proof on the rebased
tree: 37.8% saved, 797/797 syntax-valid, P99 28.5ms — unchanged.
Signatures/properties verbatim, bodies elided with call summaries,
`using` order and preproc balance intact; residual "inflated" files are
+2…+209 chars of assembly blank lines, not duplicated content.
Newtonsoft is the adversarial case (multi-targeting: heavy `#if`,
`#region`, Allman) — its conditional regions stay verbatim by design.
Latency at parity with Java (<50ms P99; max is the pre-existing
symbol-analysis cost on ~1800+-line files, shared with other languages).
- Not tested: proxy end-to-end path with C# through `ContentRouter`
(tested the `CodeAwareCompressor` API directly); CCR retrieval
round-trips (`enable_ccr=False` in proof runs); exact tiktoken counts
(chars/4 estimate — relative ratios are tokenizer-independent);
Windows/macOS; full native `uv run pytest` with the Rust extension (ran
the complete `test_code_compressor.py` in a lightweight venv; its 2
failures/4 errors reproduce identically on `main`); `mypy`.
<details>
<summary>proof_csharp.py (reproducible)</summary>
```python
"""Real behavior proof: run the real CodeAwareCompressor over a .NET repo."""
import pathlib
import statistics
import sys
import time
from headroom.transforms.code_compressor import (
CodeAwareCompressor,
CodeCompressorConfig,
)
try:
import tiktoken
ENC = tiktoken.get_encoding("cl100k_base")
def toks(s: str) -> int:
return len(ENC.encode(s, disallowed_special=()))
except Exception:
def toks(s: str) -> int:
return len(s) // 4
target = pathlib.Path(sys.argv[1])
comp = CodeAwareCompressor(CodeCompressorConfig(enable_ccr=False))
tot_before = tot_after = 0
n_files = n_compressed = n_valid = n_passthrough = n_inflated = 0
times_ms: list[float] = []
for f in sorted(target.rglob("*.cs")):
try:
code = f.read_text(encoding="utf-8-sig", errors="replace")
except OSError:
continue
t0 = time.perf_counter()
r = comp.compress(code, language="csharp")
times_ms.append((time.perf_counter() - t0) * 1000)
n_files += 1
b, a = toks(code), toks(r.compressed)
tot_before += b
tot_after += a
if r.compressed == code:
n_passthrough += 1
else:
n_compressed += 1
if r.syntax_valid:
n_valid += 1
if a > b:
n_inflated += 1
times_ms.sort()
p = lambda q: times_ms[min(int(len(times_ms) * q), len(times_ms) - 1)]
print(f"repo: {target.name} ({n_files} .cs files)")
print(f" tokens before: {tot_before:,} after: {tot_after:,} saved: {tot_before - tot_after:,} ({(1 - tot_after / tot_before) * 100:.1f}%)")
print(f" files compressed: {n_compressed} pass-through: {n_passthrough} inflated(>before): {n_inflated}")
print(f" syntax_valid: {n_valid}/{n_files}")
print(f" latency ms P50: {p(0.50):.1f} P95: {p(0.95):.1f} P99: {p(0.99):.1f} max: {times_ms[-1]:.1f} mean: {statistics.mean(times_ms):.1f}")
```
</details>
## 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
- [x] I have made corresponding changes to the documentation
- [x] My changes generate no new warnings
- [x] I have added tests that prove my fix is effective or that my
feature works
- [x] New and existing unit tests pass locally with my changes
- [x] I have updated the CHANGELOG.md if applicable
## Screenshots (if applicable)
N/A — terminal evidence above.
## Additional Notes
- Dependency justification: none added, none bumped; the `csharp`
grammar is inside the already-pinned `tree-sitter-language-pack==0.13.0`
wheel; `uv.lock` untouched.
- Architecture: malformed input passes through byte-identical; every
risky construct prefers the false negative (verbatim) over corruption;
invalid reassembly falls back to the original via the existing
validation gate (observed live); no new imports at module load; P99
<50ms on both proof repos.
- Known v1 limitations (deliberate false negatives, possible
follow-ups): expression-bodied members and property accessor bodies stay
verbatim; declarations inside `#if` regions stay verbatim.
- Related pre-existing finding, out of scope: C/C++ exhibit the same
`#if`-wrapper duplication on `main` (an `#if`-wrapped C++ class is
emitted twice, ratio 1.62). Happy to file separately.
- `mypy` unchecked above because I did not run it in my environment.
2026-07-12 19:54:38 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter grammar pack not installed")
|
|
|
|
|
class TestCSharpSupport:
|
|
|
|
|
"""C# (``csharp`` grammar) parity with Java/C++/Rust: signatures preserved
|
|
|
|
|
verbatim, method/constructor bodies compressed, block-scoped namespaces
|
|
|
|
|
routed through container compression (no verbatim re-dump), file-scoped
|
|
|
|
|
namespace headers kept ahead of types, malformed input passed through.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def _compressor(self):
|
|
|
|
|
return CodeAwareCompressor(
|
|
|
|
|
CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=1,
|
|
|
|
|
max_body_lines=1,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_block_scoped_namespace_compresses_without_redumping(self):
|
|
|
|
|
"""Block-scoped ``namespace { }`` wraps types in a declaration_list; the
|
|
|
|
|
container must compress its members and NOT be re-emitted verbatim
|
|
|
|
|
(which would duplicate the class and defeat compression — the C#
|
|
|
|
|
analogue of the #1318 class-member trap, one nesting level deeper)."""
|
|
|
|
|
code = (
|
|
|
|
|
"using System;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"namespace Acme.Widgets\n"
|
|
|
|
|
"{\n"
|
|
|
|
|
" public class WidgetService\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" public int Process(int x)\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" var a = x + 1;\n"
|
|
|
|
|
" var b = a + 2;\n"
|
|
|
|
|
" var c = b + 3;\n"
|
|
|
|
|
" Console.WriteLine(c);\n"
|
|
|
|
|
" return c;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="csharp")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.CSHARP
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
# namespace + type headers preserved verbatim
|
|
|
|
|
assert "namespace Acme.Widgets" in result.compressed
|
|
|
|
|
assert "public class WidgetService" in result.compressed
|
|
|
|
|
assert "public int Process(int x)" in result.compressed
|
|
|
|
|
# method body actually compressed
|
|
|
|
|
assert "lines omitted" in result.compressed
|
|
|
|
|
assert "return c;" not in result.compressed
|
|
|
|
|
# the class is emitted exactly once (no re-dump / duplication)
|
|
|
|
|
assert result.compressed.count("class WidgetService") == 1
|
|
|
|
|
# block-namespace wrapper still closes
|
|
|
|
|
assert result.compressed.rstrip().endswith("}")
|
|
|
|
|
|
|
|
|
|
def test_file_scoped_namespace_directive_precedes_types(self):
|
|
|
|
|
"""A file-scoped ``namespace X;`` must stay ahead of the type
|
|
|
|
|
declarations; emitting it after a type would not be valid C#."""
|
|
|
|
|
code = (
|
|
|
|
|
"using System;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"namespace Acme.Tools;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"public class Helper\n"
|
|
|
|
|
"{\n"
|
|
|
|
|
" public int Add(int a, int b)\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" var s = a + b;\n"
|
|
|
|
|
" var t = s + 1;\n"
|
|
|
|
|
" var u = t + 2;\n"
|
|
|
|
|
" return u;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="csharp")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.CSHARP
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert "namespace Acme.Tools;" in result.compressed
|
|
|
|
|
assert "public class Helper" in result.compressed
|
|
|
|
|
assert result.compressed.index("namespace Acme.Tools;") < result.compressed.index(
|
|
|
|
|
"class Helper"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_record_preserves_positional_parameters(self):
|
|
|
|
|
"""Record primary-constructor parameters are part of the signature and
|
|
|
|
|
must be preserved verbatim while the method body compresses."""
|
|
|
|
|
code = (
|
|
|
|
|
"namespace Acme.Models;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"public record Point(int X, int Y)\n"
|
|
|
|
|
"{\n"
|
|
|
|
|
" public double Dist()\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" var sq = X * X + Y * Y;\n"
|
|
|
|
|
" var r = System.Math.Sqrt(sq);\n"
|
|
|
|
|
" System.Console.WriteLine(r);\n"
|
|
|
|
|
" return r;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="csharp")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.CSHARP
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert "public record Point(int X, int Y)" in result.compressed
|
|
|
|
|
assert "public double Dist()" in result.compressed
|
|
|
|
|
assert "lines omitted" in result.compressed
|
|
|
|
|
|
|
|
|
|
def test_preprocessor_wrapped_file_is_not_duplicated(self):
|
|
|
|
|
"""A file wrapped in ``#if``/``#endif`` (idiomatic in multi-targeted
|
|
|
|
|
.NET code) must not have its content emitted twice. The visitor
|
|
|
|
|
recurses into ``preproc_if`` and captures the declarations, but the
|
|
|
|
|
top-level pass used to re-emit the uncaptured wrapper verbatim,
|
|
|
|
|
duplicating the whole file (observed on real repos: output up to
|
|
|
|
|
~1.9x the input). Opaque handling keeps it verbatim exactly once."""
|
|
|
|
|
code = (
|
|
|
|
|
"#if !HAVE_TRACE_WRITER\n"
|
|
|
|
|
"using System;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"namespace Acme\n"
|
|
|
|
|
"{\n"
|
|
|
|
|
" public enum TraceLevel\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" Off = 0,\n"
|
|
|
|
|
" Error = 1,\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
"#endif\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="csharp")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
# Content appears exactly once — no wrapper re-dump.
|
|
|
|
|
assert result.compressed.count("enum TraceLevel") == 1
|
|
|
|
|
assert result.compressed.count("using System;") == 1
|
|
|
|
|
# Never larger than the input (verbatim pass-through is acceptable).
|
|
|
|
|
assert result.compression_ratio <= 1.0
|
|
|
|
|
# Conditional directives stay balanced.
|
|
|
|
|
assert result.compressed.count("#if") == result.compressed.count("#endif")
|
|
|
|
|
|
|
|
|
|
def test_preprocessor_wrapped_usings_stay_ahead_of_types(self):
|
|
|
|
|
"""``#if``-wrapped using directives (idiomatic for multi-targeting)
|
|
|
|
|
must be emitted with the imports. Appending them as trailing top-level
|
|
|
|
|
code puts usings after type declarations — invalid C#, which trips the
|
|
|
|
|
output syntax check and falls back to no compression at all (observed
|
|
|
|
|
on real repos: hundreds of files silently uncompressed)."""
|
|
|
|
|
code = (
|
|
|
|
|
"using System;\n"
|
|
|
|
|
"#if HAVE_BIG_INTEGER\n"
|
|
|
|
|
"using System.Numerics;\n"
|
|
|
|
|
"#endif\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"namespace Acme\n"
|
|
|
|
|
"{\n"
|
|
|
|
|
" public class Calc\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" public int Compute(int x)\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" var a = x + 1;\n"
|
|
|
|
|
" var b = a * 2;\n"
|
|
|
|
|
" var c = b - 3;\n"
|
|
|
|
|
" var d = c / 4;\n"
|
|
|
|
|
" return d;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="csharp")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
# Compression must actually happen (no silent fallback).
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert "lines omitted" in result.compressed
|
|
|
|
|
# The conditional import block survives verbatim, ahead of the types.
|
|
|
|
|
assert "#if HAVE_BIG_INTEGER" in result.compressed
|
|
|
|
|
assert "using System.Numerics;" in result.compressed
|
|
|
|
|
assert result.compressed.index("#endif") < result.compressed.index("class Calc")
|
|
|
|
|
|
|
|
|
|
def test_region_markers_inside_class_compress_cleanly(self):
|
|
|
|
|
"""``#region``/``#endregion`` markers span their trailing newline, so
|
|
|
|
|
their end_point sits at column 0 of the NEXT line. Line-based child
|
|
|
|
|
extraction used to swallow that line — duplicating the following
|
|
|
|
|
method's signature and the class's closing brace (invalid output →
|
|
|
|
|
silent fallback to no compression on real repos)."""
|
|
|
|
|
code = (
|
|
|
|
|
"namespace Acme\n"
|
|
|
|
|
"{\n"
|
|
|
|
|
" public class Svc\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" #region Api\n"
|
|
|
|
|
" public int F(int x)\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" var a = x + 1;\n"
|
|
|
|
|
" var b = a * 2;\n"
|
|
|
|
|
" var c = b - 3;\n"
|
|
|
|
|
" return c;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" #endregion\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="csharp")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert "lines omitted" in result.compressed
|
|
|
|
|
# Signature emitted exactly once (regions used to duplicate it).
|
|
|
|
|
assert result.compressed.count("public int F(int x)") == 1
|
|
|
|
|
# Region markers survive, braces stay balanced.
|
|
|
|
|
assert "#region Api" in result.compressed
|
|
|
|
|
assert "#endregion" in result.compressed
|
|
|
|
|
assert result.compressed.count("{") == result.compressed.count("}")
|
|
|
|
|
|
|
|
|
|
def test_license_region_header_stays_on_top(self):
|
|
|
|
|
"""A ``#region License`` banner above the usings (near-universal in
|
|
|
|
|
real .NET code) must stay at the top of the file. Relocating it after
|
|
|
|
|
the type declarations — the old behavior for uncaptured top-level
|
|
|
|
|
nodes — is rejected by tree-sitter-c-sharp, failing output validation
|
|
|
|
|
and silently forfeiting compression for the whole file."""
|
|
|
|
|
code = (
|
|
|
|
|
"#region License\n"
|
|
|
|
|
"// Copyright (c) 2007 Example Corp.\n"
|
|
|
|
|
"// Licensed under the MIT license.\n"
|
|
|
|
|
"#endregion\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"using System;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"namespace Acme\n"
|
|
|
|
|
"{\n"
|
|
|
|
|
" public class Svc\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" public int F(int x)\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" var a = x + 1;\n"
|
|
|
|
|
" var b = a * 2;\n"
|
|
|
|
|
" var c = b - 3;\n"
|
|
|
|
|
" return c;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="csharp")
|
|
|
|
|
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
assert "lines omitted" in result.compressed
|
|
|
|
|
# Header block survives, in order, ahead of everything else.
|
|
|
|
|
assert result.compressed.index("#region License") < result.compressed.index("using System;")
|
|
|
|
|
assert result.compressed.index("// Copyright") < result.compressed.index("#endregion")
|
|
|
|
|
assert result.compressed.index("#endregion") < result.compressed.index("class Svc")
|
|
|
|
|
|
|
|
|
|
def test_malformed_csharp_passes_through_unchanged(self):
|
|
|
|
|
"""Malformed C# must pass through unchanged (no data loss; prefer false
|
|
|
|
|
negatives over serving broken or altered code)."""
|
|
|
|
|
code = (
|
|
|
|
|
"namespace Broken\n"
|
|
|
|
|
"{\n"
|
|
|
|
|
" public class Oops\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" public int F(int x)\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" var a = x + 1\n" # missing ';' and unclosed braces
|
|
|
|
|
" return a\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="csharp")
|
|
|
|
|
|
|
|
|
|
assert result.compressed == code
|
|
|
|
|
assert result.compression_ratio == 1.0
|
|
|
|
|
|
|
|
|
|
def test_detect_language_identifies_csharp(self):
|
|
|
|
|
"""Auto-detection (no explicit language hint) recognizes C#."""
|
|
|
|
|
code = (
|
|
|
|
|
"using System;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"namespace Acme\n"
|
|
|
|
|
"{\n"
|
|
|
|
|
" public class Svc\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" public int Total { get; set; }\n"
|
|
|
|
|
" public int Add(int a, int b)\n"
|
|
|
|
|
" {\n"
|
|
|
|
|
" return a + b;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
lang, confidence = detect_language(code)
|
|
|
|
|
assert lang == CodeLanguage.CSHARP
|
|
|
|
|
assert confidence > 0.0
|
feat(code): add PHP support to CodeAwareCompressor (#2423)
## Description
Adds PHP to `CodeAwareCompressor`, fixing #201. PHP was already
*detected* as code (Magika labels in `headroom/compression/detector.py`
include `php`, and the Rust `magika_detector.rs` lists it too) but there
was no PHP `LangConfig`, so PHP content silently passed through
uncompressed. This wires PHP through the tree-sitter compression path
following the C# pattern (the most recently added, fully functional
language — deliberately not the quarantined Perl path).
A secondary detection bug is fixed along the way: PHP's `$variables`
match Perl's prefilter regex, and the existing Perl-dominance guard in
`detect_language` returned `UNKNOWN` for PHP files. An explicit `<?php`
open tag — which no Perl source contains — now drops Perl from the
candidate set before that guard runs.
## Type of Change
- [ ] Bug fix
- [x] New feature
- [ ] Documentation update
- [ ] Refactor
- [ ] Other
## Changes Made
- `headroom/transforms/code_compressor.py`: `CodeLanguage.PHP` +
`phtml`/`php5`/`php7`/`php8` aliases; PHP `LangConfig` built from the
actual tree-sitter-php grammar (node names verified by parsing samples):
`namespace_use_declaration` imports,
`function_definition`/`method_declaration` functions,
`class_declaration`/`interface_declaration`/`trait_declaration` classes,
`enum_declaration` types, `declaration_list` class bodies,
`compound_statement` function bodies. `namespace_definition` maps to
`package_node` so statement-scoped `namespace App;` hoists ahead of the
`use` imports (required PHP ordering); the rare block-scoped `namespace
A { }` form takes the same path and is preserved verbatim — valid
output, just no compression inside the block. PHP prefilter regexes
added; supported-languages error message updated; `<?php`-tag Perl
disambiguation in `detect_language`.
- `headroom/transforms/content_detector.py`: `php` entry in
`_CODE_PATTERNS` so raw PHP classifies as `SOURCE_CODE` and reaches the
code-aware route.
- `tests/test_transforms/test_code_compressor.py`: new `TestPhpSupport`
mirroring `TestCSharpSupport` — signatures preserved / bodies elided,
`<?php` → `namespace` → `use` → declarations ordering, auto-detection
despite the Perl sigil overlap, alias coercion, malformed passthrough.
- `tests/test_code_compressor_language_alias.py`: `php` in the canonical
list, `phtml` in the alias table.
- `docs/content/docs/code-compression.mdx`: PHP added to the Tier 2
supported-languages row.
No new dependency: `tree-sitter-language-pack` (the existing `[code]`
extra) already ships the PHP grammar. No Rust changes needed.
## Testing
- [x] New unit tests added and passing
- [x] Full affected test suites pass locally
**Test Output**
```
$ python -m pytest tests/test_transforms/test_code_compressor.py tests/test_code_compressor_language_alias.py -q
============================= 120 passed in 7.81s =============================
$ python -m pytest tests/test_transforms/ -q
3 failed, 443 passed # the 3 failures (kompress ONNX thread caps, kompress size gate,
# text_crusher unicode parity) reproduce identically on a clean
# upstream/main checkout in this environment — pre-existing local
# ONNX runtime quirks, unrelated to this change
$ ruff check . (0.15.17, CI-pinned) → All checks passed! | ruff format --check → clean
$ mypy headroom/transforms/code_compressor.py headroom/transforms/content_detector.py --ignore-missing-imports
Success: no issues found in 2 source files
```
## Real Behavior Proof
- Environment: Windows 11, Python 3.13, tree-sitter +
tree-sitter-language-pack (<1.0) installed, branch
`feat/201-php-code-compression` off `upstream/main`.
- Exact command / steps: parsed PHP samples (namespaced class w/
methods, block-scoped namespace, mixed HTML+PHP) with
`tree_sitter_language_pack.get_parser('php')` to verify every node name
used in the config; then ran `CodeAwareCompressor().compress(php_code,
language="php")` and `compress(php_code)` (auto-detection) on a 48-line
realistic service class.
- Observed result: explicit and auto-detected paths both return
`language=CodeLanguage.PHP`, `compression_ratio=0.64`,
`syntax_valid=True`; method bodies elided to `// [N lines omitted]`
while `<?php`, `namespace`, `use` lines, class header, and all
signatures are preserved verbatim in the original order. Before the
detection fix, auto-detection returned `UNKNOWN` (Perl prefilter
dominance) — reproduced and then verified fixed.
- Not tested: exotic PHP shapes (heredoc-heavy code, attributes `#[...]`
on methods, interleaved multi-`<?php ?>` HTML templates beyond the basic
mixed case); these fall back to verbatim preservation via the
uncaptured-node pass or malformed-passthrough, both of which are covered
by tests for the simple cases.
## Review Readiness
- [x] I have performed a self-review
- [x] This PR is ready for human review
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: JerrettDavis <mxjerrett@gmail.com>
2026-08-01 00:54:13 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.skipif(not TREE_SITTER_INSTALLED, reason="tree-sitter grammar pack not installed")
|
|
|
|
|
class TestPhpSupport:
|
|
|
|
|
"""PHP (``php`` grammar) parity with C#: signatures preserved verbatim,
|
|
|
|
|
function/method bodies compressed, ``<?php`` tag and ``namespace``/``use``
|
|
|
|
|
header order preserved, Perl sigil-overlap disambiguated in detection,
|
|
|
|
|
malformed input passed through.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def _compressor(self):
|
|
|
|
|
return CodeAwareCompressor(
|
|
|
|
|
CodeCompressorConfig(
|
|
|
|
|
min_tokens_for_compression=1,
|
|
|
|
|
max_body_lines=1,
|
|
|
|
|
enable_ccr=False,
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def test_class_methods_compress_signatures_preserved(self):
|
|
|
|
|
code = (
|
|
|
|
|
"<?php\n"
|
|
|
|
|
"namespace App\\Service;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"use App\\Model\\User;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"final class UserService {\n"
|
|
|
|
|
" private $logger;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
" public function process(User $u): bool {\n"
|
|
|
|
|
" $name = strtolower(trim($u->getName()));\n"
|
|
|
|
|
" $tags = [];\n"
|
|
|
|
|
" foreach ($u->getTags() as $tag) {\n"
|
|
|
|
|
" $tags[] = $tag->normalize();\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" $this->logger->info($name);\n"
|
|
|
|
|
" return true;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="php")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.PHP
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
# signature + class header preserved verbatim
|
|
|
|
|
assert "final class UserService" in result.compressed
|
|
|
|
|
assert "public function process(User $u): bool" in result.compressed
|
|
|
|
|
# method body actually compressed
|
|
|
|
|
assert "lines omitted" in result.compressed
|
|
|
|
|
assert "$tag->normalize()" not in result.compressed
|
|
|
|
|
# the class is emitted exactly once
|
|
|
|
|
assert result.compressed.count("class UserService") == 1
|
|
|
|
|
|
|
|
|
|
def test_php_tag_and_namespace_precede_uses_and_types(self):
|
|
|
|
|
"""``<?php`` must stay first and ``namespace X;`` must precede the
|
|
|
|
|
``use`` imports and type declarations — any other order is not valid
|
|
|
|
|
PHP."""
|
|
|
|
|
code = (
|
|
|
|
|
"<?php\n"
|
|
|
|
|
"namespace App\\Tools;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"use App\\Model\\Item;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"function helper(int $x): int {\n"
|
|
|
|
|
" $acc = 0;\n"
|
|
|
|
|
" for ($i = 0; $i < $x; $i++) {\n"
|
|
|
|
|
" $acc += $i;\n"
|
|
|
|
|
" $acc -= 1;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
" return $acc;\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
result = self._compressor().compress(code, language="php")
|
|
|
|
|
|
|
|
|
|
assert result.language == CodeLanguage.PHP
|
|
|
|
|
assert result.syntax_valid is True
|
|
|
|
|
assert result.compression_ratio < 1.0
|
|
|
|
|
compressed = result.compressed
|
|
|
|
|
assert compressed.lstrip().startswith("<?php")
|
|
|
|
|
assert compressed.index("<?php") < compressed.index("namespace App\\Tools;")
|
|
|
|
|
assert compressed.index("namespace App\\Tools;") < compressed.index("use App\\Model\\Item;")
|
|
|
|
|
assert compressed.index("use App\\Model\\Item;") < compressed.index("function helper")
|
|
|
|
|
assert "lines omitted" in compressed
|
|
|
|
|
|
|
|
|
|
def test_detect_language_identifies_php(self):
|
|
|
|
|
"""Auto-detection recognizes PHP despite the Perl sigil overlap
|
|
|
|
|
(``$var`` matches Perl's prefilter; the ``<?php`` tag disambiguates)."""
|
|
|
|
|
code = (
|
|
|
|
|
"<?php\n"
|
|
|
|
|
"namespace Acme;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"use Acme\\Widget;\n"
|
|
|
|
|
"\n"
|
|
|
|
|
"class Svc {\n"
|
|
|
|
|
" public function add(int $a, int $b): int {\n"
|
|
|
|
|
" $sum = $a + $b;\n"
|
|
|
|
|
" return $sum;\n"
|
|
|
|
|
" }\n"
|
|
|
|
|
"}\n"
|
|
|
|
|
)
|
|
|
|
|
lang, confidence = detect_language(code)
|
|
|
|
|
assert lang == CodeLanguage.PHP
|
|
|
|
|
assert confidence > 0.0
|
|
|
|
|
|
|
|
|
|
def test_phtml_alias_coerces_to_php(self):
|
|
|
|
|
assert coerce_language("phtml") == CodeLanguage.PHP
|
|
|
|
|
assert coerce_language("php8") == CodeLanguage.PHP
|
|
|
|
|
|
|
|
|
|
def test_malformed_php_passes_through_unchanged(self):
|
|
|
|
|
code = "<?php\nclass Broken {\n public function oops( {\n"
|
|
|
|
|
result = self._compressor().compress(code, language="php")
|
|
|
|
|
assert result.compressed == code
|