Portfolio Manifest — Prepared 2026-09-19

Terrence Daniels

Full-Stack Java/Gradle Engineer

A deliberate migration of Axon Framework 5's build system from Maven to Gradle — converted one file at a time, each one individually inspected against the Maven source rather than run through an automated converter. Build-logic conventions are done, and test-logging, common, and update are all fully converted — every gap found along the way, forty-three so far, is disclosed in this repo's own build log, not smoothed over. Still early — 11 of 14 reactor modules haven't started converting at all.

3 of 14reactor modules fully converted
301commits, 29 days
3build-logic convention plugins done
43real gaps found & fixed
144 of 144common source files converted
8public surfaces kept in sync

Why this project

A real Maven→Gradle migration, verified rather than trusted

Pointing a conversion tool at a Maven reactor and committing whatever comes out would be faster. It would also miss everything interesting: Gradle has no equivalent of Maven's parent-POM inheritance, so a shared dependency block that's harmless in axon-parent becomes a literal circular dependency the moment it's mechanically applied to the one module that dependency points at. A checkstyle rule copied byte-for-byte looks complete right up until a second read catches the comment three lines above it explaining why one entry is deliberately missing — caught before landing, not after, but wrong on the first pass all the same.

Gradle 9.7.1 Java 25 toolchain Kotlin DSL JUnit 6 Mockito Checkstyle com.gradleup.nmcp Central Portal

Build system first, real source now underway

Build-logic conventions done · test-logging published · common complete (144/144) · update complete (27/27) · 11 modules not started

Migration order is deliberate and bottom-up: the shared Gradle conventions every module will apply — Java toolchain, checkstyle, publishing, signing — had to exist before any real module could be converted onto them. test-logging was the first module through that pipeline, published for real (org.axonframework:axon-test-logging) with its own genuine circular-dependency problem solved along the way. common followed, converted file by file in dependency order (base exception hierarchy first, then each utility class once whatever it references exists) — now fully done at 144 of 144 files, the first module beyond test-logging to reach that state. update followed, also fully done at 27 of 27 files — the first real consumer-module dependency on common anywhere in this build, which is how a genuine JSR-305 dependency-scoping gap got found and fixed along the way. The rest of the reactor — conversion, messaging, eventsourcing, modelling, and seven more — hasn't started; see todo.md for the honest current state rather than assuming more is done.

Verification over assumption

Every structural change gets a real gradle build or gradle :projects run before being called done — including re-running it live when a fix's correctness was itself in question, not just reasoning about what the fix should do.

Gaps disclosed, not hidden

No gradle/libs.versions.toml version catalog (dependency-drift risk accepted, not overlooked) and integrationtests deliberately not publishing to Maven Central, diverging from upstream's own current behavior — both written down in todo.md, not silently absorbed.

Forty-three real gaps, found and fixed

Found by running a real build, reading a live API doc, or auditing already-converted files a second time — not by assuming the first pass was right

Four turned up in this fork's own Gradle translation of Maven's inheritance and publishing model. One turned up in a third-party plugin's live interaction with a specific Gradle version, undocumented anywhere public at the time. One turned up in this fork's own .gitignore. One turned up in upstream Axon's own Maven config — real drift in a widely-used project, not a typo. One turned up in Axon's own HTML hitting a stricter tool than upstream builds with. One turned up in the CI workflow's own first real run. Seven turned up while converting common's own source files: a stale pre-Java-7 idiom carrying a dead, unreachable catch block in two files, modernized without changing behavior; two real inconsistencies caught by cross-checking new files against the interfaces and sibling classes they implement rather than reading each file in isolation; a nullness-contract chain across four files that needed the user's own judgment call to resolve, not just a mechanical fix; a nullness gap grounded in a documented JDK API contract rather than a guess; and a deliberate rewrite whose first, plausible-looking attempt turned out wrong, caught only by testing it against a real, disposable database instance. Six more turned up finishing common's last few files: missing-@Nullable gaps, a message-polarity bug, and a wrong exception type, all found by cross-checking a file against the interface or sibling it implements rather than reading it in isolation. Seven more turned up from a deliberate retroactive pass: once common reached 144 of 144 files, a second, independent audit re-checked every one of them against a DRY/SOLID charter this fork adopted partway through — not because any of them were wrong when first converted, but because the charter itself came later. All seven were the same class of gap as everything above: a javadoc claim that didn't match its own implementation, a copy-pasted error message with the polarity backwards, and five places where a value can genuinely be null but the type didn't say so. Ten turned up converting the entire update module, 27 of 27 files: a dead parameter silently discarding real vulnerability data, a duplicated classpath scan next to a record built straight from an unchecked Properties lookup, a singleton whose own field could be reassigned by any caller, an interface-wide nullness gap repeated across three implementations, two more javadoc claims that didn't match their own implementation, and a thread-safety gap confirmed identical in the real upstream source. The final two turned up from a second comprehensive drift sweep of this fork as a whole, re-checking both modules together rather than trusting either one's own individual completion as final: one more missing-@Nullable gap in common, and one more copy-pasted error message, this time naming the wrong algorithm instead of the wrong polarity.

