From 464ce305c8eb92ef70e19f37cca900daaa2e09b3 Mon Sep 17 00:00:00 2001 From: mikkel Date: Tue, 13 Oct 2026 22:41:00 -0600 Subject: [PATCH 1/2] filter_relevant: reject JavaScript camelCase options A camelCase kwarg from the JS filter raises TypeError before any model call, naming every bad key and its Python spelling. retries is max_attempts = retries + 1, retryBaseMs is retry_backoff in seconds, and ignore is skip_patterns. Other unknown keywords still fail as before. --- patchling/relevance.py | 55 +++++++++++ tests/test_filter_options.py | 183 +++++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) create mode 100644 tests/test_filter_options.py diff --git a/patchling/relevance.py b/patchling/relevance.py index fed7bb3..2b14c49 100644 --- a/patchling/relevance.py +++ b/patchling/relevance.py @@ -935,6 +935,51 @@ def _ask( return raw, prompt_tokens, completion_tokens, total_tokens, sent_effort +# CamelCase names accepted by JS ``filterRelevant``, mapped to the Python +# spelling. Inverse of ``SNAKE_CASE_OPTIONS`` in 255BITS/patchling +# ``src/filter.js`` at ad02203 (PR #7). ``sleep`` has no camelCase form. +# ``retry_base_ms`` is a second snake_case spelling of ``retryBaseMs``; the +# Python parameter is ``retry_backoff``. +CAMEL_CASE_OPTIONS = { + "apiKey": "use api_key", + "baseUrl": "use base_url", + "maxBytes": "use max_bytes", + "maxTokens": "use max_tokens", + "onOversize": "use on_oversize", + "onBinary": "use on_binary", + "maxOutputTokens": "use max_output_tokens", + "ignore": ( + "use skip_patterns (a sequence replaces DEFAULT_SKIP_PATTERNS; " + "an empty sequence classifies every path)" + ), + "retries": ( + "use max_attempts, which counts total attempts: " + "max_attempts = retries + 1" + ), + "retryBaseMs": ( + "use retry_backoff, in seconds: retry_backoff = retryBaseMs / 1000" + ), + "callLlm": "use call_llm", + "onDecision": "use on_decision", + "reasoningEffort": "use reasoning_effort", +} + + +def _reject_unknown_options(kwargs): + """Reject JS camelCase options, then any other unexpected keyword. + + CamelCase keys are reported together, with the Python spelling, before + any model call. Other unexpected keywords keep Python's usual message. + """ + wrong = [key for key in kwargs if key in CAMEL_CASE_OPTIONS] + if wrong: + fixes = "; ".join(f"{key}: {CAMEL_CASE_OPTIONS[key]}" for key in wrong) + raise TypeError(f"filter_relevant options are snake_case in Python. {fixes}") + if kwargs: + key = next(iter(kwargs)) + raise TypeError(f"filter_relevant() got an unexpected keyword argument {key!r}") + + def filter_relevant( files: dict, task: str, @@ -954,6 +999,7 @@ def filter_relevant( retry_backoff: float = DEFAULT_RETRY_BACKOFF, sleep: Optional[Callable[[float], None]] = None, call_llm: Optional[Callable] = None, + **kwargs, ): """Ask a cheap model which files are relevant to ``task``. @@ -989,7 +1035,16 @@ def filter_relevant( Credentials resolve the same way as the rest of the library: explicit ``api_key`` / ``base_url``, then ``GPTDIFF_LLM_API_KEY`` and ``GPTDIFF_LLM_BASE_URL`` (including ``set_env`` overrides). + + JavaScript camelCase options (``onBinary``, ``maxBytes``, ``ignore``, + ``retries``, ``retryBaseMs``, and every other key of + ``CAMEL_CASE_OPTIONS``) raise ``TypeError`` before any model call. The + message names each one and the Python spelling. ``retries`` is + ``max_attempts = retries + 1``. ``retryBaseMs`` is + ``retry_backoff = retryBaseMs / 1000`` seconds. ``ignore`` is + ``skip_patterns``. Any other unknown keyword still raises ``TypeError``. """ + _reject_unknown_options(kwargs) if not isinstance(files, dict): raise TypeError("files must be a dict of path -> content") if not isinstance(task, str) or task.strip() == "": diff --git a/tests/test_filter_options.py b/tests/test_filter_options.py new file mode 100644 index 0000000..7dfd0b1 --- /dev/null +++ b/tests/test_filter_options.py @@ -0,0 +1,183 @@ +"""CamelCase option names from the JS filter are rejected with Python spellings. + +No network. The injected client fails the test if ``filter_relevant`` calls it. +""" + +import re + +import pytest + +from patchling import filter_relevant +from patchling.relevance import CAMEL_CASE_OPTIONS + +# Vendored from 255BITS/patchling src/filter.js SNAKE_CASE_OPTIONS +# at ad022037d59b6c49e16e3b2f29e83dddf4e5faf2 (JS PR #7). +JS_SNAKE_CASE_OPTIONS = { + "api_key": "use apiKey", + "base_url": "use baseUrl", + "max_bytes": "use maxBytes", + "max_tokens": "use maxTokens", + "on_oversize": "use onOversize", + "on_binary": "use onBinary", + "max_output_tokens": "use maxOutputTokens", + "skip_patterns": "use ignore (an array replaces DEFAULT_IGNORE_PATTERNS; [] or false classifies every path)", + "max_attempts": "use retries, which counts extra attempts: retries = max_attempts - 1", + "retry_backoff": "use retryBaseMs, in milliseconds: retryBaseMs = retry_backoff * 1000", + "sleep": "not supported in JS; backoff uses timers. In tests pass retryBaseMs: 0 to skip the waits", + "call_llm": "use callLlm", + "on_decision": "use onDecision", + "reasoning_effort": "use reasoningEffort", + "retry_base_ms": "use retryBaseMs", +} + +_USE_NAME = re.compile(r"^use ([A-Za-z_][A-Za-z0-9_]*)\b") + + +def _recommended(guidance): + match = _USE_NAME.match(guidance) + if match is None: + return None + return match.group(1) + + +def _fail_if_called(*_args, **_kwargs): + raise AssertionError("model was called") + + +@pytest.mark.parametrize("key", list(CAMEL_CASE_OPTIONS)) +def test_each_camel_case_option_names_its_python_equivalent(key): + with pytest.raises(TypeError) as exc_info: + filter_relevant( + {"a.py": "x = 1\n"}, + "task", + cache=False, + api_key="k", + call_llm=_fail_if_called, + **{key: "x"}, + ) + message = str(exc_info.value) + assert message.startswith("filter_relevant options are snake_case in Python. ") + assert f"{key}: {CAMEL_CASE_OPTIONS[key]}" in message + + +def test_several_camel_case_options_are_named_in_one_error(): + keys = ["onBinary", "maxBytes", "retries", "ignore", "retryBaseMs"] + with pytest.raises(TypeError) as exc_info: + filter_relevant( + {"a.py": "x = 1\n"}, + "task", + cache=False, + api_key="k", + call_llm=_fail_if_called, + onBinary="include", + maxBytes=10, + retries=1, + ignore=[], + retryBaseMs=200, + ) + fixes = "; ".join(f"{key}: {CAMEL_CASE_OPTIONS[key]}" for key in keys) + assert str(exc_info.value) == f"filter_relevant options are snake_case in Python. {fixes}" + + +def test_camel_case_options_raise_before_any_model_call(): + calls = {"n": 0} + + def fail_if_called(*_args, **_kwargs): + calls["n"] += 1 + raise AssertionError("model was called") + + with pytest.raises(TypeError, match="onBinary: use on_binary"): + filter_relevant( + {"a.py": "x = 1\n"}, + "task", + cache=False, + api_key="k", + call_llm=fail_if_called, + onBinary="include", + ) + assert calls["n"] == 0 + + +def test_python_and_js_option_maps_are_exact_inverses(): + """Each JS ``snake -> use camel`` entry inverts to ``camel -> use snake``. + + ``sleep`` has no camelCase spelling. ``retry_base_ms`` is an alias of + ``retryBaseMs``; the Python parameter behind that name is ``retry_backoff``. + """ + js_pairs = {} + aliases = {} + for snake, guidance in JS_SNAKE_CASE_OPTIONS.items(): + camel = _recommended(guidance) + if camel is None: + assert snake == "sleep" + assert "not supported" in guidance + continue + if camel in js_pairs: + aliases.setdefault(camel, []).append(snake) + continue + js_pairs[camel] = snake + + py_pairs = { + camel: _recommended(guidance) + for camel, guidance in CAMEL_CASE_OPTIONS.items() + } + assert None not in py_pairs.values() + assert py_pairs == js_pairs + assert aliases == {"retryBaseMs": ["retry_base_ms"]} + assert set(CAMEL_CASE_OPTIONS) == set(js_pairs) + for camel, snake in py_pairs.items(): + assert _recommended(JS_SNAKE_CASE_OPTIONS[snake]) == camel + assert snake in CAMEL_CASE_OPTIONS[camel] + + assert "max_attempts = retries + 1" in CAMEL_CASE_OPTIONS["retries"] + assert "retries = max_attempts - 1" in JS_SNAKE_CASE_OPTIONS["max_attempts"] + assert "retry_backoff = retryBaseMs / 1000" in CAMEL_CASE_OPTIONS["retryBaseMs"] + assert "retryBaseMs = retry_backoff * 1000" in JS_SNAKE_CASE_OPTIONS["retry_backoff"] + assert CAMEL_CASE_OPTIONS["ignore"].startswith("use skip_patterns") + assert JS_SNAKE_CASE_OPTIONS["skip_patterns"].startswith("use ignore") + assert _recommended(JS_SNAKE_CASE_OPTIONS["retry_base_ms"]) == "retryBaseMs" + + +def test_camel_case_is_reported_with_an_unrelated_unknown_keyword(): + with pytest.raises(TypeError) as exc_info: + filter_relevant( + {"a.py": "x = 1\n"}, + "task", + cache=False, + api_key="k", + call_llm=_fail_if_called, + label="mine", + onBinary="include", + maxBytes=10, + ) + message = str(exc_info.value) + assert message == ( + "filter_relevant options are snake_case in Python. " + "onBinary: use on_binary; " + "maxBytes: use max_bytes" + ) + assert "label" not in message + + +def test_unknown_non_camel_case_keyword_still_fails(): + with pytest.raises(TypeError, match=r"unexpected keyword argument 'label'"): + filter_relevant( + {"a.py": "x = 1\n"}, + "task", + cache=False, + api_key="k", + call_llm=_fail_if_called, + label="mine", + ) + + +def test_snake_case_alias_of_retry_base_ms_is_an_unknown_keyword(): + with pytest.raises(TypeError, match=r"unexpected keyword argument 'retry_base_ms'"): + filter_relevant( + {"a.py": "x = 1\n"}, + "task", + cache=False, + api_key="k", + call_llm=_fail_if_called, + retry_base_ms=200, + ) From 28d25a456901d29241c8e3cf9f2182a91d39917b Mon Sep 17 00:00:00 2001 From: mikkel Date: Tue, 13 Oct 2026 23:22:51 -0600 Subject: [PATCH 2/2] filter_relevant: stop inventing kwargs for JS-only options onDecision and reasoningEffort have no filter_relevant parameter, so the TypeError now says they are not supported. retries (extra attempts) maps to max_attempts (total attempts). retryBaseMs is a millisecond base delay with no jitter, and retry_backoff is that base in seconds. ignore maps to skip_patterns, which replaces the default globs; None keeps them and an empty sequence classifies every path. --- patchling/relevance.py | 51 +++++---- tests/test_filter_options.py | 199 ++++++++++++++++++++++++++++++++--- 2 files changed, 216 insertions(+), 34 deletions(-) diff --git a/patchling/relevance.py b/patchling/relevance.py index 2b14c49..85820fd 100644 --- a/patchling/relevance.py +++ b/patchling/relevance.py @@ -935,11 +935,18 @@ def _ask( return raw, prompt_tokens, completion_tokens, total_tokens, sent_effort -# CamelCase names accepted by JS ``filterRelevant``, mapped to the Python -# spelling. Inverse of ``SNAKE_CASE_OPTIONS`` in 255BITS/patchling -# ``src/filter.js`` at ad02203 (PR #7). ``sleep`` has no camelCase form. -# ``retry_base_ms`` is a second snake_case spelling of ``retryBaseMs``; the -# Python parameter is ``retry_backoff``. +# CamelCase names accepted by JS ``filterRelevant``. Most invert +# ``SNAKE_CASE_OPTIONS`` in 255BITS/patchling ``src/filter.js`` at ad02203 +# (PR #7). ``sleep`` has no camelCase form. ``onDecision`` and +# ``reasoningEffort`` have no ``filter_relevant`` parameter. ``retry_base_ms`` +# is a second snake_case spelling of ``retryBaseMs``; the Python parameter +# is ``retry_backoff``. +# +# JS ``retries`` counts extra attempts (total = retries + 1). ``retryBaseMs`` +# is the millisecond base of an exponential wait, +# ``min(base * 2**attempt, 30000)``, with no jitter. Python ``max_attempts`` +# is the total, and ``retry_backoff`` is that base in seconds +# (``sleep(retry_backoff * 2**attempt)``), with no jitter and no cap. CAMEL_CASE_OPTIONS = { "apiKey": "use api_key", "baseUrl": "use base_url", @@ -949,19 +956,21 @@ def _ask( "onBinary": "use on_binary", "maxOutputTokens": "use max_output_tokens", "ignore": ( - "use skip_patterns (a sequence replaces DEFAULT_SKIP_PATTERNS; " - "an empty sequence classifies every path)" - ), - "retries": ( - "use max_attempts, which counts total attempts: " - "max_attempts = retries + 1" + "use skip_patterns (case-insensitive globs on the full relative path, " + "not a regex or a substring; ** crosses directories and * does not; " + "a sequence replaces DEFAULT_SKIP_PATTERNS and does not extend it; " + "None applies the defaults; " + "[] or () classifies every path, same as JS ignore=[] or ignore=false; " + "JS ignore=null also classifies every path, while Python None does not)" ), + "retries": "retries=N (extra attempts) -> max_attempts=N+1 (total attempts)", "retryBaseMs": ( - "use retry_backoff, in seconds: retry_backoff = retryBaseMs / 1000" + "retryBaseMs=M (milliseconds, base delay) -> " + "retry_backoff=M/1000 (seconds, base delay)" ), "callLlm": "use call_llm", - "onDecision": "use on_decision", - "reasoningEffort": "use reasoning_effort", + "onDecision": "not supported in Python filter_relevant", + "reasoningEffort": "not supported in Python filter_relevant", } @@ -1038,11 +1047,15 @@ def filter_relevant( JavaScript camelCase options (``onBinary``, ``maxBytes``, ``ignore``, ``retries``, ``retryBaseMs``, and every other key of - ``CAMEL_CASE_OPTIONS``) raise ``TypeError`` before any model call. The - message names each one and the Python spelling. ``retries`` is - ``max_attempts = retries + 1``. ``retryBaseMs`` is - ``retry_backoff = retryBaseMs / 1000`` seconds. ``ignore`` is - ``skip_patterns``. Any other unknown keyword still raises ``TypeError``. + ``CAMEL_CASE_OPTIONS``) raise ``TypeError`` before any model call. + ``retries=N`` (extra attempts) is ``max_attempts=N+1`` (total attempts). + ``retryBaseMs=M`` (milliseconds, base delay) is + ``retry_backoff=M/1000`` (seconds, base delay). ``ignore`` is + ``skip_patterns``: a sequence replaces the defaults and does not extend + them, ``None`` applies the defaults, and ``[]`` or ``()`` classifies + every path (JS ``[]``, ``false``, and ``null``). ``onDecision`` and + ``reasoningEffort`` are not supported. Any other unknown keyword still + raises ``TypeError``. """ _reject_unknown_options(kwargs) if not isinstance(files, dict): diff --git a/tests/test_filter_options.py b/tests/test_filter_options.py index 7dfd0b1..cabc70d 100644 --- a/tests/test_filter_options.py +++ b/tests/test_filter_options.py @@ -3,6 +3,7 @@ No network. The injected client fails the test if ``filter_relevant`` calls it. """ +import inspect import re import pytest @@ -31,6 +32,13 @@ } _USE_NAME = re.compile(r"^use ([A-Za-z_][A-Za-z0-9_]*)\b") +_ARROW_NAME = re.compile(r"->\s*([A-Za-z_][A-Za-z0-9_]*)") + +# Vendored JS snake_case keys whose camelCase option has no filter_relevant parameter. +JS_ONLY_UNSUPPORTED = { + "on_decision": "onDecision", + "reasoning_effort": "reasoningEffort", +} def _recommended(guidance): @@ -40,6 +48,22 @@ def _recommended(guidance): return match.group(1) +def _python_parameter(guidance): + """Parameter a Python guidance string tells the caller to pass. + + ``None`` means the option is not supported and names no kwarg. + """ + if guidance.startswith("not supported"): + return None + match = _USE_NAME.match(guidance) + if match is not None: + return match.group(1) + match = _ARROW_NAME.search(guidance) + if match is not None: + return match.group(1) + return None + + def _fail_if_called(*_args, **_kwargs): raise AssertionError("model was called") @@ -99,41 +123,75 @@ def fail_if_called(*_args, **_kwargs): def test_python_and_js_option_maps_are_exact_inverses(): - """Each JS ``snake -> use camel`` entry inverts to ``camel -> use snake``. + """Vendored JS map stays literal. JS-only keys are unsupported. - ``sleep`` has no camelCase spelling. ``retry_base_ms`` is an alias of - ``retryBaseMs``; the Python parameter behind that name is ``retry_backoff``. + ``sleep`` has no camelCase spelling. ``on_decision`` and + ``reasoning_effort`` name JS options with no ``filter_relevant`` + parameter, so the Python message must not invent a kwarg. + ``retry_base_ms`` is a second JS spelling of ``retryBaseMs``; the + Python parameter behind that name is ``retry_backoff``. Every other + key is an exact inverse. """ + parameters = set(inspect.signature(filter_relevant).parameters) js_pairs = {} aliases = {} for snake, guidance in JS_SNAKE_CASE_OPTIONS.items(): + if snake in JS_ONLY_UNSUPPORTED: + camel = JS_ONLY_UNSUPPORTED[snake] + assert guidance == f"use {camel}" + assert CAMEL_CASE_OPTIONS[camel] == "not supported in Python filter_relevant" + assert _python_parameter(CAMEL_CASE_OPTIONS[camel]) is None + assert snake not in parameters + assert f"use {snake}" not in CAMEL_CASE_OPTIONS[camel] + continue camel = _recommended(guidance) if camel is None: assert snake == "sleep" - assert "not supported" in guidance + assert guidance.startswith("not supported") continue if camel in js_pairs: aliases.setdefault(camel, []).append(snake) continue js_pairs[camel] = snake - py_pairs = { - camel: _recommended(guidance) - for camel, guidance in CAMEL_CASE_OPTIONS.items() - } - assert None not in py_pairs.values() + py_pairs = {} + for camel, guidance in CAMEL_CASE_OPTIONS.items(): + if camel in JS_ONLY_UNSUPPORTED.values(): + continue + name = _python_parameter(guidance) + assert name is not None + assert name in parameters + py_pairs[camel] = name + assert py_pairs == js_pairs assert aliases == {"retryBaseMs": ["retry_base_ms"]} - assert set(CAMEL_CASE_OPTIONS) == set(js_pairs) + assert "retry_base_ms" not in parameters + assert set(CAMEL_CASE_OPTIONS) == set(js_pairs) | set(JS_ONLY_UNSUPPORTED.values()) for camel, snake in py_pairs.items(): assert _recommended(JS_SNAKE_CASE_OPTIONS[snake]) == camel assert snake in CAMEL_CASE_OPTIONS[camel] - assert "max_attempts = retries + 1" in CAMEL_CASE_OPTIONS["retries"] - assert "retries = max_attempts - 1" in JS_SNAKE_CASE_OPTIONS["max_attempts"] - assert "retry_backoff = retryBaseMs / 1000" in CAMEL_CASE_OPTIONS["retryBaseMs"] - assert "retryBaseMs = retry_backoff * 1000" in JS_SNAKE_CASE_OPTIONS["retry_backoff"] - assert CAMEL_CASE_OPTIONS["ignore"].startswith("use skip_patterns") + assert CAMEL_CASE_OPTIONS["retries"] == ( + "retries=N (extra attempts) -> max_attempts=N+1 (total attempts)" + ) + assert JS_SNAKE_CASE_OPTIONS["max_attempts"] == ( + "use retries, which counts extra attempts: retries = max_attempts - 1" + ) + assert CAMEL_CASE_OPTIONS["retryBaseMs"] == ( + "retryBaseMs=M (milliseconds, base delay) -> " + "retry_backoff=M/1000 (seconds, base delay)" + ) + assert JS_SNAKE_CASE_OPTIONS["retry_backoff"] == ( + "use retryBaseMs, in milliseconds: retryBaseMs = retry_backoff * 1000" + ) + ignore = CAMEL_CASE_OPTIONS["ignore"] + assert ignore.startswith("use skip_patterns") + assert "replaces DEFAULT_SKIP_PATTERNS and does not extend it" in ignore + assert "None applies the defaults" in ignore + assert "[] or () classifies every path" in ignore + assert "ignore=false" in ignore + assert "not a regex or a substring" in ignore + assert "** crosses directories and * does not" in ignore assert JS_SNAKE_CASE_OPTIONS["skip_patterns"].startswith("use ignore") assert _recommended(JS_SNAKE_CASE_OPTIONS["retry_base_ms"]) == "retryBaseMs" @@ -181,3 +239,114 @@ def test_snake_case_alias_of_retry_base_ms_is_an_unknown_keyword(): call_llm=_fail_if_called, retry_base_ms=200, ) + + +class _YesResponse: + class _Usage: + prompt_tokens = 1 + completion_tokens = 1 + total_tokens = 2 + + class _Message: + content = "yes" + + class _Choice: + message = None + + def __init__(self): + choice = self._Choice() + choice.message = self._Message() + self.choices = [choice] + self.usage = self._Usage() + + +def _classify(files, **options): + """Run the filter offline. Returns ``(result, call_count)``.""" + + def fake_call_llm(*_args, **_kwargs): + fake_call_llm.n += 1 + return _YesResponse() + + fake_call_llm.n = 0 + result = filter_relevant( + files, + "task", + model="fake", + cache=False, + api_key="k", + call_llm=fake_call_llm, + **options, + ) + return result, fake_call_llm.n + + +def _ignored(result): + return {item.path for item in result.decisions if item.skipped_reason == "ignored"} + + +def test_skip_patterns_none_applies_the_default_list(): + result, calls = _classify( + {"uv.lock": "lock\n", "src/app.py": "x = 1\n"}, + skip_patterns=None, + ) + assert _ignored(result) == {"uv.lock"} + assert result.decisions[0].decision == "no" + assert "uv.lock" not in result.files + assert "src/app.py" in result.files + assert calls == 1 + + +@pytest.mark.parametrize("empty", [[], ()]) +def test_skip_patterns_empty_sequence_classifies_every_path(empty): + files = {"uv.lock": "lock\n", "dist/app.js": "x\n", "src/app.py": "x = 1\n"} + result, calls = _classify(files, skip_patterns=empty) + assert _ignored(result) == set() + assert set(result.files) == set(files) + assert calls == 3 + + +def test_skip_patterns_custom_list_replaces_defaults(): + result, calls = _classify( + {"uv.lock": "lock\n", "notes.tmp": "temp\n", "src/app.py": "x = 1\n"}, + skip_patterns=["*.tmp"], + ) + assert _ignored(result) == {"notes.tmp"} + assert "uv.lock" in result.files + assert "notes.tmp" not in result.files + assert calls == 2 + + +def test_skip_patterns_match_globs_not_regex_or_substring(): + # ``*`` does not cross ``/``, the match is the whole path, and case is folded. + result, _calls = _classify( + { + "uv.lock": "a\n", + "dir/uv.lock": "a\n", + "file.lock.txt": "a\n", + "Notes.TMP": "a\n", + "src/keep.py": "a\n", + }, + skip_patterns=["*.lock", "*.TMP"], + ) + assert _ignored(result) == {"uv.lock", "Notes.TMP"} + + # ``**`` crosses directories. + result, _calls = _classify( + {"dir/uv.lock": "a\n", "uv.lock": "a\n", "src/keep.py": "a\n"}, + skip_patterns=["**/uv.lock"], + ) + assert _ignored(result) == {"dir/uv.lock", "uv.lock"} + + # ``.*`` as a regex would match every name. As a glob it is a leading dot. + result, _calls = _classify( + {"a.py": "a\n", ".env": "a\n", "dir/.env": "a\n"}, + skip_patterns=[".*"], + ) + assert _ignored(result) == {".env"} + + # A bare token is not a substring of a longer name. + result, _calls = _classify( + {"uv.lock": "a\n", "lock": "a\n"}, + skip_patterns=["lock"], + ) + assert _ignored(result) == {"lock"}