Files
clean-architecture-backend-…/docs/superpowers/plans/evidence/2026-08-15-wave6-final/task3-activation-matrix.md
T

105 lines
6.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Task 3 — the ten-row activation matrix
Every row below names where its evidence is, and nothing is ticked from reasoning about the code.
Rows 17 and 10 are runtime lanes from one Compose matrix run, `20260819T025416Z-200172`; rows 8 and 9
needed work this wave and are described in full.
The per-lane `activation.json` is the application's own answer about which switches are on, not the
flags the harness passed in — which is the point of reading it rather than the lane definition.
| # | Matrix | Evidence | Resolved switches (from `activation.json`) |
| --- | --- | --- | --- |
| 1 | five off | `off-local`, `off-dev`, `off-prod` lanes + `FiveAdapterOffInventoryTest` | `local` / `dev` / `prod`, **none on** |
| 2 | JPA only | `local-jpa` lane | `local`, `persistence-jpa` |
| 3 | Mongo only | `local-mongo` lane | `local`, `persistence-mongo` |
| 4 | Messaging only | `local-messaging` lane | `local`, `messaging` |
| 5 | Notification + JPA, `INGEST_ONLY` | `local-notification-ingest` + `local-notification-handoff` | `local`, `persistence-jpa`+`notification.platform`, `APP_NOTIFICATION_PLATFORM_MODE: INGEST_ONLY` |
| 6 | Notification + JPA, `SERVING` | `local-notification-serving` | same switches, `MODE: SERVING` |
| 7 | GraphQL only | `local-graphql` lane | `local`, `graphql` |
| 8 | relay on, dependency missing | **this wave** — see below | n/a: the deployment is refused |
| 9 | JPA + Mongo | `all-adapters` lane + `PortResolutionContractTest` | `local`, both persistence switches on |
| 10 | five on | `all-adapters` lane | `local`, `graphql`,`messaging`,`persistence-jpa`,`persistence-mongo`,`notification.platform` |
Row 5's exactly-once claim is not inferred from the lane passing; the smoke client says it:
```
notification-smoke: b0e083cb-06c5-4dde-b454-4c4e26f65edc delivered exactly once and stayed that way
```
## Row 8 — a defect, found by running the row
The plan is explicit that Row 8 must be checked for the *name*, not merely for a failure. Checking it
that way found that the name was not what an operator got.
`ca-skeleton.outbox.enabled=true` with the JPA switch off is a dependency error this repository
names, and `CapabilityDependencyValidator` produces the sentence that names it. But the outbox is
also a registered relational consumer, so `DataSourceRequirement` reports that a pool is required,
`JpaOffAutoConfigurationImportFilter` therefore keeps Boot's relational auto-configurations in the
candidate set, and Hibernate and Flyway were instantiated during refresh — ahead of the
`InitializingBean` that carried the check. What actually came out was:
```
Unable to obtain connection from database: Connection to localhost:5433 refused.
```
Both components were right on their own. A pool does have consumers besides JPA, and the outbox does
need the JPA switch. The disagreement was only ever visible in **which one spoke first**, and no test
could see it: the existing `CapabilityDependencyValidatorTest` calls the validator's static method
against a `MockEnvironment`, which proves the rule computes the right sentence and nothing about
whether anything runs it in time.
**Fix.** The check moved to the environment stage
(`CapabilityDependencyEnvironmentValidator`, an `EnvironmentPostProcessor` at
`LOWEST_PRECEDENCE` alongside the master-switch and profile validators), where every property
source is resolved and nothing has been instantiated. The `InitializingBean` stays: a context built
without `spring.factories` — an `ApplicationContextRunner`, a slice test — never reaches the
post-processor, and the rule should not be optional there.
**Evidence, from the built jar rather than from a test harness** — see
[task3-row8-dependency-error.log](task3-row8-dependency-error.log):
```
### outbox on, JPA off exit code: 1
This deployment enables capabilities whose dependencies are off:
- ca-skeleton.outbox.enabled=true needs relational persistence to store rows;
set ca-skeleton.persistence-jpa.enabled=true or turn the outbox off.
- ca-skeleton.outbox.enabled=true needs somewhere to publish;
set app.messaging.enabled=true or turn the outbox off.
### relay on, outbox off exit code: 1
This deployment enables capabilities whose dependencies are off:
- ca-skeleton.outbox.relay-enabled=true only starts the scheduler for a capability that is off;
set ca-skeleton.outbox.enabled=true or turn the relay off.
```
`DependencyErrorStartupContractTest` pins this at the composition-root level, including a case that
boots all-off successfully — without it, every other assertion in that class would also be satisfied
by a validator that refuses everything.
## Row 9 — the positive half runs; the conflict half is pinned as a rule
`all-adapters` starts PostgreSQL and MongoDB and the application together, and the application
reports both persistence switches on. The two adapters implement disjoint ports, so there is no
ambiguity to resolve and no `@Primary` involved.
That absence is what needs pinning, because manufacturing a conflict would test Spring's
`NoUniqueBeanDefinitionException` rather than this repository. What can silently change is the
property the row depends on: that no port is resolved by preferring one bean over another. A
`@Primary` added later to settle an ambiguity would convert a startup rejection into a silent pick,
and every existing test would stay green, because the composition would still boot.
`PortResolutionContractTest` states the rule directly — a `@Primary` on a port implementation is
permitted only when a condition decides which one is active, which makes it a selector rather than a
tiebreak. The whole repository has three `@Primary` beans:
| bean | type | guard |
| --- | --- | --- |
| `inProcessDistributedLock` | `DistributedLockPort` | `multi-instance-enabled` false or absent |
| `distributedLockProvider` | `DistributedLockPort` | `multi-instance-enabled` true |
| `applicationTaskExecutor` | `TaskExecutor` | none — Boot's own contract wants a primary executor, and it is not a port |
The two lock beans cannot coexist, so neither is being preferred; one of them simply is not there.
The rule was falsified before being trusted: removing the `@ConditionalOnProperty` from
`distributedLockProvider` fails the test, and restoring it passes.