# Section 01: Enhance output_limit.py

**Status: IMPLEMENTED**

## Overview

This section adds two enhancements to `src/godotiq/tools/output_limit.py`:

1. A new `truncate_output()` function for raw string truncation (safety net for non-dict output)
2. Input validation in the existing `apply_output_limit()` to normalize unknown `detail` values to `"normal"`

This section has no dependencies on other sections. It is the foundation that sections 02 through 06 depend on.

**File to modify:** `/Users/salvatorefarruggio/Desktop/godotiq/src/godotiq/tools/output_limit.py`

**New test file:** `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_tools/test_truncate_output.py`

---

## Current State of output_limit.py

The file currently contains:

- `_CHAR_LIMITS` dict mapping `"brief"` to 2000, `"normal"` to 8000, `"full"` to `None`
- `_TRUNCATION_MSG` constant: `"Output truncated. Use detail='full' to see everything."`
- `_estimate_size(result: dict) -> int` — estimates JSON-serialized character count
- `apply_output_limit(result: dict, detail: str, list_keys: list[str] | None = None) -> dict` — truncates list values in a dict using binary search to fit character limits

**Critical bug in current code:** When `detail` is an unknown value (e.g., `"verbose"`, `"minimal"`, empty string, or a typo), `_CHAR_LIMITS.get(detail)` returns `None`. The function treats `None` as "no limit" (same as `"full"`), meaning unknown values silently bypass all limits. This is a security hole that must be fixed.

---

## Tests (Write First)

Create `/Users/salvatorefarruggio/Desktop/godotiq/tests/test_tools/test_truncate_output.py` with the following test structure.

### truncate_output tests

```python
"""Tests for truncate_output() and apply_output_limit() input validation."""

from __future__ import annotations

from godotiq.tools.output_limit import apply_output_limit, truncate_output


class TestTruncateOutput:
    """Tests for the truncate_output() function."""

    def test_short_text_brief_unchanged(self) -> None:
        """Text under brief limit (2000 chars) returns unchanged."""

    def test_long_text_brief_truncated(self) -> None:
        """Text over brief limit is truncated with notice appended."""

    def test_short_text_normal_unchanged(self) -> None:
        """Text under normal limit (8000 chars) returns unchanged."""

    def test_long_text_normal_truncated(self) -> None:
        """Text over normal limit is truncated with notice appended."""

    def test_full_never_truncated(self) -> None:
        """Text with detail='full' is never truncated regardless of size."""

    def test_truncation_notice_includes_line_count(self) -> None:
        """Truncation notice includes accurate count of remaining lines."""

    def test_truncated_text_fits_within_limit(self) -> None:
        """The truncated text plus notice does not exceed the character limit."""

    def test_unknown_detail_treated_as_normal(self) -> None:
        """Unknown detail value (e.g., 'verbose') is treated as 'normal'."""

    def test_empty_string_unchanged(self) -> None:
        """Empty string input returns empty string unchanged."""

    def test_text_exactly_at_boundary_unchanged(self) -> None:
        """Text exactly at the limit boundary returns unchanged (not truncated)."""


class TestApplyOutputLimitValidation:
    """Tests for apply_output_limit() input validation of detail parameter."""

    def test_unknown_detail_normalized_to_normal(self) -> None:
        """Unknown detail value applies normal limits (not unlimited)."""
        # Build a result that exceeds 8000 chars — verify it gets truncated
        # (Previously, unknown values fell through to None = unlimited)

    def test_verbose_applies_normal_limits(self) -> None:
        """detail='verbose' applies normal limits, not unlimited."""

    def test_empty_string_applies_normal_limits(self) -> None:
        """detail='' (empty string) applies normal limits, not unlimited."""

    def test_valid_brief_unchanged(self) -> None:
        """detail='brief' continues to work correctly."""

    def test_valid_normal_unchanged(self) -> None:
        """detail='normal' continues to work correctly."""

    def test_valid_full_unchanged(self) -> None:
        """detail='full' continues to work correctly (no limit)."""
```

### Key test implementation details

For `test_long_text_brief_truncated`: generate a string of 5000+ characters with multiple newlines, call `truncate_output(text, "brief")`, verify the result length is at most 2000 characters, and verify the string `"truncated"` appears in the result.

