fix(cabt): 기록이 인용한 것을 SSOT 가 안 담고 있었다
check_evidence --repo 이 clean-architecture-backend-template 에서 141건을 세고 있었다.
124건 전부를 고정 리비전 21234e38 에 대조했더니 날조는 0 이었다 — 인용은 맞고 없던
쪽이 SSOT 였다. 그래서 SSOT 를 보강한다.
넣는 것은 기록이 옮겨 적은 문장이 아니라 저장소 원문이다. 기록을 복사해 넣으면
검사기는 초록이 되지만 옮겨 적기가 어긋나도 더는 못 잡는다. 원문을 넣으면 어긋난
기록은 계속 걸린다.
- 코드 124건 → 리프 절 일곱 곳에 원문 30조각(300줄). 자리는 기록의 source 앵커와
소스 파일의 모듈을 교차시켜 정했고, 둘이 갈린 다섯은 모듈을 따르고 그 사실을 적었다
- 식별자 13건 → spring.factories · MethodSecurityConfig 원문과 줄여 적힌 경로의 전체 경로
- source 앵커 4건 → 계약이 이미 적고 있던 SSOT 앵커를 기록 frontmatter 에 맞췄다.
맨 앵커를 한 줄씩 앞에 둔다 — `_record_sources` 가 `- <공백 없는 한 덩어리>` 만 잇달아 읽는다
- 인용 4건 → concept-signed-cursor-structure 의 `{ ... }` 생략을 저장소 원문 형태로 고쳤다.
생략한 자리는 `…` 로 남기고 무엇을 줄였는지 본문에 적는다
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
This commit is contained in:
co-authored by
Claude Opus 5
parent
96d89ec6da
commit
05e96b5add
@@ -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<String> 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<T> {
|
||||
|
||||
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>(T value, Optional<OperationId> operationId) implements TransactionResult<T> {
|
||||
|
||||
public Committed {
|
||||
Objects.requireNonNull(operationId, "operationId must be non-null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionOutcome outcome() {
|
||||
return TransactionOutcome.COMMITTED;
|
||||
}
|
||||
}
|
||||
|
||||
record Participating<T>(T value) implements TransactionResult<T> {
|
||||
|
||||
@Override
|
||||
public TransactionOutcome outcome() {
|
||||
return TransactionOutcome.PARTICIPATING_PENDING_OUTER;
|
||||
}
|
||||
}
|
||||
|
||||
record DeterminateRollback<T>(RuntimeException failure) implements TransactionResult<T> {
|
||||
|
||||
public DeterminateRollback {
|
||||
Objects.requireNonNull(failure, "failure must be non-null");
|
||||
}
|
||||
|
||||
@Override
|
||||
public TransactionOutcome outcome() {
|
||||
return TransactionOutcome.DETERMINATE_ROLLBACK;
|
||||
}
|
||||
}
|
||||
|
||||
record Indeterminate<T>(
|
||||
Optional<OperationId> operationId,
|
||||
TransactionPhase lastObservedPhase,
|
||||
Optional<ReconciliationReference> 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>(T value) implements TransactionResult<T> {
|
||||
|
||||
@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<T> 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<Deque<TransactionEvidenceFrame>> 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<String> 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<HttpStatus> 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> authorizationPort) {
|
||||
AuthorizationManager<MethodInvocation> 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
|
||||
|
||||
+2
@@ -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 이 뒤집었다 — 자동설정의 무조건 빈과 드라이버 호출이 그 근거이며, 위 터미널 출력에서 확인할 수 있다.
|
||||
---
|
||||
|
||||
+14
-5
@@ -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 으로 팽창하므로 인코딩된 길이로 디코딩될 크기의 상한을 알 수 있다. 즉 디코딩하기 전에 크기를 거절할 수 있다.
|
||||
|
||||
+3
@@ -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 으로 올리지 않은 이유와 함께 기록한다.
|
||||
---
|
||||
|
||||
+2
@@ -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 이고, 수정은 자동설정에 탐침을 실행하는 빈 하나를 더하는 것이다.
|
||||
- 같은 사고의 더 상세한 기록은 저장소 문서 쪽에 있다. 지원 매트릭스가 승격과 강등 시각을 초 단위로 적고, 운영 문서와 런북이 가드를 적용한 뒤 같은 승격에서 무엇이 달라졌는지 적는다.
|
||||
---
|
||||
|
||||
@@ -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": [
|
||||
|
||||
+2
@@ -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 이 나온다. 이 저장소는 등록을 컨벤션 플러그인으로 옮겨 두었으므로 그 수는 등록된 태스크 수가 아니다. 위 터미널 출력의 레인 목록이 그것을 보여 준다.
|
||||
---
|
||||
|
||||
Reference in New Issue
Block a user