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>
154 lines
14 KiB
XML
154 lines
14 KiB
XML
<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1712" viewBox="0 0 1200 1712" 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="1710" 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">$ set -e
|
|
set -o pipefail
|
|
D=$(mktemp -d); trap 'rm -rf "$D"' EXIT
|
|
C=/shared/codebase/clean-architecture-backend-template
|
|
</tspan><tspan x="24" dy="22">M=$C/src/adapter/inbound/graphql
|
|
CACHE=/root/.gradle/caches/modules-2/files-2.1
|
|
java -version 2>&1 | grep version
|
|
cp=""
|
|
</tspan><tspan x="24" dy="22">while IFS= read -r line; do
|
|
coord=${line%%=*}; confs=${line#*=}
|
|
case ",$confs," in *",testRuntimeClasspath,"*) ;; *)</tspan><tspan x="24" dy="22"> continue ;; esac
|
|
g=${coord%%:*}; rest=${coord#*:}; n=${rest%%:*}; v=${rest##*:}
|
|
jar=$(find "$CACHE/$g/$n/$v" -name </tspan><tspan x="24" dy="22">'*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1) || true
|
|
[ -n "$jar" ] && cp="$cp:$jar"
|
|
done < <</tspan><tspan x="24" dy="22">(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' "$M/gradle.lockfile")
|
|
SIB=$(find "$C/src" -path '*/build/libs/*+21234e38cdb9*.jar' ! </tspan><tspan x="24" dy="22">-name '*-testkit.jar' | sort | tr '\n' ':')
|
|
|
|
mkdir -p "$D/src/dev/caskeleton/adapter/inbound/graphql/runtime"
|
|
cat > "$D/</tspan><tspan x="24" dy="22">src/dev/caskeleton/adapter/inbound/graphql/runtime/OperationNameProbe.java" <<'JAVA'
|
|
package dev.caskeleton.adapter.inbo</tspan><tspan x="24" dy="22">und.graphql.runtime;
|
|
|
|
import dev.caskeleton.adapter.inbound.graphql.api.GraphQlClientProfile;
|
|
import dev.caskeleton.adap</tspan><tspan x="24" dy="22">ter.inbound.graphql.api.GraphQlClientProfileName;
|
|
import dev.caskeleton.adapter.inbound.graphql.api.GraphQlOperationId;
|
|
</tspan><tspan x="24" dy="22">import dev.caskeleton.adapter.inbound.graphql.context.ActorRef;
|
|
import dev.caskeleton.adapter.inbound.graphql.context.Gr</tspan><tspan x="24" dy="22">aphQlDeadline;
|
|
import dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;
|
|
import dev.caskeleton.adapte</tspan><tspan x="24" dy="22">r.inbound.graphql.execution.GraphQlOperationNamePolicy;
|
|
import dev.caskeleton.adapter.inbound.graphql.execution.GraphQlO</tspan><tspan x="24" dy="22">perationSelection;
|
|
import dev.caskeleton.adapter.inbound.graphql.policy.GraphQlClientPolicy;
|
|
import dev.caskeleton.adapt</tspan><tspan x="24" dy="22">er.inbound.graphql.context.TenantContext;
|
|
import graphql.language.Document;
|
|
import graphql.parser.Parser;
|
|
import java.ti</tspan><tspan x="24" dy="22">me.Clock;
|
|
import java.time.Duration;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
|
|
/** 같은 질문에 답하는 세 구현 중 </tspan><tspan x="24" dy="22">무엇이 조립되고, 조립된 것이 무엇을 답하는지 갈라 본다. */
|
|
public final class OperationNameProbe {
|
|
|
|
private static fin</tspan><tspan x="24" dy="22">al String ANONYMOUS_SINGLE = "{ __typename }";
|
|
private static final String NAMED_SHORT = "query Ab { __typename }";
|
|
</tspan><tspan x="24" dy="22">private static final String NAMED_OK = "query HealthQuery { __typename }";
|
|
private static final String TWO_OPERATIONS </tspan><tspan x="24" dy="22">= "query A { __typename } query B { __typename }";
|
|
|
|
private static GraphQlClientPolicy policy(boolean namedOperationRe</tspan><tspan x="24" dy="22">quired) {
|
|
return new GraphQlClientPolicy(
|
|
16 * 1024, 64 * 1024, 12, 500, 50, 50, 1_000, 20, 50, 1_000L, 10_0</tspan><tspan x="24" dy="22">00, 5L * 1024 * 1024,
|
|
Duration.ofSeconds(10), false, false, namedOperationRequired);
|
|
}
|
|
|
|
private static Graph</tspan><tspan x="24" dy="22">QlRequestContext requestContext() {
|
|
return new GraphQlRequestContext(
|
|
ActorRef.anonymous(),
|
|
TenantCo</tspan><tspan x="24" dy="22">ntext.system("acme"),
|
|
new GraphQlClientProfile("first-party"),
|
|
Locale.ROOT,
|
|
new GraphQlOperation</tspan><tspan x="24" dy="22">Id("anonymous"),
|
|
"probe-trace",
|
|
GraphQlDeadline.after(Duration.ofSeconds(10), Clock.systemUTC()));
|
|
}
|
|
|
|
</tspan><tspan x="24" dy="22"> private static void wired(String label, String document, GraphQlClientPolicy policy) {
|
|
Document parsed = Parser.pa</tspan><tspan x="24" dy="22">rse(document);
|
|
var request = new GraphQlExecutionRequest(document, null, Map.of(), document.length(), 0);
|
|
var co</tspan><tspan x="24" dy="22">ntext = GraphQlExecutionContext.starting(request, requestContext(), null, parsed);
|
|
System.out.printf(" %-30s ", lab</tspan><tspan x="24" dy="22">el);
|
|
try {
|
|
var out = new GraphQlOperationSelectionHandler(policy).handle(context);
|
|
System.out.println("</tspan><tspan x="24" dy="22">통과 · operationId=" + out.requestContext().operationId().value());
|
|
} catch (RuntimeException refused) {
|
|
Syst</tspan><tspan x="24" dy="22">em.out.println(refused.getClass().getSimpleName() + " : " + refused.getMessage());
|
|
}
|
|
}
|
|
|
|
private static void unwi</tspan><tspan x="24" dy="22">red(String label, String client, String name, int operations) {
|
|
System.out.printf(" %-30s ", label);
|
|
try {
|
|
</tspan><tspan x="24" dy="22"> GraphQlOperationNamePolicy.production()
|
|
.verify(
|
|
new GraphQlClientProfileName(client),
|
|
</tspan><tspan x="24" dy="22"> new GraphQlOperationSelection(name, operations, false));
|
|
System.out.println("통과");
|
|
} catch (RuntimeEx</tspan><tspan x="24" dy="22">ception refused) {
|
|
System.out.println(refused.getClass().getSimpleName() + " : " + refused.getMessage());
|
|
}
|
|
</tspan><tspan x="24" dy="22">}
|
|
|
|
public static void main(String[] args) {
|
|
System.out.println();
|
|
System.out.println("[조립되는 GraphQlClientP</tspan><tspan x="24" dy="22">olicy 가 이 질문에 넣는 값]");
|
|
System.out.println(
|
|
" GraphQlClientPolicy.defaults(50, 1000, false).namedOpe</tspan><tspan x="24" dy="22">rationRequired() = "
|
|
+ GraphQlClientPolicy.defaults(50, 1_000, false).namedOperationRequired());
|
|
|
|
System</tspan><tspan x="24" dy="22">.out.println();
|
|
System.out.println("[배선된 GraphQlOperationSelectionHandler · 조립되는 값 그대로]");
|
|
wired("익</tspan><tspan x="24" dy="22">명 단일 연산", ANONYMOUS_SINGLE, policy(false));
|
|
wired("이름이 Ab 인 단일 연산", NAMED_SHORT, policy(false));
|
|
wi</tspan><tspan x="24" dy="22">red("이름이 HealthQuery 인 연산", NAMED_OK, policy(false));
|
|
wired("연산 둘 · operationName 없음", TWO_OPERATIONS, p</tspan><tspan x="24" dy="22">olicy(false));
|
|
|
|
System.out.println();
|
|
System.out.println("[같은 핸들러 · namedOperationRequired 만 true 로]");
|
|
</tspan><tspan x="24" dy="22"> wired("익명 단일 연산", ANONYMOUS_SINGLE, policy(true));
|
|
|
|
System.out.println();
|
|
System.out.println("[미배선 </tspan><tspan x="24" dy="22">GraphQlOperationNamePolicy.production().verify]");
|
|
unwired("PUBLIC · 이름 없음", GraphQlClientProfileName.PUBLIC, n</tspan><tspan x="24" dy="22">ull, 1);
|
|
unwired("ADMIN · 이름 없음", GraphQlClientProfileName.ADMIN, null, 1);
|
|
unwired("PUBLIC · 이름 Ab", Gr</tspan><tspan x="24" dy="22">aphQlClientProfileName.PUBLIC, "Ab", 1);
|
|
unwired("PUBLIC · 이름 HealthQuery", GraphQlClientProfileName.PUBLIC, "Hea</tspan><tspan x="24" dy="22">lthQuery", 1);
|
|
}
|
|
}
|
|
JAVA
|
|
javac -encoding UTF-8 -cp "$SIB$cp" -d "$D/out" "$D/src/dev/caskeleton/adapter/inbound/graphql</tspan><tspan x="24" dy="22">/runtime/OperationNameProbe.java" 2>&1 | head -20
|
|
printf '%s\n' '<configuration><root level="OFF"/></configuration>' > "</tspan><tspan x="24" dy="22">$D/out/logback.xml"
|
|
java -Dstdout.encoding=UTF-8 -cp "$D/out:$SIB$cp" dev.caskeleton.adapter.inbound.graphql.runtime.Ope</tspan><tspan x="24" dy="22">rationNameProbe 2>&1 | grep -vE '^[0-9]{4}-|^\s+at |WARN|INFO'
|
|
</tspan></text>
|
|
<text x="24" y="1168" 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</tspan></text>
|
|
<text x="24" y="1190" 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-03T08:41:11+00:00 · exit 0</tspan></text>
|
|
<line x1="24" y1="1204" x2="1176" y2="1204" stroke="#30363d"/>
|
|
<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">openjdk version "21.0.12" 2026-07-21</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"></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">[조립되는 GraphQlClientPolicy 가 이 질문에 넣는 값]</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"> GraphQlClientPolicy.defaults(50, 1000, false).namedOperationRequired() = false</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"></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">[배선된 GraphQlOperationSelectionHandler · 조립되는 값 그대로]</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"> 익명 단일 연산 통과 · operationId=anonymous</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"> 이름이 Ab 인 단일 연산 통과 · operationId=anonymous</tspan></text>
|
|
<text x="24" y="1420" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 이름이 HealthQuery 인 연산 통과 · operationId=healthquery</tspan></text>
|
|
<text x="24" y="1442" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 연산 둘 · operationName 없음 GraphQlAnonymousOperationException : operationName is required when the document declares</tspan><tspan x="24" dy="22"> 2 operations</tspan></text>
|
|
<text x="24" y="1486" 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="1508" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[같은 핸들러 · namedOperationRequired 만 true 로]</tspan></text>
|
|
<text x="24" y="1530" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> 익명 단일 연산 GraphQlAnonymousOperationException : this client profile requires a named operation</tspan></text>
|
|
<text x="24" y="1552" 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="1574" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0">[미배선 GraphQlOperationNamePolicy.production().verify]</tspan></text>
|
|
<text x="24" y="1596" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> PUBLIC · 이름 없음 GraphQlAnonymousOperationException : named operation required</tspan></text>
|
|
<text x="24" y="1618" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> ADMIN · 이름 없음 통과</tspan></text>
|
|
<text x="24" y="1640" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> PUBLIC · 이름 Ab IllegalArgumentException : invalid GraphQL operation name</tspan></text>
|
|
<text x="24" y="1662" fill="#e6edf3" font-size="14" font-family="ui-monospace, SFMono-Regular, Menlo, Consolas, monospace" xml:space="preserve"><tspan x="24" dy="0"> PUBLIC · 이름 HealthQuery 통과</tspan></text>
|
|
</svg>
|