710 lines
38 KiB
Markdown
710 lines
38 KiB
Markdown
# Wave 0 — Red Baseline Evidence
|
|
|
|
- Repository HEAD at capture: `2f5d2fc21954286213c1474d19935f571ef896ea`
|
|
- Captured: 2026-08-15
|
|
- Toolchain: JDK 21.0.11, Gradle 9.0.0, Docker Engine 29.7.2, Docker Compose 5.4.0
|
|
- Plan: [`2026-08-15-wave0-red-baseline.md`](../2026-08-15-wave0-red-baseline.md)
|
|
|
|
## Exit state
|
|
|
|
```
|
|
$ cd src && ./gradlew wave0RedReport --console=plain --no-daemon
|
|
BUILD SUCCESSFUL in 1m 7s
|
|
```
|
|
|
|
**12 red, 1 unexpectedly green.** The red set matches the plan's expected table except for
|
|
`StartupWarningZeroTest`, recorded as a deviation below.
|
|
|
|
| Red test | Closed by | Confirmed cause |
|
|
| --- | --- | --- |
|
|
| `SecretLeakScannerCharacterizationTest.methodCallWithSafeSuffixIsNotALeak` | Wave 2 C1 | safe-suffix `$` anchor cannot match past a captured `()` |
|
|
| `SecretLeakScannerCharacterizationTest.numericFencingIsNotALeak` | Wave 2 C1 | every `+` read as string concatenation |
|
|
| `FiveAdapterOffInventoryTest.jpaOffHoldsNothing` | Wave 1 T4/T9 | see JPA inventory below |
|
|
| `FiveAdapterOffInventoryTest.messagingOffHoldsNothing` | Wave 1 T7 | see messaging inventory below |
|
|
| `FiveAdapterOffInventoryTest.notificationOffHoldsNothing` | Wave 1 T3/T8 | see notification inventory below |
|
|
| `ShippedRuntimeFacadePresenceTest.mongoFacadeIsShipped` | Wave 1 T5 | `ClassNotFoundException` — not on the runtime classpath |
|
|
| `ShippedRuntimeFacadePresenceTest.graphQlFacadeIsShipped` | Wave 1 T6 | `ClassNotFoundException` |
|
|
| `ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped` | Wave 2 C3 | `ClassNotFoundException` |
|
|
| `DefaultProfileBootCharacterizationTest.localProfileStartsWithShippedDefaults` | Wave 1 T9 | relay-enabled with blank broker |
|
|
| `DefaultProfileBootCharacterizationTest.devProfileStartsWithShippedDefaults` | Wave 1 T9 / Wave 3 T2 | same validator reached first |
|
|
| `ComposeMergeCharacterizationTest.devStackRenders` | Wave 3 T3 | duplicate `/var/tmp/heap` mount target |
|
|
| `ReleaseManifestTaskExistenceTest.mongoReleaseContractNamesOnlyRegisteredTasks` | Wave 2 B5 | three unregistered tasks |
|
|
|
|
Green as planned: `StartupWarningRecorderTest`, `RuntimeMembershipClasspathAgreementTest`,
|
|
`ComposeMergeCharacterizationTest` base/local, `ShippedRuntimeFacadePresenceTest` JPA/notification,
|
|
`FiveAdapterOffInventoryTest` Mongo/GraphQL (vacuously — see Task 4), the three non-red scanner cases,
|
|
`DefaultProfileBootCharacterizationTest.prodProfileRefusesPlaintextJdbc`.
|
|
|
|
## Task 1 — secret scanner
|
|
|
|
```
|
|
$ ./gradlew :messaging:messaging-observability:test --tests '*SecretLeakScannerCharacterizationTest*'
|
|
SecretLeakScannerCharacterizationTest > RED: incrementing a fencing token is arithmetic, not concatenation FAILED
|
|
SecretLeakScannerCharacterizationTest > RED: a method call whose name ends in a safe suffix is not a leak FAILED
|
|
5 tests completed, 2 failed
|
|
```
|
|
|
|
The pre-existing repository failure this characterizes:
|
|
|
|
```
|
|
$ ./gradlew :messaging:messaging-observability:test --tests '*SecretLeakStaticScanTest*'
|
|
SecretLeakStaticScanTest > noSensitiveIdentifierIsConcatenatedIntoAString() FAILED
|
|
java.lang.AssertionError: [a concatenated secret never reaches the redactor, so it must not be written at all]
|
|
Expecting empty but was: ["KafkaSecurityConfigurer.java:104 + oauth.credentialId());",
|
|
"InMemoryAdminOperationJournal.java:110 existing.leaseToken() + 1,"]
|
|
```
|
|
|
|
## Task 3 — off-state inventories
|
|
|
|
Captured from the failure messages, with all five switches off on `local`. These are the exact type
|
|
lists Wave 1 works down.
|
|
|
|
**JPA off** — a connection pool, the entity/repository scan, and the H2 vendor configuration all
|
|
exist:
|
|
|
|
```
|
|
com.zaxxer.hikari.HikariDataSource
|
|
dev.caskeleton.adapter.outbound.persistence.audit.DomainContextAuditContextPort
|
|
dev.caskeleton.adapter.outbound.persistence.config.PersistenceJpaConfig
|
|
dev.caskeleton.adapter.outbound.persistence.config.PersistenceVendorSettings
|
|
dev.caskeleton.adapter.outbound.persistence.failure.PersistenceExceptionTranslator
|
|
dev.caskeleton.adapter.outbound.persistence.failure.StandardSqlStateErrorMapping
|
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileTransitionRepository
|
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverCleanupRepository
|
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverQuotaRepository
|
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.FileserverRecoveryRepository
|
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.JpaFileRepository
|
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.JpaUploadSessionRepository
|
|
dev.caskeleton.adapter.outbound.persistence.fileserver.repository.UploadLeaseRepository
|
|
dev.caskeleton.adapter.outbound.persistence.h2.H2IdempotencyClaimRepository
|
|
dev.caskeleton.adapter.outbound.persistence.h2.H2LocalTimeoutConfigurer
|
|
dev.caskeleton.adapter.outbound.persistence.h2.H2OutboxClaimRepository
|
|
dev.caskeleton.adapter.outbound.persistence.h2.H2PersistenceConfig
|
|
dev.caskeleton.adapter.outbound.persistence.h2.H2SqlStateErrorMapping
|
|
(list truncated in the assertion message)
|
|
```
|
|
|
|
Confirms JPA-INT-001 and the JPA half of §4.3's consumer table: the Fileserver repositories are on
|
|
this list, which is why Wave 1 Task 4's `DataSourceRequirement` must name Fileserver as a relational
|
|
consumer rather than treating the pool as JPA's alone.
|
|
|
|
**Messaging off**:
|
|
|
|
```
|
|
dev.caskeleton.adapter.outbound.messaging.MessagingConfig
|
|
dev.caskeleton.adapter.outbound.messaging.MessagingSettings
|
|
dev.caskeleton.adapter.outbound.messaging.core.DisabledMessagePublisher
|
|
dev.caskeleton.adapter.outbound.messaging.kafka.KafkaAdapterConfig
|
|
dev.caskeleton.adapter.outbound.messaging.kafka.KafkaAdapterSettings
|
|
dev.caskeleton.adapter.outbound.messaging.outbox.DisabledOutboxMessagePublisher
|
|
dev.caskeleton.adapter.outbound.messaging.outbox.Slf4jOutboxRelayFailureReportAdapter
|
|
```
|
|
|
|
Note `DisabledMessagePublisher` and `DisabledOutboxMessagePublisher`: the sentinel behaviour is
|
|
correct, but off-invariant item 9 requires the **composition root** to supply it rather than the
|
|
adapter. Wave 1 Task 7 moves it.
|
|
|
|
**Notification off** — settings bind with the master off, which is NTF-INT-005 exactly:
|
|
|
|
```
|
|
dev.caskeleton.adapter.outbound.notification.NotificationConfig
|
|
dev.caskeleton.adapter.outbound.notification.NotificationRoutesSettings
|
|
dev.caskeleton.adapter.outbound.notification.core.RoutingNotifier
|
|
dev.caskeleton.adapter.outbound.notification.email.google.GoogleEmailNotificationAdapterConfig
|
|
dev.caskeleton.adapter.outbound.notification.platform.autoconfigure.NotificationPlatformSettings
|
|
dev.caskeleton.adapter.outbound.notification.slack.webhook.SlackNotificationAdapterConfig
|
|
dev.caskeleton.bootstrap.notification.NotificationPlatformSecretsConfig$NotificationSecretsSettings
|
|
```
|
|
|
|
## Task 5 — default-profile boot
|
|
|
|
Both `local` and `dev` fail on the same validator, with the message it was written to give:
|
|
|
|
```
|
|
org.springframework.beans.factory.BeanCreationException: Error creating bean with name
|
|
'outboxRelayBrokerRequirementValidator' defined in class path resource
|
|
[dev/caskeleton/bootstrap/outbox/OutboxConfig.class]: ca-skeleton.outbox.relay-enabled is enabled
|
|
but app.messaging.broker is blank, so every claimed outbox row would fail to publish and be
|
|
exhausted to DEAD. Either configure a broker, or set ca-skeleton.outbox.relay-enabled=false so
|
|
PENDING rows are preserved until one exists.
|
|
```
|
|
|
|
`dev` reaches this validator before the `ddl-auto=update` conflict the spec recorded from `bootRun`,
|
|
so the `ddl-auto` failure is currently **masked**. It will surface once Wave 1 Task 9 sets
|
|
`relay-enabled: false`, and Wave 3 Task 2 closes it.
|
|
|
|
## Task 7 — Compose merge
|
|
|
|
```
|
|
$ docker compose -f docker-compose.yml -f docker-compose.dev.yml config
|
|
services.app.volumes[1]: target /var/tmp/heap already mounted as services.app.tmpfs[1]
|
|
```
|
|
|
|
Base and local render cleanly (`app`, and `app,db` respectively).
|
|
|
|
## Task 8 — runtime project closure
|
|
|
|
```
|
|
$ ./gradlew :app-bootstrap:runtimeClasspathManifest
|
|
$ cat app-bootstrap/build/architecture/runtime-project-closure.txt
|
|
```
|
|
|
|
The manifest resolves and `RuntimeMembershipClasspathAgreementTest` is **green** — direct
|
|
dependencies and the resolved closure agree today, because no build-only leaf is reachable. This is
|
|
the gate that must stay green while Waves 1 and 2 add the Mongo, GraphQL, and messaging edges.
|
|
|
|
## Task 9 — release manifest
|
|
|
|
```
|
|
ReleaseManifestTaskExistenceTest FAILED
|
|
the Mongo release contract names task(s) that no build file registers, so a release manifest can
|
|
report them green without ever running them
|
|
missing: [mongoAtlasTest, mongoKmsTest, mongoShardedTest]
|
|
registered: [mongoReplicaSetTest, mongoFailoverTest, mongoMigrationTest, mongoCompatibilityTest,
|
|
mongoSecurityIntegrationTest, mongoPerformanceTest, mongoStableContractTest]
|
|
```
|
|
|
|
`scripts/verify-mongodb-advanced.sh:95` also invokes `mongoShardedTest`, so that script cannot
|
|
currently succeed either.
|
|
|
|
---
|
|
|
|
## Deviations from the plan
|
|
|
|
### 1. `StartupWarningZeroTest` is green, not red
|
|
|
|
The plan expected this red. It passes: an **all-off, non-web** `local` startup emits zero WARN and
|
|
zero ERROR.
|
|
|
|
That is not a contradiction of spec §9.1 — the warnings recorded there
|
|
(`BeanPostProcessorChecker` on the authorization beans, two Micrometer late-`MeterFilter` warnings,
|
|
a Flyway converter warning on `dev`) were observed during `bootRun`, which is a **web** application
|
|
with JPA active. This test is narrower than the configuration that produces them.
|
|
|
|
Consequence for Wave 4: `StartupWarningZeroTest` as written does not yet measure the warnings Wave 4
|
|
must remove. Wave 4 Task 6 already calls for extending it to all three environments and every one-on
|
|
combination; that extension must also use `WebApplicationType.SERVLET`, or the gate will keep
|
|
passing while the warnings remain. Recorded here so the omission is not discovered as a surprise.
|
|
|
|
### 2. A test-harness defect surfaced first, and is not a production defect
|
|
|
|
Booting `CaSkeletonApplication` in-JVM from `app-bootstrap`'s own test source set fails before any
|
|
adapter is examined:
|
|
|
|
```
|
|
BeanDefinitionOverrideException: Invalid bean definition with name 'outboxEventJpaRepository'
|
|
defined in ... @EnableJpaRepositories declared on PersistenceJpaConfig: ... there is already
|
|
[...] defined in ... @EnableJpaRepositories declared on OutboxContainerTestSupport.OutboxRepositoryConfig
|
|
```
|
|
|
|
Cause: `CaSkeletonApplication` component-scans `dev.caskeleton.bootstrap`; this module's test sources
|
|
live in that package; so an in-JVM boot discovers a **test-only** configuration
|
|
(`OutboxContainerTestSupport.OutboxRepositoryConfig`) that the shipped jar has never contained.
|
|
|
|
This is a property of the measurement, not of the product — `bootRun` is unaffected. Left alone it
|
|
would have reported the same cause for every activation characterization and hidden the defects they
|
|
exist to name. `ShippedCompositionHarness` registers a `TypeExcludeFilter` that drops candidates
|
|
whose class file came from a test output directory, which is a rule about provenance rather than a
|
|
class-name list somebody has to maintain.
|
|
|
|
**This is a workaround for measuring, not a fix.** The faithful instrument is running the produced
|
|
jar as a child process, which is what Wave 2 Task E3 builds for GraphQL. If Wave 1's off-invariant
|
|
work needs stronger evidence than the harness can give, promote the activation tests to that shape
|
|
rather than trusting the exclusion.
|
|
|
|
### 3. A property-precedence trap worth remembering
|
|
|
|
`SpringApplicationBuilder#properties(String...)` contributes to `defaultProperties`, the
|
|
lowest-precedence source, so the all-off set lost to `application.yml`'s `relay-enabled: true` and
|
|
the "all-off" context died on the relay validator. The harness passes `--key=value` command-line
|
|
arguments instead. Any later test that sets an all-off baseline must do the same.
|
|
|
|
## Files added by Wave 0
|
|
|
|
Production sources changed: **none**. Verified by inspection — every path below is a test source or
|
|
a Gradle lane registration.
|
|
|
|
| File | Kind |
|
|
| --- | --- |
|
|
| `src/messaging/messaging-observability/src/test/java/.../SecretLeakScannerCharacterizationTest.java` | test |
|
|
| `src/app-bootstrap/src/test/java/.../activation/AdapterActivationInventory.java` | test fixture |
|
|
| `src/app-bootstrap/src/test/java/.../activation/ShippedCompositionHarness.java` | test fixture |
|
|
| `src/app-bootstrap/src/test/java/.../activation/FiveAdapterOffInventoryTest.java` | test |
|
|
| `src/app-bootstrap/src/test/java/.../activation/ShippedRuntimeFacadePresenceTest.java` | test |
|
|
| `src/app-bootstrap/src/test/java/.../activation/DefaultProfileBootCharacterizationTest.java` | test |
|
|
| `src/app-bootstrap/src/test/java/.../activation/StartupWarningRecorder.java` | test fixture |
|
|
| `src/app-bootstrap/src/test/java/.../activation/StartupWarningRecorderTest.java` | test |
|
|
| `src/app-bootstrap/src/test/java/.../activation/StartupWarningZeroTest.java` | test |
|
|
| `src/app-bootstrap/src/test/java/.../compose/ComposeMergeCharacterizationTest.java` | test |
|
|
| `src/app-bootstrap/src/test/java/.../registry/RuntimeMembershipClasspathAgreementTest.java` | test |
|
|
| `src/app-bootstrap/src/test/java/.../registry/ReleaseManifestTaskExistenceTest.java` | test |
|
|
| `src/app-bootstrap/build.gradle` | lane registration (`runtimeClasspathManifest`, `wave0Red`) |
|
|
| `src/messaging/messaging-observability/build.gradle` | lane registration (`wave0Red`) |
|
|
| `src/build.gradle` | lane registration (`wave0RedReport`) |
|
|
|
|
---
|
|
|
|
# Wave 1 progress note (same session)
|
|
|
|
## The repository-wide `test` is green
|
|
|
|
```
|
|
$ cd src && ./gradlew test --console=plain --no-daemon --continue
|
|
BUILD SUCCESSFUL in 3m 20s
|
|
```
|
|
|
|
It was **red at HEAD** before this work, on `SecretLeakStaticScanTest` (spec §3.1). Two changes made
|
|
it green, and neither is a suppression:
|
|
|
|
1. **The scanner defect is fixed** (Wave 2 C1, pulled forward because it was the only thing keeping
|
|
the build red). A captured method call keeps its trailing `()`, so the safe-suffix exemption's `$`
|
|
anchor never matched — stripping the call fixes it, where unanchoring the pattern would also have
|
|
exempted `credentialIdentity`, which does carry the value. And a `+` with a numeric literal on
|
|
either side is arithmetic, which cannot put a secret into a string. The three true-positive cases
|
|
in the characterization are what prove the fix was not a weakening.
|
|
2. **Wave 0's deliberately-red characterizations are excluded from the ordinary `test` task** and
|
|
reported by `wave0RedReport` instead. A permanently red `test` is a build nobody can use, and a
|
|
gate nobody can use stops catching the regressions it exists for. Wave 6 requires the tag set to
|
|
be empty, so the exclusion cannot quietly become forgetting.
|
|
|
|
## Remaining red set: 4
|
|
|
|
| Red | Closed by |
|
|
| --- | --- |
|
|
| `ShippedRuntimeFacadePresenceTest.graphQlFacadeIsShipped` | Wave 1 T6 |
|
|
| `ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped` | Wave 2 C3 |
|
|
| `ComposeMergeCharacterizationTest.devStackRenders` | Wave 3 T3 |
|
|
| `ReleaseManifestTaskExistenceTest.mongoReleaseContractNamesOnlyRegisteredTasks` | Wave 2 B5 |
|
|
|
|
Down from 12 at the Wave 0 baseline.
|
|
|
|
## Known defect left as found: the shared-contract scope rule
|
|
|
|
`CleanArchitectureTest.SHARED_CONTRACT_CONTAINS_ONLY_OPERATIONAL_CONTRACT_PACKAGES` has two faults
|
|
that mask each other:
|
|
|
|
- its subject pattern is `..shared..`, which matches any package named `shared` anywhere rather than
|
|
the shared-contract module;
|
|
- its allowlist contains the bare module root `dev.caskeleton.shared` and the check matches by
|
|
prefix, so **every** package inside the module is blessed automatically.
|
|
|
|
Net effect: the rule catches nothing inside the module it is named after, and does catch unrelated
|
|
leaves that happen to have a `shared` package — which is how it surfaced, when the Mongo leaf joined
|
|
the composition root's analysis scope.
|
|
|
|
Fixing it properly requires the violation fixture that proves the rule works to move inside
|
|
`dev.caskeleton.shared`, which then puts the fixture in the rule's own analysis scope, which requires
|
|
reworking how fixtures are scanned. That is a separate change from adapter activation, so the rule is
|
|
left as found with the defect documented at the rule itself, and the Mongo tenancy package added to
|
|
the allowlist to keep the build honest in the meantime. **This is technical debt, not a fix.**
|
|
|
|
## Corrections to the spec and to these plans, found by executing them
|
|
|
|
| Claim | Reality |
|
|
| --- | --- |
|
|
| `ca-skeleton.idempotency.provider` | The property does not exist. It is `ca-skeleton.capabilities.idempotency.provider`. |
|
|
| `app.fileserver.enabled` | It is `app.fileserver-platform.enabled`. |
|
|
| Notification/GraphQL selectors are legacy master aliases | They are subordinate settings that stay valid while the adapter is on. Treating them as aliases made every shipped configuration ambiguous. |
|
|
| `AutoConfigurationImportFilter` registers in `.imports` | It registers in `META-INF/spring.factories`. Getting this wrong fails open silently — the filter simply never runs. |
|
|
| The JPA root can import one exported entry | Inverting the vendor→config import to create one produced a package cycle. The composition root names both vendor configs instead, and the export surface admits them with that reason recorded. |
|
|
|
|
## Shipping Mongo pulled in reactive Mongo
|
|
|
|
Adding the Mongo leaf to `app-bootstrap` put `spring-boot-starter-data-mongodb-reactive` and
|
|
`mongodb-driver-reactivestreams` on the runtime classpath. The index's scope boundaries exclude
|
|
reactive Mongo from the shipped Stable runtime, and leaving the starter there would have let Boot
|
|
build a second client and pool from the same URI as soon as the master switch went on. Both are
|
|
excluded at the composition root rather than in the leaf, which still compiles the reactive paths for
|
|
a future promotion.
|
|
|
|
---
|
|
|
|
# Session close — Wave 1 complete, Wave 2/3 partially landed
|
|
|
|
## Verified state
|
|
|
|
```
|
|
$ cd src && ./gradlew test --console=plain --no-daemon --continue
|
|
BUILD SUCCESSFUL in 2m 58s
|
|
|
|
$ ./gradlew verifyEnvKeys verifyCleanArchitectureDependencies \
|
|
verifyRuntimeModuleMembership verifyPublicPathSnapshot
|
|
verifyRuntimeModuleMembership: 2 runtime composition(s) match the registry
|
|
BUILD SUCCESSFUL
|
|
|
|
$ ./gradlew wave0RedReport
|
|
1 red remaining
|
|
```
|
|
|
|
**12 red at the Wave 0 baseline → 1.** The survivor is
|
|
`ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped`, which Wave 2 C3 closes: the
|
|
messaging platform has no production sender, only a test fake, so giving it runtime membership now
|
|
would ship a path that cannot work.
|
|
|
|
## Wave 1 — complete (13/13)
|
|
|
|
| Task | Delivered |
|
|
| --- | --- |
|
|
| T1 | `MasterSwitch` / `MasterSwitchParser` in `shared-contract` — five names in one place, strict parse |
|
|
| T2 | `MasterSwitchEnvironmentPostProcessor` — rejects before any detail namespace binds |
|
|
| T3 | Both composition-root scans narrowed; the properties scan names its packages because it has no `excludeFilters` |
|
|
| T4 | `PersistenceJpaRootAutoConfiguration` + `DataSourceRequirement` + `JpaOffAutoConfigurationImportFilter` |
|
|
| T5 | Mongo shipped, one authority, reactive starter excluded from the runtime |
|
|
| T6 | GraphQL shipped, one authority, Boot GraphQL auto-configurations filtered |
|
|
| T7 | Messaging bridge gated; disabled sentinels moved to the composition root |
|
|
| T8 | `NotificationRootAutoConfiguration` — secrets and registries named rather than scanned |
|
|
| T9 | Migration, outbox and idempotency under capability roots |
|
|
| T10 | `CapabilityDependencyValidator` — 8 rules, each naming the exact missing switch |
|
|
| T11 | `DatabaseReadinessGroupPostProcessor` — `db` membership derived from the capability closure |
|
|
| T12 | 8 env registry rows + YAML binding + a contract test derived from the enum |
|
|
| T13 | Membership gate reads the resolved runtime closure instead of declared dependencies |
|
|
|
|
Measured effect, JPA off: **~30 beans → 0** (pool, entity scan, repositories, Hibernate, Flyway, DB
|
|
health all gone).
|
|
|
|
## Pulled forward from later waves
|
|
|
|
- **Wave 2 C1** — the secret scanner's two false positives fixed at the cause. This is what made a
|
|
repository-wide green `test` possible; it had been red at HEAD.
|
|
- **Wave 2 B5** — the three ghost Mongo lanes demoted to `experimental_contracts` rather than
|
|
implemented, with the script and doc updated to match.
|
|
- **Wave 3 T1** — profileless deploys refused, scoped to the deployable artifact so slice tests are
|
|
unaffected.
|
|
- **Wave 3 T3** — the dev Compose `tmpfs: !override []` fix; merge verified and mount targets checked
|
|
for uniqueness in the merged model.
|
|
|
|
## The defaulting cascade, and what it cost
|
|
|
|
Removing the relay's blanket refusal exposed the failure underneath it, exactly as this document
|
|
predicted — and then that one exposed a third. The sequence was:
|
|
|
|
1. relay-enabled with a blank broker (fixed in T9);
|
|
2. `ddl-auto=update` against a Flyway-owned schema (fixed by changing the tracked `.env`);
|
|
3. `logging.level.root` failing to bind, because **62 placeholders in `application.yml` had no inline
|
|
default at all** and only `application-local.yml` pinned enough of them for one profile to start.
|
|
|
|
55 of those 62 now carry a default. The remaining seven are deliberate: the datasource URL, username
|
|
and password, the application name, and the JWT issuer and audience — a default for any of them is a
|
|
deployment running against something nobody chose. CORS allowed-origins was moved out of that set
|
|
after the fact: CORS is off by default and an empty origin list is the safest value rather than an
|
|
arbitrary one, so it defaults to empty.
|
|
|
|
One of the added defaults was itself wrong — `max-age-seconds` got `600s` while the field is a
|
|
`long` — which is worth recording because it only surfaced through a real boot. A bulk defaulting
|
|
pass needs a boot per profile to be believed.
|
|
|
|
## Still open
|
|
|
|
- `ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped` (Wave 2 C3).
|
|
- The `..shared..` ArchUnit rule remains defective and documented at the rule itself; see the earlier
|
|
note. Unchanged this session.
|
|
- Waves 2 (remaining), 3 (Compose lane matrix, Keycloak, MinIO), 4, 5 and 6 are untouched.
|
|
|
|
---
|
|
|
|
# Continuation — Wave 3 T2 and Wave 4 T1 landed; a Wave 4 fix disproved
|
|
|
|
## Verified state
|
|
|
|
```
|
|
$ cd src && ./gradlew test BUILD SUCCESSFUL
|
|
$ ./gradlew verifyEnvKeys verifyCleanArchitectureDependencies \
|
|
verifyRuntimeModuleMembership verifyPublicPathSnapshot
|
|
BUILD SUCCESSFUL
|
|
$ ./gradlew wave0RedReport 2 red
|
|
```
|
|
|
|
## Wave 3 T2 — env source separation, complete
|
|
|
|
`src/.env` is untracked. `src/.env.example` (309 keys, generated from the registry, 12
|
|
secret-classified keys left empty) and `src/.env.local.example` are tracked in its place, and
|
|
`.gitignore` carries `src/.env*` with the two examples negated.
|
|
|
|
`verifyEnvKeys` now reads the example. Reading the real file made it false in **both** directions: it
|
|
passed only where an operator's own environment file happened to exist, and it would have passed with
|
|
no example at all — so the file an adopter actually copies was never verified, while a file full of
|
|
real credentials was a build input. Proven by deleting `src/.env` and re-running: green.
|
|
|
|
Its rule B was inverted while retargeting. It required every key in the file to be an
|
|
`application.yml` placeholder, which is true of a hand-maintained `.env` and false of a catalogue —
|
|
most registry keys are bound by typed settings inside a leaf. Inverted to "every registered `APP_` key
|
|
appears in the example", it now catches the drift that actually matters: a key added to the registry
|
|
that never reached the file an adopter copies.
|
|
|
|
`SPRING_PROFILES_ACTIVE` was also corrected in the registry — `type: enum`, `required: true`, no
|
|
default — to match Wave 3 T1.
|
|
|
|
## Wave 4 T1 — MeterFilter ordering, complete
|
|
|
|
`MetricsContractConfig` and `SampleMetricsContractConfig` both install their filters through a
|
|
`MeterRegistryCustomizer` instead of a `@PostConstruct` that fetched the registry. The warning was the
|
|
visible half of the real defect: a filter applies only to meters registered after it, so the
|
|
cardinality and distribution policies were being applied to some meters and not others. Both
|
|
composition roots changed together, because fixing one leaves the warning reproducible from the other.
|
|
|
|
The Boot 4 package is `org.springframework.boot.micrometer.metrics.autoconfigure`, found by inspecting
|
|
the resolved jars rather than assumed.
|
|
|
|
## Wave 4 T4 — attempted, disproved, and left as found
|
|
|
|
The spec calls the structured log's `profile` field a drift: it reads `SPRING_PROFILES_ACTIVE`, so
|
|
overriding the profile on the command line while a stale value sits in the environment stamps lines
|
|
with the stale one. The drift is real.
|
|
|
|
The obvious fix — bind the field to `spring.profiles.active` — **does not work**, and an existing
|
|
contract test said so. `LogProfileDriftCharacterizationTest` was written to settle the question by
|
|
observation rather than argument, and it observed an empty string: Logback initialises before that
|
|
property resolves. A field that says nothing is not an improvement on a field that is sometimes wrong.
|
|
|
|
The binding was reverted to what it was, with the reason recorded at the declaration, and the
|
|
characterization kept as a tagged red. The fix needs a different mechanism — setting the logger
|
|
context property from the resolved environment once it is ready, rather than declaring the source in
|
|
XML — which is Wave 4's to build.
|
|
|
|
## Remaining red: 2
|
|
|
|
| Red | Closed by |
|
|
| --- | --- |
|
|
| `ShippedRuntimeFacadePresenceTest.messagingPlatformFacadeIsShipped` | Wave 2 C3 — no production sender exists, only a test fake |
|
|
| `LogProfileDriftCharacterizationTest` | Wave 4 — needs the mechanism above |
|
|
|
|
---
|
|
|
|
# Continuation — Wave 3 Compose lane matrix (static half) and Keycloak realm
|
|
|
|
## Verified state
|
|
|
|
```
|
|
$ cd src && ./gradlew test BUILD SUCCESSFUL
|
|
$ ./gradlew verifyEnvKeys verifyCleanArchitectureDependencies \
|
|
verifyRuntimeModuleMembership verifyPublicPathSnapshot BUILD SUCCESSFUL
|
|
$ ./scripts/verify-compose-profile-contracts.sh
|
|
all 15 lanes match src/config/runtime/compose-profile-contracts.json
|
|
$ ./gradlew wave0RedReport 2 red
|
|
```
|
|
|
|
## What landed
|
|
|
|
**`src/config/runtime/compose-profile-contracts.json`** — the lane SSOT. 15 lanes, each fixing a
|
|
Compose profile, a file stack, an explicit Spring runtime, and the exact service set that stack must
|
|
render. Exact rather than superset: a lane that quietly gains a service is a lane whose evidence
|
|
describes a different stack than the one that ran.
|
|
|
|
**`docker-compose.infra.yml`** — every shared service, each carrying Compose profiles so nothing
|
|
starts unless a lane names it: PostgreSQL, a single-node Mongo replica set with an idempotent
|
|
initiator, Kafka, Mailpit, MinIO with bucket bootstrap, Keycloak, and the three one-shot smoke
|
|
clients. Infrastructure no longer lives inside environment overlays, which is what let `local` stop
|
|
meaning "the app plus a database".
|
|
|
|
**`docker-compose.prod-smoke.yml`** — a production-shaped runtime whose JDBC URL carries
|
|
`sslmode=verify-full`, so the prod validators are satisfied rather than bypassed.
|
|
|
|
**`scripts/verify-compose-profile-contracts.sh`** — the static entry point, wired into the ordinary
|
|
test suite so a lane cannot drift until somebody remembers to run a shell script. It checks the
|
|
Compose version floor, the exact service set per lane, the rendered `SPRING_PROFILES_ACTIVE`, and
|
|
mount-target uniqueness in the merged model.
|
|
|
|
**Keycloak realm** (`infra/keycloak/`) — `ca-skeleton-api` as a confidential client with a service
|
|
account, standard flow and direct access grant off, an audience mapper and realm/client role mappers.
|
|
The client secret is a `${...}` reference; the entrypoint reads it from a mounted secret file and
|
|
execs `kc.sh`, so no value reaches Git, the rendered config, or `docker inspect`.
|
|
|
|
**Smoke clients** — `auth-smoke` (the seven realm checks, against the same issuer URL the app is
|
|
given), `object-storage-smoke` (upload → HEAD → download → delete → wrong-credential rejection, none
|
|
skippable), `notification-smoke` (three phases, so the handoff lane's accept and verify are the same
|
|
client talking about the same request id).
|
|
|
|
## What the verifier caught immediately
|
|
|
|
Writing it was worth it before running anything. On first execution it failed four lanes:
|
|
|
|
- `off-local` rendered `app,db`, because the `db` service was still in the local overlay as well as
|
|
in the new infra file;
|
|
- three lanes could not render at all, because the local overlay's `depends_on: db` pointed at a
|
|
service their profile does not enable — Compose refuses that outright.
|
|
|
|
Both are the same mistake: infrastructure declared where the environment is described. `db` now lives
|
|
only in the infra file, and the `depends_on` is gone — ordering belongs to the runtime-smoke wrapper,
|
|
which knows which services a lane actually starts.
|
|
|
|
## Two follow-on defects found and fixed
|
|
|
|
**Untracking `src/.env` broke local Compose on a fresh clone.** `docker compose config` failed
|
|
outright because the local overlay declared `env_file: ./src/.env`. Marked `required: false`, and
|
|
verified by moving the file aside: the stack renders. A convenience override had become a hard
|
|
prerequisite for rendering the stack at all.
|
|
|
|
**The developer host-port contract moved with the service.**
|
|
`DeveloperExperienceContractTest.localComposePublishesTheHostPortTheCommittedDatasourceUrlTargets`
|
|
asserted against the local overlay. It now asserts against the infra file and additionally that the
|
|
service carries the `local-jpa` profile — without which the port assertion would pass for a service
|
|
no lane ever brings up.
|
|
|
|
## Still open in Wave 3
|
|
|
|
`scripts/run-compose-runtime-smoke.sh` — the dynamic half — is not written. Nothing here has been
|
|
started; what is verified is that all 15 lanes render exactly what they claim, with the right Spring
|
|
runtime and no mount collisions. The lanes have not been run, and this document does not claim they
|
|
have.
|
|
|
|
---
|
|
|
|
# Continuation — the Compose lanes actually run
|
|
|
|
## Verified state
|
|
|
|
```
|
|
$ cd src && ./gradlew test verifyEnvKeys verifyCleanArchitectureDependencies \
|
|
verifyRuntimeModuleMembership verifyPublicPathSnapshot BUILD SUCCESSFUL
|
|
$ ./scripts/verify-compose-profile-contracts.sh all 15 lanes match
|
|
$ ./scripts/run-compose-runtime-smoke.sh --lane off-local passed
|
|
$ ./scripts/run-compose-runtime-smoke.sh --lane off-dev passed
|
|
$ ./scripts/run-compose-runtime-smoke.sh --lane off-prod passed
|
|
```
|
|
|
|
Each lane's own report, fetched from the running application rather than asserted from the flags the
|
|
lane passed in:
|
|
|
|
| lane | activeProfile | switches on | dataSourceRequiredBy |
|
|
| --- | --- | --- | --- |
|
|
| `off-local` | `local` | none | not required |
|
|
| `off-dev` | `dev` | none | not required |
|
|
| `off-prod` | `prod` | none | not required |
|
|
|
|
`docker ps -a` and `docker volume ls` show no surviving `casmoke` project or volume.
|
|
|
|
**This is the Wave 1 exit criterion, demonstrated for the first time in a real container:** all five
|
|
adapters off, three environments, no infrastructure of any kind, and the application reporting so
|
|
itself.
|
|
|
|
## What was built
|
|
|
|
`scripts/run-compose-runtime-smoke.sh` — the dynamic entry point. Unique project per lane, evidence
|
|
directory that refuses to reuse a previous run's, static contract then `config` then `create`,
|
|
`up --wait` on long-running services only, a bounded readiness poll, every declared one-shot with a
|
|
non-zero exit failing the lane, sanitized evidence, and a `trap` teardown scoped to the lane's own
|
|
project — logs collected before the teardown, not after.
|
|
|
|
`AdapterActivationEndpoint` / `AdapterActivationReport` — the application's own answer about what
|
|
resolved on. A lane asserting on its own environment passes whenever it set the variables correctly,
|
|
which is not the claim being made.
|
|
|
|
## Six defects the lanes found, in the order they surfaced
|
|
|
|
Each was invisible to every check that existed before, and none would have been found by reading.
|
|
|
|
1. **A stale image.** The app service declares both `build:` and `image:`, so Compose reused a tag
|
|
from an older state of the repository — the first run failed on a class that no longer exists in
|
|
the tree. The wrapper now builds explicitly. A lane running a stale image produces evidence about
|
|
code nobody changed.
|
|
2. **The actuator is on its own connector.** Fetching `8080/actuator` returned an empty file that
|
|
read exactly like a failed assertion about the profile.
|
|
3. **The activation endpoint was authenticated.** Management auth is JWT, so only a lane with an
|
|
identity provider could have read it — excluding the all-off lanes, whose claim is the hardest to
|
|
check any other way. It is now permit-all alongside health/info/prometheus, and
|
|
`AdapterActivationReportShapeTest` holds it to property names and booleans so that stays true.
|
|
`ManagementActuatorSecurityContractTest` records the allowlist decision rather than absorbing it.
|
|
4. **`off-local` was passing by luck.** It read the developer's own `src/.env` for the seven
|
|
deliberately-undefaulted values. The wrapper now generates them per run, so a lane reproduces
|
|
anywhere rather than on the machine it was written on.
|
|
5. **The dev overlay has no healthcheck**, so `up --wait` returned as soon as the container was
|
|
created and the first fetch landed before startup finished. The wrapper polls with a bound rather
|
|
than trusting `--wait` alone.
|
|
6. **`environment:` beat `env_file:` in the prod overlay.** `APP_DATASOURCE_PASSWORD:
|
|
"${APP_DATASOURCE_PASSWORD:-}"` read the host shell, not the lane's generated file, and injected
|
|
an empty string — which the prod env validator then refused, correctly, about a value the lane had
|
|
actually supplied.
|
|
|
|
## Not claimed
|
|
|
|
The twelve infrastructure-bearing lanes have not been run. `--matrix` exists and is untested against
|
|
them; what is demonstrated is the three all-off lanes end to end and that all fifteen render exactly
|
|
what they claim. Keycloak, MinIO, Mongo, Kafka and Mailpit have been written and rendered, not
|
|
started.
|
|
|
|
---
|
|
|
|
# Continuation — the infrastructure lanes, and what running them found
|
|
|
|
## Verified state
|
|
|
|
```
|
|
$ cd src && ./gradlew test verifyEnvKeys verifyCleanArchitectureDependencies \
|
|
verifyRuntimeModuleMembership verifyPublicPathSnapshot BUILD SUCCESSFUL
|
|
$ ./scripts/verify-compose-profile-contracts.sh all 15 lanes match
|
|
$ ./scripts/run-compose-runtime-smoke.sh --matrix src/config/runtime/compose-profile-contracts.json
|
|
all 4 blocking lanes passed
|
|
```
|
|
|
|
No surviving `casmoke` container or volume.
|
|
|
|
| lane | activeProfile | switches on | vendor |
|
|
| --- | --- | --- | --- |
|
|
| `off-local` | local | none | none |
|
|
| `off-dev` | dev | none | none |
|
|
| `off-prod` | prod | none | none |
|
|
| `local-mongo` | local | `persistence-mongo` | none |
|
|
|
|
`local-mongo` is the first adapter proven on against real infrastructure: a single-node replica set,
|
|
the Mongo master switch on, no other switch on, and no relational connection required.
|
|
|
|
## The contract gained two assertions, because a green lane was not yet a meaningful one
|
|
|
|
**`expectedSwitchesOn`.** A lane named `local-jpa` that ran with JPA off would render the right
|
|
services, start cleanly, and prove nothing. The wrapper now compares the switches the application
|
|
reports against what the lane asked for.
|
|
|
|
**`expectedPersistenceVendor`.** `local-jpa` passed for a while against H2 while the PostgreSQL
|
|
container it started sat untouched beside it — `application-local.yml` pinned an in-memory URL as a
|
|
literal, which outranks any environment a caller supplies. Every other field in the report looked
|
|
correct. The report now carries the vendor resolved from the JDBC URL, and the lane asserts it.
|
|
|
|
That pin was not unique. `application-local.yml` also pinned `app.messaging.broker: ""` and the two
|
|
notification provider selectors, so `local-messaging` started Kafka, set `APP_MESSAGING_BROKER=kafka`
|
|
and was refused by the dependency validator for a value it had supplied. All four are placeholders
|
|
now; the defaults are unchanged, so a developer who sets nothing gets exactly what they got before.
|
|
|
|
## Four defects in the wrapper itself
|
|
|
|
1. **It reported success for lanes it never ran.** `docker compose exec` consumes stdin, and inside a
|
|
plain `while read` loop it ate the remaining lanes — the first ran, the loop ended, and the script
|
|
said all six passed. Reading on fd 3 fixes it; a ran-count guard makes a partial matrix a failure
|
|
rather than a pass. A wrapper whose own success message is a false green is worse than no wrapper.
|
|
2. **Compose project names reject uppercase**, so the run id is lowercased for the project while the
|
|
evidence directory keeps the readable timestamp.
|
|
3. **`env_file` lists merge and the later file wins.** The developer's optional `src/.env`, declared
|
|
by the local overlay after the base, silently overrode the lane's own values. Lane settings now go
|
|
into a generated `environment:` overlay, which beats every `env_file` regardless of order.
|
|
4. **`up --wait` is not a readiness gate where no healthcheck exists** — the dev overlay has none, so
|
|
the first fetch landed before startup finished. The wrapper polls with a bound.
|
|
|
|
## Two spec findings confirmed in a real composition, not inferred
|
|
|
|
**MSG-INT-003.** With a healthy Kafka and the broker selected, startup fails on a missing
|
|
`KafkaSender` bean: the legacy Kafka configuration requires a project-supplied sender, and production
|
|
has none — only the tests provide a fake. This is precisely why the messaging platform leaves must
|
|
not get runtime membership before a real transport bridge exists.
|
|
|
|
**GQL-INT-002.** `APP_GRAPHQL_DEPLOYMENT_MODE` is registered and bound, but the platform still reads
|
|
the old `production` boolean and `environment` enum, which default to `false` and `PRODUCTION_PUBLIC`.
|
|
The startup validator therefore sees a production deployment with introspection enabled and refuses.
|
|
|
|
A third was found that the spec did not predict: **shipping GraphQL into the same context as the rest
|
|
of the application produced two `Clock` beans**, because the platform's clock was conditioned on its
|
|
own bean *name* rather than on the type. Every injection point wanting a `Clock` failed to start. It
|
|
now backs off on the type, which is what auto-configuration is for — and this could not have happened
|
|
while the leaf was build-only.
|
|
|
|
## Lanes marked not-blocking, with reasons recorded in the contract
|
|
|
|
`local-jpa`, `local-messaging-outbox`, the three notification lanes, `shared-infra-local`,
|
|
`shared-infra-dev`, `prod-smoke` and `all-adapters` are blocked on an open JPA finding: the entity
|
|
scan is unconditional while the Flyway migration streams are partitioned by capability, so
|
|
`ddl-auto=validate` against real PostgreSQL fails on `fs_cleanup_item`. Scoping the entity scan to
|
|
active capabilities is Wave 2 JPA work.
|
|
|
|
`local-messaging` is blocked on MSG-INT-003 and `local-graphql` on GQL-INT-002, both above.
|
|
|
|
Each carries its reason in `compose-profile-contracts.json` and each keeps its assertions, so the
|
|
lanes fail loudly rather than passing against the wrong thing.
|