|
2 | 2 |
|
3 | 3 | These stay bounded (2–3 files). They isolate failure modes the seed and |
4 | 4 | extended corpus do not: leftover type aliases, catch-site drift, field |
5 | | -aliases, a leftover keyword-parameter alias, and a public facade that |
6 | | -must survive a helper signature change. |
| 5 | +aliases, a leftover keyword-parameter alias, a leftover payload-key |
| 6 | +alias, and a public facade that must survive a helper signature change. |
7 | 7 |
|
8 | 8 | Golden strings are fixtures for the scorer and stub Ollama, not live |
9 | 9 | model dumps. Live rates are not claimed here. |
|
20 | 20 | "rename_dataclass_field", |
21 | 21 | "widen_return_keep_facade", |
22 | 22 | "rename_kwarg_across_files", |
| 23 | + "rename_payload_key_across_files", |
23 | 24 | ) |
24 | 25 |
|
25 | 26 |
|
@@ -85,6 +86,12 @@ def _keyword_used(tree: ast.AST, name: str) -> bool: |
85 | 86 | ) |
86 | 87 |
|
87 | 88 |
|
| 89 | +def _string_used(tree: ast.AST, value: str) -> bool: |
| 90 | + return any( |
| 91 | + isinstance(node, ast.Constant) and node.value == value for node in ast.walk(tree) |
| 92 | + ) |
| 93 | + |
| 94 | + |
88 | 95 | def _function_returns_tuple(tree: ast.AST, name: str) -> bool: |
89 | 96 | if not isinstance(tree, ast.Module): |
90 | 97 | return False |
@@ -762,6 +769,109 @@ def _kwarg_renamed(by_path: dict[str, str]) -> str | None: |
762 | 769 | return None |
763 | 770 |
|
764 | 771 |
|
| 772 | +# --- rename payload key user_id -> account_id across builder and reader --- |
| 773 | + |
| 774 | +RECORD_SOURCE = """\ |
| 775 | +def make(name: str) -> dict[str, str]: |
| 776 | + return {"user_id": name} |
| 777 | +""" |
| 778 | + |
| 779 | +LABEL_SOURCE = """\ |
| 780 | +from record import make |
| 781 | +
|
| 782 | +
|
| 783 | +def label(name: str) -> str: |
| 784 | + payload = make(name) |
| 785 | + return f"acct:{payload['user_id']}" |
| 786 | +""" |
| 787 | + |
| 788 | +RENAME_PAYLOAD_TASK = ( |
| 789 | + "Rename the payload key user_id to account_id in record.py and label.py. " |
| 790 | + "Update make() so it writes account_id only, and label() so it reads " |
| 791 | + "account_id only. Do not keep user_id as a dict key, .get() fallback, " |
| 792 | + "or compatibility alias. Preserve label('ada') == 'acct:ada', " |
| 793 | + "label('') == 'acct:', and make('ada') == {'account_id': 'ada'}. " |
| 794 | + "Return two fenced Python files with path comments record.py and " |
| 795 | + "label.py, no prose." |
| 796 | +) |
| 797 | + |
| 798 | +RENAME_PAYLOAD_GOLDEN = '''\ |
| 799 | +```python |
| 800 | +# record.py |
| 801 | +def make(name: str) -> dict[str, str]: |
| 802 | + return {"account_id": name} |
| 803 | +``` |
| 804 | +
|
| 805 | +```python |
| 806 | +# label.py |
| 807 | +from record import make |
| 808 | +
|
| 809 | +
|
| 810 | +def label(name: str) -> str: |
| 811 | + payload = make(name) |
| 812 | + return f"acct:{payload['account_id']}" |
| 813 | +``` |
| 814 | +''' |
| 815 | + |
| 816 | +RENAME_PAYLOAD_PARTIAL = '''\ |
| 817 | +```python |
| 818 | +# record.py |
| 819 | +def make(name: str) -> dict[str, str]: |
| 820 | + return {"account_id": name} |
| 821 | +``` |
| 822 | +''' |
| 823 | + |
| 824 | +RENAME_PAYLOAD_ALIAS = '''\ |
| 825 | +```python |
| 826 | +# record.py |
| 827 | +def make(name: str) -> dict[str, str]: |
| 828 | + return {"account_id": name, "user_id": name} |
| 829 | +``` |
| 830 | +
|
| 831 | +```python |
| 832 | +# label.py |
| 833 | +from record import make |
| 834 | +
|
| 835 | +
|
| 836 | +def label(name: str) -> str: |
| 837 | + payload = make(name) |
| 838 | + return f"acct:{payload.get('account_id') or payload.get('user_id')}" |
| 839 | +``` |
| 840 | +''' |
| 841 | + |
| 842 | +RENAME_PAYLOAD_PREFIX = '''\ |
| 843 | +```python |
| 844 | +# record.py |
| 845 | +def make(name: str) -> dict[str, str]: |
| 846 | + return {"account_id": name} |
| 847 | +``` |
| 848 | +
|
| 849 | +```python |
| 850 | +# label.py |
| 851 | +from record import make |
| 852 | +
|
| 853 | +
|
| 854 | +def label(name: str) -> str: |
| 855 | + payload = make(name) |
| 856 | + return f"user:{payload['account_id']}" |
| 857 | +``` |
| 858 | +''' |
| 859 | + |
| 860 | + |
| 861 | +def _payload_key_renamed(by_path: dict[str, str]) -> str | None: |
| 862 | + try: |
| 863 | + record = ast.parse(by_path.get("record.py", "")) |
| 864 | + label = ast.parse(by_path.get("label.py", "")) |
| 865 | + except SyntaxError as exc: |
| 866 | + return f"unparseable Python: {exc.msg}" |
| 867 | + for path, tree in (("record.py", record), ("label.py", label)): |
| 868 | + if _string_used(tree, "user_id") or _name_used(tree, "user_id"): |
| 869 | + return f"{path} still references user_id; do not keep a key alias" |
| 870 | + if not _string_used(tree, "account_id"): |
| 871 | + return f"{path} must use the account_id payload key" |
| 872 | + return None |
| 873 | + |
| 874 | + |
765 | 875 | HARDER_CASES: tuple[EvalCase, ...] = ( |
766 | 876 | EvalCase( |
767 | 877 | id="rename_exception_across_files", |
@@ -834,18 +944,38 @@ def _kwarg_renamed(by_path: dict[str, str]) -> str | None: |
834 | 944 | ), |
835 | 945 | max_tokens=1200, |
836 | 946 | ), |
| 947 | + EvalCase( |
| 948 | + id="rename_payload_key_across_files", |
| 949 | + tool="local_refactor", |
| 950 | + task=RENAME_PAYLOAD_TASK, |
| 951 | + files=( |
| 952 | + {"path": "record.py", "content": RECORD_SOURCE}, |
| 953 | + {"path": "label.py", "content": LABEL_SOURCE}, |
| 954 | + ), |
| 955 | + required_paths=("record.py", "label.py"), |
| 956 | + required_top_level=("make", "label"), |
| 957 | + extra_structure=_payload_key_renamed, |
| 958 | + behavior=( |
| 959 | + BehaviorCheck("label", "label", ("ada",), "acct:ada"), |
| 960 | + BehaviorCheck("label", "label", ("",), "acct:"), |
| 961 | + BehaviorCheck("record", "make", ("ada",), {"account_id": "ada"}), |
| 962 | + ), |
| 963 | + max_tokens=1200, |
| 964 | + ), |
837 | 965 | ) |
838 | 966 |
|
839 | 967 | HARDER_GOLDEN = { |
840 | 968 | "rename_exception_across_files": RENAME_EXCEPTION_GOLDEN, |
841 | 969 | "rename_dataclass_field": RENAME_FIELD_GOLDEN, |
842 | 970 | "widen_return_keep_facade": WIDEN_RETURN_GOLDEN, |
843 | 971 | "rename_kwarg_across_files": RENAME_KWARG_GOLDEN, |
| 972 | + "rename_payload_key_across_files": RENAME_PAYLOAD_GOLDEN, |
844 | 973 | } |
845 | 974 |
|
846 | 975 | HARDER_OBSERVED_FIRST = { |
847 | 976 | "rename_exception_across_files": RENAME_EXCEPTION_PARTIAL, |
848 | 977 | "rename_dataclass_field": RENAME_FIELD_PARTIAL, |
849 | 978 | "widen_return_keep_facade": WIDEN_RETURN_PARTIAL, |
850 | 979 | "rename_kwarg_across_files": RENAME_KWARG_PARTIAL, |
| 980 | + "rename_payload_key_across_files": RENAME_PAYLOAD_PARTIAL, |
851 | 981 | } |
0 commit comments