Skip to content

test(corpus): pair every detector, and fix what reading the refusals found - #1185

Merged
PIsberg merged 6 commits into
mainfrom
test/corpus-every-detector
Nov 3, 2026
Merged

PIsberg merged 6 commits into
mainfrom
test/corpus-every-detector

Conversation

@PIsberg

@PIsberg PIsberg commented Nov 3, 2026 •

Copy link
Copy Markdown
Owner

The corpus could not prove every detector works. 13 of 148 had no pair, each with a written reason, and 7 agent-fed detectors had pairs that only told them what happened. Every detector now has a row that must fire and a twin that must stay silent, and a detector with no pair fails the build.

Reading the refusals against their detectors, instead of adding rows around them, found real defects.

Defects this found

  • LIVELOCKS reported CRITICAL on healthy code. A probe drew it on platform workers from a body that only increments a counter: each worker sampled itself RUNNABLE in its own finally and its finished peers WAITING in the pool's queue, so a run that finished every round read as rapid state cycling. It saw nothing on the default virtual workers, and a real spin was a finding on neither. It now classifies the workers still in their bodies when a round times out, through Thread.getState(), on both kinds of worker. ConcurrencyRunner changed for this, and the runner.detector.inert detector=LivelockDetector log event is gone (a contract under invariant 8, so the test pinning it changed with it). So is a dumpAllThreads per body.
  • DEPRECATED_THREAD_API reported any name. It publishes DEPRECATED_APIS and documents the argument as one of them, and never read the list. This is a behaviour change for a consumer passing a name off the list, which now draws no finding.
  • VIRTUAL_THREAD_CPU_BOUND measured only the segment after the last yield, so one yield before the end excused a computation of any length.
  • COMPLETABLE_FUTURE_OBTRUDE_ABUSE needed a hand-written record of a call the agent had just woven. It is agent-fed now.

Decisions to check

  • virtualThread_yieldPointResetsTimer_noViolation was rewritten, not satisfied. It pinned the yield defect above: a 300 ms spin excused by one yield. It now holds ten 30 ms segments against a 250 ms threshold.
  • DEPRECATED_THREAD_API is not woven. Thread.stop, suspend and resume are gone in JDK 26 (checked with javap on 21 and 26), so a body cannot make the call on every JDK this builds on. Its pair is told.
  • EXPLICIT_GC and the obtrude detector stay at PROMPT, held in PairEvidence with the reason: a WeakReference test and a recovery path make those calls on purpose.
  • slf4j-nop is removed from corpus-eval. A provider in test sources uses slf4j-api's own BasicMDCAdapter, so a leaked MDC key is visible with one dependency fewer.

What a pair shows, and what it does not

  • EXPLICIT_GC and COMPLETABLE_FUTURE_OBTRUDE_ABUSE report every event they receive, so their twins hold the weave to selecting the right call rather than testing a judgement.
  • A recording-fed pair shows the detector separates what it was told. That is why PairEvidence holds those below VERDICT.
  • corpus-eval.md has a table of what each of the 13 refusals said against what reading the detector found.

How it was verified

Check Result
New unit tests, before the fixes failed: LivelockRoundTimeoutTest 4 of 4, plus the deprecated-name and yield tests
20 detectors made to report nothing (one-off) all 20 new firing rows red, and 7 older recorded rows
Deciding condition removed from 12 detectors (one-off) the 12 matching silent rows red
All six corpus lanes, JDK 26 on Windows pass: 311, 275, 148, 148 (112 skipped by design, library rows only), 78, 139
Fast tier across the reactor pass: 3903, 97, 13

Not verified: the EXPLICIT_GC twin and the 7 woven per-thread twins were not mutated, so nothing has shown those silent rows can fail (#1181). The full -P e2e suite and JDK 21 and 25 ran only in CI, not locally. The mutation runs were one-off and are not a standing gate.

What CI found that local runs did not:

  • Consumer Fixture and the 7 JUnit legs recorded "Thread.getId()", which the tightened list dropped. Thread.getId is deprecated since JDK 19, so it joined the list, and the changelog's Upgrading section tells a consumer passing another name.
  • Test Suite (21, ubuntu) failed aSpinningVirtualWorkerIsACandidate with an empty report, so no worker was in its body at the 300 ms deadline. timeoutMs counts from before the detectors are built. That cause fits the evidence and is not confirmed; the test and the corpus row now allow 2000 ms.
  • OS-Sensitive Tests (macos-latest) was cancelled once with no runner and no steps while githubstatus reported degraded service, and passed on re-run.

Docs

README, docs/DETECTOR_CATALOG.md, docs/detector-catalog/01-foundations.md, docs/analysis/corpus-eval.md, docs/analysis/corpus-eval-future-improvements.md, docs/agent/diagnostics.md, docs/architecture/runtime-guarantees.md, docs/DEPENDENCIES.md, docs/CHANGELOG.md, corpus-eval/README.md, examples/README.md and examples/07-livelock. Guardrails and diagrams are regenerated.

VibeTags: considered LivelockDetector.roundTimedOut and the new runner calls, and added nothing: the one fact worth keeping, that it runs before the interrupts, is a comment at the call. The existing @AILoadBearing note on execute named the removed snapshot and was corrected.

Closes #1161
Closes #1162
Closes #1163

Follow-ups: #1179, #1180, #1181, #1182, #1183, #1184

🤖 Generated with Claude Code

PIsberg and others added 6 commits November 3, 2026 21:23
…om peers' samples

LIVELOCKS reported CRITICAL on healthy code. A probe on platform workers drew it from a body that
only increments a counter: each worker sampled from its own finally saw itself RUNNABLE and its
finished peers WAITING in the pool's queue, so a run that finished every round alternated between
the two and read as rapid state cycling. On the default virtual workers the same samples were empty,
and a real spin was not a finding on either, so the detector could say nothing true.

The one observation that can show a livelock is that the body did not finish. The runner now marks
workers in and out of the body and asks the detector to classify those still inside when a round
times out, before it interrupts them. Thread.getState() answers for virtual threads too, so the
runner.detector.inert event for this detector is gone, and the dumpAllThreads per body with it. The
only threshold is the timeoutMs the test chose. captureSnapshot() stays for a body sampling peers.

Verified: LivelockRoundTimeoutTest failed 4 of 4 before this change (a false CRITICAL on healthy
bodies, silence on a spinning platform worker, a spinning virtual worker and a parked worker) and
passes after it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Found reading the detectors the corpus refused to pair, to find the silent half each refusal said
could not exist.

DeprecatedThreadApiDetector documented its name as one of DEPRECATED_APIS and never read that list,
so a wrapper recording every thread call reported Thread.interrupt at HIGH. Thread.stop, suspend and
resume are gone in JDK 26, which CI builds on, so weaving them is not an option: a body that compiles
everywhere can only record.

VirtualThreadCpuBoundTaskDetector measured the segment after the last yield, so one yield just before
the end excused everything before it, on a report that says "ran N ms without a yield point".

AgentConcurrencyUtilHooks already wove obtrudeValue and obtrudeException for the happens-before model
and told COMPLETABLE_FUTURE_OBTRUDE_ABUSE nothing, so it needed a hand-written record to see a call the
agent had just substituted. It is agent-fed now, which is what makes complete() a real silent twin.

Verified: both new unit tests failed before the change. The obtrude feed is held by the corpus pair
in the next commit, whose firing row depends on it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The corpus could not prove every detector works: 13 of 148 had no pair, each with a written reason,
and seven more agent-fed detectors had only pairs that told them what happened (#1161, #1162, #1163).
Read against their detectors, the refusals were out of date or named a silent half nobody had
written, so the list is gone and a detector with no pair now fails EveryDetectorIsPairedTest.

A row reading a clock fires by construction, because a slower machine only waits longer, and its twin
does not read the clock at all where that is possible. Latches force the gap LOCK_DOWNGRADE and
MEMORY_ORDERING were refused for needing. A slf4j-api BasicMDCAdapter behind a provider in test
sources replaces slf4j-nop, so a leaked MDC key is visible with one dependency fewer, and a stub under
the name the agent matches reaches the woven EntityManager path.

Verified: all six lanes pass locally on JDK 26. Each row's outcome was read from its report.

Closes #1161
Closes #1162
Closes #1163

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…does

COMPLETABLE_FUTURE_OBTRUDE_ABUSE sees the woven call now, so ASSERTED understated it and
DetectorEvidenceMatchesCodeTest refused the row. It is OBSERVED like EXPLICIT_GC. Both stay at PROMPT,
held in PairEvidence with the reason: a recovery path and a WeakReference test make these calls on
purpose and would draw the same finding, so a VERDICT would fail correct builds.

virtualThread_yieldPointResetsTimer_noViolation pinned the defect fixed earlier in this branch: a 300 ms
spin excused by one yield just before the end. It now holds what a yield is for, ten 30 ms segments
against a 250 ms threshold. LivelockRoundTimeoutTest drives the engine, so it carries @e2e as
E2eTagGuardTest requires; that means -P fast does not run it and -P e2e does.

Verified: the fast tier passes across the reactor (3903, 97 and 13 tests) and the e2e selection ran
LivelockRoundTimeoutTest, 4 of 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
README, the catalog, the corpus write-up, the changelog and the livelock example described refusals that
no longer exist and a LivelockDetector that sampled at the end of each body. A reader deciding whether
to trust a finding read the wrong thing, and the example's README promised output it could not produce.

The write-up keeps the earlier sections as the record of what was believed, and adds a table of what each
refusal said against what reading the detector found, the limits of a pair where the silent half tests
the weave rather than a judgement, and the one-off run that showed the rows can fail: with 20 detectors
silenced all 20 new firing rows went red, and with the deciding condition removed from 12 detectors the
12 matching silent rows did. The EXPLICIT_GC twin and the seven woven per-thread twins were not measured
that way, and it says so.

Guardrails and diagrams are regenerated through tools/guardrail-drift.sh's path list and
tools/generate-architecture-diagrams.sh, because the runner's load-bearing note and its sequence changed.

Verified: CorpusClaimsInDocsTest passes on all six lanes, which derives every count these documents state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…stuck round time to start

CI found two things JDK 26 on Windows did not.

Consumer Fixture and the seven JUnit legs recorded "Thread.getId()" and got no finding once the detector
read DEPRECATED_APIS. Thread.getId is deprecated since JDK 19, so the list was incomplete as well as
unread, and tightening it dropped a legitimate name without a word. It joins the list, the fixture uses
the listed spelling, and the changelog tells a consumer passing another name what changes.

Test Suite (21, ubuntu) failed aSpinningVirtualWorkerIsACandidate with an empty report, which means no
worker was in its body at the 300 ms deadline. timeoutMs counts from the start of execute, before the
run's detectors are built, so a loaded runner can spend it on setup. That cause fits the evidence and is
not confirmed. Both the test and the corpus row now allow 2000 ms.

Verified: LivelockRoundTimeoutTest, DeprecatedThreadApiDetectorTest and the consumer fixture (286 tests)
pass locally. Corpus Eval had already passed on JDK 21 and 25 on the previous commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Nov 4, 2026

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant