diff --git a/docs/clean-architecture-backend-template/final/document.md b/docs/clean-architecture-backend-template/final/document.md index f9feacf..b096679 100644 --- a/docs/clean-architecture-backend-template/final/document.md +++ b/docs/clean-architecture-backend-template/final/document.md @@ -2483,6 +2483,38 @@ test: ``` +#### 기록이 인용한 원문 — `21234e38` + +> `tech-log-studio/` 의 기록이 인용한 코드가 이 문서에 없었다(`check_evidence --repo`). 인용한 줄은 고정 리비전 `21234e38` 에 실재하는 것을 +> `git grep -F` 로 확인했고, 없던 쪽은 이 문서였다. **옮겨 적은 문장이 아니라 저장소 +> 원문을 담는다** — 기록을 복사해 넣으면 옮겨 적기가 어긋나도 검사기가 더는 못 잡는다. + +`src/shared-contract/src/main/java/dev/caskeleton/shared/metrics/CardinalityBounds.java:21-33` — `concept-cardinality-bounds-as-types.md` 가 인용한다. + +```java + public static final int STATUS_CODE = 7; + + /** URI template tag bound — routes must be template-normalised (e.g. {@code /users/{id}}). */ + public static final int URI_TEMPLATE = 200; + + /** Distinct named external dependency tag bound. */ + public static final int DEPENDENCY_NAME = 50; + + /** Error code tag bound — kept in sync with the row count of {@code error-codes.yaml}. */ + public static final int ERROR_CODE = 100; + + /** Tenant id tag bound — bounded mapping-table id / cohort bucket only, no raw UUID. */ + public static final int TENANT_ID = 1000; +``` + +`src/shared-contract/src/main/java/dev/caskeleton/shared/metrics/ForbiddenMetricTags.java:26-27` — `concept-cardinality-bounds-as-types.md` 가 인용한다. + +```java + public static final Set FORBIDDEN = + Set.of("user_id", "request_id", "raw_url", "raw_query", "raw_header_value", "ip_address"); +``` + + --- ## A03. application-core @@ -2944,6 +2976,77 @@ test: ``` +#### 기록이 인용한 원문 — `21234e38` + +> `tech-log-studio/` 의 기록이 인용한 코드가 이 문서에 없었다(`check_evidence --repo`). 인용한 줄은 고정 리비전 `21234e38` 에 실재하는 것을 +> `git grep -F` 로 확인했고, 없던 쪽은 이 문서였다. **옮겨 적은 문장이 아니라 저장소 +> 원문을 담는다** — 기록을 복사해 넣으면 옮겨 적기가 어긋나도 검사기가 더는 못 잡는다. + +`src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionResult.java:12-14` — `concept-transaction-result-algebra.md` 가 인용한다. + +```java +public sealed interface TransactionResult { + + TransactionOutcome outcome(); +``` + +`src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionResult.java:16-51` — `concept-transaction-result-algebra.md` 가 인용한다. + +```java + record Committed(T value, Optional operationId) implements TransactionResult { + + public Committed { + Objects.requireNonNull(operationId, "operationId must be non-null"); + } + + @Override + public TransactionOutcome outcome() { + return TransactionOutcome.COMMITTED; + } + } + + record Participating(T value) implements TransactionResult { + + @Override + public TransactionOutcome outcome() { + return TransactionOutcome.PARTICIPATING_PENDING_OUTER; + } + } + + record DeterminateRollback(RuntimeException failure) implements TransactionResult { + + public DeterminateRollback { + Objects.requireNonNull(failure, "failure must be non-null"); + } + + @Override + public TransactionOutcome outcome() { + return TransactionOutcome.DETERMINATE_ROLLBACK; + } + } + + record Indeterminate( + Optional operationId, + TransactionPhase lastObservedPhase, + Optional reconciliationReference) +``` + +`src/application-core/src/main/java/dev/caskeleton/application/transaction/TransactionResult.java:23-32` — `concept-transaction-result-algebra.md` 가 인용한다. + +```java + public TransactionOutcome outcome() { + return TransactionOutcome.COMMITTED; + } + } + + record Participating(T value) implements TransactionResult { + + @Override + public TransactionOutcome outcome() { + return TransactionOutcome.PARTICIPATING_PENDING_OUTER; +``` + + --- ## A04. adapter-outbound-support @@ -8611,6 +8714,203 @@ test: ``` +#### 기록이 인용한 원문 — `21234e38` + +> `tech-log-studio/` 의 기록이 인용한 코드가 이 문서에 없었다(`check_evidence --repo`). 인용한 줄은 고정 리비전 `21234e38` 에 실재하는 것을 +> `git grep -F` 로 확인했고, 없던 쪽은 이 문서였다. **옮겨 적은 문장이 아니라 저장소 +> 원문을 담는다** — 기록을 복사해 넣으면 옮겨 적기가 어긋나도 검사기가 더는 못 잡는다. + +`case-a-retry-implementation-nobody-calls.md` 가 인용한다. + +기록이 `rg` 출력을 줄여 적은 경로의 전체 경로다. + +```text +src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/outbox/OutboxReaper.java +``` + +`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/query/SignedJsonCursorCodec.java:84-106` — `concept-signed-cursor-structure.md` 가 인용한다. + +```java + if (encoded == null || encoded.isBlank()) { + throw new IllegalArgumentException("cursor must not be blank"); + } + // First line, before any substring, decode or MAC. A paging endpoint is public, and everything + // below this point allocates in proportion to what the caller sent: repeatedly posting a very + // large token made the server build strings, byte arrays and a MAC input before it had any + // reason to believe the token was real. A page-size bound does not bound the token. + if (encoded.length() > MAX_ENCODED_LENGTH) { + throw new IllegalArgumentException("cursor exceeds the maximum token length"); + } + int payloadSeparator = encoded.indexOf(SEPARATOR); + int macSeparator = encoded.lastIndexOf(SEPARATOR); + if (payloadSeparator <= 0 || macSeparator <= payloadSeparator) { + throw new IllegalArgumentException("malformed cursor"); + } + String version = encoded.substring(0, payloadSeparator); + if (!VERSION.equals(version)) { + throw new IllegalArgumentException("unknown cursor version"); + } + // Base64 expands by 4/3, so the encoded payload segment's length bounds the decoded size + // exactly. Checking it here refuses an oversized payload without allocating it first. + int encodedPayloadLength = macSeparator - payloadSeparator - 1; + if (decodedLengthOf(encodedPayloadLength) > MAX_PAYLOAD_BYTES) { +``` + +`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/api/transaction/TransactionCompletionEvidence.java:16-16` — `concept-transaction-result-algebra.md` 가 인용한다. + +```java +public enum TransactionCompletionEvidence { +``` + +`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/notification/NotificationSchemaStream.java:25-28` — `concept-independent-flyway-streams.md` 가 인용한다. + +```java + public static final String LOCATION = "classpath:db/migration/jpa/notification-platform"; + + /** The history table this stream records into, separate from the primary one. */ + public static final String HISTORY_TABLE = "flyway_jpa_notification_history"; +``` + +`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaMetricTags.java:18-23` — `concept-cardinality-bounds-as-types.md` 가 인용한다. + +```java +public record JpaMetricTags( + String persistenceUnit, + String operationName, + String queryName, + String outcome, + String failureCategory) { +``` + +`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/observation/JpaMetricTags.java:28-35` — `concept-cardinality-bounds-as-types.md` 가 인용한다. + +```java + public JpaMetricTags { + persistenceUnit = orNone(persistenceUnit); + operationName = orNone(operationName); + queryName = orNone(queryName); + outcome = orNone(outcome); + failureCategory = orNone(failureCategory); + LowCardinality.requireRegistered( + persistenceUnit, operationName, queryName, outcome, failureCategory); +``` + +`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/postgresql/idempotency/IdempotencyTransitionGateway.java:25-30` — `concept-cas-tuple-and-update-count.md` 가 인용한다. + +```java + update idempotency_record + set status = 'EXECUTING', + state_revision = state_revision + 1, + last_transition_operation_id = ?, + last_transition_kind = 'START', + last_transition_result_digest = ?, +``` + +`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java:65-69` — `case-a-retry-implementation-nobody-calls.md` 가 인용한다. + +```java + AttemptResult attemptResult = executeOnce(request, action, policy); + if (!shouldRetry(request.policyId(), attemptResult, attempt)) { + return attemptResult.result(); + } + if (!retryBackoff.pauseBeforeRetry(request.callBudget(), attempt)) { +``` + +`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/SpringPolicyTransactionPort.java:145-145` — `case-a-retry-implementation-nobody-calls.md` 가 인용한다. + +```java + if (policyId != TransactionPolicyId.COMMAND_SERIALIZABLE_REPLAY_SAFE +``` + +`src/adapter/outbound/persistence-jpa/src/main/java/dev/caskeleton/adapter/outbound/persistence/transaction/TransactionEvidenceContext.java:38-38` — `concept-transaction-result-algebra.md` 가 인용한다. + +```java + private static final ThreadLocal> FRAMES = new ThreadLocal<>(); +``` + +`src/adapter/outbound/persistence-jpa/src/main/resources/db/experimental-rls/V1__tenant_rls.sql:40-40` — `concept-rls-three-preconditions.md` 가 인용한다. + +```sql + using (tenant_id = current_setting('app.tenant_id', true)) +``` + +`src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/jpa/fileserver/V1__create_fileserver_metadata.sql:10-15` — `concept-file-state-machine-and-ready.md` 가 인용한다. + +```sql + FROM capability_schema_registry + WHERE capability_id = 'jpa-flyway-migration' + AND core_epoch >= 1 + AND lifecycle_state = 'ACTIVE' + ) THEN + RAISE EXCEPTION 'fileserver metadata requires active core epoch 1'; +``` + +`src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V6__capability_schema_registry_adoption.sql:6-13` — `concept-capability-schema-registry.md` 가 인용한다. + +```sql + IF to_regclass('public.idempotency_record') IS NULL THEN + RAISE EXCEPTION 'legacy adoption requires idempotency_record'; + END IF; + IF to_regclass('public.outbox_event') IS NULL THEN + RAISE EXCEPTION 'legacy adoption requires outbox_event'; + END IF; + IF to_regclass('public.int_lock') IS NULL THEN + RAISE EXCEPTION 'legacy adoption requires INT_LOCK'; +``` + +`src/adapter/outbound/persistence-jpa/src/main/resources/db/migration/postgresql/V6__capability_schema_registry_adoption.sql:18-22` — `concept-capability-schema-registry.md` 가 인용한다. + +```sql +CREATE TABLE capability_schema_registry ( + capability_id varchar(128) NOT NULL, + schema_stream varchar(32) NOT NULL, + installation_origin varchar(32) NOT NULL, + core_epoch integer NOT NULL, +``` + +`src/build-logic/src/main/groovy/ca.strict-test-lane.gradle:13-16` — `concept-strict-test-lane.md` 가 인용한다. + +```groovy +// lane('mongoReplicaSetTest') { +// tag = 'mongodb-replicaset' +// description = 'Single-node replica set contract lane.' +// customize = { test -> applyMongoImageSelection(test) } +``` + +`src/gradle/jpa-evidence.gradle:343-358` — `concept-evidence-grades-and-provenance.md` 가 인용한다. + +```groovy + if (manifest.attainedReadiness == 'R2') { + if (manifest.profile != 'r2') { + violations << "${cardId}: R2 requires the r2 profile" + } + if (source.worktreeDirty != false) { + violations << "${cardId}: R2 requires a clean worktree" + } + if (!missing.isEmpty()) { + violations << "${cardId}: R2 has missing evidence ${missing}" + } + if (producer.ciJob == 'local-unpublished') { + violations << "${cardId}: R2 requires a real CI job identity" + } + if (!((manifest.artifactLocation as String) ==~ + /(?i)(https|s3|gs):\/\/\S+/)) { + violations << "${cardId}: R2 requires an externally retained artifact location" +``` + +`src/gradle/jpa-evidence.gradle:422-422` — `concept-evidence-grades-and-provenance.md` 가 인용한다. + +```groovy + description = 'Mutation-tests JPA evidence schema, no-skip, content hash, and R2 provenance checks.' +``` + +`src/gradle/jpa-evidence.gradle:518-518` — `concept-evidence-grades-and-provenance.md` 가 인용한다. + +```groovy + 'verifyJpaEvidenceHarnessContract: OK — skip, dirty/local R2, and content mutation fail closed.') +``` + + --- ## A06. adapter-outbound-persistence-mongo @@ -13441,6 +13741,24 @@ test: ``` +#### 기록이 인용한 원문 — `21234e38` + +> `tech-log-studio/` 의 기록이 인용한 코드가 이 문서에 없었다(`check_evidence --repo`). 인용한 줄은 고정 리비전 `21234e38` 에 실재하는 것을 +> `git grep -F` 로 확인했고, 없던 쪽은 이 문서였다. **옮겨 적은 문장이 아니라 저장소 +> 원문을 담는다** — 기록을 복사해 넣으면 옮겨 적기가 어긋나도 검사기가 더는 못 잡는다. + +`src/adapter/outbound/cache-redis/src/main/java/dev/caskeleton/adapter/outbound/cache/redis/sdk/lettuce/command/CommandPolicyGuard.java:41-46` — `concept-redis-admission-stages.md` 가 인용한다. + +```java + private final RedisCommandCatalog catalog; + private final RedisPermitVerifier permitVerifier; + private final RedisCapabilities capabilities; + private final RedisNamespace namespace; + private final RedisKeyRenderer keyRenderer; + private final ToIntFunction slotCalculator; +``` + + --- ## A11. adapter-outbound-httpclient @@ -14285,6 +14603,149 @@ evidence/raw/332-httpclient-dualstack-localhost-masks-tls-permanent.txt evidence/raw/333-eighteen-docs-source-drift-zero.txt ``` +#### 기록이 인용한 원문 — `21234e38` + +> `tech-log-studio/` 의 기록이 인용한 코드가 이 문서에 없었다(`check_evidence --repo`). 인용한 줄은 고정 리비전 `21234e38` 에 실재하는 것을 +> `git grep -F` 로 확인했고, 없던 쪽은 이 문서였다. **옮겨 적은 문장이 아니라 저장소 +> 원문을 담는다** — 기록을 복사해 넣으면 옮겨 적기가 어긋나도 검사기가 더는 못 잡는다. + +`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/apache/ApacheFailureClassifier.java:36-42` — `concept-transport-failure-stage-and-category.md` 가 인용한다. + +```java + for (Throwable cause : chain(failure)) { + TransportFailure recognized = recognize(cause, lastObservedStage); + if (recognized != null) { + return recognized; + } + } + return conservative(lastObservedStage, FailureCategory.UNKNOWN, "TRANSPORT_FAILURE"); +``` + +`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/AttemptStage.java:9-21` — `concept-transport-failure-stage-and-category.md` 가 인용한다. + +```java +public enum AttemptStage { + VALIDATION(0, true), + AUTHENTICATION(1, true), + POOL_ACQUIRE(2, true), + DNS(3, true), + CONNECT(4, true), + TLS_HANDSHAKE(5, true), + PROXY_CONNECT(6, true), + REQUEST_HEADERS(7, false), + REQUEST_BODY(8, false), + RESPONSE_HEADERS(9, false), + RESPONSE_BODY(10, false), + COMPLETE(11, false); +``` + +`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/api/operation/FailureCategory.java:35-36` — `concept-transport-failure-stage-and-category.md` 가 인용한다. + +```java + public boolean permanent() { + return this == CONFIGURATION +``` + +`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/resilience/DefaultRetryEligibilityEngine.java:37-107` — `concept-transport-failure-stage-and-category.md` 가 인용한다. + +```java + if (context.failureCategory().permanent()) { + return RetryDenied.permanentFailure(context.failureCategory().name()); + } + if (context.evidence() == ExecutionEvidence.PARTIAL_RESPONSE) { + // A partial response that never reached the caller may still be retried for a safe + // operation; once a byte was delivered the earlier guard has already denied it. + return context.safelyIdempotent() + ? RetryAllowed.of("PARTIAL_RESPONSE") + : AmbiguousFailure.remoteOutcomeUnknown(); + } + if (context.evidence() == ExecutionEvidence.SENT_NO_RESPONSE && !context.safelyIdempotent()) { + return AmbiguousFailure.remoteOutcomeUnknown(); + } + return statusOrFailureDecision(context); + } + + private RetryDecision statusOrFailureDecision(RetryContext context) { + Optional status = context.responseStatus(); + if (status.isPresent()) { + return statusDecision(context, status.get().value()); + } + return failureDecision(context); + } + + private RetryDecision statusDecision(RetryContext context, int status) { + return switch (status) { + // 408, 425 and 429 all mean the request reached the upstream and was answered, so repeating + // one is only safe under the same rule as every other repeat. These three used to skip that + // check: a non-idempotent POST answered 429 was retried, and a rate-limited upstream that had + // already accepted the work got it a second time. A 429 is a scheduling signal, never a + // statement that nothing happened. + case 408 -> + context.safelyIdempotent() + ? allowWithin(context, "REQUEST_TIMEOUT") + : AmbiguousFailure.remoteOutcomeUnknown(); + // 425 Too Early: repeating once without early data is safe; repeating repeatedly is not. + case 425 -> { + if (!context.safelyIdempotent()) { + yield AmbiguousFailure.remoteOutcomeUnknown(); + } + yield context.attempt() == 1 + ? allowWithin(context, "TOO_EARLY") + : RetryDenied.maxAttempts(); + } + case 429 -> + context.safelyIdempotent() + ? allowWithin(context, "RATE_LIMITED") + : RetryDenied.notRetryableStatus(status); + case 401 -> + context.credentialRefreshAvailable() + && context.attempt() == 1 + && context.safelyIdempotent() + ? RetryAllowed.of("UNAUTHORIZED_REFRESH") + : RetryDenied.notRetryableStatus(status); + case 500 -> + context.transientServerErrorStatuses().contains(500) && context.safelyIdempotent() + ? allowWithin(context, "UPSTREAM_TRANSIENT") + : RetryDenied.notRetryableStatus(status); + case 502, 503, 504 -> + context.safelyIdempotent() + ? allowWithin(context, "UPSTREAM_UNAVAILABLE") + : AmbiguousFailure.remoteOutcomeUnknown(); + default -> RetryDenied.notRetryableStatus(status); + }; + } + + private RetryDecision failureDecision(RetryContext context) { + return switch (context.failureCategory()) { + case POOL_ACQUIRE_TIMEOUT -> RetryAllowed.of("POOL_ACQUIRE_TIMEOUT"); + case DNS -> RetryAllowed.of("DNS"); + case CONNECT -> RetryAllowed.of("CONNECT"); +``` + +`src/adapter/outbound/httpclient/src/main/java/dev/caskeleton/adapter/outbound/httpclient/transport/TransportFailure.java:17-34` — `concept-transport-failure-stage-and-category.md` 가 인용한다. + +```java +public record TransportFailure( + AttemptStage stage, ExecutionEvidence evidence, FailureCategory category, String safeReason) { + + public TransportFailure { + Objects.requireNonNull(stage, "stage"); + Objects.requireNonNull(evidence, "evidence"); + Objects.requireNonNull(category, "category"); + Objects.requireNonNull(safeReason, "safe reason"); + } + + public static TransportFailure notSent( + AttemptStage stage, FailureCategory category, String safeReason) { + return new TransportFailure(stage, ExecutionEvidence.NOT_SENT, category, safeReason); + } + + public static TransportFailure sentNoResponse( + AttemptStage stage, FailureCategory category, String safeReason) { + return new TransportFailure(stage, ExecutionEvidence.SENT_NO_RESPONSE, category, safeReason); +``` + + --- ## A12. adapter-outbound-messaging @@ -17868,6 +18329,45 @@ test: ``` +#### 기록이 인용한 원문 — `21234e38` + +> `tech-log-studio/` 의 기록이 인용한 것이 이 문서에 없었다(`check_evidence --repo`). +> 인용한 것은 고정 리비전 `21234e38` 에 실재하는 것을 확인했고, 없던 쪽은 이 문서였다. +> **옮겨 적은 문장이 아니라 저장소 원문을 담는다** — 기록을 복사해 넣으면 옮겨 적기가 +> 어긋나도 검사기가 더는 못 잡는다. + +`src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/MethodSecurityConfig.java:3-10` — `case-a-retry-implementation-nobody-calls.md` 가 인용한다. + +```java +import dev.caskeleton.application.security.AuthorizationPort; +import dev.caskeleton.application.security.RequiresPermission; +import org.aopalliance.intercept.MethodInvocation; +import org.springframework.aop.Advisor; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.Pointcuts; +import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; +import org.springframework.beans.factory.ObjectProvider; +``` + +`src/adapter/inbound/web/src/main/java/dev/caskeleton/adapter/inbound/web/authz/MethodSecurityConfig.java:48-60` — `case-a-retry-implementation-nobody-calls.md` 가 인용한다. + +```java + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + static Advisor requiresPermissionAuthorizationAdvisor( + ObjectProvider authorizationPort) { + AuthorizationManager manager = + new RequiresPermissionAuthorizationManager(authorizationPort::getObject); + + Pointcut onMethod = AnnotationMatchingPointcut.forMethodAnnotation(RequiresPermission.class); + Pointcut onClass = AnnotationMatchingPointcut.forClassAnnotation(RequiresPermission.class); + Pointcut pointcut = Pointcuts.union(onMethod, onClass); + + return new AuthorizationManagerBeforeMethodInterceptor(pointcut, manager); + } +``` + + --- ## A15. adapter-inbound-grpc @@ -20987,6 +21487,59 @@ test: ``` +#### 기록이 인용한 원문 — `21234e38` + +> `tech-log-studio/` 의 기록이 인용한 코드가 이 문서에 없었다(`check_evidence --repo`). 인용한 줄은 고정 리비전 `21234e38` 에 실재하는 것을 +> `git grep -F` 로 확인했고, 없던 쪽은 이 문서였다. **옮겨 적은 문장이 아니라 저장소 +> 원문을 담는다** — 기록을 복사해 넣으면 옮겨 적기가 어긋나도 검사기가 더는 못 잡는다. + +`src/app-bootstrap/src/main/resources/META-INF/spring.factories:1-13` — `concept-three-assembly-paths.md` 가 인용한다. + +```properties +org.springframework.boot.EnvironmentPostProcessor=\ +dev.caskeleton.bootstrap.activation.MasterSwitchEnvironmentPostProcessor,\ +dev.caskeleton.bootstrap.activation.RuntimeEnvironmentProfileValidator,\ +dev.caskeleton.bootstrap.activation.CapabilityDependencyEnvironmentValidator,\ +dev.caskeleton.bootstrap.tracing.TracingSamplingEnvironmentPostProcessor,\ +dev.caskeleton.bootstrap.runtime.RedisReadinessGroupPostProcessor,\ +dev.caskeleton.bootstrap.runtime.DatabaseReadinessGroupPostProcessor + +org.springframework.boot.SpringBootExceptionReporter=\ +dev.caskeleton.bootstrap.runtime.startup.StartupFailureExceptionReporter + +org.springframework.boot.autoconfigure.AutoConfigurationImportFilter=\ +dev.caskeleton.bootstrap.autoconfigure.persistencejpa.JpaOffAutoConfigurationImportFilter +``` + +`case-a-retry-implementation-nobody-calls.md` 가 인용한다. + +기록이 `rg` 출력을 줄여 적은 경로의 전체 경로다. + +```text +src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java +src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/autoconfigure/jpa/JpaTransactionAutoConfiguration.java +``` + +`src/app-bootstrap/src/main/java/dev/caskeleton/bootstrap/CaSkeletonApplication.java:43-56` — `concept-three-assembly-paths.md` 가 인용한다. + +```java + "dev.caskeleton.bootstrap", + "dev.caskeleton.adapter", + "dev.caskeleton.application", + "dev.caskeleton.domain", + "dev.caskeleton.shared" + }, + excludeFilters = { + @ComponentScan.Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class), + @ComponentScan.Filter( + type = FilterType.CUSTOM, + classes = AutoConfigurationExcludeFilter.class), + @ComponentScan.Filter( + type = FilterType.REGEX, + pattern = CaSkeletonApplication.AUTO_CONFIGURED_PACKAGES) +``` + + --- ## A19. messaging-platform @@ -31607,6 +32160,26 @@ src/messaging/messaging-inbox-jdbc-postgresql/.../InboxCleanupJob.java:56 src/app-bootstrap/src/test/.../MessagingCapabilityRegistryContractTest.java:61 ``` +#### 기록이 인용한 원문 — `21234e38` + +> `tech-log-studio/` 의 기록이 인용한 코드가 이 문서에 없었다(`check_evidence --repo`). 인용한 줄은 고정 리비전 `21234e38` 에 실재하는 것을 +> `git grep -F` 로 확인했고, 없던 쪽은 이 문서였다. **옮겨 적은 문장이 아니라 저장소 +> 원문을 담는다** — 기록을 복사해 넣으면 옮겨 적기가 어긋나도 검사기가 더는 못 잡는다. + +`src/messaging/messaging-outbox-jdbc-postgresql/src/main/resources/db/migration/messaging/V2__messaging_outbox_lease_fencing.sql:16-23` — `concept-fenced-lease.md` 가 인용한다. + +```sql + ADD COLUMN lease_owner VARCHAR(160), + ADD COLUMN lease_token BIGINT NOT NULL DEFAULT 0, + ADD COLUMN next_attempt_at TIMESTAMPTZ; + +-- Backfill is unnecessary for correctness — the default is 0 and the first claim increments it — +-- but the constraint states the invariant the code depends on. +ALTER TABLE messaging_outbox + ADD CONSTRAINT ck_messaging_outbox_lease_token CHECK (lease_token >= 0); +``` + + --- ## A19-MESSAGING-POLICY. messaging-policy diff --git a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-a-flag-that-validates-an-unwired-subsystem.md b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-a-flag-that-validates-an-unwired-subsystem.md index a526084..e437d12 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-a-flag-that-validates-an-unwired-subsystem.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/assembly-ownership/case/case-a-flag-that-validates-an-unwired-subsystem.md @@ -14,6 +14,8 @@ assets: evidence: - ../../../final/evidence/raw/a-flag-that-validates-an-unwired-subsystem.txt source: + - final/document.md#4-2 + - final/document.md#a06 - 분석 문서는 mongo 어댑터 편 §49 다. 플래그가 그대로 바인딩된다는 것은 같은 문서 §6 의 바인딩 관측값이고, 형제 입력의 처리 차이도 그 절에 있다. 반대쪽 실행체가 출하되어 플래그와 무관하게 조립된다는 것은 §65 와 §68 이다. - 같은 문서 §50 은 둘 다 실행체가 없다고 적는다. 그 줄은 sub-scope 06 시점의 요약이고 §68 이 뒤집었다 — 자동설정의 무조건 빈과 드라이버 호출이 그 근거이며, 위 터미널 출력에서 확인할 수 있다. --- diff --git a/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/concept/concept-signed-cursor-structure.md b/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/concept/concept-signed-cursor-structure.md index bab837a..0b87cd2 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/concept/concept-signed-cursor-structure.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/bounding-by-type/concept/concept-signed-cursor-structure.md @@ -100,17 +100,26 @@ MAC 이 버전까지 덮는 것이 이 구조의 첫 결정이다. 페이로드 ## 검증 순서 -디코드 경로는 값을 해석하기 전에 형태부터 검사한다. +디코드 경로는 값을 해석하기 전에 형태부터 검사한다. `…` 는 던지는 줄을 줄인 것이고, +줄이지 않은 줄은 `SignedJsonCursorCodec.java:84-106` 원문 그대로다. ```java -if (encoded == null || encoded.isBlank()) { ... } +if (encoded == null || encoded.isBlank()) { + … +} if (encoded.length() > MAX_ENCODED_LENGTH) { throw new IllegalArgumentException("cursor exceeds the maximum token length"); } -if (payloadSeparator <= 0 || macSeparator <= payloadSeparator) { ... } -if (!VERSION.equals(version)) { ... } +if (payloadSeparator <= 0 || macSeparator <= payloadSeparator) { + … +} +if (!VERSION.equals(version)) { + … +} // Base64 expands by 4/3, so the encoded payload segment's length bounds the decoded size -if (decodedLengthOf(encodedPayloadLength) > MAX_PAYLOAD_BYTES) { ... } +if (decodedLengthOf(encodedPayloadLength) > MAX_PAYLOAD_BYTES) { + … +} ``` 주석 한 줄이 순서의 이유를 담는다. Base64 는 4/3 으로 팽창하므로 인코딩된 길이로 디코딩될 크기의 상한을 알 수 있다. 즉 디코딩하기 전에 크기를 거절할 수 있다. diff --git a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-a-lease-without-an-owner.md b/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-a-lease-without-an-owner.md index be2127f..d6bfdd5 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-a-lease-without-an-owner.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/owner-safe-state-machines/case/case-a-lease-without-an-owner.md @@ -14,6 +14,9 @@ assets: evidence: - ../../../final/evidence/raw/a-lease-without-an-owner.txt source: + - final/document.md#4-3 + - final/document.md#a19 + - final/document.md#a08 - 분석 문서는 메시징 플랫폼 편 §7.3 이 V2 헤더를 인용하며 시간과 소유권 토큰의 차이를 짚고, outbox 어댑터 편 §12.3 이 두 세대의 술어와 SET 절과 반환 타입을 표로 대조한다. `@Deprecated` 가 없다는 것은 reliability-api 편이 P2 로 다룬다. 옛 경로가 남아 있는 것은 열린 질문이 아니라 P3 결함이고, 권고는 애너테이션이 아니라 제거다. - 같은 형태의 다른 다섯 곳은 JPA 어댑터 편 §70·§71·§89 와 §83.3, mongo 어댑터 편 §55 다. §83.2 는 파일서버 쪽에 남은 구간 — 리스 만료 직후 인수 전에 아직 settle 할 수 있는 창 — 을 finding 으로 올리지 않은 이유와 함께 기록한다. --- diff --git a/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/case/case-a-startup-probe-that-never-runs.md b/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/case/case-a-startup-probe-that-never-runs.md index cd47c2e..b03ed5e 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/case/case-a-startup-probe-that-never-runs.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/redis-command-admission/case/case-a-startup-probe-that-never-runs.md @@ -14,6 +14,8 @@ assets: evidence: - ../../../final/evidence/raw/a-startup-probe-that-never-runs.txt source: + - final/document.md#4-4 + - final/document.md#a10 - 분석 문서는 cache-redis 어댑터 편 §6 이다. 그 절이 탐침이 확인하는 넷을 나열하고, 넷째의 javadoc 을 인용하고, 두 탐침의 프로덕션 참조가 javadoc 링크 하나뿐이라는 것과 자동설정이 탐침을 부르지 않는다는 것을 기록한다. 판정은 P2 이고, 수정은 자동설정에 탐침을 실행하는 빈 하나를 더하는 것이다. - 같은 사고의 더 상세한 기록은 저장소 문서 쪽에 있다. 지원 매트릭스가 승격과 강등 시각을 초 단위로 적고, 운영 문서와 런북이 가드를 적용한 뒤 같은 승격에서 무엇이 달라졌는지 적는다. --- diff --git a/docs/clean-architecture-backend-template/tech-log-studio/tech-log-tree.json b/docs/clean-architecture-backend-template/tech-log-studio/tech-log-tree.json index 0262ee6..89bb644 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/tech-log-tree.json +++ b/docs/clean-architecture-backend-template/tech-log-studio/tech-log-tree.json @@ -7,9 +7,9 @@ "revision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916", "verified": "git rev-parse HEAD 가 이 값과 같다 (2026-09-07 확인)" }, - "ssotSha256": "74b4986fd8621f6fd61791232777dc0e6c8ef999cbc2343df050bd5921ea03ac", + "ssotSha256": "68c09e69b095f50e030a3b7454417d07f9ecb1a9c9ed4c04197b0f406ecc2405", "sourceRevision": "21234e38cdb9a926cbc92bb97a2aee2e4a7d2916", - "generatedAt": "2026-09-08", + "generatedAt": "2026-09-11", "candidateScope": { "document": "final/document.md", "sections": [ diff --git a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-release-gate-with-no-evidence-producer.md b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-release-gate-with-no-evidence-producer.md index 2074d76..5e9cdd5 100644 --- a/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-release-gate-with-no-evidence-producer.md +++ b/docs/clean-architecture-backend-template/tech-log-studio/what-a-gate-does-not-prove/case/case-a-release-gate-with-no-evidence-producer.md @@ -15,6 +15,8 @@ evidence: - ../../../final/evidence/raw/a-release-gate-with-no-evidence-producer.txt - ../../../final/evidence/raw/tl-grpc-release-gate-no-producer.txt source: + - final/document.md#7-6 + - final/document.md#a20 - 분석 문서는 gRPC 플랫폼 편 §3.2 다. 그 절이 지원 매트릭스의 현재 시제 문장과 게이트의 설계 근거를 인용하고, 생성 지점 넷이 전부 테스트라는 것과 messaging 이 그것을 세 층으로 닫았다는 대비를 적는다. - 같은 절이 근거로 든 검색은 리프의 build.gradle 안에서 태스크 등록 문자열을 세는 것이라 0 이 나온다. 이 저장소는 등록을 컨벤션 플러그인으로 옮겨 두었으므로 그 수는 등록된 태스크 수가 아니다. 위 터미널 출력의 레인 목록이 그것을 보여 준다. ---