headroom/docs/content/docs/code-compression.mdx
Parideboy 6d5516dcb8
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-07-31 15:54:13 -07:00

174 lines
5.6 KiB
Text

---
title: Code Compression
description: AST-aware compression that preserves imports, signatures, and types while compressing function bodies. Powered by tree-sitter.
---
Headroom's CodeAwareCompressor uses tree-sitter to parse source code into an AST, then selectively compresses function bodies while preserving the structural elements that LLMs need -- imports, signatures, type annotations, and error handlers.
## Why AST-Aware Compression?
Naive truncation breaks code. Cutting a function in half leaves invalid syntax that confuses the LLM. CodeAwareCompressor guarantees:
- **Syntax validity** -- output always parses correctly
- **Structural preservation** -- imports, signatures, types, decorators are kept intact
- **Lightweight** -- ~50MB of tree-sitter parsers, loaded lazily and cached
## Supported Languages
| Tier | Languages | Support Level |
|---|---|---|
| Tier 1 | Python, JavaScript, TypeScript | Full AST analysis |
| Tier 2 | Go, Rust, Java, C, C++, C#, PHP | Function body compression |
## What Gets Preserved vs Compressed
**Always preserved:**
- Import statements
- Function and method signatures
- Class definitions
- Type annotations
- Decorators
- Error handlers (`try`/`except`, `try`/`catch`)
**Compressed:**
- Function bodies (implementations)
- Comments (unless configured to preserve)
- Verbose docstrings (configurable: full, first line, or removed)
## Example
```python
from headroom.transforms import CodeAwareCompressor
compressor = CodeAwareCompressor()
code = '''
import os
from typing import List
def process_items(items: List[str]) -> List[str]:
"""Process a list of items."""
results = []
for item in items:
if not item:
continue
processed = item.strip().lower()
results.append(processed)
return results
'''
result = compressor.compress(code, language="python")
print(result.compressed)
# import os
# from typing import List
#
# def process_items(items: List[str]) -> List[str]:
# """Process a list of items."""
# results = []
# for item in items:
# # ... (5 lines compressed)
# pass
print(f"Compression: {result.compression_ratio:.0%}") # ~55%
print(f"Syntax valid: {result.syntax_valid}") # True
```
## Configuration
```python
from headroom.transforms import CodeAwareCompressor, CodeCompressorConfig, DocstringMode
config = CodeCompressorConfig(
preserve_imports=True, # Always keep imports
preserve_signatures=True, # Always keep function signatures
preserve_type_annotations=True, # Keep type hints
preserve_error_handlers=True, # Keep try/except blocks
preserve_decorators=True, # Keep decorators
docstring_mode=DocstringMode.FIRST_LINE, # FULL, FIRST_LINE, REMOVE
target_compression_rate=0.2, # Keep 20% of tokens
max_body_lines=5, # Lines to keep per function body
min_tokens_for_compression=100, # Skip small content
language_hint=None, # Auto-detect if None
fallback_to_kompress=True, # Use Kompress for unknown langs
)
compressor = CodeAwareCompressor(config)
result = compressor.compress(code)
```
### Configuration Options
| Option | Default | Description |
|---|---|---|
| `preserve_imports` | `True` | Keep all import statements |
| `preserve_signatures` | `True` | Keep function/method signatures |
| `preserve_type_annotations` | `True` | Keep type hints |
| `preserve_error_handlers` | `True` | Keep try/except blocks |
| `preserve_decorators` | `True` | Keep decorators |
| `docstring_mode` | `FIRST_LINE` | How to handle docstrings: `FULL`, `FIRST_LINE`, `REMOVE` |
| `target_compression_rate` | `0.2` | Fraction of tokens to keep (0.2 = keep 20%) |
| `max_body_lines` | `5` | Max lines to keep per function body |
| `min_tokens_for_compression` | `100` | Skip files smaller than this |
| `language_hint` | `None` | Override language detection |
| `fallback_to_kompress` | `True` | Use Kompress for unsupported languages |
## Before and After
```python
# Before (full source file)
def process_data(items: List[str]) -> Dict[str, int]:
"""Process items and count occurrences."""
result = {}
for item in items:
item = item.strip().lower()
if item in result:
result[item] += 1
else:
result[item] = 1
return result
# After (signature preserved, body compressed)
def process_data(items: List[str]) -> Dict[str, int]:
"""Process items and count occurrences."""
result = {}
for item in items:
# ... (5 lines compressed)
pass
```
The LLM sees the function's purpose, its input/output types, and the general approach -- enough to reason about the code without needing every implementation line.
## Installation
```bash
# Install tree-sitter language pack
pip install "headroom-ai[code]"
```
## Memory Management
Tree-sitter parsers are lazy-loaded and cached. You can free memory when done:
```python
from headroom.transforms import is_tree_sitter_available
from headroom.transforms.code_compressor import unload_tree_sitter
# Check if tree-sitter is installed
print(is_tree_sitter_available()) # True
# Free memory when done
unload_tree_sitter()
```
## Performance
| Metric | Value |
|---|---|
| Compression | 40-70% token reduction |
| Speed | ~10-50ms per file |
| Memory | ~50MB (tree-sitter parsers) |
| Syntax validity | Guaranteed |
<Callout type="info" title="Automatic routing">
When you use the Headroom proxy or call `compress()`, source code is automatically detected and routed to CodeAwareCompressor. Direct usage gives you control over compression settings per language.
</Callout>