merge: mateo/translation-rule-hardening into translation-v2-providers

Clean merge (append-only Makefile/semgrep/ruff additions plus the two
gate scripts). CLAUDE.md freshness and file-size gates now run in
make lint-translation; verified against the merged tree in the next
baseline pass.
This commit is contained in:
mateo-berri
2026-06-12 05:58:43 +00:00
6 changed files with 405 additions and 0 deletions
@@ -0,0 +1,169 @@
# litellm/translation v2 effects boundary, encoded as deterministic checks
# (06-quality-team.md, "algebraic effects for I/O and side effects"). Scoped
# to the translation package; the rest of the codebase is unaffected.
#
# The translation core is pure: every ambient input (clock, ids, env, fs,
# network, randomness) must arrive as a value through deps.TranslationDeps or
# the injected engine.http.HttpPort. The two boundary modules that DEFINE
# those ports are whitelisted by path; they declare the window, they do not
# get to reach through it either today (both are pure), but a future port
# default would live there and nowhere else.
rules:
- id: translation-no-ambient-clock
message: >
ambient effect: reading the clock (time.*, datetime.now/utcnow/today)
inside litellm/translation. Inject the timestamp via TranslationDeps
(tenets: algebraic effects for I/O).
severity: ERROR
languages: [python]
paths:
include:
- litellm/translation
exclude:
# boundary modules: the only place a port/default may live
- litellm/translation/deps.py
- litellm/translation/engine/http.py
pattern-either:
- pattern: time.time(...)
- pattern: time.time_ns(...)
- pattern: time.monotonic(...)
- pattern: time.monotonic_ns(...)
- pattern: time.perf_counter(...)
- pattern: time.perf_counter_ns(...)
- pattern: time.sleep(...)
- pattern: datetime.datetime.now(...)
- pattern: datetime.datetime.utcnow(...)
- pattern: datetime.datetime.today(...)
- pattern: datetime.date.today(...)
- pattern: datetime.now(...)
- pattern: datetime.utcnow(...)
- pattern: datetime.today(...)
- pattern: date.today(...)
metadata:
category: correctness
tags: [python, translation-v2, algebraic-effects]
confidence: HIGH
- id: translation-no-ambient-ids-or-randomness
message: >
ambient effect: generating ids or randomness (uuid.*, fastuuid,
random.*, secrets.*, os.urandom) inside litellm/translation. Inject the
generator via TranslationDeps (tenets: algebraic effects for I/O).
severity: ERROR
languages: [python]
paths:
include:
- litellm/translation
exclude:
- litellm/translation/deps.py
- litellm/translation/engine/http.py
pattern-either:
- pattern: uuid.uuid1(...)
- pattern: uuid.uuid3(...)
- pattern: uuid.uuid4(...)
- pattern: uuid.uuid5(...)
- pattern: import fastuuid
- pattern: from fastuuid import $X
- pattern: import random
- pattern: from random import $X
- pattern: import secrets
- pattern: from secrets import $X
- pattern: os.urandom(...)
metadata:
category: correctness
tags: [python, translation-v2, algebraic-effects]
confidence: HIGH
- id: translation-no-ambient-env
message: >
ambient effect: reading process environment (os.environ, os.getenv)
inside litellm/translation. Env-derived flags enter as values on
TranslationDeps, built once at the dispatch seam (tenets: algebraic
effects for I/O; dependency injection, no global singletons).
severity: ERROR
languages: [python]
paths:
include:
- litellm/translation
exclude:
- litellm/translation/deps.py
- litellm/translation/engine/http.py
pattern-either:
- pattern: os.environ
- pattern: os.environb
- pattern: os.getenv(...)
- pattern: os.putenv(...)
- pattern: os.unsetenv(...)
metadata:
category: correctness
tags: [python, translation-v2, algebraic-effects]
confidence: HIGH
- id: translation-no-ambient-filesystem
message: >
ambient effect: touching the filesystem (open(), io.open(), pathlib
read/write) inside litellm/translation. The package translates values;
file content must arrive pre-read via TranslationDeps (tenets:
algebraic effects for I/O).
severity: ERROR
languages: [python]
paths:
include:
- litellm/translation
exclude:
- litellm/translation/deps.py
- litellm/translation/engine/http.py
pattern-either:
- pattern: open(...)
- pattern: io.open(...)
- pattern: $P.read_text(...)
- pattern: $P.write_text(...)
- pattern: $P.read_bytes(...)
- pattern: $P.write_bytes(...)
- pattern: $P.open(...)
- pattern: os.remove(...)
- pattern: os.unlink(...)
- pattern: os.makedirs(...)
- pattern: os.mkdir(...)
- pattern: shutil.$F(...)
- pattern: tempfile.$F(...)
metadata:
category: correctness
tags: [python, translation-v2, algebraic-effects]
confidence: HIGH
- id: translation-no-ambient-network
message: >
ambient effect: a network client (requests/httpx/aiohttp/urllib/socket)
inside litellm/translation. The ONLY I/O surface is the injected
engine.http.HttpPort; the seam owns clients (tenets: algebraic effects
for I/O; dependency injection).
severity: ERROR
languages: [python]
paths:
include:
- litellm/translation
exclude:
- litellm/translation/deps.py
- litellm/translation/engine/http.py
pattern-either:
- pattern: import requests
- pattern: from requests import $X
- pattern: from requests.$M import $X
- pattern: import httpx
- pattern: from httpx import $X
- pattern: from httpx.$M import $X
- pattern: import aiohttp
- pattern: from aiohttp import $X
- pattern: from aiohttp.$M import $X
- pattern: import urllib
- pattern: import urllib.request
- pattern: from urllib import $X
- pattern: from urllib.$M import $X
- pattern: import socket
- pattern: from socket import $X
metadata:
category: correctness
tags: [python, translation-v2, algebraic-effects]
confidence: HIGH
@@ -0,0 +1,91 @@
# litellm/translation composition-over-inheritance, encoded (06-quality-team.md
# tenet: composition over inheritance). Scoped to the translation package.
#
# A provider is a module of pure functions; behavior is composed, never
# inherited. The only legitimate bases are structural/declarative ones:
# Protocol ports, ABC port definitions, pydantic wire models (BaseModel and
# the package's single-level WireModel base), NamedTuple/TypedDict records,
# and the Enum family. errors.py is excluded by path: it is the one module
# allowed to define error machinery, including Exception subclasses if the
# seam contract ever needs them.
#
# Engine semantics this encoding relies on (verified against semgrep
# 1.157.0): `class $C($B): ...` matches any class with at least one
# positional base and binds $B to the FIRST base only. So the first rule
# fully checks single-base classes, and the second rule closes the
# multi-base hole by banning multiple inheritance outright -- which is what
# the tenet wants anyway. A justified exception (e.g. a str+Enum mixin on
# py3.10) carries an inline `# nosemgrep: <rule-id>` with a reason.
#
# First-run measurement (2026-06-12): 25 classes with bases in the tree
# (HttpPort(Protocol); WireModel(BaseModel); 23x single-base WireModel
# subclasses), zero findings -- false-positive rate 0, rule enabled.
rules:
- id: translation-no-inheritance
message: >
inheritance: class with a non-declarative base inside
litellm/translation. Compose pure functions instead of subclassing;
allowed bases are Protocol/ABC ports, pydantic wire models
(BaseModel/WireModel), NamedTuple/TypedDict, and Enum kinds (tenets:
composition over inheritance).
severity: ERROR
languages: [python]
paths:
include:
- litellm/translation
exclude:
# the one module allowed to define error machinery (Exception bases)
- litellm/translation/errors.py
patterns:
- pattern: |
class $C($B):
...
- metavariable-pattern:
metavariable: $B
patterns:
- pattern-not: Protocol
- pattern-not: typing.Protocol
- pattern-not: Protocol[...]
- pattern-not: typing.Protocol[...]
- pattern-not: ABC
- pattern-not: abc.ABC
- pattern-not: BaseModel
- pattern-not: pydantic.BaseModel
- pattern-not: WireModel
- pattern-not: NamedTuple
- pattern-not: typing.NamedTuple
- pattern-not: TypedDict
- pattern-not: typing.TypedDict
- pattern-not: Enum
- pattern-not: enum.Enum
- pattern-not: StrEnum
- pattern-not: enum.StrEnum
- pattern-not: IntEnum
- pattern-not: enum.IntEnum
- pattern-not: IntFlag
- pattern-not: enum.IntFlag
metadata:
category: correctness
tags: [python, translation-v2, composition-over-inheritance]
confidence: HIGH
- id: translation-no-multiple-inheritance
message: >
multiple inheritance inside litellm/translation. One declarative base
at most; compose behavior with functions instead of mixins (tenets:
composition over inheritance).
severity: ERROR
languages: [python]
paths:
include:
- litellm/translation
exclude:
- litellm/translation/errors.py
pattern: |
class $C($B1, $B2):
...
metadata:
category: correctness
tags: [python, translation-v2, composition-over-inheritance]
confidence: HIGH
+2
View File
@@ -167,6 +167,8 @@ lint-translation: install-test-deps
uv tool run --from 'pyright==1.1.406' pyright --project litellm/translation
uv tool run --from 'semgrep==1.157.0' semgrep scan --config .semgrep/rules litellm/translation --error -q
$(UV_RUN) lint-imports
$(UV_RUN) python scripts/check_translation_file_sizes.py
$(UV_RUN) python scripts/check_translation_claude_md.py
LITELLM_LOCAL_MODEL_COST_MAP=True $(UV_RUN) pytest tests/test_litellm/translation --tb=short -q
test-unit-root: install-test-deps
+9
View File
@@ -4,6 +4,12 @@
# module-global rebinding (PLW0603). Inherits the repo config; ruff's
# hierarchical discovery applies this file to this subtree automatically,
# so the repo-wide `ruff check` enforces it with no extra wiring.
#
# Match/union hygiene (tenet: tagged unions + match over branching on
# types): ruff 0.15 has no use-match-over-isinstance rule, so exhaustiveness
# stays with pyright (reportMatchNotExhaustive + assert_never). What IS
# deterministic here: E721 (no `type(x) ==` branching), SIM101 (no
# duplicated isinstance chains), FURB168 (no isinstance None checks).
extend = "../../ruff.toml"
preview = true
@@ -15,6 +21,9 @@ extend-select = [
"PLR0915",
"ANN",
"PLW0603",
"E721",
"SIM101",
"FURB168",
]
[lint.mccabe]
+84
View File
@@ -0,0 +1,84 @@
"""CLAUDE.md structural-freshness gate for litellm/translation
(06-quality-team.md tenet: deliberate file/folder structure with a
hand-written CLAUDE.md mapping it).
Structural only, no prose judgment:
- litellm/translation/CLAUDE.md must exist
- every real directory under the package (any depth) must be mentioned as
``<name>/`` somewhere in CLAUDE.md
- every ``<name>/`` token in CLAUDE.md's tree code block must be a real
directory (no phantom dirs)
Run: python scripts/check_translation_claude_md.py
"""
from __future__ import annotations
import re
import sys
from pathlib import Path
PACKAGE = Path(__file__).resolve().parent.parent / "litellm" / "translation"
CLAUDE_MD = PACKAGE / "CLAUDE.md"
DIR_TOKEN = re.compile(r"([A-Za-z_][\w.]*)/")
def is_tooling_dir(path: Path) -> bool:
parts = path.relative_to(PACKAGE).parts
return any(part == "__pycache__" or part.startswith(".") for part in parts)
def real_dirs() -> frozenset[str]:
return frozenset(
p.name
for p in PACKAGE.rglob("*")
if p.is_dir() and not is_tooling_dir(p)
)
def tree_block_lines(text: str) -> tuple[str, ...]:
blocks = re.findall(r"^```\n(.*?)^```$", text, re.DOTALL | re.MULTILINE)
if not blocks:
return ()
return tuple(line.split("#", 1)[0] for line in blocks[0].splitlines())
def main() -> int:
if not CLAUDE_MD.is_file():
print(
"CLAUDE.md freshness: litellm/translation/CLAUDE.md is missing "
"(tenets: deliberate structure, hand-written CLAUDE.md at the root)"
)
return 1
text = CLAUDE_MD.read_text(encoding="utf-8")
dirs = real_dirs()
unmentioned = sorted(d for d in dirs if f"{d}/" not in text)
claimed = frozenset(
m for line in tree_block_lines(text) for m in DIR_TOKEN.findall(line)
)
phantoms = sorted(claimed - dirs - {"translation"})
if not unmentioned and not phantoms:
print(
f"CLAUDE.md freshness OK: {len(dirs)} directories all mapped, "
"no phantom entries"
)
return 0
for d in unmentioned:
print(
f"CLAUDE.md freshness: directory '{d}/' exists but is not in "
"litellm/translation/CLAUDE.md; map it (tenets: deliberate "
"structure, CLAUDE.md kept true)"
)
for d in phantoms:
print(
f"CLAUDE.md freshness: CLAUDE.md's tree names '{d}/' but no such "
"directory exists under litellm/translation; prune it (tenets: "
"deliberate structure, CLAUDE.md kept true)"
)
return 1
if __name__ == "__main__":
sys.exit(main())
+50
View File
@@ -0,0 +1,50 @@
"""No-monster-files gate for litellm/translation (06-quality-team.md tenet:
no monster files or god objects).
Hard line cap per .py file. 720 = 1.3x today's largest file (ir.py, 555
lines, the deliberate home of every IR union) so the current tree passes
with ~30% headroom; everything else is under 360. Raising the cap is a
reviewed decision, not a drive-by edit.
Run: python scripts/check_translation_file_sizes.py
"""
from __future__ import annotations
import sys
from pathlib import Path
MAX_LINES = 720
PACKAGE = Path(__file__).resolve().parent.parent / "litellm" / "translation"
def line_count(path: Path) -> int:
with path.open("r", encoding="utf-8") as handle:
return sum(1 for _ in handle)
def main() -> int:
counts = sorted(
((line_count(p), p) for p in PACKAGE.rglob("*.py")), reverse=True
)
offenders = [(n, p) for n, p in counts if n > MAX_LINES]
if not offenders:
biggest, where = counts[0]
print(
f"translation file sizes OK: {len(counts)} files, "
f"largest {where.relative_to(PACKAGE.parent.parent)} at {biggest} "
f"lines (cap {MAX_LINES})"
)
return 0
print(
f"monster file: {len(offenders)} file(s) over the {MAX_LINES}-line cap "
"(tenets: no monster files or god objects). Split along the package "
"map in litellm/translation/CLAUDE.md:"
)
for n, p in offenders:
print(f" {p.relative_to(PACKAGE.parent.parent)}: {n} lines")
return 1
if __name__ == "__main__":
sys.exit(main())