Files
document-haness/docs/clean-architecture-backend-template/final/evidence/rendered/a10-f005-hyperloglog-merge-scope.svg
T
DongHyeonkaandClaude Opus 5 b2963105a8 docs(keycloak-session-store): import the session-storage lab as a new project
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>
2026-09-04 22:51:59 +09:00

151 lines
12 KiB
XML

<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1448" viewBox="0 0 1200 1448" role="img">
<title>terminal evidence</title>
<desc>Terminal-style rendering generated from retained command output. Sensitive-looking values are redacted in the visual asset.</desc>
<rect x="1" y="1" width="1198" height="1446" rx="14" fill="#0d1117" stroke="#30363d"/>
<rect x="1" y="1" width="1198" height="44" rx="14" fill="#161b22"/>
<rect x="1" y="30" width="1198" height="14" fill="#161b22"/>
<circle cx="24" cy="22" r="6" fill="#ff5f57"/>
<circle cx="44" cy="22" r="6" fill="#febc2e"/>
<circle cx="64" cy="22" r="6" fill="#28c840"/>
<text x="92" y="27" fill="#8b949e" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace">terminal evidence</text>
<text x="24" y="68" fill="#c9d1d9" font-size="15" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">$ cat &gt; /tmp/r2_budget_scan.py &lt;&lt;&#x27;PY&#x27;
# -*- coding: utf-8 -*-
&quot;&quot;&quot;R2 명령마다, 비용 상한이 호출자 서명에서 오는지 SDK가 </tspan><tspan x="24" dy="22">만들어 넣는지 가른다.
명령 이름이 나오는 메서드에 예산이 없으면, 그 메서드가 같은 파일에서 부르는
헬퍼까지 두 단계 따라</tspan><tspan x="24" dy="22">간다. 거기서도 못 찾으면 미판정으로 남긴다.
&quot;&quot;&quot;
import re
import pathlib
root = pathlib.Path(&#x27;src/main&#x27;)
risk, cur = {}</tspan><tspan x="24" dy="22">, None
for line in (root / &#x27;resources/redis-sdk/redis-command-policy.yml&#x27;).read_text().splitlines():
m = re.match(r&#x27;</tspan><tspan x="24" dy="22">^ ([A-Z][A-Z0-9_. -]*):\s*$&#x27;, line)
if m:
cur = m.group(1).strip()
m = re.match(r&#x27;^ risk: (R\d)&#x27;, li</tspan><tspan x="24" dy="22">ne)
if m and cur:
risk[cur] = m.group(1)
DERIVED = (&#x27;context.collectionBudget(&#x27;, &#x27;context.scanBudget(&#x27;, &#x27;co</tspan><tspan x="24" dy="22">ntext.scriptBudget(&#x27;)
NAME = re.compile(r&#x27;&quot;([A-Z][A-Z0-9_. ]{1,20})&quot;&#x27;)
def methods(src):
for m in re.finditer(r&#x27;\n</tspan><tspan x="24" dy="22"> (?:[A-Za-z@&lt;].*?)\)\s*\{&#x27;, src, re.S):
i = src.index(&#x27;{&#x27;, m.start())
depth, j = 1, i + 1
while</tspan><tspan x="24" dy="22"> j &lt; len(src) and depth:
depth += (src[j] == &#x27;{&#x27;) - (src[j] == &#x27;}&#x27;)
j += 1
sig = src[m.s</tspan><tspan x="24" dy="22">tart():i]
name = re.findall(r&#x27;(\w+)\s*\($&#x27;, sig.split(&#x27;\n&#x27;)[-1] or sig) or re.findall(r&#x27;(\w+)\s*\(&#x27;, sig)
</tspan><tspan x="24" dy="22"> yield (name[-1] if name else &#x27;&#x27;), sig, src[i:j]
def classify(src):
&quot;&quot;&quot;kind per method, then two hops through sam</tspan><tspan x="24" dy="22">e-file helper calls.&quot;&quot;&quot;
table = {}
for name, sig, body in methods(src):
table.setdefault(name, []).appen</tspan><tspan x="24" dy="22">d({
&#x27;sig&#x27;: sig, &#x27;body&#x27;: body,
&#x27;takes&#x27;: &#x27;OperationBudget budget&#x27; in sig,
&#x27;makes&#x27;: any</tspan><tspan x="24" dy="22">(d in body for d in DERIVED),
})
def kind_of(name, depth=0):
for entry in table.get(name, []):
</tspan><tspan x="24" dy="22"> if entry[&#x27;takes&#x27;]:
return &#x27;호출자&#x27;
if entry[&#x27;makes&#x27;]:
return &#x27;SDK&#x27;
</tspan><tspan x="24" dy="22"> if depth &gt;= 2:
return None
for entry in table.get(name, []):
for callee in set(re</tspan><tspan x="24" dy="22">.findall(r&#x27;\b([a-z]\w+)\s*\(&#x27;, entry[&#x27;body&#x27;])):
if callee in table and callee != name:
</tspan><tspan x="24" dy="22"> got = kind_of(callee, depth + 1)
if got:
return got
return None
</tspan><tspan x="24" dy="22"> out = {}
for name, sig, body in methods(src):
found = {n for n in NAME.findall(body) if risk.get(n) == &#x27;</tspan><tspan x="24" dy="22">R2&#x27;}
if not found:
continue
if &#x27;OperationBudget budget&#x27; in sig:
kind = &#x27;호출자&#x27;
</tspan><tspan x="24" dy="22"> elif any(d in body for d in DERIVED):
kind = &#x27;SDK&#x27;
else:
kind = None
</tspan><tspan x="24" dy="22"> for callee in set(re.findall(r&#x27;\b([a-z]\w+)\s*\(&#x27;, body)):
if callee in table and callee != name:
</tspan><tspan x="24" dy="22"> kind = kind_of(callee, 1)
if kind:
break
for n in fou</tspan><tspan x="24" dy="22">nd:
out.setdefault(n, set()).add(kind)
return out
found = {}
for f in sorted(root.rglob(&#x27;*.java&#x27;)):
</tspan><tspan x="24" dy="22"> for name, kinds in classify(f.read_text()).items():
found.setdefault(name, set()).update(kinds)
sdk, caller, </tspan><tspan x="24" dy="22">unknown = [], [], []
for name in sorted(found):
kinds = found[name] - {None}
(caller if &#x27;호출자&#x27; in kinds else s</tspan><tspan x="24" dy="22">dk if &#x27;SDK&#x27; in kinds else unknown).append(name)
print(&#x27;정책 카탈로그의 R2 명령 :&#x27;, sum(1 for v in risk.values() if v ==</tspan><tspan x="24" dy="22"> &#x27;R2&#x27;))
print(&#x27;src/main 에 이름이 문자열로 나오는 R2 명령 :&#x27;, len(found))
print()
print(&#x27;SDK 가 만들어 넣는다 : %d개&#x27; % </tspan><tspan x="24" dy="22">len(sdk))
for i in range(0, len(sdk), 4):
print(&#x27; &#x27;, &#x27; &#x27;.join(&#x27;%-14s&#x27; % n for n in sdk[i:i + 4]).rstrip())
print(</tspan><tspan x="24" dy="22">)
print(&#x27;호출자 서명이 받는다 : %d개&#x27; % len(caller))
for i in range(0, len(caller), 4):
print(&#x27; &#x27;, &#x27; &#x27;.join(&#x27;%-14</tspan><tspan x="24" dy="22">s&#x27; % n for n in caller[i:i + 4]).rstrip())
print()
print(&#x27;출처를 못 가린 것 : %d개&#x27; % len(unknown))
for i in range(0, le</tspan><tspan x="24" dy="22">n(unknown), 4):
print(&#x27; &#x27;, &#x27; &#x27;.join(&#x27;%-14s&#x27; % n for n in unknown[i:i + 4]).rstrip())
print()
print(&#x27;한 이름이 두 </tspan><tspan x="24" dy="22">경로에 있으면 호출자 쪽으로 센다.&#x27;)
PY
python3 -X utf8 /tmp/r2_budget_scan.py; rm -f /tmp/r2_budget_scan.py</tspan></text>
<text x="24" y="794" fill="#8b949e" font-size="13" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">cwd: /shared/codebase/clean-architecture-backend-template/src/adapter/outbound/cache-redis</tspan></text>
<text x="24" y="816" fill="#8b949e" font-size="13" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">time: 2026-09-02T12:52:36+00:00 · exit 0</tspan></text>
<line x1="24" y1="830" x2="1176" y2="830" stroke="#30363d"/>
<text x="24" y="870" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">정책 카탈로그의 R2 명령 : 109</tspan></text>
<text x="24" y="892" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">src/main 에 이름이 문자열로 나오는 R2 명령 : 56</tspan></text>
<text x="24" y="914" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
<text x="24" y="936" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">SDK 가 만들어 넣는다 : 34개</tspan></text>
<text x="24" y="958" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> BITCOUNT BITPOS BLMOVE BLPOP</tspan></text>
<text x="24" y="980" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> BRPOP DEL FT.CREATE FT.SEARCH</tspan></text>
<text x="24" y="1002" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> HSCAN LMOVE LREM LTRIM</tspan></text>
<text x="24" y="1024" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> MGET PFCOUNT PFMERGE RENAME</tspan></text>
<text x="24" y="1046" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> RENAMENX SCAN SCRIPT LOAD SMOVE</tspan></text>
<text x="24" y="1068" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> SRANDMEMBER SSCAN TS.CREATERULE TS.DELETERULE</tspan></text>
<text x="24" y="1090" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> UNLINK WATCH XAUTOCLAIM XPENDING</tspan></text>
<text x="24" y="1112" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> XRANGE XREAD XREADGROUP XREVRANGE</tspan></text>
<text x="24" y="1134" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> XTRIM ZSCAN</tspan></text>
<text x="24" y="1156" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
<text x="24" y="1178" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">호출자 서명이 받는다 : 14개</tspan></text>
<text x="24" y="1200" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> APPEND BITFIELD BITOP EVALSHA</tspan></text>
<text x="24" y="1222" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> GEOSEARCH GEOSEARCHSTORE GETRANGE HGETALL</tspan></text>
<text x="24" y="1244" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> LRANGE SDIFF SETRANGE SINTER</tspan></text>
<text x="24" y="1266" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> SUNION ZRANGE</tspan></text>
<text x="24" y="1288" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
<text x="24" y="1310" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">출처를 못 가린 것 : 8개</tspan></text>
<text x="24" y="1332" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> FCALL FCALL_RO JSON.GET JSON.OBJKEYS</tspan></text>
<text x="24" y="1354" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> SORT SORT_RO TOPK.LIST TS.RANGE</tspan></text>
<text x="24" y="1376" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"></tspan></text>
<text x="24" y="1398" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">한 이름이 두 경로에 있으면 호출자 쪽으로 센다.</tspan></text>
</svg>