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
6.7 KiB
JSON
15 lines
6.7 KiB
JSON
{
|
|
"assetKey": "a16-f004-graphqloperationnamepolicy-run",
|
|
"kind": "terminal",
|
|
"command": "set -e\nset -o pipefail\nD=$(mktemp -d); trap 'rm -rf \"$D\"' EXIT\nC=/shared/codebase/clean-architecture-backend-template\nM=$C/src/adapter/inbound/graphql\nCACHE=/root/.gradle/caches/modules-2/files-2.1\njava -version 2>&1 | grep version\ncp=\"\"\nwhile IFS= read -r line; do\n coord=${line%%=*}; confs=${line#*=}\n case \",$confs,\" in *\",testRuntimeClasspath,\"*) ;; *) continue ;; esac\n g=${coord%%:*}; rest=${coord#*:}; n=${rest%%:*}; v=${rest##*:}\n jar=$(find \"$CACHE/$g/$n/$v\" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1) || true\n [ -n \"$jar\" ] && cp=\"$cp:$jar\"\ndone < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' \"$M/gradle.lockfile\")\nSIB=$(find \"$C/src\" -path '*/build/libs/*+21234e38cdb9*.jar' ! -name '*-testkit.jar' | sort | tr '\\n' ':')\n\nmkdir -p \"$D/src/dev/caskeleton/adapter/inbound/graphql/runtime\"\ncat > \"$D/src/dev/caskeleton/adapter/inbound/graphql/runtime/OperationNameProbe.java\" <<'JAVA'\npackage dev.caskeleton.adapter.inbound.graphql.runtime;\n\nimport dev.caskeleton.adapter.inbound.graphql.api.GraphQlClientProfile;\nimport dev.caskeleton.adapter.inbound.graphql.api.GraphQlClientProfileName;\nimport dev.caskeleton.adapter.inbound.graphql.api.GraphQlOperationId;\nimport dev.caskeleton.adapter.inbound.graphql.context.ActorRef;\nimport dev.caskeleton.adapter.inbound.graphql.context.GraphQlDeadline;\nimport dev.caskeleton.adapter.inbound.graphql.context.GraphQlRequestContext;\nimport dev.caskeleton.adapter.inbound.graphql.execution.GraphQlOperationNamePolicy;\nimport dev.caskeleton.adapter.inbound.graphql.execution.GraphQlOperationSelection;\nimport dev.caskeleton.adapter.inbound.graphql.policy.GraphQlClientPolicy;\nimport dev.caskeleton.adapter.inbound.graphql.context.TenantContext;\nimport graphql.language.Document;\nimport graphql.parser.Parser;\nimport java.time.Clock;\nimport java.time.Duration;\nimport java.util.Locale;\nimport java.util.Map;\n\n/** 같은 질문에 답하는 세 구현 중 무엇이 조립되고, 조립된 것이 무엇을 답하는지 갈라 본다. */\npublic final class OperationNameProbe {\n\n private static final String ANONYMOUS_SINGLE = \"{ __typename }\";\n private static final String NAMED_SHORT = \"query Ab { __typename }\";\n private static final String NAMED_OK = \"query HealthQuery { __typename }\";\n private static final String TWO_OPERATIONS = \"query A { __typename } query B { __typename }\";\n\n private static GraphQlClientPolicy policy(boolean namedOperationRequired) {\n return new GraphQlClientPolicy(\n 16 * 1024, 64 * 1024, 12, 500, 50, 50, 1_000, 20, 50, 1_000L, 10_000, 5L * 1024 * 1024,\n Duration.ofSeconds(10), false, false, namedOperationRequired);\n }\n\n private static GraphQlRequestContext requestContext() {\n return new GraphQlRequestContext(\n ActorRef.anonymous(),\n TenantContext.system(\"acme\"),\n new GraphQlClientProfile(\"first-party\"),\n Locale.ROOT,\n new GraphQlOperationId(\"anonymous\"),\n \"probe-trace\",\n GraphQlDeadline.after(Duration.ofSeconds(10), Clock.systemUTC()));\n }\n\n private static void wired(String label, String document, GraphQlClientPolicy policy) {\n Document parsed = Parser.parse(document);\n var request = new GraphQlExecutionRequest(document, null, Map.of(), document.length(), 0);\n var context = GraphQlExecutionContext.starting(request, requestContext(), null, parsed);\n System.out.printf(\" %-30s \", label);\n try {\n var out = new GraphQlOperationSelectionHandler(policy).handle(context);\n System.out.println(\"통과 · operationId=\" + out.requestContext().operationId().value());\n } catch (RuntimeException refused) {\n System.out.println(refused.getClass().getSimpleName() + \" : \" + refused.getMessage());\n }\n }\n\n private static void unwired(String label, String client, String name, int operations) {\n System.out.printf(\" %-30s \", label);\n try {\n GraphQlOperationNamePolicy.production()\n .verify(\n new GraphQlClientProfileName(client),\n new GraphQlOperationSelection(name, operations, false));\n System.out.println(\"통과\");\n } catch (RuntimeException refused) {\n System.out.println(refused.getClass().getSimpleName() + \" : \" + refused.getMessage());\n }\n }\n\n public static void main(String[] args) {\n System.out.println();\n System.out.println(\"[조립되는 GraphQlClientPolicy 가 이 질문에 넣는 값]\");\n System.out.println(\n \" GraphQlClientPolicy.defaults(50, 1000, false).namedOperationRequired() = \"\n + GraphQlClientPolicy.defaults(50, 1_000, false).namedOperationRequired());\n\n System.out.println();\n System.out.println(\"[배선된 GraphQlOperationSelectionHandler · 조립되는 값 그대로]\");\n wired(\"익명 단일 연산\", ANONYMOUS_SINGLE, policy(false));\n wired(\"이름이 Ab 인 단일 연산\", NAMED_SHORT, policy(false));\n wired(\"이름이 HealthQuery 인 연산\", NAMED_OK, policy(false));\n wired(\"연산 둘 · operationName 없음\", TWO_OPERATIONS, policy(false));\n\n System.out.println();\n System.out.println(\"[같은 핸들러 · namedOperationRequired 만 true 로]\");\n wired(\"익명 단일 연산\", ANONYMOUS_SINGLE, policy(true));\n\n System.out.println();\n System.out.println(\"[미배선 GraphQlOperationNamePolicy.production().verify]\");\n unwired(\"PUBLIC · 이름 없음\", GraphQlClientProfileName.PUBLIC, null, 1);\n unwired(\"ADMIN · 이름 없음\", GraphQlClientProfileName.ADMIN, null, 1);\n unwired(\"PUBLIC · 이름 Ab\", GraphQlClientProfileName.PUBLIC, \"Ab\", 1);\n unwired(\"PUBLIC · 이름 HealthQuery\", GraphQlClientProfileName.PUBLIC, \"HealthQuery\", 1);\n }\n}\nJAVA\njavac -encoding UTF-8 -cp \"$SIB$cp\" -d \"$D/out\" \"$D/src/dev/caskeleton/adapter/inbound/graphql/runtime/OperationNameProbe.java\" 2>&1 | head -20\nprintf '%s\\n' '<configuration><root level=\"OFF\"/></configuration>' > \"$D/out/logback.xml\"\njava -Dstdout.encoding=UTF-8 -cp \"$D/out:$SIB$cp\" dev.caskeleton.adapter.inbound.graphql.runtime.OperationNameProbe 2>&1 | grep -vE '^[0-9]{4}-|^\\s+at |WARN|INFO'\n",
|
|
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
|
|
"exitCode": 0,
|
|
"executedAt": "2026-09-03T08:41:11+00:00",
|
|
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
|
|
"raw": "evidence/raw/a16-f004-graphqloperationnamepolicy-run.txt",
|
|
"svg": "evidence/rendered/a16-f004-graphqloperationnamepolicy-run.svg",
|
|
"rawSha256": "3285e25be66410773499754ecb856c315656bdb45d96fa5444b2b6eddf2cd5dd",
|
|
"lines": 19,
|
|
"redaction": "없음 — 로그 줄만 걸러낸 실행 출력"
|
|
}
|