{ "assetKey": "a10-f005-hyperloglog-merge-scope", "kind": "terminal", "command": "cat > /tmp/r2_budget_scan.py <<'PY'\n# -*- coding: utf-8 -*-\n\"\"\"R2 명령마다, 비용 상한이 호출자 서명에서 오는지 SDK가 만들어 넣는지 가른다.\n\n명령 이름이 나오는 메서드에 예산이 없으면, 그 메서드가 같은 파일에서 부르는\n헬퍼까지 두 단계 따라간다. 거기서도 못 찾으면 미판정으로 남긴다.\n\"\"\"\nimport re\nimport pathlib\n\nroot = pathlib.Path('src/main')\nrisk, cur = {}, None\nfor line in (root / 'resources/redis-sdk/redis-command-policy.yml').read_text().splitlines():\n m = re.match(r'^ ([A-Z][A-Z0-9_. -]*):\\s*$', line)\n if m:\n cur = m.group(1).strip()\n m = re.match(r'^ risk: (R\\d)', line)\n if m and cur:\n risk[cur] = m.group(1)\n\nDERIVED = ('context.collectionBudget(', 'context.scanBudget(', 'context.scriptBudget(')\nNAME = re.compile(r'\"([A-Z][A-Z0-9_. ]{1,20})\"')\n\n\ndef methods(src):\n for m in re.finditer(r'\\n (?:[A-Za-z@<].*?)\\)\\s*\\{', src, re.S):\n i = src.index('{', m.start())\n depth, j = 1, i + 1\n while j < len(src) and depth:\n depth += (src[j] == '{') - (src[j] == '}')\n j += 1\n sig = src[m.start():i]\n name = re.findall(r'(\\w+)\\s*\\($', sig.split('\\n')[-1] or sig) or re.findall(r'(\\w+)\\s*\\(', sig)\n yield (name[-1] if name else ''), sig, src[i:j]\n\n\ndef classify(src):\n \"\"\"kind per method, then two hops through same-file helper calls.\"\"\"\n table = {}\n for name, sig, body in methods(src):\n table.setdefault(name, []).append({\n 'sig': sig, 'body': body,\n 'takes': 'OperationBudget budget' in sig,\n 'makes': any(d in body for d in DERIVED),\n })\n\n def kind_of(name, depth=0):\n for entry in table.get(name, []):\n if entry['takes']:\n return '호출자'\n if entry['makes']:\n return 'SDK'\n if depth >= 2:\n return None\n for entry in table.get(name, []):\n for callee in set(re.findall(r'\\b([a-z]\\w+)\\s*\\(', entry['body'])):\n if callee in table and callee != name:\n got = kind_of(callee, depth + 1)\n if got:\n return got\n return None\n\n out = {}\n for name, sig, body in methods(src):\n found = {n for n in NAME.findall(body) if risk.get(n) == 'R2'}\n if not found:\n continue\n if 'OperationBudget budget' in sig:\n kind = '호출자'\n elif any(d in body for d in DERIVED):\n kind = 'SDK'\n else:\n kind = None\n for callee in set(re.findall(r'\\b([a-z]\\w+)\\s*\\(', body)):\n if callee in table and callee != name:\n kind = kind_of(callee, 1)\n if kind:\n break\n for n in found:\n out.setdefault(n, set()).add(kind)\n return out\n\n\nfound = {}\nfor f in sorted(root.rglob('*.java')):\n for name, kinds in classify(f.read_text()).items():\n found.setdefault(name, set()).update(kinds)\n\nsdk, caller, unknown = [], [], []\nfor name in sorted(found):\n kinds = found[name] - {None}\n (caller if '호출자' in kinds else sdk if 'SDK' in kinds else unknown).append(name)\n\nprint('정책 카탈로그의 R2 명령 :', sum(1 for v in risk.values() if v == 'R2'))\nprint('src/main 에 이름이 문자열로 나오는 R2 명령 :', len(found))\nprint()\nprint('SDK 가 만들어 넣는다 : %d개' % len(sdk))\nfor i in range(0, len(sdk), 4):\n print(' ', ' '.join('%-14s' % n for n in sdk[i:i + 4]).rstrip())\nprint()\nprint('호출자 서명이 받는다 : %d개' % len(caller))\nfor i in range(0, len(caller), 4):\n print(' ', ' '.join('%-14s' % n for n in caller[i:i + 4]).rstrip())\nprint()\nprint('출처를 못 가린 것 : %d개' % len(unknown))\nfor i in range(0, len(unknown), 4):\n print(' ', ' '.join('%-14s' % n for n in unknown[i:i + 4]).rstrip())\nprint()\nprint('한 이름이 두 경로에 있으면 호출자 쪽으로 센다.')\nPY\npython3 -X utf8 /tmp/r2_budget_scan.py; rm -f /tmp/r2_budget_scan.py", "cwd": "/shared/codebase/clean-architecture-backend-template/src/adapter/outbound/cache-redis", "exitCode": 0, "executedAt": "2026-09-02T12:52:36+00:00", "sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916", "raw": "evidence/raw/a10-f005-hyperloglog-merge-scope.txt", "svg": "evidence/rendered/a10-f005-hyperloglog-merge-scope.svg", "rawSha256": "46574b8edc50afe45a313b03759d6b8fd84ea5b9399b51377b0dd1f41c27cfbf", "lines": 25, "redaction": "none — 코드베이스 정적 검색" }