For `test_truncation_notice_includes_line_count`: generate a string with a known number of lines (e.g., 100 lines of 100 chars each = 10000 chars), call `truncate_output(text, "brief")`, parse the notice text to find the remaining-lines count, and verify it is accurate (total lines minus lines that fit in the truncated portion).

For `test_unknown_detail_normalized_to_normal` in the validation class: build a result dict with a list of 100 items each ~200 chars (total ~20000 chars). Call `apply_output_limit(result, "verbose", list_keys=["items"])`. Verify the output is truncated (has `output_truncated` key). This confirms that `"verbose"` no longer silently bypasses limits.

---

## Implementation Details

### 1. Add input validation to `apply_output_limit()`

At the top of the function body, before the line `limit = _CHAR_LIMITS.get(detail)`, add normalization logic:

- Define the set of valid values: `{"brief", "normal", "full"}`
- If `detail` is not in the valid set, normalize it to `"normal"`
- Optionally log a warning (use `import logging` at module level, `logger = logging.getLogger(__name__)`)

The normalization must happen **before** the `_CHAR_LIMITS.get(detail)` call so that the rest of the function works with a known-good value.

The existing function signature does not change. The behavior change is: unknown values now get normal limits (8000 chars) instead of no limits.

### 2. Add `truncate_output()` function

Add a new public function with this signature:

```python
def truncate_output(text: str, detail: str, tool_name: str = "") -> str:
    """Truncate raw text output to fit detail-level character limits.

    Safety net for string-based output. Uses the same character limits as
    apply_output_limit() (brief=2000, normal=8000, full=unlimited).

    Args:
        text: The raw text to potentially truncate.
        detail: "brief", "normal", or "full".
        tool_name: Optional tool name for the truncation notice.

    Returns:
        The (possibly truncated) text with a notice appended if truncated.
    """
```

Implementation logic:

1. Normalize `detail` using the same validation as `apply_output_limit()` (unknown values become `"normal"`)
2. Look up the limit from `_CHAR_LIMITS` for the normalized detail value
3. If limit is `None` (i.e., `"full"`), return text unchanged
4. If `len(text) <= limit`, return text unchanged
5. If text exceeds limit:
   - Calculate `cut_point = limit - 200` (reserve 200 chars for the truncation notice, same overhead constant used in `apply_output_limit()`)
   - Truncate the text to `text[:cut_point]`
   - Count remaining lines: `remaining_lines = text[cut_point:].count("\n") + 1`
   - Build notice: `"\n\n... truncated ({remaining_lines} more lines). Use detail='full' for complete output."`
   - If `tool_name` is provided, optionally include it in the notice for debugging
   - Return `truncated_text + notice`

The function should handle edge cases: empty string input, text with no newlines in the truncated portion.

### 3. Module-level additions

Add at the top of the file (after existing imports):

```python
import logging

logger = logging.getLogger(__name__)
```

Add to the module's public API: the `truncate_output` function should be importable as `from godotiq.tools.output_limit import truncate_output`.

### 4. Constants to reuse

The valid detail values set should be defined once and reused by both functions:

```python
_VALID_DETAIL_VALUES = {"brief", "normal", "full"}
```

---

## Verification Checklist

All verified:

- [x] `from godotiq.tools.output_limit import truncate_output` works
- [x] `from godotiq.tools.output_limit import apply_output_limit` still works
- [x] `apply_output_limit(result, "unknown_value")` applies normal limits (not unlimited)
- [x] `truncate_output("short", "brief")` returns `"short"` unchanged
- [x] `truncate_output("x" * 5000, "brief")` returns a string of at most 2000 chars
- [x] All existing tests in `tests/test_tools/test_output_limits.py` still pass (12 passed)
- [x] New tests in `tests/test_tools/test_truncate_output.py` pass (17 tests)
- [x] Full test suite passes (883 tests)

## Deviations from Plan

1. **`tool_name` wired into notice** — Code review caught that `tool_name` was dead code. Now included as `[tool_name]` prefix in truncation notice when provided.
2. **Test count: 17 instead of 16** — Added `test_no_newlines_in_remainder` per code review (zero-newline edge case).
3. **`_make_big_result` uses 200 items** — Increased from 100 to 200 to reliably exceed 8000 chars for `apply_output_limit()` validation tests.