01
testImplementation(project(":test-logging")) — a circular dependency Gradle would build without complaint
Maven's axon-parent unconditionally adds axon-test-logging as a test dependency to every inheriting module — upstream's own pom.xml dodges the resulting self-reference by parenting that one module on the root aggregator instead. Gradle's convention-plugin model has no equivalent "skip one level of inheritance" lever, and the shared java-conventions plugin already had that dependency line, so applying it to test-logging itself would create a literal self-dependency. Guarded with a one-line if (project.name != "test-logging"), verified with a real gradle :test-logging:dependencies run showing no self-reference in the resolved graph.
high · build-breaking
02
nmcp 1.6.1 — configuration cache crashes on providers.environmentVariable()
nmcpSettings { centralPortal { username = providers.environmentVariable(...) } } fails Gradle 9.2's configuration cache with cannot serialize object of type ... ValueSourceProvider. Reproduced directly by running the build, not inferred from a stack trace read in isolation — searched publicly first and found nothing documented about it. Fixed with a plain System.getenv(...) read instead of the lazy provider.
high · undocumented plugin interaction
03
build/ — simultaneously real content and Gradle's own output directory
The root build/ directory does double duty: a real, intentionally-tracked module directory (mirroring Maven's build/parent, build/checkstyle.xml) and Gradle's default build-output location for the root project — the same path, two purposes. A blanket .gitignore exception meant to protect the former was quietly letting Gradle's own generated reports through as trackable files. Replaced with an explicit allow-list of just the real paths, verified with git check-ignore -v against both cases before trusting it.
medium · gitignore drift
04
One missing module directory blocked the entire build's configuration, not just itself
Every include(...) in settings.gradle.kts pointed at a module directory that didn't exist yet, and Gradle refuses to configure a build at all under that condition — not just skip the missing module. Confirmed with a real failing gradle run before deciding on a fix. Fixed by commenting each include out individually, brought back one at a time as that module is actually converted.
high · build-breaking
05
Root pom.xml's Javadoc-skip list names four modules that don't exist in this reactor
axon-todo, axon-legacy, axon-legacy-aggregate, and axon-legacy-saga appear in upstream's maven-javadoc-plugin configuration but match nothing in this Axon 5 reactor's actual module list — uncleaned drift from an Axon 4-era POM, confirmed by cross-checking every real module name. Not carried forward.
low · upstream config drift
06
Published artifacts were silently getting the wrong Maven coordinate
Every Axon module's real artifactId is axon-<name>, but Gradle's base.archivesName defaults to the bare project directory name — and, confirmed by generating the actual POM rather than assuming the jar filename being right meant the coordinate was too, MavenPublication.artifactId doesn't inherit from archivesName either; a second, separate default needed overriding. test-logging had already been generating a POM with <artifactId>test-logging</artifactId> instead of axon-test-logging since its own commit. Fixed once, centrally, for every module.
high · silent publish-coordinate bug
07
Gradle's javadoc task rejected Axon's own HTML outright
The moment common got its first real source file, javadoc failed with error: self-closing element not allowed on a <p/> tag. Not a source bug to hand-fix: upstream's own root pom.xml already disables doclint entirely (<doclint>none</doclint>) for exactly this reason — JDK 25's stricter javadoc, not upstream's. Gradle's side had just never replicated that suppression. Fixed centrally, confirmed with a clean gradle :common:build afterward.
high · build-breaking
08
The CI workflow's own first run failed in 15 seconds
gradle-version: '9.2' in the new build-verification workflow failed immediately with Error: Gradle version 9.2 does not exist — gradle/actions/setup-gradle needs the exact release string, not Gradle's own shorthand. Caught by watching the actual run, not assumed green because the YAML looked right. Fixed to '9.2.0', verified with a second real run that took 1m53s instead of 15 seconds — the difference between failing at setup and actually building.
medium · CI misconfiguration
09
Jar manifest's Implementation-Vendor still said AxonIQ B.V.
maven-jar-plugin's addDefaultImplementationEntries derives Implementation-Vendor from root pom.xml's <organization> — carried over literally into the Gradle jar manifest during the convention-plugin translation, even though every other piece of POM metadata (license, SCM, developers) had already been repointed at this fork's own ownership. Caught on a routine re-read of the convention plugin, not by a build failure. Fixed to this fork's actual owner.
low · publish-metadata drift
10
Two files carried a UTF-8 idiom with an unreachable catch block
digest.Digester.md5Hex() and io.IOUtils.UTF8 both used the pre-Java-7 getBytes("UTF-8") / Charset.forName("UTF-8") pattern. getBytes(String) throws a checked UnsupportedEncodingException that can never actually fire — UTF-8 support is guaranteed by the JLS — so upstream's own source carries a dead catch block around it. Modernized both to java.nio.charset.StandardCharsets.UTF_8, which let the unreachable catch in Digester be removed outright. No behavioral change — same singleton Charset, identical bytes.
low · code modernization
11
WeakReferenceCache.computeIfAbsent() threw the wrong exception type for its own documented contract
The Cache interface's own default method throws IllegalArgumentException when a supplier produces null — WeakReferenceCache's override of the same method threw IllegalStateException instead, for the identical condition. Not caught by reading the file alone; caught by cross-checking it against the interface it implements, a technique adopted specifically because a first pass in isolation had already missed it once.
medium · contract inconsistency
12
SimpleEntityManagerProvider validated its constructor argument differently from its own siblings
Used Assert.notNull(...) (→ IllegalArgumentException) where every other class converted in the same package by that point used Objects.requireNonNull(...) (→ NullPointerException) for the same kind of constructor validation. Both are individually defensible; only one matches what its neighbors already do. Standardized on the sibling pattern, found the same way as gap #11 — by comparing against already-converted files in the same package rather than reviewing each file only against its own Maven source.
low · consistency drift
13
A nullness-contract chain across four files, resolved only after checking with the user
AbstractMethodPropertyAccessStrategy.propertyFor declared its property parameter @Nullable, widening PropertyAccessStrategy's own non-null abstract contract, then forwarded it unchecked into getterName(String property) — non-null in the same class. BeanPropertyAccessStrategy's override of getterName inherited that widening, but its body calls property.charAt(0) unconditionally — a real NPE if the annotation were ever taken at its word. The real call path never passes null, but upstream's own test file declares @Nullable on its test-double overrides of the same method, so the widening wasn't obviously an accident either — confirmed the fix with the user rather than deciding unilaterally, the one judgment call this fork treated as genuinely ambiguous. Separately, the same investigation found the return type had the opposite problem: every real implementation returns null in some branch and the only caller loops on it, but neither the abstract method nor one of its two overrides declared @Nullable Property<T> — fixed to match the one override that already had it right.
medium · nullness contract, confirmed not assumed
14
A parameter's nullability didn't match what the JDK itself documents about it
ConnectionWrapperFactory.isEmpty and .invokeMethodAndUnwrapNestedException both receive args from InvocationHandler.invoke's own parameter, which the JDK documents as null (not an empty array) when the invoked method takes no arguments. isEmpty already null-checked it but didn't declare @Nullable; the other method forwarded the same possibly-null value without declaring it either. Unlike gap #13, this one needed no judgment call — the JDK's own contract settles it. Fixed both.
low · JDK-contract nullness gap
15
The "obvious" modernization was wrong, and a real Oracle instance is what caught it
Upstream ships Oracle11Utils, a pre-12c workaround for Oracle lacking native auto-increment: it retrofits one via CREATE SEQUENCE + CREATE OR REPLACE TRIGGER. Nothing else in the Axon codebase calls it — confirmed by grepping the whole Maven source — so rewriting it for Oracle 23ai, which has native identity columns, breaks nothing else. First attempt: ALTER TABLE ... MODIFY (col GENERATED AS IDENTITY ...) on the existing plain column — failed for real against a live, disposable Oracle 23ai Free container with ORA-30673 ("column to be modified is not an identity column"). Oracle's MODIFY identity clause only adjusts a column that's already identity; it cannot convert a plain one. The actual fix — drop the column, re-add it declared identity — was verified the same way (three inserts producing ids 1, 2, 3), and is itself a real, documented trade-off: unlike the original's purely-additive trigger, this discards any existing data in that column. A docker-compose.yml plus two shell scripts (modeled on this user's other portfolio forks' verification tooling, scoped down to the one container this repo actually needs) now exist specifically so this kind of claim never has to be taken on faith.
medium · wrong-then-corrected, verified against a real instance
16
An interface's own contract didn't match its only real implementation
Component<C>.resolve(Configuration) was declared non-null, but AbstractComponent — the only implementation base class that exists — overrides it @Nullable. Found during a deliberately-requested post-closure health check, not a routine file-by-file pass: this file was one of twelve converted under real compile-time pressure (a circular closure spanning three packages), so the whole closure got a second, holistic look once it landed rather than trusting each file's individual review alone. Traced whether it's actually reachable today — both real subclasses always produce non-null, so it doesn't bite yet — but the interface's own annotation didn't match its base implementation's real contract. Same class of gap as WeakReferenceCache and the property-package chain earlier, this time caught by reviewing a whole closure together rather than one file at a time.
low · nullness contract, found on a second pass
17
A lambda that only javac resolved correctly
LifecycleRegistry's Consumer<Configuration> overloads of onStart/onShutdown each forward to a block lambda that returns a CompletableFuture<?> — value-compatible only with the LifecycleHandler overload, not the void-returning Consumer one, so javac resolves it correctly per JLS 15.27.3 with no cast needed. The IDE's language server resolved it to the wrong overload anyway, flagging valid return statements as errors. Two sibling overloads a few lines up in the very same file already disambiguate this exact shape with an explicit (LifecycleHandler)/(Consumer<Configuration>) cast; these two just hadn't followed their own file's own established pattern. Added the matching cast to both.
low · overload-resolution disambiguation
18
A decorator's declared contract didn't match what its only real caller actually passes it
ComponentDecorator.decorate's delegate parameter was declared plain non-null, but its one real call site — DecoratedComponent.doResolve — passes delegate.resolve(configuration) straight through, which is genuinely @Nullable per Component.resolve()'s own contract (gap #16 above). Same class of mismatch as #16, found the same way: reading the new file against the interfaces it actually calls, not just against its own Maven source. Fixed by marking the parameter @Nullable.
low · nullness contract
19
Two overloads of a private helper widened nullability without saying so
DefaultComponentRegistry.LocalConfiguration.fromParent's Class<C> and TypeReference<C> overloads both declared their name parameter non-null, but both are called from getOptionalComponent's own @Nullable String name and forward it straight into Configuration.getOptionalComponent(..., @Nullable String). Fixed both to @Nullable.
low · nullness contract
20
An assertion message described half of what it actually checks
DefaultComponentRegistry.getModuleConfiguration's Assert.nonEmpty(name, "The name must not be null.") only names the null case, but nonEmpty also rejects an empty string — DecoratorDefinition.java's identical call already said "must not be empty or null." Matched it.
low · message accuracy
21
A validation message had its own polarity backwards
BaseModule.componentRegistry's requireNonNull(registryAction, "The registryAction must be null.") fires precisely when the value is null, but the message claims the opposite requirement — a copy-paste error that would mislead anyone reading the exception rather than the source. Fixed to "must not be null."
low · message-polarity bug
22
An import that nothing in the file actually used
DefaultAxonApplication imported org.jspecify.annotations.NonNull, but nothing in the file references it — only the unrelated Objects.requireNonNull static import shares the substring, which is likely how it went unnoticed. Removed.
low · dead import
23
Another interface/implementation nullness mismatch, same shape as gap #16
AxonConfigurationImpl.getParent() genuinely returns null for the root configuration, but wasn't declared @Nullable — while Configuration.getParent() itself is, and DefaultComponentRegistry's own LocalConfiguration.getParent() already implements it correctly. Matched the interface's real contract.
low · nullness contract
24
The wrong exception type for a state violation, not an argument problem
DefaultAxonApplication.registerLifecycleHandler's "configuration already initialized" check threw IllegalArgumentException, but nothing about an argument is wrong here — it's a timing/state violation. BaseModule's identical "already been built" check correctly uses IllegalStateException. Matched.
low · wrong exception type
25
A utility method's javadoc described an ordering its own implementation doesn't produce
CollectionUtils.intersect's javadoc claims matched items are returned "in the order as found in collection2," but the implementation seeds a lookup set from collection2 and then iterates collection1, appending matches in that order — confirmed byte-identical to the real upstream method, so this is genuine upstream drift, not something this fork introduced. Found during a deliberate retroactive DRY/SOLID re-audit of all 144 finished common files, not a build failure. Corrected the javadoc to match the actual, correct behavior rather than changing a working algorithm to match a wrong sentence.
low · javadoc/implementation mismatch
26
Same message-polarity bug as gap #21, one package over
Components.postProcessComponents's requireNonNull(processor, "The component post processor must be null.") has the identical backwards polarity as the BaseModule fix above — confirmed present in the real upstream source too. Fixed to "must not be null."
low · message-polarity bug
27
A return path documented as nullable in prose, but not in the type
FutureUtils.joinAndUnwrap(future, timeout) can return null by its own javadoc, yet only the 1-arg overload that just delegates to it carried @Nullable — confirmed present in the real upstream source too. Added it to the 2-arg overload as well.
low · nullness contract
28
Two more parameters documented as nullable in prose, but not in the type
IOUtils.closeQuietly and closeQuietlyIfCloseable both say in their own javadoc that the argument "may be null, in which case nothing happens" — neither parameter carried @Nullable. Added it to both.
low · nullness contract
29
A method that returns null from two different paths, declaring neither
MavenArtifactVersionResolver.get() returns null explicitly when the metadata file is missing, and implicitly via Properties#getProperty when the version key isn't present — confirmed present in the real upstream source. Declared @Nullable.
low · nullness contract
30
A default value's nullability didn't survive being forwarded between overloads
JdbcUtils's 3-arg nextAndExtract/extract overloads call their 4-arg siblings with a literal null defaultValue, and the 4-arg bodies can return that null straight through — neither the parameter nor the return type on the 4-arg overloads declared @Nullable, confirmed present in the real upstream source. Fixed both.
low · nullness contract
31
Four cache implementations dropped their interface's own @Nullable
Cache.get(K) is declared @Nullable — a cache miss is the normal, documented case — but none of its four implementations (WeakReferenceCache, JCacheAdapter, EhCacheAdapter, NoCache) repeated the annotation on their override, despite every one of them genuinely returning null on a miss. Fixed all four to match the interface they implement.
low · nullness contract, four files
32
UpdateCheckResponse.parseUpdate() silently discarded real vulnerability data
The private helper's vulnerabilities parameter was passed in from its one call site but never referenced anywhere in the method body — only upgrades was actually used, meaning any vulnerability data reaching this method was silently dropped rather than recorded. Found while converting the update module's first real source file, not assumed correct just because it compiled. Removed the dead parameter and confirmed the vulnerability path is populated correctly by its own dedicated parseVulnerability method instead.
medium · dead parameter, silent data loss
33
AxonVersionDetector ran the same classpath scan twice and silently violated its own record's contract
detectAxonModules()'s outer loop iterated over every known group ID calling getResources("META-INF/maven/") — a fixed string that doesn't depend on the group ID at all, so the identical classpath scan ran once per group ID, duplicating every URL before any filtering happened. Separately, the method that maps a scanned entry to an Artifact (a record whose groupId/artifactId/version fields are all non-nullable) built one straight from Properties.getProperty(key), which returns null on a missing key — silently constructing an invalid record instead of being caught and skipped like every other malformed-input path in the same file. Pulled the scan outside the loop entirely, and added a validation check that throws rather than building the record with a null field.
medium · functional bug, nullness contract
34
DefaultUsagePropertyProvider.INSTANCE could be reassigned by any caller
Declared public static but not final, despite the class's own javadoc describing it as "implemented as a singleton" — any external caller could reassign the field and break that contract outright, not just a theoretical risk given the field's visibility. Added final.
low · contract violation
35
UsagePropertyProvider and three of its implementations dropped the interface's own @Nullable
getDisabled()'s javadoc documents a null return, but the interface's own signature was bare Boolean — and CommandLineUsagePropertyProvider, EnvironmentVariableUsagePropertyProvider, and PropertyFileUsagePropertyProvider all repeated the identical gap on their own getDisabled()/getUrl() overrides. EnvironmentVariableUsagePropertyProvider had it a third time on its own nested EnvironmentVariableSupplier.get() functional interface, and PropertyFileUsagePropertyProvider had it twice more on fields that stay null if the home directory can't be resolved or its property file fails to load. Same class of gap as the four Cache implementations above (gap #31), just spread across a different module's interface and its implementations instead of one interface's four adapters. Fixed all of them.
low · nullness contract, four files
36
DelayedTask.failureCause — a genuinely nullable field with no @Nullable
The field is only ever assigned inside a failure catch block and stays null otherwise, but carried no @Nullable under this package's own @NullMarked declaration — confirmed present in the real upstream package-info.java too. Added it to both the field and getFailureCause()'s return type.
low · nullness contract
37
KotlinVersion's memoized field had no volatile and no @Nullable
The lazily-initialized version field used an unsynchronized check-then-set pattern with no volatile — a genuine visibility gap under the JVM memory model (low-severity here since the detection logic it guards is idempotent and side-effect-free, but the same class of bug static analyzers flag as an unsafe lazy-init pattern). The same field was also declared non-null while being initialized to null. Fixed both, and — since the IDE's null-flow analysis doesn't narrow fields across statements the way it does local variables, because a field could change from another thread between a check and its use — refactored the accessor to read the volatile field into a local variable once rather than re-reading it two or three times.
medium · thread-safety, nullness contract
38
MachineId's own javadoc promised a fallback its code didn't actually deliver
The class's own javadoc promises a UUID fallback "if the ID can't be stored or read," but the catch block only logged on failure. The "can't be stored" path was already safe, since the fallback UUID is generated before the write attempt — but "can't be read" (an existing file that throws while being read back) left the field null, contradicting the class's own documented contract. Fixed by falling back to a fresh UUID in the catch block if the field was never set.
medium · javadoc/implementation mismatch
39
UpdateCheckerHttpClient's javadoc described an HTTP method the code never sends
The javadoc claimed "if firstRequest is true, it will send a POST request, otherwise a PUT request," but the code always sends .GET() — the first-request flag only ever sets an X-First-Run header, never the HTTP method. Same class of gap as CollectionUtils.intersect (gap #25): the GET-based implementation is clearly deliberate and working, so the javadoc was corrected to describe reality rather than inventing a POST/PUT implementation that was never actually written.
low · javadoc/implementation mismatch
40
UpdateChecker had two more cross-thread fields missing volatile, and one wrong log argument
delayedTask was correctly made volatile during this file's original conversion, with the reasoning for why its two siblings (firstRequest, errorRetryBackoffFactor) didn't need it checked explicitly at the time rather than applied blindly. That reasoning turned out wrong: a second, independent re-audit traced the actual statement order and found both fields are written after the next virtual thread is already started, with no happens-before guarantee the new thread would see them — confirmed identical in the real upstream source, a pre-existing bug this fork's conversion didn't introduce. In the same file, a log statement also passed the whole updateCheckResponse record where .checkInterval() was clearly intended, since the very next line uses .checkInterval() correctly. Fixed both, the same way gap #16 was found — on a second pass, not the first.
medium · thread-safety, found on a second pass
41
update's classpath was silently missing a type its own dependency's bytecode references
common declares its JSR-305 dependency compileOnly, deliberately, so it doesn't leak into either module's published artifact — but compileOnly is never transitive, even through update's api(project(":common")). common's compiled bytecode carries real @Nonnull/@Nullable annotations throughout, so update's classpath was genuinely missing a type its own dependency's class files reference. javac tolerates this; Eclipse's compiler doesn't. Confirmed this exact gap exists in the real upstream Maven source too (common/pom.xml uses provided scope, update/pom.xml never references it either) — update is simply the first real consumer of common anywhere in this build. Fixed by adding the identical compileOnly declaration to update directly — now a standing check for every future module that depends on common.
high · build-system dependency gap
42
FutureUtils.ignoreResult() didn't declare what its own javadoc already promised
The method's own javadoc states its return is always null, and its body does return null unconditionally, but the Void return type carried no @Nullable — unlike the sibling joinAndUnwrap overloads (gap #27), which already follow this convention. Found on a second, independent re-audit of already-converted common files run alongside the update module's own re-audit, the same technique that caught gap #16.
low · nullness contract
43
Digester.newInstance()'s own error message named the wrong algorithm
The method accepts any algorithm name via its own parameter, but its NoSuchAlgorithmException handler always reported "This environment doesn't support the MD5 hashing algorithm" — even when called with a different algorithm entirely, since nothing outside this fork ever exercises that path with anything but MD5. Fixed to report the actual algorithm that was requested.
low · message accuracy
Not counted in the 43 above, but part of the same retroactive audit: three real DRY duplications (a type-name ternary copy-pasted across two ComponentDescriptor implementations, two near-identical private filter methods in Components, and JdbcSQLErrorCodesResolver reimplementing a cause-chain walk ExceptionUtils already provides) and a batch of pure formatting drift (eight IntelliJ-only //noinspection comments invisible to this project's actual Eclipse-based tooling, sixteen doubled blank lines, one file still using tab indentation). All fixed the same session, all verified the same way — but none of them change behavior, so none of them are counted as a "gap," matching how the ComponentRegistry javadoc typo above wasn't either.
Not counted in the 43 above, but part of the same discipline: converting update also turned up a redundant double-call fixed twice (once in this module, once already fixed in common), a renamed ALL_CAPS field that should never have been a constant-style name, and several formatting/modifier-order nits — none of which change behavior, so none are counted. A later, second comprehensive drift sweep of the whole fork also found two more fields that compile-time analysis showed were safe to make final (ClassUtils's two static fields, MachineId's single-assignment field) and one more redundant double-call (ExceptionUtils.isExplicitlyNonTransient's duplicated getCause()) — same reasoning, same exclusion.
Formerly an honest gap, now closed: the Gradle wrapper (gradlew, gradlew.bat, a checksum-verified gradle-wrapper.jar) is committed as of 2026-08-25 — blocked until then by gap #04 above, which is why it took this long. A real gap caught before committing it, not after: gradlew was staged non-executable (git file mode 100644), which would have failed every Linux CI checkout with Permission denied. See todo.md for the full story.

Keeping the record honest across eight surfaces

todo.md, README, the wiki, the diagrams, this page, the project board, the GitHub profile, and the landing page — checked, not assumed in sync

A direct review caught something a self-driven sweep hadn't: "Axon Framework 5" was hyperlinked to this fork's own repository in three separate places — README.md, todo.md, and the wiki's home page — which read as a project linking to itself under someone else's name. Fixed consistently across all three the same session it was flagged, not left for the next person to notice.

Per-checkpoint discipline

todo.md gets updated at each real phase boundary — a module converted, a plugin written, a bug fixed — rather than batched at the end of a session where drift is easy to lose track of.

Caught by review, not hidden either way

Not every inconsistency gets self-caught before it ships. When one doesn't, the fix is disclosed the same way the original gaps are — including the fact that it took a second pair of eyes, not just the corrected result.

Judgment calls

Where the real trade-offs got made, and written down

Not every decision has an obviously-correct answer, and pretending otherwise would just move the judgment call somewhere less visible. This fork targets a Java 25 toolchain where upstream floors at 21 — a deliberate choice that measurably raises the minimum JDK needed to consume this fork's published artifacts, not a default left unexamined. Central Portal publishing (Sonatype's Maven Central replacement) has no official Gradle plugin at all, so the community one used here (com.gradleup.nmcp) was verified against its live documentation rather than assumed from training knowledge — which is exactly how gap #02 above got found in the first place. Both trade-offs are recorded in todo.md as deliberate choices, not oversights discovered later.