Five {@link} targets in main do not exist, and nothing in CI would ever say so #459

Closed
opened 2026-09-10 13:05:41 +02:00 by ltms · 2 comments
Owner

What I measured

While verifying #456 (a documentation-only PR) I ran the javadoc reference check, because for a
doc-only change that is the closest thing to a test. It fails on main.

Command, run in fleetd/ at main = 2af13ab:

mvn -B -DskipTests javadoc:javadoc -Ddoclint=reference

Exit code 1, BUILD FAILURE, five error: reference not found:

File Line The broken reference
msg/ReplyPushLoop.java 47 {@link #injectNudge(String, int, int, int)}
config/ConfigRef.java 180 {@link ConfigRefProfileCoverageTest}
config/FleetConfig.java 1548 (see the log line)
msg/AmqpReplyInbox.java 249 (see the log line)
msg/MessageService.java 716 (see the log line)

I did not check the last three by hand — read each error line in the build output rather than
trusting this table for them.

Two of the five have a clear cause worth naming, because they are different problems:

  • ReplyPushLoop.java:47 names a method signature that no longer matches. The method may
    have gained or lost a parameter and the javadoc was not updated. That is a link that rotted.
  • ConfigRef.java:180 links to ConfigRefProfileCoverageTest, a test class. Main-source
    javadoc cannot see the test classpath, so this link can never resolve. It should be
    {@code ConfigRefProfileCoverageTest} instead.

Why it matters

mvn clean test is green. mvn clean install is green. The javadoc goal is the only thing that
looks at these links, and nothing runs it — not the local build gate in CLAUDE.md, not
.gitea/workflows/ci.yml. So a comment can point at a method that does not exist and no check
anywhere says so.

This is the same shape as fleetd #449: a claim with no instrument behind it. In #449 the fake and
the real herdr disagreed for weeks because the one test that could tell them apart was excluded
from CI by name. Here the instrument is not excluded — it was never wired up.

The cost is small but real. These comments are the instruction surface for the next person who
touches the code. A {@link} that lands nowhere teaches them to stop following links.

Scope

  1. Fix the five broken references. For each one, say which of the two kinds it was: a signature
    that rotted, or a target javadoc can never see (a test class, a private member, a class outside
    the compiled sources). The second kind becomes {@code ...}.

  2. Add the check to CI as its own step in .gitea/workflows/ci.yml, so it fails a PR that adds a
    sixth. Suggested:

    - name: javadoc reference lint
      run: mvn -B -DskipTests javadoc:javadoc -Ddoclint=reference
    

    Use -Ddoclint=reference and not -Ddoclint=all. all also turns on the syntax,
    html and missing groups, and missing alone will report a very large number of
    undocumented members. Count them before you consider widening the group; do not widen it in
    this ticket.

  3. Report the count before and after, from the command, not from an estimate.

Acceptance criteria

  • mvn -B -DskipTests javadoc:javadoc -Ddoclint=reference in fleetd/ exits 0.
  • The count is quoted from the command's own output, and a control is shown: run the same command
    on the commit before the fix and show it printed 5. A zero that comes from a broken command
    proves nothing.
  • CI runs the check on every PR, and the PR shows a job log where the step actually ran.
  • No {@link} was fixed by deleting the sentence around it.

Found while verifying #456. Not caused by it: the same 5 errors appear on 2af13ab, which is
main before that merge, in the same 5 files, and none of them is a file #456 touched.

## What I measured While verifying #456 (a documentation-only PR) I ran the javadoc reference check, because for a doc-only change that is the closest thing to a test. It fails on `main`. Command, run in `fleetd/` at `main` = `2af13ab`: ``` mvn -B -DskipTests javadoc:javadoc -Ddoclint=reference ``` Exit code 1, `BUILD FAILURE`, five `error: reference not found`: | File | Line | The broken reference | |---|---|---| | `msg/ReplyPushLoop.java` | 47 | `{@link #injectNudge(String, int, int, int)}` | | `config/ConfigRef.java` | 180 | `{@link ConfigRefProfileCoverageTest}` | | `config/FleetConfig.java` | 1548 | (see the log line) | | `msg/AmqpReplyInbox.java` | 249 | (see the log line) | | `msg/MessageService.java` | 716 | (see the log line) | I did not check the last three by hand — read each error line in the build output rather than trusting this table for them. Two of the five have a clear cause worth naming, because they are different problems: - `ReplyPushLoop.java:47` names a method **signature** that no longer matches. The method may have gained or lost a parameter and the javadoc was not updated. That is a link that rotted. - `ConfigRef.java:180` links to `ConfigRefProfileCoverageTest`, a **test class**. Main-source javadoc cannot see the test classpath, so this link can never resolve. It should be `{@code ConfigRefProfileCoverageTest}` instead. ## Why it matters `mvn clean test` is green. `mvn clean install` is green. The javadoc goal is the only thing that looks at these links, and nothing runs it — not the local build gate in `CLAUDE.md`, not `.gitea/workflows/ci.yml`. So a comment can point at a method that does not exist and no check anywhere says so. This is the same shape as fleetd #449: a claim with no instrument behind it. In #449 the fake and the real herdr disagreed for weeks because the one test that could tell them apart was excluded from CI by name. Here the instrument is not excluded — it was never wired up. The cost is small but real. These comments are the instruction surface for the next person who touches the code. A `{@link}` that lands nowhere teaches them to stop following links. ## Scope 1. Fix the five broken references. For each one, say which of the two kinds it was: a signature that rotted, or a target javadoc can never see (a test class, a private member, a class outside the compiled sources). The second kind becomes `{@code ...}`. 2. Add the check to CI as its own step in `.gitea/workflows/ci.yml`, so it fails a PR that adds a sixth. Suggested: ```yaml - name: javadoc reference lint run: mvn -B -DskipTests javadoc:javadoc -Ddoclint=reference ``` Use `-Ddoclint=reference` and not `-Ddoclint=all`. `all` also turns on the `syntax`, `html` and `missing` groups, and `missing` alone will report a very large number of undocumented members. Count them before you consider widening the group; do not widen it in this ticket. 3. Report the count before and after, from the command, not from an estimate. ## Acceptance criteria - `mvn -B -DskipTests javadoc:javadoc -Ddoclint=reference` in `fleetd/` exits 0. - The count is quoted from the command's own output, and a control is shown: run the same command on the commit before the fix and show it printed 5. A zero that comes from a broken command proves nothing. - CI runs the check on every PR, and the PR shows a job log where the step actually ran. - No `{@link}` was fixed by deleting the sentence around it. Found while verifying #456. Not caused by it: the same 5 errors appear on `2af13ab`, which is `main` before that merge, in the same 5 files, and none of them is a file #456 touched.
Author
Owner

All five lines, from the build output, so the ticket does not send anyone hunting. Run in
/Users/dai.ha/LTMS/claude-bridge/fleetd at main = 2af13ab, exit code 1:

src/main/java/dev/ltms/fleet/msg/ReplyPushLoop.java:47: error: reference not found
 * ({@link #injectNudge(String, int, int, int)}). Work that arrives while the lead is busy is
           ^
src/main/java/dev/ltms/fleet/config/ConfigRef.java:180: error: reference not found
 * {@link ConfigRefProfileCoverageTest} shape (one level up, over {@code FleetConfig} itself rather
          ^
src/main/java/dev/ltms/fleet/config/FleetConfig.java:1548: error: reference not found
     * single name hardcoded in {@link HerdrPeerLauncher} rather than driven by config (gitea issue
                                       ^
src/main/java/dev/ltms/fleet/msg/AmqpReplyInbox.java:249: error: reference not found
     * is unlike {@link #handleRecovery} and {@link #close()}, whose bare {@code held.clear()} is
                        ^
src/main/java/dev/ltms/fleet/msg/MessageService.java:716: error: reference not found
     * genuinely {@code ASKING} was unrecoverable.</strong> {@link #resolveQuestion} had already
                                                            ^

So they fall into three kinds, not two:

  1. A signature that no longer matches — ReplyPushLoop:47, {@link #injectNudge(String, int, int, int)}. Check the current parameter list before you edit the text.
  2. A target javadoc cannot see — ConfigRef:180 links to a test class; FleetConfig:1548 links to HerdrPeerLauncher, which lives in dev.ltms.fleet.member and is not imported by FleetConfig. Both become {@code ...}, or FleetConfig's becomes a fully qualified {@link dev.ltms.fleet.member.HerdrPeerLauncher} if the link is worth keeping.
  3. A name that is simply wrong — AmqpReplyInbox:249 {@link #handleRecovery} and MessageService:716 {@link #resolveQuestion}. Find the real method name; do not delete the sentence.

FleetConfig:1548 is the interesting one for a reviewer. Its javadoc talks about a name
"hardcoded in HerdrPeerLauncher", so the sentence is about a real cross-package fact. That is
worth keeping and worth qualifying, not deleting.

All five lines, from the build output, so the ticket does not send anyone hunting. Run in `/Users/dai.ha/LTMS/claude-bridge/fleetd` at `main` = `2af13ab`, exit code 1: ``` src/main/java/dev/ltms/fleet/msg/ReplyPushLoop.java:47: error: reference not found * ({@link #injectNudge(String, int, int, int)}). Work that arrives while the lead is busy is ^ src/main/java/dev/ltms/fleet/config/ConfigRef.java:180: error: reference not found * {@link ConfigRefProfileCoverageTest} shape (one level up, over {@code FleetConfig} itself rather ^ src/main/java/dev/ltms/fleet/config/FleetConfig.java:1548: error: reference not found * single name hardcoded in {@link HerdrPeerLauncher} rather than driven by config (gitea issue ^ src/main/java/dev/ltms/fleet/msg/AmqpReplyInbox.java:249: error: reference not found * is unlike {@link #handleRecovery} and {@link #close()}, whose bare {@code held.clear()} is ^ src/main/java/dev/ltms/fleet/msg/MessageService.java:716: error: reference not found * genuinely {@code ASKING} was unrecoverable.</strong> {@link #resolveQuestion} had already ^ ``` So they fall into three kinds, not two: 1. **A signature that no longer matches** — `ReplyPushLoop:47`, `{@link #injectNudge(String, int, int, int)}`. Check the current parameter list before you edit the text. 2. **A target javadoc cannot see** — `ConfigRef:180` links to a test class; `FleetConfig:1548` links to `HerdrPeerLauncher`, which lives in `dev.ltms.fleet.member` and is not imported by `FleetConfig`. Both become `{@code ...}`, or `FleetConfig`'s becomes a fully qualified `{@link dev.ltms.fleet.member.HerdrPeerLauncher}` if the link is worth keeping. 3. **A name that is simply wrong** — `AmqpReplyInbox:249` `{@link #handleRecovery}` and `MessageService:716` `{@link #resolveQuestion}`. Find the real method name; do not delete the sentence. `FleetConfig:1548` is the interesting one for a reviewer. Its javadoc talks about a name "hardcoded in `HerdrPeerLauncher`", so the sentence is about a real cross-package fact. That is worth keeping and worth qualifying, not deleting.
Author
Owner

Fixed in #539, merged.

Verified on the merged tree, with a control

The branch was based on f1640f5 and main had moved to 57cd96f (#537), so I merged origin/main with the branch in a scratch worktree and tested that, not the branch. The merge was clean and the file sets are disjoint.

command exit error: reference not found
control, main 57cd96f mvn -B -DskipTests javadoc:javadoc -Ddoclint=reference 1 5
merged tree same command 0 0

The five errors on main landed in exactly the five files the ticket named: ReplyPushLoop:47, ConfigRef:211, FleetConfig:1673, AmqpReplyInbox:249, MessageService:716. So the zero after the fix comes from a command that demonstrably reports non-zero when there is something to report.

mvn clean install on the merged tree: exit 0, Tests run: 1704, Failures: 0, Errors: 0, Skipped: 0. The worker reported 1701 because that was its base; the merged tree picks up #537's three tests. Expected difference, not a discrepancy.

The CI step actually ran

This is the claim a green job does not support by itself, and it is why the ticket asked for the job log rather than the job result. Run 1791, job 2852:

step 4  "javadoc reference lint"   completed / success
step 5  "Failing test output"      completed / skipped

The API distinguishes the two states in the same job, and step 4 is not one of them. A misindented or conditionally-skipped step would have shown as skipped the way step 5 does.

The five, and I checked the signatures myself

file kind fix
ReplyPushLoop.java:47 rotted signature #injectNudge(String, int, int, int) → (String, int, int, int, int, int)
ConfigRef.java target javadoc can never see (test class) {@link ConfigRefProfileCoverageTest} → {@code ...}
FleetConfig.java rotted — class not package-qualified → {@link dev.ltms.fleet.member.HerdrPeerLauncher}
AmqpReplyInbox.java rotted member — wrong owner {@link #handleRecovery} → {@link RecoveryListener#handleRecovery(Recoverable)}
MessageService.java rotted member — wrong owner {@link #resolveQuestion} → {@link Rendezvous#resolveQuestion(String, String, String)}

I confirmed the three signature claims in the code rather than taking them from the report: injectNudge really takes six parameters at :737-739; resolveQuestion(String, String, String) is on Rendezvous:176; and handleRecovery is declared on an anonymous RecoveryListener at AmqpReplyInbox:201, not on AmqpReplyInbox itself — so {@link #handleRecovery} could never have resolved.

The two "wrong owner" cases are worth noting as a third kind alongside the two the ticket named. They are not stale signatures and not unreachable targets: the member exists and is reachable, it just lives on a different type than # implies. #foo is the easiest reference to get wrong, because it reads as correct right up until a tool checks it.

No {@link} was fixed by deleting the sentence around it. Every change is a reference swap with the prose intact.

Accepted and not fixed

The lint still emits 5 pre-existing invalid input: '<' warnings from Worktrees.java:8. They are warnings, not errors; -Ddoclint=reference does not fail on them; and widening the doclint group was explicitly out of scope here. The worker reported this rather than quietly leaving it, which is the right call — a caveat that goes unmentioned becomes a surprise for whoever next widens the group.

Fixed in #539, merged. ## Verified on the merged tree, with a control The branch was based on `f1640f5` and main had moved to `57cd96f` (#537), so I merged `origin/main` with the branch in a scratch worktree and tested **that**, not the branch. The merge was clean and the file sets are disjoint. | | command | exit | `error: reference not found` | |---|---|---|---| | **control**, main `57cd96f` | `mvn -B -DskipTests javadoc:javadoc -Ddoclint=reference` | 1 | **5** | | **merged tree** | same command | 0 | **0** | The five errors on main landed in exactly the five files the ticket named: `ReplyPushLoop:47`, `ConfigRef:211`, `FleetConfig:1673`, `AmqpReplyInbox:249`, `MessageService:716`. So the zero after the fix comes from a command that demonstrably reports non-zero when there is something to report. `mvn clean install` on the merged tree: exit 0, `Tests run: 1704, Failures: 0, Errors: 0, Skipped: 0`. The worker reported 1701 because that was its base; the merged tree picks up #537's three tests. Expected difference, not a discrepancy. ## The CI step actually ran This is the claim a green job does not support by itself, and it is why the ticket asked for the job log rather than the job result. Run 1791, job 2852: ``` step 4 "javadoc reference lint" completed / success step 5 "Failing test output" completed / skipped ``` The API distinguishes the two states in the same job, and step 4 is not one of them. A misindented or conditionally-skipped step would have shown as `skipped` the way step 5 does. ## The five, and I checked the signatures myself | file | kind | fix | |---|---|---| | `ReplyPushLoop.java:47` | rotted signature | `#injectNudge(String, int, int, int)` → `(String, int, int, int, int, int)` | | `ConfigRef.java` | target javadoc can never see (test class) | `{@link ConfigRefProfileCoverageTest}` → `{@code ...}` | | `FleetConfig.java` | rotted — class not package-qualified | → `{@link dev.ltms.fleet.member.HerdrPeerLauncher}` | | `AmqpReplyInbox.java` | rotted member — wrong owner | `{@link #handleRecovery}` → `{@link RecoveryListener#handleRecovery(Recoverable)}` | | `MessageService.java` | rotted member — wrong owner | `{@link #resolveQuestion}` → `{@link Rendezvous#resolveQuestion(String, String, String)}` | I confirmed the three signature claims in the code rather than taking them from the report: `injectNudge` really takes six parameters at `:737-739`; `resolveQuestion(String, String, String)` is on `Rendezvous:176`; and `handleRecovery` is declared on an **anonymous** `RecoveryListener` at `AmqpReplyInbox:201`, not on `AmqpReplyInbox` itself — so `{@link #handleRecovery}` could never have resolved. The two "wrong owner" cases are worth noting as a third kind alongside the two the ticket named. They are not stale signatures and not unreachable targets: the member exists and is reachable, it just lives on a different type than `#` implies. `#foo` is the easiest reference to get wrong, because it reads as correct right up until a tool checks it. No `{@link}` was fixed by deleting the sentence around it. Every change is a reference swap with the prose intact. ## Accepted and not fixed The lint still emits 5 pre-existing `invalid input: '<'` **warnings** from `Worktrees.java:8`. They are warnings, not errors; `-Ddoclint=reference` does not fail on them; and widening the doclint group was explicitly out of scope here. The worker reported this rather than quietly leaving it, which is the right call — a caveat that goes unmentioned becomes a surprise for whoever next widens the group.
ltms closed this issue 2026-09-12 08:46:50 +02:00
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: fleet/fleetd#459