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

6.2 KiB
Raw Blame History

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:

### 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.