1
0
Fork 0

refactor(calibration): consolidate tunable thresholds into calibration.py

Session D: move every calibration constant (entropy floors 5.4/128, 5.1/64,
4.7/40 + shape floors; MAX_SCAN_CHARS; rot13-min; cognitive-load lengths
2000/2500; disposition ranks; active-content severities) into one documented
calibration.py, so a parallel Node/TS port can mirror exactly the same numbers.

Pure refactor, zero behavior change: calibration is a leaf module (imports only
report.Severity) that entropy/lexicon/disposition/active_content now source
their thresholds from. MAX_SCAN_CHARS is re-exported from lexicon so output.py
and existing callers are unaffected. The 347 pre-existing tests pass unmodified;
new test_calibration.py freezes the values and asserts each detector actually
reads its threshold from calibration (identity-checked, not a dead copy).
This commit is contained in:
Kjell Tore Guttormsen 2026-07-15 09:44:53 +02:00
commit ee402e4ea8
6 changed files with 212 additions and 34 deletions

View file

@ -41,13 +41,18 @@ from dataclasses import dataclass
from pathlib import Path
from urllib.parse import unquote
from .calibration import (
COGNITIVE_LOAD_MIN_LEN,
COGNITIVE_LOAD_TAIL_START,
MAX_SCAN_CHARS,
ROT13_MIN_LEN as _ROT13_MIN_LEN,
)
from .entropy import try_decode_base64
from .report import Finding, Report, Severity, Source
# --- self-safety: input-size cap (OWASP LLM10) ------------------------------
# Large enough for a real ingested document; beyond it we scan the prefix and
# flag, so runtime stays bounded even on a decompression-bomb-sized input.
MAX_SCAN_CHARS = 1_000_000
# Self-safety input-size cap (OWASP LLM10), rot13 variant floor, and the
# cognitive-load-trap lengths all live in `calibration` (the Node port shares
# them). MAX_SCAN_CHARS is re-exported here for `output` and existing callers.
_LEXICON_FILE = "injection_lexicon.json"
_FLAG_MAP = {"i": re.IGNORECASE, "m": re.MULTILINE, "s": re.DOTALL}
@ -261,9 +266,9 @@ def check_cognitive_load_trap(text: str) -> str | None:
chars* of long text (>=2500), else ``None``. Placement is the signal: an
override buried at the tail of verbose output is a human-in-the-loop trap.
"""
if len(text) < 2500:
if len(text) < COGNITIVE_LOAD_MIN_LEN:
return None
tail = text[2000:]
tail = text[COGNITIVE_LOAD_TAIL_START:]
for pattern in load_lexicon():
if pattern.severity is Severity.CRITICAL and pattern.regex.search(tail):
return pattern.id
@ -271,8 +276,8 @@ def check_cognitive_load_trap(text: str) -> str | None:
# --- variant set + scan ------------------------------------------------------
_ROT13_MIN_LEN = 40 # shorter strings hit rot13-look-alike false positives
# _ROT13_MIN_LEN (imported from calibration): shorter strings hit
# rot13-look-alike false positives.
def _build_variants(text: str) -> list[tuple[str, str]]: