The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
15 lines
4.7 KiB
JSON
15 lines
4.7 KiB
JSON
{
|
|
"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 — 코드베이스 정적 검색"
|
|
}
|