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>
218 lines
13 KiB
Plaintext
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
|
|
|