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>
This commit is contained in:
DongHyeonka
2026-09-04 22:51:59 +09:00
co-authored by Claude Opus 5
parent 43bccd08a8
commit b2963105a8
5017 changed files with 372751 additions and 4943 deletions
@@ -0,0 +1,11 @@
{
"assetKey": "001-git-snapshot",
"kind": "terminal",
"command": "git rev-parse HEAD && git status --short --branch",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-08-29T00:04:07+00:00",
"sourceRevision": "a24ece9cf797f7ea647e33bf846b115208ed1ba5",
"raw": "evidence/raw/001-git-snapshot.txt",
"proves": "analysis source revision and working-tree snapshot"
}
@@ -0,0 +1,11 @@
{
"assetKey": "002-module-inventory",
"kind": "terminal",
"command": "python3 module inventory from src/config/architecture/modules.json",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-08-29T00:04:07+00:00",
"sourceRevision": "a24ece9cf797f7ea647e33bf846b115208ed1ba5",
"raw": "evidence/raw/002-module-inventory.txt",
"proves": "initial 44-leaf project/module sizing inventory"
}
@@ -0,0 +1,11 @@
{
"assetKey": "003-domain-core-test",
"kind": "terminal",
"command": "./gradlew :domain-core:test --console=plain --no-daemon",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-08-29T00:04:07+00:00",
"sourceRevision": "a24ece9cf797f7ea647e33bf846b115208ed1ba5",
"raw": "evidence/raw/003-domain-core-test.txt",
"proves": ":domain-core:test task completes successfully in the current analysis environment"
}
@@ -0,0 +1,11 @@
{
"assetKey": "004-clean-architecture-test",
"kind": "terminal",
"command": "./gradlew :app-bootstrap:test --tests dev.caskeleton.bootstrap.architecture.CleanArchitectureTest --console=plain --no-daemon",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-08-29T00:04:25+00:00",
"sourceRevision": "a24ece9cf797f7ea647e33bf846b115208ed1ba5",
"raw": "evidence/raw/004-clean-architecture-test.txt",
"proves": "CleanArchitectureTest completes successfully against the current production class graph"
}
@@ -0,0 +1,11 @@
{
"assetKey": "005-shared-contract-test",
"kind": "terminal",
"command": "./gradlew :shared-contract:test --console=plain",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-08-29T04:03:05+00:00",
"sourceRevision": "a24ece9cf797f7ea647e33bf846b115208ed1ba5",
"raw": "evidence/raw/005-shared-contract-test.txt",
"proves": ":shared-contract:test completes successfully at the analyzed revision"
}
@@ -0,0 +1,11 @@
{
"assetKey": "006-shared-contract-edge-rate-limit-test",
"kind": "terminal",
"command": "./gradlew :shared-contract:edgeRateLimitContractTest --console=plain",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-08-29T04:03:18+00:00",
"sourceRevision": "a24ece9cf797f7ea647e33bf846b115208ed1ba5",
"raw": "evidence/raw/006-shared-contract-edge-rate-limit-test.txt",
"proves": "provider-neutral edge rate-limit strict contract lane completes successfully"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-build-gate-that-is-not-in-the-build",
"kind": "terminal",
"command": "echo '# 이 비교를 참조하는 파일'; grep -rl 'RedisCommandMetadataDiff' --include=*.java . | sed 's|^\\./||'; echo; echo '# 이 비교를 실행하는 Gradle 태스크'; grep -rln 'RedisCommandMetadataDiff' --include=*.gradle --include=*.gradle.kts . || echo '(없음)'; echo; echo '# CI 워크플로에서 부르는 단계'; grep -rln 'RedisCommandMetadataDiff' ../.github/workflows 2>/dev/null || echo '(없음)'",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T00:06:50+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-build-gate-that-is-not-in-the-build.txt",
"svg": "evidence/rendered/a-build-gate-that-is-not-in-the-build.svg",
"rawSha256": "32c6d3de4fb0be0ec942e222f5f4543b041464ef1129022e79dc310df44cda4e",
"lines": 9,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-catalog-nine-entries-short",
"kind": "terminal",
"command": "f=adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/JpaModuleBoundaryTest.java; echo '# PACKAGE_CATALOG javadoc — 이전 카탈로그와 트리의 크기'; sed -n '60,70p' $f; echo; echo '# DAG 테스트가 기록한 사이클'; sed -n '314,329p' $f",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T00:09:58+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-catalog-nine-entries-short.txt",
"svg": "evidence/rendered/a-catalog-nine-entries-short.svg",
"rawSha256": "8c98419770c3b6cc8ac44b7b2e61b43bf749d9b570d525bb7355f8b0a1d749df",
"lines": 30,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-certifying-lane-that-compared-nothing",
"kind": "terminal",
"command": "echo '# 이 레인에서 그 플래그는 사라졌다'; grep -c 'performance\\.assertions\\.enabled' src/adapter/outbound/persistence-jpa/build.gradle; echo; echo '# 그러나 저장소에는 남아 있다 — 같은 플래그가 다른 레인을 게이트한다'; grep -rn 'performance\\.assertions\\.enabled' src/adapter/outbound/httpclient/build.gradle src/adapter/outbound/persistence-mongo/build.gradle .github/workflows/httpclient-nightly.yml; echo; echo '# 옛 소스셋 이름이 남아 있는 곳'; grep -n 'jpaPlatformPerformanceTest' src/adapter/outbound/persistence-jpa/build.gradle",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T00:18:36+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-certifying-lane-that-compared-nothing.txt",
"svg": "evidence/rendered/a-certifying-lane-that-compared-nothing.svg",
"rawSha256": "8d856406171d9179dd90a617ce70412825242e6216b85f6f0c2057a33f2d60b2",
"lines": 20,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-circuit-breaker-permit-that-leaks-on-local-rejection",
"kind": "terminal",
"command": "p=adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/AttemptResiliencePipeline.java; i=adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/AttemptCircuitBreaker.java; echo '# execute 진입부 — 세 가드와 그 거부 경로'; grep -n 'tryAcquirePermission()\\|bulkhead.tryAcquire()\\|rateLimiter.onCompleted()\\|throw new Http' $p; echo; echo '# 정상 경로의 반납'; sed -n '118,121p' $p; echo; echo '# AttemptCircuitBreaker 의 추상 연산 전수'; sed -n '/^public interface AttemptCircuitBreaker/,/^ static /p' $i | grep -E '^ [A-Za-z].*;$'; echo; echo '# releasePermission — src 아래 java 매치 수'; grep -rc 'releasePermission' --include=*.java -r . | grep -v ':0' | wc -l; echo '# releasePermission 이 등장하는 파일 (저장소 전체)'; cd .. && grep -rl 'releasePermission' --include=*.java --include=*.md . | sed 's|^\\./||'",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T00:24:28+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-circuit-breaker-permit-that-leaks-on-local-rejection.txt",
"svg": "evidence/rendered/a-circuit-breaker-permit-that-leaks-on-local-rejection.svg",
"rawSha256": "66a824c54f7c4ff3b24653174a2e2c2668c3374577af20c4049cc99c4b334db4",
"lines": 26,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-cleanup-claim-without-fencing",
"kind": "terminal",
"command": "m=adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V3__fileserver_fenced_cleanup_lease.sql; r=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverCleanupRepository.java; e=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/CleanupItemEntity.java; echo '# 추가된 컬럼 넷과 널 정책'; sed -n '17,21p' $m; echo; echo '# 완료 갱신이 토큰으로 행을 찾는다'; grep -n 'and c.claimToken = :token' $r; echo; echo '# 회수기가 널 만료를 제외한다'; grep -n 'leaseUntil is not null' $r; echo; echo '# claim_fence 를 쓰는 곳 / 읽는 곳'; grep -rn 'claimFence' $r $e; echo '-- CleanupItemEntity 의 getter'; grep -c 'public .* get' $e; grep -n 'getClaimToken\\|getLeaseUntil\\|getClaimFence' $e",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T00:34:13+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-cleanup-claim-without-fencing.txt",
"svg": "evidence/rendered/a-cleanup-claim-without-fencing.svg",
"rawSha256": "fe13d815ed51584fa5c06e4e2d78f3925f7cc98858e8e9ca77c36364eb60ebce",
"lines": 21,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-closing-transport-reported-as-a-permanent-business-failure",
"kind": "terminal",
"command": "echo '# rejectedLocally 가 붙이는 범주 — 네 전송 어댑터 production'; for f in nats-experimental/src/main/java/dev/caskeleton/messaging/nats/NatsJetStreamTransport pulsar-experimental/src/main/java/dev/caskeleton/messaging/pulsar/PulsarMessagingTransport kafka/src/main/java/dev/caskeleton/messaging/kafka/KafkaMessagingTransport rabbit/src/main/java/dev/caskeleton/messaging/rabbit/RabbitMessagingTransport; do p=messaging/messaging-$f.java; printf '%-28s ' $(basename $p .java); sed -n '/private static.*rejectedLocally/,/^ }/p' $p | grep -o 'FailureCategory\\.[A-Z_]*'; done; echo; echo '# 종료 중 발행은 전송에 닿기 전에 승인 게이트가 거절한다'; grep -n 'SHUTTING_DOWN' messaging/messaging-policy/src/main/java/dev/caskeleton/messaging/policy/MessagingAdmissionController.java; grep -n 'CATEGORY = FailureCategory' messaging/messaging-core-api/src/main/java/dev/caskeleton/messaging/api/error/MessageBackpressureException.java; grep -n 'admission.stopAcceptingNewWork' messaging/messaging-spring-boot-starter/src/main/java/dev/caskeleton/messaging/autoconfigure/MessagingShutdownLifecycle.java; echo; echo '# 두 전송 빈의 조립 지점'; grep -rn 'new dev.caskeleton.messaging.kafka.KafkaMessagingTransport' messaging/messaging-spring-boot-starter/src/main | sed 's|.*/||'; echo -n 'RabbitMessagingTransport 를 조립하는 production 파일: '; grep -rl 'RabbitMessagingTransport' --include=*.java messaging/*/src/main | grep -v 'messaging-rabbit/src/main/java/dev/caskeleton/messaging/rabbit/RabbitMessagingTransport.java' | wc -l",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T00:43:33+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-closing-transport-reported-as-a-permanent-business-failure.txt",
"svg": "evidence/rendered/a-closing-transport-reported-as-a-permanent-business-failure.svg",
"rawSha256": "a156d3eef4e3bd49c608d4e874a6182773feb87a33755c2633e1c68a46fb0e50",
"lines": 14,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-customizer-that-discarded-the-bound-property",
"kind": "terminal",
"command": "v=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/PostgreSqlPersistenceConfig.java; s=sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/bootstrap/persistence/SamplePostgreSqlPersistenceConfig.java; echo '# 벤더 설정 — 사건을 기록한 javadoc'; sed -n '91,108p' $v; echo; echo '# 벤더 설정 — 지금 구현'; sed -n '109,118p' $v; echo; echo '# 같은 빈 이름을 쓰는 sample 쪽 — 아직 무조건 호출'; sed -n '29,40p' $s; echo; echo '# H2 쪽은 커스터마이저를 아예 두지 않는다'; grep -n 'No Flyway location customizer' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/h2/H2PersistenceConfig.java",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T00:51:52+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-customizer-that-discarded-the-bound-property.txt",
"svg": "evidence/rendered/a-customizer-that-discarded-the-bound-property.svg",
"rawSha256": "3c7642594984866c7ff5a0c70071ddb01e159838129cd1d2b180994472fded77",
"lines": 48,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-delayed-delivery-flag-without-the-topology-that-delivers-it",
"kind": "terminal",
"command": "echo '# 능력 성분 순서 — 여덟째'; grep -n 'boolean delayedDelivery' messaging/messaging-core-api/src/main/java/dev/caskeleton/messaging/api/destination/MessagingCapabilities.java; echo '# Rabbit 이 넘기는 열두 값 (여덟째가 참)'; sed -n '42,43p' messaging/messaging-rabbit/src/main/java/dev/caskeleton/messaging/rabbit/RabbitMessagingTransport.java; echo ' ^^^^ delayedDelivery'; echo; echo '# 이 플래그를 읽는 엔진과 그 조립 지점'; grep -n 'delayedDelivery' messaging/messaging-policy/src/main/java/dev/caskeleton/messaging/policy/DefaultRetryDecisionEngine.java; grep -n -A2 'public RetryDecisionEngine retryDecisionEngine' messaging/messaging-spring-boot-starter/src/main/java/dev/caskeleton/messaging/autoconfigure/MessagingCoreAutoConfiguration.java; echo; echo '# 그 결정의 유일한 production 소비자와 delay 처리'; sed -n '91,93p' messaging/messaging-kafka/src/main/java/dev/caskeleton/messaging/kafka/KafkaRetryExecutor.java; echo; echo '# 지연 큐를 기술하는 타입을 참조하는 파일'; grep -rl 'RabbitRetryQueueTopology' --include=*.java .; echo; echo -n '# 큐를 선언하는 코드 (AmqpAdmin/declareQueue/QueueBuilder) 매치: '; grep -rn 'AmqpAdmin\\|declareQueue\\|QueueBuilder' --include=*.java messaging/ | wc -l",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T00:59:53+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-delayed-delivery-flag-without-the-topology-that-delivers-it.txt",
"svg": "evidence/rendered/a-delayed-delivery-flag-without-the-topology-that-delivers-it.svg",
"rawSha256": "a872392f2ad6091122ae48bc6d318838e8db0061c2b5cf6caaad857d750fdf3f",
"lines": 23,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-digest-that-covered-who-but-not-what",
"kind": "terminal",
"command": "P=src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyDigestPolicy.java; S=src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java; echo '# 다이제스트가 덮던 다섯 성분과 그때 같아지던 쌍'; sed -n '17,26p' $P; echo; echo '# 지금 각 호출부가 실제로 넘기는 의미 인자'; grep -n 'transitionDigest(\"START\"\\|transitionDigest(\"RENEW\"\\|transitionDigest(\"RELEASE\"' $S; sed -n '279,285p' $S; sed -n '308,318p' $S; echo; echo '# 이어 붙일 때 각 성분 앞에 적는 길이와, 해시가 실제로 먹는 바이트'; sed -n '71,72p' $P; grep -n 'getBytes(StandardCharsets' $P; grep -n 'VERSION' $P | head -2; echo; echo '# 그런데 완료 재생 분기는 이 다이제스트를 비교하지 않는다'; sed -n '261,266p' $S",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T01:18:37+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-digest-that-covered-who-but-not-what.txt",
"svg": "evidence/rendered/a-digest-that-covered-who-but-not-what.svg",
"rawSha256": "cb834fc0bab35ff6fc13a96e6a9ef1d5fa49dae1ec590a458940a837f4796b2f",
"lines": 49,
"redaction": "none — 분석 문서 머리"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-five-second-string-that-broke-every-prod-deploy-bind",
"kind": "terminal",
"command": "d=$(mktemp -d); cat > $d/BindCheck.java <<'JAVA_EOF'\nimport com.zaxxer.hikari.HikariConfig;\nimport java.util.Map;\nimport org.springframework.boot.context.properties.bind.Binder;\nimport org.springframework.boot.context.properties.source.MapConfigurationPropertySource;\n\npublic class BindCheck {\n public static void main(String[] args) {\n for (String value : new String[] {\"5s\", \"5000\"}) {\n var source =\n new MapConfigurationPropertySource(\n Map.of(\"spring.datasource.hikari.connection-timeout\", value));\n try {\n HikariConfig bound =\n new Binder(source).bind(\"spring.datasource.hikari\", HikariConfig.class).get();\n System.out.println(value + \" -> OK connectionTimeout=\" + bound.getConnectionTimeout());\n } catch (Exception failure) {\n System.out.println(value + \" -> REJECTED\");\n for (Throwable t = failure; t != null; t = t.getCause()) {\n System.out.println(\" \" + t.getClass().getName() + \": \" + t.getMessage());\n }\n }\n }\n }\n}\nJAVA_EOF\nG=/root/.gradle/caches/modules-2/files-2.1\nCP=$(for a in spring-boot/4 spring-core/7 spring-beans/7 spring-context/7 HikariCP/7 commons-logging/1.3.6 slf4j-api/2.0.18; do find $G -path \"*/$a*\" -name '*.jar' | grep -v sources | head -1; done | paste -sd:)\necho '# 사용한 산출물'\necho \"$CP\" | tr ':' '\\n' | sed 's|.*/||'\njavac -cp \"$CP\" -d $d $d/BindCheck.java\necho\necho '# 같은 키에 두 형식을 넣고 실제로 바인딩한다'\njava -cp \"$CP:$d\" BindCheck 2>&1 | grep -v '^SLF4J'\n",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T01:15:50+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-five-second-string-that-broke-every-prod-deploy-bind.txt",
"svg": "evidence/rendered/a-five-second-string-that-broke-every-prod-deploy-bind.svg",
"rawSha256": "73d78627fec839c03b13222437ea1aaff1e0c4733c4a6b6c7ae949379e507c70",
"lines": 15,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-five-second-string-that-broke-every-prod-deploy",
"kind": "terminal",
"command": "y=app-bootstrap/src/main/resources/application.yml; v=app-bootstrap/src/main/java/dev/caskeleton/bootstrap/runtime/HikariPoolConstraintValidator.java; t=app-bootstrap/src/test/java/dev/caskeleton/bootstrap/runtime/ShippedDefaultBindabilityTest.java; echo '# 출하 파일 안의 같은 이름, 다른 타입 — 네임스페이스와 함께'; grep -n 'hikari:' $y | head -1; sed -n '61p' $y; grep -n '^ tomcat:\\|^ tomcat:' $y | head -1; sed -n '333p' $y; echo; echo '# 앞쪽이 바인딩되는 세터'; javap -cp \"$(find /root/.gradle/caches/modules-2/files-2.1 -name 'HikariCP-*.jar' | grep -v sources | head -1)\" com.zaxxer.hikari.HikariConfig | grep -i 'ConnectionTimeout'; echo; echo '# 같은 키를 읽는 시작 검증기는 다른 규칙으로 읽는다'; grep -n 'implements SmartInitializingSingleton' $v; grep -n 'CONNECTION_TIMEOUT' $v | head -2; grep -n 'DurationStyle' $v; echo; echo '# 지금은 정적 가드가 이 형식을 막는다'; sed -n '39,47p' $t; echo ' (그 가드가 한 번 헛짚었던 자리)'; sed -n '106,108p' $t",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T01:15:50+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-five-second-string-that-broke-every-prod-deploy.txt",
"svg": "evidence/rendered/a-five-second-string-that-broke-every-prod-deploy.svg",
"rawSha256": "4ee297e00eabd2cce0a9f9729346480df02a2b4cd8800aea6453369f7d132d28",
"lines": 31,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-flag-that-validates-an-unwired-subsystem",
"kind": "terminal",
"command": "M=adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo; A=$M/autoconfigure/MongoPlatformAutoConfiguration.java; S=$M/autoconfigure/MongoPlatformSettings.java; echo -n '# 트랜잭션 패키지의 타입 수: '; find $M/transaction -name '*.java' | wc -l; ls $M/transaction/ $M/transaction/retry/ $M/transaction/session/ | grep '\\.java' | sed 's/^/ /'; echo; echo -n '# 자동설정이 그 타입들을 참조하는 매치 (Transaction / CausalSession): '; echo \"$(grep -c 'Transaction' $A) / $(grep -c 'CausalSession' $A)\"; echo -n '# transaction 패키지 밖 main 참조: '; grep -rn 'outbound\\.mongo\\.transaction' --include=*.java . | grep '/main/' | grep -vc '/mongo/transaction/'; echo; echo '# 플래그는 선언되고 검증기까지 가서 판정에 쓰인다'; grep -n 'boolean transactions' $S; grep -n 'properties.transactions()' $A; grep -n -A3 'transactionsEnabled && ' $M/autoconfigure/MongoStartupValidator.java; echo; echo '# 다만 그 검사는 프로브가 있어야 돌고, 이 저장소는 프로브를 출하하지 않는다'; sed -n '330,332p' $A; sed -n '383,384p' $A; echo -n '# MongoTopologyProbe 를 빈으로 만드는 main 코드: '; grep -rn -B2 'MongoTopologyProbe ' --include=*.java . | grep '/main/' | grep -c '@Bean'; echo; echo '# 같은 생성자가 입력마다 다르게 처리한다'; sed -n '47p;55p;62,65p' $S; echo; echo '# 반대쪽 실행체는 플래그와 무관하게 조립된다'; sed -n '188,190p;207,209p' $A; echo -n '# 이 경로에서 두 플래그를 보는 조건: '; grep -c \"ConditionalOnProperty(prefix = \\\"ca-skeleton.persistence-mongo.platform\\\"\" $A; echo '# 그 주석이 없다고 적은 드라이버 호출'; grep -n 'changeStream(\\|watchCollection(\\|startAfter(\\|resumeAfter(' $M/changestream/consumer/SpringReactiveChangeStreamSource.java; echo; echo '# 그 위 소비자가 요구하는 다섯 포트'; sed -n '228,233p' $A",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T01:20:46+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-flag-that-validates-an-unwired-subsystem.txt",
"svg": "evidence/rendered/a-flag-that-validates-an-unwired-subsystem.svg",
"rawSha256": "553ad6ba8beceb0eeb5809e86ebb540cfbda640a53cb5f0c84cae69a84508c53",
"lines": 70,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-lease-without-an-owner",
"kind": "terminal",
"command": "O=messaging/messaging-outbox-jdbc-postgresql/src/main; M=$O/resources/db/migration/messaging/V2__messaging_outbox_lease_fencing.sql; R=$O/java/dev/caskeleton/messaging/outbox/JdbcOutboxRepository.java; I=messaging/messaging-reliability-api/src/main/java/dev/caskeleton/messaging/reliability/OutboxRepository.java; echo '# V2 가 더한 컬럼과 제약'; sed -n '15,23p' $M; echo; echo '# 다섯 상태에서 여섯으로'; grep -n \"status IN\" $O/resources/db/migration/messaging/V1__messaging_outbox.sql | head -1; sed -n '33p' $M; echo; echo '# 청구가 소유자와 토큰을 같은 문장에서 쓴다'; sed -n '129,133p' $R; echo '# 그 청구가 읽는 술어'; sed -n '117,124p' $R; echo '# V2 가 그 술어에 맞춰 만든 부분 인덱스'; sed -n '37,39p' $M; echo; echo '# 최종 쓰기는 소유자와 토큰을 조건으로 걸고, 0행을 삼키지 않는다'; grep -n \"AND lease_owner = ? AND lease_token = ?\" $R; sed -n '394,396p' $R; echo; echo '# 릴레이가 부르는 것은 전부 신세대다'; grep -rn 'repository\\.\\(mark\\|release\\|claim\\|lease\\)' --include=*.java $O/java | sed 's|.*/||'; echo; echo '# 펜싱 없는 옛 경로는 인터페이스에 남아 있다'; grep -n 'List<OutboxRecord> leaseBatch' $I; echo '# 그 메서드 자신의 javadoc 은 아직 안전을 주장한다'; sed -n '27,29p' $I; echo '# 폐기를 적은 것은 신세대 쪽 javadoc 이다'; sed -n '41,44p' $I; echo -n '# messaging 트리 전체의 @Deprecated 매치: '; grep -rn '@Deprecated' --include=*.java messaging/ | wc -l; echo -n '# .leaseBatch( 를 부르는 main 코드: '; grep -rn '\\.leaseBatch(' --include=*.java . | grep -c '/main/'; echo; echo '# 두 세대가 AMBIGUOUS 에서 남기는 것이 다르다'; sed -n '332,334p' $R; echo ' ---구세대'; sed -n '438,439p' $R; echo; echo '# 같은 형태가 다른 곳에도 있었다고 파일서버 마이그레이션이 적는다'; grep -n 'notification dispatcher' adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V3__fileserver_fenced_cleanup_lease.sql",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T01:32:03+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-lease-without-an-owner.txt",
"svg": "evidence/rendered/a-lease-without-an-owner.svg",
"rawSha256": "95cdb5c25fc5d718a06fd0369c5f8ef2c067117dca5924a1abcd6942e1ac249b",
"lines": 73,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-policy-reversed-by-a-later-filter",
"kind": "terminal",
"command": "W=adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web; P=$W/mvc/filter/WebMvcRequestIdFilter.java; L=$W/filter/RequestLoggingFilter.java; A=$W/mvc/autoconfigure/WebMvcPlatformAutoConfiguration.java; S=$W/mvc/autoconfigure/WebMvcPlatformSettings.java; C=app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java; echo '# 앞 필터는 자동설정이 조립한다'; grep -n 'WebMvcPlatformAutoConfiguration' adapter/inbound/web/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports; grep -n 'ConditionalOnWebApplication(type' $A; sed -n '144,148p' $A; echo; echo '# 그 설정값의 기본은 거짓이다'; sed -n '27,29p' $S; echo -n '# 무인자 생성자를 부르는 곳: '; grep -rn 'new WebMvcRequestIdFilter()' --include=*.java . | sed 's|.*/||' | tr '\\n' ' '; echo; echo; echo '# 두 필터가 같은 헤더 이름을 따로 선언한다'; grep -n 'REQUEST_ID_HEADER = \\|TRACEPARENT_HEADER = ' $P; grep -n 'HEADER_REQUEST_ID = \\|HEADER_TRACEPARENT = ' $L; echo; echo '# 앞 필터: 순서를 선언하고 자기 값을 속성과 응답 헤더에 쓴다'; grep -n 'HIGHEST_PRECEDENCE' $A; grep -n 'setAttribute(REQUEST_ID_ATTRIBUTE\\|setHeader(REQUEST_ID_HEADER' $P; echo; echo '# 뒤 필터: 스캔되고, 순서 선언이 없고, 정화한 클라이언트 값을 같은 헤더와 MDC 에 쓴다'; grep -n '@Component' $L; echo -n ' 순서 선언(@Order/Ordered/getOrder) 매치: '; grep -c '@Order\\|Ordered\\|getOrder' $L; grep -n 'MAX_ID_LENGTH = \\|HeaderSanitizer.sanitize(inbound' $L; sed -n '56,58p;61p;68p' $L; echo; echo '# 스캔 제외 정규식에 이 패키지가 있는가'; head -1 $L; echo -n ' 제외 정규식의 web 항목: '; grep -o 'inbound\\\\\\\\.web\\\\\\\\.[a-z]*\\\\\\\\.[a-z]*' $C | tr '\\n' ' '; echo; echo -n ' 그중 web.filter 매치: '; grep -c 'inbound\\\\\\\\.web\\\\\\\\.filter' $C; echo; echo '# 앞 필터가 속성에 넣은 값을 읽는 main 코드'; grep -n 'public static Optional<WebRequestId> requestId(' $P; echo -n ' 그 접근자와 속성 상수를 읽는 main 참조: '; grep -rn 'REQUEST_ID_ATTRIBUTE\\|WebMvcRequestIdFilter.requestId(' --include=*.java . | grep '/main/' | grep -vc 'WebMvcRequestIdFilter.java'; echo; echo '# 정화가 버리는 범위'; sed -n '17,22p' $W/observability/HeaderSanitizer.java",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T01:39:06+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-policy-reversed-by-a-later-filter.txt",
"svg": "evidence/rendered/a-policy-reversed-by-a-later-filter.svg",
"rawSha256": "74226f84180c7070816385f7c1f134d85feb3989381fd8f6759ea275bb2643e1",
"lines": 53,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-read-then-delete-race-on-the-upload-lease",
"kind": "terminal",
"command": "M=adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V4__fileserver_upload_terminal_state.sql; R=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/UploadLeaseRepository.java; C=application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java; U=application-core/src/main/java/dev/caskeleton/application/fileserver/upload; echo '# V4 가 더한 상태 컬럼과 제약, 그리고 부분 인덱스'; sed -n '31,32p;37,39p;46,49p' $M; echo; echo '# 이 저장소의 쓰기·전이 문장 여섯'; grep -n 'int acquireLease\\|int renewLease\\|int commitOffset\\|int releaseLease\\|int terminate\\|int claimForCleanup' $R; echo '# 그중 활성 상태를 조건으로 거는 줄'; grep -n \"s.lifecycleState = 'ACTIVE'\" $R; echo '# 클래스 javadoc 은 이렇게 적는다'; sed -n '18,19p' $R; echo '# 그러나 리스 반납 문장의 조건은 이것뿐이다'; sed -n '95,96p' $R; echo; echo '# 최종화와 정리 청구가 요구하는 상태'; sed -n '113p;117p' $R; sed -n '147,148p' $R; echo; echo '# 취소 경로 — 트랜잭션 시작부터 끝까지 연속'; sed -n '250,263p' $U/DefaultUploadApplicationService.java; echo; echo '# 검증 실패 경로 — 같은 람다의 시작과 세 쓰기와 끝'; sed -n '237,241p' $U/DefaultFinalizeUploadService.java; echo ' ......'; sed -n '249,254p' $U/DefaultFinalizeUploadService.java; echo; echo '# 정리는 세션을 먼저 읽고, 있을 때만 청구한다'; sed -n '139,143p' $C",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T01:40:45+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-read-then-delete-race-on-the-upload-lease.txt",
"svg": "evidence/rendered/a-read-then-delete-race-on-the-upload-lease.svg",
"rawSha256": "ec6cb3a7e666605332de0e59876503d6ce0ce1d4aa1952f913ac325a20e15231",
"lines": 72,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-red-test-misread-as-a-product-defect-host-control",
"kind": "terminal",
"command": "d=$(mktemp -d)\ncat > $d/cp.gradle <<'GRADLE_EOF'\ngradle.projectsEvaluated {\n def p = rootProject.findProject(':adapter:outbound:httpclient')\n if (p != null) {\n p.tasks.register('printTestCp') {\n doLast { println 'TESTCP=' + p.sourceSets.test.runtimeClasspath.files.findAll { it.exists() }.join(':') }\n }\n }\n}\nGRADLE_EOF\ncat > $d/HostControl.java <<'JAVA_EOF'\nimport dev.caskeleton.adapter.outbound.httpclient.apache.ApacheBlockingTransportProvider;\nimport dev.caskeleton.adapter.outbound.httpclient.apache.ApacheFailureClassifier;\nimport dev.caskeleton.adapter.outbound.httpclient.api.operation.AttemptStage;\nimport dev.caskeleton.adapter.outbound.httpclient.profile.ClientProfile;\nimport dev.caskeleton.adapter.outbound.httpclient.profile.RuntimeGeneration;\nimport dev.caskeleton.adapter.outbound.httpclient.testkit.ClientProfiles;\nimport dev.caskeleton.adapter.outbound.httpclient.testkit.MockHttpServer;\nimport dev.caskeleton.adapter.outbound.httpclient.testkit.NoopLifecycleListener;\nimport dev.caskeleton.adapter.outbound.httpclient.testkit.TlsFixture;\nimport dev.caskeleton.adapter.outbound.httpclient.testkit.TlsMaterials;\nimport dev.caskeleton.adapter.outbound.httpclient.transport.TransportFailure;\nimport java.net.URI;\nimport java.util.Optional;\nimport org.springframework.web.client.RestClient;\n\npublic class HostControl {\n\n static String cut(String s) {\n if (s == null) return \"null\";\n s = s.replace('\\n', ' ');\n return s.length() > 96 ? s.substring(0, 96) + \"...\" : s;\n }\n\n static void run(String label, TlsFixture serverFixture, TlsFixture clientTrust, String host)\n throws Exception {\n try (MockHttpServer server = MockHttpServer.startTls(serverFixture.serverSocketFactory(), false)) {\n server.enqueueJson(200, \"{\\\"id\\\":1}\");\n URI base = server.uri(\"/\");\n URI root = new URI(base.getScheme(), null, host, base.getPort(), \"/\", null, null);\n URI target = new URI(base.getScheme(), null, host, base.getPort(), \"/users/1\", null, null);\n ClientProfile profile = ClientProfiles.builder(\"partner\").baseUrl(root).build();\n ApacheBlockingTransportProvider provider =\n new ApacheBlockingTransportProvider(\n Optional.empty(),\n ignored -> Optional.of(TlsMaterials.trustOnly(clientTrust)),\n ignored -> Optional.empty());\n System.out.println(\"=== \" + label + \" @\" + host + \" ===\");\n try {\n RestClient client =\n RestClient.builder()\n .requestFactory(\n provider.create(profile, new RuntimeGeneration(1), NoopLifecycleListener.INSTANCE))\n .build();\n try {\n client.get().uri(target).retrieve().body(String.class);\n System.out.println(\" 요청이 성공했다\");\n } catch (RuntimeException failure) {\n int i = 0;\n for (Throwable t = failure; t != null; t = t.getCause()) {\n System.out.println(\" [\" + (i++) + \"] \" + t.getClass().getSimpleName() + \" :: \" + cut(t.getMessage()));\n }\n TransportFailure c = new ApacheFailureClassifier().classify(failure, AttemptStage.TLS_HANDSHAKE);\n System.out.println(\" -> stage=\" + c.stage() + \" category=\" + c.category()\n + \" permanent=\" + c.category().permanent());\n }\n } finally {\n provider.close(profile, new RuntimeGeneration(1));\n }\n }\n }\n\n public static void main(String[] args) throws Exception {\n for (String host : new String[] {\"localhost\", \"127.0.0.1\"}) {\n // 테스트와 같은 픽스처 구성: 신뢰불가 CA 만 서버와 클라이언트가 서로 다른 신뢰재료를 쓴다.\n run(\"untrusted-authority\", TlsFixture.trusted(), TlsFixture.trusted(), host);\n TlsFixture expired = TlsFixture.expired();\n run(\"expired-certificate\", expired, expired, host);\n TlsFixture mismatch = TlsFixture.hostnameMismatch();\n run(\"hostname-mismatch\", mismatch, mismatch, host);\n }\n }\n}\nJAVA_EOF\nCP=$(./gradlew -I $d/cp.gradle :adapter:outbound:httpclient:printTestCp --offline --console=plain -q 2>/dev/null | grep '^TESTCP=' | head -1 | cut -d= -f2-)\njavac -nowarn -cp \"$CP\" -d $d $d/HostControl.java 2>&1 | head -5\necho '# 같은 서버·같은 신뢰재료로 접속 호스트만 바꾼다'\njava -cp \"$CP:$d\" HostControl 2>/dev/null\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T01:51:16+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-red-test-misread-as-a-product-defect-host-control.txt",
"svg": "evidence/rendered/a-red-test-misread-as-a-product-defect-host-control.svg",
"rawSha256": "b3c6814158e193cd01f98d39fd3c7229c673f7a42088236fd37da6d5c9c43b6a",
"lines": 32,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-red-test-misread-as-a-product-defect",
"kind": "terminal",
"command": "H=adapter/outbound/httpclient/src\nT=$H/test/java/dev/caskeleton/adapter/outbound/httpclient/security/MutualTlsHandshakeContractTest.java\necho '# 모듈 전체를 돌리면'\n./gradlew :adapter:outbound:httpclient:test --offline --console=plain 2>&1 | grep -E 'FAILED$|tests completed|AssertionFailedError'\necho\necho '# 멈추는 자리는 두 번째 단언이다'\ngrep -n '' $T | sed -n '164,171p'\necho ' (167 통과 · 168 실패 · 169 이하 미실행)'\necho\necho '# 픽스처가 돌려주는 것은 호스트명이다'\ngrep -n -A2 'public URI uri(String path)' $H/testkit/java/dev/caskeleton/adapter/outbound/httpclient/testkit/MockHttpServer.java\necho\necho '# 이 컨테이너에서 그 호스트명이 푸는 주소'\ngrep -n localhost /etc/hosts\ncat > /tmp/Res.java <<'JAVA_EOF'\nimport java.net.InetAddress;\npublic class Res {\n public static void main(String[] a) throws Exception {\n for (InetAddress ia : InetAddress.getAllByName(\"localhost\")) System.out.println(\" \" + ia);\n }\n}\nJAVA_EOF\njavac -d /tmp /tmp/Res.java && java -cp /tmp Res\necho -n '# src 아래의 preferIPv4Stack / preferIPv6Addresses 설정: '\ngrep -rn 'preferIPv4Stack\\|preferIPv6Addresses' . | wc -l\necho\necho '# 강등이 재시도 판정에 닿는 지점'\nF=$H/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/FailureCategory.java\nsed -n '34,44p' $F\nE=$H/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/DefaultRetryEligibilityEngine.java\necho ' 엔진의 가드 순서'\nsed -n '19,23p;37,39p' $E\nsed -n '107p' $E\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T01:50:59+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-red-test-misread-as-a-product-defect.txt",
"svg": "evidence/rendered/a-red-test-misread-as-a-product-defect.svg",
"rawSha256": "1836173a051bfdbdec3df248bdc4f4da465269275f2d74080883b51b69f3b0c4",
"lines": 55,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-release-gate-with-no-evidence-producer",
"kind": "terminal",
"command": "E=grpc/grpc-testkit/src/main/java/dev/caskeleton/grpc/release/GrpcReleaseEvidence.java\nT=grpc/grpc-testkit/src/test/java/dev/caskeleton/grpc/release/GrpcStableReleaseGateTest.java\necho '# 게이트가 읽는 증거의 다섯 성분 — 전부 호출자가 넘긴다'\nsed -n '16,21p' $E\necho\necho '# 그 객체를 만드는 지점'\ngrep -rn 'new GrpcReleaseEvidence(' --include=*.java . | sed 's|.*/src/||'\necho\necho -n '# 그 테스트에 붙은 @Tag: '\ngrep -c '@Tag' $T\nD=$(mktemp)\n./gradlew check --dry-run --offline -q 2>/dev/null > $D\necho -n '# 그래서 check 그래프 안에 있다: '\ngrep -cE '^:grpc:grpc-testkit:test ' $D\necho '# 그 check 를 도는 워크플로'\ngrep -n 'gradlew check' ../.github/workflows/*.yml | sed 's|.*/\\.github|.github|'\necho\necho '# 등급 넷에 대응하는 증거 레인은 컨벤션이 등록해 둔다'\n./gradlew :grpc:grpc-testkit:tasks --group verification --offline -q 2>/dev/null | grep '^grpc[A-Z]' | cut -c1-60\necho -n '# 그중 check 그래프에 들어 있는 것: '\ngrep -cE 'grpcInProcess|grpcNetty|grpcFault|grpcPerformance' $D\necho -n '# 28개 워크플로 중 grpc 를 담은 것: '\ngrep -rli grpc ../.github/workflows/ | wc -l\necho\necho '# 대비 — messaging 은 레인이 파일을 쓰고 태스크가 양방향 대조하고 CI 가 돌린다'\ngrep -rn 'broker-certification-evidence' --include=*.java --include=*.gradle . | sed 's|.*/src/||' | head -3\nsed -n '82,84p' messaging/messaging-kafka/build.gradle\nsed -n '48,52p' ../.github/workflows/messaging-certification.yml\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T01:56:34+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-release-gate-with-no-evidence-producer.txt",
"svg": "evidence/rendered/a-release-gate-with-no-evidence-producer.svg",
"rawSha256": "7b08d9416494c8593a17c26b307380bee816c41bcaf2e5e1fd6110cf0d17d98e",
"lines": 39,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-replay-store-with-no-eviction-path",
"kind": "terminal",
"command": "R=grpc/grpc-policy/src/main/java/dev/caskeleton/grpc/idempotency/GrpcOutcomeReplay.java; D=grpc-advanced/grpc-advanced-streaming/src/main/java/dev/caskeleton/grpc/advanced/streaming/GrpcClientMessageDeduplicator.java; S=grpc-advanced/grpc-advanced-streaming/src/main/java/dev/caskeleton/grpc/advanced/streaming/GrpcClientStreamSessionId.java; echo '# 결과 재생 저장소 — 항목 하나의 크기만 제한한다'; sed -n '18,19p;41,49p' $R; echo -n '# remove / clear / evict / expire 매치: '; grep -c 'remove\\|clear\\|evict\\|expire' $R; echo '# 개수를 볼 수 있는 유일한 창구'; sed -n '59,61p' $R; echo -n '# 그 값을 읽는 main 코드: '; grep -rn 'replay.size()\\|Replay.size()' --include=*.java . | grep -c '/main/'; echo -n '# 이 클래스를 잡는 main 코드: '; grep -rn 'GrpcOutcomeReplay' --include=*.java . | grep '/main/' | grep -vc 'GrpcOutcomeReplay.java'; echo '# javadoc 이 이 저장소를 부르는 이름'; sed -n '13,14p' $R; echo; echo '# 형제 모듈의 중복 제거기 — 같은 형태를 자기 javadoc 이 비판한다'; sed -n '11,17p' $D; echo '# 비판을 지키는 쪽: 세션당 항목 하나'; sed -n '21,23p' $D; echo '# 지키지 않는 쪽: 적용된 메시지마다 한 항목'; sed -n '74,77p' $D; echo '# 재생 판정이 실제로 보는 것'; grep -n 'alreadyApplied(message.sequence())' $D; echo -n '# 이 파일의 remove / clear / removeIf 매치: '; grep -c 'remove\\|clear\\|evict\\|expire' $D; echo '# 그 둘이 있는 자리'; sed -n '109,113p' $D; echo '# 그 접두사가 실제로 무엇인가'; sed -n '40p;44p' $S; echo; echo -n '# 두 모듈의 런타임 소속: '; python3 -c \"import json;m=json.load(open('config/architecture/modules.json'));ls=[x for x in m.get('modules',m) if isinstance(x,dict)];print(' '.join(f\\\"{x['id']}={x.get('runtime_memberships')}\\\" for x in ls if x.get('id','').endswith('grpc-policy') or x.get('id','').endswith('grpc-advanced-streaming')))\"",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T01:58:32+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-replay-store-with-no-eviction-path.txt",
"svg": "evidence/rendered/a-replay-store-with-no-eviction-path.svg",
"rawSha256": "385d4a3e445ad6eefa9a4f03d16515c446326c7f1d370fd8e8d74308f5f5488c",
"lines": 54,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-report-that-cannot-carry-a-datasource",
"kind": "terminal",
"command": "P=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence\nB=app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa\necho '# 능력 선언 — 제약을 평범한 문자열로만 담는 이유'\nsed -n '10,17p' $P/api/capability/CapabilitySupport.java\necho '# 컴팩트 생성자가 강제하는 것'\nsed -n '19,28p' $P/api/capability/CapabilitySupport.java\necho\necho '# 특권 리포트 — 담지 않는 것이 정의다'\nsed -n '8,14p' $P/security/DatabasePrivilegeReport.java\necho\necho '# 밖으로 나가는 리포트 — 담을 수 없는 것'\nsed -n '8,17p' $B/JpaPlatformReport.java\necho '# 그 리포트가 특권 리포트를 담는 방식과, 그 불리언이 실제로 계산하는 것'\nsed -n '35,40p' $B/JpaPlatformReport.java\nsed -n '55,57p' $B/JpaPlatformReport.java\necho -n '# 역할 이름과 search_path 를 검사하는 정책의 main 호출자: '\ngrep -rn 'requireSafe(' --include=*.java . | grep '/main/' | grep -vc '/persistence/security/'\necho\necho '# 두 리포트의 컴포넌트 선언부에 있는 provider/자격증명 식별자 수'\nfor f in $B/JpaPlatformReport.java $P/security/DatabasePrivilegeReport.java; do\n echo -n \" $(basename $f): \"\n sed -n '/^public record/,/) {$/p' \"$f\" | grep -ic 'jdbcurl\\|password\\|host\\|url\\|datasource\\|entitymanager\\|sessionfactory'\ndone\necho\necho '# 그 리포트를 내보내는 엔드포인트'\ngrep -n '@Endpoint\\|@ReadOperation' $B/JpaPlatformEndpoint.java\necho -n ' @WriteOperation / @DeleteOperation / @Selector 매치: '\ngrep -c '@WriteOperation\\|@DeleteOperation\\|@Selector' $B/JpaPlatformEndpoint.java\nsed -n '10,14p' $B/JpaPlatformEndpoint.java\necho\necho '# 다만 그 엔드포인트는 지금 노출되지 않는다'\necho -n \" 저장소에서 'jpaplatform' 이 나오는 곳: \"\ngrep -rn 'jpaplatform' --include=*.yml --include=*.yaml --include=*.properties --include=*.java . | grep -vc /build/\ngrep -rn 'jpaplatform' --include=*.yml --include=*.yaml --include=*.properties --include=*.java . | grep -v /build/ | sed 's|.*/||'\necho ' 출하 설정의 노출 목록'\ngrep -n -A2 'include: health' app-bootstrap/src/main/resources/application.yml\necho ' 등록 지점이 그것을 적어 둔다'\nsed -n '131,133p' $B/JpaPlatformRuntimeAutoConfiguration.java\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T02:10:16+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-report-that-cannot-carry-a-datasource.txt",
"svg": "evidence/rendered/a-report-that-cannot-carry-a-datasource.svg",
"rawSha256": "0509f66ce40017d18a4aa74259e013c9865443a8a72c54a5290f2899ec33206f",
"lines": 78,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-resumed-redrive-skips-what-it-could-not-move",
"kind": "terminal",
"command": "M=messaging/messaging-admin-runtime/src/main/java/dev/caskeleton/messaging/admin/runtime\nA=messaging/messaging-admin-api/src/main/java/dev/caskeleton/messaging/admin\nT=messaging/messaging-admin-runtime/src/test/java/dev/caskeleton/messaging/admin/runtime\necho '# 재개 지점의 계약'\ngrep -n 'param resumeFrom' $M/RedriveService.java\necho '# 그리고 건너뛰는 자리의 주석'\nsed -n '109,113p' $M/RedriveService.java\necho\necho '# 그런데 체크포인트가 세는 것은 시도한 개수다'\nsed -n '105,107p;114,120p' $M/RedriveService.java\necho\necho '# 그 체크포인트 값이 다음 재개 지점이 된다'\nsed -n '55,61p' $M/InMemoryAdminOperationJournal.java\nsed -n '101,106p' $M/InMemoryAdminOperationJournal.java\necho '# 두 저널 구현이 각각 되돌아가지 않게 고정한다'\ngrep -n 'Math.max(current.itemsCompleted()' $M/InMemoryAdminOperationJournal.java | head -1\ngrep -rn 'GREATEST' --include=JdbcAdminOperationJournal.java . | sed 's|.*/||' | head -1\necho\necho '# 완료로 닫힌 승인은 다시 인수되지 않는다'\nsed -n '86,89p' $M/InMemoryAdminOperationJournal.java\necho\necho '# 목록은 매번 다시 조회하고, 확인되지 않은 재발행은 정착시키지 않는다'\ngrep -n 'source.peek(request.source' $M/RedriveService.java\nsed -n '10,13p' $A/RedriveResult.java\necho\necho '# 전부 정산됐는지 묻는 술어가 이미 있다'\nsed -n '41,48p' $A/RedriveResult.java\necho -n '# 그 술어를 부르는 main 코드: '\ngrep -rn 'isFullyAccounted' --include=*.java . | grep '/main/' | grep -vc 'RedriveResult.java'\necho -n '# 부르는 테스트 메서드: '\ngrep -rn 'isFullyAccounted' --include=*.java . | grep '/test/' | sed 's|.*/||'\necho\necho '# 이 경로에는 실행 가능한 구현이 없다'\necho -n ' 오케스트레이터를 생성하는 코드: '\ngrep -rn 'new DefaultMessagingAdminService' --include=*.java . | wc -l\necho -n ' RedriveSource 를 구현하는 main 코드: '\ngrep -rn 'implements RedriveService.RedriveSource' --include=*.java . | grep -c '/main/'\necho ' 유일한 구현과 그 조회·정착'\nsed -n '183,201p' $T/RedriveResumptionTest.java\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T02:14:53+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-resumed-redrive-skips-what-it-could-not-move.txt",
"svg": "evidence/rendered/a-resumed-redrive-skips-what-it-could-not-move.svg",
"rawSha256": "a32b123e148be248309cf0a689987896b7916c92518d4a19b90d23ca935cb51d",
"lines": 88,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-retry-implementation-nobody-calls",
"kind": "terminal",
"command": "set -e\nJ=src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction\nB=src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa\necho \"# 삭제를 설명하는 문단과 그 마지막 문장\"\nsed -n \"33,39p\" $B/JpaTransactionAutoConfiguration.java\necho\necho \"# 그 문장이 지목한 클래스의 선언\"\ngrep -n \"public final class FullTransactionRetryCoordinator\" $J/FullTransactionRetryCoordinator.java | sed \"s/^/ /\"\necho\necho \"# 그 포트를 실제로 구현하는 클래스\"\ngrep -rn \"implements PolicyTransactionPort\" --include=*.java src | grep \"/src/main/\" | sed \"s|.*/java/dev/caskeleton/| |\"\ngrep -n \"@Component\" $J/SpringTransactionPort.java | sed \"s/^/ /\"\necho\necho \"# 조립된 쪽의 재시도 루프와 그 게이트\"\nsed -n \"63,73p\" $J/SpringPolicyTransactionPort.java\nsed -n \"143,149p\" $J/SpringPolicyTransactionPort.java\necho\necho \"# 두 구현이 읽는 값\"\nsed -n \"38,43p\" $J/JpaTransactionSettings.java\ngrep -n \"DEFAULT_MAX_RETRY_ELAPSED\\|retryCoordinator(\" $B/JpaTransactionAutoConfiguration.java | sed \"s/^/ /\"\necho\necho \"# main 에서 코디네이터를 언급하는 파일 전부\"\ngrep -rn \"FullTransactionRetryCoordinator\" --include=*.java src | grep \"/src/main/\" | sed \"s|.*/java/dev/caskeleton/| |\"\necho\necho \"# 파라미터로 받거나 컨테이너에서 꺼내는 곳\"\nprintf \" FullTransactionRetryCoordinator 를 파라미터로 받는 선언 : %s\\n\" \"$(grep -rn 'FullTransactionRetryCoordinator [a-z]' --include=*.java src | grep '/src/main/' | grep -v 'public FullTransactionRetryCoordinator' | wc -l)\"\nprintf \" ObjectProvider<FullTransactionRetryCoordinator> : %s\\n\" \"$(grep -rn 'ObjectProvider<FullTransactionRetryCoordinator>' --include=*.java src | wc -l)\"\nprintf \" getBean 으로 이 타입을 꺼내는 곳 : %s\\n\" \"$(grep -rn 'getBean(FullTransactionRetryCoordinator' --include=*.java src | wc -l)\"\nprintf \" 소스 밖에서 이 이름이 나오는 곳 : %s\\n\" \"$(grep -rn 'FullTransactionRetryCoordinator' src --binary-files=without-match --exclude-dir=build --exclude-dir=.gradle 2>/dev/null | grep -vc '\\.java:' || true)\"\necho\necho \"# 선언된 능력\"\ngrep -n \"Full-transaction retry\" docs/jpa/support-matrix.md | sed \"s/^/ /\"\necho\necho \"# 지워진 이름을 건드린 커밋 (경로 제한 없음)\"\ngit log --all --oneline -S\"RetryableJpaTransactionInterceptor\" | sed \"s/^/ /\"\necho\necho \"# 같은 트리에서 이미 도는 AOP\"\ngrep -rn \"@Transactional\" --include=*.java src/adapter/outbound/persistence-jpa/src/main | sed \"s|.*/java/dev/caskeleton/| |\" | head -4\ngrep -n \"Advisor\\|AnnotationMatchingPointcut\" src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/MethodSecurityConfig.java | sed \"s/^/ /\" | head -4",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T13:33:03+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-retry-implementation-nobody-calls.txt",
"svg": "evidence/rendered/a-retry-implementation-nobody-calls.svg",
"rawSha256": "e758b2aa43e9fe1a6f60b311260c1d42b217f9150badd79376ba65a09099479f",
"lines": 84,
"redaction": "none — 코드베이스 정적 검색 및 이력 조회"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed",
"kind": "terminal",
"command": "G=grpc/grpc-client/src/main/java/dev/caskeleton/grpc/client\nC=grpc/grpc-policy/src/main/java/dev/caskeleton/grpc/security/GrpcCredentialRotationManager.java\necho '# 회전 — 읽고 판단한 뒤 조건 없이 쓴다'\nsed -n '87,94p;102,105p' $G/GrpcChannelRuntimeRegistry.java\necho '# 같은 클래스의 첫 설치는 비교 후 교체를 쓴다'\nsed -n '45,47p' $G/GrpcChannelRuntimeRegistry.java\necho -n '# 그 파일의 compareAndSet 매치 수: '\ngrep -c 'compareAndSet' $G/GrpcChannelRuntimeRegistry.java\necho -n '# 덮인 대체본을 담는 곳 — draining 에 add 되는 값: '\ngrep -n 'add(previous)' $G/GrpcChannelRuntimeRegistry.java\necho\necho '# 이 세대가 들고 있는 것은 채널이 아니다'\ngrep -n 'private final\\|private volatile' $G/GrpcChannelRuntime.java\necho -n '# 이 모듈 main 의 io.grpc 참조: '\ngrep -rn 'io\\.grpc' --include=*.java grpc/grpc-client/src/main/ | wc -l\necho\necho '# 계수기 — 확인하고 별도 연산으로 줄인다'\nsed -n '50,54p;66,70p' $G/GrpcChannelRuntime.java\necho -n '# 이 파일의 compareAndSet / updateAndGet 매치: '\ngrep -c 'compareAndSet\\|updateAndGet' $G/GrpcChannelRuntime.java\necho '# 올리는 쪽은 배수가 시작되면 거절한다'\nsed -n '41,45p' $G/GrpcChannelRuntime.java\necho '# 조용함 판정'\nsed -n '93,95p' $G/GrpcChannelRuntime.java\necho -n '# finishUnaryCall 의 프로덕션 호출자: '\ngrep -rn 'finishUnaryCall' --include=*.java . | grep -v '/test/' | grep -vc 'GrpcChannelRuntime.java'\necho\necho '# 회수 — 동기화 목록을 잠그지 않고 순회한다'\ngrep -n 'synchronizedList' $G/GrpcChannelRuntimeRegistry.java\nsed -n '113,115p;122,131p' $G/GrpcChannelRuntimeRegistry.java\necho -n '# retireQuiescent 의 프로덕션 호출자: '\ngrep -rn 'retireQuiescent' --include=*.java . | grep -v '/test/' | grep -vc 'GrpcChannelRuntimeRegistry.java'\necho\necho '# 옆 모듈 — 배수 완료는 판단조차 없다'\nsed -n '117,121p' $C\necho -n '# 그 파일의 compareAndSet / updateAndGet / synchronized 매치: '\ngrep -cE 'compareAndSet|updateAndGet|synchronized' $C\necho\necho -n '# 두 모듈의 런타임 소속: '\npython3 -c \"import json;m=json.load(open('config/architecture/modules.json'));ls=[x for x in m.get('modules',m) if isinstance(x,dict)];print(' '.join(f\\\"{x['id']}={x.get('runtime_memberships')}\\\" for x in ls if x.get('id','').endswith('grpc-client') or x.get('id','').endswith('grpc-policy')))\"\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T02:27:51+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.txt",
"svg": "evidence/rendered/a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed.svg",
"rawSha256": "8aec7ab0ec62008120b7a7bbc3ec02eb1d5a65bf5b2892587c041deee207d323",
"lines": 77,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-soak-threshold-written-for-a-grade-that-does-not-exist",
"kind": "terminal",
"command": "B=grpc-advanced/grpc-advanced-bootstrap/src; G=$B/main/java/dev/caskeleton/grpc/advanced\necho '# 게이트 javadoc 의 등급 모형 — 둘'\nsed -n '12,17p' $G/release/GrpcAdvancedPromotionGate.java\necho '# 그리고 상수 둘'\nsed -n '20,24p' $G/release/GrpcAdvancedPromotionGate.java\necho\necho '# 그런데 등급 열거형의 값은 넷이다'\ngrep -n '^ [A-Z_]*(' $G/bootstrap/GrpcCapabilityGrade.java\necho -n \"# 그중 'Stable default' 에 해당하는 값: \"\ngrep -c 'STABLE_DEFAULT\\|StableDefault' $G/bootstrap/GrpcCapabilityGrade.java\necho\necho '# 증거 검사와 담금 선택 — 증거 쪽에는 목표 등급 조건이 없다'\nsed -n '44,49p' $G/release/GrpcAdvancedPromotionGate.java\necho -n '# 증거 항목 수: '\ngrep -c 'missing.add(' $G/release/GrpcAdvancedPromotionEvidence.java\ngrep -n 'missing.add(' $G/release/GrpcAdvancedPromotionEvidence.java | sed 's/.*missing.add(//;s/);//' | tr '\\n' ' '\necho\necho\necho '# 추적 등급은 중간 등급을 먼저 밟아야 한다'\nsed -n '60,65p' $G/release/GrpcAdvancedPromotionGate.java\necho\necho '# 긴 갈래를 실행하는 테스트가 고른 전이'\nsed -n '47,50p;58,63p' $B/test/java/dev/caskeleton/grpc/advanced/release/GrpcAdvancedPromotionGateTest.java\necho\necho '# 중간 등급으로 올라가는 테스트가 준 담금'\nsed -n '67,68p;78,85p' $B/test/java/dev/caskeleton/grpc/advanced/release/GrpcAdvancedPromotionGateTest.java\necho\necho '# 그 테스트들을 실제로 돌린 결과'\n./gradlew :grpc-advanced:grpc-advanced-bootstrap:test --tests '*PromotionGate*' --offline --console=plain 2>&1 | grep -E 'tests completed|BUILD'\nX=$(find grpc-advanced/grpc-advanced-bootstrap/build/test-results -name '*PromotionGateTest.xml' | head -1)\ngrep -o 'tests=\"[0-9]*\" skipped=\"[0-9]*\" failures=\"[0-9]*\" errors=\"[0-9]*\"' \"$X\" | head -1\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T02:31:19+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-soak-threshold-written-for-a-grade-that-does-not-exist.txt",
"svg": "evidence/rendered/a-soak-threshold-written-for-a-grade-that-does-not-exist.svg",
"rawSha256": "9936aebbfa815936257a5ee595dbb42cd739c2aad6440f0bb43183e300109dc4",
"lines": 66,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-startup-probe-that-never-runs",
"kind": "terminal",
"command": "R=adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config\nT=adapter/outbound/cache-redis/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config\necho '# 탐침이 자기를 소개하는 문단'\nsed -n '14,21p' $R/RedisStartupProbe.java\necho\necho '# 그것이 확인하는 넷'\nsed -n '54,68p' $R/RedisStartupProbe.java\necho\necho '# 넷째의 javadoc — 실측과 결론'\nsed -n '98,112p' $R/RedisCapabilityProbe.java\necho '# 그리고 검사가 두 겹인 이유'\nsed -n '118,124p' $R/RedisCapabilityProbe.java\necho\necho '# 그 실패는 일어나지 않는다'\necho -n ' 두 탐침 밖에서 그것들을 언급하는 main 코드: '\ngrep -rn 'RedisCapabilityProbe\\|RedisStartupProbe' --include=*.java . | grep '/main/' | grep -v 'RedisCapabilityProbe.java' | grep -vc 'RedisStartupProbe.java'\ngrep -rn 'RedisCapabilityProbe\\|RedisStartupProbe' --include=*.java . | grep '/main/' | grep -v 'RedisCapabilityProbe.java' | grep -v 'RedisStartupProbe.java' | sed 's|.*/||'\necho -n ' confirm( 을 부르는 main 코드: '\ngrep -rn '\\.confirm(' --include=*.java adapter/outbound/cache-redis | grep -c '/main/'\necho\necho '# 자동설정이 만드는 빈'\ngrep -n -A2 '@Bean' $R/RedisSdkAutoConfiguration.java | grep 'public ' | sed 's/- */ /'\necho -n ' 그중 탐침: '\ngrep -n -A3 '@Bean' $R/RedisSdkAutoConfiguration.java | grep -c 'Probe'\necho\necho '# 로직을 고정하는 테스트 케이스 수'\necho -n ' RedisCapabilityProbeTest: '; grep -c '@Test' $T/RedisCapabilityProbeTest.java\necho -n ' RedisStartupProbeTest: '; grep -c '@Test' $T/RedisStartupProbeTest.java\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T02:33:02+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-startup-probe-that-never-runs.txt",
"svg": "evidence/rendered/a-startup-probe-that-never-runs.svg",
"rawSha256": "593c42b7f462f8fbc31483eb28c4674be2da0c81a5f98a2570a7d942374a3517",
"lines": 70,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-startup-validator-that-requires-a-key-nothing-signs-with",
"kind": "terminal",
"command": "G=adapter/inbound/graphql/src; M=$G/main/java/dev/caskeleton/adapter/inbound/graphql; T=$G/test/java/dev/caskeleton/adapter/inbound/graphql\necho '# 시작 검증기가 거절하는 문구'\nsed -n '43p' $M/autoconfigure/GraphQlPlatformStartupValidator.java\necho -n '# 그 검증기는 실제로 돈다 — 자동설정의 호출 지점: '\ngrep -n 'validator.validate(properties)' $M/autoconfigure/GraphQlPlatformAutoConfiguration.java\necho\necho '# 그 키 설정을 소비하는 main 코드'\ngrep -rn 'cursor().keyIds()' --include=*.java $M | sed 's|.*/||'\necho -n '# 그중 액추에이터 엔드포인트를 만드는 main 코드: '\ngrep -rn 'new GraphQlPlatformActuatorEndpoint' --include=*.java . | grep -c '/main/'\necho\necho '# 서명 코덱과 키링'\necho -n ' GraphQlCursorCodec 구현체 수: '\ngrep -rln 'implements GraphQlCursorCodec' --include=*.java . | wc -l\necho -n ' 그것을 만드는 main 코드: '\ngrep -rn 'new HmacGraphQlCursorCodec\\|GraphQlCursorKeyRing.of(\\|GraphQlCursorKeyRing.single(' --include=*.java . | grep '/main/' | grep -v 'HmacGraphQlCursorCodec.java' | grep -vc 'GraphQlCursorKeyRing.java'\necho\necho '# 서명될 커서가 지금 있는가'\necho -n ' 커넥션 조립기를 만드는 main 코드: '\ngrep -rn 'new GraphQlConnectionAssembler' --include=*.java . | grep -c '/main/'\necho ' 배포되는 스키마 전체'\ngrep -v '^#' $M/../../../../../resources/graphql/skeleton.graphqls 2>/dev/null || grep -v '^#' $G/main/resources/graphql/skeleton.graphqls\necho\necho '# 그런데 릴리스 게이트가 읽는 매니페스트는 이미 승인 목록에 넣어 두었다'\ngrep -n -B3 'SIGNED_CURSOR_CONNECTION' $M/release/GraphQlStableCapabilityManifest.java | head -6\necho\necho '# 그 부재를 고정하는 테스트'\nsed -n '78,84p' $T/autoconfigure/GraphQlPolicyRequestPathTest.java\necho '# 그리고 요구는 남기라고 못 박는 네 번째 케이스'\nsed -n '100,110p' $T/autoconfigure/GraphQlPolicyRequestPathTest.java\necho\necho '# 등급표가 두 능력에 매긴 등급'\ngrep -n 'cursor 서명\\|mutation 멱등성' adapter/inbound/graphql/CLAUDE.md | cut -c1-70\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T02:41:05+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-startup-validator-that-requires-a-key-nothing-signs-with.txt",
"svg": "evidence/rendered/a-startup-validator-that-requires-a-key-nothing-signs-with.svg",
"rawSha256": "7c787f63cee203f0036fe508ecdf41069c5402b4b58f2b8a9826af8ae7e64d85",
"lines": 52,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-test-that-passed-on-the-wrong-guard",
"kind": "terminal",
"command": "N=adapter/outbound/notification/src\nC=$N/main/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationBindingCompiler.java\nT=$N/test/java/dev/caskeleton/adapter/outbound/notification/catalog/NotificationBindingCompilerTest.java\necho '# 첫 가드가 SINGLE 아닌 전략을 전부 거부한다'\nsed -n '140,145p' $C\necho '# 그 뒤 전략을 보지 않는 검사 둘'\nsed -n '146,158p' $C\necho\necho '# switch 네 갈래 — SINGLE 을 뺀 셋은 도달할 수 없다'\nsed -n '159,185p' $C\necho\necho '# 그리고 폴백 그래프 검사가 던질 수 있는 두 문구'\ngrep -n 'fallback references unknown target' $C\ngrep -n 'cyclic notification fallback graph' $C\necho\necho '# 순환을 거부한다는 테스트'\ngrep -n 'void legacyReceiptUnsafeFallbackBoundsAndCyclesAreRejected' $T\necho '# 그 테스트가 만든 팬아웃 라우트'\nsed -n '246,253p' $T\necho '# 그리고 순환 라우트 — 전략과 범위 값'\nsed -n '279,300p' $T\necho '# 그 둘에 붙은 단언'\ngrep -n 'hasMessageContaining' $T | sed -n '5,6p'\necho\necho '# 이 두 분기를 태울 라우트를 만드는 곳 — 저장소 전수'\ngrep -rn 'NotificationRouteStrategy\\.\\(FAN_OUT_ALL\\|ORDERED_FALLBACK\\)' --include=*.java . | sed 's|.*/||'\necho -n '# 설정 파일의 비-SINGLE 전략: '\ngrep -rn 'FAN_OUT_ALL\\|ORDERED_FALLBACK' --include=*.json --include=*.yaml --include=*.yml . | wc -l\necho\necho '# 그 테스트를 실제로 돌린 결과'\n./gradlew :adapter:outbound:notification:test --tests '*NotificationBindingCompiler*' --offline --console=plain 2>&1 | grep -E 'BUILD|tests completed'\nX=$(find $N/../build/test-results -name '*NotificationBindingCompilerTest.xml' 2>/dev/null | head -1)\n[ -n \"$X\" ] && grep -o 'tests=\"[0-9]*\" skipped=\"[0-9]*\" failures=\"[0-9]*\" errors=\"[0-9]*\"' \"$X\" | head -1\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T02:48:01+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-test-that-passed-on-the-wrong-guard.txt",
"svg": "evidence/rendered/a-test-that-passed-on-the-wrong-guard.svg",
"rawSha256": "3271ecb9ed1356089f705ff969cc3502243b98761b41779f78477f191168005a",
"lines": 106,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-transaction-capability-true-and-its-validator-never-run",
"kind": "terminal",
"command": "M=messaging; K=$M/messaging-kafka/src/main/java/dev/caskeleton/messaging/kafka\nS=$M/messaging-spring-boot-starter/src/main/java/dev/caskeleton/messaging/autoconfigure\necho '# 능력 성분의 아홉째 자리'\ngrep -n 'boolean brokerTransaction' $M/messaging-core-api/src/main/java/dev/caskeleton/messaging/api/destination/MessagingCapabilities.java\necho '# Kafka 가 넘기는 열두 값 (아홉째가 참)'\nsed -n '63,64p' $K/KafkaMessagingTransport.java\necho ' ^^^^ brokerTransaction'\necho -n '# 그 상수 선언이 프로파일을 참조하는가: '\nsed -n '62,64p' $K/KafkaMessagingTransport.java | grep -c 'profile\\|Profile'\necho -n '# 이 플래그를 읽는 main 코드: '\ngrep -rn 'brokerTransaction()' --include=*.java . | grep -c '/main/'\necho -n '# 바로 옆 열째 플래그를 읽는 main 코드: '\ngrep -rn 'deduplicatedPublish()' --include=*.java . | grep '/main/' | sed 's|.*/||'\necho '# 그 열째가 거짓인 이유'\nsed -n '53,56p' $K/KafkaMessagingTransport.java\necho\necho '# 트랜잭션 검증기가 요구하는 다섯'\ngrep -n 'if (' $K/KafkaTransactionProfileValidator.java\nsed -n '46,52p' $K/KafkaTransactionProfileValidator.java\necho\necho '# 기동 검증으로 감싸이는 검증기와 직접 불리는 검증기'\ngrep -rn 'StartupProfileValidation<' --include=*.java $S | grep -v 'StartupProfileValidation.java' | grep 'public ' | sed 's|.*/||'\ngrep -n 'new DestinationProfileValidator().validateAll' $S/MessagingCoreAutoConfiguration.java\necho '# 그런데 트랜잭션 검증기는 그냥 빈이다'\nsed -n '72,77p' $S/KafkaMessagingAutoConfiguration.java\necho -n '# 자동설정 밖에서 그것을 언급하는 main 코드: '\ngrep -rn 'KafkaTransactionProfileValidator' --include=*.java . | grep '/main/' | grep -v 'KafkaTransactionProfileValidator.java' | grep -vc 'KafkaMessagingAutoConfiguration.java'\necho -n '# 그것을 만드는 test 코드: '\ngrep -rn 'KafkaTransactionProfileValidator' --include=*.java . | grep -c '/test/'\necho\necho '# 감쌀 수 없는 이유는 인자 수가 아니다'\nsed -n '35,40p' $S/StartupProfileValidation.java\necho -n '# 두 번째 인자의 출처 — transactionalIdPrefix 가 나오는 곳: '\ngrep -rn 'transactionalIdPrefix\\|transactional-id-prefix' --include=*.java --include=*.yml --include=*.properties . | grep -v /build/ | sed 's|.*/||'\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T02:52:53+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-transaction-capability-true-and-its-validator-never-run.txt",
"svg": "evidence/rendered/a-transaction-capability-true-and-its-validator-never-run.svg",
"rawSha256": "f29b62722b7d8dd348908d7249cad9e9001a42d52a466068affd027bc2e4c4d2",
"lines": 54,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-validator-checking-the-wrong-datasource-run",
"kind": "terminal",
"command": "set -e\nset -o pipefail\ncd /shared/codebase/clean-architecture-backend-template/src\nX=app-bootstrap/build/test-results/test/TEST-dev.caskeleton.bootstrap.autoconfigure.jpa.JpaResolvedDataSourceValidationTest.xml\nrm -f \"$X\"\nset +e\n./gradlew :app-bootstrap:test --tests '*JpaResolvedDataSourceValidationTest*' --offline -q 2>&1 | tail -5\ngradle_status=${PIPESTATUS[0]}\nset -e\necho \"gradle 종료 코드 : $gradle_status\"\npython3 - \"$X\" <<'PY'\nimport sys, xml.etree.ElementTree as ET\nsuite = ET.parse(sys.argv[1]).getroot()\nprint(f\" suite : {suite.get('name').rsplit('.', 1)[-1]}\")\nprint(f\" tests={suite.get('tests')} failures={suite.get('failures')} \"\n f\"errors={suite.get('errors')} skipped={suite.get('skipped')} time={suite.get('time')}s\")\nfor case in suite.findall('testcase'):\n verdict = '실패' if (case.find('failure') is not None or case.find('error') is not None) else '통과'\n print(f\" {verdict} · {case.get('name')} ({case.get('time')}s)\")\nPY\n",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-03T12:41:50+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-validator-checking-the-wrong-datasource-run.txt",
"svg": "evidence/rendered/a-validator-checking-the-wrong-datasource-run.svg",
"rawSha256": "fbae9cc7864d99f814ac73c5c46d53aa4b68f0de4d6d60999c3d1c01030086f8",
"lines": 6,
"redaction": "없음 — gradle 출력 끝부분과 JUnit 결과"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a-validator-that-demands-tls-and-an-assembly-that-omits-it-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/messaging/messaging-spring-boot-starter\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\")\n\ncat > \"$D/TlsDefaultProbe.java\" <<'JAVA'\nimport org.apache.kafka.clients.CommonClientConfigs;\nimport org.apache.kafka.clients.producer.ProducerConfig;\nimport org.apache.kafka.common.config.SaslConfigs;\nimport org.apache.kafka.common.config.SslConfigs;\nimport org.apache.kafka.common.serialization.ByteArraySerializer;\nimport org.apache.kafka.common.serialization.StringSerializer;\nimport java.util.HashMap;\nimport java.util.Map;\nimport java.util.TreeSet;\n\n/** 조립되는 두 생산자의 설정 맵을 그대로 Kafka 클라이언트에 넘겨 해석된 보안 값을 읽는다. */\npublic final class TlsDefaultProbe {\n\n /** KafkaMessagingAutoConfiguration.messagingKafkaProducer:139-152 이 넣는 것 전부. */\n private static Map<String, Object> platformTransport() {\n Map<String, Object> config = new HashMap<>();\n config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, \"broker-1:9093\");\n config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);\n config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, ByteArraySerializer.class);\n config.put(ProducerConfig.ACKS_CONFIG, \"all\");\n config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);\n return config;\n }\n\n /** KafkaSenderConfig.kafkaSeamProducer:65-80 이 넣는 것 전부. */\n private static Map<String, Object> seam() {\n Map<String, Object> config = new HashMap<>();\n config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, \"broker-1:9093\");\n config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);\n config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, StringSerializer.class);\n config.put(ProducerConfig.ACKS_CONFIG, \"all\");\n config.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);\n config.put(ProducerConfig.MAX_BLOCK_MS_CONFIG, 10_000);\n config.put(ProducerConfig.LINGER_MS_CONFIG, 0);\n config.put(ProducerConfig.REQUEST_TIMEOUT_MS_CONFIG, 15_000);\n config.put(ProducerConfig.DELIVERY_TIMEOUT_MS_CONFIG, 30_000);\n return config;\n }\n\n private static void report(String label, Map<String, Object> config) {\n System.out.println(\" \" + label);\n System.out.println(\" 조립부가 넣은 키 \" + config.size() + \" 개 : \" + new TreeSet<>(config.keySet()));\n ProducerConfig resolved = new ProducerConfig(config);\n System.out.println(\n \" security.protocol = \"\n + resolved.getString(CommonClientConfigs.SECURITY_PROTOCOL_CONFIG));\n System.out.println(\n \" ssl.endpoint.identification.algorithm = \\\"\"\n + resolved.getString(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG) + \"\\\"\");\n System.out.println(\n \" sasl.mechanism = \"\n + resolved.getString(SaslConfigs.SASL_MECHANISM));\n System.out.println(\n \" sasl.jaas.config = \"\n + resolved.getPassword(SaslConfigs.SASL_JAAS_CONFIG));\n System.out.println(\n \" ssl.enabled.protocols = \"\n + resolved.getList(SslConfigs.SSL_ENABLED_PROTOCOLS_CONFIG));\n System.out.println();\n }\n\n public static void main(String[] args) {\n System.out.println();\n System.out.println(\"kafka-clients \" + org.apache.kafka.common.utils.AppInfoParser.getVersion()\n + \" 가 아래 설정 맵에서 해석하는 보안 값\");\n report(\"KafkaMessagingAutoConfiguration.messagingKafkaProducer\", platformTransport());\n report(\"KafkaSenderConfig.kafkaSeamProducer\", seam());\n }\n}\nJAVA\njavac -encoding UTF-8 -cp \"$cp\" -d \"$D/out\" \"$D/TlsDefaultProbe.java\" 2>&1 | head -10\njava -Dstdout.encoding=UTF-8 -cp \"$D/out:$cp\" TlsDefaultProbe 2>&1 | grep -vE '^[0-9]{4}-|^\\s+at |WARN |INFO |SLF4J'\n",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-03T12:25:21+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-validator-that-demands-tls-and-an-assembly-that-omits-it-run.txt",
"svg": "evidence/rendered/a-validator-that-demands-tls-and-an-assembly-that-omits-it-run.svg",
"rawSha256": "8166ac858937e31c89d3b966af339b8d7ca308e0c93d3359f550afe3bb25416f",
"lines": 19,
"redaction": "없음 — 로그 줄만 걸러낸 실행 출력"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-validator-that-demands-tls-and-an-assembly-that-omits-it",
"kind": "terminal",
"command": "set -e\nset -o pipefail\nK=src/messaging/messaging-kafka/src/main/java/dev/caskeleton/messaging/kafka\nA=src/messaging/messaging-spring-boot-starter/src/main/java/dev/caskeleton/messaging/autoconfigure\nB=src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/messaging\nstrip() { sed -E 's#^src/[^:]*/src/(main|test|testFixtures)/[^:]*/([^:/]+):([0-9]+):# \\1 · \\2:\\3 #'; }\ncut_range() { awk -v f=\"$2\" -v a=\"$3\" -v b=\"$4\" 'NR>=a && NR<=b {printf \" %s:%d %s\\n\", f, NR, $0}' \"$1\"; }\n\necho \"# 운영 프로파일에 KafkaProfileValidator 가 거는 두 요구\"\ncut_range $K/KafkaProfileValidator.java KafkaProfileValidator.java 51 58\n\necho\necho \"# 그 검증기가 기동에서 도는 경로와, 그 거부를 지키는 테스트\"\ngit grep -n 'kafkaProfileStartupValidation' -- 'src/**/*.java' | strip\ngit grep -n 'implements InitializingBean\\|afterPropertiesSet' -- '**/StartupProfileValidation.java' | strip\ngit grep -n 'aProductionKafkaBrokerWithoutTransportSecurityFailsStartup' -- 'src/**/*.java' | strip\n\necho\necho \"# 두 조립부를 켜는 프로퍼티\"\ngit grep -n 'name = \"enabled\", havingValue = \"true\"' -- 'src/**/*.java' | grep -iE 'messaging' | strip\ngit grep -n '\"kafka\", KafkaMessagingAutoConfiguration.class' -- 'src/**/*.java' | strip\ngrep -n '^@ConditionalOnProperty' $B/KafkaSenderConfig.java | sed -E 's#^([0-9]+):# main · KafkaSenderConfig.java:\\1 #'\necho -n \" 출하 기본값 : \"\ngrep -n 'APP_MESSAGING_ENABLED' src/app-bootstrap/src/main/resources/application.yml | head -1 | sed 's#^#application.yml:#'\n\necho\necho \"# 프로덕션에서 KafkaProducer 를 만드는 두 자리 — 설정 맵 원문 그대로\"\ngrep -n '@Bean\\|@ConditionalOnMissingBean' $A/KafkaMessagingAutoConfiguration.java \\\n | awk -F: '$1>=126 && $1<=130' | sed -E 's#^([0-9]+): *# KafkaMessagingAutoConfiguration.java:\\1 #'\ncut_range $A/KafkaMessagingAutoConfiguration.java KafkaMessagingAutoConfiguration.java 139 153\necho\ngrep -n '@Bean(name = \"kafkaSeamProducer\"\\|@ConditionalOnMissingBean(name' $B/KafkaSenderConfig.java \\\n | sed -E 's#^([0-9]+): *# KafkaSenderConfig.java:\\1 #'\ncut_range $B/KafkaSenderConfig.java KafkaSenderConfig.java 64 81 | grep -v '^ KafkaSenderConfig.java:7[0-6]'\n\necho\necho \"# security.protocol 을 넣는 프로덕션 코드 — 상수명과 리터럴 양쪽으로\"\ngit grep -n 'SECURITY_PROTOCOL\\|\"security\\.protocol\"\\|SslConfigs\\|SaslConfigs\\|CommonClientConfigs' -- 'src/**/*.java' \\\n | grep '/main/' | strip\n\necho\necho \"# KafkaSecurityConfigurer.configure 가 자격 종류마다 하는 일\"\nawk 'NR>=88 && NR<=115 && (/properties.put\\(/ || /case BrokerCredentialProfile/ || /throw new/) {printf \" KafkaSecurityConfigurer.java:%d %s\\n\", NR, $0}' \\\n $K/KafkaSecurityConfigurer.java\n\necho\necho \"# 그 클래스가 빈이 되기까지의 조건 사슬\"\nawk 'NR>=105 && NR<=112 {printf \" KafkaMessagingAutoConfiguration.java:%d %s\\n\", NR, $0}' $A/KafkaMessagingAutoConfiguration.java\ngit grep -n 'ConditionalOnBean(CredentialProvider.class)\\|CredentialRuntimeRegistry credentialRuntimeRegistry' -- 'src/**/*.java' | strip\nn=$(git grep -ln 'implements CredentialProvider' -- 'src/**/*.java' | grep -c '/main/' || true)\necho \" CredentialProvider 를 구현하는 main 클래스 : $n 건\"\necho \" 그 인터페이스를 구현하는 자리 전부 :\"\ngit grep -n 'implements CredentialProvider' -- 'src/**/*.java' | strip\n\necho\necho \"# 그 빈을 파라미터나 필드로 받는 코드 (빈을 만드는 팩토리 자신은 뺀다)\"\nhits=$(git grep -n 'KafkaSecurityConfigurer' -- 'src/**/*.java' \\\n | grep -v '/KafkaSecurityConfigurer.java:' \\\n | grep -v 'public KafkaSecurityConfigurer kafkaSecurityConfigurer' \\\n | grep -v 'return new KafkaSecurityConfigurer' | grep -v 'import ' | grep '/main/' || true)\nif [ -z \"$hits\" ]; then echo \" main 에서 0 건\"; else echo \"$hits\" | strip; fi\necho \" getBean · ObjectProvider · 빈 이름 문자열로 가져가는 자리 :\"\ng=$(git grep -n 'getBean(KafkaSecurityConfigurer\\|ObjectProvider<KafkaSecurityConfigurer>\\|\"kafkaSecurityConfigurer\"' -- 'src/**/*.java' || true)\nif [ -z \"$g\" ]; then echo \" 0 건\"; else echo \"$g\" | strip; fi\n\necho\necho \"# 조립을 확인하는 테스트가 단언하는 것과, 실 브로커 시험이 붙는 컨테이너\"\ngit grep -n 'void selectingKafkaAssemblesOnlyKafka\\|void aSelectedTransportAssemblesAPublisher' -- 'src/**/*.java' | strip\ngit grep -n 'hasSingleBean\\|doesNotHaveBean' -- '**/MessagingStarterOffContractTest.java' | strip\ngit grep -n 'new KafkaContainer(' -- 'src/**/MessagingLiveRoundTripQualificationTest.java' | strip\n",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-03T12:25:08+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-validator-that-demands-tls-and-an-assembly-that-omits-it.txt",
"svg": "evidence/rendered/a-validator-that-demands-tls-and-an-assembly-that-omits-it.svg",
"rawSha256": "72b47b4d66f485e84de208664844697d0ad0c23e4e43b0a18df95a18d75d9ef0",
"lines": 102,
"redaction": "코드베이스 측정으로 교체"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-weaker-private-copy-on-the-wired-path-probe",
"kind": "terminal",
"command": "echo '# 값 타입에 목적지를 직접 넣어 무엇이 통과하는지 본다'\nsed -n '10,19p' /tmp/ed/WebPushProbe.java\necho\njavac -encoding UTF-8 -cp application-core/build/classes/java/main -d /tmp/ed/probe /tmp/ed/WebPushProbe.java \\\n && java -Dstdout.encoding=UTF-8 -cp application-core/build/classes/java/main:/tmp/ed/probe WebPushProbe\n\necho\necho '# 이웃 호출처는 같은 결함을 이미 고쳤고, 고치면서 무엇이 남았는지 적었다'\nsed -n '29,33p' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webhook/WebhookSubscription.java\n\necho\necho '# 그 엔드포인트가 가는 곳'\ngrep -n -A3 'return new NotificationHttpRequest' adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/webpush/WebPushRequestMapper.java\n\necho\necho '# 그 값을 만드는 main 코드 — 저장이 아니라 저장된 것을 되읽는 경로다'\ngrep -rn 'new WebPushSubscriptionValue' --include=*.java . | grep '/main/' | sed 's|.*/||'\nP=adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/security/AesGcmContactPointProtector.java\ngrep -n 'ProtectedContactPoint protect(\\|ContactPointValue reveal(\\|private static WebPushSubscriptionValue parseWebPush' \"$P\"\necho '# 접수 유스케이스는 그 채널을 주소 문자열로 받지 않는다'\nsed -n '205,211p' application-core/src/main/java/dev/caskeleton/application/notification/platform/dispatch/AcceptNotificationApplicationUseCase.java\necho '# 알림 플랫폼 마스터 스위치의 출하 기본값'\ngrep -n 'APP_NOTIFICATION_PLATFORM_ENABLED' app-bootstrap/src/main/resources/application.yml",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:21:47+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-weaker-private-copy-on-the-wired-path-probe.txt",
"svg": "evidence/rendered/a-weaker-private-copy-on-the-wired-path-probe.svg",
"rawSha256": "dcfda14138291401fa6d431e3dc2f389470be365d1534bdd2daeaf155c968a5a",
"lines": 49,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a-weaker-private-copy-on-the-wired-path",
"kind": "terminal",
"command": "V=application-core/src/main/java/dev/caskeleton/application/notification/platform/contact/WebPushSubscriptionValue.java\nN=adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/provider/http/NotificationEndpoints.java\nG=adapter/outbound/notification/src/test/java/dev/caskeleton/adapter/outbound/notification/platform/provider/http/EndpointGuardCallSiteTest.java\n\necho '# 값 타입의 컴팩트 생성자가 하는 검사 전부'\ngrep -n '' \"$V\" | sed -n '22,38p'\necho '# 다섯째 검사의 구현 — 같은 파일 안의 private 메서드'\ngrep -n '' \"$V\" | sed -n '50,59p'\n\necho\necho '# 그 private 사본이 베낀 원본. 같은 모듈의 공개 함수다.'\ngrep -n '' \"$N\" | sed -n '24,35p'\n\necho\necho '# 같은 파일의 다른 공개 함수가 자기가 누구를 위한 것인지 적는다'\ngrep -n '' \"$N\" | sed -n '47,51p'\necho '# 그 함수가 실제로 하는 검사'\ngrep -n '' \"$N\" | sed -n '65,99p' | grep -v ': *//' | grep -v ': *$'\n\necho\necho '# 강한 쪽의 프로덕션 호출처'\ngrep -rn 'NotificationEndpoints.requireExternallyRoutable' --include=*.java . | grep '/main/' | sed 's|.*/||'\necho -n '# 그중 웹푸시: '\ngrep -rn 'NotificationEndpoints.requireExternallyRoutable' --include=*.java . | grep '/main/' | grep -ci webpush\necho -n '# 값 타입이 두 공개 함수 중 하나라도 부르는 줄: '\ngrep -c 'NotificationEndpoints' \"$V\"\n\necho\necho '# 호출처 도달을 고정하려고 만든 테스트가, 무엇을 목록으로 들고 있는가'\ngrep -n '' \"$G\" | sed -n '17,22p'\ngrep -n '@DisplayName' \"$G\"\necho -n '# 그 파일에서 WebPush 를 언급하는 줄: '\ngrep -c 'WebPush' \"$G\"",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 1,
"executedAt": "2026-09-02T03:21:47+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a-weaker-private-copy-on-the-wired-path.txt",
"svg": "evidence/rendered/a-weaker-private-copy-on-the-wired-path.svg",
"rawSha256": "d44231245b29e7b5a8c66c2faa381f00a4da08a6ec9ad83b0ca2e6e551114bf5",
"lines": 102,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a01-f002-idfactory-newid",
"kind": "terminal",
"command": "D=domain-core/src/main/java/dev/caskeleton/domain/identifier\nS=sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio\n\necho '# 식별자 생성 포트 전문'\ncat $D/IdFactory.java\necho '# 그 포트가 만드는 값의 계약'\nsed -n '11,15p' $D/ResourceId.java\n\necho\necho -n '# 이 포트를 직접 구현하는 main 클래스: '\ngrep -rn 'implements IdFactory<' --include=*.java . | grep -c '/main/'\necho '# 샘플이 정의한 팩토리 인터페이스 넷'\ngrep -rn 'interface .*IdFactory' --include=*.java . | grep '/main/' | grep -v '/domain-core/' | sed 's|^\\./||'\necho '# 그중 포트를 확장한 둘을 구현하는 어댑터'\ngrep -rn 'class .* implements \\(WorkLogIdFactory\\|PosterIdFactory\\)' --include=*.java . | grep '/main/' | sed 's|.*/||'\n\necho\necho '# 확장하는 둘은 메서드를 더하지 않는다'\ngrep -n 'public interface' $S/domain/worklog/WorkLogIdFactory.java $S/domain/poster/PosterIdFactory.java\necho '# 확장하지 않는 둘은 반환형이 포트의 바운드를 만족하지 못한다'\ngrep -n 'String newEventId();' $S/domain/worklog/OutboxEventIdFactory.java\ngrep -n 'AllocatedOperation allocate();' $S/application/posterimage/PosterImageOperationIdFactory.java\n\necho\necho '# 실제 구현이 무엇을 부르는가'\nsed -n '17,21p' $S/adapter/outbound/identifier/UuidWorkLogIdFactory.java\n\necho\necho '# 값 객체가 실제로 검사하는 것'\nsed -n '15,24p' $S/domain/worklog/WorkLogId.java\n\necho\necho '# 모듈 README 가 적는 책임 분리'\nsed -n '37,45p' domain-core/README.md\n\necho\necho '# 저장소 수준 유일성을 실제로 강제하는 곳'\ngrep -n 'PRIMARY KEY' sample-portfolio/src/main/resources/db/sample-migration/V2__work_log.sql\ngrep -n 'PRIMARY KEY' sample-portfolio/src/main/resources/db/sample-migration/V7__poster.sql",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:24:41+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a01-f002-idfactory-newid.txt",
"svg": "evidence/rendered/a01-f002-idfactory-newid.svg",
"rawSha256": "adba3239e210b029c090117c03da21c260030d60ab0198141ada702fb01555c5",
"lines": 73,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f004-retrydecision-reason-probe",
"kind": "terminal",
"command": "T=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction\nO=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation\n\necho '# 프로덕션이 실제로 내는 사유 전수'\ngrep -n 'retryable persistence failure' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/RetryDecision.java\ngrep -n 'RetryDecision\\.\\(fail\\|reconcile\\|retry\\)(' -A2 $T/DefaultJpaRetryPolicy.java | grep '\"'\n\necho\necho '# 같은 모듈이 저카디널리티를 이미 정의해 뒀다'\nsed -n '12,20p' $O/LowCardinality.java\nsed -n '24,34p' $O/LowCardinality.java\n\necho\necho '# 그 사유들을 그 가드에 실제로 넣어 본다'\nCP=adapter/outbound/persistence-jpa/build/classes/java/main\njavac -encoding UTF-8 -cp $CP -d /tmp/ed/probe /tmp/ed/ReasonProbe.java \\\n && java -Dstdout.encoding=UTF-8 -cp $CP:/tmp/ed/probe ReasonProbe",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:27:21+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f004-retrydecision-reason-probe.txt",
"svg": "evidence/rendered/a05-f004-retrydecision-reason-probe.svg",
"rawSha256": "dd1a3bf67f1ab2cb62a3f64c4a94b17dd6ea4acfa88b0886bb65e52717a9a2fd",
"lines": 41,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f004-retrydecision-reason",
"kind": "terminal",
"command": "J=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction\nT=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction\nO=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation\n\necho '# 사유 필드와 그 문서'\nsed -n '6,12p' $J/RetryDecision.java\necho '# 컴팩트 생성자가 검사하는 것 전부'\nsed -n '14,27p' $J/RetryDecision.java\necho '# 공개 팩토리 넷. 사유를 인자로 받는 것은 셋이다.'\ngrep -n 'public static RetryDecision' $J/RetryDecision.java\necho -n '# 이 파일에서 길이·어휘·형식을 검사하는 코드: '\ngrep -c 'length()\\|matches(\\|Set.of(\\|enum' $J/RetryDecision.java\n\necho\necho '# 이 타입을 소비하는 main 파일 전부'\nFILES=$(grep -rl 'persistence.api.transaction.RetryDecision' --include=*.java . | grep '/main/')\necho \"$FILES\" | sed 's|.*/||'\necho -n '# 그중 reason() 을 읽는 줄: '\ngrep -rn '\\.reason()' $FILES | wc -l\n\necho\necho '# 그런데 결정 객체 자체는 메트릭을 내는 곳까지 간다'\ngrep -n 'listener.onAttemptFailed' $T/FullTransactionRetryCoordinator.java\ngrep -n 'LowCardinality.requireRegistered\\|\\.tag(' $O/JpaRetryObservation.java",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:27:20+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f004-retrydecision-reason.txt",
"svg": "evidence/rendered/a05-f004-retrydecision-reason.svg",
"rawSha256": "b99dd4894928172f41b66c3bd9eb7e83fe031c33d7d79f92a4d13c4fe5131ee2",
"lines": 40,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f005-jparetrypolicy-probe",
"kind": "terminal",
"command": "echo '# 애플리케이션이 준 정책을 넣고, 그 정책이 불리는지 센다'\nsed -n '46,53p' /tmp/ed/CustomPolicyProbe.java\nsed -n '63,66p' /tmp/ed/CustomPolicyProbe.java\necho\nCP=$(sed 's/^TESTCP=//' /tmp/ed/cpjpa.txt)\njavac -encoding UTF-8 -cp \"$CP\" -d /tmp/ed/probe /tmp/ed/CustomPolicyProbe.java \\\n && java -Dstdout.encoding=UTF-8 -cp \"$CP:/tmp/ed/probe\" CustomPolicyProbe\n\necho\necho '# 플랫폼이 자기 빈에 넣는 기본 프로파일'\nsed -n '168,178p' app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:30:06+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f005-jparetrypolicy-probe.txt",
"svg": "evidence/rendered/a05-f005-jparetrypolicy-probe.svg",
"rawSha256": "1121c5ad3f74afcdb1617486ffe0e27b0ccbdcb867157b9d880bde5002a1618b",
"lines": 30,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f005-jparetrypolicy",
"kind": "terminal",
"command": "A=app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java\nC=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/FullTransactionRetryCoordinator.java\nP=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionProfile.java\n\necho '# 조립 팩토리 둘. 앞의 것이 왜 그렇게 만드는지 적혀 있다.'\nsed -n '91,111p' $A\n\necho\necho '# 뒤의 오버로드가 넘긴 정책이 담기는 필드'\nsed -n '34,43p' $C\necho '# 그 필드를 읽는 유일한 자리'\nsed -n '88,94p' $C\necho '# 그런데 프로파일 타입이 그 조건을 금지한다'\nsed -n '26,30p' $P\n\necho\necho '# retryCoordinator( 호출 전수 (선언 제외)'\ngrep -rn 'retryCoordinator(' --include=*.java . | grep -v 'public FullTransactionRetryCoordinator' | sed 's|^\\./||'\necho '# 코디네이터를 직접 생성하는 곳과 그때 넘기는 정책'\ngrep -rn -A2 'new FullTransactionRetryCoordinator(' --include=*.java . | grep 'policy\\|RetryPolicy' | sed 's|^\\./||' ",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:30:06+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f005-jparetrypolicy.txt",
"svg": "evidence/rendered/a05-f005-jparetrypolicy.svg",
"rawSha256": "3fdb1881743d716d924c4efe0df052d8745fa8942e0887f3227cef9d9d3dfa0d",
"lines": 55,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f006-stable-chain",
"kind": "terminal",
"command": "M=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java\nA=app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java\nC=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/CommitFailureClassifier.java\n\necho '# 루트의 몫이라고 적힌 두 가지'\ngrep -n '' \"$A\" | sed -n '51,62p'\necho -n '# 그 분류기 팩토리를 부르는 코드: '\ngrep -rn 'commitFailureClassifier()' --include=*.java . | grep -v '/build/' | grep -vc 'public CommitFailureClassifier commitFailureClassifier'\necho '# 이 자동설정 클래스가 스프링 설정인가'\ngrep -n 'class JpaTransactionAutoConfiguration\\|Plain construction' \"$A\"\necho '# 이 클래스를 쓰는 프로덕션 코드가 부르는 메서드'\ngrep -rn -A2 'new JpaTransactionAutoConfiguration(' --include=*.java . | grep -v '/build/' | grep '\\.' | sed 's|^\\./||'\n\necho\necho '# 완료 불명 예외가 만들어지는 유일한 곳과 그것을 부르는 유일한 곳'\ngrep -rn 'new TransactionCompletionUnknownException' --include=*.java . | grep '/main/' | sed 's|^\\./||'\ngrep -rn 'translateCommitFailure' --include=*.java . | grep '/main/' | grep -v 'CommitFailureClassifier.java' | sed 's|^\\./||'\necho '# 그 분류기가 프레임에서 꺼내는 값'\ngrep -n 'frame\\.map\\|frame\\.flatMap' \"$C\"\necho -n '# 단계를 읽는 접근자를 부르는 코드: '\ngrep -rn 'committing()' --include=*.java . | grep -v '/build/' | grep -vc 'public boolean committing'\n\necho\necho '# 조립되는 실행기는 프레임을 만든다'\ngrep -n -B2 'TransactionEvidenceContext.begin' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringJpaTransactionExecutor.java\ngrep -n 'ConditionalOnBean(PlatformTransactionManager\\|SpringJpaTransactionExecutor transactionExecutor' app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java\n\necho\necho '# 그 매니저의 순서를 실행하는 테스트가 있는가'\necho -n '# EvidenceAwareJpaTransactionManagerTest 안의 그 타입 참조: '\ngrep -c 'EvidenceAwareJpaTransactionManager' adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManagerTest.java\ngrep -n 'The manager itself needs\\|exercised end to end' adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManagerTest.java",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:48:09+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f006-stable-chain.txt",
"svg": "evidence/rendered/a05-f006-stable-chain.svg",
"rawSha256": "d031ec02975f0f5858b22549458fff931f6e3cab10dab7bc34a013785a99dc1b",
"lines": 45,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f006-stable",
"kind": "terminal",
"command": "M=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/EvidenceAwareJpaTransactionManager.java\n\necho '# 능력 리포트가 이 능력에 매긴 등급'\ngrep -rn 'COMPLETION_EVIDENCE' --include=*.java --exclude-dir=build . | grep '/main/' | sed 's|.*/||'\n\necho\necho '# 매니저의 javadoc 이 주장하는 것'\ngrep -n '' \"$M\" | sed -n '11,23p'\necho '# 같은 파일의 코드가 하는 것'\ngrep -n '' \"$M\" | sed -n '49,73p'\n\necho\necho '# 그 매니저를 만드는 코드'\ngrep -rn 'new EvidenceAwareJpaTransactionManager\\|EvidenceAwareJpaTransactionManager\\.standard' --include=*.java --exclude-dir=build . | sed 's|^\\./||'\necho -n '# 자기 파일과 javadoc 을 뺀 생성 지점: '\ngrep -rn 'new EvidenceAwareJpaTransactionManager\\|EvidenceAwareJpaTransactionManager\\.standard' --include=*.java --exclude-dir=build . \\\n | grep -v 'transaction/EvidenceAwareJpaTransactionManager.java' | grep -vc ' \\* '\necho -n '# FQCN 이 설정 리소스에 등장하는 곳: '\ngrep -rln 'EvidenceAwareJpaTransactionManager' --exclude-dir=build --include=*.yml --include=*.properties --include=*.imports --include=spring.factories --include=*.xml --include=*.json . | wc -l\necho -n '# 그 클래스를 상속하는 코드: '\ngrep -rn 'extends EvidenceAwareJpaTransactionManager' --include=*.java --exclude-dir=build . | wc -l\n\necho\necho '# 트랜잭션 매니저 빈을 등록하는 코드 (애너테이션 다음 여섯 줄까지 본다)'\necho -n '# main: '\ngrep -rn -A6 '@Bean' --include=*.java --exclude-dir=build . | grep '/main/' | grep -cE 'TransactionManager +[A-Za-z]+\\('\necho -n '# test: '\ngrep -rn -A6 '@Bean' --include=*.java --exclude-dir=build . | grep '/test/' | grep -cE 'TransactionManager +[A-Za-z]+\\('\ngrep -rn -A6 '@Bean' --include=*.java --exclude-dir=build . | grep '/test/' | grep -E 'TransactionManager +[A-Za-z]+\\(' | sed 's|.*/||' ",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:48:36+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f006-stable.txt",
"svg": "evidence/rendered/a05-f006-stable.svg",
"rawSha256": "53f721c9d2e413612bde42943f0d6813748b9919bd2894725afa247f6a75fc72",
"lines": 56,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f007-transactionprofileregistry-adr",
"kind": "terminal",
"command": "echo '# 사슬의 나머지가 사라진 지점'\ngit log --oneline -S'RetryableJpaTransactionInterceptor' -- . | cat\ngit show 2f5d2fc2 --stat --oneline | grep -i 'RetryableJpaTransaction'\ngit show 2f5d2fc2 --stat --oneline | tail -1\necho -n '# 그 클래스가 지금 main 에 남긴 줄: '\ngrep -rn 'RetryableJpaTransactionInterceptor' --include=*.java . | grep -c '/main/'\n\necho\necho '# 삭제되기 직전, 그 인터셉터가 레지스트리를 어떻게 썼는가'\ngit show 2f5d2fc2^:src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/RetryableJpaTransactionInterceptor.java \\\n | grep -n 'DEFAULT_ORDER =\\|TransactionProfileRegistry profiles\\|profiles\\.require\\|coordinator\\.execute'\n\necho\necho '# 같은 커밋이 레지스트리 javadoc 에서 애너테이션 이름만 지웠다'\ngit show 2f5d2fc2 -- adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java \\\n | grep '^[-+] \\*'\n\necho\necho '# 문자열 이름을 fail-closed 로 찾는 형제는 하나뿐이다'\ngrep -n 'public SafeSortField require(String' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/SafeSortRegistry.java\ngrep -n 'registry\\.require(' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/SafeSortMapper.java\necho '# 다른 둘은 키 타입이 달라 오타가 들어올 자리가 없다'\ngrep -n -B1 'require(GrpcChannelProfileName' grpc/grpc-client/src/main/java/dev/caskeleton/grpc/client/GrpcChannelRuntimeRegistry.java\ngrep -n -B1 'require(MongoConsistencyProfile' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/consistency/MongoConsistencyRegistry.java\n\necho\necho '# ADR 은 결정 절과 강제 절 양쪽에서 사라진 것을 전제한다'\ngrep -n 'ordered outside\\|DEFAULT_ORDER' ../docs/adr/ADR-JPA-002-full-transaction-retry.md\necho -n '# DEFAULT_ORDER 가 소스 전체에 있는 줄: '\ngrep -rn 'DEFAULT_ORDER' --include=*.java . | wc -l\necho -n '# JPA 리프 main 에 MethodInterceptor 나 Advisor 가 있는 줄: '\ngrep -rn 'MethodInterceptor\\|Advisor' --include=*.java adapter/outbound/persistence-jpa/src/main | wc -l\n\necho\necho '# 레지스트리를 지키는 유일한 테스트가 고정하는 것'\ngrep -n 'Plan Task\\|@DisplayName' adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistryTest.java",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:33:25+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f007-transactionprofileregistry-adr.txt",
"svg": "evidence/rendered/a05-f007-transactionprofileregistry-adr.svg",
"rawSha256": "e5d1bc4177fab6ee8303e4364b6d631fc840579f6c20b3ec8cc7546682cb2c21",
"lines": 43,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f007-transactionprofileregistry",
"kind": "terminal",
"command": "SELF=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistry.java\n\necho '# 자리 — 이 리프의 공개 API 패키지인가, 구현 패키지인가'\ngrep -n '^package' \"$SELF\"\necho -n '# 리프의 api/** 아래 main 파일: '\nfind adapter/outbound/persistence-jpa/src/main/java -path '*/api/*' -name '*.java' | wc -l\necho -n '# 그중 이 파일: '\nfind adapter/outbound/persistence-jpa/src/main/java -path '*/api/*' -name 'TransactionProfileRegistry.java' | wc -l\n\necho\necho '# 제거 조건 하나 — 외부 프로덕션 참조'\necho -n '# 자기 파일을 뺀 main 참조: '\ngrep -rnw 'TransactionProfileRegistry' --include=*.java . | grep '/main/' | grep -vc \"$SELF\"\necho -n '# 자동설정 등록 리소스와 spring.factories: '\ngrep -rl 'TransactionProfileRegistry' --include=*.imports --include=spring.factories . 2>/dev/null | wc -l\necho '# 패키지는 스캔 대상이지만 이 클래스에 스테레오타입이 없다'\ngrep -rn 'persistence.transaction' --include=JpaAdapterComponentsConfig.java . | sed 's|.*/||'\necho -n '# 이 파일의 스테레오타입 애너테이션: '\ngrep -c '^@\\|^ @Component\\|^ @Service' \"$SELF\"\necho -n '# 전용 단위 테스트 한 파일 안의 참조: '\ngrep -rcw 'TransactionProfileRegistry' adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionProfileRegistryTest.java\n\necho\necho '# 제거 조건 둘 — 설정 경로'\necho -n '# 출하 yml 전부에서 프로파일을 이름으로 등록하는 키: '\ngrep -rl 'transaction-profile\\|transactionProfile' app-bootstrap/src/main/resources sample-portfolio/src/main/resources 2>/dev/null | wc -l\necho -n '# @ConfigurationProperties 클래스 전부에서의 참조: '\ngrep -rln '@ConfigurationProperties' --include=*.java . | grep '/main/' | xargs grep -lw 'TransactionProfileRegistry' 2>/dev/null | wc -l\n\necho\necho '# 제거 조건 셋 — 대체 경로가 같은 동작을 증명하는가'\necho -n '# 레지스트리가 담는 타입 TransactionProfile 이 나타나는 main 줄: '\ngrep -rnw 'TransactionProfile' --include=*.java . | grep '/main/' | grep -v 'api/transaction/TransactionProfile.java' | grep -vc \"$SELF\"\necho -n '# 그중 import 와 javadoc 을 뺀 타입 사용: '\ngrep -rnw 'TransactionProfile' --include=*.java . | grep '/main/' | grep -v 'api/transaction/TransactionProfile.java' | grep -v \"$SELF\" | grep -vc 'import \\|{@link'\necho '# 그 줄이 있는 파일'\ngrep -rlw 'TransactionProfile' --include=*.java . | grep '/main/' | grep -v 'api/transaction/TransactionProfile.java' | grep -v \"$SELF\" | sed 's|.*/||' | sed 's/^/# /'\necho '# 포트 시그니처 — 프로파일은 이름이 아니라 두 번째 인자의 객체다'\ngrep -n 'T execute(' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/JpaTransactionExecutor.java\necho -n '# 자기 파일 밖 main 에서 TransactionProfile 을 만드는 곳: '\ngrep -rn 'TransactionProfile\\.write(\\|TransactionProfile\\.read(\\|new TransactionProfile(' --include=*.java . | grep '/main/' | grep -vc 'api/transaction/TransactionProfile.java'\necho '# 포트 자체를 참조하는 main 파일 (단어 경계, 구현체와 구분)'\ngrep -rlnw 'JpaTransactionExecutor' --include=*.java . | grep '/main/' | sed 's|.*/||' | sed 's/^/# /'\n\necho\necho '# 프로덕션 트랜잭션이 실제로 지나는 곳은 다른 타입이다'\ngrep -n 'implements PolicyTransactionPort' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java\ngrep -n 'inTransaction(TransactionRequest' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java\necho -n '# 그 파일 안의 TransactionProfile 참조: '\ngrep -cw 'TransactionProfile' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java\necho -n '# PolicyTransactionPort 를 쓰는 main 파일: '\ngrep -rlw 'PolicyTransactionPort' --include=*.java . | grep -c '/main/' ",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:32:47+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f007-transactionprofileregistry.txt",
"svg": "evidence/rendered/a05-f007-transactionprofileregistry.svg",
"rawSha256": "0c2378fe164b1cae89e9c91db15fd75fcdb063258623568fc7809a06e6e93733",
"lines": 37,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f012-identity-probe",
"kind": "terminal",
"command": "echo '# 같은 IDENTITY 매핑을 두 접근 방식으로 선언해 가드에 넘긴다'\nsed -n '11,30p' /tmp/ed/BatchGuardProbe.java\necho\nCP=$(sed 's/^TESTCP=//' /tmp/ed/cpjpa.txt)\njavac -encoding UTF-8 -cp \"$CP\" -d /tmp/ed/probe /tmp/ed/BatchGuardProbe.java \\\n && java -Dstdout.encoding=UTF-8 -cp \"$CP:/tmp/ed/probe\" BatchGuardProbe\n\necho\necho '# 하이버네이트는 그 둘을 어떻게 읽는가 — 시퀀스를 대조군으로 둔다'\njavac -encoding UTF-8 -cp \"$CP\" -d /tmp/ed/probe /tmp/ed/IdentityBootstrapProbe.java \\\n && java -Dstdout.encoding=UTF-8 -cp \"$CP:/tmp/ed/probe\" IdentityBootstrapProbe 2>/dev/null | grep '접근\\|대조군'\n\necho\necho '# 가드의 거절이 근거로 삼는 측정과 그것이 도는 레인'\nI=adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/IdStrategyContractTest.java\ngrep -n '@Tag\\|@DisplayName\\|isGreaterThan\\|isZero\\|BATCH_SIZE =\\|ROWS =' \"$I\"\ngrep -n -A3 \"registerJpaPlatformLane(\" adapter/outbound/persistence-jpa/build.gradle | grep -E \"jpaPlatformContractTest|'jpa-contract'\"\ngrep -rn 'jpaPlatformContractTest' ../.github/workflows/*.yml | sed 's|.*/||' ",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:55:57+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f012-identity-probe.txt",
"svg": "evidence/rendered/a05-f012-identity-probe.svg",
"rawSha256": "5ed3cd4ac745df2871ba494b9ba913165186b68dedc07254cbe7dcef71a74dc7",
"lines": 48,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f012-identity",
"kind": "terminal",
"command": "G=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/batch/HibernateBatchConfigurationGuard.java\n\necho '# 가드가 식별자를 찾는 방식'\ngrep -n '' \"$G\" | sed -n '49,75p'\n\necho\necho '# 이 타입을 언급하는 곳 전부 (자기 파일 포함)'\ngrep -rnw 'HibernateBatchConfigurationGuard' --include=*.java --exclude-dir=build . | sed 's|^\\./||'\necho -n '# 그 타입을 인스턴스화하는 main 코드: '\ngrep -rn 'new HibernateBatchConfigurationGuard(' --include=*.java --exclude-dir=build . | grep -c '/main/'\necho -n '# 같은 패키지의 프로파일 레지스트리를 읽는 main 코드: '\ngrep -rnw 'JpaBatchProfileRegistry' --include=*.java --exclude-dir=build . | grep '/main/' | grep -vc 'batch/JpaBatchProfileRegistry.java'\necho '# 계획 문서가 이 과제의 산출물로 적은 것'\ngrep -rn 'startup diagnostics for IDENTITY' ../docs/superpowers/plans/2026-08-11-jpa-persistence-platform-implementation-plan.md\n\necho\necho '# 가드가 거절하려는 전략이 main 에 나타나는 곳'\ngrep -rn 'GenerationType.IDENTITY' --include=*.java --exclude-dir=build . | grep '/main/' | sed 's|^\\./||'\necho -n '# main 에서 @GeneratedValue 를 쓰는 줄: '\ngrep -rnw '@GeneratedValue' --include=*.java --exclude-dir=build . | grep -c '/main/'\n\necho\necho '# 출하 엔티티의 식별자 접근 방식'\necho -n '# main 의 @Entity 파일: '\ngrep -rl '^@Entity' --include=*.java --exclude-dir=build . | grep -c '/main/'\necho -n '# 그중 @Id 를 필드에 단 파일: '\ngrep -rl '^@Entity' --include=*.java --exclude-dir=build . | grep '/main/' | xargs grep -lw '@Id' | wc -l\necho -n '# @Id 바로 뒤가 메서드 선언인 것 (프로퍼티 접근): '\ngrep -rA1 -w '@Id' --include=*.java --exclude-dir=build . | grep '/main/' | grep -c 'public .* get.*()'\necho -n '# @Access 나 AccessType 을 쓰는 줄 (저장소 전체): '\ngrep -rwE '@Access|AccessType' --include=*.java --exclude-dir=build . | wc -l\necho -n '# @EmbeddedId 를 쓰는 main 파일: '\ngrep -rlw '@EmbeddedId' --include=*.java --exclude-dir=build . | grep '/main/' | sed 's|.*/||' ",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:55:56+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f012-identity.txt",
"svg": "evidence/rendered/a05-f012-identity.svg",
"rawSha256": "9c4542285561ec793c0ddf5fcccc3cbaebeff56e65bc410970f9454e12c88f4a",
"lines": 51,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f013-specificationpolicy-specification-unrestricted",
"kind": "terminal",
"command": "S=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata/SpecificationPolicy.java\nP=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/querydsl/PredicatePolicy.java\n\necho '# 문서가 무엇을 막겠다고 하는가 — 두 가지다'\ngrep -n '' \"$S\" | sed -n '7,17p'\necho '# 실제 검사'\ngrep -n '' \"$S\" | sed -n '31,51p'\n\necho\necho '# 이 타입을 언급하는 곳 (레포 전체, 언어 무관)'\ngrep -rInw 'SpecificationPolicy' --exclude-dir=.git --exclude-dir=build --exclude-dir=.gradle .. | sed 's|^\\.\\./||' | cut -c1-150\necho -n '# 두 검사 메서드를 부르는 코드 (정적 임포트 포함, 레포 전체): '\ngrep -rIn -e 'SpecificationPolicy\\.require' -e 'import static .*SpecificationPolicy\\.' \\\n --exclude-dir=.git --exclude-dir=build --exclude-dir=.gradle .. | wc -l\n\necho\necho '# 같은 리프의 형제 정책. 같은 토큰을 쓰고 같은 널 비교를 한다.'\ngrep -n 'ALLOW_UNBOUNDED_TOKEN =' \"$S\" \"$P\"\ngrep -n '' \"$P\" | sed -n '29,36p'\necho '# 형제 쪽 호출처와 그것을 부르는 테스트'\ngrep -rn 'PredicatePolicy\\.requireBounded' --include=*.java --exclude-dir=build . | sed 's|^\\./||' ",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T03:59:51+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f013-specificationpolicy-specification-unrestricted.txt",
"svg": "evidence/rendered/a05-f013-specificationpolicy-specification-unrestricted.svg",
"rawSha256": "51a49f0cb7c9700127834ce3a8be409de511d50912c733754ee43b9ed7073d40",
"lines": 61,
"redaction": "none — 출력에 자격증명이 없다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f014-collection-fetch-pagination-lane",
"kind": "terminal",
"command": "echo '# 릴리스 레지스트리가 이 게이트에 붙인 생산자 태스크'\ngrep -n -A3 '\"collection-fetch-pagination\"' src/config/jpa/release-registry.json\necho -n '# blocking 이 참인 게이트 / 거짓인 게이트: '\nprintf '%s / %s\\n' \"$(grep -c '\"blocking\": true' src/config/jpa/release-registry.json)\" \\\n \"$(grep -c '\"blocking\": false' src/config/jpa/release-registry.json)\"\nM=src/adapter/outbound/persistence-jpa/src/testkit/java/dev/caskeleton/adapter/outbound/persistence/testkit/release/JpaReleaseManifest.java\necho -n '# 매니페스트 파서가 blocking 을 읽는 줄: '\ngrep -c 'blocking' \"$M\"\necho -n '# 게이트 검증기가 blocking 을 읽는 줄: '\nsed -n '1046,1090p' src/build.gradle | grep -c 'blocking'\n\necho\necho '# 그 태스크와 게이트 테스트의 태그'\ngrep -n -A3 \"registerJpaPlatformLane(\" src/adapter/outbound/persistence-jpa/build.gradle | grep -E \"jpaPlatform|'jpa-\"\ngrep -n 'includeTags tag' src/adapter/outbound/persistence-jpa/build.gradle\ngrep -n '@Tag' src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/HibernateCollectionFetchPaginationContractTest.java\necho -n '# jpa-queryplan 태그를 단 클래스: '\ngrep -rl '@Tag(\"jpa-queryplan\")' --include=*.java --exclude-dir=build src | sed 's|.*/||'\n\necho\necho '# 그 불일치를 지나가게 하는 검증기가 무엇을 보는가'\nsed -n '1058,1078p' src/build.gradle | grep 'violations <<' | sed 's/^ */ /'\necho '# 레지스트리를 읽는 단위 테스트의 게이트 단언'\ngrep -n -A3 'for (String gate : JpaReleaseGate.required())' \\\n src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/testkit/release/../JpaReleaseManifestTest.java 2>/dev/null \\\n || grep -n -A3 'for (String gate : JpaReleaseGate.required())' \\\n src/adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/testkit/JpaReleaseManifestTest.java\n\necho\necho '# 그래도 그 테스트는 다른 레인으로 돈다'\ngrep -rn 'jpaPlatformContractTest' .github/workflows/*.yml | sed 's|.*/||' | cut -c1-90\ngrep -n 'dependsOn jpaPlatformContractTest' src/adapter/outbound/persistence-jpa/build.gradle",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T04:06:45+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f014-collection-fetch-pagination-lane.txt",
"svg": "evidence/rendered/a05-f014-collection-fetch-pagination-lane.svg",
"rawSha256": "1ae44ab4529b76bc1195b7737e690b2d6754547ec3cc0958a6cd14dda9efa1a0",
"lines": 46,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f014-collection-fetch-pagination-probe",
"kind": "terminal",
"command": "mkdir -p /tmp/probe\ncat > /tmp/probe/cp.sh <<'CPSH'\n# 락파일의 한 구성에 실린 좌표 전부를 캐시 경로로 바꾼다. Gradle 실행 없이 재현된다.\nLOCK=${1:-adapter/outbound/persistence-jpa/gradle.lockfile}\nCONF=${2:-postgresqlIntegrationTestRuntimeClasspath}\nCACHE=/root/.gradle/caches/modules-2/files-2.1\ncp=\"\"\nwhile IFS= read -r line; do\n coord=${line%%=*}; confs=${line#*=}\n case \",$confs,\" in *\",$CONF,\"*) ;; *) continue ;; esac\n group=${coord%%:*}; rest=${coord#*:}; name=${rest%%:*}; ver=${rest##*:}\n jar=$(find \"$CACHE/$group/$name/$ver\" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1)\n [ -n \"$jar\" ] && cp=\"$cp:$jar\"\ndone < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' \"$LOCK\")\necho \"${cp#:}\"\nCPSH\ncd src\nCP=$(bash /tmp/probe/cp.sh)\necho '# 락파일이 고정한 제공자'\necho \"$CP\" | tr : '\\n' | grep -E 'hibernate-core|/h2-' | sed 's|.*/||'\necho -n '# 저장소에서 fail_on_pagination_over_collection_fetch 를 켜는 곳: '\ngrep -rn 'fail_on_pagination' --include=*.java --include=*.yml --include=*.yaml --include=*.properties --include=*.gradle --exclude-dir=build .. | wc -l\n\necho\necho '# 게이트와 같은 모양의 조회를 그 제공자에게 직접 시킨다'\ncat > /tmp/probe/FetchPageProbe.java <<'JAVA'\nimport jakarta.persistence.CascadeType;\nimport jakarta.persistence.Entity;\nimport jakarta.persistence.GeneratedValue;\nimport jakarta.persistence.GenerationType;\nimport jakarta.persistence.Id;\nimport jakarta.persistence.ManyToOne;\nimport jakarta.persistence.OneToMany;\nimport java.util.ArrayList;\nimport java.util.List;\nimport org.hibernate.SessionFactory;\nimport org.hibernate.cfg.Configuration;\n\n/** 컬렉션 페치에 페이지 상한을 걸었을 때 상한이 SQL 로 가는지 본다. */\npublic class FetchPageProbe {\n\n @Entity(name = \"PagedParent\")\n public static class Parent {\n @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) Long id;\n String label;\n @OneToMany(mappedBy = \"parent\", cascade = CascadeType.ALL) List<Child> children = new ArrayList<>();\n }\n\n @Entity(name = \"PagedChild\")\n public static class Child {\n @Id @GeneratedValue(strategy = GenerationType.SEQUENCE) Long id;\n String label;\n @ManyToOne Parent parent;\n }\n\n public static void main(String[] args) {\n Configuration cfg = new Configuration();\n cfg.setProperty(\"hibernate.connection.driver_class\", \"org.h2.Driver\");\n cfg.setProperty(\"hibernate.connection.url\", \"jdbc:h2:mem:fetch;DB_CLOSE_DELAY=-1\");\n cfg.setProperty(\"hibernate.connection.username\", \"sa\");\n cfg.setProperty(\"hibernate.hbm2ddl.auto\", \"create-drop\");\n cfg.setProperty(\"hibernate.generate_statistics\", \"true\");\n cfg.addAnnotatedClass(Parent.class);\n cfg.addAnnotatedClass(Child.class);\n\n try (SessionFactory factory = cfg.buildSessionFactory()) {\n factory.inTransaction(session -> {\n for (int p = 0; p < 50; p++) {\n Parent parent = new Parent();\n parent.label = \"parent-\" + p;\n for (int c = 0; c < 4; c++) {\n Child child = new Child();\n child.label = \"child-\" + p + '-' + c;\n child.parent = parent;\n parent.children.add(child);\n }\n session.persist(parent);\n }\n });\n\n long before = factory.getStatistics().getPrepareStatementCount();\n List<Parent> page = factory.fromTransaction(session ->\n session.createQuery(\n \"select distinct p from PagedParent p left join fetch p.children order by p.id\",\n Parent.class)\n .setMaxResults(20)\n .getResultList());\n long statements = factory.getStatistics().getPrepareStatementCount() - before;\n\n System.out.println(\"반환된 부모 수 : \" + page.size());\n System.out.println(\"그 조회가 낸 프리페어드 스테이트먼트 : \" + statements);\n System.out.println(\"fail_on_pagination_over_collection_fetch : \"\n + factory.getSessionFactoryOptions().isFailOnPaginationOverCollectionFetchEnabled());\n }\n }\n}\nJAVA\njavac -encoding UTF-8 -nowarn -cp \"$CP\" -d /tmp/probe /tmp/probe/FetchPageProbe.java 2>&1 | grep -v '^Note:'\njava -Dstdout.encoding=UTF-8 -cp \"$CP:/tmp/probe\" FetchPageProbe 2>&1 \\\n | grep -E 'HHH90003004|반환된|스테이트먼트|fail_on' | sed 's/^[0-9:.]* \\[main\\] //'\n\necho\necho '# 서버 없이 매 빌드마다 도는 레인에도 같은 형태가 있다'\nsed -n '11,19p' adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/testkit/fetch/FetchPaginationExpectationTest.java",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T04:06:14+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f014-collection-fetch-pagination-probe.txt",
"svg": "evidence/rendered/a05-f014-collection-fetch-pagination-probe.svg",
"rawSha256": "91a21e235440739eb729b0da52a4bac4a742c691b207551b59f36ec425a0a517",
"lines": 21,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f014-collection-fetch-pagination",
"kind": "terminal",
"command": "T=src/adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/platform/HibernateCollectionFetchPaginationContractTest.java\nE=src/adapter/outbound/persistence-jpa/src/testkit/java/dev/caskeleton/adapter/outbound/persistence/testkit/fetch/FetchPaginationExpectation.java\n\necho '# 지원 매트릭스가 이 게이트에 붙인 목적'\ngrep -n 'collection-fetch-pagination' docs/jpa/support-matrix.md\n\necho\necho '# 테스트가 무엇을 단언한다고 적는가'\ngrep -n '' \"$T\" | sed -n '16,31p'\n\necho\necho '# 그 테스트가 실제로 단언하는 것'\ngrep -n 'assertThat\\|@DisplayName\\|PARENTS =\\|CHILDREN_PER_PARENT =\\|PAGE_SIZE =' \"$T\"\n\necho\necho '# 두 번째 단언이 보는 값은 어디서 오는가'\ngrep -n 'requiresDatabaseLimit is therefore\\|hibernate74PostgreSql\\|return new FetchPaginationExpectation' \"$E\"\ngrep -n -A2 'public static FetchPaginationExpectation hibernate74PostgreSql' \"$E\"\n\necho\necho '# 같은 리프의 이웃 클래스가 이 형태를 이름으로 부른다'\ngrep -n -B2 -A2 'green check that proves the constant equals itself' \\\n src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate/HibernateProviderPolicy.java",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T03:45:44+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f014-collection-fetch-pagination.txt",
"svg": "evidence/rendered/a05-f014-collection-fetch-pagination.svg",
"rawSha256": "bc7c6cc3b74318173ee6f1b67622b26bcee3ee2a23ae31443ee4a8a3c825cf45",
"lines": 51,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f018-sql",
"kind": "terminal",
"command": "H=src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/hibernate\nS=src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/springdata\n\necho '# 검사기가 무엇을 하는지, 그리고 무엇을 위한 것인지'\ngrep -n '' $H/NamedStatementInspector.java | sed -n '7,18p'\ngrep -n '' $H/NamedStatementInspector.java | sed -n '20,43p'\necho '# 이름 형식이 이미 * 와 / 를 막는다'\ngrep -n 'Pattern.compile' src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/QueryName.java\n\necho\necho '# 하이버네이트가 이 구현을 설치하는 경로 — 저장소 루트, 확장자 무제한'\necho -n '# statement_inspector 설정 키: '\ngrep -rn --binary-files=without-match 'statement_inspector\\|statementInspector\\|STATEMENT_INSPECTOR' --exclude-dir=build --exclude-dir=.git . | wc -l\necho -n '# HibernatePropertiesCustomizer / SessionFactoryBuilder / ServiceRegistry / Integrator: '\ngrep -rln 'HibernatePropertiesCustomizer\\|SessionFactoryBuilder\\|StandardServiceRegistry\\|org.hibernate.integrator' --exclude-dir=build --exclude-dir=.git . | wc -l\necho -n '# persistence.xml: '\nfind . -name persistence.xml -not -path '*/build/*' | wc -l\necho -n '# src 리소스와 자동설정 등록 파일의 FQCN: '\ngrep -rln 'NamedStatementInspector' --exclude-dir=build src/*/src/main/resources src/*/*/*/src/main/resources 2>/dev/null | wc -l\necho -n '# 그 클래스의 단위 테스트: '\nfind src -name 'NamedStatementInspector*Test.java' -o -name 'QueryNameContext*Test.java' | grep -vc build\n\necho\necho '# 이름을 컨텍스트에 넣는 세 곳'\ngrep -rn 'QueryNameContext\\.with(' --include=*.java --exclude-dir=build src | grep '/main/' | sed 's|^src/||'\necho -n '# 그 컨텍스트를 읽는 곳: '\ngrep -rn 'QueryNameContext\\.current()' --include=*.java --exclude-dir=build src | wc -l\n\necho\necho '# 이름을 SQL 로 옮기는 두 번째 경로'\ngrep -rn 'org.hibernate.comment' --include=*.java --exclude-dir=build src | grep '/main/' | sed 's|^src/||'\ngrep -n 'setHint(COMMENT_HINT' $S/JpaRepositoryFragmentSupport.java src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/querydsl/QuerydslJpaSupport.java | sed 's|.*/||'\necho -n '# 그 힌트가 SQL 로 나오게 하는 use_sql_comments 를 켜는 곳: '\ngrep -rn --binary-files=without-match 'use_sql_comments\\|useSqlComments' --exclude-dir=build --exclude-dir=.git . | wc -l\n\necho\necho '# 이름을 넣는 세 클래스를 누가 쓰는가'\nfor C in JpaStreamExecutor JpaKeysetQuerySupport JpaRepositoryFragmentSupport; do\n echo -n \"# $C 그 타입을 쓰는 파일 전부: \"\n grep -rlnw \"$C\" --include=*.java --exclude-dir=build src | sed 's|.*/||' | tr '\\n' ' '; echo\n echo -n \"# 상속(전체): \"\n grep -rn \"extends $C\" --include=*.java --exclude-dir=build src | sed 's|.*/||' | tr '\\n' ' '; echo\ndone",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T04:10:31+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f018-sql.txt",
"svg": "evidence/rendered/a05-f018-sql.svg",
"rawSha256": "74c726f1a709094bee07533d7df83e06e526afb4551aa4daff29827f06fec9b7",
"lines": 69,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f019-ssot",
"kind": "terminal",
"command": "L=adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/JpaModuleBoundaryTest.java\nR=app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java\n\necho '# 리프가 선언하는 export 목록'\ngrep -n -A14 'EXPORTED_PACKAGES =' \"$L\"\n\necho\necho '# 컴포지션 루트의 소비자 규칙이 같은 목록을 다시 적는다'\nsed -n '1112,1120p' \"$R\"\nsed -n '1129,1148p' \"$R\"\n\necho\necho '# 두 집합의 차이'\npython3 - <<'PYX'\nimport re\ndef pkgs(path, marker, span):\n text = open(path, encoding='utf-8').read().splitlines()\n start = next(i for i, l in enumerate(text) if marker in l)\n body = '\\n'.join(text[start:start + span])\n return {p for p in re.findall(r'\"([a-z][a-z0-9.]*)\"', body) if not p.endswith('.')}\nleaf = pkgs('adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/JpaModuleBoundaryTest.java', 'EXPORTED_PACKAGES =', 16)\nroot = pkgs('app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java', 'java.util.Set<String> EXPORTED =', 22)\nprint(f'# 리프 {len(leaf)}개 / 루트 {len(root)}개')\nprint(f'# 루트에만 있는 것: {sorted(root - leaf)}')\nprint(f'# 리프에만 있는 것: {sorted(leaf - root)}')\nPYX\n\necho\necho '# 사본은 정본을 이름으로 적는다'\ngrep -n 'owns the definition' \"$R\"\necho -n '# app-bootstrap 에서 그 정본 클래스를 언급하는 줄: '\ngrep -rn 'JpaModuleBoundaryTest' --include=*.java --exclude-dir=build app-bootstrap | wc -l\necho -n '# 선언 파일 밖에서 EXPORTED_PACKAGES 를 참조하는 자바 코드: '\ngrep -rn 'EXPORTED_PACKAGES' --include=*.java --exclude-dir=build . | grep -vc 'JpaModuleBoundaryTest.java'\n\necho\necho '# 리프의 export 목록을 쓰는 두 시험이 무엇을 보는가'\ngrep -n -A2 '@DisplayName(\"the export list names only packages that exist\")' \"$L\"\ngrep -n -A2 '@DisplayName(\"every exported package is one the catalog governs\")' \"$L\"",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T04:24:12+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f019-ssot.txt",
"svg": "evidence/rendered/a05-f019-ssot.svg",
"rawSha256": "0866dc172440fe0164347bfbc64c60481f27e90a5b3ceb1b6555f361b3f90f29",
"lines": 65,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f020-inspect-claim",
"kind": "terminal",
"command": "P=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java\nM=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRecordRows.java\nE=application-core/src/main/java/dev/caskeleton/application/idempotency/v2/IdempotencyExecutorV2.java\nI=adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlIdempotencyIntegrationTest.java\n\necho '# 청구 경로는 행을 잠근 뒤 데이터베이스 시각을 읽고 만료를 먼저 본다'\ngrep -n '' \"$P\" | sed -n '127,143p'\ngrep -n -A4 'private static boolean isExpiredCompleted' \"$P\"\necho -n '# claim 본문에서 databaseNow 를 부르는 줄: '\nsed -n '120,170p' \"$P\" | grep -c 'databaseNow'\n\necho\necho '# 조회 경로 전문'\ngrep -n '' \"$P\" | sed -n '393,427p'\necho -n '# inspect 본문에서 databaseNow 를 부르는 줄: '\nsed -n '393,427p' \"$P\" | grep -c 'databaseNow'\necho '# inspect 본문이 replayUntil 로 하는 일'\nsed -n '393,427p' \"$P\" | grep -n 'replayUntil'\necho '# 만료 비교는 파일에 한 곳뿐이고, 그것을 부르는 곳'\ngrep -n 'isExpiredCompleted' \"$P\"\necho '# 조회가 쓰는 조회 SQL 에 시간 술어가 있는가'\ngrep -n -A6 'SELECT_ROW_SQL' \"$M\" | head -10\n\necho\necho '# 조회 결과를 받는 세 자리'\ngrep -n -A3 'case COMPLETED_REPLAY' \"$E\"\n\necho\necho '# 출하 통합 시험의 만료 시험이 무엇을 만료시키는가'\ngrep -n ' void ' \"$I\" | grep -i expired\ngrep -n 'Duration.ofMillis(25)' \"$I\"\ngrep -n -A3 'private static IdempotencyClaimRequest request(' \"$I\"",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T04:35:46+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f020-inspect-claim.txt",
"svg": "evidence/rendered/a05-f020-inspect-claim.svg",
"rawSha256": "6e593699caca084c7ca0492d96ebfb5e84f65e0e1d20ce22efde4616db802ac3",
"lines": 95,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f021-complete-replayttl",
"kind": "terminal",
"command": "P=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java\nI=adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlIdempotencyIntegrationTest.java\nD=adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyDigestPolicyTest.java\nR=adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/idempotency\n\necho '# 두 번째 완료가 비교하는 것'\ngrep -n '' \"$P\" | sed -n '261,268p'\n\necho\necho '# 첫 완료가 다이제스트에 넣는 것과 그 이유 주석'\ngrep -n '' \"$P\" | sed -n '274,285p'\n\necho\necho '# 공용 헬퍼와 그것이 읽는 열'\ngrep -n -A13 'private static ReplayVerdict replayVerdict' \"$P\"\ngrep -n 'lastTransition' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyRecordRow.java\ngrep -n 'last_transition_result_digest' adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/idempotency/V1__expand_owner_safe_v2.sql\n\necho\necho '# 그 헬퍼를 쓰는 네 전이'\ngrep -n 'switch (replayVerdict(' \"$P\" | sed 's/^/ /'\necho -n '# complete 본문에서 그것을 부르는 줄: '\nawk '/public IdempotencyCompleteOutcome complete\\(/,/^ }$/' \"$P\" | grep -c 'replayVerdict'\n\necho\necho '# 값이 다르다는 것은 이미 단위 시험이 고정한다'\ngrep -n -A4 'twoCompletionsWithDifferentReplayWindowsDiffer' \"$D\"\n\necho\necho '# 갱신 경로의 주석'\ngrep -n '' \"$P\" | sed -n '216,218p'\n\necho\necho '# 출하 통합 시험이 덮는 인자 재생'\ngrep -n ' void ' \"$I\" | grep -iE 'conflict|retention|lease|renewal|SameArguments'\necho -n '# 그 파일에서 완료를 부르는 곳: '\ngrep -c 'store.complete(' \"$I\"\necho -n '# ALREADY_COMPLETED_SAME_RESULT 를 단언하는 곳: '\ngrep -c 'ALREADY_COMPLETED_SAME_RESULT' \"$I\"\n\necho\necho '# 레디스 구현도 같은 자리에서 멈춘다'\ngrep -n -A5 'case \"ALREADY\" ->' \"$R/RedisIdempotencyStoreAdapter.java\" | head -8\ngrep -n \"return {'ALREADY'\" \"$R/IdempotencyScripts.java\"\ngrep -n \"if ARGV\\[8\\] ~= ''\" \"$R/IdempotencyScripts.java\"",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T04:43:45+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f021-complete-replayttl.txt",
"svg": "evidence/rendered/a05-f021-complete-replayttl.svg",
"rawSha256": "8d0a4df8d3246c1af3da01256ec1ae478fd7aab2ea26baa3c795bc81b6e818dc",
"lines": 81,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f022-stable",
"kind": "terminal",
"command": "V=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/PostgreSqlRuntimeRoleVerifier.java\nD=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicy.java\nR=app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformReport.java\nA=app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfiguration.java\nC=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/capability/SupportLevel.java\n\necho '# 검증기 자신의 javadoc 이 하는 주장'\ngrep -n '' \"$V\" | sed -n '18,19p'\n\necho\necho '# 정책이 검사하는 네 항목'\ngrep -n 'allowedRoles.contains\\|report.canCreateInSchema()\\|report.canCreateInDatabase()\\|searchPathPolicy.requireSafe' \"$D\"\n\necho\necho '# 검증기는 프로덕션에서 실제로 불린다 — 다만 기동이 아니라 리포트를 만들 때다'\ngrep -n -A5 'private DatabasePrivilegeReport readPrivileges' \"$A\"\ngrep -n 'roleVerifier.verify(dataSource)' \"$A\"\n\necho\necho '# 정책을 넘기는 두 인자짜리 호출'\ngrep -n -A3 'public void requireSafe(DataSource dataSource' \"$V\"\necho -n '# 그 호출을 하는 곳 (레포 전체): '\ngrep -rn 'requireSafe([a-zA-Z]*, *[a-zA-Z]*)' --include=*.java --exclude-dir=build . | grep -c 'requireSafe(runtime\\|requireSafe(dataSource'\ngrep -rn '\\.requireSafe([a-zA-Z]*, *[a-zA-Z]*)' --include=*.java --exclude-dir=build . | sed 's|^\\./||'\necho -n '# DatabaseRolePolicy 를 만드는 main 코드: '\ngrep -rn 'new DatabaseRolePolicy\\|DatabaseRolePolicy\\.of' --include=*.java --exclude-dir=build . | grep -c '/main/'\necho -n '# DatabaseRolePolicy 를 언급하는 파일 전부: '\ngrep -rl 'DatabaseRolePolicy' --include=*.java --exclude-dir=build . | sed 's|.*/||' | tr '\\n' ' '; echo\n\necho\necho '# 기동 검사 빈이 실행하는 것'\ngrep -n -A6 'InitializingBean jpaPlatformStartupCheck' \"$A\" 2>/dev/null || \\\n grep -n -A6 'InitializingBean jpaPlatformStartupCheck' app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java\n\necho\necho '# 액추에이터의 검증 완료 표시가 보는 것'\ngrep -n '' \"$R\" | sed -n '36,41p'\ngrep -n 'privileges != null' \"$R\"\ngrep -n 'openInViewDisabled && runtimeRoleVerified' \"$R\"\n\necho\necho '# 그 표시를 고정하는 시험이 넣는 입력'\ngrep -n 'new DatabasePrivilegeReport' app-bootstrap/src/test/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformAutoConfigurationTest.java\necho '# 정책 쪽 단위 시험은 그 두 조합을 실제로 넣는다'\ngrep -n 'new DatabasePrivilegeReport' adapter/outbound/persistence-jpa/src/test/java/dev/caskeleton/adapter/outbound/persistence/security/DatabaseRolePolicyTest.java\n\necho\necho '# 안정 등급의 정의'\ngrep -n -B2 ' STABLE,' \"$C\"",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T04:41:19+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f022-stable.txt",
"svg": "evidence/rendered/a05-f022-stable.svg",
"rawSha256": "b720c6eb9a7f10aff1a99cdb2ccd425547700dfb409f8e8eddfbdfb01e302076",
"lines": 63,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f025-filequotaservice-commit",
"kind": "terminal",
"command": "S=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaFileQuotaService.java\nR=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverQuotaRepository.java\nG=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaQuotaCommitGateway.java\n\necho '# 서비스 javadoc 이 네 연산을 조건부라 부른다'\ngrep -n '' \"$S\" | sed -n '19,25p'\necho '# 저장소 인터페이스 javadoc 은 한 걸음 더 나간다'\ngrep -n '' \"$R\" | sed -n '13,18p'\necho -n '# 그런데 세 질의 시그니처에 version 파라미터: '\ngrep -A6 'int extend(\\|int commit(\\|int release(' \"$R\" | grep -c 'version'\n\necho\necho '# 연장 질의의 조건'\ngrep -n '' \"$R\" | sed -n '22,32p'\necho '# 확정 질의의 조건'\ngrep -n '' \"$R\" | sed -n '38,50p'\n\necho\necho '# 만료는 상태로 남지 않는다'\ngrep -rn 'EXPIRED' --include=*.java --exclude-dir=build . | grep -i quota | sed 's|^\\./||'\necho -n '# STALE_QUOTA_RESERVATION 을 enqueue 하는 main 코드: '\ngrep -rn 'STALE_QUOTA_RESERVATION' --include=*.java --exclude-dir=build . | grep -c 'enqueue'\n\necho\necho '# 이 포트의 네 메서드를 부르는 프로덕션 코드'\nfor m in reserve extend commit release; do\n printf '# quotaService.%-8s : %s\\n' \"$m()\" \"$(grep -rn \"quotaService\\.$m(\" --include=*.java --exclude-dir=build . | grep -c '/src/main/')\"\ndone\ngrep -rn 'quotaService\\.' --include=*.java --exclude-dir=build . | grep '/src/main/' | sed 's|^\\./||'\n\necho\necho '# 프로덕션 확정이 실제로 지나는 곳'\ngrep -rn 'quotaGateway\\.commit(' --include=*.java --exclude-dir=build . | grep '/main/' | sed 's|^\\./||'\necho '# 그 게이트웨이가 살아 있는 예약을 고르는 조회의 조건'\ngrep -n '' \"$R\" | sed -n '90,100p'\necho '# 살아 있는 예약이 없을 때'\ngrep -n -A12 'private void recordCommittedWithoutReservation' \"$G\"\necho '# 그 행을 만드는 생성자의 인자 순서 — 다섯째가 expiresAt'\ngrep -n -A9 'public QuotaReservationEntity(' $(find . -name QuotaReservationEntity.java -path '*/main/*')",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T04:56:12+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f025-filequotaservice-commit.txt",
"svg": "evidence/rendered/a05-f025-filequotaservice-commit.svg",
"rawSha256": "9bf3a7696a473f22e6109d26476cb422264b05d14c394fe914d9611ae2381a09",
"lines": 93,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f026-enqueue",
"kind": "terminal",
"command": "Q=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaRecoveryQueue.java\nR=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverRecoveryRepository.java\nV=adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V2__fileserver_recovery_and_staging_cleanup.sql\nE=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/entity/RecoveryItemEntity.java\nT=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringTransactionPort.java\n\necho '# javadoc 은 넣기를 upsert 로 정의한다'\ngrep -n '' \"$Q\" | sed -n '19,22p'\necho '# 저장소 javadoc 은 최신 이유가 이긴다고 적는다'\ngrep -n '' \"$R\" | sed -n '24,30p'\n\necho\necho '# 구현은 갱신하고, 0 이면 저장한다'\ngrep -n '' \"$Q\" | sed -n '42,50p'\necho '# 그 저장이 persist 로 갈지 merge 로 갈지 정하는 것'\necho -n '# 엔티티의 @Version 필드: '\ngrep -c '@Version' \"$E\"\necho -n '# Persistable 구현: '\ngrep -c 'Persistable' \"$E\"\n\necho\necho '# 데이터베이스가 파일당 열린 항목을 하나로 강제한다'\ngrep -n -B1 -A3 'CREATE UNIQUE INDEX uq_fs_recovery_open' \"$V\"\n\necho\necho '# 호출자가 실제로 받는 예외'\ngrep -n -A9 'private <T> T executeLegacy' \"$T\"\ngrep -n '\"23505\"' adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/failure/StandardSqlStateErrorMapping.java\n\necho\necho '# 이 넣기를 부르는 프로덕션 코드와 그 경계'\ngrep -rn 'recoveryQueue\\.enqueue(' --include=*.java --exclude-dir=build . | grep '/main/' | sed 's|^\\./||'\nsed -n '318,326p' application-core/src/main/java/dev/caskeleton/application/fileserver/upload/DefaultFinalizeUploadService.java\necho -n '# 조정 쪽 진입점을 부르는 프로덕션 코드: '\ngrep -rn 'reconcileRecoverable(' --include=*.java --exclude-dir=build . | grep '/main/' | grep -vc 'FileReconciliationService.java\\|DefaultFileReconciliationService.java'\necho '# 조정이 집어 올리는 대상의 조건'\ngrep -n 'STALE_AFTER' application-core/src/main/java/dev/caskeleton/application/fileserver/recovery/DefaultFileReconciliationService.java\n\necho\necho '# 같은 저장소가 승패를 데이터베이스에 물어보는 자리'\ngrep -rn 'ON CONFLICT' --include=*.java --include=*.sql --exclude-dir=build . | grep -i 'do nothing' | sed 's|^\\./||' | head -3",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T05:03:14+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f026-enqueue.txt",
"svg": "evidence/rendered/a05-f026-enqueue.svg",
"rawSha256": "e986fdaf7ef26fd4e383c697a66bed13250aea7be7c29c5c91abe51a1c8d39a6",
"lines": 69,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f027-maximum-attempts",
"kind": "terminal",
"command": "Q=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/JpaCleanupQueue.java\nR=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver/repository/FileserverCleanupRepository.java\nS=application-core/src/main/java/dev/caskeleton/application/fileserver/cleanup/DefaultCleanupService.java\n\necho '# 회수 메서드가 자기 javadoc 에 적어 둔 것'\ngrep -n '' \"$R\" | sed -n '108,131p'\necho -n '# 저장소 전체에서 attempt 를 비교하는 줄: '\ngrep -c 'attempt *[<>=]\\|attempt)' \"$R\"\necho '# 저장소가 attempt 에 하는 일 전부'\ngrep -n 'attempt' \"$R\"\n\necho\necho '# 예산을 끊는 곳은 코드베이스에 한 군데다'\ngrep -rn 'ABANDONED' --include=*.java --exclude-dir=build \\\n adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/fileserver \\\n application-core/src/main/java/dev/caskeleton/application/fileserver | sed 's|.*/||'\ngrep -n -A9 'public void markFailed' \"$Q\"\n\necho\necho '# 회수 대상을 고르는 조회 — 만료된 것만, 시도 한계 없음'\ngrep -n '' \"$R\" | sed -n '93,101p'\necho '# 다시 청구되는 조건 — 상태만이 아니라 다음 시도 시각도 본다'\ngrep -n '' \"$R\" | sed -n '16,23p'\n\necho\necho '# 회수 래퍼가 next_attempt_at 에 넣는 시각'\ngrep -n -A14 'public int reclaimExpiredClaims' \"$Q\"\necho '# 배치는 시각을 한 번 잡아 회수와 청구에 같이 쓴다'\ngrep -n -A10 'Instant now = clock.instant();' \"$S\" | head -12\ngrep -n 'LEASE = ' \"$Q\"\n\necho\necho '# 정상 실패 경로는 실제로 불린다'\ngrep -rn 'markFailed(item,' --include=*.java --exclude-dir=build \"$S\" | sed 's|.*/||'\ngrep -n -B2 -A3 'CLEANUP_ATTEMPT_FAILED' \"$S\" | head -8\n\necho\necho '# 도달성'\ngrep -n 'addFixedDelayTask' app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/fileserver/FileserverCleanupConfiguration.java\ngrep -n 'APP_FILESERVER_PLATFORM_ENABLED\\|APP_FILESERVER_PLATFORM_CLEANUP_ENABLED' app-bootstrap/src/main/resources/application.yml",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T05:43:41+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f027-maximum-attempts.txt",
"svg": "evidence/rendered/a05-f027-maximum-attempts.svg",
"rawSha256": "4e8bea35859c875b79a41738facde8b2ad2a1e0dec153f62eae36881a0cde0c1",
"lines": 113,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f029-for-update-skip-locked",
"kind": "terminal",
"command": "S=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/JdbcReconciliationJobStore.java\nC=adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/platform/RecipientClaimSql.java\nW=$(find . -name ReconciliationJobWorker.java -path '*/main/*')\nB=adapter/outbound/notification/src/main/java/dev/caskeleton/adapter/outbound/notification/platform/dispatch/NotificationBackgroundWorkers.java\nV=adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/notification-platform/V3__notification_platform_inbox_admin.sql\n\necho '# 청구 저장소의 javadoc'\ngrep -n '' \"$S\" | sed -n '25,26p'\necho '# 청구 문장과 그것을 실행하는 메서드'\ngrep -n '' \"$S\" | sed -n '44,52p'\ngrep -n -A7 'public List<ReconciliationJob> claimDue' \"$S\"\necho -n '# 그 클래스의 @Transactional / TransactionPort: '\ngrep -c 'Transactional\\|TransactionPort\\|inWrite' \"$S\"\n\necho\necho '# 같은 패키지의 전달 큐는 같은 잠금 구문에 UPDATE 를 붙인다'\ngrep -n '' \"$C\" | sed -n '27,50p'\n\necho\necho '# 작업자는 조회와 정산 사이에 제공자를 부른다'\ngrep -n -A16 'public int reconcileOnce' \"$W\"\necho -n '# 그 클래스의 @Transactional / TransactionPort: '\ngrep -c 'Transactional\\|TransactionPort\\|inWrite' \"$W\"\n\necho\necho '# 조정 작업 표의 열'\ngrep -n -A12 'CREATE TABLE notification_reconciliation_job' \"$V\"\necho -n '# 그 표 정의 안에서 owner/lease/status 를 포함한 줄: '\nsed -n '/CREATE TABLE notification_reconciliation_job/,/^);/p' \"$V\" | grep -ci 'owner\\|lease\\|status'\necho '# 이 표를 건드리는 마이그레이션 전부'\ngrep -rn 'notification_reconciliation_job' --include=*.sql --exclude-dir=build . | sed 's|.*/||'\n\necho\necho '# 도달 조건'\ngrep -n 'APP_NOTIFICATION_PLATFORM_ENABLED\\|APP_NOTIFICATION_PLATFORM_MODE' app-bootstrap/src/main/resources/application.yml\ngrep -n -A3 'Executors.newScheduledThreadPool' \"$B\"\ngrep -n 'schedule(\"reconciliation\"' \"$B\"",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T06:03:12+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f029-for-update-skip-locked.txt",
"svg": "evidence/rendered/a05-f029-for-update-skip-locked.svg",
"rawSha256": "843f1ad30c0d8bd6ae7b8887fa8798ee0bf9597f0e066974162aeba8ef8d8acb",
"lines": 97,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f033-jpa-flyway-migration-gate",
"kind": "terminal",
"command": "echo '# 이 태스크를 실행하는 워크플로 둘, 그리고 그 방아쇠'\nfor w in ci-quality-gates jpa-r2-evidence; do\n f=../.github/workflows/$w.yml\n echo \"# $w.yml\"\n sed -n '/^on:/,/^permissions:/p' \"$f\" | sed '$d;/^$/d' | sed 's/^/# /'\n grep -nE '^ jpa-[a-z0-9-]+:|verifyJpa' \"$f\" | sed 's/^/# /'\ndone\n\necho\necho '# 게이트는 매니페스트 태스크를 거쳐 활성 카드의 준비도 태스크에 의존한다'\nsed -n '534,538p' gradle/jpa-evidence.gradle\n\necho\necho '# 실제 태스크 그래프 (--dry-run 은 그래프의 모든 태스크를 SKIPPED 로 찍는다)'\n./gradlew :adapter:outbound:persistence-jpa:verifyJpaCandidateEvidence --dry-run --no-daemon 2>/dev/null \\\n | grep -E 'postgresqlMigrationIntegrationTest|generateJpaEvidenceManifests|verifyJpaCandidateEvidence'\n\necho\necho '# R2 집계 게이트가 매니페스트에서 이름으로 요구하는 카드 일곱'\nsed -n '888,896p' gradle/jpa-evidence.gradle",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T06:31:11+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f033-jpa-flyway-migration-gate.txt",
"svg": "evidence/rendered/a05-f033-jpa-flyway-migration-gate.svg",
"rawSha256": "20e811e45c1d6a6d25c668912756025a9711f2e6f7f1553e3854b4e95558e9bf",
"lines": 37,
"redaction": "none — 빌드 구성 조회, 자격증명 없음"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f033-jpa-flyway-migration-run",
"kind": "terminal",
"command": "mkdir -p /tmp/probe\nset -e\necho '# 카드의 생산자 태스크를 재실행 강제로 돌린다'\ngit status --short | head -3\necho \"작업 트리 변경: $(git status --short | wc -l) 줄\"\ntimeout 900 ./gradlew :adapter:outbound:persistence-jpa:postgresqlMigrationIntegrationTest \\\n --rerun-tasks --console=plain 2>&1 \\\n | grep -E 'FAILED|tests completed|BUILD|AssertionFailedError' | head -10 || true\n\necho\necho '# 두 실패의 단언 메시지'\ncat > /tmp/probe/failmsg.py <<'PYEOF'\n\"\"\"실패한 단언 메시지만 뽑는다. 메서드 이름은 Gradle 출력이 이미 말한다.\"\"\"\nimport glob, html, re\nfor path in sorted(glob.glob(\n 'adapter/outbound/persistence-jpa/build/test-results/'\n 'postgresqlMigrationIntegrationTest/TEST-*.xml')):\n for failure in re.findall(r'<failure[^>]*>(.*?)</failure>',\n open(path, encoding='utf-8').read(), re.S):\n print(html.unescape(failure).split('\\tat ')[0].strip())\n print()\nPYEOF\npython3 /tmp/probe/failmsg.py",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T06:09:54+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f033-jpa-flyway-migration-run.txt",
"svg": "evidence/rendered/a05-f033-jpa-flyway-migration-run.svg",
"rawSha256": "dea9bf696f6b245e6a11ab0d1412be0971fc994a5da3c9f4a95be2ba1df5c8f3",
"lines": 27,
"redaction": "none — 빌드 출력, 자격증명 없음"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a05-f033-jpa-flyway-migration",
"kind": "terminal",
"command": "T=adapter/outbound/persistence-jpa/src/postgresqlIntegrationTest/java/dev/caskeleton/adapter/outbound/persistence/readiness/PostgreSqlMigrationIntegrationTest.java\n\necho '# 이 카드의 상태와 생산자 태스크'\ngrep -n -A6 '\"jpa-flyway-migration\": {' config/jpa/readiness-cards.yaml\n\necho\necho '# 이 카드를 선행 조건으로 두는 카드 전수'\npython3 - <<'PY'\nimport json\ncards = json.load(open('config/jpa/readiness-cards.yaml'))['cards']\nfor name, card in cards.items():\n if 'jpa-flyway-migration' in card.get('prerequisites', []):\n print('# ', name.ljust(34), card['state'])\nprint('# 집계 게이트 jpa-primary-foundation 의 선행 조건:')\nfor p in cards['jpa-primary-foundation']['prerequisites']:\n print('# ', p)\nPY\n\necho\necho '# 집계 게이트는 카드 id 를 이름으로 요구한다'\ngrep -n \"jpa-flyway-migration'\" gradle/jpa-evidence.gradle\n\necho\necho '# 그 태스크가 고정한 적용 집합 셋'\ngrep -n 'containsExactly(\"1\", \"3\", \"4\", \"5\", \"6\")\\|containsExactly(\"0\", \"1\")\\|containsExactly(\"1\")' \"$T\"\n\necho\necho '# 두 스트림의 실제 마이그레이션'\nM=adapter/outbound/persistence-jpa/src/main/resources/db/migration\necho -n '# postgresql : '\nls \"$M/postgresql\" | sed 's/__.*//' | sort -V | tr '\\n' ' '; echo\necho -n '# jpa/core : '\nls \"$M/jpa/core\" | sed 's/__.*//' | sort -V | tr '\\n' ' '; echo\n\necho\necho '# 단언이 마지막으로 갱신된 시점과 그 뒤 들어온 마이그레이션'\nfor f in \"$T\" \"$M/jpa/core/V2__\"* \"$M/postgresql/V9__\"* \"$M/postgresql/V10__\"* \"$M/postgresql/V11__\"* \"$M/postgresql/V12__\"*; do\n echo \"# $(git log -1 --format='%ad %h' --date=short -- \"$f\") $(basename \"$f\")\"\ndone\n\necho\necho '# 같은 소스 세트가 이 실패 유형을 주석으로 적어 두었다'\nL=$(find . -name PostgreSqlOptionalStreamLifecycle.java -not -path '*/build/*')\nsed -n '140,144p' \"$L\"",
"cwd": "/shared/codebase/clean-architecture-backend-template/src",
"exitCode": 0,
"executedAt": "2026-09-02T06:27:08+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a05-f033-jpa-flyway-migration.txt",
"svg": "evidence/rendered/a05-f033-jpa-flyway-migration.svg",
"rawSha256": "e9e95ae3633c78c232105e5f3e5f233eff6dac58a8df60539376cf431a32d349",
"lines": 56,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a06-f003-change-streams-true-shipped",
"kind": "terminal",
"command": "S=src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo\nT=src/adapter/outbound/persistence-mongo/src/test/java/dev/caskeleton/adapter/outbound/mongo\nA=$S/autoconfigure/MongoPlatformAutoConfiguration.java\n\necho '# source 빈이 놓인 자리의 조건 넷'\nsed -n '41,49p' \"$A\"\necho ' ...'\nsed -n '187,190p' \"$A\"\necho ' ...'\nsed -n '207,209p' \"$A\"\n\necho\necho '# 그 빈이 커서를 연다'\nsed -n '45,49p' $S/changestream/consumer/SpringReactiveChangeStreamSource.java\n\necho\necho '# 소비자 빈이 요구하는 조건 다섯'\nsed -n '227,233p' \"$A\"\n\necho\necho '# 그 다섯을 구현하는 클래스 전수'\ngrep -rn 'implements MongoResumeCheckpointStore\\|implements MongoResumeTokenCodec\\|implements MongoChangeProjector\\|implements MongoChangeDeduplicationStore' \\\n --include=*.java src | grep -v '/build/' | sed 's#'\"$T\"'/#test …/#' | sed 's#:.*class # #'\n\necho\necho '# 저장소 자신의 배선 시험이 고정한 네 상태'\ngrep -n 'void a\\|void the\\|void with' $T/autoconfigure/MongoReactiveWiringTest.java\n\necho\necho '# 이 자동 구성 파일에서 changeStreams 가 나오는 곳'\ngrep -n 'changeStreams' \"$A\"",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T06:56:07+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a06-f003-change-streams-true-shipped.txt",
"svg": "evidence/rendered/a06-f003-change-streams-true-shipped.svg",
"rawSha256": "48b821fbb8f7a94ade9721409f032e9ea734fecf4a8f57d39ad75ef05fe41635",
"lines": 56,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a06-f003-change-streams-true",
"kind": "terminal",
"command": "S=src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo\n\necho '# 컴팩트 생성자가 네 입력을 세 방식으로 처리한다'\nsed -n '43,66p' $S/autoconfigure/MongoPlatformSettings.java\n\necho\necho '# 시작 검증기의 변경 스트림 분기'\ngrep -n -A2 'changeStreamsEnabled &&' $S/autoconfigure/MongoStartupValidator.java\n\necho\necho '# 그 분기가 내는 문장이 저장소에 나오는 곳 전수'\ngrep -rn 'change streams are enabled' --include=*.java src | sed 's#'\"$S\"'/#…/#'\n\necho\necho '# changeStreams 라는 이름이 나오는 곳 전수 (다른 타입의 동명 정적 팩터리 포함)'\ngrep -rn '\\.changeStreams()' --include=*.java src | grep -v '/build/' | sed 's#src/adapter/outbound/persistence-mongo/src/##'\n\necho\necho '# MongoStartupValidator 를 만드는 곳 전수'\ngrep -rn 'new MongoStartupValidator' --include=*.java src | grep -v '/build/' | sed 's#src/adapter/outbound/persistence-mongo/src/##' ",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T06:55:29+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a06-f003-change-streams-true.txt",
"svg": "evidence/rendered/a06-f003-change-streams-true.svg",
"rawSha256": "0e05a5410e9bbdc5ef5c84589c782e708914e4cf9baf1845aca8c7424001cc1a",
"lines": 43,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a06-f011-mongoregexpolicy-forbidden",
"kind": "terminal",
"command": "Q=src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/query\n\necho '# 두 팩토리가 만드는 값'\nsed -n '41,44p;90,93p' $Q/MongoRegexPolicy.java\n\necho\necho '# validate 의 네 검사'\nsed -n '100,121p' $Q/MongoRegexPolicy.java\n\necho\necho '# 길이 검사가 쓰는 비교'\nsed -n '136,140p' $Q/MongoRegexPolicy.java\n\necho\necho '# forbidden() 을 부르는 곳 전수'\ngrep -rn 'MongoRegexPolicy\\.forbidden()' --include=*.java src | grep -v '/build/' || echo ' (선언 자리 말고는 없음)'\n\necho\necho '# withRegexPolicy 를 부르는 곳 전수'\ngrep -rn 'withRegexPolicy' --include=*.java src | grep -v '/build/' | sed 's#src/adapter/outbound/persistence-mongo/src/##'\n\necho\necho '# MongoQueryPolicy 를 만드는 곳 전수'\ngrep -rn 'MongoQueryPolicy\\.allowing' --include=*.java src | grep -v '/build/' | sed 's#src/adapter/outbound/persistence-mongo/src/##'\n\necho\necho '# 자동 구성이 query 패키지에서 만드는 빈'\ngrep -n -B1 'public MongoBudgetEnforcer mongoBudgetEnforcer' $(dirname $Q)/autoconfigure/MongoPlatformAutoConfiguration.java",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T07:02:08+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a06-f011-mongoregexpolicy-forbidden.txt",
"svg": "evidence/rendered/a06-f011-mongoregexpolicy-forbidden.svg",
"rawSha256": "70c2da3a2e095b574234a33abd789d5940888206628d4234cceb0c68e20707c5",
"lines": 58,
"redaction": "none — 분석 문서 발췌"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a06-f013-recordapplied-around",
"kind": "terminal",
"command": "M=src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/migration\nT=src/adapter/outbound/persistence-mongo/src/test/java/dev/caskeleton/adapter/outbound/mongo/migration\n\necho '# 조립된 경로에서 갱신과 원장 기록 사이에 있는 것'\nsed -n '139,142p' $M/MongoMigrationRunner.java\nsed -n '144,148p' $M/MongoMigrationRunner.java\necho ' ...'\nsed -n '154,158p' $M/MongoMigrationRunner.java\n\necho\necho '# 완료를 만드는 팩토리는 체크포인트를 남기지 않는다'\nsed -n '24,34p' $M/MongoMigrationResult.java\n\necho\necho '# 그 갱신이 던지는 것'\nsed -n '160,165p' $M/MongoCollectionMigrationLock.java\necho '# 같은 인터페이스의 다른 잠금 구현이 같은 상황에 던지는 것'\nsed -n '57,63p' $M/flamingock/FlamingockLockAdapter.java\n\necho\necho '# 다른 원장 구현은 펜스 인자를 쓰지 않는다'\nsed -n '61,65p' $M/flamingock/FlamingockLedgerAdapter.java\n\necho\necho '# recordApplied 를 부르는 시험 전수'\ngrep -rn 'ledger.recordApplied(' --include=*.java $T | sed \"s#$T/##\"\necho '# 그중 실서버에서 두 번 부르는 시험이 단언하는 것'\nsed -n '230,234p' $T/MongoMigrationLaneTest.java\n\necho\necho '# 고유 색인을 만드는 메서드를 부르는 곳 전수'\ngrep -rln 'ensureIndexes()' --include=*.java src | sed 's#src/adapter/outbound/persistence-mongo/src/##' ",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T07:40:13+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a06-f013-recordapplied-around.txt",
"svg": "evidence/rendered/a06-f013-recordapplied-around.svg",
"rawSha256": "784b9bf99d0358d05c7bf29a0487f4de049119cdb887d688b4185b7f6eef73c1",
"lines": 69,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a06-f013-recordapplied",
"kind": "terminal",
"command": "M=src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/migration\n\necho '# 인터페이스가 적는 계약'\nsed -n '18,29p' $M/MongoMigrationLedger.java\n\necho\necho '# 구현이 하는 일'\nsed -n '93,99p' $M/MongoCollectionMigrationLedger.java\n\necho\necho '# 그 구현이 부르는 검사와 그 검사의 javadoc'\nsed -n '171,182p' $M/MongoCollectionMigrationLedger.java\n\necho\necho '# 같은 파일의 saveCheckpoint 는 저장된 펜스를 조건으로 건다'\nsed -n '145,153p' $M/MongoCollectionMigrationLedger.java\n\necho\necho '# 그리고 중복 키를 잡아 번역한다. 그렇게 된 이유가 주석에 있다'\nsed -n '141,144p' $M/MongoCollectionMigrationLedger.java\nsed -n '157,167p' $M/MongoCollectionMigrationLedger.java",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T07:40:13+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a06-f013-recordapplied.txt",
"svg": "evidence/rendered/a06-f013-recordapplied.svg",
"rawSha256": "a619cb222bd01624d74178533274e0e07e97739d7a2c46fc277d07a3f928947d",
"lines": 64,
"redaction": "none — 출력에 자격증명이 없다"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a06-f016-flamingock",
"kind": "terminal",
"command": "M=src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/migration\n\necho '# 펜스 메서드가 돌려주는 값과 그 javadoc'\nsed -n '43,54p' $M/flamingock/FlamingockLockAdapter.java\n\necho\necho '# 실행자의 진입점이 지나는 세 관문'\nsed -n '73,93p' $M/MongoMigrationRunner.java\n\necho\necho '# 그 파일에서 리스의 펜스를 읽는 곳 전수'\ngrep -n 'lock.fence()\\|migrations.forEach\\|return migrations.stream' $M/MongoMigrationRunner.java\n\necho\necho '# 154행이 넘기는 곳과 157행이 넘기는 곳이 그 값을 다루는 방식'\ngrep -n 'requireCurrentFence' $M/MongoCollectionMigrationLedger.java\nsed -n '178,181p' $M/MongoCollectionMigrationLedger.java\n\necho\necho '# 마이그레이션이 재개 가능한지 실행 전에 알 수 있는가'\ngrep -n ' [A-Za-z].*(' $M/MongoMigration.java | grep -v '^\\s*//'\n\necho\necho '# 이 잠금 어댑터가 나오는 곳 전수'\ngrep -rn 'FlamingockLockAdapter' --include=*.java src | grep -v '/build/' \\\n | sed 's#src/adapter/outbound/persistence-mongo/src/##'\n\necho\necho '# 실행자와 원장과 잠금을 프로덕션에서 참조하는 곳'\ngrep -rn 'MongoMigrationRunner\\|MongoMigrationLedger\\|MongoMigrationLock' --include=*.java src \\\n | grep -v '/build/' | grep '/main/' | grep -v \"/migration/\" || echo ' (없음)'\n\necho\necho '# 마이그레이션을 적용하는 진입점 전수'\ngrep -rn 'List<MongoMigrationResult>' --include=*.java src | grep -v '/build/' \\\n | sed 's#src/adapter/outbound/persistence-mongo/src/##' ",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T07:56:40+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a06-f016-flamingock.txt",
"svg": "evidence/rendered/a06-f016-flamingock.svg",
"rawSha256": "f571f978d909ddf880b77df5c2a15ea9c2a5ec3eac20ad3cf18d7ddb9558addd",
"lines": 73,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a06-f018-changestreams-false",
"kind": "terminal",
"command": "S=src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo\nA=$S/autoconfigure/MongoPlatformAutoConfiguration.java\n\necho '# 검증기의 인접한 두 분기'\nsed -n '97,111p' $S/autoconfigure/MongoStartupValidator.java\n\necho\necho '# 두 분기의 좌항이 오는 곳'\nsed -n '361,364p' \"$A\"\n\necho\necho '# 그 검사가 열리는 조건'\nsed -n '327,332p' \"$A\"\necho ' ...'\nsed -n '346,352p' \"$A\"\n\necho\necho '# changeStreams 는 컴팩트 생성자에서 고정된다'\nsed -n '48,55p' $S/autoconfigure/MongoPlatformSettings.java\necho -n '# 자동 구성 파일에서 changeStreams 가 나오는 줄 : '\ngrep -c 'changeStreams' \"$A\"\n\necho\necho '# 소비자 빈에 붙은 조건'\nsed -n '225,234p' \"$A\"\n\necho\necho '# 스트림을 여는 시점의 실패를 복구 정책이 분류하는 방식'\nsed -n '20,21p' $S/changestream/recovery/MongoChangeStreamRecoveryPolicy.java\nsed -n '53,61p' $S/changestream/recovery/MongoChangeStreamRecoveryPolicy.java\nsed -n '64,65p' $S/changestream/recovery/MongoChangeStreamRecoveryPolicy.java\n\necho\necho '# 그 런북의 증상 절 전체'\nsed -n '/^## Symptoms/,/^## /p' docs/mongodb/runbooks/failover.md | head -12\necho -n '# 그 런북 전체에서 standalone·oplog·40573 이 나오는 줄 : '\ngrep -ci 'standalone\\|oplog\\|40573' docs/mongodb/runbooks/failover.md",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 1,
"executedAt": "2026-09-02T08:20:08+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a06-f018-changestreams-false.txt",
"svg": "evidence/rendered/a06-f018-changestreams-false.svg",
"rawSha256": "6ab0a35bf1c365d22f5821e341a0064e5bc91008bf698fc7f88b5224fec3501d",
"lines": 91,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a06-f020-tls-stable",
"kind": "terminal",
"command": "S=src/adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo\nT=src/adapter/outbound/persistence-mongo/src/test/java/dev/caskeleton/adapter/outbound/mongo\n\necho '# 팩토리의 javadoc 이 자기가 무엇을 고치려고 만들어졌는지 적는다'\nsed -n '17,24p' $S/client/MongoClientSettingsFactory.java\n\necho\necho '# 저장소 전체에서 그 타입과 자격증명 해석기를 언급하는 곳'\ngrep -rn 'MongoClientSettingsFactory\\|MongoCredentialResolver' \\\n --include=*.java --include=*.md --include=*.txt --include=*.yml . \\\n --exclude-dir=build --exclude-dir=.git \\\n | grep -v '/mongo/client/MongoClientSettingsFactory.java\\|/mongo/security/MongoCredentialResolver.java' \\\n | grep -v 'MongoClientSettingsFactoryTest\\|MongoProductionSecurityContractTest' \\\n | sed 's#^\\./src/adapter/outbound/persistence-mongo/##'\n\necho\necho '# 프로파일 검증이 production 에 요구하는 것'\nsed -n '140,145p' $S/autoconfigure/MongoProfileProperties.java\n\necho\necho '# 그 검증이 켜지려면 무엇이 있어야 하나'\nsed -n '391,403p' $S/autoconfigure/MongoPlatformAutoConfiguration.java\n\necho\necho '# 출하 설정이 이 모듈 아래 노출하는 키'\ngrep -n -A8 '^ persistence-mongo:' src/app-bootstrap/src/main/resources/application.yml | grep -E '^[0-9]+[-:] +[a-z-]+:' \necho -n '# platform.profiles 를 넣는 곳 전수 : '\ngrep -rc 'persistence-mongo.platform.profiles' --include=*.yml --include=*.properties --include=*.java . \\\n --exclude-dir=build --exclude-dir=.git 2>/dev/null | awk -F: '{s+=$2} END {print s+0}'\n\necho\necho '# 이 리프가 이미 쓰는 배선 수단'\ngrep -n -B4 'public MongoClientSettingsBuilderCustomizer' $S/autoconfigure/MongoDriverObservabilityAutoConfiguration.java\necho -n '# 프로덕션에서 그 수단을 등록하는 곳 전수 : '\ngrep -rn 'MongoClientSettingsBuilderCustomizer' --include=*.java src | grep -v '/build/' | grep '/main/' | grep -c '@Bean\\|public MongoClientSettingsBuilderCustomizer'\n\necho\necho '# 이 경계를 보는 시험이 있는가'\nsed -n '15,24p' $T/client/MongoClientSettingsFactoryTest.java\necho '# TLS 레인 시험이 쓰는 클라이언트'\ngrep -rn -B2 'applyToSslSettings' --include=*.java $T | sed \"s#$T/##\"",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T08:42:15+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a06-f020-tls-stable.txt",
"svg": "evidence/rendered/a06-f020-tls-stable.svg",
"rawSha256": "fb95534f00e10b673539406db06d0313e119365c332ff488747ba555fb856ad2",
"lines": 69,
"redaction": "SVG 렌더러가 secret: 로 시작하는 자격증명 참조 문자열을 가렸다 — 원문은 저장소의 코드/문서 리터럴이고 실제 비밀값이 아니다"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a07-f001-uuidcodec-elsewhere",
"kind": "terminal",
"command": "echo '# 문자열에서 UUID 를 만드는 리프 밖 프로덕션'\necho -n '# 파일 : '\ngrep -rl 'UUID.fromString' --include=*.java src --exclude-dir=build | grep '/main/' \\\n | grep -v '^src/adapter/outbound/identifier/' | wc -l\necho -n '# 호출 줄 : '\ngrep -rn 'UUID.fromString' --include=*.java src --exclude-dir=build | grep '/main/' \\\n | grep -v '^src/adapter/outbound/identifier/' | wc -l\ngrep -rl 'UUID.fromString' --include=*.java src --exclude-dir=build | grep '/main/' \\\n | grep -v '^src/adapter/outbound/identifier/' | sed 's#^src/# #'\n\necho\necho '# 그중 하나는 표준 파서의 관대함을 알고 먼저 거른다'\nsed -n '19,21p;32,34p' src/adapter/inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/scalar/UuidScalar.java\n\necho\necho '# 문서가 toUuid 의 목적으로 든 컬럼 변환이 실제로 잇는 구간'\necho -n '# @JdbcTypeCode(SqlTypes.UUID) 를 단 프로덕션 파일 : '\ngrep -rl '@JdbcTypeCode(SqlTypes.UUID)' --include=*.java src --exclude-dir=build | grep -c '/main/'\necho -n '# 그 애너테이션 개수 : '\ngrep -rn '@JdbcTypeCode(SqlTypes.UUID)' --include=*.java src --exclude-dir=build | grep -c '/main/'\necho -n '# 그 애너테이션이 붙은 필드의 타입 : '\npython3 - <<'PY'\nimport collections, pathlib, re\nkinds = collections.Counter()\nfor path in pathlib.Path('src').rglob('*.java'):\n if 'build' in path.parts or '/main/' not in str(path):\n continue\n lines = path.read_text(encoding='utf-8').splitlines()\n for index, line in enumerate(lines):\n if '@JdbcTypeCode(SqlTypes.UUID)' not in line:\n continue\n for following in lines[index + 1:index + 6]:\n found = re.search(r'(?:private|protected|public)\\s+([A-Za-z_][\\w.<>]*)\\s+\\w+\\s*;', following)\n if found:\n kinds[found.group(1)] += 1\n break\nprint(dict(kinds))\nPY\n\necho\necho '# 그래서 문자열 구간은 매퍼가 따로 한다'\ngrep -n -B2 'UUID.fromString' \\\n src/sample-portfolio/src/main/java/dev/caskeleton/sample/portfolio/adapter/outbound/persistence/mapper/WorkLogPersistenceMapper.java | head -6\n\necho\necho '# 리프에 의존해도 되는 모듈'\npython3 - <<'PY'\nimport json\nd = json.load(open('src/config/architecture/modules.json'))\nmodules = d['modules']\nallowed = [m['id'] for m in modules\n if 'adapter-outbound-identifier' in m.get('allowed_dependencies', [])]\nprint('# 전체', len(modules), '중', len(allowed), ':', ', '.join(allowed))\nPY\n\necho '# 예제 애플리케이션이 코덱을 쓰지 않는 이유를 적어 둔 자리'\nsed -n '333,335p' src/sample-portfolio/README.md",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T09:02:43+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a07-f001-uuidcodec-elsewhere.txt",
"svg": "evidence/rendered/a07-f001-uuidcodec-elsewhere.svg",
"rawSha256": "70506315cde67fae759ca30de705345317982fc04bf8d2829b42cca053c98b83",
"lines": 43,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a07-f001-uuidcodec",
"kind": "terminal",
"command": "L=src/adapter/outbound/identifier\n\necho '# 모듈을 여기 둔 논거'\nsed -n '25,27p' $L/CLAUDE.md\n\necho\necho '# 리프의 공개 타입 셋'\nls $L/src/main/java/dev/caskeleton/adapter/outbound/identifier/ | grep -v package-info\n\necho\necho '# 각 타입 이름이 리프 밖에 나오는 곳 전수 (확장자 제한 없음)'\nfor t in HmacUserPrincipalPseudonymizer RandomUploadIdentifierFactory UuidCodec; do\n echo \"# $t\"\n grep -rn \"$t\" . --exclude-dir=build --exclude-dir=.git --exclude-dir=.gradle \\\n | grep -v '^\\./src/adapter/outbound/identifier/' | sed 's#^\\./# #'\ndone\n\necho\necho '# UuidCodec 의 메서드를 부르는 줄 전수'\ngrep -rn 'UuidCodec\\.' . --exclude-dir=build --exclude-dir=.git --exclude-dir=.gradle \\\n | sed 's#^\\./src/# #' ",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T09:02:26+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a07-f001-uuidcodec.txt",
"svg": "evidence/rendered/a07-f001-uuidcodec.svg",
"rawSha256": "5163ea9ea7b631a4be9df20f2af3fb619bb7d1e27aaabb7994050aafde88bf48",
"lines": 40,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a07-f002-normalize-probe",
"kind": "terminal",
"command": "set -e\nD=$(mktemp -d)\ntrap 'rm -rf \"$D\"' EXIT\n\nCP=src/adapter/outbound/identifier/build/classes/java/main\nSRC=src/adapter/outbound/identifier/src/main/java/dev/caskeleton/adapter/outbound/identifier/UuidCodec.java\necho \"# 클래스 산출물이 소스보다 새로운가 : $([ \"$CP/dev/caskeleton/adapter/outbound/identifier/UuidCodec.class\" -nt \"$SRC\" ] && echo yes || echo no)\"\n\ncat > \"$D/NormalizeProbe.java\" <<'JAVA'\nimport dev.caskeleton.adapter.outbound.identifier.UuidCodec;\nimport java.util.LinkedHashMap;\nimport java.util.List;\nimport java.util.Map;\n\npublic final class NormalizeProbe {\n\n private static final String CANONICAL = \"0190bd6e-7c3e-7abc-8def-0123456789ab\";\n\n public static void main(String[] args) {\n System.out.println(\"java.version = \" + System.getProperty(\"java.version\"));\n System.out.println();\n\n System.out.println(\"[계약이 말하는 두 갈래]\");\n show(CANONICAL);\n show(\"0190BD6E-7C3E-7ABC-8DEF-0123456789AB\");\n show(\"not-a-uuid\");\n show(\"0190bd6e7c3e7abc8def0123456789ab\");\n System.out.println();\n\n System.out.println(\"[정규형에서 한 글자를 지운다]\");\n for (String input : List.of(\n \"0190bd6e-7c3e-7abc-8def-123456789ab\",\n \"0190bd6e-7c3e-7abc-8def-0123456789a\",\n \"0190bd6e-7c3e-7abc-8def-012345678ab\",\n \"0190bd6e7c3e-7abc-8def-0123456789ab\")) {\n show(input);\n System.out.println(\" 원본과 같은가 : \" + CANONICAL.equals(safe(input)));\n }\n System.out.println();\n\n System.out.println(\"[길이가 36이어도 대시 위치가 다르면 같은 경로로 떨어진다]\");\n show(\"0000001-00001-0001-0001-000000000001\");\n System.out.println();\n\n System.out.println(\"[그룹이 규정 자릿수를 넘으면 상위 비트를 버린다]\");\n show(\"100000001-1-1-1-1\");\n show(\"1-1-1-1-1000000000001\");\n System.out.println();\n\n System.out.println(\"[십육진이 아닌 것도 통과한다]\");\n show(\"+1-1-1-1-1\");\n show(\"١-1-1-1-1\");\n System.out.println();\n\n System.out.println(\"[서로 다른 입력이 같은 식별자가 된다]\");\n Map<String, String> byOutput = new LinkedHashMap<>();\n for (String input : List.of(\"1-1-1-1-1\", \"01-01-01-01-01\", \"+1-1-1-1-1\",\n \"00000001-0001-0001-0001-000000000001\", \"1-1-1-1-2\")) {\n byOutput.merge(safe(input), \"\\\"\" + input + \"\\\"\", (a, b) -> a + \", \" + b);\n }\n byOutput.forEach((output, inputs) -> System.out.println(\" \" + output + \" <- \" + inputs));\n System.out.println();\n\n System.out.println(\"[같은 파서를 쓰는 형제 메서드]\");\n System.out.println(\" toUuid(\\\"1-1-1-1-1\\\") = \" + UuidCodec.toUuid(\"1-1-1-1-1\"));\n }\n\n private static String safe(String input) {\n try {\n return UuidCodec.normalize(input);\n } catch (RuntimeException rejected) {\n return rejected.getClass().getSimpleName();\n }\n }\n\n private static void show(String input) {\n String pad = \" \".repeat(Math.max(1, 40 - input.length()));\n try {\n System.out.println(\" normalize(\\\"\" + input + \"\\\")\" + pad\n + \"-> \\\"\" + UuidCodec.normalize(input) + \"\\\"\");\n } catch (RuntimeException rejected) {\n System.out.println(\" normalize(\\\"\" + input + \"\\\")\" + pad\n + \"-> \" + rejected.getClass().getSimpleName() + \": \" + rejected.getMessage());\n }\n }\n}\nJAVA\n\njavac -encoding UTF-8 -cp \"$CP\" -d \"$D\" \"$D/NormalizeProbe.java\"\njava -Dstdout.encoding=UTF-8 -cp \"$D:$CP\" NormalizeProbe",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T09:21:32+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a07-f002-normalize-probe.txt",
"svg": "evidence/rendered/a07-f002-normalize-probe.svg",
"rawSha256": "1a3ef436d76fef9a0cd2465f3bd87fe0984a83d45b6216af5ce6bddaaa52d826",
"lines": 36,
"redaction": "none — 밀폐 탐침 실행 출력, 자격증명 없음"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a07-f002-normalize",
"kind": "terminal",
"command": "L=src/adapter/outbound/identifier\nP=$L/src/main/java/dev/caskeleton/adapter/outbound/identifier/UuidCodec.java\n\necho '# 계약이 적힌 두 곳'\nsed -n '10,21p' \"$P\"\necho '# README'\nsed -n '21,22p' $L/README.md\n\necho\necho '# 같은 파서를 쓰는 형제 메서드'\nsed -n '23,25p' \"$P\"\n\necho\necho '# 명세의 거부 케이스'\ngrep -n -A6 '형식이 잘못된' $L/src/test/groovy/dev/caskeleton/adapter/outbound/identifier/UuidCodecSpec.groovy\necho -n '# 명세 전체의 거부 단언 수 : '\ngrep -c 'thrown(' $L/src/test/groovy/dev/caskeleton/adapter/outbound/identifier/UuidCodecSpec.groovy\n\necho\necho '# 같은 저장소의 UUID 스칼라는 그 계약을 코드로 지킨다'\nG=src/adapter/inbound/graphql/src/main/java/dev/caskeleton/adapter/inbound/graphql/scalar/UuidScalar.java\nsed -n '32,34p' \"$G\"\nsed -n '55,60p' \"$G\"\necho '# 그 관문을 지나는 곳'\ngrep -n 'parse(' \"$G\" | sed 's/^/ /' ",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T09:21:18+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a07-f002-normalize.txt",
"svg": "evidence/rendered/a07-f002-normalize.svg",
"rawSha256": "375365120e3a6dc597a2fee9e09b475ae7161d20eb7895f6b618802426cc7732",
"lines": 47,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a07-f004-claude",
"kind": "terminal",
"command": "L=src/adapter/outbound/identifier\n\necho '# 지침 문서가 적은 의존성'\nsed -n '29,34p' $L/CLAUDE.md\n\necho\necho '# 빌드 파일의 선언 블록'\nsed -n '/^dependencies {/,/^}/p' $L/build.gradle\n\necho\necho '# 뒤 문장이 든 두 이름을 각각 빌드 파일과 잠금 파일에서 찾는다'\necho -n '# 빌드 파일의 domain-core 언급(모든 표기) : '\ngrep -c 'domain-core\\|domainCore' $L/build.gradle\necho -n '# 잠금 파일 줄 수 / uuid-creator·f4b6a3 : '\necho \"$(wc -l < $L/gradle.lockfile) / $(grep -c 'uuid-creator\\|f4b6a3' $L/gradle.lockfile)\"\necho -n '# 그 잠금 파일이 덮는 configuration : '\ngrep -v '^#' $L/gradle.lockfile | sed 's/.*=//' | tr ',' '\\n' | sort -u | tr '\\n' ' '; echo\necho -n '# 프로덕션 클래스패스의 잠금 상태 : '\ngrep '^empty=' $L/gradle.lockfile\ngrep -n 'lockAllConfigurations()\\|lockMode = LockMode.STRICT' src/build.gradle | sed 's/^/ /'\n\necho\necho '# 그 라이브러리를 선언하는 곳'\ngrep -rn 'uuid-creator' --include=build.gradle src | sed 's#^src/# #'\n\necho\necho '# 앞 문장의 셋과 강제되는 허용 목록'\npython3 - <<'PY'\nimport json\nregistry = json.load(open('src/config/architecture/modules.json'))\nentry = next(m for m in registry['modules'] if m['id'] == 'adapter-outbound-identifier')\nallowed = entry['allowed_dependencies']\nprint('# 레지스트리 allowed_dependencies :', allowed)\nprint('# 문서가 든 셋 중 목록에 없는 것 :',\n sorted({'application-core', 'domain-core', 'shared-contract'} - set(allowed)))\nPY\n\necho '# 그 목록이 강제되는 사슬'\ngrep -n 'allowedProjectDependencies = registry.modules' src/build.gradle | sed 's/^/ /'\nsed -n '1466,1472p' src/build.gradle\ngrep -n \"dependsOn rootProject.tasks.named('verifyCleanArchitectureDependencies')\" src/build.gradle | sed 's/^/ /' ",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T09:37:40+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a07-f004-claude.txt",
"svg": "evidence/rendered/a07-f004-claude.svg",
"rawSha256": "a9a6e636ce6cde2d5887c4cbea301325fdedb21535c910ef5aff30e79cb2c01a",
"lines": 39,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a07-f006-claude",
"kind": "terminal",
"command": "L=src/adapter/outbound/identifier\nA=src/app-bootstrap/src/test/java/dev/caskeleton/bootstrap/architecture/CleanArchitectureTest.java\n\necho '# 지침 문서의 금지 절과 첫 항목에 붙은 근거 둘'\nsed -n '35,41p' $L/CLAUDE.md\n\necho\necho '# 아키텍처 규칙 쪽 — 선언과 그것이 막는 이름들'\nsed -n '1288,1305p' $A\n\necho\necho '# 그 규칙이 실제로 도는가'\ngrep -n '@AnalyzeClasses' $A | sed 's/^/ /'\ngrep -n \"implementation project(':adapter:outbound:identifier')\" src/app-bootstrap/build.gradle | sed 's/^/ /'\ngrep -n './gradlew check' .github/workflows/ci-quality-gates.yml | head -1 | sed 's/^/ /'\n\necho\necho '# 그 리프의 프로덕션 클래스패스'\ngrep -n '^empty=' $L/gradle.lockfile | sed 's/^/ /'\n\necho\necho '# 훅 쪽 — 그 파일이 있다는 자리'\necho -n '# git ls-files .claude : '\ngit ls-files .claude | wc -l\necho -n '# 전 이력에서 .claude 아래 추적된 적 : '\ngit log --all --name-only --pretty=format: | sort -u | grep '^\\.claude' | wc -l\necho -n '# 작업 트리 : '\nls -A .claude | tr '\\n' ' '; echo\ngit check-ignore -v .claude/hooks/ca_import_gate.py | sed 's/^/ /'\n\necho\necho '# 문서가 특정한 규칙 번호가 해석되는가'\ngrep -rnI '\\bG4\\b' --exclude-dir=.git --exclude-dir=build --exclude-dir=.gradle . 2>/dev/null \\\n | grep -iv graphql | sed 's#^\\./# #'\n\necho\necho '# 그 훅을 고치겠다던 계획의 현재 상태'\nsed -n '1,3p' docs/superpowers/plans/2026-07-20-harness-policy-engine.md\necho '# 그것을 대체한 문서의 목표'\ngrep -n -A1 '^\\*\\*Goal:\\*\\*' docs/superpowers/plans/2026-07-25-harness-free-mode-b-amendment.md | head -3\necho -n '# 그 계획이 새로 만들겠다던 디렉터리 : '\n[ -d .harness ] && echo 있음 || echo 없음",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T09:52:56+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a07-f006-claude.txt",
"svg": "evidence/rendered/a07-f006-claude.svg",
"rawSha256": "f329c639cbd773bf02457383ab16c38868ba4905c6c768e9b3fbeeb52e26e772",
"lines": 54,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a08-f005-transferbufferpool-maxborrowedbytes-peak",
"kind": "terminal",
"command": "set -e\nD=$(mktemp -d)\ntrap 'rm -rf \"$D\"' EXIT\nmkdir -p \"$D/src/dev/caskeleton/adapter/outbound/fileserver/platform/local\"\n\ncat > \"$D/src/dev/caskeleton/adapter/outbound/fileserver/platform/local/BorrowPeakProbe.java\" <<'JAVA'\npackage dev.caskeleton.adapter.outbound.fileserver.platform.local;\n\nimport dev.caskeleton.application.fileserver.api.StorageNamespace;\nimport dev.caskeleton.application.fileserver.api.UploadId;\nimport dev.caskeleton.application.fileserver.api.content.AppendResult;\nimport dev.caskeleton.application.fileserver.api.content.WriteFence;\nimport java.io.IOException;\nimport java.nio.ByteBuffer;\nimport java.nio.channels.ReadableByteChannel;\nimport java.nio.file.Files;\nimport java.nio.file.Path;\nimport java.util.List;\nimport java.util.UUID;\n\n/** Runs the same append the bounded-memory regression runs, at several payload sizes. */\npublic final class BorrowPeakProbe {\n\n private static final int BUFFER_BYTES = 128 * 1024;\n\n public static void main(String[] args) throws IOException {\n Path root = Files.createTempDirectory(\"borrow-peak\");\n System.out.println(\"BUFFER_BYTES = \" + BUFFER_BYTES);\n System.out.println();\n System.out.println(\"payload maxBorrowedBytes BUFFER_BYTES 이하 버퍼 크기의 배수\");\n for (long payload : List.of(\n 1L * 1024 * 1024, 16L * 1024 * 1024, 256L * 1024 * 1024)) {\n long peak = peakFor(root, payload);\n System.out.printf(\n \"%-18s %-18d %-18s %d%n\",\n (payload / (1024 * 1024)) + \"MiB\", peak,\n peak <= BUFFER_BYTES ? \"예\" : \"아니오\", peak / BUFFER_BYTES);\n }\n System.out.println();\n System.out.println(\"[대조] 반납 없이 두 번 빌리면 그 단언이 깨진다\");\n TransferBufferPool leaking = new TransferBufferPool(BUFFER_BYTES, 4);\n leaking.borrow();\n leaking.borrow();\n System.out.println(\" maxBorrowedBytes = \" + leaking.maxBorrowedBytes()\n + \" BUFFER_BYTES 이하 = \" + (leaking.maxBorrowedBytes() <= BUFFER_BYTES));\n }\n\n private static long peakFor(Path root, long payload) throws IOException {\n TransferBufferPool pool = new TransferBufferPool(BUFFER_BYTES, 4);\n runAppend(root, pool, payload);\n return pool.maxBorrowedBytes();\n }\n\n private static void runAppend(Path root, TransferBufferPool pool, long payload)\n throws IOException {\n SafeFileChannelFactory channels = new SafeFileChannelFactory(true);\n LocalAppendEngine engine = new LocalAppendEngine(channels, pool);\n UploadId uploadId = UploadId.of(UUID.randomUUID());\n Path staging = new DefaultPhysicalPathResolver(root).stagingPath(uploadId);\n Files.createDirectories(staging.getParent());\n Files.createFile(staging);\n LocalUploadHandle handle =\n new LocalUploadHandle(uploadId, StorageNamespace.of(\"tenant-a\"), staging, Long.MAX_VALUE);\n AppendResult result =\n engine.append(root, handle, 0, new Generated(payload), payload, WriteFence.unfenced());\n if (result.committedOffset() != payload) {\n throw new IllegalStateException(\"unexpected committed offset\");\n }\n }\n\n private static final class Generated implements ReadableByteChannel {\n private long remaining;\n private byte next;\n\n private Generated(long total) {\n this.remaining = total;\n }\n\n @Override\n public int read(ByteBuffer destination) {\n if (remaining <= 0) {\n return -1;\n }\n int count = (int) Math.min(destination.remaining(), remaining);\n for (int index = 0; index < count; index++) {\n destination.put(next++);\n }\n remaining -= count;\n return count;\n }\n\n @Override\n public boolean isOpen() {\n return true;\n }\n\n @Override\n public void close() {}\n }\n}\nJAVA\n\nCACHE=/root/.gradle/caches/modules-2/files-2.1\nLOCK=src/adapter/outbound/fileserver/gradle.lockfile\nCP=src/adapter/outbound/fileserver/build/classes/java/main\nCP=$CP:src/application-core/build/classes/java/main:src/domain-core/build/classes/java/main\nwhile IFS= read -r line; do\n coord=${line%%=*}; confs=${line#*=}\n case \",$confs,\" in *\",testRuntimeClasspath,\"*) ;; *) continue ;; esac\n g=${coord%%:*}; r=${coord#*:}; n=${r%%:*}; v=${r##*:}\n jar=$(find \"$CACHE/$g/$n/$v\" -name '*.jar' ! -name '*sources*' ! -name '*javadoc*' 2>/dev/null | head -1)\n [ -n \"$jar\" ] && CP=\"$CP:$jar\"\ndone < <(grep -E '^[a-zA-Z0-9._-]+:[^=]+=' \"$LOCK\")\n\njavac -encoding UTF-8 -cp \"$CP\" -d \"$D/out\" \\\n \"$D/src/dev/caskeleton/adapter/outbound/fileserver/platform/local/BorrowPeakProbe.java\"\njava -Dstdout.encoding=UTF-8 -cp \"$D/out:$CP\" \\\n dev.caskeleton.adapter.outbound.fileserver.platform.local.BorrowPeakProbe",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T10:10:59+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a08-f005-transferbufferpool-maxborrowedbytes-peak.txt",
"svg": "evidence/rendered/a08-f005-transferbufferpool-maxborrowedbytes-peak.svg",
"rawSha256": "e460836ec24222bfa403400469d1c1fbd5e5be9ab00d9035910ce998798443bd",
"lines": 9,
"redaction": "none — 밀폐 탐침 실행 출력, 자격증명 없음"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a08-f005-transferbufferpool-maxborrowedbytes",
"kind": "terminal",
"command": "F=src/adapter/outbound/fileserver/src\nP=$F/main/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/TransferBufferPool.java\nT=$F/test/java/dev/caskeleton/adapter/outbound/fileserver/platform/local/LocalAppendMemoryTest.java\nE=/shared/document-detail/clean-architecture-backend-template/evidence/raw/145-fileserver-local-io-probes.txt\n\necho '# 자바독이 적은 결합'\nsed -n '55,58p' \"$P\"\n\necho\necho '# 그 이름이 src 아래 자바 파일에 나오는 곳 전수'\ngrep -rn 'maxBorrowedBytes' --include=*.java src | grep -v '/build/' \\\n | sed 's#src/adapter/outbound/fileserver/src/# #'\n\necho\necho '# 원본 절이 근거로 댄 증거 파일이 무엇을 출력했나'\nsed -n '/=== 8.4 doc.behaviour/,/^exit=/p' \"$E\" | sed 's/adapter.outbound.fileserver.src./ …\\//'\n\necho\necho '# 그 시험이 실제로 도는가'\ngrep -n '@Test\\|@Tag\\|@Disabled' \"$T\" | sed 's/^/ /'\ngrep -n 'LocalAppendMemoryTest' .github/workflows/*.yml | sed 's/^/ /'\ngrep -n 'LargeFileBoundedMemoryTest' .github/workflows/*.yml | sed 's/^/ /' ",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T10:10:59+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a08-f005-transferbufferpool-maxborrowedbytes.txt",
"svg": "evidence/rendered/a08-f005-transferbufferpool-maxborrowedbytes.svg",
"rawSha256": "729556a8f4470e14e2d865f4133bf4e2af01a8d04a86d18c3c50220726ba78ed",
"lines": 28,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a10-f001-readme-counts",
"kind": "terminal",
"command": "L=src/adapter/outbound/cache-redis\nR=$L/src/main/java/dev/caskeleton/adapter/outbound/cache/redis\nC=$R/sdk/config/RedisSdkAutoConfiguration.java\nM=$L/src/main/java\n\necho '# 표가 없다고 적은 자리를 센다'\necho -n '# sdk/lettuce/connection : '\necho \"$(find $R/sdk/lettuce/connection -name '*.java' | wc -l) 파일 $(find $R/sdk/lettuce/connection -name '*.java' -exec cat {} + | wc -l) 줄\"\nfind $R/sdk/lettuce/connection -name '*.java' -exec wc -l {} + | sort -rn | sed -n '2,5p' | sed \"s#$R/# #\"\necho '# 표가 든 다섯 포트 중 구현이 있는 넷'\nfor p in cache idempotency ratelimit lease; do\n printf ' %-12s %s 파일 %s 줄\\n' \"$p\" \"$(find $R/$p -name '*.java' | wc -l)\" \"$(find $R/$p -name '*.java' -exec cat {} + | wc -l)\"\ndone\necho -n ' 합계 '\necho \"$(find $R/cache $R/idempotency $R/ratelimit $R/lease -name '*.java' | wc -l) 파일 $(find $R/cache $R/idempotency $R/ratelimit $R/lease -name '*.java' -exec cat {} + | wc -l) 줄\"\necho -n '# 다섯째 session : '\nls -d $R/session 2>/dev/null || { echo -n '패키지 없음, main 전체에서 session 문자열 '; grep -rn 'session' -i --include=*.java $M | wc -l; }\necho -n '# 그 행에 이름이 없어 뺀 패키지 : '\necho \"realtime $(find $R/realtime -name '*.java' -exec cat {} + | wc -l) 줄, keyspace $(find $R/keyspace -name '*.java' -exec cat {} + | wc -l) 줄\"\necho -n '# 상태 기여자 : '\necho \"$(wc -l < $R/sdk/config/RedisHealthContributor.java) + $(wc -l < $R/sdk/config/RedisCorrectnessRoles.java)\"\n\necho\necho '# 조립되지 않는다고 적힌 자동 설정의 @Bean 메서드'\ngrep -n '@Bean' -A2 \"$C\" | grep -E '@Bean|@Conditional|public ' | grep -v '^55:' | sed 's/^/ /'\n\necho\necho '# build.gradle 이 등록한 간선 셋과 앞 주석'\nsed -n '7,15p' $L/build.gradle\n\necho\necho '# 그 간선들을 임포트하는 파일'\nfor e in application shared adapter.outbound.support; do\n printf ' dev.caskeleton.%-24s %s 파일\\n' \"$e\" \"$(grep -rl \"^import dev.caskeleton.$e\" $M | wc -l)\"\ndone\necho -n '# 주석의 주어인 sdk 패키지만 보면 : '\ngrep -rl 'dev.caskeleton.application\\|dev.caskeleton.shared' $R/sdk | wc -l\n\necho\necho '# 스무 줄 뒤의 별개 주석'\nsed -n '33,39p' $L/build.gradle",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T10:44:47+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a10-f001-readme-counts.txt",
"svg": "evidence/rendered/a10-f001-readme-counts.svg",
"rawSha256": "4a0f4da067cc855b7231a101cded9f30e89fd1c7b4dbdb77cbe42c25414969e2",
"lines": 58,
"redaction": "none — 코드베이스 정적 검색"
}
@@ -0,0 +1,14 @@
{
"assetKey": "a10-f001-readme",
"kind": "terminal",
"command": "L=src/adapter/outbound/cache-redis\nT=$L/src/test/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config/RedisSdkAutoConfigurationTest.java\n\necho '# README 가 스스로 정한 세 축과 그 증거'\nsed -n '17,21p' $L/README.md\n\necho\necho '# 준비도 표'\nsed -n '23,28p' $L/README.md\n\necho\necho '# 표 아래 두 문단'\nsed -n '30,33p;35,37p' $L/README.md\n\necho\necho '# 세 줄 뒤 같은 README 의 산문'\nsed -n '39,46p' $L/README.md\n\necho\necho '# 둘째 열의 증거로 지목된 시험이 단언하는 것'\nsed -n '170,180p' \"$T\"",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T10:44:47+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a10-f001-readme.txt",
"svg": "evidence/rendered/a10-f001-readme.svg",
"rawSha256": "a68cf4be2597482900b9d85d30652285bca9c65a600ce62686498b801d02f126",
"lines": 46,
"redaction": "none — 코드베이스 정적 검색"
}
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
{
"assetKey": "a10-f004-pub-sub",
"kind": "terminal",
"command": "set -e\nK=src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/key\nO=src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/api/operations\nC=src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/config\necho \"# 세 타입이 조립 결과에 대해 하는 일\"\nsed -n \"23,26p;34,37p\" $O/PubSubChannel.java\nsed -n \"22,25p;33,36p\" $O/ShardedPubSubChannel.java\nsed -n \"22,32p;38,41p\" $O/PubSubPattern.java\necho\necho \"# requireRenderedSize 가 나오는 곳 전부 (첫 줄은 선언)\"\ngrep -rn \"requireRenderedSize\" src --include=*.java --include=*.groovy | sed \"s|.*/sdk/| sdk/|\"\necho\necho \"# 조각이 받는 규칙과 상한 상수\"\ngrep -n \"MAX_KEY_BYTES\\|Pattern TOKEN\\|Pattern IDENTIFIER\" $K/RedisKeyRules.java | sed \"s/^/ /\"\ngrep -n \"requireToken\\|requireIdentifier\" $K/RedisNamespace.java $K/RedisKeyName.java $K/RedisSlotTag.java | sed \"s|.*/sdk/api/key/| |\"\necho\necho \"# 키를 렌더하는 곳: 조각 사이에 슬롯 태그가 하나 더 들어간다\"\nsed -n \"39,46p\" $K/RedisKeyRenderer.java\necho\necho \"# 그 검사가 보는 상한은 생성자로 주입받는다\"\nsed -n \"22,30p\" $K/RedisKeyRenderer.java\necho\necho \"# 주입하는 곳\"\nprintf \" src/main 에서 new RedisKeyRenderer( : %s 건\\n\" \"$(grep -rn 'new RedisKeyRenderer(' src --include=*.java | grep '/src/main/' | wc -l)\"\nprintf \" src/test 에서 new RedisKeyRenderer( : %s 건\\n\" \"$(grep -rn 'new RedisKeyRenderer(' src --include=*.java | grep '/src/test/' | wc -l)\"\necho \" getMaxKeyBytes / getLimits 를 부르는 src/main 코드\"\ngrep -rn \"getMaxKeyBytes()\\|getLimits()\" src --include=*.java | grep \"/src/main/\" | sed \"s|.*/sdk/| sdk/|\"\necho \" 설정 자체는 등록되어 있다\"\ngrep -n \"APP_REDIS_LIMITS_MAX_KEY_BYTES\" docs/registries/env-keys.yaml | sed \"s/^/ /\"\nsed -n \"241p;254,256p\" $C/RedisSdkSettings.java | sed \"s/^/ /\"\necho\necho \"# 기본 이름공간\"\nsed -n \"153,155p\" $C/RedisSdkSettings.java",
"cwd": "/shared/codebase/clean-architecture-backend-template",
"exitCode": 0,
"executedAt": "2026-09-02T11:21:34+00:00",
"sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916",
"raw": "evidence/raw/a10-f004-pub-sub.txt",
"svg": "evidence/rendered/a10-f004-pub-sub.svg",
"rawSha256": "2065e7b4fc9e1444b40416736114107f0bdbe625010d303acd70f1fb6e6d3e74",
"lines": 89,
"redaction": "none — 코드베이스 정적 검색"
}

Some files were not shown because too many files have changed in this diff Show More