Files
clean-architecture-backend-…/docs/superpowers/plans/evidence/2026-08-15-wave6-final/task6-p0-reconciliation.md
T

10 KiB
Raw Blame History

Task 6 — the five reviews' P0 findings, reconciled

The remediation design this wave executed never referenced a review id (grep -c over it: zero). It took one theme from the five reviews — five adapters ship and do not run — and Waves 06 executed that theme. So "how much of the reviews is reflected" had never been measured. This is the first measurement, and then the work that followed it.

Three read-only audits ran in parallel, one per review family, each instructed to treat a passing test as evidence only when it exercises the real composition. Their headline claims were then re-checked by hand before being acted on; one was wrong and is recorded as such below.

Where the P0 set stood when measured

Review P0 CLOSED PARTIAL OPEN
jpa 6 5 1 0
messaging 5 2 3 0
graphql 2 1 1 0
notification 12 1 11 0
mongodb 9 0 9 0
total 34 9 25 0

Nothing was OPEN: every P0 had been worked. What the audits found instead, in three independent voices, was the same shape — the implementation is substantially real and the gate under it is thin. Six notification adapter classes were referenced by zero tests. Both Mongo executors were referenced by zero tests. Every messaging real-broker test skips silently without Docker. And the Compose matrix, the strongest evidence this repository produces, runs in no workflow.

What was fixed, and how each was proven

NTF-004 / JPA-004 — the completion write was not fenced

Two audits reached this independently from different reviews, which is why it was taken first.

The claim is fenced (a single FOR UPDATE SKIP LOCKED CTE that bumps lease_fence) and the renew is fenced. The completion was findById → mutate → saveAndFlush with no owner or fence predicate. A worker whose lease expired during the provider call — the one stretch the platform deliberately spends outside a transaction — came back and wrote its outcome over the row a new holder had already claimed. The @Version column does not stop that: it detects a concurrent edit, not a superseded writer, and the late worker's read is recent enough to win.

Fixed with conditional statements in the same idiom as the renew, saveHeldBy/transitionHeldBy on the port returning empty when the lease is gone, and all four branches of applyNextAction moved onto them. Losing the lease is not an error: the new holder owns the job and will record its own outcome.

@Modifying(clearAutomatically = true) on both, because a native update bypasses the persistence context and the immediate re-read would otherwise be served the values it just replaced — a trap one of the audits had found elsewhere in this same platform.

Proof: two real-PostgreSQL cases in the JPA contract lane. A superseded holder's completion matches zero rows and the live holder's state is untouched; the current holder's completion writes. The second exists because without it the first is satisfied by a statement that matches nothing ever.

NTF-012 — the SSRF guard had no callers

requireExternallyRoutable refuses the cloud metadata service, RFC 1918, link-local, IPv6 local, userinfo disguise and multi-answer DNS. It had an eight-case test suite, all green, and one occurrence in the repository: its own definition. The two sites it was written for — WebhookSubscription and SesProviderProperties — still called requireSecureOrLoopback, which reads the scheme and nothing else.

Both now call it. The new test goes through the constructors rather than the helper, because testing the helper again is exactly what failed to catch this.

What is not closed: allowLoopback is true, so a user-supplied target naming localhost still passes. Closing it means the allowance becomes a decision the caller states, and the caller does not exist — WEBHOOK has no ProviderRuntimeAssembler, so a webhook profile refuses to boot and nothing in production constructs the record. Writing the policy now would mean choosing its default with no caller to check it against. Recorded in the code at the call site.

NTF-001 — the documented callback switch broke startup

CallbackRequestFactory is a constructor argument of the MVC controller, the WebFlux handler and the WebFlux configuration, and was produced by no production code — the only instantiation was in a test. So APP_NOTIFICATION_PLATFORM_CALLBACKS_ENABLED=true, a key in the env registry and in the configuration reference, did not enable callbacks; it failed the boot on an unsatisfied dependency.

The beans now exist, conditioned on the same switch. The missing trusted-proxies setting came with them, defaulting to empty — with no entry, forwarded headers are not believed, because honouring them unconditionally lets any caller choose the URL its own signature is checked against. Registered in application.yml, the env registry, .env.example and the configuration reference; both notification gates pass.

MNG-007 — a health view Actuator could not read, and a reactive half nothing built

