Files
document-haness/docs/clean-architecture-backend-template/final/evidence/raw/128-mongo-api-negative-space-probes.txt
T
DongHyeonkaandClaude Opus 5 b2963105a8 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>
2026-09-04 22:51:59 +09:00

218 lines
13 KiB
Plaintext

Sub-scope 02 (mongo) negative-space probes: api surface reachability, invariant siblings, duplicate mechanisms, drift
revision=a24ece9cf797f7ea647e33bf846b115208ed1ba5
executedAt=2026-08-29T23:31:37+00:00
cwd=/shared/codebase/clean-architecture-backend-template/src
observation boundary: tracked sources at this revision. api/** is the intended external
surface, so a zero in-repository reference is not evidence of dead code.
=== 8.1 api types referenced from outside the owning leaf ===
$ git grep -l 'outbound\.mongo\.api\.' -- . ':!adapter/outbound/persistence-mongo/**'
exit=1
=== 8.1b framework-free rule: any Spring/driver/BSON/Reactor import under api/** ===
$ git grep -n 'import org\.springframework\|import com\.mongodb\|import org\.bson\|import reactor\.' -- 'adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api'
exit=1
=== 8.2 invariant siblings: which api records refuse an incoherent construction ===
$ grep -n 'requireCommitUnknown\|requireTransient' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTransactionCommitUnknownException.java adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTransactionTransientException.java
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTransactionCommitUnknownException.java:32: requireCommitUnknown(failureContext));
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTransactionCommitUnknownException.java:36: private static MongoFailureContext requireCommitUnknown(MongoFailureContext context) {
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTransactionTransientException.java:30: requireTransient(failureContext));
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTransactionTransientException.java:33: private static MongoFailureContext requireTransient(MongoFailureContext context) {
exit=0
$ sed -n '30,55p' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoFailureContext.java
int attempt,
Duration elapsed,
String traceId) {
public MongoFailureContext {
Objects.requireNonNull(scope, "scope");
Objects.requireNonNull(operationType, "operationType");
Objects.requireNonNull(consistencyProfile, "consistencyProfile");
Objects.requireNonNull(category, "category");
Objects.requireNonNull(outcome, "outcome");
Objects.requireNonNull(errorLabels, "errorLabels");
Objects.requireNonNull(serverCode, "serverCode");
Objects.requireNonNull(elapsed, "elapsed");
Objects.requireNonNull(traceId, "traceId");
errorLabels = Set.copyOf(errorLabels);
if (attempt < 1) {
throw new IllegalArgumentException("attempt must be at least 1");
}
if (elapsed.isNegative()) {
throw new IllegalArgumentException("elapsed must not be negative");
}
}
/**
* The commit-unknown context of design §14.3.
*
exit=0
$ grep -n 'isAmbiguous\|forbidsBlindReplay' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoExecutionOutcome.java
45: public boolean isAmbiguous() {
50: public boolean forbidsBlindReplay() {
51: return isAmbiguous() || this == PARTIAL_BULK_WRITE;
exit=0
=== 8.2b the no-cause rule: the root's claim, the one constructor that breaks it, its call site, and the test ===
$ grep -n 'no constructor accepts' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoPersistenceException.java
11: * query parameter into an exception message. Second, no constructor accepts a {@link Throwable}
exit=0
$ grep -n 'initCause' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTimeoutException.java
27: initCause(cause);
exit=0
$ git grep -n 'MongoTimeoutException(' -- 'adapter/outbound/persistence-mongo/src/main'
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTimeoutException.java:16: public MongoTimeoutException(MongoFailureContext failureContext) {
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoTimeoutException.java:25: public MongoTimeoutException(MongoFailureContext failureContext, Throwable cause) {
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/DefaultMongoFailureTranslator.java:104: case TIMEOUT -> new MongoTimeoutException(failureContext);
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/reactive/DefaultReactiveMongoExecutor.java:152: new dev.caskeleton.adapter.outbound.mongo.api.error.MongoTimeoutException(
exit=0
$ grep -n 'exceptionsDoNotExposeADriverCause' -A 10 adapter/outbound/persistence-mongo/src/test/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoFailureContextTest.java
58: void exceptionsDoNotExposeADriverCause() {
59- MongoPersistenceException exception =
60- new MongoTransactionCommitUnknownException(
61- MongoFailureContext.commitUnknown(
62- new MongoOperationName("order.reserve"), "251", Duration.ZERO),
63- "read the transaction record");
64-
65- assertThat(exception.getCause()).isNull();
66- assertThat(exception.category()).isEqualTo(MongoFailureCategory.TRANSACTION_COMMIT_UNKNOWN);
67- }
68-}
exit=0
=== 8.2c schema-version failure: dedicated category, and what the two construction sites attach ===
$ grep -n 'SCHEMA_VERSION_UNSUPPORTED' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoFailureCategory.java
68: SCHEMA_VERSION_UNSUPPORTED,
exit=0
$ git grep -n 'new MongoDataSchemaUnsupportedException(' -- 'adapter/outbound/persistence-mongo/src/main'
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/schema/MongoSchemaVersionPolicy.java:85: return new MongoDataSchemaUnsupportedException(
adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/DefaultMongoFailureTranslator.java:111: new MongoDataSchemaUnsupportedException(failureContext, -1, -1, -1);
exit=0
$ sed -n '80,90p' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/schema/MongoSchemaVersionPolicy.java
}
private MongoDataSchemaUnsupportedException unsupported(DocumentSchemaVersion version) {
MongoFailureContext context =
MongoFailureContext.rejected(new MongoOperationName("schema.version-check"));
return new MongoDataSchemaUnsupportedException(
context, version.value(), range.minimumSupported().value(), range.current().value());
}
}
exit=0
$ grep -n 'rejected(' -A 14 adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoFailureContext.java | grep -n 'OPERATION_REJECTED\|NOT_SENT'
6:140- MongoFailureCategory.OPERATION_REJECTED,
7:141- MongoExecutionOutcome.NOT_SENT,
exit=0
$ sed -n '104,112p' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/DefaultMongoFailureTranslator.java
case TIMEOUT -> new MongoTimeoutException(failureContext);
case CURSOR -> new MongoCursorException(failureContext);
case DOCUMENT_TOO_LARGE -> new MongoDocumentTooLargeException(failureContext, -1L, -1L);
case SHARD_ROUTING -> new MongoShardRoutingException(failureContext);
case RESUME -> new MongoResumeException(failureContext);
case ENCRYPTION -> new MongoEncryptionException(failureContext);
case SCHEMA_VERSION_UNSUPPORTED ->
new MongoDataSchemaUnsupportedException(failureContext, -1, -1, -1);
// Bulk partial failure and local rejection are raised by the layers that own their detail —
exit=0
=== 8.3 duplicate mechanism: the two profile-name records ===
$ diff <(sed 's/Database/X/g;s/database/x/g' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/DatabaseProfileName.java) <(sed 's/Collection/X/g;s/collection/x/g' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/CollectionProfileName.java)
6c6
< * Registered logical name of a x profile (design §7.1, §28).
---
> * Registered logical name of a x profile (design §7.1, §16.2).
8,12c8,11
< * <p>A profile name selects a configured connection, credential, consistency default and timeout
< * set. It is never the physical x name supplied by a caller: allowing that would turn a
< * request value into a routing decision and into a metric tag. The pattern therefore rejects
< * slashes, whitespace and generated-identifier shapes, which is what {@code Dynamic collection
< * profile} in the design's startup failure list means in practice.
---
> * <p>Every operation resolves its guardrails — field allowlist, operator allowlist, budget, index
> * manifest, TTL policy — through this name. Accepting a caller-supplied x string instead
> * would defeat all of them at once, so the same dynamic-value rejection as {@link
> * DatabaseProfileName} applies here.
18d16
< /** A value that looks like a generated identifier is dynamic input, not a registered profile. */
exit=1
=== 8.3b duplicate mechanism: retry semantics carried twice ===
$ grep -n 'boolean retryable' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoFailureContext.java
26: boolean retryable,
exit=0
$ grep -n 'Encoding that as a scope rather than' -B 2 -A 3 adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/error/MongoRetryScope.java
6- * <p>The design's central retry rule is that {@code TransientTransactionError} and {@code
7- * UnknownTransactionCommitResult} demand opposite responses: the first replays the whole body from
8: * a new session, the second must never replay the body at all. Encoding that as a scope rather than
9- * a {@code retryable} boolean is what stops the two from collapsing into one flag at the call site.
10- */
11-public enum MongoRetryScope {
exit=0
$ grep -n 'retryScope\|bodyReplayAllowed' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/MongoFailureClassification.java
16: MongoFailureCategory category, MongoExecutionOutcome outcome, MongoRetryScope retryScope) {
21: Objects.requireNonNull(retryScope, "retryScope");
22: if (retryScope == MongoRetryScope.COMMIT_ONLY
75: public boolean bodyReplayAllowed() {
76: return retryScope == MongoRetryScope.WHOLE_OPERATION
77: || retryScope == MongoRetryScope.WHOLE_TRANSACTION;
exit=0
$ grep -n 'classification.bodyReplayAllowed()\|classification.ambiguous()' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/failure/DefaultMongoFailureTranslator.java
83: classification.bodyReplayAllowed(),
84: classification.ambiguous(),
exit=0
=== 8.3c the placeholder profile identity shares the value space of a real one ===
$ grep -n 'UNSPECIFIED' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/MongoOperationScope.java
18: public static final String UNSPECIFIED = "unspecified";
37: new DatabaseProfileName(UNSPECIFIED),
38: new CollectionProfileName(UNSPECIFIED));
43: return !UNSPECIFIED.equals(databaseProfile.value())
44: && !UNSPECIFIED.equals(collectionProfile.value());
exit=0
$ grep -n 'FORMAT' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/DatabaseProfileName.java
16: private static final Pattern FORMAT = Pattern.compile("[a-z][a-z0-9-]{2,63}");
23: if (value == null || !FORMAT.matcher(value).matches() || UUID_LIKE.matcher(value).matches()) {
exit=0
=== 8.4 consistency descriptor: which incoherent pairings the record refuses ===
$ sed -n '28,42p' adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/consistency/MongoConsistencyDescriptor.java
Objects.requireNonNull(profile, "profile");
Objects.requireNonNull(readPreference, "readPreference");
Objects.requireNonNull(readConcern, "readConcern");
Objects.requireNonNull(writeConcern, "writeConcern");
Objects.requireNonNull(guarantee, "guarantee");
if (requiresCausalSession && !"majority".equals(readConcern)) {
throw new IllegalArgumentException(
"a causal session profile requires majority read concern: " + profile);
}
if (requiresCausalSession && !"majority".equals(writeConcern)) {
throw new IllegalArgumentException(
"a causal session profile requires majority write concern: " + profile);
}
}
exit=0
$ grep -n 'has not chosen durability' -B 3 -A 1 adapter/outbound/persistence-mongo/src/main/java/dev/caskeleton/adapter/outbound/mongo/api/consistency/MongoConsistencyProfile.java
5- *
6- * <p>These are profiles rather than three independent knobs because read preference, read concern
7- * and write concern only mean something together. A caller that picks {@code majority} write
8: * concern and {@code secondaryPreferred} reads has not chosen durability, it has chosen a bug.
9- *
exit=0