MongoPlatformHealthIndicator computed topology mismatch, secondary availability and a bounded detail map, and implemented neither HealthIndicator nor HealthContributor. The adapter carries no Actuator dependency and should not: the delivery platform had already established the shape, where the adapter computes the facts and the composition root maps them onto Health. Done the same way.

Separately, every reactive class in the leaf — executor, consistency binder, session factory, cursor guard — was declared by no configuration. They shipped and no configuration could construct them. Now wired in a nested configuration conditioned on a ReactiveMongoTemplate bean, not just the class: the class is on the compile classpath unconditionally, so a class condition alone would try to build the reactive path in a servlet-only deployment and fail a startup for a capability nobody asked for. Both the positive and the negative case are asserted.

MNG-005 / MNG-023 — a deadline that only produced a report

The blocking executor compared elapsed time to the declared timeout after the callback returned, and said so in its own comment: a Java callback cannot be interrupted mid driver call. That is an overrun report, not a deadline.

The scoped API is the narrowed surface where the number can actually be sent, so every method taking a Query or an Aggregation now carries it as maxTimeMS, which the server enforces. insert has no query to attach it to. Duration.ZERO is refused, because zero means "no limit" to the server and accepting it would turn a misconfiguration into an unbounded operation.

The raw escape hatches (rawOperations(), executeInternal(...)) are genuinely used inside the platform by the geospatial, atomic and bulk operations, and their callers live in sibling packages, so package-private cannot express the rule. It is enforced as a boundary from the composition root — the only place that sees both the platform and everything consuming it. The rule was falsified by widening its scope until it fired, and a second case asserts the platform still uses them, so the rule cannot pass by the hatches having been deleted.

MNG-006 — a guard that compared a declaration against nothing

LocalDateTimeMappingGuard was constructed withoutConverters() and then asked to validate the manifest. It could only ever reject LOCAL_DATE_TIME_WITH_REGISTERED_CONVERTER: a deployment that had registered the named converter was rejected exactly as loudly as one that had not, so the check that exists to distinguish those two cases could not tell them apart. It now reads the converters the deployment actually registered, and a test asserts the same manifest passes with the converter and fails without it.

MNG-008 — a promotion gate that required five of the six categories it declares

MongoAdvancedPromotionEvidence.REQUIRED listed six; MongoAdvancedPromotionGate.verify() required five. migration was missing, so a promotion could pass with no migration evidence at all.

GQL-002 — the batch policy applied to nothing

GraphQlBatchLoaderRegistrar carried the chunking, the budget and the request scope, was unit tested, and was declared by no configuration — the only file mentioning it was itself. A field resolving through @BatchMapping or a DataLoader met none of it. The chain (BatchPolicyRegistry → DataLoaderFactory → BatchLoaderRegistrar) is now assembled by the platform. The batch ceiling reuses maximumPageSize rather than adding a setting: both answer how many rows one downstream call may ask for, and a batch limit above the page limit would let one request fan out past the bound it already accepted.

One thing an audit got wrong, and one fix that was reverted

The mongodb audit reported that the UUID axis is "declaration-only" and mapped onto the driver nowhere. It is mapped — MongoClientSettingsFactory calls .uuidRepresentation(...). Verified before acting.

Acting on the adjacent concern — the manifest and the profile describing the same fact independently — a startup check was written to refuse a disagreement between them. Writing its test showed the disagreement cannot occur: MongoUuidRepresentation has two values, only STANDARD is writable, and an existing check already refuses the other. A guard for a state that cannot arise is the same "declaration nothing checks" this wave has been removing, so it was reverted rather than kept with an unfalsifiable test.

What remains, and why

  • NTF-012 loopback residue — belongs with the change set that gives WEBHOOK an assembler.
  • MSG-015 — the application-owned port and anti-corruption bridge between application-core and the messaging platform. The review names it and specifies the target shape; it is a runtime wiring change, and the review itself says the physical work is a separate change set.
  • MSG-003/004/005 residue — on code no deployment can execute: the platform's outbox and inbox migrations are applied only by tests, the production Kafka transport is publish-only, and Rabbit declares no transport bean.
  • P1 and P2 — 100 findings, never audited. They were not in this remediation's scope and their state is unknown. Saying so is the honest position; the P0 audit took three parallel agents and the P1/P2 set is three times larger.