# Technical Documentation: ben-manes/caffeine

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/ben-manes/caffeine)

### Overview (README.md)

[](https://github.com/ben-manes/caffeine/actions?query=workflow%3Abuild+branch%3Amaster)
[](https://github.com/ben-manes/caffeine/actions?query=workflow%3Abuild+branch%3Amaster)
[](https://coveralls.io/github/ben-manes/caffeine?branch=master)
[](https://central.sonatype.com/artifact/com.github.ben-manes.caffeine/caffeine)
[](https://www.javadoc.io/doc/com.github.ben-manes.caffeine/caffeine)
[](https://www.apache.org/licenses/LICENSE-2.0.html)
[](https://caffeine.develocity.cloud/scans)
<a href="https://github.com/ben-manes/caffeine/wiki">
<img align="right" height="90px" src="https://raw.githubusercontent.com/ben-manes/caffeine/master/wiki/logo.png">
</a>

Caffeine is a [high performance][benchmarks], [near optimal][efficiency] caching library. For more
details, see our [user's guide][users-guide] and browse the [API docs][javadoc] for the latest
release.

### Cache

Caffeine provides an in-memory cache using a Google Guava inspired API. The improvements draw on our
experience designing [Guava's cache][guava-cache] and [ConcurrentLinkedHashMap][clhm].

```java
LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
    .maximumSize(10_000)
    .expireAfterWrite(Duration.ofMinutes(5))
    .refreshAfterWrite(Duration.ofMinutes(1))
    .build(key -> createExpensiveGraph(key));
```

#### Features at a Glance

Caffeine provides flexible construction to create a cache with a combination of the following
optional features:

* [automatic loading of entries][population] into the cache, optionally asynchronously
* [size-based eviction][size] when a maximum is exceeded based on [frequency and recency][efficiency]
* [time-based expiration][time] of entries, measured since last access or last write
* [asynchronously refresh][refresh] when the first stale request for an entry occurs
* keys automatically wrapped in [weak references][reference]
* values automatically wrapped in [weak or soft references][reference]
* [notification][listener] of evicted (or otherwise removed) entries
* [writes propagated][compute] to an external resource
* accumulation of cache access [statistics][statistics]

In addition, Caffeine offers the following extensions:

* [JSR-107 JCache][jsr107]
* [Guava adapters][guava-adapter]
* [Simulation][simulator]

Use Caffeine in a community provided integration:

* [Spring Cache][spring]: Simple, modern, productive Java
* [Play Framework][play]: High velocity web framework
* [Micronaut][micronaut]: A modern, full-stack framework
* [Coroutines][caffeine-coroutines]: Kotlin Coroutines extension
* [Bootique][bootique]: A fast, simple Java platform
* [Quarkus][quarkus]: Supersonic Subatomic Java
* [Camel][camel]: Routing and mediation engine
* [Scaffeine][scaffeine]: Scala wrapper for Caffeine
* [JHipster][jhipster]: Generate, develop, deploy
* [Aedile][aedile]: Kotlin wrapper for Caffeine

Powering infrastructure near you:

* [Dropwizard][dropwizard]: Ops-friendly, high-performance, RESTful APIs
* [Cassandra][cassandra]: Manage massive amounts of data, fast
* [Coherence][coherence]: Mission critical in-memory data grid
* [Accumulo][accumulo]: A sorted, distributed key/value store
* [Kafka][kafka]: A distributed event streaming platform
* [HBase][hbase]: A distributed, scalable, big data store
* [Apache Solr][solr]: Blazingly fast enterprise search
* [Infinispan][infinispan]: Distributed in-memory data grid
* [Redisson][redisson]: Ultra-fast in-memory data grid
* [OpenWhisk][open-whisk]: Serverless cloud platform
* [Corfu][corfu]: A cluster consistency platform
* [Grails][grails]: Groovy-based web framework
* [Finagle][finagle]: Extensible RPC system
* [Neo4j][neo4j]: Graphs for Everyone
* [Druid][druid]: Real-time analytics

### In the News

* An in-depth description of Caffeine's architecture.
  * [Design of a Modern Cache: part #1][modern-cache-1], [part #2][modern-cache-2]
    ([slides][modern-cache-slides]) at [HighScalability][]
  * [The Adaptive Window, From the Ground Up][adaptive-window] on optimizing shifting workloads
* Caffeine is presented as part of research papers evaluating its novel eviction policy.
  * [TinyLFU: A Highly Efficient Cache Admission Policy][tinylfu]
    by Gil Einziger, Roy Friedman, Ben Manes
  * [Adaptive Software Cache Management][adaptive-tinylfu]
    by Gil Einziger, Ohad Eytan, Roy Friedman, Ben Manes
  * [Lightweight Robust Size Aware Cache Management][size-tinylfu]
    by Gil Einziger, Ohad Eytan, Roy Friedman, Ben Manes

### Download

Download from [Maven Central][maven] or depend via Gradle:

```gradle
implementation("com.github.ben-manes.caffeine:caffeine:3.3.1")

// Optional extensions
implementation("com.github.ben-manes.caffeine:guava:3.3.1")
implementation("com.github.ben-manes.caffeine:jcache:3.3.1")
```

For Java 11 or above, use `3.x` otherwise use `2.x`.

See the [release notes][releases] for details of the changes.

Snapshots of the development version are available in
[Sonatype's snapshots repository][snapshots].

[benchmarks]: https://github.com/ben-manes/caffeine/wiki/Benchmarks
[users-guide]: https://github.com/ben-manes/caffeine/wiki
[javadoc]: https://www.javadoc.io/doc/com.github.ben-manes.caffeine/caffeine
[guava-cache]: https://github.com/google/guava/wiki/CachesExplained
[clhm]: https://github.com/ben-manes/concurrentlinkedhashmap
[population]: https://github.com/ben-manes/caffeine/wiki/Population
[size]: https://github.com/ben-manes/caffeine/wiki/Eviction#size-based
[time]: https://github.com/ben-manes/caffeine/wiki/Eviction#time-based
[refresh]: https://github.com/ben-manes/caffeine/wiki/Refresh
[reference]: https://github.com/ben-manes/caffeine/wiki/Eviction#reference-based
[listener]: https://github.com/ben-manes/caffeine/wiki/Removal
[compute]: https://github.com/ben-manes/caffeine/wiki/Compute
[statistics]: https://github.com/ben-manes/caffeine/wiki/Statistics
[simulator]: https://github.com/ben-manes/caffeine/wiki/Simulator
[guava-adapter]: https://github.com/ben-manes/caffeine/wiki/Guava
[jsr107]: https://github.com/ben-manes/caffeine/wiki/JCache
[maven]: https://central.sonatype.com/artifact/com.github.ben-manes.caffeine/caffeine
[releases]: https://github.com/ben-manes/caffeine/releases
[snapshots]: https://central.sonatype.org/publish/publish-portal-snapshots/#consuming-snapshot-releases-for-your-project
[efficiency]: https://github.com/ben-manes/caffeine/wiki/Efficiency
[tinylfu]: https://dl.acm.org/doi/10.1145/3149371?cid=99659224047
[adaptive-tinylfu]: https://dl.acm.org/doi/10.1145/3274808.3274816?cid=99659224047
[size-tinylfu]: https://dl.acm.org/doi/10.1145/3507920?cid=99659224047
[modern-cache-1]: https://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html
[modern-cache-2]: https://highscalability.com/blog/2019/2/25/design-of-a-modern-cachepart-deux.html
[modern-cache-slides]: https://docs.google.com/presentation/d/1NlDxyXsUG1qlVHMl4vsUUBQfAJ2c2NsFPNPr2qymIBs
[HighScalability]: https://highscalability.com
[spring]: https://docs.spring.io/spring-framework/reference/integration/cache/store-configuration.html#cache-store-configuration-caffeine
[scaffeine]: https://github.com/blemale/scaffeine
[kafka]: https://kafka.apache.org
[hbase]: https://hbase.apache.org
[cassandra]: https://cassandra.apache.org
[solr]: https://solr.apache.org/
[infinispan]: https://infinispan.org
[neo4j]: https://github.com/neo4j/neo4j
[finagle]: https://github.com/twitter/finagle
[druid]: https://druid.apache.org/docs/latest/configuration/index.html#cache-configuration
[jhipster]: https://www.jhipster.tech/
[open-whisk]: https://openwhisk.apache.org/
[camel]: https://github.com/apache/camel/blob/master/components/camel-caffeine/src/main/docs/caffeine-cache-component.adoc
[coherence]: https://docs.oracle.com/en/middleware/standalone/coherence/14.1.1.2206/develop-applications/implementing-storage-and-backing-maps.html#GUID-260228C2-371A-4B91-9024-8D6514DD4B78
[corfu]: https://github.com/CorfuDB/CorfuDB
[micronaut]: https://docs.micronaut.io/latest/guide/index.html#caching
[play]: https://www.playframework.com/documentation/latest/JavaCache
[redisson]: https://github.com/redisson/redisson
[accumulo]: https://accumulo.apache.org
[dropwizard]: https://www.dropwizard.io
[grails]: https://grails.org
[quarkus]: https://quarkus.io
[aedile]: https://github.com/sksamuel/aedile
[bootique]: https://bootique.io/
[caffeine-coroutines]: https://github.com/be-hase/caffeine-coroutines
[adaptive-window]: https://ben-manes.github.io/caffeine/wiki/adaptive-window.html

---

### .Claude Docs Audit Output (`.claude/docs/audit-output.md`)

# Audit Output Location

Write reports and transient analysis under `.local/`, grouped by producing model:

```
.local/audits/<model>/<name>.md
```

- **`<model>`** — the short id of the model that produced the report, lowercased, no vendor
  prefix and no context-window suffix: `opus-5`, `fable-5`, `sonnet-5`, `haiku-4.5`,
  `gpt-5.6-sol`. When a run spans models (an auditor on one, an evaluator on another), use the
  model that **orchestrated** it. An assigned `AUDIT_REPORT_PATH` overrides this derivation
  entirely; see Rules for agents.
- **`shared`** occupies the `<model>` slot for artifacts that are not one model's output: the
  consolidated backlog, a bug spec worked across sessions, anything aggregating several runs.
- **`<name>`** — the invoking skill's name (`audit-adversarial.md`). A multi-agent run suffixes
  per agent (`audit-jcache-conformance-groupABC.md`, `audit-sibling-divergence-groupA.md`) and
  a verification pass adds `-verification`; the canonical unsuffixed name is the synthesis.
  **Never overwrite a report you were dispatched to verify, consolidate, or read.**

There is no date or cycle level. Rerunning an audit under the same model replaces its report.

Examples:

```
.local/audits/opus-5/audit-jmm.md
.local/audits/gpt-5.6-sol/audit-jmm.md                  # same audit, other provider
.local/audits/fable-5/audit-adversarial.md
.local/audits/fable-5/audit-adversarial-evidence/r1-challenges.md
.local/audits/shared/audit-consolidated.md              # cross-model queue index
.local/audits/shared/queue/<section>/<item-id>.md       # one work item
```

Keep `.local/` between sessions for handoffs and cross-model comparison. It is gitignored and
machine-local; checked-in guidance must remain useful if that tree is absent.

## Consolidated queue

Cross-audit consolidation, including `run-audits.sh --consolidate`, uses this layout:

- `.local/audits/shared/audit-consolidated.md`: brief workflow instructions, counts by status, and
  section tables with linked ID, status, and short subject. Claims and evidence go in item files.
- `.local/audits/shared/queue/<section>/<item-id>.md`: one item per file. Sections, in order, are
  `core`, `async`, `jcache`, `guava`, `simulator`, `examples`, `build-ci`, `docs`. ID prefixes match
  the section except `simulator` uses `sim`, `examples` uses `ex`, and `build-ci` uses `ci`.
- `.local/audits/shared/queue/history.md`: consolidation provenance, report inventory, coverage
  limits, and prior cross-item decisions. Consult relevant portions, not the entire history.

Each item starts with `# <item-id>: <short subject>`, `Status: <status>`, and `Severity: <severity>`.
Include a `Prior rulings and context` section linking to named anchors in `history.md`, then the
current decision and next step, original claim, source-report links and finding IDs, and earlier
evidence/counterarguments. Distinguish reported measurements from current checks.
Use `open` for pending work (including disputed claims), `unverified` for unresolved reachability,
contract, or impact, `resolved` for an evidenced repair, and `closed` for an agreed no-fix outcome.

Extend the queue in place. Reuse IDs for matching claims; assign new numeric IDs above the section's
maximum, never recycling or renumbering them. Separate distinct subclaims without dropping their
parent ID. Preserve resolved/closed decisions when claims recur, and retain contrary evidence.
Never prune items to meet a survival ratio.

Map history entries by mechanism and scope, including family members not named by ID. Label
near-matches and historical counterarguments without treating them as current dispositions.
Give each history entry a stable named anchor. When adding or revising one, update the affected
items' links. If a mechanism review finds no related entry, record that explicitly with the review
date; a missing section means unreviewed. Neither case exempts an item from canonical module rules.

After each pass, refresh index links and counts from item files, using an existing helper if
available. Verify report coverage, retained items/counterarguments, and relative links. Work from
the selected item and relevant rulings; use a fresh context for unrelated items and leave a concise
handoff. Per-skill synthesis reports remain in the producing model's directory.

## Rules for agents

- The orchestrator computes the output path once and passes it to every agent as
  **`AUDIT_REPORT_PATH`**. Use that path, not the agent's self-reported model name; group and
  verification siblings go beside it. `.github/scripts/run-audits.sh` exports it;
  `.claude/agents/auditor.md` Phase 4 and `.claude/hooks/audit-report-guard.sh` honor it.
- The auditor agent must not **read** anything under `.local/audits/` (prior conclusions bias a
  fresh run) while still being required to **write** its own report there. See
  `.claude/agents/auditor.md` Evidence Boundaries.
- Long-running tooling (`audit-temporal-walk`) takes its model from the `AUDIT_MODEL`
  environment variable, since a shell-launched walk cannot know it; the invoking agent exports
  its own model id. `audit_paths.reports_dir` falls back to an existing tree for the module when
  that variable is missing, so a resumed walk still finds its `state.json`.

---

### .Claude Docs Audit Rounds (`.claude/docs/audit-rounds.md`)

# Audit Rounds

Read when starting or triaging a batch `/audit-*` round.

Complete the audit batch before repairs. Use the
[consolidated queue format](audit-output.md#consolidated-queue), verify no claims or counterarguments
were dropped, then work one item at a time and revisit coverage affected by fixes. Keep the tracker
current across sessions. A full cycle has typically taken 2–3 weeks.

**Serialize the runs.** Quota exhaustion mid-run *breaks* an audit rather than pausing it, so
never fire runs in parallel. `/audit-adversarial` is the single priciest skill (roughly half a
weekly quota: 8 reviewers plus their evaluator challenges), so order the batch value-descending
and an early cutoff still banks the most bugs.

**Prefer a second model on a skill over a third model repeating it.** Prior paired runs produced
model-unique findings, including their most severe claims. The preferred order is:
`subsystem-safety`, `liveness`, `feature-interaction`, `jcache-conformance`,
`sibling-divergence`. `build-ci` and `serialization` are the least worth it.

**Leave `/audit-jdk-seam` out of a batch.** It is kept for an occasional manual run, not the
normal checklist.

**Down-weight, but do not skip:** re-entrancy (a full pass has closed at zero, and callback
re-entrancy warnings are not wanted, which removes the remedy from most of what it finds);
simulator periphery (a testing tool — its bugs mislead benchmarks, they do not harm users);
and the formal-shape lenses (jmm, linearizability, arithmetic, correctness-proof,
map-contract), which are cheap to run but have gone several passes without a core defect.

**A report row is a claim, not a confirmed defect.** An earlier sweep reported 186 claims,
26 after source review, and 7 after standing rulings; even high-rated claims were rejected.
Those historical counts do not establish a rejection target or release confidence. Check the
contract and matching module rulings first. Use a focused reproduction for a behavior repair;
a source/contract proof can settle a documentation correction or no-code disposition. Preserve
the trigger, controls, counterarguments, and evidence limits.

**Use `general-purpose`, not `auditor`, for verification, triage, and consolidation passes**
over existing reports. The auditor carries a mandatory-report-write gate (`SubagentStop`) that
will pick a canonical filename and overwrite the source report you are consolidating from. If
you must use it for reuse, assign it an explicit `-verification` path.

---

### .Claude Docs Design Decisions (`.claude/docs/design-decisions.md`)

# Design Decisions

Intentional behavior and review invariants, with their rationale and evidence. Read the
relevant section before changing a mechanism; use [ruled-out](ruled-out.md) for adjudications.

## Eviction

**Weight=0 is a pinning feature.** Entries with weight=0 are skipped during eviction
(`evictFromWindow`, `evictFromMain`, `evictEntry`). This is an intentional user-facing
API (inherited from ConcurrentLinkedHashMap where weight was >=1, Guava's Cache where >= 0).
Used internally for in-flight async futures.

*The re-scan cost is accepted.* `evictFromWindow` restarts at the deque head each cycle and
never relocates a zero-weight node, and `evictFromMain` skips its victim inline, so cold
pinned entries at the LRU end are re-traversed whenever the region is over budget — O(pinned)
per cycle in the worst case. A dedicated zero-weight queue to hold them out of the scan was
explored and rejected: it made the eviction paths messier for little value. Don't propose it
again without a measurement showing the scan actually costs something.

**Region transfers are budgeted per maintenance cycle and re-armed.** `evictFromWindow` and
`demoteFromMainProtected` move at most `QUEUE_TRANSFER_THRESHOLD` entries per cycle and set
`PROCESSING_TO_REQUIRED` when a backlog remains, because a `setMaximum` shrink can leave either
region arbitrarily oversized and one cycle would otherwise drain it whole under `evictionLock`.
The climber's `increaseWindow` / `decreaseWindow` carry their remainder in `adjustment` instead
(see the climber entries below). A region briefly over its maximum right after a resize is the
budget, not a leak; the protected demotion lacked the re-arm from the 2019 adaptive commit until
the 2026-08 audit sweep, which is why an idle cache could sit with protected oversized until the
next access. Don't remove either re-arm.

**Transient negative weightedSize is acceptable.** `maximumSize` allows eviction
before/after threshold. Eventual consistency is fine given documented promises.
Weight convergence is guaranteed by the telescoping sum property across all write
buffer task orderings.

**Weights are static after computation.** The weigher is called at creation/update
time. The weight is not recalculated afterward — relative weights don't influence
eviction ordering, only total capacity accounting.

**`FrequencySketch.reset()`'s pause is accepted, and chunking it is the wrong trade.** The reset
halves every counter under `evictionLock`: 0.29 ms at a 100k maximum, 2.0 ms at 1M, 32 ms at 10M,
238 ms at 100M. It is rare in proportion to the cache, since `sampleSize` is `10 × maximum` and the
reset fires when that many increments have been recorded, so a 100M cache resets once per billion
recorded reads, about 0.24 ns/read amortised. Amortising the pause means chunking it, which lowers
eviction quality by querying a half-reset sketch. The default async eviction hides the pause
entirely, and on a caller-runs executor the worst case is still cheaper than the misses the sketch
prevents. The `count += Long.bitCount(table[i] & ONE_MASK)` odd-counter correction is a
cross-iteration reduction and is what stops the loop auto-vectorizing; dropping it is much faster
but loses the correction, and no argument has justified that. SIMD is the answer once it is
available. It has produced no user reports and does not show in `GetPutBenchmark`.

**A weak-key lookup allocates, and that is accepted.** The weak-key node factories build a
`LookupKeyReference` per lookup (20.0 ns/op and 24 B/op against 2.6 ns/op and 0 B/op for strong
keys); it is handed to `ConcurrentHashMap.get`, so escape analysis cannot remove it. Avoiding the
allocation means caching one wrapper in a thread-local and mutating it to set and unset the
referent around each lookup, which pins that instance to the thread. That idea was raised and
rejected in issue #294 over virtual threads, where an instance per virtual thread is unbounded,
and over classloader pinning. A young-generation allocation is the accepted trade.

**Policy weight uses a signed 62-bit payload returned as a `long`.** Reordered update tasks
are normal: the value is written under the node monitor but the task is queued after it is
released, so a second writer can queue first. The node's own field then walks past the true value
and back, which the design tolerates. What it cannot tolerate is truncation. Once the intermediate
sum leaves the int range, a `transfer` copies the truncated value into the long region totals, and
nothing replays that copy: the node settles at its real weight while `windowWeightedSize` or
`mainProtectedWeightedSize` keeps a permanent 2^32 residue. Measured 2026-09-08 with weights near
`Integer.MAX_VALUE`, the window then reads as permanently over its maximum, `evictFromWindow`
drains it every cycle, and every later arrival goes straight to probation, so the admission window
is dead for the life of the cache. The released 3.2.4 and 3.2.5 reproduce it, so it is not new.
A sign test on the transfer does not close it: half the wraps land positive, where the value is a
plausible weight. The write buffer bounds queued contributions, but not writers paused before
publishing their updates, including inside inline removal listeners. The historical witness
reached 34 bits; no practically reachable overflow of the packed payload has been demonstrated.
`policyWeight` holds the low 32 bits and `metadata`'s spare 30 bits the signed high portion, at no
extra layout cost (JOL over all 147 node classes is unchanged, where a plain `long` field grows the
16 strong-value weighted classes by 8 bytes). Pinned by
`BoundedLocalCacheTest.put_reorderedUpdates_leaveNoRegionResidue`, which orders two writers through
a removal listener on a direct executor. Don't re-narrow the field, and don't guard the transfer
on the sign instead.

**Two notions of "weighted", and the internal one gates on both.** The `isWeighted` field is whether
the caller configured a weigher, and it is what `Policy.Eviction` reads for `isWeighted()`,
`weightOf` and `weightedSize` (`CaffeineTest.maximumWeight_large` pins this for an explicit
`Weigher.singletonWeigher()`, where the method answers false). `BoundedLocalCache.isWeighted()`
answers a different question, whether entries may be assigned *different* weights, and that is what
decides whether the frequency sketch can be sized from the maximum or has to be sized from the live
entry count. Neither test alone answers it: an async cache always wraps its weigher in
`AsyncWeigher`, so identity against the singleton weigher says "varies" even for an unweighted one,
while a caller who passes `Weigher.singletonWeigher()` explicitly sets the field even though every
entry weighs the same. The method conjoins them. Sizing the sketch from the entry count is not free:
`ensureCapacity` forgets all counts when it grows, so filling a `maximumSize(10_000)` async cache
allocated the sketch twice (8192 then 16384) and discarded the frequencies gathered over the first
half, where the sync equivalent allocates 16384 once. It also called `data.mappingCount()` on every
insertion past half the maximum. The one case still answered wrongly is an async cache given an
explicit `Weigher.singletonWeigher()`, which needs unwrapping `AsyncWeigher` to reach and costs only
that same extra warmup allocation. `BoundedLocalCacheTest.isWeighted_onlyWhenWeightsVary` pins the
cases answered correctly.

**The climber has three size tiers, in the configured maximum's native units.** Weight units
are deliberate: the weighted stress track included about 200 entries of 25–100MB in a 10GB
cache and scored 14 wins / no losses. A later comparison on 18 byte-weighted trace cells found
no consistent reactive advantage at low resident counts. Grow-only entry-count selection gave
small, mixed changes; bidirectional selection repeatedly reset the controller on cells crossing
the threshold. Retain the configured-weight threshold on this evidence; see
[entry-count tier selection](hill-climber.md#entry-count-tier-selection-for-weighted-caches).
Many tiny entries under a small weight bound land on reactive, which remains safe there.

| Maximum | Controller | Reason |
|---|---|---|
| ≤512 | Slow reactive: grow first, minimum initial step 2, decay 0.995 rather than 0.98, period stretch capped at 4× | Integer windows and noisy samples make an initial shrink a no-op and prevent later reversal. |
| 513–4096 | Standard reactive | Density was near-neutral below ~2048 (+0.12–0.24pp), but lost up to 10pp on corda+loop at 513–1024. |
| >4096 | Goal-audited density | Added about 125pp across the large cells of the 48-trace set while accepting small frequency-trace regressions. |

The gates are `SLOW_ADAPT_THRESHOLD` and `DENSITY_THRESHOLD`; slow tuning uses
`SLOW_ADAPT_DECAY_RATE` and `SLOW_ADAPT_RATIO_CAP`.

**Density compares regions within one sample.** It uses
`ln((windowHits / windowMaximum) / (mainHits / (maximum - windowMaximum)))`, with a step
proportional to error (`DENSITY_GAIN = 0.03`), capped at `MAX_STEP_FRACTION` (30% of maximum).
This avoids the cross-sample workload swings
that obscure reactive gradients on a flat hit-rate curve, but it is average resident density,
not marginal value. Its equilibrium depends on the starting window; ghosts would supply a
different signal but were rejected for cost. Extending density below 4096, even with the probe
machine, lost 1.3pp on `cs@563`. Kickoff, regret, anneal, and wide-start variants traded one
small-cache trap for another rather than removing the bias.

**Blind corners require experiments.** A region with fewer than `requestCount >>
MIN_SIGNAL_SHIFT` hits (~0.1%) supplies too little evidence to hold position. Pure density
can pin at the floor about 28pp below LRU at any size. Probe only when the starved region is
small (≤¼ maximum), or the whole sample is dead; probing for a starved large main region
destroys the functioning window on corda. Deep refractory rungs increase both reach and
commitment, while cheap early adjudication protects thin-signal floors. Failures command a full
return and back off (`PROBE_BACKOFF_INITIAL` 16 through 64 samples), costing about 1pp on short
w50/S1 traces and amortizing toward
zero with longer runs. Preserve the current router and ownership rules in
[climber review constraints](#climber-review-constraints) and [hill-climber](hill-climber.md) §4.

The evidence behind the asymmetric verdicts and crash prices remains relevant:

- Starvation-walk interior noise pricing is `min(max(5pp, 3×rateDeviationEma), 15pp)`.
  It recovered 8.2pp on the dosed-mixture trap; dropping the cap lost 0.6pp on `metronome`.
- Audit crashes retain the depth bound and buy persistence in time. The first crash is cheap;
  an already-crashed equilibrium's retry tolerates two below-bar samples and aborts on the
  third. This raised the constructed moat from 41.97 to 44.3; a 22-cell real-corpus scan found
  no echo. Giving every walk tolerance failed `mixture_d025` / `mixmod` on short traces.
- Sharing crash state paired independent pulses into rung 64 / clock wait 128 and a
  130-sample floor pin, 6pp below LRU. Non-crash endings retire only their owner's streak;
  an audit confirm alone also clears starvation state when resetting that ladder. Pins:
  `audit_budgetExpiry_leavesTheStarvationLedger` and
  `walkStep_reversalThroughBase_leavesTheOtherLayersLedger`.
- An up-probe prices the capacity it takes from probation, frozen at arm. Main's protected
  average vetoed useful positions on `trickle` (14–17pp below its own engine); live probation
  became enriched by the walk's demotions and vetoed every escape on `demoflood`. Down-probes
  keep the average comparison. The low-hit-rate `lowmix` trade remains a gate sentinel, with
  no real-trace echo across the defended corpus.
- Crash veto precedes adjudication, so a destroyed region cannot win by a density ratio
  against another region earning nothing. A neutral verdict is not a success (`S3`). The
  current reversed-confirm rule is in the checklist below; older blanket reward rules no
  longer describe the machine.

**The density floor is 2% (`WINDOW_FLOOR_FRACTION`), with an upward clamp from below it.**
The initial window is 1%, so
a clamp that only blocks further shrink can wedge below the intended floor. At size 600 a
0.5% floor held only three entries and the recency phase scored about 23% versus 44% optimum.
The 2% large-cache safeguard cost at most 0.35pp on frequency-optimal workloads; kickoff and
EWMA-regret escapes lost up to 8pp on w50 or only moved transiently before retrapping.
Re-run corda+loop at 4097–8192 before lowering the floor. Resizing resets probe state and
`sample.previousHitRate`, so the new geometry is not judged against an old sample.

**The sketch's shrink retrack and `reset()`'s zero clamp are a matched pair.** `ensureCapacity`
keeps the table (it is grow-only, since reallocating wipes the counts and blacks out admission)
but re-points `sampleSize` at the new maximum, so a shrunken cache does not age on its old size's
cadence; the observed count is clamped to just below the sample to keep the equality reset test
reachable. That retrack breaks the precondition `reset()` was written under: `count/4 < size` held
only while the table matched the sample (`count/4 ≤ 8·maximum < 10·maximum = sampleSize`), so
without a floor at zero the correction underflows to a large negative `size` and no counter is
halved until `++size` climbs back — restoring exactly the old-size cadence the retrack removes,
and doing it for roughly `4 × tableLength` additions across successive resets. Measured
(2026-08-07, audit-arithmetic F1): a 64K-entry cache shrunk to 256 lands at `size = −62,579`,
about 25 sample periods, and recovery is driven only by *novel* traffic — 1M requests against
saturated counters advanced it by 932. Reachable with no `Policy` call: a weighted cache retracks
on every addition, so updating resident entries to heavy weights collapses the entry count
(200,000 → 20 measured) and drives it. During the stall `admit()` degrades to tie-breaking, so a
post-shrink working set cannot displace stale survivors. The clamp is inert whenever the table
matches the maximum, so it costs nothing on the normal path. Don't remove either half. Pinned by
`FrequencySketchTest.ensureCapacity_shrink_denseTable_agesOnSchedule` — note its neighbour
`_shrink_resetReachable` exercises the same flow on an *empty* table, where the correction is 1,
which is why the underflow shipped.

The retained table's reset cost after a large shrink is accepted. After a 1M-to-1K
shrink, an 8 MiB table took 1.94 ms under `evictionLock`, about 194 ns per increment amortized
against 0.19 ns when table and maximum match. Cost scales with peak/current ratio: a 10× swing
was negligible, while ~1000× was visible. This is reachable through unweighted
`Policy.Eviction.setMaximum` as well as weighted retracking. Hysteretic shrinking was declined
because it conflicts with per-addition weighted retracking and loses the admission history.

The large-cache **sample period is `SAMPLE_MULTIPLIER × maximum` (4×), decoupled from the
frequency sketch's own 10× reset** (they use separate counters — `sample.hits`/`sample.misses`
gate
the climber, `FrequencySketch.size` gates the sketch reset). At 10× a large cache gets only one to
three adaptations over a finite benchmark trace and never reaches a large (20–50%) optimal window; 4×
gives it enough steps to converge. It is kept at 4× rather than lower because a shorter period makes
the density estimate near a small converged window noisy enough to jitter a frequency-friendly
workload off its optimum — the `cs` trace craters at 2×. The density signal is inherently
recency-biased (it over-values the window), so frequency-optimal traces (`fiu_madmax`, `cs`,
`fiu_webmail`, several ARC traces) give back ~0.5–2pp versus the reactive climber; on every one they
still beat LRU and match/beat Merlin and stay near-optimal, and the trade buys large recency-workload
gains (corda's cliff, `OLTP`/`fiu_ikki`/`metaCDN`/`fiu_homes` converging to their ceiling). An
*adaptive* period (short while moving, long once settled) was tried and **rejected**: starting short
made dup-heavy/phasey traces jitter persistently and never lengthen, cratering `cs` worse than a fixed
2×. Don't reintroduce it.

**The climber is held until the cache is half full, and the gate is residency, not the sketch's
existence.** `climb()` skips `determineAdjustment` while `weightedSize() < (maximum() >>> 1)`, discarding the
sample through `WindowClimber.discardSample` once it fills, and still resets it outright when the
sketch is uninitialized. A cache below that
line is not evicting, so its sampled hit rate says nothing about the window/main split, and the
sketch's `sampleSize` caps the sample period, so a sketch sized for less than the maximum reads
that meaningless signal on a compressed cadence. `Caffeine.initialCapacity(n)` is one way in (the
generated constructor calls `ensureCapacity(min(maximum, n))`) and growing `maximum` through
`Policy.Eviction.setMaximum` is the other. Measured 2026-09-08 on `product.Caffeine`: a
`maximumSize(400_000)` cache hinted at `initialCapacity(1000)` scored 29.27 on `arc/S3` against
41.04 un-hinted, `arc/DS1@1M` 8.76 against 12.48, `arc/P3@100k` 33.50 against 40.43, and the
window sat at 47–80% of the maximum forty million operations later where the un-hinted arm rested
at 26%. The sketch reinflates on schedule at half fill; the window it drove to does not come back,
which is why the old acceptance ("a transient that self-heals as the cache fills") was wrong. Don't
weaken the gate back to the sketch's initialization alone, and don't latch it. The unlatched form
is what closes the `setMaximum` growth case, and it is two field reads under `evictionLock`. Pinned
by `BoundedLocalCacheTest.adapt_partiallyFilled_holdsWindow` / `adapt_filled_climbsWindow`.

**Climber `adjustment` is a multi-cycle carry-over, not stale state.** `increaseWindow` /
`decreaseWindow` transfer at most `QUEUE_TRANSFER_THRESHOLD` (e.g. 1000) nodes per maintenance
cycle, then store the *unfulfilled* remainder back into the climber's `adjustment`
(`quota` / `-quota`) — the leftover, not zero. On a large cache the per-decision step
(`≈ 0.0625 × maximum`, e.g. 62,500 at a 1M maximum) dwarfs the (e.g 1000) node cap, so a single
climber decision is deliberately drained across many later cycles. Each of those cycles
`determineAdjustment` early-returns at `requestCount < effectiveSampleSize` (the sample was
reset and has not refilled) **without touching `adjustment`**, and `climb` re-applies the
carried remainder. Once the sample refills, a fresh `determineAdjustment` overwrites
`adjustment` with a new decision. This looks like "a stale adjustment re-applied without a
fresh hit-rate sample," but it is the completion mechanism for a work-capped transfer.

The symmetric give-back after the transfer loop (`mainProtectedMaximum += quota;
windowMaximum -= quota`) keeps the partition sum (`windowMaximum + mainProtectedMaximum +
implicit-probation == maximum`) constant — the maxima track the *partial* transfer that
actually happened (added by "Fix bugs in adaptive policy"). Three consequences
worth not flagging:
- **Pinned leftover.** If the carried `quota` is smaller than the policy weight of every
  candidate (e.g. `quota = 1` while all entries weigh 100), the loop moves nothing and
  re-stores the same value, so the window stays put until a real sample overwrites
  `adjustment`. The window genuinely cannot grow by a fraction of an indivisible heavy entry.
  `increaseWindow` may already have demoted protected entries against its provisional cap.
  Giving back the quota restores the caps, not those entries' positions; later promotion depends
  on accesses and available protected capacity.
- **Probation is the implicit slack region.** The transfer draws from both probation and
  protected but only decrements `mainProtectedWeightedSize` for protected moves, so probation
  absorbs the difference between the window-maximum shift and the protected weight moved
  (`Δ windowMaximum == total weight transferred` holds exactly).
- **The quota is a soft knob, not an accounting invariant.** `quota` is how much room the
  climber may borrow from the other region for a probabilistic guess about an unknown future;
  taking a little too much or too little is meaningless. A node carrying a transient negative
  `policyWeight` (a same-key `UpdateTask` reordered against its predecessor — see "Two weight
  fields") passes `quota < weight` and *inflates* the quota via `quota -= weight`, so the
  give-back can overshoot and move `windowMaximum` / `mainProtectedMaximum` opposite to the
  commanded direction, or out of `[0, maximum]`. That is not a defect and must not be clamped:
  the partition sum still holds, the region *weighted sizes* stay exact (they debit the same
  snapshot they credited), a negative `windowMaximum` / `mainProtectedMaximum` only makes
  `evictFromWindow` / `demoteFromMainProtected` drain that region, a policy-quality wobble. An
  out-of-range cap walks back only by the weight each later transfer moves: the
  `min(adjustment, donor)`, `<= 1` and `max(0, …)` guards stop a call from pushing it further
  out, they do not pull it back. A swing larger than a cycle's transfer (one key's weight
  swinging by more than the window holds) therefore suspends the split for many cycles, the
  window cap above `maximum` idling `evictFromWindow` while `evictFromMain` still bounds the
  total; the verdict does not turn on the duration. The invariant that matters is
  that `policyWeight` *converges*, so a region's size keeps reflecting the entries inside it;
  how a mid-flight snapshot lands on the quota does not. Clamping the quota also would not
  restore a reservation — it just relocates the inaccuracy from the maxima to the transfer
  volume. The duration of the excursion does not change this ruling.

The hardening companion to this: `ReactiveClimber.samplePeriod` guards the small-cache
`ratio` against a `0/0` NaN (when both the maximum and step size are zero). The NaN would
otherwise zero the effective sample size, defeat the sample guard, and poison
`sample.previousHitRate`. Neither construction nor reactive decay reaches that state.
Generated constructors call `WindowClimber.resized(maximum())`, which seeds a step of at least
`2.0`, including for `maximumSize(0)`; decay never rounds it to exactly `0.0`. Keep the guard
as defensive hardening. `BoundedLocalCacheTest.adapt_smallCache_zeroMagnitudeDoesNotPoisonHitRate`
explicitly forces the zero step.

**The climber commands in `double`; the cache applies `(long)` of the command, truncated
toward zero once at publication.** Positions, region maxima, and the walk's base are `long`, and
position identity is band-based (`Reading.stableBand`, 2% of the maximum), so a command's lost
fraction is not itself an error. Two consequences follow and are accepted: `walkStep`'s
base-crossing predicate reads the continuous command, so a reversal whose truncated landing is
exactly the base fails the walk one sample earlier than an integer predicate would (the same
ending, one sample cheaper); and the 2% floor is a `double`, so the integer window rests one entry
below it (pinned by `walkStep_floorBasedWalk_endsAtBudgetWithAFullUndo`), and at maxima above
2^53 weight units the floor comparison has a rounding band of a few units, which is unreachable.
Don't make the floor or the predicate integral.
**Undo accounting closes in integral published commands:**
`undoRemaining` is a `long` charged with each return command as published, not with the
fractional capped stride. Charged with the fraction it closed short of the base by the cap's
fraction per capped stride (8,192: 2,457 + 2,457 + 84 for a 5,000 return), and at a permanently
starved corner, where every deep-rung probe fails and undoes, that re-based each cycle 1–2 entries
toward the probed direction, a slow creep toward the corner boundary. Pinned by
`probeEnding_adjudication_wrongSignFailsAndDoubles` (the commands sum to the distance).

**Physical undo arrival is best effort.** The cache can retain unapplied movement when an entry
does not fit or the transfer budget runs out; the next completed sample replaces that carry.
Several capped strides can each lose a remainder, and later probes can inherit the resulting
landing. There is no one-entry bound per return or across returns, and the transfer-cap case also
affects unweighted caches. Retaining the base and reconciling actual movement was measured in two
forms: bounded to the original return duration, or continued until arrival/nonprogress. Across ten
weighted cells at eight seeds, mean object-hit-rate changes were only -0.050 to +0.060pp, while the
latter prolonged retreats and showed a throughput slowdown signal. Retain the approximation on
that evidence, not an assumption that displacement is harmless or confined to tiny caches. See
[reconciling failed-probe returns](hill-climber.md#reconciling-failed-probe-returns) for scope and costs.

**Async load completions replace quietly.** A completed future's `handleCompletion` (and the
bulk `fillProxies`) calls `replace(..., quietly= true)`: the UpdateTask finalizes the weight and
expiration but skips `onAccess`'s sketch increment and climber hit counters. The entry already
paid its miss at insertion; counting the completion as an access doubled the key's per-load
admission frequency and window-attributed one synthetic, write-buffer-lossless hit per miss —
measured at up to −38.6pp (w50) on the density climber and −12.7pp (corda+loop stress @ 512) on
the reactive climber. A
material quiet update (weight changed, or the write time moved beyond the 1s tolerance) routes
through the UpdateTask and still reorders the deques; an immaterial one skips policy work.
With access expiry alone, a slow same-weight completion can leave a live head ahead of older
expired entries when the tail is newer. Cleanup can defer reaping those entries until the head
is reordered or expires; reads still filter them. This is the same accepted delayed reaping as
when a read-buffer reorder is dropped. User-initiated writes remain loud by design. Don't re-add
access recording to the completion path, and don't flag the immaterial-completion skip as a
missing reorder/refresh. Pinned by
`ExpireAfterAccessTest.quietCompletion_staleOrder_defersPhysicalExpiration`.

The `refreshIfNeeded` completion's remap is quiet the same way (`RemapHints.quietly`, honored at
`remap`'s update dispatch): the triggering read already recorded its access, so a loud reload
completion double-counted every refresh-eligible read into the sketch and the climber's hit
sample (1000 reads → 2000 of each once past the refresh interval). A reload finalization is
bookkeeping, not a usage. Manual `LoadingCache.refresh` / async `tryComputeRefresh` completions
are unchanged (explicit per-call API action, no read-stream amplification); revisit only with a
measured skew. Pinned by `BoundedLocalCacheTest.refreshCompletion_doesNotRecordAccess`.

A committed refresh completion always takes the material branch, so `remap`'s immaterial one is
quiet-capable but unreached in production (a rejected one returns at the `preserveTimestamps`
exit, before the dispatch). `refreshIfNeeded` requires `refreshAfterWrite`, which
makes `exceedsWriteTimeTolerance`'s refresh disjunct true at every completion: either the
configured duration is within the 1s tolerance, or the refresh fired because the entry aged past a
duration longer than the tolerance. The `quietly` guard on that branch is not defense-in-depth, it
is the same contract `replace` implements for the async completions, whose callers do not require
`refreshAfterWrite` and do settle immaterially. Don't delete it as dead code; it is pinned through
the `compute(..., hints)` seam by `BoundedLocalCacheTest.remap_quietly_doesNotRecordAccess` and its
loud twin.

**In-place reloads record a climber miss.** `computeIfAbsent`, `compute`, `put`, and `putIfAbsent`
reuse a node when its entry has expired or its value was collected. Their `UpdateTask` uses
`Access.RELOAD` to increment the sketch and record the miss an `AddTask` would record after
physical removal. `replace` rejects dead entries. `Access.QUIET` takes precedence for internal
refresh completions.

For caches with size eviction, a nonquiet reload must use `UpdateTask` even when the weight is
unchanged and timestamps are within tolerance: the read buffer always records `Access.HIT`.
Access expiration without write expiration exposed this missing case in `remap`;
`expiredRemap_recordsClimberMiss` covers both expiration modes.

Steady contamination can cancel in cross-sample rate differences, but it biases the density
tier's within-sample regional ratio. In an earlier synthetic reuse study (maximum 512, reuse
gap equal to expiry), misattribution moved the converged window from 402 entries to 5 across
all seeds and both maintenance lags. Nearly all false hits were in main, and reloads rose from
4.2% to 13.6%. Those measurements concern the earlier reload-accounting fix, not the
same-weight `remap` case. No end-user hit-rate loss has been established for either case.

The simulator's `product.Caffeine` uses neither expiration nor reference values, so its gate
cannot exercise this path. Pins: `BoundedLocalCacheTest.expiredReload_recordsClimberMiss`,
`expiredRemap_recordsClimberMiss`, `expiredPut_recordsClimberMiss`, and `put_recordsClimberHit`.
They also check the sketch increment, which a quiet update would incorrectly suppress.

**Random admission of rejected candidates.** The TinyLFU admission filter admits 1 in 128
candidates of frequency 6 or more that would otherwise be rejected. This is the HashDoS
protection: an entry whose frequency hash collisions have raised, whether it is the victim or was
admitted as an inflated candidate, is eventually evicted rather than holding its place. It lets the
inflated entry leave the cache; it does not prevent its admission (see
[ruled-out](ruled-out.md#core), *Eviction and maintenance*).

## Climber review constraints

The current machine and its rejected alternatives are described in [hill-climber](hill-climber.md)
§4–6. Preserve these distinctions when changing related branches:

- **Recovery takes samples.** A density sample is 4× maximum requests; typical log error of
  1.5–2 yields steps of 4–6% of maximum. Descending from an 80% window takes 13–16 samples,
  or 52–63× maximum requests. On ten frequency-optimal cells, 41 samples recovered 73% of
  an 80% plant, two samples recovered 2%, and 4× replay cut the deficit 3–8×. Density recovered
  roughly twice as much as reactive. Shortening the period needs a measurement of its jitter cost.
- **Reactive reversal substitutes for a floor.** The reactive tier can reach a one-entry
  window; `Reading.floor`'s 2% floor belongs only to density. A banded reactive law without a
  floor drove corda from 30.96 to 1.13. Any change that sustains its runs must revisit the floor.
- **Setpoint density has an accepted burst bias.** Budgeted transfers can leave actual window
  occupancy above `windowMaximum`, inflating its density. Candidates still pass `admit` and
  `LocalCacheSubject` checks the bound at quiescence. Do not substitute occupancy without measuring.
- **References differ by purpose.** Up-probes freeze probation density and its sample length
  at arm, scaling the baseline to the live sample length; down-probes compare one sample.
  Starvation-walk deviation pricing stays live. Audits freeze their rate reference and use a
  raw-sample streak plus one beat-base test. The rail's `3×deviation` price is separate.
- **Each layer owns its state.** Crash streaks and ladders belong to their walk. Non-crash
  endings clear only that layer's streak, so alternating crash/budget endings do not repeatedly
  get tolerance. Crashes never double the audit clock. Stillness alone advances that clock;
  moving samples decrement its run by one rather than reset it. Only interior-chosen directions
  update the alternation bit; a due audit pre-empts a refractory hold.
- **Walk exits use different bars.** Audit crash abort is a level test against the arm's frozen
  rate, with 5pp depth capped at `AUDIT_BAR_FRACTION` of that rate. Reversal is a first-difference
  test, priced by `AUDIT_BAR_FRACTION × max(baseHitRate, noiseBand)` under the same absolute cap.
  That fraction is `0.15 × VETO_MARGIN_SCALE`; widening the bars failed the measured controls.
  A floor-based walk can miss `crossesBase` through integer rounding; budget expiry gives the
  same FAILED price and full return command, pinned by
  `WindowClimberTest.walkStep_floorBasedWalk_endsAtBudgetWithAFullUndo`.
- **Anchor discard and metric reset stay paired on crash-scale shifts.** The inherited EMA is
  about 80% old regime. A distant crash keeps the claim and reference. On-anchor `resync` runs
  even during return drain; planting waits for both a walk and its pending undo to finish.
  Symmetric claim aging lost 11.9pp on the ramp control; one-sided aging disarmed the rail.
  The pairing also holds inside a starvation walk, whose crash and reversal bars the reset widens
  to their cap: a census of the battery found seven such stand-downs, all on gains, which neither
  bar judges, and skipping the reset through 38 of them on seeded runs changed no hit rate.
- **An audit judges against the position it leaves.** `Walk.baseSmoothedRate`, not `Anchor.rate`,
  is its baseline (`ghostclaim` 31.2 → 48.5; `cp_w100` +2.0). Discarding a claim on a still swing
  loses retreat recovery (`moat_h3000`, −0.7 to −1.9 on all eight seeds); resetting the metric
  while keeping the claim sends the rail to a dead anchor (`ghostclaim_p30` 41.2 vs 49.2).
- **A return retests its frozen claim only at its original position.** `RETEST_SETTLE = 2`
  avoided the seed losses of 1 and 3; `ghostclaim_p35/p40` improved 31.7 → 33.9 and 31.4 → 33.5.
  Retest follows `anchor.returning`, rechecks `isAt`, and is cleared by both `discard` and `plant`.
  Clearing only on stand-down misses budget-expired returns and confirmed-position replants
  (#2002). Do not retest a claim after its anchor has moved onto a position the return never reached.
- **A reversed starvation confirm is a completed experiment.** Escalate its ladder and hand
  to density with zero refractory; rewarding it restarted a recurring dither (668/881 confirms,
  `bandtrap2` −4.4pp and an absorbing `shallowmoat`). Accepted costs include `arc_DS1` −0.7,
  `deadphase` −0.2, and `norank_rep_r6` seed 3 falling 41 → 20 while seven seeds were unchanged.
  The unlanded `wedgeshift` guard needs a holdout. Only deepest-commitment, audit-grade confirms
  park; other starvation confirms keep their density handoff.
- **Park and refractory scopes remain narrow.** A parked audit covers its own walk's crash-scale
  move, but an external shift and undo arrival still judge the park (`demoflood` −1.9 if widened).
  Only a starvation undo arms the starvation refractory. Removing audit-undo rearming bought
  `widepin` +5.1, `rep_r6` seed 3 +5.4, `shallowmoat` +1.1 for `metronome` −0.9,
  `balloonflip` −0.3 and `cp_w050` −0.55.
- **Repeat confirms deepen rather than reward.** Per-direction memory tracks the farthest
  confirmed window; starvation failure/crash clears it, audit endings and anchor discard do not
  (`absolve_p8` 27.95 → 46.3). A park's first audit follows its confirming direction only while
  the park and smoothed-rate guard still hold. Consume that exception at arm; later audits
  alternate. Raw-rate substitution fails `absolve_p12`; unguarded direction costs `moat_h5000`
  1.75pp and fails `climbtrend_up` / `whisper_mod_p6`.
- **Main-space experiments already priced the alternatives.** Across 276 cells, plain-LRU main
  lost 93.1pp net (mean −0.337); SLRU won >1pp on 40 cells versus nine, 38/46 traces preferred it,
  and N=3 had no sign flips. Partial promotion gates were worse than either extreme (−25.7/
  −20.3pp tails), because sparse protected entries stay outside probation's victim pool.
  The 80/20 split and one-hit promotion beat alternative constants; a perfect per-cell oracle
  was only +0.21pp after the max-of-N noise floor, with winners changing across sizes on all
  46 traces. Wrong constants cost −6.16pp to −25.72pp. Probation stays near 19.8%; the density
  window borrows protected capacity over roughly [2%, 80.2%].

Audit confirms park without a parting steering step; starvation confirms normally do neither.
The room check and walk use `Ladder.stride`. Before changing floor-crossing arithmetic, use
seeded admission comparisons to distinguish a repair from a shifted probabilistic outcome.

## Expiration

**EXPIRE_TOLERANCE = 1 second.** Expiration is a maximum lifetime,
not a minimum hold time. Like ScheduledExecutorService, the timing is never exact.
The tolerance applies to multiple per-entry timestamps:
- `writeTime` reorder decisions when a write updates a live value
  (`exceedsWriteTimeTolerance`) — avoids write buffer saturation from rapid timer wheel
  rescheduling, ~4x throughput on write-heavy workloads. A write over an expired or collected
  value is a creation and restarts the write time.
- `accessTime` updates on the read path — avoids cache-line true-sharing on a hot
  entry under `expireAfterAccess`. When the configured duration is `<= tolerance`
  the skip is bypassed so tiny expiration windows still behave exactly.
- `variableTime` updates on the read path — the same saving under an `Expiry`, with the same
  bypass. Only an extension is skipped: a read that moves the deadline earlier is stored, since
  skipping it would serve the entry past the deadline the `Expiry` asked for. An unchanged
  deadline is not rewritten, even inside the bypass. Under `Expiry.creating` or `writing` every hit
  in an entry's last second keeps its deadline, and rewriting it was a CAS that cost ~45 ns per hit
  on 8 threads. The skip is pinned by
  `BoundedLocalCacheTest.expireTolerance_variableTime_skipsForUnchangedShortDuration`.

**A `put` dates a new entry from its call, not its insertion.** The create path reads the clock
when it builds the node, before `data.putIfAbsent`, and a retry after a node-monitor wait reuses
that node. An insertion that waits on another thread's bin (a same-key computation that returns
null, a bin neighbour's computation, a synchronous eviction listener in maintenance) publishes the
entry already aged by the wait, so it expires early by that much, or is a miss on the next read
and an `EXPIRED` notification when the wait outlasts its duration. That is not a defect: the
contract does not say when the clock starts, and either placement satisfies it. The caller obtained
the value before the call, so after an hour's wait the value is an hour older, and dating it from
the call does not overstate its freshness. A computation dates its result after the function
instead (#191), because the cache produced that value under the lock. The price of a back-dated
insert is in the order deques: appended at the tail with the oldest time, it makes the next
`expireAfterWriteEntries` scan relink every fresher live head before reaching it (one walk of the
live deque, once per such insert), and in access order it can linger unreadable behind live
heads until they expire, with size eviction reporting it as `EXPIRED`.

**ASYNC_EXPIRY = ~220 years** (`Async.java`). Computing futures get this sentinel
duration to prevent expiration during async computation. The `isComputingAsync()`
check tests both the `isAsync` flag AND whether the future is complete.

**The sentinel also records that a load has not been accounted for, so every
read-extension path preserves it.** `AsyncExpiry.expireAfterUpdate` routes to the
user's `expireAfterCreate` when `currentDuration > MAXIMUM_EXPIRY`, which is how
`handleCompletion`'s quiet replace tells a first load from an update. `AsyncExpiry`
cannot make that distinction on a read: it keys off `getIfReady`, so once the value
arrives it delegates and returns a real duration. The guard therefore belongs in the
cache, and both `tryExpireAfterRead` and `expireAfterRead` carry it (`isAsync &&
currentDuration > MAXIMUM_EXPIRY` returns without consulting the `Expiry`). The
window it covers is not the in-flight state, which `AsyncExpiry` handles: it is the
value arriving while the completion waits on the node lock a `putIfAbsent` is
holding. Consuming the sentinel there also hands the user's callback a 220-year
`currentDuration`, and an `Expiry` that returns what it was given, `Expiry.creating`
among them, then pins the entry for that span instead of expiring it after its
configured duration.

**The completion finalizes only what the insertion deferred.** `handleCompletion`
finalizes an entry with a same-instance quiet `replace`, which lands on the update path.
For a future that was in flight when it was inserted that is the entry's first and only
evaluation, since the install stored the sentinel and `AsyncExpiry` routes it back to
`expireAfterCreate`. This includes an incomplete future replacing a ready value: it begins a
pending lifecycle, then receives creation expiry on materialization. `AsyncTest.asyncExpiry_pending`
pins a pending update discarding a real prior duration; `asyncExpiry_completion` pins the
subsequent creation callback. A future that was **already complete** when inserted was weighed and
dated by that write, so finalizing it again charged the creation as an update: the user's
`expireAfterCreate` ran at the install and their `expireAfterUpdate` a moment later, and
the update's duration is the one the entry kept. Measured with an `Expiry` of 1h on create
and 1m on update, `put(k, completedFuture(v))` and a loader returning a completed future
both produced a 1m entry where the synchronous cache and an in-flight future produced 1h.
So the callers pass whether the insertion deferred its accounting and the completion skips
the replace when it did not. A ready future replacing a live entry likewise takes one
update rather than two. Reachability is not exotic: any loader that can answer without I/O
returns a completed future.

Three things about the shape are load-bearing. **The readiness is read before the store**,
because a future observed as ready before the install was ready when the install evaluated
it, whereas one observed afterwards may have completed in between, and skipping there
leaves the entry holding weight 0 and the sentinel permanently (measured: moving the read
after the `put` stranded 2,950 of 20,000 entries against a completion race, versus 0 of
500,000 with the read where it is). **The decision cannot live in `AsyncExpiry`**: its
`expireAfterUpdate` receives the key, the new value and the current duration, so a
completion's `replace(k, f, f)` and a genuine `put` of an equal ready future are identical
from inside it; the sentinel test only works for the deferred case because the entry still
carries the mark. **And it must not be `AsyncExpiry.expireAfterCreate` deferring every
creation** — the synchronous view's `asMap()` compute family installs completed futures
with no completion handler at all, so deferring strands them at the sentinel and the user's
`expireAfterCreate` is never called (9,013 failures across the narrowed async matrix when
tried).

**The flag is conservative rather than exact, so the write re-tests the mark.** Reading
before the store rules out one direction only. A future observed as ready was ready at the
install, but one observed as unready may still complete before the install evaluates it, and
the stale flag then charges as an update a creation the install already accounted for.
Measured on the shipped defaults with an ordinary future and an `Integer` key, four to nine
puts in every 200,000 take that path, and a key whose `hashCode` completes the future makes
it deterministic, yielding a 59s entry for the 1h/1m `Expiry` above. No read point fixes it,
since readiness is monotonic and neither side of the store is the observation the install
used, and narrowing the window is not a repair. So `replace` asks the entry instead. A quiet
write, which only a completion performs, preserves the variable time when the entry no
longer carries the sentinel, and still finalizes the weight and the write time. Nothing else
can have cleared the mark, because the two read-extension paths above return early on it.

**MAXIMUM_EXPIRY = ~150 years** (`Long.MAX_VALUE >> 1`). User-provided variable expiration
durations are clamped to prevent nanoTime arithmetic overflow; fixed `expireAfterWrite` and
`expireAfterAccess` durations are stored as given (see *Timing and arithmetic* in
[ruled-out](ruled-out.md) for a near-`MAX` one under a scheduler). `now + ASYNC_EXPIRY`
overflows to negative after ~73 years of JVM uptime, but this is within the
documented assumption that JVM instances don't run for 73+ years continuously.

**accessTime uses opaque write, not CAS.** CAS on every read would cause contention
storms on hot entries. Backward movement only causes benign early expiration, which
is acceptable. Variable time CAS (`casVariableTime`) is justified because
`expireAfterRead` can change duration arbitrarily.

**Read-path expiry extension can briefly resurrect a just-expired entry — accepted.**
A reader that observed an entry live and then extends it (`tryExpireAfterRead`'s
`casVariableTime` for variable expiry, or `setAccessTime` for `expireAfterAccess`) can land
the extension just after the entry crossed its boundary, leaving it visible slightly later than
expiry. This is inherent to lock-free read-extension over lazy expiration: the expired entry
keeps its timestamp until maintenance removes it, so the CAS — which only checks the field is
unchanged — cannot reject an expired entry, and any fresh-clock guard before the write still
races a context switch between the read and the write; only a read-path lock (rejected) would
close it. The window is a few instructions for a normal `Expiry` callback; a wide one requires a
slow `expireAfterRead` (callback misuse, like a slow `Weigher`). So "never visible later than
expiry" is best-effort for read-extension — the over-stay is bounded by one duration and
self-heals on the next maintenance. Don't add a *fresh-clock* re-check guard for this
over-stay — it still races a context switch and can't reject an already-expired entry.
(Distinct from the `node.getValue() == value` value-identity check that `casVariableTime`
*does* carry: it stops a read duration rebinding onto a *replaced* value and is load-bearing;
keep it. It covers a replacement during the `expireAfterRead` callback because a `loadLoadFence`
orders the deadline the CAS expects before the value it checks, pairing with `setValue`'s
`storeStoreFence` as `hasExpired`'s fence does. Without it, aarch64 satisfied the value load first,
so a reader saw the replacement's deadline with the old value, passed the check, and gave the
replacement the old value's read duration, later than its own when that duration is longer
(`ExpiredReadTear.ReadExtension`). It does not cover a replacement in the few instructions between
the check and the CAS: an update that keeps the exact deadline, as an `expireAfterUpdate`
returning `currentDuration` does, leaves the timestamp the CAS compares unchanged, so that
replacement takes the read's duration, a same-deadline rebind bounded by one duration like the
over-stay above. The adapter accepts the same race for its native timer.)

**Bulk reads evaluate expiry at a single scan-wide `now`, by design.** `getAllPresent`
(and `containsValue`) read `expirationTicker()` once and reuse that `now` for every
element's `hasExpired`/`setAccessTime`/`tryExpireAfterRead`. This gives the batch
*internal consistency* — every key judged at one instant, a point-in-time snapshot. A key
late in a long scan can therefore be returned present just after a concurrent single-key
`get` (fresh `now`) reported it expired (a LATE-direction over-stay). That's accepted
best-effort: a concurrent single-key read can always disagree with a bulk read on a
boundary entry under lock-free expiration, the over-stay self-heals on the next
maintenance/access, and it lasts no longer than the scan (measured at up to about 2 ms for
10,000 keys and 140 ms for 1,000,000), inside `EXPIRE_TOLERANCE` unless a *user*
`Expiry.expireAfterRead` callback is slow (callback misuse). Don't "fix" this by re-reading
the ticker per element — it judges keys of the same call at different instants (a downgrade
of the snapshot) and adds a `nanoTime` read per key on the hot path.

**A removal's cause is attributed at the instant that removal is performed, and `clear()` is not
an exception to that.** `clear()` reads the ticker once and passes that `now` to `removeNode`,
which amortizes the read across the entries it removes under the eviction lock and calls a
user-supplied `Ticker` once rather than once per entry while holding it. It is a cost decision,
not a point-in-time guarantee, and it cannot become one: the sweep breaks out as soon as
`writeBuffer.size()` reaches `WRITE_BUFFER_MAX / 2`, and every straggler goes through the public
per-key `remove(key)`, which reads its own clock. Measured on 200,000 entries with the deadline
crossed after the sweep began, one writer running: the prefix covered by the captured clock was 5
and 18 entries in two of three trials, with the remaining ~199,990 attributed `EXPIRED` by their
own reads. Uncontended, the same call reported all 200,000 `EXPLICIT`. Across methods the gap is
the same thing from outside: with a ticker stepping 1 ms per read and the deadline set midway
through, `invalidateAll()` reported 1,000 `EXPLICIT` where `invalidateAll(keys)`, a loop of
independent `remove(key)` calls, reported 250 `EXPLICIT` and 750 `EXPIRED`. So cause counts are
not comparable between the two bulk calls and are not stable within one `clear()`. The consequence
is confined to the removal cause and `evictionCount`, both best-effort. Don't read the captured
`now` as a snapshot: making it one needs an internal removal variant carrying it through the
straggler path, and dropping it puts a per-entry ticker call back under the eviction lock.

**A hit probes the value future's readiness only where the answer is consumed.** `hasExpired`
is timestamp-only, so a reader probes `isComputingAsync` solely on an expired verdict, and the
successful-read blocks in `getIfPresent` and `computeIfAbsent`'s optimistic hit test
`expiresAfterRead()` (access or variable) before their own probe, since `setAccessTime` and
`tryExpireAfterRead` early-return otherwise. `Async.getIfReady` calls `isDone`,
`isCompletedExceptionally` and `join`, each an acquire load of `CompletableFuture.result` that
the JIT cannot elide or coalesce, so a `maximumSize`-only async hit was paying six of them for
a result no branch could read. Measured on `AsyncGetPutBenchmark.read_only` (M3 Max, JDK 26, 8
threads, 3 forks, ABA): 238–246M ops/s before, 370–384M after, **+53%**; an acquire load is
`ldar` on arm64, so x86 should gain less. The probe count per healthy hit is 0 with no
expiration or with expire-after-write, 1 for expire-after-access and 2 for variable expiry (the
extra one is `AsyncExpiry.expireAfterRead`'s own `getIfReady`), pinned by
`BoundedLocalCacheTest.getIfPresent_readinessProbes`. Don't cache the first probe's answer to
save the second: a future can complete, or be obtruded, between the two observations.

**The expiry read protocol pairs timestamp-before-value reads with value-before-timestamp
writes.** A lock-free read must never return a value whose EXPIRED notification a concurrent
rewrite already fired. `hasExpired` is therefore timestamp-only and every lock-free reader
consults it before loading the value; a `loadLoadFence` at the end of `hasExpired` (repeated
before `tryExpireAfterRead`'s value-identity check) and a `storeStoreFence` in the generated
`setValue` hold both orders on weak memory, and `put`
stores the value before `setWriteTime` (the other rewrite sites already did). A reader that
observes a fresh timestamp therefore observes the rewritten value, closing the LATE direction.
The EARLY direction (stale timestamp with the fresh value) is one spurious miss that
linearizes between the old value's expiry and the rewrite, a 64-bit timestamp read being
atomic, unless a reader already returned the fresh value: a writer preempted between its value
and timestamp stores across the old deadline shows the value, a miss, then the value again. The
other non-linearizable windows are the read-extension resurrection and same-deadline rebind
above, the bulk single-`now` scans, a read preempted after its clock sample (it judges a value
published later against that clock, so it can return a replacement past its deadline, even one
created expired), a compute whose function outlasts its input's remaining lifetime (a
concurrent read reports the input expired before the result is stamped with a fresh clock),
and a `put` or `replace` over a live entry, which judges liveness under the node monitor before
calling `expireAfterUpdate` (a read crossing the old deadline in between reports the key absent
while the write returns or replaces that value with `REPLACED`; a few instructions for fixed
expiry, the callback's run time for variable expiry).
Each is bounded; closing them needs a read-path lock or a second ticker read per read, both
rejected above, or applying the function twice, which the compute contract forbids. A caller
acting on an expired verdict must exempt an in-flight async load by
probing `isComputingAsync` against a value loaded after `hasExpired` returns. `writeTime`'s
setter is opaque (it was plain, a formal tearing gap on 32-bit VMs), and the swap in `put`
shifts a nanosecond `refreshIfNeeded` window from suppressing a refresh to launching one
wasted best-effort reload, absorbed by the reservation re-check and the ABA commit guards.
Pinned by `ExpirationFrayTest.getIfPresent_expiringRewrite_neverReturnsExpiredValue` (failed
on the first iteration before the reorder) and the `ExpiredReadTear` jcstress test, whose
old-modes model reproduced the tear at 0.21% of samples on aarch64 while the fixed pairing
produced zero across a tough-mode soak. The measured price on the M3 Max is nil: the reader
fence sits within run-to-run drift on `HotEntryBenchmark`'s expiring configs, and the writer
fence is below `GetPutBenchmark.write_only`'s noise floor (interleaved baseline/fixed forks;
that cell drifts more between forks than any fence effect, so judge it with paired runs, not a
plain before/after). Don't move a reader's `getValue()` above its `hasExpired` call, and don't
reorder a writer's `setValue` below its timestamp stores.

**Expiration eviction is capped at `EXPIRATION_THRESHOLD` (1000) entries per maintenance
cycle.** `expireAfterAccessEntries` (shared across its window/probation/protected deques),
`expireAfterWriteEntries`, and the variable `TimerWheel.advance` each evict at most this
many entries, then set `PROCESSING_TO_REQUIRED` so `rescheduleCleanUpIfIncomplete` re-arms
and the backlog drains across subsequent cycles — mirroring `drainWriteBuffer`'s cap and the
climber's `QUEUE_TRANSFER_THRESHOLD`. The cap is high enough that normal traffic never
reaches it; it only bounds the abnormal spike where a cache with no `Scheduler` goes idle,
lets a large population expire logically, then returns to traffic — one maintenance cycle
would otherwise evict the whole backlog under `evictionLock`, stalling any writer that
overflows the write buffer and assists (post "Assist maintenance directly when the write buffer is full"). The work isn't reduced, only
sliced, and since eviction runs async by default the slicing keeps a single cycle from
blocking a thread too long. The **timer wheel** rewinds `nanos` to `previousTimeNanos` when
its budget is exhausted and restores the unprocessed bucket remainder, so the next advance
reprocesses the backlog. Empty buckets rescan cheaply, but a large advance can cause
rescheduled survivors to be revisited after a rewind. The eviction check keeps non-expired
nodes from being evicted early. A capped cycle can briefly leave expired entries counting
toward `weightedSize`, so a same-cycle
`evictEntries` could pick a live victim over an expired one; negligible — frequency-based
selection favors the cold expired entries and it self-corrects next cycle. Don't flag the
cap as under-expiring, and don't remove the `PROCESSING_TO_REQUIRED` re-arm.

**The expiration scans hold no entry across a callback except their captured tail.** `evictEntry`
delivers the removal notification; under `executor(Runnable::run)` or the rejection fallback the
listener runs inline, and a `RemovalListener` is permitted to modify the cache, so a nested
`maintenance()` can unlink, transfer, or reorder any entry before the scan continues. Reentrancy
is not a supported style and cannot be detected or refused, so the requirement is only that it
not corrupt or wedge the scan. After `evictEntry` a scan therefore resumes from its deque's head
rather than the successor it read before the callback, and it repositions with a bare
`moveToBack`, since the entry it holds is always its deque's head. Without a nested cycle the
head is that successor, because each step removes the head or moves it behind `last`; an entry
that `evictEntry` resurrects is examined again. Resuming on the held successor fails three ways:
- An *unlinked* entry has both links null, so `unlink` runs `first = next; last = prev` and
  discards the whole deque, leaving every entry in it live in `data` and in no eviction queue.
- A *transferred* entry carries the links of the deque that now holds it, so `unlink` splices
  that deque and assigns one of its nodes as this deque's `first`/`last`. The window, probation,
  and protected deques **share one pair of link fields on the node**, so
  `AccessOrderDeque.contains` answers "linked somewhere", not "linked here".
- A successor *moved behind `last`* (a listener that renews entries and runs `cleanUp()`) leaves
  every entry the walk meets fresher than `last`, so the walk reorders them forever.
Guarding the reposition with `contains` and the queue type covers only the first two. Counting
nested cycles would need a field and would see only relinks made by `maintenance()`. Pinned by
`BoundedLocalCacheTest.maintenance_recursive_accessOrder` / `_writeOrder`,
`expireAfterAccess_promotedDuringScan`, and
`ExpirationTest.expire_removalListener_renewsScanSuccessors`, and searched by
`ReentrancyFuzzer`, which found the relinked-successor loop within a minute of fuzzing from an
empty corpus. Don't resume a scan on an entry read before `evictEntry`.

**Expiration scans re-check their captured tail after callbacks.** A scan ends once it has
examined `last`, and reorders do not consume the eviction budget, so a nested cycle that removes
or transfers `last` leaves entries cycling indefinitely under `evictionLock`. While `last` stays
linked the walk reaches it, because between evictions every step moves one entry from in front of
`last` to behind it. After `evictEntry`, a scan with work remaining checks tail membership, and
for access order the tail's queue type as well, since membership answers only "linked somewhere";
the write-order links are exclusive to the one write-order deque, so `expireAfterWriteEntries`
needs only membership. If absent, it exhausts the budget so `PROCESSING_TO_REQUIRED` starts a
fresh scan. A scan that reached its own tail needs no re-arm. The timer wheel instead uses its
field-backed `pending` sentinel and rejects nested advances with `advancing`. Pins:
`maintenance_recursive_accessOrder_removedTail` and `maintenance_recursive_writeOrder_removedTail`.

The wheel budget counts **only evictions**, never the cascade (rescheduling a non-expired
node to a finer level) — mirroring the deque caps, which count `evictEntry` but not the
`moveToBack` reorder. Cascading a densely-populated coarse bucket is O(n) and *not* sliced,
but it's accepted: an O(1) pointer splice with no CHM write or listener (~1–2% of an
eviction), done at most once per node per advance, and a given coarse bucket cascades only
~once per its multi-day span. The only trigger is a whole cache landing in one coarse bucket
— entries scheduled past the ~6.5-day overflow span (a JVM won't outlive it) or bulk-loaded
at startup with periodic reload (a cache anti-pattern). Debated and declined (2026-07-03):
capping it bounds a lock-hold no worse than one already-accepted post-cap eviction cycle
(the equivalent threshold is ~50–100K), for the cost of new concurrently-mutated state in
the wheel. **Critically, a cascade cap must never reuse the eviction rewind:** the rewound
re-advance re-scans the finer levels the cascaded nodes moved to and re-cascades them,
starving the eviction drain (or livelocking). Evictions can rewind only because an evicted
node is gone, so the re-traversal skips it. Don't cap cascades via the rewind.

*If ever revisited (needs a repro — a coarse bucket with ~10^6 live entries pinning
`evictionLock` past the eviction cap while a writer-assist blocks):* a safe cascade cap needs
a **forward-carried backlog**, not a rewind — the wheel's analog of the climber's `adjustment`
carry-over. On hitting the budget, stitch the unprocessed remainder (the current bucket's tail
plus the un-visited buckets/levels, reusing the nodes' existing variable-order links) into a
backlog list held on the wheel, and let `nanos` advance **normally**. The next advance flushes
that backlog first (evict the due, reschedule the rest), then resumes the level walk — forward
progress, nothing re-scanned. The hard part is lifecycle reconciliation between advances: a
backlogged node that gets `deschedule`d unlinks transparently (same links), but `reschedule`
must move it out of the backlog and back into a wheel bucket, and the flush must tolerate the
list shrinking under it. That concurrent-mutation surface in the codebase's most intricate
structure is why it's deferred, not the mechanism itself.

**Collected references are drained at `REFERENCE_THRESHOLD` (1000) per queue per maintenance
cycle**, then `PROCESSING_TO_REQUIRED` re-arms the backlog like every other budget here. The
budget counts *polls* rather than evictions, so a run of stale references cannot extend the
hold, and each queue gets its own so both make progress. This is the expiration cap's shape
with a different trigger: a garbage collection clears an arbitrary number of keys or values at
once, with no user action to blame it on, and the drain then ran to exhaustion under
`evictionLock`. Measured (M3 Max, JDK 26, 1M weak keys cleared and enqueued at once): one
`cleanUp()` held the lock **130–190 ms**; capped it is 0.12–0.5 ms per cycle over 1000 cycles,
and a concurrent writer completed 2.8k–81k operations during the backlog where it had completed
~1,100. The cost is ~45% more total drain time from the extra cycles. **What the cap does not
do is bound a concurrent operation's wait**: `rescheduleCleanUpIfIncomplete` re-submits
immediately on the common pool and `evictionLock` is not fair, so the drainer barges back in
and the backlog as a whole still holds the lock (a probe acquiring and releasing every 100 µs
got in 2–92 times over the backlog, capped or not). That residual is shared with the expiration
budget and is a lock-fairness question, not a reason to drop the cap or to widen it.

**The maintenance consumer never waits for a producer's publication.** `MpscGrowableArrayQueue`
publishes in two steps, a CAS of the producer index and then a release store of the element, and
the strong `poll()` spins on that element when the index says the queue is non-empty. Both
consumers (`drainWriteBuffer` and `clear`) hold `evictionLock`, so a producer descheduled
between those two instructions — a container throttling a thread, say — stalled every other
policy operation for its whole pause and burned a core doing it. Both use `relaxedPoll()`, which
returns null instead, and JCTools offers exactly that pairing for a consumer that must not
block. Nothing is stranded: every production `offer` is `afterWrite`'s, and `scheduleAfterWrite`
runs after `offer` returns, so the passed-over task is re-armed by its own producer (IDLE →
REQUIRED, or PROCESSING_TO_IDLE → PROCESSING_TO_REQUIRED, which makes maintenance's final CAS
fail). `clear` is safe for a second reason: it already abandons the buffer once a concurrent
writer refills it past `WRITE_BUFFER_MAX / 2`, `AddTask` links only `if (isAlive)`, deque
removal is contains-guarded, and `makeDead` takes the weight from the node rather than from the
policy precisely because an update may still be buffered, so a task that outlives the entry it
describes costs a weight swing that telescopes back to zero. It ends with
`rescheduleCleanUpIfIncomplete`, which is what carries the remainder forward.

## Exception Handling

**Checked-exception conversion and the listener guards restore interruption.** Kotlin, Scala,
and Groovy callbacks can throw `InterruptedException` through `Function`, `BiFunction`,
`CacheLoader`, or `RemovalListener`; checked exceptions are a javac rule, not a JVM constraint.
`Caffeine.toUnchecked` rethrows `Error`, returns `RuntimeException` by identity, restores the
current thread's interrupt status for `InterruptedException` (JDK interruptible waits clear it
when throwing), and otherwise wraps in `CompletionException`. The default
`CacheLoader.asyncLoad` converts inside its supplier, on whichever thread the executor uses.
Conversion is used by loader chains, catch-commit-rethrow for COLLECTED/EXPIRED recomputation,
and `AsyncBulkCompleter`. Absent-key paths and `UnboundedLocalCache` propagate unchanged and need
no conversion-side repair. The eviction and removal listener guards log the failure instead of
converting it, so they restore the status before logging: the eviction listener runs on the
thread doing the eviction, often the caller's own `put` or `compute`, and a removal listener
runs on the caller under a caller-runs executor or a rejected submission. Later interruptible
calls on that thread then fail at once, including the listener's for the next entry evicted in
the same pass; that is the interrupt taking effect, not a cascade to contain. The statistics,
scheduler, and future-completion guards do not restore, since the callbacks they wrap are not
expected to wait.

**Catch-commit-rethrow pattern** in `doComputeIfAbsent` and `remap`. Both catch
`Throwable`, not just RuntimeException. When user code
(mapping function, weigher, expiry) throws after `notifyEviction` was called, the
phantom eviction is made real: the node is retired, null is returned to CHM, and the
exception is deferred past cleanup.

**notifyEviction is called BEFORE user code**, not after. This can't be reordered —
it preserves linearizability for resource-based listeners (e.g., file delete before
recreate). The catch-commit-rethrow pattern handles the case where user code then
throws.

**wasEvicted flag** in `remap`: `boolean wasEvicted = (ctx.cause != null)` is captured
BEFORE the try block because `ctx.cause` can change from null to REPLACED
inside the try. The catch block uses `!wasEvicted` to distinguish eviction-path
exceptions (commit+defer) from non-eviction exceptions (immediate rethrow).

**`remap`'s no-op exit records itself in `ComputeContext.unmodified`; the post-write dispatch
must not re-derive it.** The in-lambda short-circuit takes the exit on four conditions
(`preserveTimestamps`, a same-instance return, and no removal cause); the dispatch used to test
only the hint, so a hinted call that *did* mutate committed the mutation and then skipped its
`AddTask`/`UpdateTask`. A hinted update left `weightedSize` short by the delta forever, and a
hinted create on an absent key (the absent branch has no short-circuit at all) installed a node
linked into no deque and counted in no weight, which `makeDead` later *subtracted* from
`weightedSize` anyway, relaxing the bound by that much per cycle. Not reachable from the public
API: every caller that sets `preserveTimestamps` returns the instance it was handed, and each
insert branch returns before the hint is set. It is latent because `RemapHints` is
package-private, so nothing warns a future caller. Pinned by
`BoundedLocalCacheTest.remap_preserveTimestamps_newValueDiffers_publishesTheUpdate` and
`remap_preserveTimestamps_absentCreate_publishesTheAddition`. Don't restore a second copy of the
predicate; the exit that skips the work is the one that says so.

**`remap` same-instance return is a setter no-op, NOT a metadata no-op**. When a
user `compute`/`merge` remapping function returns the same value instance as the
current value, `setValue` is skipped, but `weight`, `accessTime`, `variableTime`,
and `writeTime` still update. This is intentional: `compute` is a mutation API,
so a same-value return is still treated as a write for expiration, refresh eligibility,
and eviction policy, subject to timestamp tolerance. The only documented full no-op is the
explicit `preserveTimestamps` path. A reader expecting `compute(k, (k, v) -> v)` to leave eviction
ordering and expiration deadlines unchanged would be surprised; the source does not call this
out, so this entry is the canonical place the behavior is documented (preferred over a source
comment).

**Value-bearing user callbacks propagate; fire-and-forget callbacks are guarded.**
`Ticker`, `Weigher`, `Expiry`, and loaders return information the cache needs, with no safe
default. Stats, scheduling, and listener wrappers can instead return an empty/default result.
Do not add containment for broken equality, hostile futures, or JVM errors. The precedent is
to fix the user component (as Quarkus did for its broken future), not add defensive code or a
must-not-throw note to `Ticker`.

A value-bearing throw can land after a commit. Examples include `AddTask` / `UpdateTask`
updating policy totals before a ticker read, and refresh/async prologues reading `statsTicker`
before cleanup. Such failures can skew accounting or strand tokens/proxies; those mechanisms
are accepted under this boundary, not denied. Broken key equality can cost the size bound. A
`hashCode` that throws in `evictEntry` during an `UpdateTask` skips that update's `weightedSize`
delta, relaxing the bound by the delta for the cache's lifetime; one that throws when admission
compares frequencies stops size eviction, logging each failed cycle, until it stops throwing,
because every removal hashes the key too. Containment often invokes the same broken component.
Preserve existing targeted cleanup, including refresh completion's own-token catch; the boundary
is not a reason to remove it.

**Concurrent standard-future obtrusion is the supported exception.** Between readiness checks
and `join`, an `obtrudeException` can make `Async.getIfReady` throw. A plain-future stress run
produced 3.5M throws in 1.28B query rounds. Its narrow `CancellationException` /
`CompletionException` catch now returns null as the method promises, including to maintenance
under `evictionLock`; other hostile-subclass exceptions propagate. Completion handlers are
one-shot, so obtruding after success can leave a physical entry that queries filter. That is
accepted, although the entry never expires: `evictEntry` reads the failed future as in flight and
re-dates it with the async sentinel, so only size eviction or an explicit removal reclaims it.
`AsyncBulkCompleter.failProxies` removes before obtruding.

**A maintenance throw defers buffered work; it does not drop it.** The final CAS can settle
`PROCESSING_TO_IDLE → IDLE` on a throw, leaving unprocessed tasks in the buffer. Later writes
re-arm, and a full buffer forces inline assist. Do not force `REQUIRED` or add a re-arm to
`performCleanUp`'s throw path. The inline fallback's own write task is different: it must run
in `maintenance`'s `finally`, preserving the repair for that task's former loss.

**Executor rejection and lock contention need different epilogues.**
`scheduleDrainBuffers` deliberately does not follow rejection-fallback maintenance with
`rescheduleCleanUpIfIncomplete`. The rejecting executor cannot use the common-pool arm; a
pacer would submit to the same broken executor, where its fire-time rejection is swallowed.
Recovery relies on later activity, as documented for discarded/never-run
tasks. A drain that merely bounces off `clear`'s held lock still has a healthy executor and
must reschedule. Do not generalize the rejection exception to that case.

## Buffers

**`MpscGrowableArrayQueue` is a shaded JCTools port and is not wrap-safe; that is accepted.** At
`pIndex == cIndex == 2^63 - 256` the "is there room" sum overflows and a resize is selected on an
empty maximum chunk, stranding the odd producer-index marker so every producer spins at 100% CPU. It
needs 2^62 offers on one never-reset queue (millennia at a measured 10-20M put/sec), and upstream
master is identical. Don't harden it: `(pIndex - cIndex) < bufferCapacity` does not suffice
(`producerLimit` stores the same wrapping sum), and rolling the index back after `producerBuffer =
newBuffer` strands the consumer, which is worse than the spin.

**`StripedBuffer.offer` treats `FULL` as success and only expands on `FAILED`.** A failed CAS is
a contention hint; weak CAS can also fail spuriously. A full buffer only means the drain is behind,
which striping does not fix. Routing `FULL` into `expandOrRetry` would let one thread's
routinely-full buffer grow the table and allocate stripes nobody contends for. So a `FULL` home
stripe returns `FULL` without probing a sibling, which is what makes a stalled stripe skip that
thread's reads until it drains. Both are intended: the `FULL` return is the signal that tells
`afterRead` to drain. Don't make `FULL` probe for another slot.

**A thread's starting stripe never moves.** The probe is re-derived from the thread id on every
`offer`, so the incremental hashing in `expandOrRetry` varies the slot only within one call. This is
forced, not chosen: `ThreadLocalRandom.getProbe`/`advanceProbe`, which `Striped64` uses to
permanently move a colliding thread, are package-private to `java.util.concurrent`. Don't "restore"
the convergence search; the only alternative is a `ThreadLocal` on the read hot path.

## References

**The keyReference in a weak/soft value reference is read and written opaquely.** The
field is set during construction and, under `synchronized(node)`, mutated to a sentinel
value (`RETIRED_*_KEY` / `DEAD_*_KEY`) when the node is retired or dies. Lock-free
readers tolerate the resulting staleness window as weakly-consistent observation, but a
strong-key weak/soft-value node keeps its key *inside* that reference, so `getKey()` and
`isAlive()` read the field independently: with plain reads, a reader that observed the
sentinel and then observed the older key would judge a retired node alive while handing
out an internal sentinel as its key. Opaque access forbids exactly that, since opaque
operations on one variable are coherent, and it costs nothing at runtime because the
constraint binds the compiler rather than the hardware (opaque loads compile to ordinary
loads on x86 and aarch64 alike). The field stays non-volatile and the constructor's store
stays plain, the object not yet being published; only the accessors are opaque. A jcstress
probe was written to arbitrate first and found no violation over both weak and soft values
while exercising the window (it observed the monotonic `key`-then-retired interleaving),
so it was discarded rather than kept as a pin: the hazard is a legal compiler
transformation that a green run cannot refute.

In `setValue`, a new `WeakValueReference` is installed via
`setRelease` followed by `VarHandle.storeStoreFence()`, then the old reference is
cleared via `ref.clear()`. The fence prevents the old reference's `clear()` from
being reordered before the publication of the new reference. Without the fence, a
reader that re-reads the same reference and observes a cleared referent cannot
distinguish "the clear was already committed" from "the clear's store buffer is
ahead of the new reference's publication" — breaking the `getValue` re-check loop
invariant. The reader's `loadLoadFence` pairs with that ordering through the observed clear;
this does not require first acquiring the replacement reference. See
[RA fences and their specializations](https://gee.cs.oswego.edu/dl/html/j9mm.html).
`setRelease` alone orders the new reference's constructor writes before
the publication, but does not constrain the subsequent `ref.clear()` against any
racing reader (#1820, confirmed on aarch64 M3 Max via JCStress IntermittentNull test).

In the constructor, a plain `VALUE.set` is used since the object itself is not yet
published. Strong-value setters also use a store-store fence, to order the value before
subsequent timestamp stores as required by the [expiry read protocol](#expiration);
they have no reference-clearing operation to order.

**Weigher.boundedWeigher** wraps all user weighers and enforces `weight >= 0` at
runtime via `requireArgument`.

## Concurrency

**`maximum` and `weightedSize` have a plain reader and an acquire reader, and no public-facing path
may use the plain one.** `AddMaximum.addPlainAndAcquireField` emits `maximum` as a `VarHandle.get`,
which guarantees bitwise atomicity only up to 32 bits, so a plain read of the 64-bit field can tear
against `setMaximum`'s release write on a 32-bit JVM. The plain reader is for callers already
holding `evictionLock`; everything reachable from the public API takes
`maximumAcquire`/`weightedSizeAcquire`. There is no way to assert an access mode in a test, so this
is a review-time invariant: check the call site's lock state, not the accessor's name. The three
duration fields need no such care, since `addAcquireReleaseField` emits only an acquire reader.

**No debug-mode assertions.** Runtime invariant assertions are impractical for
concurrent code — too hard to assert on a running system. Correctness relies on
testing (Fray, LinCheck, JCStress) and static analysis (ErrorProne `@GuardedBy`).

**nanoTime is monotonic.** Per JVM spec, `System.nanoTime()` is monotonic. Backward
movement would be a JVM bug, not a cache issue.

**`scheduleAfterWrite`'s IDLE arm retries its failed swap rather than acting on what it read.**
The status can advance between the opaque read and the compare-and-swap, so a writer that read
`IDLE` may find `PROCESSING_TO_IDLE`. Dropping the failed swap and calling `scheduleDrainBuffers`
anyway leaves that write with no driver: the guard sees a drain in flight and returns, while the
drain has already passed the task's slot, and its exit swap then settles the machine at `IDLE`
with the task still buffered. Retrying against the observed status routes to the processing arm,
which converts the exit to `PROCESSING_TO_REQUIRED` — the same shape the processing arm has always
used, and why the two arms now look alike. The end state it prevents was benign (the task is not
lost; the next write, a `cleanUp`, or a read that fills a stripe drains it, which is the deferral
already priced for the maintenance-throw path and the `WRITE_BUFFER_MAX` cap) and reaching it
needs the entry store's visibility to lag the drain's loads, so this is a tidiness fix, not a
correctness one — recorded because three audit runs have now reached for this machine. `IDLE` is
written in exactly one place, the value-checked `casDrainStatus(PROCESSING_TO_IDLE, IDLE)`, so no
`REQUIRED` is ever swallowed. Don't collapse the arm back to an unconditional swap. Pinned by
`BoundedLocalCacheTest.scheduleAfterWrite_staleIdle_retriesAgainstTheObservedStatus`.

**Read recording starts with the frequency sketch.** A fastpath cache (bounded, strong keys and
values, no access expiration) starts with the disabled read buffer, so its reads skip the eviction
policy until the sketch is initialized: at half occupancy, when presized by `initialCapacity`, or
when `setMaximum` makes the cache half full. Each of those sites calls `recordReads()`, which swaps
in a `BoundedBuffer` under the eviction lock. A test that initializes the sketch directly must call
`recordReads()` as well; `BoundedLocalCacheTest.fastpath`, `fastpath_presized` and
`fastpath_setMaximum` pin the three sites.

`readBuffer` is volatile, so the swap is a safe publication. A plain field is also safe: a new
buffer's constructor writes nothing, and JLS §17.4.4 makes default values visible to every thread.
But that holds only while `StripedBuffer` stays stateless at construction, plain reads have no
guarantee of ever observing the swap, and race detectors flag it (a TSAN report, #2020). An
acquire read, as `drainStatus` uses, would also suffice, but C2 compiles the two identically on
every port except PPC64, the only one where a volatile load gets a leading `sync`. The volatile
read was kept because the JLS memory model formally defines it, while the other modes lack a full
formal specification
([Lea, JDK 9 memory order modes](https://gee.cs.oswego.edu/dl/html/j9mm.html)). On an Apple M3
Max, plain and volatile measured within 3% of each other on JDK 11 and 27 in
`GetPutBenchmark.read_only`, and on 11, 26 and 27 in a harness matching it. A JDK 26 JMH run with
release-26 bytecode showed volatile 24% faster under identical inlining; it did not reproduce on
11 or 27, so it is a C2 layout artifact rather than a cheaper read.

The hit path offers to the buffer without testing the sketch. The `skipReadBuffer()` check it
replaced loaded `sketch.table`, which shares a cache line with `size`, and the maintenance thread
writes `size` for nearly every drained read. On a read-only JMH sweep of a presized 65,536-entry
cache (Apple M3 Max), removing the check took a hit from 11.65 to 9.99 ns at one thread and from
11.13 to 7.00 ns per thread at eight, and a cache under half full was no slower. Two alternatives
were measured and rejected: padding `size` 128 bytes away from `table` recovered less (12.21 to
10.35 and 10.28 to 9.02 ns/op) at 128 bytes per cache, and keeping the count in the table's
trailing slot recovered a third of the gain at one thread and none at eight, for a reason not
established.

The maximums and weighted sizes (`maximum`, `weightedSize`, and the window and protected pairs) are
padded from the fields that a hit loads, because the maintenance thread writes them for drained
writes, promotions and climber steps. Unpadded, they shared a 128-byte line with an expiring or
refreshing cache's `ticker` and duration (`expiresAfter*Nanos`, `refreshAfterWriteNanos`) at every
alignment, and with the stats counter and the base class's `data`, `nodeFactory` and `readBuffer`
at others; a line that also held `drainStatus`, which every drain writes anyway, cost little. On an
Apple M3 Max with eight readers and a writer doing 500k put+invalidate/s, averaged over the cache's
16 alignments in a line on JDK 27, padding cut the mean time per hit (eight readers over their
throughput) from 28.1 to 24.6 ns for a size and expire-after-access cache, from 23.1 to 21.8 ns
with stats, and from 17.1 to 16.0 ns for a size-only cache, whose put+invalidate fell from 399 to
329 ns. JDK 25 agreed, except that the size-only hit was within noise there.

So `AddMaximum.supertypes` puts a class of 15 longs (120 bytes) before the class that adds the six
fields, and another between that class and its expiring and refreshing subclasses: 120 bytes per
cache bounded by size or weight, 240 with expiration or refresh. The class that adds them holds only
those longs, and its climber, sketch and access-order deques go in a class before the first pad,
because HotSpot (JDK 15 and later) places a subclass's fields in its superclass's alignment gaps:
those references leave a 4-byte gap, and a subclass's `ticker` filled it beside the counters. Each
pad is a class of its own because a class's own fields of one size may be laid out in any order (JDK
17 and 21 sort them with the platform's `qsort`, which on macOS swaps the first and last of the run;
JDK 24 made the sort stable, JDK-8341471). A size-only cache's end is left unpadded: the object
after it is the collector's choice (the eviction lock at allocation, the write buffer's leading pad
after a young collection on G1 or Parallel), and the gains were the same with either beside it.
Moving the six fields into a separate object would cost about 20 bytes, but that object shares its
line with whatever the collector places beside it, which JOL cannot check. The JDK pads its own
separate counter objects (`ConcurrentHashMap.CounterCell`, `Striped64.Cell`) for that reason.
`BoundedLocalCacheTest.evictionFields_ownCacheLines` checks the layout with JOL; only JDK 15 and
later fill gaps, so only they can fail it.

## Node State

**Two weight fields**: `weight` (entry's perspective, guarded by `synchronized(node)`)
and `policyWeight` (policy's perspective, guarded by evictionLock). They're correlated
but updated at different times — this is intentional for the telescoping sum to work.

`makeDead` subtracting the finalized `getWeight()` (not `policyWeight`) and `UpdateTask.run`
being deliberately dead-guard-free are a **matched pair**: a late-applied `UpdateTask` adds back
exactly the δ that `makeDead` over-subtracted. Don't add an `isDead` guard to `UpdateTask` and
don't switch `makeDead` to `policyWeight` — either one alone breaks the cancellation.
Both `retire()` and `die()` preserve the queue-type bits, so late updates use the same region
classification as `makeDead`. The same reconciliation applies to global and regional weighted
sizes; until late updates arrive, a region counter need not equal its deque's policy-weight sum.
Because racing updates offer their `UpdateTask`s outside the node lock, out-of-order
drains can leave a live node's `policyWeight` transiently negative; the climb transfer
loops then charge that weight to their quota and over-shift the region caps beyond the
commanded adjustment (the net can even invert the commanded direction). Adjudicated
tolerated, not guarded (2026-07, audit-adaptivity F1): the caps are the controller's
policy targets, not capacity enforcement (eviction and the total bound compare `weightedSize`
and each node's `policyWeight` with `maximum`, and eviction on the same transient before it
telescopes is the premature eviction `ruled-out.md` §Core accepts), and the split coerces back
on its own: the next completed sample overwrites the inflated carry-over, the below-floor lift
is not step-capped, and the excursion is bounded by one weigher swing per writer racing on the
key. Don't
clamp the transfer quota against negative weights, and don't "fix" the offer ordering.

**Queue type constants** are plain ints, not enums: WINDOW=0, PROBATION=1, PROTECTED=2.
The field is plain (not volatile), guarded by evictionLock.

## ConcurrentHashMap Constraints

**No recursive computations.** Writing to the cache from inside an atomic
compute/computeIfAbsent/merge callback violates ConcurrentHashMap's contract; this is
not a Caffeine bug. Detection is best-effort: encountering an empty bin's ReservationNode
throws `IllegalStateException("Recursive update")` (surfaced raw, unwrapped), as does a
recursive append detected by `compute` or `computeIfAbsent` in a populated linked bin.
Other recursive updates, including tree-bin operations, can go undetected and silently
corrupt (lost inserts, double count updates, clobbered writes). A nested insert that crosses
the resize threshold runs the resize on the computing thread, which re-enters the held bin and
abandons the resize for the life of the map, leaving misplaced keys that iteration finds, `get`
misses, and `invalidateAll()` does not remove. Never rely on the ISE as a safety net. During a
refresh completion this can orphan the key's `refreshes` token (suppressing its auto-refresh)
only if `data.compute` throws *before* `remap`'s lambda — a broken `hashCode` or a rare
cross-bin ISE (same-key recursion silently re-enters a populated bin instead); in-lambda throws
self-clean on the exits a completion can reach (the create-branch `finally` and the
present-entry `catch`). The one exit that *preserves* — an absent-branch **user-function** throw
(both siblings; ULC guards its catch with `value != null`) — a refresh completion never hits,
because its own lambda cannot throw before materialization. Either way the orphan self-heals on
the next write/removal. Don't add a catch-side `refreshes.remove` — it re-throws on the
broken-`hashCode` sibling.

The same restriction covers a read inside a computation or eviction listener that triggers an
inline refresh update or inline maintenance. Two remappers reading each other's stale key can
deadlock when completed reloads commit under the enclosing computations' locks, even with a pure
loader and the default executor, and a commit that removes a bin neighbour (a `null` reload)
leaves the computed key mapped to a dead node if the computation removes it. On an executor that
runs tasks on the caller (`Runnable::run`, a saturated `CallerRunsPolicy` pool), any read that
schedules maintenance runs the whole cycle under the locks the callback inherits. Its eviction of
the entry under computation or of a bin neighbour loses the computed value while the call returns
it, notifies the old value twice, drifts the weight, or leaves a dead node mapped; from `put`'s
eviction listener it takes the bin under the node monitor and deadlocks against a concurrent
removal or load of that key. Both are still unsupported recursive modification under the existing
warnings that these callbacks must not modify the cache; neither calls for a dispatch change,
callback-scope tracking, or another API warning. A liveness check after the callback is too late:
the nested eviction has already unlinked the map's node, so the enclosing compute writes through
stale pointers whatever the lambda returns. The check could only choose between losing the value
silently and throwing, and it cannot detect a neighbour's eviction.

The **`evictionListener` runs inside the CHM compute lambda** — `notifyEviction` is called
within `data.compute`/`computeIfPresent`, holding the entry's bin lock — so it is subject to this
rule: a listener that modifies the cache (same-key *or* other-key) is a recursive update → an ISE
(caught + logged in `notifyEviction`, so the write is silently lost) or silent corruption. The
corruption can outlive the listener: a dead node left mapped makes every later `put` of its key
throw the broken-equality `IllegalStateException`, which blames the key's `equals` or `hashCode`,
and neither `invalidate` nor `invalidateAll` removes it. The `Caffeine.evictionListener` javadoc
says "must not modify this cache." That (and the parallel
`mappingFunction`/`remappingFunction` warnings across `Cache`/`AsyncCache`/`LoadingCache`/`Policy`)
was tightened from the wording inherited from ConcurrentHashMap's `compute` javadoc — "must not
attempt to update any *other* mappings" (CHM's phrasing through JDK 13; JDK-8232652 replaced it with
"must not modify this map" in JDK 14, though `merge` still carries the old form) — which by negation
wrongly implied a *same-key* mutation was safe. The `removalListener`, by contrast, runs outside the
atomic operation (async/after the fact) and *may* modify the cache. Don't flag a same-key mutation
from the eviction listener as a corruption bug — it's documented misuse.

**CHM never hands back a stored key, so the unbounded cache notifies with the caller's
instance.** `compute` and `computeIfPresent` pass the remapping function the key the caller
supplied, and the table keeps the instance it already held; there is no API that returns the
stored one. `UnboundedLocalCache` is a thin wrapper over that map, so its `Object`-accepting
removals notify with the argument, where `BoundedLocalCache` reads `node.getKey()` and Guava's
`LocalCache` reads `entry.getKey()`. A `RemovalListener` therefore receives an equal key, not a
particular instance. The visible consequence is a cross-type equal key: with a stored `HashSet`
and an equal `TreeSet` passed to `remove(Object)`, `remove(Object, Object)`,
`keySet().remove(Object)` or `entrySet().remove(Object)`, all eight bounded and unbounded cells
removed the entry, but a correctly typed listener ran on every bounded one and none of the
unbounded ones, where the checkcast throws and `notifyRemoval` logs it as a listener exception.
Recovering the canonical key there needs an O(n) scan or a per-entry node, which is the structure
the unbounded cache deliberately does not have, so this is a boundary rather than a repair.
Identity preference in cases that lose no notification is separately unspecified.

**CHM bin blocking is not a Caffeine bug.** `compute()` locks the hash bin. If the
mapping function (cache loader) is slow, all other operations on keys in the same
bin are blocked. This is the #1 recurring user issue (~20 reports). The answer is
always: use `AsyncCache` for slow loaders, increase `initialCapacity` to reduce
collisions, or make loaders faster.

**A refresh commit waits on its bin like any write.** The commit is a `compute` run by the thread
that sees the reload complete: the `refresh` caller, or the read that triggered an automatic
refresh, when the reload is already complete; otherwise the thread completing it, such as an
executor worker. It waits out any computation holding the key's bin. `refresh`'s "asynchronously"
describes the load. Dispatching the commit with `whenCompleteAsync` would add a task per refresh,
give up installing a completed reload before `refresh` returns and returning it from the
triggering read, and still run inline under a caller-runs executor. A load that holds the bin while
waiting for work on the executor completing the reload deadlocks with the commit, as when Guava's
`CacheLoader.asyncReloading` shares a single-thread executor with the loader's own fetches; that
wiring is the application's, under the executor rule in
[standing principles](ruled-out.md#standing-principles).

**The same doctrine covers a two-map deadlock, not just blocking, including when it wedges an
innocent thread.** A loader that touches the cache can take the `refreshes` bin monitor and then a
`data` bin monitor, while any write takes `data` and then `refreshes` through `discardRefresh`.
Opposite orders, and `findDeadlockedThreads()` reports it on both implementations. A read closes
the cycle as well as a write. Reading a stale key registers its refresh, which takes a second
`refreshes` bin, so two reloads reading keys in each other's bins deadlock on `refreshes` alone, and
a nested reload that completes inline commits into a `data` bin. On one thread, a registration that
lands in the bin the reload holds makes CHM throw `Recursive update`: from the loader's read when
the bin was empty, failing the outer refresh, or from the caller's `get` or `refresh` when it was
populated. The counterparty can be maintenance evicting or expiring a key whose refresh is
registered; it waits holding `evictionLock`, so every later `cleanUp()` and every writer that falls
back to inline maintenance hangs with it. The reload body holds the monitor only on a caller-runs
executor or in an `asyncReload`'s synchronous prefix; a `CacheLoader.reload` on the default
executor runs on a pool thread. It is declined on the same basis: the only path that takes another
lock while holding a `refreshes` bin is the user's `reload`/`asyncReload`, and
`CacheLoader.reload`'s javadoc bolds the prohibition ("loading **must not** attempt to update any
mappings of this cache directly or block waiting for other cache operations to complete").
Everything else inside `refreshes.computeIfAbsent` is either a lock-free `data` read
(`getIfPresentQuietly`) or the `asyncReload` call itself, and the completion `handle` that does the
`data` work is attached **outside** the lambda, deliberately, so the refreshes monitor is not held
while it runs. Keep it outside.

Two objections are answered rather than ignored. That the victim is an ordinary `put` which did
nothing wrong is true, and is a property of every lock-order inversion in a shared structure; the
counterparty of misuse is always innocent. And a silent deadlock is a worse diagnostic than
`Cache.get`'s documented `IllegalStateException` for recursive updates, which is a fair criticism
of the *diagnosis* rather than evidence the ordering is wrong. Don't reorder the internal maps to
make a forbidden loader safe.

**`Expiry` must not call into the cache, and carries no javadoc saying so.** The callback is
invoked under `synchronized(prior)` in `put` and under the bin lock on the compute paths. A
calculator that calls `cleanUp()` or a `Policy` ordering method waits on `evictionLock` while
holding the node monitor, deadlocking against the maintenance thread's `evictEntry`, which takes
those locks in the documented order. One that calls `invalidate` re-enters the monitor on its own
thread, retires the node, and lets the outer `put` commit into the dead entry. Neither is guarded.
The omitted warning is deliberate: computing a duration has no reason to re-enter the cache, so
the note that fits `CacheLoader.reload` and `evictionListener` would be noise on `Expiry` and
`Weigher`. Hoisting the callback out of the monitor the way `Weigher` was hoisted is not available
either, since `expireAfterUpdate` and `expireAfterRead` read the node's variable time under it and
the compute-path invocations need the atomic context.

**A `Policy` ordering snapshot or `setMaximum` inside an atomic scope re-enters maintenance on any
executor.** Both take `evictionLock` and run a maintenance cycle, and `Cache.policy()` forbids
policy operations within another operation's atomic scope. The eviction listener runs while its
victim is still linked and counted, so a nested cycle there evicts the same victim again, through
the reentrant lock, until `StackOverflowError`: one due eviction became 407, and the drifted
counters let a `maximumSize(2)` cache hold 407 entries. From a remapping function the nested cycle
corrupts the bin like an inline eviction (see *No recursive computations*) or deadlocks against
maintenance. Not guarded: a held-lock check in `snapshot` would miss the listener's `cleanUp()`
and the remapping-function arms.

**`clear()`/`invalidateAll()` do not wait for an in-flight `computeIfAbsent` insert.** The insert
is invisible to `clear()`'s `data.values()` snapshot (the CHM Traverser skips the in-flight
`ReservationNode`), so it survives and serializes *after* the clear. CHM blocks per-bin and removes
it, but both orderings are linearizable, and the layered design can't see CHM's internal per-bin state
to block (a fix would require forking CHM). Inserts only — an in-flight *update* on an existing node
**is** waited for (`clear()`'s `removeNode` goes through `computeIfPresent` + `synchronized(node)`).
Documented user-facing on the `invalidateAll()` javadoc ("behavior … is undefined for an entry that is
being loaded (or reloaded) and is otherwise not present") + the wiki. Don't flag the clear-vs-in-flight-
insert divergence.

**Eviction is async, not immediate.** After `put`, the cache may temporarily exceed
`maximumSize` until the executor runs maintenance. Use `executor(Runnable::run)` for
inline eviction in tests, or call `cleanUp()` before assertions.

That determinism has a cost worth knowing before reaching for it under load. The expiration and
window scans bound *evictions*, not traversal: `remaining--` runs only on an eviction or a
transfer, so a node the scan relinks and skips (an in-flight async load, a zero-weight entry) is
free and the walk continues past it. The default executor hides that, since maintenance coalesces
through the drain status and many writes share one cycle. `Runnable::run` removes the coalescing,
so a burst of pending async loads pays a walk over the pending set on every write in the burst,
which is quadratic across it. Fine for a test with a handful of entries; not a knob to reach for
in a benchmark or a reproduction that holds thousands of loads in flight. The expiration scans are
self-correcting once any load completes (they relink pending entries to the MRU end and stop at the
first completed, unexpired node), so their cost needs a deque with no completed entry at all. The
window scan relinks nothing, so its walk persists while pending entries sit at the window head; its
price under the default executor is in the pending-async scan entry of
[ruled-out](ruled-out.md#core).

**Expiration and cleanup are amortized, not instant.** Caffeine performs maintenance
during write operations and occasionally during reads. For idle caches, use
`Scheduler.systemScheduler()` to get prompt expiration. This is best-effort with
no hard timing guarantees.

**No close() by design.** The `Cache` interface deliberately does not extend
`Closeable`. The cache is a data structure that becomes GC-eligible when
unreferenced. `PerformCleanupTask` is a `WeakReference` to its cache, which breaks the
scheduler→cache reference chain, so scheduled maintenance becomes a no-op
when the cache is unreachable. (JCache's `CacheProxy` is the only component
with explicit close semantics, as required by JSR-107.)

## Refresh

**Refresh returns the stale value, not the fresh one.** `get()` returns the current
value immediately and triggers an async reload. The next `get()` returns the
refreshed value. This is the entire point — hiding reload latency from callers.

**Refresh only triggers on access.** An idle cache with no reads will never refresh.
For proactive refresh, use `ScheduledExecutorService` with `cache.refresh(key)`.

**"Logged and swallowed" is a promise about the future's result, not about producing it.**
`refresh` documents that a failed reload is logged and swallowed, and that covers the
`CompletableFuture` completing exceptionally: the load ran and failed, the mapping is unchanged,
and the caller who ignores the returned future sees nothing. It does not cover `asyncReload`
throwing *synchronously*, which happens before there is a future at all. Those are distinct
failures. Guava and early Caffeine did not hand the future back to the caller, which is the
wording's origin; a loader that throws while merely constructing its future has a bug in the most
basic step, and surfacing it is right. Do not "fix" `refresh`/`refreshAll` to swallow a
synchronous throw from `asyncReload`/`asyncLoad`, and do not cite the declared `throws Exception`
on those methods as evidence that it should be swallowed.

**`expireAfterAccess` + `expireAfterWrite` together is discouraged.** Inherited from
Guava for compatibility. The two timestamps are independent; whichever has the
shortest remaining duration wins. Prefer `expireAfter(Expiry)` for custom logic.

## Iteration

**`EntrySet.removeIf` predicates receive immutable snapshots** (`Map.entry(k,v)`), so
`setValue` throws. Like CHM and Guava's JDK-8078726 repair, removal is conditional on the
captured value. This applies to bounded, unbounded, async synchronous `AsMapView`, and raw
`AsyncEntrySet`; both async views delegate to the inner cache's `removeIf`, like their values
views. Keep write-through entries for `iterator` / `spliterator` / `toArray`, and do not
restore the positional `iterator.remove` default for predicate removal.

**Map equality uses size, iteration over this map, and `count == expectedSize`.** The
AbstractMap shape is symmetric with HashMap for `equals`-keyed caches and costs O(n), versus
CHM's O(n+m) two-sided scan. Under `weakKeys()` it is not: the cache equals a `HashMap` holding
an equal but distinct key, and that `HashMap` does not equal the cache (`IdentityHashMap` is
unequal both ways). Two equal-but-distinct keys also make the cache equal a map holding one of
them plus an entry the cache lacks, with unequal hash codes, as Guava's `weakKeys()` cache does;
only `IdentityHashMap`'s identity pass avoids that, at the cost of making the single-key case
false both ways. The final count catches maintenance trimming dead entries after the size
prescreen; otherwise a surviving subset can incorrectly compare equal. Preserve it in
`BoundedLocalCache.equals` and the future-typed `LocalAsyncCache.AsMapView.equals`.

**`asMap()` iteration is not a cache read.** Iterators do not update access times or
frequency counters. This prevents iteration from polluting the eviction policy.
Expired entries are skipped during iteration.

**Under `weakKeys()` the key and entry spliterators still claim `DISTINCT`, and must.** The
`weakKeys()` javadoc names `IdentityHashMap` as the model for its semantics, and the JDK's own
identity-keyed map advertises `DISTINCT` on exactly those two spliterators while omitting it on
`values()`, which is the shape Caffeine matches. It produces the same triple audits report as an
anomaly: two keys that are `equals` but not `==` give `size() == 2`,
`keySet().stream().distinct().count() == 2`, and `new HashSet<>(keySet()).size() == 1` (verified
on JDK 25). Removing the flag would not be a no-op, it would be wrong: `distinct()` would then run
with `equals`, merging two genuinely distinct live entries so the stream reports fewer elements
than the cache holds. Under identity semantics the keys *are* distinct, so the flag is the correct
claim.

**Identity comparison applies to one-sided queries, never to `equals` or `hashCode`.** A
one-sided query (`remove(k, v)`, `replace(k, old, new)`, `containsValue`, `values().remove`,
`keySet().removeAll`) can be identity-based unilaterally, because the worst case is declining to
act. A bilateral contract cannot: the other side is `AbstractMap` or `AbstractSet`, which compares
with `equals` and never reciprocates, so identity there breaks `Object.equals` symmetry and buys
nothing. Measured with the cache holding one value and a `HashMap` holding an equal-but-distinct
one: at HEAD both directions of `equals` are true, while an identity-comparing `equals` gives false
one way and true the other. This is the boundary of the `_byIdentity` convention, and every method
that convention covers is one-sided. No implementation anywhere compares keys by equality and
values by identity in `equals`: `IdentityHashMap` is reference-based on keys and values alike, and
Guava's `LocalCache` extends `AbstractMap` and declares no `equals` at all.

**So value identity is coherent with no map, and that is accepted.** Under `weakValues()` or
`softValues()` the value-bearing queries compare by identity (`containsValue`, `remove(k, v)`,
`replace(k, old, new)`, `values().contains`/`remove`, `entrySet().contains`/`remove`, and through
`AbstractSet` the entry view's `equals`), while `Map.equals`, `hashCode` and the
`WriteThroughEntry` objects the views emit compare with `equals`. The bulk removals compare with
`equals` as well: `values().removeAll`/`retainAll` ask the argument collection and
`entrySet().removeAll`/`retainAll` compare the emitted entries, so they can remove an entry
`contains` reports absent or keep one, under `weakKeys()` too; `IdentityHashMap`'s identity entries
do neither. Probed against
a `HashMap` holding an equal-but-distinct value: `equals` true both ways with matching hash codes,
`entrySet().equals` false one way and true the other, `contains` and `containsValue` false, and the
emitted entry equal to the probe entry. Guava's `weakValues()` cache reproduces that row element for
element, though its `values().remove` removes by equality where Caffeine's compares identity. Both
coherent alternatives are worse. `IdentityHashMap` buys internal coherence and is
still asymmetric against a `HashMap` (`equals` false one way, true the other, differing hash codes),
which is the bilateral case above. Making the queries `equals`-based would contradict the value
semantics `weakValues()` and `softValues()` document. Reference implementations do not agree with
each other here, and the cache is biased toward size eviction rather than reference collection, so
do not raise the disagreement as a defect or propose a consistency repair.

**View equality has an accepted physical-size/logical-content gap.** Key and entry views
inherit `AbstractSet.equals` / `hashCode`. `size()` includes expired or collected entries
pending maintenance; iteration, containment, and hash code skip them. With live `a` and
pending-dead `b`, `HashSet{a,b}.equals(keySet)` can return true while the reverse comparison
is false and hash codes differ. The argument's `HashSet.equals` performs the true-returning
comparison, so overriding the view's equality cannot repair it. A logical `size()` was
rejected to preserve the lock-free physical estimate; a physical hash is unavailable once a
weak key is collected. `WeakHashMap` has the same changes-during-comparison problem.
Values views use identity equality like CHM. The same gap lets `isEmpty()` disagree with
iteration (an `isEmpty()`-guarded `iterator().next()` can throw `NoSuchElementException`),
null-pads a `toArray(new T[size()])`, and lets a receiver that compares entry sets (Guava's
`ImmutableMap`) equal `asMap()` while holding a never-cached entry per pending one. The inherited
`AbstractSet.equals` can also return true under one concurrent `put` that lands after its size
reads; the mirrored shape (physical `size()` prescreen, iterate this view, probe the argument's
`contains`, `count == expectedSize`) was evaluated and declined, since it reverses two adjudicated
rows and moves the `weakKeys()` false positive to the view. For exact comparisons, call
`cleanUp()` with no concurrent operations; under variable expiration an entry that expired
within the current wheel tick stays until the next tick (see [TimerWheel](#timerwheel)). An
additional `asMap()` size warning was declined as redundant with the cache's existing
approximation contract.

The async synchronous view has the same gap for in-flight futures, which its `size()` counts and
its iteration skips. `cleanUp()` cannot remove them, so the gap lasts until the load settles.
Receivers split by algorithm: `AbstractMap`'s size check (`HashMap`, the bounded cache, the view
itself) makes the comparison false in both directions, while `ConcurrentHashMap`'s two-sided scan,
which the unbounded cache delegates to, makes `chm.equals(view)` true and `view.equals(chm)` false.
No equality on the view is symmetric with both, and moving the unbounded cache to the size check
would leave `ConcurrentHashMap` itself asymmetric.

## Maintenance nudges

**Read paths nudge `scheduleDrainBuffers()` when they observe an expired/collected
entry** (`getIfPresent`, `containsKey`, `containsValue`, `getAllPresent`, the iterator,
and the key/value/entry spliterators), so lazily-detected garbage is reclaimed promptly.
On a plain miss, `getIfPresent` still assists when the status is `REQUIRED`; `getAllPresent`
and membership reads have no corresponding absent-entry assist. The nudge is a cheap flag
check when maintenance is already running. On a caller-runs executor it runs maintenance **inline**,
so an in-progress scan of `data.values()` can have a node reaped underneath it — e.g.
`containsValue` is an O(n) scan and the internal `LocalCacheSubject` validator calls it
*per node* while iterating `data.values()`; a weak key collected mid-scan is then
drained, correctly removing and killing a node the weakly-consistent iterator still
yields. Production readers tolerate a dead node (they check `isAlive`/`getValue`), and
the validator was made robust to it: it iterates `data.entrySet()` and validates a node
only if it is still mapped under its key, so a node reaped mid-scan is skipped while a
node genuinely stuck in the map (a leak) stays mapped and is still caught. Inside a
computation or eviction listener the same inline cycle is unsupported nesting; see
[ConcurrentHashMap Constraints](#concurrenthashmap-constraints).

**A failed `replace` nudges only when the entry is garbage.** Both overloads signal "did not
update" by clearing `ReplaceContext.oldValue`, which alone cannot tell a dead or expired entry
from a caller's expected value that did not match. `ReplaceContext.garbage` separates the two so
that only the first schedules maintenance. "fix minor edge cases in put and remap" added the nudge
for the expired case but hung it on the shared signal, so `replace(k, expected, new)` also
submitted a task on every healthy CAS failure. Over 10⁶ failing replaces against a one-entry
cache, removing it took an unexpiring cache from 34 ms to 15 ms and an `expireAfterWrite` cache
from 33 ms to 28 ms. Both directions are pinned:
`AsMapTest.replaceConditionally_wrongOldValue_noMaintenance` and
`ExpirationTest.replaceConditionally_expired_maintenance`.

The conditional overload tests `hasExpired` before `containsValue`, matching `remove(k, v)`'s
branch order, so an expired entry is reclaimed whether or not the caller's expectation held.
Testing `containsValue` first would let a healthy mismatch skip the ticker read and reach 16 ms
on the expiring cache, and it was rejected: the two conditional operations should read alike, and
the saving is one `System.nanoTime()` (9 ms per 10⁶ calls on the same machine). `containsValue`
cannot be tested before the null checks at all, since a cleared value reference fails it and the
entry would be misfiled as a mismatch.

## Known JDK Interactions

**StackOverflowError can leak the eviction lock.** If user code causes a
`StackOverflowError` inside a cache operation, the eviction lock can stay permanently held and
block all subsequent writes (JDK-8318888, open; JDK-8319309 is its duplicate). It happens at
release, when `unlock()` fails to run, and at acquisition: `lock()` and `tryLock()` are
`@ReservedStackAccess`, so an overflow inside them completes the acquisition and raises the error
on return, before the caller's `try`. The canonical `lock(); try { } finally { unlock(); }` leaks
the same way, so no call-site change removes it.

**`PerformCleanupTask` is not a `ForkJoinTask`.** `ForkJoinPool.execute(Runnable)` does not wrap a
`ForkJoinTask`, and its source comment says "avoid re-wrap". The steal path claims a slot with a
CAS on the slot content, then writes the absolute value `base = b + 1`. A worker can stall between
its `base` recheck and the CAS. If the pool receives the same task object again, the CAS can
succeed on a later copy, one or more laps around the array. The worker then moves `base` back by a
multiple of the array length, and every slot is null. No code in the pool repairs this state.

JDK 22 and later reject every submission to that queue with `Queue capacity exceeded` (#2021).
JDK 21 and earlier silently lose or overwrite the tasks in that queue. From v2.2.4 to v3.3.0, the
task extended `ForkJoinTask`, and `exec()` returned false. This reuse avoided the per-submission
adapter, which was a profiling hotspot in 2016. The reuse also made a throw from `exec()`
permanent: `run()` catches every `Throwable`, so only its own `logger.log` could throw, on a
`StackOverflowError` while logging or a logging backend that throws, and that completed the task
exceptionally so that a `ForkJoinPool` never ran it again, leaving maintenance to the write
buffer's inline assist and `cleanUp()`. A fresh wrapper per submission confines such a throw to
that submission.

A call to `reinitialize()` before each submission does not prevent the race. The race needs only
the same reference in the same slot, with any task status. A small set of rotated task objects
does not prevent it either. The fix is in the class and not in `scheduleDrainBuffers`, because the
pacer also submits the same object through `Scheduler.forScheduledExecutorService`.

The pool now allocates one `RunnableExecuteAction` for each submission. `drainStatus` coalesces the
submissions that cache operations trigger, and the pacer holds at most one delayed submission. The
two paths can overlap, so one cache can have two submissions in flight. On JDK 25.0.1,
`GetPutBenchmark` and a `weakKeys()` copy ran with 3 forks and `-prof gc`. Every throughput change
was within the 99.9% confidence interval. Allocation increased by at most 0.06 B/op, and by 0.4
B/op for `weakKeys()` read/write. The 2016 result, that the adapter halved `weakKeys()` read
throughput, did not reproduce. Each bounded cache is 8 bytes smaller, because its task no longer
has the `ForkJoinTask` status field.

The task extends `WeakReference` rather than holding one, which saves an object per cache (16
bytes with compressed oops). Its `clear()` and `enqueue()` do nothing, so only the garbage
collector clears the referent. `enqueue()` clears without calling `clear()`, so both overrides are
needed. Without them, an executor that cleared the task it receives would make every later
execution a no-op. This is a deliberate exception to treating a contract-violating component as
the user's bug: the overrides cost nothing on any path, and the `ForkJoinTask` version guarded the
same way with no-op `complete`, `completeExceptionally`, and `cancel`. An executor can still halt
maintenance by never running the task. `BoundedLocalCacheTest.cleanupTask_ignoreClear` pins the
overrides.

## Pacer

The Pacer rate-limits expiration maintenance scheduling. It uses
`TOLERANCE = ceilingPowerOfTwo(1 second)` (~1.07s) as a minimum delay threshold,
preventing scheduling storms from rapid expirations.

`nextFireTime = 0L` is the unscheduled/cancelled sentinel. `calculateSchedule`
bumps any computed fire time that would equal `0L` up to `1L` to prevent a
collision with the sentinel — needed only in edge cases where `now + TOLERANCE`
or `scheduleAt` lands exactly on zero, but the guard removes the ambiguity for
readers of `schedule()`'s recursion check.

**`future == null && nextFireTime != 0L` is a deliberate transient, not a wedge —
and its safety leans on `GuardedScheduler`.** `schedule()` commits `nextFireTime`
(via `calculateSchedule`) *before* calling `scheduler.schedule()`, then publishes
the returned `future`. Between those two steps the pacer is momentarily in that
state, which the immediate-scheduler short-circuit relies on: an immediate scheduler
runs `command` synchronously inside `schedule()`, re-entering `schedule()` before the
`future` is published, and the `nextFireTime != 0L` check breaks the recursion.
**Reaching that state on the replacement path is what makes the short-circuit
complete, and it is why the reschedule arm calls `cancel()` rather than
`future.cancel(...)`.** `cancel()` clears `nextFireTime` and the field together, so a
re-entrant call meets a fully unscheduled pacer and takes the guard; cancelling the
future alone leaves the old one published, and then `!future.isDone()` is permanently
false for an immediate scheduler, so every re-entry cancels and reschedules without
bound. That was a live defect: a `Scheduler` running its command synchronously and
returning a *completed* future hung `put` on every executor including `commonPool`,
silently, because `GuardedScheduler` and `PerformCleanupTask.run` swallow the
`StackOverflowError` and the stack immediately re-descends. Pinned by
`ExpirationTest.schedule_immediate_completed`; its neighbour `schedule_immediate`
returns an *incomplete* future, which is the shape a real inline scheduler never
produces and is what masked this. If
`scheduler.schedule()` could *throw* on the first call, that same state would never
clear — every later `schedule()` early-returns and `cancel()` no-ops (`future` stays
null), permanently disabling prompt expiration. It can't: `GuardedScheduler` catches
every delegate throw and maps a null return to `DisabledFuture`. `Caffeine.getScheduler`
wraps a *user-supplied* scheduler this way, but passes the built-in `systemScheduler()`
and `disabledScheduler()` through unwrapped — Caffeine's own scheduler types are known
not to throw or return null, so only user types need guarding. Either way
`scheduler.schedule()` here never throws or returns null. The ordering is safe as written
— don't wrap it in a try/catch to "harden" an unreachable throw, and don't hand `Pacer`
an unguarded *user* scheduler (that, not the ordering, would be the bug).

**`SystemScheduler` returns a future separate from its timer, so that a cancel removes the JDK's
delayed task.** The timer is `orTimeout` on an internal future, and its timeout completes the
returned future and submits the task. Any completion of the returned future, a cancel included,
completes the timer normally, the one completion on which `orTimeout`'s canceller removes the
delayed task on every JDK: on 11, and on 17 before 17.0.14, it ignores an exceptional completion
such as a cancel (JDK-8303742). `delayedExecutor` keeps no handle at all, so with it each
cancelled task stayed queued until its original fire time (896 B on JDK 25), one per cycle of a
cache emptying and refilling. `SchedulerTest.systemScheduler_cancel` pins the release; on JDK 11,
the default test JVM, a single future fails it. Revisit when the JDK baseline moves: from 21 one
future suffices (`completeOnTimeout`, with the task chained by `thenRunAsync`), and from 25
`ForkJoinPool.commonPool().schedule` returns a cancellable future.

**The system scheduler has three accepted consequences.** A cache that is dropped leaves its
pending timer queued until it fires. The cache is held weakly and remains collectable, but a
process that churns caches with long expiries keeps about churn rate times remaining delay of
those timers. The timer submits the task from the JDK's shared delay thread, so with a direct
executor the maintenance cycle, eviction listener included, runs on that thread and delays every
other `orTimeout`, `completeOnTimeout` and `delayedExecutor` task in the JVM until it returns. A
rejection at fire time loses that cycle only: the returned future is complete before the
submission, so the next re-arm schedules normally, and the rejection is dropped in the timer's
discarded stage. The `executor(Executor)` javadoc warns that an executor "that discards tasks or
never runs them may experience non-deterministic behavior."

**`rescheduleCleanUpIfIncomplete` piggybacks an already-scheduled pacer fire, by
design.** Its immediate resubmission is limited to `executor == ForkJoinPool.commonPool()`,
since it cannot tell a transparent wrapper from a caller-runs executor, so a wrapper around the
common pool (for context propagation, say) takes the custom-executor arms: with a removal
listener under continuous eviction a forwarding wrapper measured 232.9 against 144.5 ns per
insert, against about 8 ns without one. A `drainStatus == REQUIRED` backlog re-arms the
pacer only when `!pacer.isScheduled()`; if a fire is already pending (the next expiration
event), the backlog rides that fire rather than stacking a second schedule. An *expiration*
backlog stays prompt regardless — a >`EXPIRATION_THRESHOLD` backlog leaves an
already-expired deque/wheel head, so `getExpirationDelay` returns `≤ 0` and
`expireEntries` already scheduled the pacer at `TOLERANCE` (~1s). Size eviction is
uncapped (drains fully in one cycle), so it never backlogs. A region transfer
(`evictFromWindow`, `demoteFromMainProtected`) is capped at `QUEUE_TRANSFER_THRESHOLD` and
re-arms after `expireEntries` armed the pacer, so a large window resize rides the pending fire
too, delaying the rebalance rather than overfilling the cache. A *reference* backlog defers
the same way a write-buffer one does, and the delay costs a late `COLLECTED` notification
rather than a stale read, while the entry's other half (a weak key's value, or a weak or soft
value's key, node and table slot) stays reachable until a cycle polls it. A GC schedules no
maintenance, `cleanUp()` polls at most `REFERENCE_THRESHOLD + 1` references per queue per
call, and a cache with only reference collection has no pacer even with a `Scheduler`. The
shape is a *write-buffer* backlog (`drainWriteBuffer`'s `WRITE_BUFFER_MAX` cap, reached only when
a concurrent writer refills during the drain, or its `relaxedPoll` passing over a slot a
producer has not published) on a cache whose next expiration is
distant, that then goes idle: the buffered policy tasks — LRU/weight bookkeeping over
CHM mappings that are *already committed and visible* — wait for that distant fire or
any later write / read-stripe / `cleanUp`. If the pass over an unpublished slot instead settles
`IDLE` (the accepted weak-memory strand) while no other entry is expiring, no fire is pending,
and only a later operation drains the task. Worst observable is a transient over-
`maximumSize` on an idle cache, the documented async-eviction contract — plus, under
*variable* expiry, a deferred **expiration notification**: an entry whose `AddTask` is
still buffered is not yet in the timer wheel, so it was invisible to the
`getExpirationDelay` that armed the pending fire, and its removal listener waits for that
fire (which can exceed the entry's own TTL, since the armed time came from the
policy-visible entries) or the next cache operation. Reads stay correct throughout — lazy
`hasExpired` gates every read. Best-effort amortized maintenance; don't drop the gate to
force a ~1s reschedule (it churns the distant fire's cancel+reschedule for a narrow,
self-healing transient).

**Without a `Scheduler`, maintenance is amortized onto callers, by design.** The immediate
re-arm in `rescheduleCleanUpIfIncomplete` is restricted to `commonPool` because any other
executor may run the submission on the calling thread, where the work is no longer
amortized and the caller pays a whole cycle. The deferred pacer arm above is the fallback
for a custom executor, and it needs a `Scheduler`. Configuring one requests prompt expiration;
a size-only cache has no pacer. Without this arm, a `REQUIRED` backlog waits for the next cache
operation, so a quiesced cache stays over `maximumSize` until then. The excess is capped,
not unbounded: the backlog is write-buffer tasks, `MpscGrowableArrayQueue` is bounded at
`WRITE_BUFFER_MAX`, and a full buffer forces `afterWrite`'s inline assist, so a quiesced
cache's `estimatedSize()` cannot exceed `maximum + WRITE_BUFFER_MAX`. Measured with
`maximumSize(10)`, a single-thread executor and no scheduler, 200,000 writes then idle: 4
of 10 runs stayed over maximum, and the largest residue over 20 runs was 1,987 against a
2,058 bound. The live peak while writers run is higher. A drain applies up to
`WRITE_BUFFER_MAX + 1` insertions before `evictEntries` runs, producers refill the buffer
meanwhile, and each writer blocked in the inline assist has already committed its mapping, so
the peak is `maximum + 2 * WRITE_BUFFER_MAX + 1` plus the writers. With `WRITE_BUFFER_MAX` at
2,048, 16 writers peaked 4,080-4,113 over maximums of 10 to 10,000 and 64 writers 4,161 over,
converging to the maximum when they stopped. The model is a garbage collector's: the excess is
capped and reclaimed on the next operation rather than on a timer. Don't add a third arm. A caller
cannot know whether the executor would run the submission inline, and moving it into
`PerformCleanupTask`, where a held eviction lock does identify a caller-runs execution after the
fact, fails on the same ground: prompt size eviction in an idle cache is not a contract the cache
offers.

## Refresh Internals

**The loader's original future is both refresh token and public result.** Its identity
represents the generation. Per-generation copies would change `refresh(k)` / `policy.refreshes`
and stop cancellation reaching the loader's future. Any holder's completion reaches it the same
way: `complete(v)` is cached and counted as a load success, `orTimeout` fails every caller waiting
on that load and removes the entry, and completing a `Policy.refreshes()` future installs its value
and discards the loader's. Completion also precedes dependent
handlers, so awaiting that future cannot guarantee the cache-updating handler has run.

Reusing one pending future for two generations of the same key is outside this model: the
older handler can claim the successor's token and both can dispose of the produced value,
affecting a non-idempotent listener. Ordinary LIFO dependent completion makes the window
narrow; a coalescing loader can hand out copies itself. A write-time ownership condition
repairs neither double disposal nor orphaning across preserved timestamp changes.

Distinct future instances with custom equality are likewise unsupported. Map conditional
`remove` / `replace` and `refreshes.remove` use equality, which matches identity for standard
`CompletableFuture` but can remove a successor for a hostile subclass. Repair would require
identity-conditional operations throughout both caches and the refresh registry, not a local
ownership check.

**A manual synchronous-cache refresh uses one `LocalLoadingCache.RefreshOperation` per call.**
It carries the registration and completion state, while the loader's original future remains
the token and return value. Registration runs inside `refreshes.compute`; completion is attached
after it returns, and coalescing onto an existing refresh attaches no additional handler.
The async view's absent-key branch registers with `putIfAbsent` rather than a compute and owes
the same rule: it attaches its by-key release only when it won the registration. Attaching that
release to whichever future it returns puts a `refreshes.remove` on a token this caller never
registered, and since dependents run most recently registered first it fires ahead of the owner's
commit, which then reads itself unowned and discards a reload no write had superseded. The
identity argument does not catch it, because the token removed is the owner's; only the caller is
wrong. A refresh completion that commits a mutation still discards the current registration by
key. Pinned by `AsyncLoadingCacheTest.refresh_absent_racingRefresh`.

**`refreshIfNeeded` is intentionally lock-free.** Reads of `writeTime`, `getKey`,
`getValue`, `getKeyReference`, `isAlive`, and the CAS of `writeTime` happen
without `synchronized(node)`. A stale observation could let `asyncReload` fire
on a just-retired node, but the completion-path ABA guards (`currentValue ==
oldValue` + `(node.getWriteTime() & ~1L) == writeTime`) discard the result. Cost
of the rare spurious loader call is accepted to keep the refresh fast path lock-free.
The write-time guard is itself immune to a same-instance overwrite reusing the old
value: a refresh-eligible entry's age exceeds `refreshAfterWriteNanos`, which makes
`exceedsWriteTimeTolerance` true in both of its refresh arms (a duration within the
tolerance takes the always-true disjunct, a longer one is exceeded by the age), so an
intervening write always moves `writeTime` and the stale completion cannot match it.
That arithmetic is load-bearing; a change letting an update skip `setWriteTime` on a
refresh-eligible entry would re-open the stale-reload commit.

**The low bit of `writeTime` is a soft-lock marker, and the completion ABA check
must mask it.** A reader probing for a refresh CASes `writeTime → writeTime | 1`
while it registers the token in `refreshes`, then resets it; it starts no load if
the token already exists, so its transient marker is invisible to any stampede check.
The completion compares the *base* write time (`& ~1L`), not the raw value — a
concurrent reader's transient soft-lock is not a modification, and comparing the raw
value discards a perfectly good reload (issue #1970). The completion also keeps the
token registered in `refreshes` across the value swap: it reads the token to confirm
ownership rather than removing it, and the compute machinery's `discardRefresh` clears
it *after* `setWriteTime`. Holding it for the swap keeps concurrent reads debounced so
none can trigger a stampede in the remove-then-refresh window. Finally, the
`computeIfAbsent` lambda re-validates the marker under its per-key atomicity and aborts
when a concurrent refresh already completed (its `setWriteTime` cleared our marker),
so a delayed reader that passed the `containsKey` gate does not launch a duplicate,
stale reload from the same prior value. On the absent-create path the token clear runs
in a `finally`, so a `weigher` or `expiry` callback that throws while creating the entry
cannot orphan the token (#1970).

**`discardRefresh` is deliberately over-aggressive.** A mutation that races a
refresh discards whatever token is in `refreshes` without trying to prove it's
the same generation. A refresh that registered before the mutation published its value was
launched against the old value, so killing it is correct for linearizability. Insertions and
unbounded writes release inside the map computation, before the new mapping is visible, so they
discard only such refreshes. A bounded update of an existing entry (`put`, both `replace`
methods, and `remap`'s present-entry write) publishes the value before its release, so a refresh
registered in that gap loaded the new value and is discarded as well. That over-discard is
accepted: the successor's reload is declined and the next refresh loads again.

**A refresh completion's mutating exits follow the same rule, and releasing only the completing
token was rejected.** A commit that replaces, creates, or removes the entry is a mutation, and a
manual `refresh(k)` may replace its completed token while it runs. Releasing only the completing
token rescues a successor that loaded the published value, which only a bounded update exposes,
but keeps one that loaded the replaced value and can never install: its registration outlives the
commit, so a later `refresh(k)` coalesces onto its load and is discarded with it, and
`refreshIfNeeded`'s `containsKey` gate suppresses automatic refresh until that load finishes. That
is the residue the prescreen race below leaves, widened to any refresh that registers during a
commit before its value is published. In aligned races of 10,000 trials per case, owner-conditional
release removed 31 discarded successors when nothing followed and added 147 discarded later
refreshes when one did. Pinned by
`RefreshAfterWriteTest.refresh_committingCompletion_discardsStaleSuccessor` and its
`refreshIfNeeded` and absent-create twins, which park the commit in its `Weigher`.

**The bounded cache's `containsKey` prescreen stays, and the race it leaves open is
benign** (accepted). `ConcurrentHashMap.remove` takes the bin lock
whether or not the key is there, so the prescreen is what keeps an ordinary write
off that lock. It was added to the bounded cache only; the unbounded cache still
removes unconditionally. What the prescreen cannot see is an in-progress
`computeIfAbsent` reservation, which reads as absent, so a write landing inside
that window leaves a token launched from the generation it superseded. On the
automatic path nothing is committed wrongly: the completion's ABA guards reject
the value, and the registration re-validates `node.getWriteTime()` inside the
reservation, which rejects any write that lands earlier. That first clause is load-bearing and was
only true by accident until 2026-08-24. The completion's write-time term reads
the node captured when the reload started, and `retire()`/`die()` leave
`writeTime` frozen, so a remove and reinsert of the **same value instance**
presented an unchanged value and an unchanged write time and the guard committed
a reload launched from the dead generation. The commit branch now also requires
`node.isAlive()`, which is exactly the generation test the other two terms cannot
make; both remain necessary, since the write time still catches an in-place
update of a node that never died. Pinned by
`BoundedLocalCacheTest.refreshIfNeeded_reinsertedNode_rejectsStaleReload`. There the residue is
bounded by one load and is a delay rather than a loss: automatic refresh is
suppressed for that key while the orphan is in flight (`refreshIfNeeded` gates on
the same `containsKey`), and a `refresh(k)` issued after the write coalesces onto
the superseded token and is discarded. Don't drop the prescreen to close it, and
don't add a registration-side re-check: closing it there would fix only
`refreshIfNeeded` and leave the manual `LocalLoadingCache` and
`LocalAsyncLoadingCache` registrations, which carry no write-time marker to test,
while implying all three were closed.

**A manual refresh that started absent can commit across the same window, and that is
accepted.** `LocalLoadingCache.RefreshOperation` has no node and no write time, so its only
ownership test is `currentValue == oldValue`, which is `null == null` when the key was absent
at registration. A `put` followed by an `invalidate` inside the reservation window therefore
leaves the token registered and lets the completion install its loaded value over both.
Measured on the bounded cache with a loader parked inside the registration, 5/5 per arm: put
then invalidate installs the load, put alone leaves the written value (so the guard works
wherever it can tell the two apart), and a present-start refresh is discarded. A present start
whose racing writes end on the instance it read (`put(k, 6); put(k, 5)` after reading 5) installs
over them, since the identity test cannot tell that from no write. The escape needs the loader to
run inside `refreshes.compute`, which means a direct executor, a saturated pool with
`CallerRunsPolicy`, or an `AsyncCacheLoader` returning a completed future; on
`ForkJoinPool.commonPool()` the registration publishes first and the refresh is discarded 0/5.
The resulting history is still a legal linearization (put, invalidate, then the refresh's
install), and no public contract covers it: `Cache.invalidate` is undefined only for an entry
being loaded and otherwise not present, which the put makes false. The asynchronous cache does
not share the shape, since its absent case installs an in-flight future through
`computeIfAbsent` on `data` and registers with `putIfAbsent`.

**The unbounded cache keeps the unconditional `remove`, so a write there waits out an inline
refresh load on the same key** (measured: a 2004 ms `put` against a 2 s load). Accepted.
`UnboundedLocalCache` is a light `ConcurrentHashMap` wrapper, and CHM's `put` is pessimistic
anyway, so there is no optimization proper to apply there. The bounded cache takes the
complexity because it already coordinates a great deal of metadata for speed.

**A cleared reference is equal only to itself.** `InternalReference`'s `equals` compares referents,
so two distinct references that have both been cleared used to compare equal (`null == null`) and
aliased whenever their cached `System.identityHashCode` values collided. Nothing in production
depends on that: every lookup that can involve a cleared reference (`drainKeyReferences`,
`drainValueReferences`, `evictEntry`) passes the *same* object, which the identity short-circuit
already answers. `referenceEquals` and `objectEquals` therefore require a live referent. The one
property this drops is cross-type null equality between a `WeakValueReference` and a
`SoftValueReference`, which cannot arise since a cache is one or the other; `ReferenceTest
.reference_equality` now asserts each cleared reference forms its own equality group.

**Automatic refresh uses `>` where expiration uses `>=`, and that stays.** `refreshIfNeeded` tests
`(now - writeTime) > refreshAfterWriteNanos()` while `hasExpired` tests `>=` on all three of its
predicates, so with equal durations an entry is expired one nanosecond before it is
refresh-eligible. The `>` matches Guava and is unobservable on a real ticker (only a `FakeTicker`
advanced by exactly the duration can see it). Ruled 2026-08-24: the javadoc's "once a fixed
duration has elapsed" is not wrong enough to reword, and other Guava-compatibility choices rank
ahead of this one. Don't re-raise the boundary or the asymmetry.

**All three refresh registrations key `refreshes` by `referenceKey(key)`, never by the node's own
key reference.** Under weak keys a node's `retire()`/`die()` calls `clear()` on the very
`WeakKeyReference` the node holds, and `InternalReference.equals` compares referents by identity,
so a cleared reference is equal to no reference built later for the same live key. A token
registered under the node's reference and preserved past `retire()` (the `preserveRefresh` exits
do exactly that) is then unreachable: the successor's own `discardRefresh` builds a fresh
reference and cannot match it, and `Policy.refreshes()` skips it because its referent is null, so
the registration and its future are retained for the cache's lifetime and the growth is invisible
through the public view. `LookupKeyReference` holds the key strongly and is never cleared, which
is why the two manual paths always used it; `refreshIfNeeded` now does too. This costs no extra
retention: the loader call and the completion closure both capture the key strongly for the
future's lifetime. The `containsKey` prescreen still reads `node.getKeyReference()`, which is
free and equal to the lookup key while the key is live, so the allocation lands once per
registration rather than on every refresh-eligible read. Don't key `refreshes` by anything the
node owns. Pinned by
`BoundedLocalCacheTest.refreshIfNeeded_weakKeys_preservedTokenIsDiscardable`.

**A refresh completion releases its own token in its `catch`, not only through `remap`.** The
completion's commit normally clears the registration inside `remap`, so the outer `catch` looks
redundant. It is not: `remap` has throw sites that precede every `discardRefresh`, including
`requireIsAlive`'s broken-`equals` check and the ticker read behind `hasExpired`. A throw there leaves
the registration behind, and because `refreshIfNeeded` gates on `refreshes.containsKey`, that key's
**automatic refresh is suppressed for the rest of the cache's life** from one transient
user-component failure. All three completions therefore mirror their own error branch with the
identity-conditional `refreshes.remove(keyReference, ownFuture)`, which cannot take a successor's
token. Note the throw sites *after* the discard are already safe, since the material tail discards
before returning from the map computation. Pinned by
`BoundedLocalCacheTest.refreshIfNeeded_completionThrows_releasesToken` and its `refresh` /
`refreshAsync` twins, which fail a refresh completion through a throwing `Ticker`.

**A rejected reload is notified even though it was never in the cache.** When the
completion's `compute` declines to install the reloaded value it sets a cause and calls
`notifyRemoval(key, value, cause)` — `EXPLICIT` on the absent exit, `REPLACED` on the reject
exit (a result identical to the current value is not notified). So a `RemovalListener` can see a
value that was never a mapping. That is intentional and follows from linearizability: the value
was produced, the cache decided not to keep it, and the listener is the disposal hook, so *not* notifying
would be the surprise — the value would be dropped with no chance to release what it holds.
Deliberately not spelled out in the public javadoc: the surrounding refresh ordering is
vague there (Guava was not linearizable either), and pinning this corner would over-specify it.
Don't "fix" the notification away, and don't treat the two causes as interchangeable.

Returning the captured `oldValue` still offers it as the new value to retain. If it is no
longer current, rejecting the refresh rejects that attempted resurrection and notifies again;
the earlier removal does not discharge disposal of this new offer. Do not suppress by comparing
with the captured old argument. Disposal after a user `Weigher` or `Expiry` throws during
installation is outside the cache's responsibilities; keep the existing failure cleanup.

The one exception is an internal **query-style no-op**, flagged with `RemapHints.preserveRefresh`:
`putIfAbsent` on a present key, a non-matching conditional `remove`/`replace`, or an async-view
retry that returns an unfinished future without invoking the user's remapping function. Public
remapping functions cannot set these hints; returning the same user value is still a write.
These internal no-op paths leave a racing refresh intact. Both
`BoundedLocalCache.remap` and `UnboundedLocalCache.remap` honor the hint (a same-instance
return with `preserveRefresh` set skips `discardRefresh`); a real mutation still discards.
The unbounded cache used to drop the hint and cancel the reload — the sibling caches must
stay in sync here.

The same hint also **owner-scopes a refresh completion**. When a refresh finishes it
re-enters `compute` to install or reject the reloaded value. Because `refreshes` holds one
future per key, a by-key `discardRefresh` from that completion is safe *only while the
completing refresh still owns the registration*. If a newer refresh has since registered — the
prior token was cleared by a racing write, or by an `invalidate` + `refresh` that re-registers
an `asyncLoad` on the now-absent key — the by-key discard would steal the successor's token,
dropping its freshly loaded value and leaving the cache stale (self-heals only on the next
refresh-eligible read). So each completion path (`LocalLoadingCache.refresh`,
`LocalAsyncLoadingCache.tryComputeRefresh`, `BoundedLocalCache.refreshIfNeeded`) computes
`owned = refreshes.get(kr) == ownFuture` and sets `preserveRefresh = !owned` on its non-commit
exits — reject *and* absent — mirroring the error path, which was already owner-scoped
(`refreshes.remove(kr, ownFuture)`). The hint protects a successor already present at the
ownership check. A successor that registers between that check and the by-key no-op release
can still be discarded; tightening this residual was declined. A later refresh recovers, and
this interval does not justify restoring the rejected token-bearing hint.
Honoring the hint therefore extends beyond the
same-instance no-op block: `remap`'s two absent **null-return** exits (`n == null` and the
evicted-retire) and the unbounded absent exit skip the discard when `preserveRefresh` is set.
The absent-**create** exit does not, and must not: installing a value is a mutation, so the
over-aggressive-discard doctrine applies to it like any other write. Both manual paths create only
in their owned branch, but a successor can replace the completed token before the `finally` runs;
that successor loaded the key as absent, so discarding it is the doctrine rather than a steal.
Every one of the twelve callers that sets `preserveRefresh` either returns null or returns the
existing value of a present entry, so the exit is not reachable with the hint set. Pinned by
`BoundedLocalCacheTest.remap_absentCreate_discardsPendingRefresh` and
`RefreshAfterWriteTest.refresh_committingAbsentCompletion_discardsStaleSuccessor`.

The `!computeIfAbsent` evicted-retire exit is outside the rule for a second reason: it runs
*before* the remapping function, and the remapping function is the only thing that ever assigns
`preserveRefresh`, so no hint exists yet whatever the caller passed (`computeIfPresent` and
`replaceAll`, the only non-creating callers, pass none). Reaping a dead entry for a caller that
may not recreate it is a purge, so it discards, the same reading as `invalidate`/`clear` below.
Pinned by `BoundedLocalCacheTest.remap_evictedRetire_nonCreating_discardsPendingRefresh`.

On a **reject** exit the hint cannot stand alone. `remap` honors `preserveRefresh` for a
same-instance return only at its `preserveTimestamps` no-op exit, so all three completion paths
set `preserveTimestamps` unconditionally there. `refreshIfNeeded` used to set it only when the
value or write time had changed, which left the one sub-case owner-scoping exists for
(registration superseded, entry untouched) falling through to the material tail. Both halves of
the fix then failed together: the tail's by-key discard stole the successor's token, and its
`setWriteTime` moved the write time the successor's own ABA guard tests, so the successor's fresh
value was dropped too and `expireAfterWrite` was extended by the stale reload's in-flight
duration. Gating the tail's discard on `preserveRefresh` fixes only the first half, which is why
the bounded cache does not carry that gate; `UnboundedLocalCache.remap` does, because it has no
no-op exit to route to. There is no debounce lost: a write that discards a token moves the write
time anyway, and a superseded token blocks re-arming through `refreshes.containsKey`. Pinned by
`RefreshAfterWriteTest.refreshIfNeeded_staleAfterReinsert_preservesEntry`, which reaches the case
through a remove and reinsert of the same value instance, since a dead node's write time is
frozen and the completion's ABA guard therefore sees no change. A rejected completion now leaves
the entry refresh-eligible, so the next read arms a fresh reload instead of waiting out another
interval; `BoundedLocalCacheTest.refreshIfNeeded_skip_discarded` reads quietly for that reason.

The **absent-branch** steal is reachable in **sync mode only**: a successor `refresh(k)` on an
absent key registers an `asyncLoad` without inserting the entry, so the stale completion observes the entry absent;
in async mode the successor's `get` inserts an in-flight future, making the entry present so the
completion takes the reject branch instead. Don't reintroduce an unconditional by-key discard on
a non-mutating completion exit; a mutating exit keeps it (see the over-aggressive discard above).

The same sibling-sync covers a **vanished-key skip**: a non-creating caller (`replaceAll`,
`computeIfPresent`) whose key was concurrently removed hits `remap` with `value == null` and
returns null — a no-op, not a mutation, so it must **not** discard a refresh registered
*after* the removal (that refresh raced nothing). Both caches thread a `computeIfAbsent` flag
(`false` for `replaceAll`/`computeIfPresent`, `true` for `compute`/`merge`) and return early
without discarding on the absent branch when creation is disallowed. A creating caller
(`compute`/`merge`) that returns null on an absent key still discards (over-aggressive, as
above). `UnboundedLocalCache.remap` used to discard unconditionally on the absent+null path,
diverging from `BoundedLocalCache` on the `replaceAll`-races-remove race. The async views route
`computeIfPresent`, and the synchronous view's conditional writes when the key vanishes between
their quiet prescreen and the compute, through the creating `compute`, so they discard an absent
key's registration. That is harmless: an async registration for an absent key can only be an
orphan (the optimistic refresh inserts before registering, and eviction and expiry discard with
the entry), so a later `refresh(k)` starts a fresh load instead of joining it.

**`invalidate(k)`/`clear()` discard an *absent* key's pending refresh — a purge is not a
query.** A `refresh(k)` on an absent key registers a reload in `refreshes` with **no data-map
node** (sync `asyncLoad` only), so a `remove`/`clear` that reaches `discardRefresh` only through
a present-node lambda leaves the registration alive; its completion then commits (`owned` still
holds, `null == oldValue[0]`) and **resurrects the key past the purge** — sync-only, since an
async refresh-of-absent inserts a physical in-flight entry that `remove` does see.
The fix keeps `remove(Object)` on `data.compute` (not `computeIfPresent`) so an absent key still
enters the lambda **under the bin lock** and discards there — deliberately, because the refresh
*completion* commits under that same bin lock, so doing the discard outside it (after a
`computeIfPresent` miss) races: the completion can insert between the absence check and the
discard. `clear()` purges the whole `refreshes` map up front for the same reason (the node loop
can't reach node-less registrations). This is the over-aggressive-discard doctrine applied to a
purge, which is legitimate exactly as it is for present keys. Note `remove(k, v)` is **not**
extended: a conditional remove that matched nothing (absent or wrong value) is a query-style
no-op that "raced nothing," so it preserves the refresh (like `remove(k, wrongValue)` on a
present key). The traversal-based bulk removals (`removeIf` and `retainAll` on the key, value
and entry views) and `computeIfPresent` also preserve the node-less registration: the key is not
an element of any view, so they match nothing for it, and only a purge (`remove(k)`,
`invalidate`, `invalidateAll`, `clear()`, `compute -> null`) discards it. Don't move the
absent-key discard back outside the bin lock.

**A sync `refresh(k)` on an absent key is an isolated side-load; the async view makes it a
first-class in-flight entry — a structurally-forced divergence, not a bug.**
`LocalLoadingCache.refresh` on an absent key registers `asyncLoad` **only in `refreshes()`** and
leaves the data map untouched until the completion `compute` inserts. So the pending reload is
invisible to a concurrent `get(k)`, whose `computeIfAbsent` sees an absent map and loads **again**
— two loader invocations, and the refresh's value is then discarded with a **phantom `REPLACED`**
notification carrying a value that was never a mapping. The async view
(`LoadingCacheView.tryOptimisticRefresh`) instead side-loads via `asyncCache.get(...)`, inserting
the in-flight future into the data map immediately, so a concurrent `get` joins it (one load) and
the future is joinable / invalidatable / cancellable. This is inherent: a sync cache **cannot** join
an in-progress `computeIfAbsent` from outside the bin lock, so it cannot dedup a refresh against a
`get` the way the async view (whose loads are visible cache entries) can. The `LoadingCache.refresh`
javadoc — "Returns an existing future without doing anything if another thread is currently loading
the value for {@code key}" — holds under the **narrow** reading (another *refresh* in flight; both
siblings dedup via the `refreshes` map) but not the broad reading (any load, including a
`get`-initiated one); that scope gap is intentional and left as-is. Cancellation blast radius
differs accordingly: cancelling the future from async `refresh(absentKey)` cancels the *shared*
in-cache load (all `get` waiters get `CancellationException`, `handleCompletion` removes the entry),
while cancelling the sync refresh future only unregisters the isolated reload and leaves a
concurrent `get`'s own load untouched. An expired entry that maintenance has not yet removed
takes the same absent route in both caches: if maintenance removes it before the load completes,
that removal discards the sync cache's registration, so the loaded value is notified as `EXPLICIT`
while `refresh` still returns it, whereas the async view replaced the expired entry with its
in-flight load when the refresh began and installs the value. Don't try to make the sync cache
dedup a refresh against a `get`; pinned by `RefreshAfterWriteTest.refresh_absent_sideLoad_*`.

**Sync `getAll` discards an unloaded key's refresh only on the sequential path — by design,
and it reflects a real consistency difference, not a bug.** For a key that fails to load
while a refresh is in flight (an absent key whose refresh is still pending): the **sequential**
path (`loadSequentially`, no `loadAll` override) loads via `get(key)` → `computeIfAbsent`, so a
null load *discards* the refresh; the **bulk** path (an overridden `loadAll`) loads via
`loadAll` outside any lock and side-loads the results with `put`, so an *omitted* key never
reaches `compute` and its refresh *survives*. This is not an oversight to unify. A single
`get(k)` that loads null discards (same `computeIfAbsent`), and sequential `getAll` is exactly
N linearized `get(k)` calls — each observes absence *atomically under the bin lock*, which is
the standing that justifies discarding a racing refresh. The bulk path has no such standing:
`loadAll` cannot run under a lock (CHM won't let us hold a bin across the load, and we can't
lock entries), so it is a **non-linearizable side-load** — closer to `refreshAll` than to a
load. Its only linearized moments are the `put` insert/replace points (where we match Guava,
atomic or not); the "this key was absent" observations happen outside any lock and a key may
materialize afterward (we stomp the still-missing keys but do not remove ones that appeared).
So a `put`, `invalidate` or `clear()` issued while `loadAll` runs is overridden when its results
are stored, re-caching what the loader read, where a per-key load makes the writer wait on its bin.
With no atomic absence-observation instant, it has nothing to hang a discard on. So the
sequential path discards because it *can* judge; the bulk path preserves because it *can't* —
forcing bulk to discard would impose an absence-decision onto the one path structurally unable
to make one. Don't "fix" the split; don't add `discardRefresh` to the `LocalCache` interface
for it. The asynchronous bulk load does not stomp a write to a requested key that lands during the
load: `fillProxies` completes the proxy the write displaced, so the caller receives the loaded
value, the cache keeps the write, and the write's `REPLACED` notification carries the loaded value.
Unrequested keys it returns are stored with `put` and do stomp such a write, or another caller's
in-flight load of that key, whose waiters keep their own result.

**`doComputeIfAbsent`'s new-node path preserves a racing refresh on a weigher/expiry throw —
by design; don't add a `discardRefresh` there.** It discards on a clean value return (a real
mutation) and on a clean null return (the loader's authoritative "no value" verdict, same as a
single `get(k)` that loads null), but a `weigher.weigh`/`expireAfterCreate` throw aborts the
creation without installing anything, so it is *not* wrapped in a token-clearing `finally`. The
rule is coherent: a clean completion (value or null) is an authoritative verdict that discards a
racing refresh; a throw is an aborted op with no verdict, and an independent in-flight refresh
may still legitimately populate the absent key, so it is preserved. This is asymmetric with
`remap`'s create branch, which *does* discard on a throw (`try {…} finally { discardRefresh }`,
the #1970 fix) — but only because `remap` doubles as the **refresh-completion** path and must
self-clean *its own* token on every exit; it can't tell a user compute from a completion, so it
discards uniformly (a doctrine-safe over-discard). That uniform discard is scoped to the exits a
completion can reach — the present-entry `catch` and the create-branch `finally`; the
absent-branch **user-function** throw precedes any materialization and no completion throws there,
so both siblings *preserve* it. `UnboundedLocalCache.remap`'s catch used to discard on that path
too (a blanket `catch (Throwable)` from the "unbounded compute throws" fix whose intent was the
*present*-entry parity); it was narrowed to `if (value != null)` to match `BoundedLocalCache`,
which never had an absent-branch catch. `doComputeIfAbsent` is only ever a user load,
so it can afford the correct behavior. Adding the `finally` here would make a *failed*
`computeIfAbsent` also abort an unrelated legitimate refresh — the inverted direction. Pinned by
`BoundedLocalCacheTest.computeIfAbsent_absent_weigherThrows_keepsRefresh` (extends the
already-adjudicated "mapping-function throw doesn't discard is correct" to the weigher/expiry
throw).

**Quick reference — `discardRefresh` across the compute family** (verified consistent, both
siblings). `compute`/`merge`/`computeIfPresent`/`replaceAll` all route through `remap`;
`computeIfAbsent` uses `doComputeIfAbsent` (BLC) or a direct `data.computeIfAbsent` lambda (ULC).

| Verdict | `remap` (compute · merge · computeIfPresent · replaceAll) | `computeIfAbsent` |
|---|---|---|
| clean value (mutation) | discard | discard |
| clean null — present (removal) | discard | discard |
| clean null — absent (no-value verdict) | discard | discard |
| same-instance no-op (`preserveRefresh`) | preserve | preserve (returns existing) |
| user-function throw — present | discard | discard |
| user-function throw — absent | preserve | preserve |
| weigher/expiry throw — absent create | **discard** | **preserve** |

Cross-cutting rules layered on top: non-creating callers (`computeIfPresent`/`replaceAll`) on a
*vanished* key take the `computeIfAbsent=false` early return and preserve; refresh completions and
async-view no-ops set `preserveRefresh` so a stale completion can't steal a successor's registration
(owner-scoping); `invalidate`/`clear` discard an absent key's refresh (a purge, not a query). The
**only** intentional split is the last row — `remap` doubles as the completion path and self-cleans,
`doComputeIfAbsent` is a user-load-only path (and ULC, having no weigher/expiry, has no such case at
all). The unbounded `remap` catch is guarded by `value != null`, preserving an absent
user-function throw like the bounded implementation.

The **jcache adapter deliberately does not get this narrowing**: `RemapHints` does not cross the
package boundary, so the adapter's query-style operations (a failed `putIfAbsent`, a NONE-action
entry processor) do discard an in-flight refresh and reset the write time. Adjudicated won't-fix —
see `.claude/rules/jcache-adapter.md`.

## Async Synchronous View

**`AsyncCache.synchronous().asMap()` queries are logical, mutations are
physical.** `containsKey`, `get`, iteration, and `containsValue` treat in-flight
entries as absent (`Async.isReady` / `Async.getIfReady`). But `KeySet.remove`,
`removeAll`, `removeIf`, `retainAll`, and `EntryIterator.remove` operate on the
raw delegate map without blocking on in-flight futures. Blocking everywhere
would invite deadlock, and unlike `values()`'s removals and `entrySet().removeIf`/`retainAll`,
which skip an in-flight mapping, these discard it. That is deliberate: the view is best effort,
and it is better to discard too much than to keep stale contents on linearization assumptions
the async cache cannot support. `keySet().contains(k) != keySet().remove(k)`
on a loading entry is accepted. `size()`/`isEmpty()` are physical too — they delegate straight to
the backing map and count in-flight entries.

**Value-conditional CAS ops are the exception to "raw", and it is CHM-faithful.** `remove(k,v)`,
`replace(k,v)`, `replace(k,old,new)` and the `compute`/`merge` family **block** on an in-flight
future (resolved *outside* the `compute`, then CAS inside), mirroring CHM where a mutation must
take the bin lock an in-flight `compute` holds: in-flight is absent to *reads* but blocks
*mutations* as if present. Only the key-based removals above are raw. Bulk collection-view ops
split along the same line: `values().removeAll`/`remove`/`retainAll` are value-searches that skip
in-flight via the `getIfReady` filter (like CHM `ValuesView.remove`), so they do not block on a
mapping in flight when scanned (one replaced by a pending future between the scan and its
conditional `remove(k, v)` still waits in `remapWhenSettled`), while
`entrySet().removeAll`'s iterate-argument branch routes through the blocking `remove(k,v)` (like
CHM `EntrySetView.remove` → `map.remove(k,v)` → bin lock) so it can. It iterates the argument only
when the keys are strong and the argument is not a `Set` larger than the physical `size()`, which
counts in-flight entries, so whether it waits for and removes an in-flight entry depends on how many
other loads are in flight. Don't "fix" that asymmetry —
and a test on the blocking path must coordinate threads (complete the future off-thread), as the
`_async` conditional `remove`/`replace` tests do.

**A logical read can return the value of a superseded future — the synchronous
view is not linearizable.** Each read is a two-step composite (map read, then
`Async.getIfReady` unwrap) with no re-validation between. If the future found in
step one is in-flight, is superseded by a concurrent `put`/`invalidate`, and then
completes, the resumed unwrap returns that future's value even though it was the
mapping at no instant in the reader's window (the completion's identity-conditional
`replace`/`remove` no-ops once the future is unmapped). Only the raw
`AsyncCache.asMap()` view over the futures is linearizable; the synchronous view
reads "the future it found." Double-collecting (re-reading the mapping after the
unwrap and returning null on change) would close it but adds a map re-read to every
sync-view hit for a narrow, non-linearizable-by-design corner. Don't add the
re-check guard.

The same two-step shape is `AsMapView.computeIfAbsent`'s optimistic prior-future
branch, the one exit in its retry loop that does not re-read the mapping. A future
removed and completed while the caller sits between the map read and the `isDone`
test yields that future's value with no mapper call, where an unpaused caller would
have waited on the pending future, looped to `getIfPresentQuietly`, found the key
gone, and computed. Same adjudication: the value came from a future that was the
mapping when it was read, an in-flight mapping counts as present for
`computeIfAbsent` under the coalescing rule below, and `ConcurrentHashMap` promises
nothing about a mapping surviving the return either.

**The compute variants adopt the found future too — `synchronous().get(k, func)` /
`getAll` load-coalesce, they do not recompute.** The present-branch of
`LocalAsyncCache.get` returns any in-flight future it finds (no `isDone`/`isReady`
screen) and `AbstractCacheView.resolve` joins it, so the caller's function never runs
and the future's outcome is adopted — a null result, a rethrown *foreign* load
exception, a raw `CancellationException` (`resolve` doesn't unwrap it), or an
indefinite wait on a stuck future. This diverges from sync `Cache.get(k, func)` (=
`computeIfAbsent`, the ConcurrentHashMap family, which recomputes when the prior load
left no mapping) and from the *same view's* `asMap().computeIfAbsent` (whose retry loop
recomputes on null/exception/cancel). That is not a bug: `Cache.get`/`getAll` promise
neither model, load-coalescing is a legitimate policy (Guava's `LoadingCache` coalesces;
CHM does not), and the synchronous view cannot fully emulate the async view's
linearizability regardless — it "reads the future it found" for computes as well as
reads. Whether coalescing is better or worse is perspective-dependent; the point is only
that it *differs*. Don't "fix" `get(k, func)` by routing it through
`AsyncAsMapView.computeIfAbsent`'s retry loop.

The adopted outcome includes the caller's own failure. A failed future stays mapped until its
completion's dependent logs the failure and removes it, and the caller's `join` can return while
the completing thread is still in that dependent. An immediate retry, through the view or from a
dependent on the future, then rethrows the same exception without calling its function, in 85-99%
of immediate view retries on defaults. Computations are not special here: a future passed to `put`
that fails is visible to the same reads until its removal runs, which is why the public javadoc
does not name a failed computation's window. Removing the failure in the view before rethrowing
was declined: it helps only that caller's retry, since any other call in the window still finds
the future, and "the mapping is left unestablished" holds once the removal runs.

**Read bookkeeping happens only at the lookup that finds a ready value.** `asMap().putIfAbsent`
and `computeIfAbsent` record the access (access time, read expiry, frequency) on their first
lookup. A value they obtain by waiting on a load, or that their atomic step finds after that lookup
missed, is returned without it, so a variable `Expiry` sees no `expireAfterRead` until the next
read. Waiting matches `Cache.get(k, func)` and the future-typed API, neither of which applies read
expiry to an in-flight future. The raced exit is the one place a ready value skips the read
(`Cache.get` goes through `computeIfAbsent`, which reads the entry it finds); covering it needs a
second map read or a remap hint honoured by both caches, for a window between two steps.

**A write or read in the finalization window is accounted as meeting an in-flight load.** The
view's caller has the loaded value once the future completes, but `handleCompletion`'s quiet
finalization runs in a dependent that can follow it, and until then the node carries the async
sentinel. A `put` there is dated by `expireAfterCreate` rather than `expireAfterUpdate`, a read's
`expireAfterRead` is dropped, and the loaded value's own creation is then skipped or applied over
the read. Only which `Expiry` leg dates the entry differs; the mapping, listener and weight are
correct. Telling a delivered value from an in-flight one needs a readiness probe on every write
and read.

In the same window the age and remaining-time accessors (`ageOf`, `getExpiresAfter(key)`) return
empty and every weight surface reports 0, while the value reads, `getEntryIfPresentQuietly` and
the snapshots report the entry present with its current deadline, which under variable expiry is
the insertion time plus `ASYNC_EXPIRY`. Both are deliberate: the entry cannot expire before the
handler runs, and its creation expiry has not been computed; hiding the entry would make
`getEntryIfPresentQuietly` disagree with `getIfPresentQuietly`, and a clamp would still report
about 146 years. Pinned by the `ageOf_async_pendingCompletion` tests and
`ExpireAfterVarTest.getExpiresAfter_async_pendingCompletion`.



## Async Put Re-registration

**`AsyncCache.put(k, future)` re-registers a completion handler whenever the prior
mapping differs by identity, so re-inserting an already-registered future
double-fires `handleCompletion`.** The dedup in `LocalAsyncCache.put` only skips a
*consecutive* same-instance put (`prior == castedFuture`), so `put(k, f1);
put(k, f2); put(k, f1)` leaves two `whenComplete` handlers on `f1`. A single
completion then replays `replace` + `recordLoadSuccess` once per handler. Accepted,
because there is no correct dedup: we cannot inspect a future's already-registered
dependent actions, and tracking our own registration history would be wrong (unbounded,
and stale the moment the entry is replaced). It is benign regardless. Re-inserting a
specific future instance after replacing it is unusual, and the second
`replace(k, f1, f1)` is idempotent: `notifyOnReplace` suppresses on identity, and the
quiet write preserves the expiration because the first handler already cleared the async
sentinel, so the user's `Expiry` sees one creation rather than a creation and an update.
The load is still counted once per handler. Pinned by
`AsyncCacheTest.put_reregisteredInstance_completionRegisteredTwice`.

**Failure cleanup is scoped to the future, not to each insertion.** A deferred finalizer that
fails its weigher or expiry removes `(key, future)` conditionally. If the same completed future
was removed and reinserted meanwhile, that cleanup can remove it again: reinsertion does not
create a new computation or cleanup owner. A distinct successor survives, as pinned by
`AsyncCacheTest.handleCompletion_weigherFails_preservesConcurrentReplacement` and its `getAll`
twin. Do not add an insertion-generation token for the same-future case. Repurposing that future
with `obtrudeValue` does not establish an independent computation lifetime either; use a distinct
future for that. This does not change the intentional bulk-proxy obtrusion below.

**A cancelled bulk proxy stays mapped, and the completer re-asserts its lifecycle.**
`getAll` installs a proxy per absent key and gives its lifecycle to `AsyncBulkCompleter`
rather than to `handleCompletion`, so a caller who cancels the mapped proxy leaves the
entry in place. Measured: the entry stays mapped and counted while queries filter it, a
`get` in that window adopts the cancelled future instead of starting a fresh load, and
once the load settles `fillProxies` obtrudes the value onto that same future, leaving the
entry holding it and the proxy no longer cancelled. The single-key path differs because
its `handleCompletion` removes an entry whose future completed without a value, so
cancelling there is self-healing.

Accepted 2026-08-16 as the least bad of unsatisfactory options, and the reasoning is what to
re-read before proposing a change. Cancellation does not stop the computation, so the value
still materializes, and a cache that dropped the entry on cancel would have no way to hand that
value to a removal listener for cleanup, where an entry removed while in flight does have a
listener attached to notify. A later `refresh(key)`, `invalidate(key)`, or synchronous-view
write such as `putIfAbsent` or `compute` still treats the cancelled proxy as a completed
valueless mapping and replaces or removes it, so in that sequence the bulk value reaches
neither the cache nor the listener, as after a per-key cancel. Cancelling says downstream
chained actions may be abandoned, which still happens; removing the mapping directly says the
value is not cacheable. Obtruding is then the cache treating the value as having materialized
elsewhere and re-asserting its own lifecycle over it. Removal-on-cancel is not free either: the
cancellation happens outside the cache, so it takes a dependent action per proxy, and it would
leave the completer's `replace` unable to install the loaded value. `CompletableFuture` offers
no API that satisfies every case and there is no clean decision matrix, so don't add
cancel-aware completion logic to the bulk path, and don't screen the cancelled future out of
`LocalAsyncCache.get`'s present branch, which the adopt-the-found-future decision covers.

## TimerWheel

**Wheel resolution is one level-0 tick: `2^30` ns (~1.074s).** An advance within a tick
(for example `0 → 1`) yields `delta = 0`; crossing `-1 → 0` after the wrap-bias repair crosses
a rebased boundary and yields 1. Hashed wheels process whole ticks to retain O(1) insertion
and deletion; see [research foundations](research-foundations.md) on timing wheels.

Queries filter an expired entry immediately, but physical removal and its notification can
wait until the next tick. On a fake ticker, expiries of 1ns, 1ms, and 500ms stayed resident
after `cleanUp`; `setExpiresAfter(k, 0)` was reaped at `2^30`, not `2^30 - 1`.
The bound is one tick, not a whole wheel revolution: `expire` scans `[start, start + delta]`
inclusively (`steps = 1 + delta`) and checks each node's deadline. `getIfPresent` and
`containsKey` filter without removing; the compute path (`get(k, fn)` through `remap`) reaps.
Fixed expiration is more eager because its deque has no tick boundary to wait for.

**That bound assumes a deadline that has not moved since the node was scheduled.** A
variable-expiry read applies its new duration through `tryExpireAfterRead`'s CAS and leaves the
matching `reschedule` to `onAccess`, which runs only when the read buffer accepted the node. A
dropped offer leaves the node in the bucket its previous deadline chose, so a shortened deadline
waits for that bucket's sweep. Measured with an `Expiry` of 1h on create and 2s on read, 400 keys
read once each on the default executor and system ticker: 208 to 368 keys across five trials kept
their original bucket and none were readable, and the deterministic fake-clock arm reaped them with
`EXPIRED` at 3574s against the 3s they had asked for. A `Scheduler` does not shorten the window,
because `getExpirationDelay` reads bucket occupancy rather than deadlines and reports the next flush
3569s out. The delay ends at the sweep of the previous deadline's bucket, at or before that deadline,
so a dropped read never makes an entry outlive the schedule it already had; a lengthened deadline
self-heals the same way, its node sitting in an earlier bucket whose sweep pushes it forward.
Accepted: the node reference is gone once the offer is dropped, routing the reschedule onto the write
buffer would put most reads of such a configuration on the MPSC queue, and declining the CAS instead
would leave the entry readable past the duration the user asked for.

**`setExpiresAfter` reschedules through the write buffer only for a shortening or for an extension
beyond the 1s tolerance.** A smaller extension relies on the same self-healing, so `oldest()` and
`youngest()` order the entry by a deadline at most a second stale. A shortening keeps the write
buffer: a lost reschedule would leave the entry resident past its new deadline. The JCache adapter
calls `setExpiresAfter` on every access-expiry hit, and the `UpdateTask` made each hit pay what a
write pays. Alone, the skip took such a hit from ~2.2 us to ~195 ns on 8 threads. Beside the
adapter's own millisecond skip it saves ~25 ns more. Pinned by
`BoundedLocalCacheTest.expireTolerance_setExpiresAfter` and
`ExpirationTest.setExpiresAfter_reschedules`.

The resolution is accepted for a cache's maximum-lifetime contract. It aligns with the 1s
expiration tolerance and pacer's minimum delay; a bounded cache can also reclaim by size.
An eager current-bucket sweep remains a possible optimization, not an approved change.
It costs O(bucket occupancy) per maintenance cycle: detach onto `pending`, deschedule, and
reschedule every not-yet-due node, instead of paying once per tick. Measure that cost before
removing `delta <= 0`; a nearest-bucket heap is another structure for stricter timer resolution.

**`expire` detaches its bucket onto a field-backed, circular `pending` sentinel.** Detachment
prevents a not-yet-due node from being rescheduled into the list being drained, especially on
the last wheel's single bucket. It does not prevent a nested `RemovalTask` from descheduling
an entry it already holds. A stack-local detached chain used to strand the remainder when
that operation cleared the carried successor's links. Preserve all three protections:

- The chain lives on a **field** (`pending.next`), not a local, so a nested `unlink` of the head
  repairs the walk's anchor through the ordinary path with no special case. The loop must re-read
  `pending.getNextInVariableOrder()` each iteration rather than carry `next` across the callback.
  This is the leg `advancing` cannot cover: a nested `deschedule`/`reschedule` arrives via
  `drainWriteBuffer` → `RemovalTask`/`UpdateTask` and never calls `advance`. Verified by keeping
  `advancing` while restoring the stack-frame detach — the regression test fails identically.
- Both lists stay **circular**. A linear or sentinel-less chain breaks `reschedule`'s
  `getNextInVariableOrder() != null` scheduled-check (a tail with a null `next` reads as
  unscheduled, so it gets linked into a bucket while still chained) and makes `unlink` a silent
  no-op on the tail. The invariant is *scheduled ⟺ non-null links*, which is also why the in-flight
  node's links are nulled before it is processed. A self-closing ring fixes the live-bucket splice
  but **not** the stranding — verified by building it and watching the regression test still fail.
- `advance` **defers while `advancing`**. A recursive advance would append its bucket to the same
  list and splice the combination into its own bucket, and — the leg that matters more — it moves
  `nanos` forward underneath the in-progress caller, which then rewinds or skips buckets and leaves
  entries permanently unexpired. It returns before touching `nanos`, so the nested call is a true
  no-op rather than skipping a time span. The predicate must be the explicit flag, **not** "is
  `pending` non-empty": the outer walk empties `pending` the moment it unlinks its last node, so
  the inferred version lets a nested advance through in exactly that window. That was a real
  defect in the first cut of this fix, found by the fuzzer in 559 runs.

Pinned by `BoundedLocalCacheTest.maintenance_recursive` (a removal listener on a same-thread
executor invalidating a sibling and calling `cleanUp()`).

**`getExpirationDelay` returns when a bucket is next *flushed*, not when an entry is due.** A flush
either expires the entry or **cascades it towards a finer resolution**, so the wake-up is needed even
when nothing is due at it. `expire` covers ticks `[previousTicks, previousTicks + delta]`, so a bucket
at offset *b* flushes at `(b << SHIFT[i]) - (nanos & spanMask)` and the **current** bucket — whose tick
has already passed — flushes at offset **1**, one tick out, *not* a full `SPANS[i]`. Both places that
compute this must use that offset-by-one:
- The main scan clamps with `Math.max(1, j - start)`. It previously fell back to a whole `SPANS[i]`
  when the current bucket was occupied *and* returned before probing `start + 1`, hiding an event one
  bucket ahead that then fired up to a span late (68.7s / 1.22h / 1.63d on wheels 1–3; wheel 0 is
  absorbed because both values are ≤ `Pacer.TOLERANCE`). Reachable because `findBucket` chooses the
  **wheel by duration** but the **index by absolute time**: a duration within `nanos & spanMask` of
  `SPANS[i+1]` wraps a full revolution onto the current bucket, so a far-future entry masks an
  imminent one. Don't restore the `delay > 0 ? delay : SPANS[i]` ternary — with `buckets >= 1` the
  result is always positive, so the guard is dead.
- `peekAhead` checks the higher wheel's **current** bucket as well as `ticks + 1`; both flush at the
  same instant. This leg is benign on its own — only wrap-arounds land in a wheel ≥ 1 current bucket
  (measured: over 300 random wheels no entry there is ever due before that wheel's next tick), so the
  missed flush would only re-file an alias. It is honored anyway so the derivation "current bucket ⇒
  only aliases" isn't load-bearing, and so the contract is assertable.

Partial-drain remainders are **not** affected — `expireVariableEntries` re-arms via
`PROCESSING_TO_REQUIRED`, so the backlog never depends on this delay. Pinned by
`TimerWheelTest.getExpirationDelay_occupiedCurrentBucket` and by `getExpirationDelay_fuzzy`, which
asserts **exact equality** against an oracle scanning every bucket of every wheel. That fuzzy
assertion was previously vacuous — it guarded the assert with its own condition and compared an
absolute `variableTime` against a relative delay — which is why the defect survived the fuzzer.

**The `VarExpiration` traversers list wheel 0's current bucket last.** It holds the entries due
within the current tick and aliases a revolution away, so `oldest()` lists the former after entries
due up to a minute later, and `youngest()` lists them first. That is accepted under the views'
"roughly ordered" contract. Starting at the current bucket misplaces the aliases instead, and
splitting it into two visits must classify each timer once, since a concurrent read can move a
deadline between the visits and list the entry twice or not at all; the buffering that needs is
not worth it for a best-guess order. The traversers are also wheel-major and unsorted within a
bucket, so they can invert two entries by up to one span of the higher wheel.

**Interner `drainKeyReferences` does not need a value-identity check.** Unlike values, keys on
interned nodes never rebind. Cleared references now compare equal only to themselves. In the old
implementation, hash-colliding cleared keys could alias, but both polls still evicted both nodes;
only attribution swapped, unobservable with uniform `Boolean.TRUE` values and no listener.

## Builder and bulk loading

**`LoadingCache.getAll` is not atomic.** Valid entries may commit before failure and are not
rolled back. Per-key null results omit that key; bulk maps must omit a key to mean no value.
An explicit null value fails the bulk load in both sync and async paths, matching Guava's failure,
though not its residue: Guava first stores every non-null entry, where the synchronous path keeps
only the entries before the first null and the asynchronous path keeps none.

**`Caffeine`'s builder mirrors Guava's `CacheBuilder` validation shape, asymmetries included.**
`refreshAfterWrite(long, TimeUnit)` checks the unit before state and requires `duration > 0`;
`expireAfterWrite` / `expireAfterAccess` do not check the unit first and allow zero.
Consequently `expireAfterWrite(-1, null)` throws `IllegalArgumentException` rather than NPE.
Changing either order diverges from `CacheBuilder` and the adapter's compatibility suite.

**`Caffeine.from(CaffeineSpec)` disables strict parsing**, matching `CacheBuilderSpec` and
allowing programmatic overrides such as a weigher after `maximumSize`. The possibility of
accidentally disabling eviction is accepted.

## Serialization

**The `SerializationProxy` captures configuration only, and intentionally drops
`executor`, `scheduler`, and custom `StatsCounter` suppliers.** Threads and
executors are runtime state, not serializable configuration; the deserialized
cache uses the defaults (common pool, disabled scheduler, default counter),
matching Guava's proxy behavior. Don't propose capturing them.

**Serialization is same-version only, and that is not a gap to close** (ruled
2026-08-15). The library never promises cross-version compatibility, and neither
does Guava; a promise of that kind has to be offered by the contract rather than
expected of it. So a finding of the shape "a stream written by an older release
deserializes into a broken or silently wrong cache" is closed on principle, not
on reachability or on whether the failure is loud. No `serialVersionUID` bump, no
`readObject` defaulting, no wire-format tolerance shim, no release note, no
javadoc qualifier. This covers `SerializationProxy`, `CaffeineConfiguration`, and
any serializable type added later.

**Proxy field names and sentinel values are still wire format within a version.**
Two changes broke cross-version streams: the `loader` → `cacheLoader` rename
(3.0.4) and the `0` → `UNSET_INT` sentinel change for the three duration fields
(3.2.4, the zero-duration fix). The reason to know this is not compatibility but
debugging a bug report: streams from ≤ 3.2.3 carry literal `0` for unset
durations, and a field *absent* from an old stream deserializes to the JVM
default (`0`/`null`), because field initializers do not run during
deserialization.

---

### .Claude Docs Finding Taxonomy (`.claude/docs/finding-taxonomy.md`)

# Finding Taxonomy

Standard classification for all audit and review findings. Referenced by the
auditor agent and all `/audit-*` and `/review-change` skills.

## Severity

| Level | Definition | Example |
|-------|-----------|---------|
| critical | Violates correctness under any valid interleaving | Data race causing lost update |
| high | Violates contract under specific conditions | ConcurrentMap postcondition broken on retry path |
| medium | Correctness preserved but fragile | Works only because of current CHM implementation detail |
| low | Style or robustness concern | Missing null check on code path unreachable today |

State the affected configuration separately from the harm within it. Being non-default or in a
lower-priority module does not establish a severity ceiling; apply the module's stated use-case
scope and price the demonstrated impact.

## Categories

| Category | Scope |
|----------|-------|
| memory-model | JMM access mode violations, missing happens-before edges |
| state-corruption | Weight divergence, lifecycle violations, deque corruption |
| specification | ConcurrentMap/Map/Cache/Policy API contract violations |
| resource-leak | Unclosed resources, unrecoverable state after Error |
| notification | Missing, reordered, or duplicated removal/eviction notifications |
| exception-safety | Incomplete cleanup on exception paths, phantom state |
| api-surprise | Public method returning nonsensical values for valid inputs |
| performance | O(n) on O(1) path, unnecessary allocation on hot path |
| redundancy | Removable work or state with a behavioral-equivalence proof and a concrete source simplification |
| liveness | Progress/termination failure without corruption: stranded drain status, lost wakeup, unbounded stall |
| external-contract | Misuse of a third-party or JDK API contract (dispose-on-error, merge-less collectors, live-view iteration) |

## Severity must be priced on a realistic configuration

A severity is a claim about what a **user** can reach, so the reproduction has to run the
configuration a user runs. The test harness is built out of instruments that exist to make
behaviour deterministic, and each of them can manufacture an impact nobody can reach in
production:

- **A frozen `FakeTicker`.** Several mechanisms self-heal simply because the clock moves.
  `TimerWheel.advance` sets `nanos = currentTimeNanos` unconditionally, so with
  `Ticker.systemTicker()` any operation reaching maintenance invalidates a live wheel iterator.
  Freeze the ticker and that detector stops working, which turned a truncated `Policy` snapshot
  into a reproducible non-terminating traversal holding `evictionLock`. With a real ticker the
  same scenario throws. The wedge was an artifact of the instrument.
- **`executor(Runnable::run)` or `CacheExecutor.DIRECT`.** Inline maintenance removes the
  coalescing that hides per-operation cost and turns an asynchronous drain into a re-entrant one.
  Use it to control scheduling; its timings do not price default-executor behavior.

Before assigning `high` or `critical`, re-run the witness with the defaults a user gets: the
system ticker and the common pool. If the impact depends on harness effects absent from the
claimed user configuration, identify those effects and do not attribute them to that configuration.
A mechanism that is real and an impact that is reachable are two separate claims, and the second
is the one severity encodes.

## Confidence

| Level | Meaning |
|-------|---------|
| high | Concrete interleaving or input constructed; reproducible |
| medium | Plausible scenario but depends on timing or conditions not fully verified |

Omit low-confidence speculation entirely — it wastes triage time.

For `redundancy`, high confidence may rest on a complete source equivalence proof covering the
relevant exits, callers, and observations. Keep cleanup severity `low` and report test execution
and runtime effects separately. A measurable speedup or broken contract is not required; do not
invent one to justify the cleanup.

## Classification (for triage)

| Label | Meaning |
|-------|---------|
| patch | Real issue, fixable in the current change |
| defer | Real issue but pre-existing, not introduced by this change |
| reject | Noise, false positive, or explained by design decisions |
| escalated | Cannot determine statically; needs dynamic testing |

---

### .Claude Docs Hill Climber (`.claude/docs/hill-climber.md`)

# The adaptive window climber

The deep reference for `WindowClimber`, the window-sizing loop `BoundedLocalCache` drives from
`climb`: what it must do, why that is hard, the shipped design, and the graveyard of alternatives
with the reasons that killed them. Read this before touching `determineAdjustment`, the climber
constants, or the simulator's climbing package. The quick rules live in `rules/design-decisions.md`.

This is a design record, not a project log. Every entry answers one of two questions — what the
machine does and why, or what was tried and why it failed — and anything that only answered "what
happened on a given day" has been removed. Where a number appears it is load-bearing: it is the
price of a step, the bar a family is held to, or the measurement that settles an argument. Numbers
that can be re-derived by running the gate battery are not kept here.

`wiki/adaptive-window.html` is the companion design document, written for an external reader judging
the algorithm and its evidence. Internal QA logistics — test and fuzzer names, mutation baselines,
gate tooling — stay out of it and live here and in `rules/testing.md`.

Naming: the **window climber** is the whole controller (`WindowClimber`); its tiers are each named
for their steering signal — the **reactive climber** (≤4096, cross-sample ΔHR) and the **density
climber** (>4096, within-sample density ratio). The density tier in full is a **goal-audited density
climber**: density steers, probes rescue its blind corners, and the goal-metric layer (anchor, guard
rail, equilibrium audits) polices what density cannot judge. "density arm/tier" below always means
the steering component, not the whole machine.

The organising result of the original failure atlas — the steering rule rests where
`capacityShare = hitShare`, not at the hit-rate optimum — is §2.1's identity, and the
marginal-steering thread it opened is settled in §5.

## 1. The problem

W-TinyLFU splits the cache into a recency-serving admission window (LRU) and a frequency-gated main
region (SLRU behind a TinyLFU filter). The best split is workload-dependent and non-stationary:
recency-heavy workloads want windows of 20–80%, frequency-heavy ones want ~1%, and real traces
switch regimes. The climber's job is to track the optimal split online using only signals the cache
can observe about itself, at zero cost to the request path.

Practical comparators include LRU, static windows and ARC on the same access stream, with MiniSim
testing what additional observation state can reveal. The objective is broad workload quality
within the cache's cost budget, not a universal policy optimum. Losses to these references remain
generic headroom; richer application metadata cannot explain them away.
A specialized policy may do better using context or structure, such as request category, turn and
prefix depth. OPT/Bélády instead knows future accesses. A gap to that oracle proves neither that
an online feature can recover the gap nor that learning such a feature must be expensive.

Inferring useful reuse structure from generic observations is a separate, testable question. It
requires bounded state and work, enough labels during cold start, an observation stream available
in the cache, and controls that distinguish persistence from scans and changing workloads.
The failed skew classifiers (§5's opening) do not rule out all inexpensive inference. Arbitrary
key hash values do not supply application semantics: do not treat them as addresses,
request categories or turns. Specialized contextual policies and generic adaptivity therefore
need separate evidence, while sharing the same discipline for information, action and cost (§6).

Constraints that shape everything:
- **The counterfactual is invisible.** The cache observes only what its *current* split earns.
  What a bigger window *would have* caught requires ghost state (ARC's B1/B2), which Ben rejected:
  heavy, implies untrue things, and does not pay off as caches grow.
- **Moving has real cost.** A resize transfers nodes between regions (capped per maintenance cycle
  by `QUEUE_TRANSFER_THRESHOLD` with a multi-cycle carry-over) and displaces resident entries.
  On corda_large the *churn alone* cost 10.5pp — every fixed window scored 33.33 while the moving
  climber scored 22.8.
- **The goal metric is noisy at exactly the wrong times.** A phasey workload swings the sample hit
  rate ±10–20pp for reasons unrelated to the window, burying the window's marginal effect (often
  under 1pp) — a cross-sample hit-rate comparison then chases the workload's mood.
- **There is no setpoint.** Control-theoretic machinery (PID and friends) needs an error signal
  toward a target; hit-rate climbing has no target, so integrators accumulate noise unboundedly
  (the 2026-05 PID catastrophe: corda_lg −32 to −98pp, unfixable by any anti-windup variant).

## 2. The signals

- **Cross-sample ΔHR (the reactive climber, ≤4096):** bold-driver — keep direction while the hit
  rate improves, reverse otherwise, decay the step, re-anneal to a 6.25% step when |ΔHR| ≥ 5pp
  (`RESTART_THRESHOLD`). Goal-driven, so it can be *confused but never persistently
  wrong* — the restart keeps it exploring. Weaknesses: churns on flat/phasey curves (corda), and
  cannot cross wide plateaus deliberately (bold-driver flips on any sub-noise negative ΔHR, turning
  a plateau crossing into a random walk).
- **Within-sample density ratio (the large tier's primary signal):**
  `error = ln(windowDensity / mainDensity)` with `windowDensity = sample.windowHits/windowMaximum`
  and `mainDensity = (sample.hits − sample.windowHits)/(maximum − windowMaximum)`; step
  `sign(error) · min(0.30·max, |error|·0.03·max)`, window floored at 2% of max. Both densities are
  measured in the same sample, so the workload's phase cancels in the ratio — this is what makes it
  immune to the swings that defeat the reactive climber, and why it converges recency workloads to
  their static-window ceiling (OLTP 1%→~21%, metaCDN, fiu_homes, corda's collapse fixed).
- **What density cannot see:** it is *resident-only* and measures *average* (not marginal) value.
  Two consequences: (a) **bias** — window hits are cheaper to earn, so frequency-optimal traces
  give back ~0.5–2.5pp typically (worst observed 4.3pp: websearch2@4M holds a ≳10% window where
  static-1% ≈ reactive wins, a fresh-holdout find; all still beat LRU by 11–61pp,
  that cell by +24); (b) **blindness** —
  a region earning ~nothing has an unmeasurable density, and the signal will happily hold or push
  it further into nothing forever.
- **Signal hygiene (what feeds the counters):** hits are recorded in `onAccess` (read-buffer
  drained, so real hits are ~5% lossy single-threaded — the drain was historically triggered only
  by the offer *after* the fill; the buffer now reports full on the filling offer so the loss is
  recovered), misses in `AddTask`. Only genuine usages may count: async load completions replace
  **quietly** (no sketch increment, no hit counters — the UpdateTask only finalizes weight/expiry).
  When loud, each async miss injected one synthetic, write-buffer-lossless, window-attributed hit
  plus a doubled per-load admission frequency: the reactive tier wedged on phase shifts
  (corda+loop @ 512 −12.7pp, deterministic) and the density tier read miss-heavy async traffic as
  window-dense (w50 −38.6pp). Any new write-path task that calls `onAccess` must decide
  loud-vs-quiet explicitly — user-initiated writes are loud, internal bookkeeping is quiet.

### 2.1 The control-theory map

The density tier was derived from measurement, not from a textbook, but it lands on a named
architecture: a **supervisory switching controller** with **hysteresis** and **state-dependent
average dwell time**, wrapped around a **certainty-equivalence** inner loop, with an explicit
**dual-control probing** term — an event-triggered, non-dithering **extremum-seeking** scheme.
The vocabulary is worth carrying because three of the design's empirical facts fall out of it as
derivations, and because it names what a proposed change would be breaking.

| Mechanism | Formalism | Reference |
|---|---|---|
| `DensityClimber.steer` on `ln(d_w/d_m)` | certainty-equivalence inner loop; P-on-error into an integrating plant = I-on-the-balance-condition | Åström & Wittenmark |
| the log ratio itself | exponentiated-gradient step on the capacity simplex (scale-free by construction) | Kivinen & Warmuth 1997 |
| its rest point vs. the optimum | average- vs. marginal-value equalization | Stone, Turek & Wolf 1992 |
| anchor + guard rail | hysteresis switching; `VETO_MARGIN_MIN` is the constant `h` | Morse, Mayne & Goodwin 1992; Hespanha, Liberzon & Morse 2003 |
| `VETO_STREAK`, `AUDIT_COMMITMENT`, `Ladder.commitmentDepth`, `Anchor.freshLeft` | dwell time | Morse 1996/1997; Liberzon 2003 |
| the doubling ladders | average dwell time with chatter bound `N₀` | Hespanha & Morse 1999 |
| probes and audits | dual control — steering has no *dual effect*, the probe supplies it | Feldbaum 1960–61; Bar-Shalom & Tse 1974 |
| the probe verdict (retain unless refuted) | unfalsified control — no plant model, data from the active controller only | Safonov & Tsao 1997 |
| starvation bar | loss-of-persistent-excitation detector | Boyd & Sastry 1986 |
| confirm / veto streaks | sequential change detection | Page 1954; Basseville & Nikiforov 1993 |
| terrain B, the moat, the rung-scaled stride | non-local stability of extremum seeking (escape needs large-amplitude excitation) | Tan, Nešić & Mareels 2006 |

Three consequences worth stating outright, because each replaces a fact currently carried as
folklore:

1. **"There is no setpoint" is true of the goal metric and false of the loop.** The command is a
   *change* in window size and the cache accumulates it, so the plant is a pure integrator and
   proportional-on-`error` **is** integral action with setpoint `error = 0`. The 2026-05 PID
   catastrophe was not "integrators explode here" — it integrated `HR − target`, an error whose
   zero was *invented*, through a plant that already integrates. The density arm integrates an
   error whose zero is a physical balance condition. The design decision was to move the
   integrator onto a signal that has a true zero, which is why no anti-windup variant could
   rescue the old one.
2. **The rest point has a name and the recency bias has a derivation.**
   `error = ln((H_w/C_w)/(H_m/C_m)) = logit(hitShare) − logit(capacityShare)`, so `error = 0`
   ⟺ `capacityShare = hitShare`: **average-value** (proportional-share) equalization. The
   optimum equalizes **marginals**, `H'_w = H'_m`. For hit curves concave through the origin,
   average ≥ marginal, and the region with the larger average−marginal gap is over-allocated;
   the window is an LRU serving tight reuse, so it saturates fastest and has the larger gap.
   That *derives* the documented give-back ("frequency-optimal traces give back ~0.5–2.5pp") and
   its direction. It also says what the goal-metric layer is: not a patchwork, but the
   correction term for a bias with a closed form. Terrain shape B is simply non-concavity, where
   any first-order matching rule rests self-consistently — F-4 is the definition, not a surprise.
3. **The ladders imply a duty cycle, and the duty study measured the real one.**
   Worst-case arithmetic (a 17-sample walk against each rung) overstates the machine 2–4×: over
   6193 density samples on the 49-cell battery the mean **armed** excursion is 4.2 samples
   (starvation) and 7.7 (audit), and the occupancy split is `steer` 42.2%, `park` 25.9%, audit
   excursion 19.0%, starvation excursion 5.9%, blind-corner `hold` 6.1%, `undo` 1.0% — the
   machine commands nothing (32.0%) more often than it explores. (R4-F1's "6% → 43%" shieldtrap
   occupancy was the round-3 clock; at the shipped tree that cell reads 13.4% park + 13.4%
   walk.) A crash-prone workload sustains
   audit duty indefinitely (~11% at cycle ≈7.7+64: `crashnoise_a12`, `whisper_mod_a12`,
   `h4c1_attack`), because the wait reaches 512 only through completed deepest-rung failures
   while a crash deliberately keeps the ladder cadence — R4-F1's shape, structural. Two
   refutations from the same study: **the constants are not derivable from one declared duty
   number** (a declared audit band recovers two of the four), and **no duty or dwell bound can
   see the deferral defects** — H4-C1 and adv4-F2 violate the schedule's *provenance*, not its
   magnitude (`maxOverdue` measures 0 through the whole 130-sample H4-C1 pin). What the duty
   budget does generate is the C2 *progress* clause (§6).

Two caveats so the map is not over-claimed. Morse's hysteresis lemma bounds the switch count
only for **monotone or exponentially discounted** monitoring signals; `Rates.smoothed` is a
symmetric EMA whose `Anchor.rate` re-syncs on-anchor, so the finiteness bound does not transfer
as written (Hespanha–Liberzon–Morse's discounted monitoring signals are the standard repair, and
would give a stated bound on anchor churn). And "scale-independent hysteresis" in that paper is
the *multiplicative* form `μ_p(1+h) < μ_q`; the rail is additive with a noise-adaptive margin —
related, not the same.

The map also predicts the rail/audit split the F1 study found by measurement: in dual control the
**cautious** branch is conservative in the *loss* metric while the **probing** branch is optimized
for *information*, so the two want opposite pricings and must not share one margin. That has now
found three instances, the last of them **inside a single walk**: the rail's margin against the
audit's confirming streak (F1), the starvation probe's bar against the audit's (adv3), and the
audit walk's own crash abort against its reversal (§4). The third is the sharpest
reading of the principle, because the two exits are five lines apart in the same walk and shared
one threshold: the crash abort is the cautious branch, priced on the loss it is measuring, while
the reversal is the probing branch, which must not turn around on evidence it cannot distinguish
from noise. Where a proposed change makes one bar serve two questions, this is the shape to check
for. One open
direction comes from the same lens: a detrended confirm reference (both
`Walk.beatBase` and `Anchor.freshLeft` exist because the reference is an un-detrended *level*, and
"a trend clears any raw streak" is what detrending removes by construction).

The map's other two suggestions were **built and refuted** by the derived-guard study
(§5). Freezing the walk's deviation reference at arm is wrong because that feedback is
load-bearing, and a within-sample confidence interval is both too small to bind and answers the
wrong question for the position jam. Read those graveyard entries before proposing either again:
a formalism that names a mechanism correctly still does not license changing it.

## 3. The adversarial cases (the ones that must always be re-run)

The generators, run commands, and per-family verdicts for this section live in the committed
`/climber-gate` skill (`.claude/skills/climber-gate/`); the traces are deterministic and
regenerate on demand, so only the generators are preserved. New families are found by
`/audit-regret`, which searches a compositional workload generator for cells where the machine
does not close the gap to its static ceiling and classifies each failure (wrong equilibrium,
slow convergence, masked signal, insufficient exploration, oscillation, memory, irreversible
damage, aliasing, premature commitment, tier discontinuity, structural) against the layer that
owns it; a confirmed family lands here and in the gate table.

- **corda + 5×loop + corda phase-shift stress** (real traces, bundled): run at
  512/513/1024/2048/4096/4097/8192; no cliffs at either tier boundary; near static ceiling.
- **mixture family** (`gen.py mixture`, seed 7): steady Zipf
  hot-set (defends main, feeds mainDensity) + twice-accessed items at reuse distance
  D ∈ (2%, 25%]·max. No phase structure. Pure density pins at the floor for 100% of adaptations
  and lands ~28pp below LRU — at 8192 and any scale where D falls in the band (the trap is
  scale-relative because the floor is 2%·max). This is a plausible production shape (popular
  content + session-scoped re-reads) and the reason a tier threshold alone cannot make density safe.
- **phases family**: alternating loop-over-0.9·max and pair-reuse-at-D phases; d050 (D=50%·max)
  traps pure density at 8192 AND 32768. It is **bimodal with a 14pp run-to-run range** (the
  admission tiebreak picks the basin), so §6's ±0.1–0.8pp noise model does NOT hold and
  single-run numbers here are uninterpretable; the trace-start **alignment offset alone is worth
  up to 12.5pp**, frozen for the run because the sample grid never dithers (no aliasing comb at
  commensurate ratios — 38 ratios × 12 reps, mean +0.62pp mixed sign). What is real is the **lag
  limit**: phases shorter than ~2 sample periods (~8·max requests) are untrackable. Current
  classification: d050 is one of the three genuine steady-state pins, −8 against
  LRU converged, its grid-locked cadence constructed-only.
- **deadphase**: hot-set + pure one-shot scan bursts. Proves exploration during dead samples is
  ~free (admission shields main) — all variants sit at the ceiling. **But the safety is a knife
  edge on the ε symmetry, not on the dead sample** (failure atlas): a fully dead sample
  gives `ln(ε/ε) = 0`, a no-op; break the symmetry with as few as **six window hits** and the same
  sample yields `err = +17.4` and the full 30%-of-max step. A rider of **380 requests in 1.97M
  (0.019%)** riding a victim's own scan phases costs the victim 8.3pp and drags the window from
  0.03 to 0.48; at 0.4% it pins the ceiling for −9.6pp. Delivered through `armProbe`'s
  refractory fall-through, which suppresses the probe but not the density arm. Reactive tier at
  4096 on the same trace: −0.44pp.
- **widepin**: whole-working-set alternation (pairs at 0.6·max ↔ loop over 0.85·max). A fixed
  window wins by never moving; every online climber pays here. Re-classified: the
  whole-trace deficit is mostly **warmup** (it converges to a 77.9% window against an 80%
  optimum, final third −3.7 vs LRU), and the row is **seeded-only** — its unseeded distribution
  is a 50/50 basin coin whose old bar sat at its own mean. Bar: no per-seed drift, seeds 1–8.
- **Thin-signal floors** (w50@123038, S1/S2/S3, DS1): frequency-ish traces whose floor window
  earns wh/n in the *same band* as a trapped window (w50: 131–437 hits per 492k sample vs trap
  strays ≤57 per 32k). **No resident-only threshold separates them** — this is measured, not
  conjectured, and it is why probes+adjudication exist instead of a smarter starvation predicate.
- **lowmix** (`gen_attacks.py`, seeds 7/11/13): low-HR (2–7%) bistable shape whose probation
  concentrates the reuse band while the good state is an LRU-ward diluted window — the marginal
  verdict's named trade (the frozen baseline false-vetoes the escape that the rejected
  diluted-average form confirms by seed-luck). Sentinel: no drift below the recorded values.
- **demoflood** (`demoflood.py`): protected-saturating hot core + walk-reachable band — the
  constructive proof that live-probation adjudication is an absorbing false-veto and the
  pre-walk freeze is earned. Gate row: frozen-v5a must stay far above the live-variant's pin.
- **whisper + window-irrelevant modulation** (`whisper_mod.py`) and **mixmod**
  (`gen_adv.py mixmod`): the F1 dose instruments — a hit-rate modulation that provably does not
  change the optimal window (pin-sweep verified) must not turn the goal-metric layer off. The
  deviation-priced confirm failed this dose-responsively; owned by the raw-sample streak
  confirm and the fresh-park shield, with the deep a0.12 dose (whose scatter crosses the
  audit's crash bar) closed by the crash-streak time persistence — the sentinels now sit
  within half a point of LRU. `mixmod` reads −2.4 against the reactive arm and is
  **constructed-only**. `esc_jam` and `tenant_s10` ride along as
  jam-family and co-tenant-family sentinels for the same layer.
- **shieldtrap / climbtrend / loopcliff** (`gen_adv.py`, round-2 instruments promoted to gate
  rows by round 4): regime-change-after-confirm, trend-driven misconfirm (plus the flat+wave
  `saw_p40` variant), and the no-cliff invariant at the structural misconfirm landing.
  `shieldtrap` and `saw_p40` are the **audit-amplification sentinels** (R4-F1): the round-3
  clock repairs multiply audit reachability — worth +10.4 on the attack rows, giving back
  −1.0..−2.1 on these already-below-LRU synthetics (audit share of the run 6% → 43% on
  shieldtrap, which also turns bimodal) while twelve real density-tier cells move ±0.07.
  The rows exist so the next audit-schedule change is measured against the cost side too.
  **Part of that give-back was never the schedule's**: the audit-bar split recovers
  +0.79/+1.75/+1.17 (N=8: 78.06/77.19/77.44 → 78.84/78.94/78.62, re-based in the gate table) with
  `saw_p40` at 0.00, because `AUDIT_BAR_FRACTION` was pricing the walk's reversal at a median
  0.28σ here and killing walks on samples the machine itself calls noise. Attribute the remainder
  to reachability, not all of it.
- **posjam** (`gen_jam.py`, round 3's position-jam instruments): the whisper base with the
  whisper dose *modulated* at the sample cadence instead of constant — dose-matched and
  provably window-irrelevant (LRU and every static window identical to the flat control). The
  modulation makes the density arm command super-band steps, which starved the hard-reset
  audit clock forever (66.9 → 56.1 = the audit-free pin, −8.9 below LRU; scale-invariant at
  16384). Owned by the decaying clock and the audit-owned schedule (the `j50` ±50%-jitter and
  `d25` period-2.5 rows recover to 66.4–66.5); the sample-aligned every-sample variant (`d0`)
  still jams — it needs ±¼-sample grid alignment (the D4 exposure class), is a deterministic
  9.3pp regression against the reactive arm, and is **constructed-only, priced and parked**
  (the stillness-measure study stays deliberately unspent). A slow
  square duty re-creates the starved clock below every-sample motion (the Sol
  round's `entry_duty`, a dose note on the `posjam_d0` gate row); the loss there is the
  rider's class-3 mask, not the jam.
  The optional `audit-regret/specs/bandrider.json` control uses a finite-distance rider
  (pairs at 0.15·max) with a sine-modulated share, a steady far band at 0.67·max, and a
  Zipf core. At maximum 8192, workload seed 1093 and 256 continuous samples, admission
  seeds 1–8 give hybrid **51.44** (51.36–51.52), bit-identical to `noaudit`, against
  **62.01** for reactive and a **64.94** best tested static reference. All eight default-start
  histories have no audits after one early starvation confirmation. The later 128-sample
  suffix still loses 12.92pp to the same fixed 45% reference, so the deficit is not only
  startup. Holding probation at its live 1622-entry allocation gives 64.78–64.90 at
  43/50/60% windows; actual adaptive starts at 43% and 60% give 65.28 and 65.37. These are
  different initial histories, not frozen decision counterfactuals. The observed small-window
  orbit is path-dependent. Eleven one-factor neighbors at 128 continuous samples retain 13.4–13.9pp gaps at one
  admission seed (amplitude, period, phase, distance, scale and trace seed); a lower modulation dose
  escapes later. Its amplitude ladder mixes delayed escape, unsuccessful audits and audit
  absence, so a hit-rate threshold does not identify a mechanism boundary. This is another
  class-3/class-5 dose beside `entry_duty`, not a new failure class or mandatory gate bar.
  No individual missed audit's benefit was measured.
  The `audit-regret/specs/stillness_starve.json` dose is the class at its extreme: a trickle rider
  and a two-reference band, square-modulated together, keep 63–90% of samples moving, so not one
  audit arms in 128 samples, the calibration audit included. Seeded 1–8 at 8192 it reads
  51.16 ± 0.13, identical to `noaudit`, against reactive's 56.08 and a 60.0 static peak, at every
  period from 2.3 to 9.0 samples. The shrink (`stillness_starve_min.json`, the band dropped) keeps
  the mechanism, and holding the rider steady arms four audits. Another dose, not a new family.
- **crashnoise / mixnoise** (`crashnoise.py`, `mixnoise.py`, adv3 rows): the whisper and
  mixture bases under a mean-centred, RMS-normalised rate modulation whose **amplitude** is
  the dose — the walk's exit-bar instruments. The two interior exits test different statistics
  (crash abort = level vs the rate frozen at arm; reversal = first difference vs the previous
  sample — §4), so the dose ladder is a threshold, not a gradient: nothing moves under the
  bar, the cell falls off where the scatter crosses it. `mn8_sine_a10` was the defect on the
  **starvation-probe** path — the wave crash-cycled every up-probe one stride off the floor,
  severing the mixture trap's only escape — healed by the probe-side pricing (51.6 → 60.7,
  +5.6 over LRU). `cn_sine_a12`, the audit-side sentinel, is **bimodal** (its "deterministic"
  record was an N=2 artifact) and healed most of the way by the crash-streak time persistence
  (−2.8 → −0.8 vs LRU); the residual is the crash abort's robustness price.
- **whisper** (`whisper.py`): a mixture base plus a ~0.2% immediately-re-read trickle sized to
  keep the window's sample hits JUST above the starvation bar, so no blind corner is ever
  declared and no probe arms — the sharpest F4 instrument (pinned −9pp below LRU at 8192, −12
  at 16384, knife-edged exactly on `MIN_SIGNAL_SHIFT`; the quarter-dose variant drops below
  the bar and the marginal verdict escapes where the average form stays pinned). Owned by the
  equilibrium audit: 66.8 against LRU 64.6 on every seed, identical under both verdict forms
  above the bar.
- **shallowmoat** (spec `audit-regret/specs/shallowmoat.json`):
  a zipf over 1.5·max that fills main and gives the window only stray hits, a two-reference band
  6,200 requests apart caught only past a ~58% window, and a one-shot scan that keeps probation
  thin. The static curve is a valley 2pp deep and 57% of the cache wide with a 19pp prize behind
  it, so no walk crashes and no first-round walk reaches it. Seeded 1–8: 27.4–28.5 against a
  42.0 ceiling and LRU 39.4, and the same product planted at 70% holds 42.0. Two basins, one
  outcome: the starvation up-probe adjudicates at 4×bar hits three or four strides out, confirms
  against the thin frozen probation, `Ladder.reward` resets the rung, and the density arm reverses
  the window in the same sample, so the ladder alternates 1↔2 and no commitment depth is ever
  reached (absorbing at 127 decisions); or the calibration audit confirms on the trace's warmup
  trend at 32%, one stride short of the cliff, and parks. Reach (class 4) through a successful
  verdict erasing the ledger (class 6). Splitting the band over a spread of reuse distances keeps
  the gap (26.8 / 29.0 at N=8) and adds the average law's rest-point give-back from above. The
  reactive arm beats the machine on 6 of 8 seeds (+2.0 mean).
  Reach was repaired the same day (§5): a confirm the density arm reverses now
  deepens the ladder instead of resetting it, and the wedge seeds cross the cliff at s17 (30.7 /
  30.0 / 30.6 against 28.5 / 28.4 / 28.5). The row still reads far below the ceiling because the
  found position is not kept: after a walk from the floor, density's rest point on the far side is
  at or below the cliff (main holds the protected core, 3.7 hits per entry against the window's
  1.2), the guard rail cannot catch the fall (the fall inflates the deviation its margin is priced
  from), and the deep walk's confirm at the top is itself reversed, so nothing parked. Retention
  landed: a reversed confirm at the deepest commitment that the goal metric
  confirms parks as an audit's does, and the park survives its own audits' crash-scale moves; the
  wedge seeds read 36.5 / 36.4 / 36.3 (repeat 2: 37.1) against the 42.0 ceiling, the residual being
  the top corner's periodic down-audit crashing at the cliff. Two facts from `/audit-regret` round 2
 : the basin is decided by the first sample's window hits against the bar (30–31 hits arm a
  probe at s1 against a fat frozen probation, which fails, basin B; 32–34 leave the floor sighted until
  s3, when a probe against a thin one confirms, the wedge), so a regenerated trace reshuffles which seeds
  cross; and both repairs live on the starvation ladder, so a rider that keeps the floor sighted removes
  them: `veilmoat` (the same trace plus a k=8 burst re-read 40 requests apart at 0.06% of traffic, worth
  0.3pp) arms no starvation probe on any seed, and every seed then takes the audit path (26.84 ± 0.34 at
  64 samples on 8 of 8 against a paired unmasked control of 4 of 8 crossing; the rung-32 up-audit crosses
  at ~s88, 32.02 at 128 samples), and after the recorded top-corner loss the guard rail's veto and
  density's slide form a ~17-sample limit cycle with no re-escape (32.45 ± 0.07 at 256 samples, where the
  unmasked geometry re-escapes through the deep starvation walk to 34–40). The mask holds from ~3e-4 of
  traffic (below that the ladder is the parent's own lottery); a dose note, not a family. A park's first
  audit following its walk turns the basin-B seeds' second audit up from the 32% park
  into the cliff crossing at s52 (27.4 → 30.1 on the 64-sample row, 32.2 → 34.0 at repeat 2; the wedge
  seeds identical) and does the same for `veilmoat` (26.84 → 30.54 on 8 of 8, 32.02 → 33.93 at 128
  samples, 32.45 → 33.11 at 256); the retention residual after each escape is unchanged.
- **absolve** (specs `audit-regret/specs/absolve.json` and
  `absolve_p8.json`): a lure population (pairs 1,300 requests apart, 10% of traffic) pulsed on and off
  with a period of 16 samples inside a wide flat valley, a far band (pairs 6,000 apart, 45%) caught only
  past ~54% of the cache, and a zipf core in main. Static: 18.8 to 10%, a +5pp shelf from the lure's knee
  to 50%, 52.5 at 70%; LRU 51.5. Seeded 1–8: 22.59 ± 0.18 at 64 samples, 24.77 ± 0.11 at 128, an escape at
  s144–159 (36.4 at 256 on seeds 1–4), reactive 25.5 and noaudit 22.9 trapped as well, and the same product
  planted at 70% holds (51.07 ± 0.21). Two mechanisms by trace length. Through 64 samples it is the audit
  tier: the calibration audit's level-test confirm at 32% on a phase step (the lure switching off raises
  main's hits by 5pp against a stale reference), a 32-sample park, the down-first alternation and its crash
  when the lure returns mid-walk, the undo's arrival discarding the anchor: `shallowmoat`'s basin B at
  another dose, on every seed. From s60 a cycle paced by the lure: the off phase blanks the window and the
  corner arms a ×1 walk, the on phase confirms it at 2,200–3,400 on the lure's own hits (the verdict cannot
  attribute the watched region's hits, and the lure is dense per slot and worth nothing above its 8% knee,
  `flood_j100`'s shape pulsed), density rests on the lure at ~37% then slams the window home when it goes
  off, and the ladder gains one rung per period because it gets one walk per period, with the kept
  confirms resetting it to 1 (`Ladder.reward`); the escape needs a walk longer than any first-round one
  (six ×1 strides from an arm caught mid-slam at ~1,650, or the rung-32 ×2 walk) and comes ~140 samples in.
  The average law also abandons a partial catch of the band at 4,300–4,400 (window density 1.30 against
  main's 1.59). At a period of 8 (`absolve_p8`, 256 samples) every confirm is kept, the rung reads 1 or 2 on
  223 of 255 samples and never reaches 32, the audit clock never fires (the position never stills 16
  samples), and the cell is absorbing: 27.92 ± 0.19 against a 57.4 ceiling and LRU 55.6. Robust to the
  grid (period 12: 26.35 ± 0.16 at 128 samples; a phase offset: 28.52 ± 0.86; period 20 a 2-of-8 escape
  lottery), to the band distance and to 16384 (18 of 18 neighborhood cells at 32–34pp at 64 samples), and
  a knife edge on the lure's share at ~1.6% of traffic, where the calibration audit's direction flips from
  up-from-the-floor to down-from-an-interior-position. Reach (class 4) through the verdict's attribution
  (class 3), density's chase (class 5) and the ladder reset (class 6); the period-8 form is class 6
  outright, the `deferreward` shape (§5) made deterministic. **Repaired**: the starvation ladder prices a confirm at or
  short of the farthest window its walks have confirmed as a completed experiment, so the period-8 form
  escalates 1 → 64 over six periods and the rung-64 ×4 walk crosses the valley in two strides (27.95 →
  46.29 at 256 samples, above the reactive law's 45.28); and a park's first audit follows the walk that
  confirmed it while the claim stands, so the period-16 form's second audit walks up from the 32% park
  into the band instead of down into the lure's knee (24.77 → 41.46 at 128 samples; 36.45 → 46.89 at 256;
  period 12 and the phase offset 26.4 / 28.5 → 44.0 / 43.1). Period 20 stays a 2-of-8 lottery. What
  remains after each escape is the recorded top-corner residual (the down-audit crashing at the cliff and
  the undo's arrival discarding the anchor).
- **ghostclaim** (spec `audit-regret/specs/ghostclaim.json`): two
  phases, a zipf core with a band 2,000 requests apart and a scan for 28 samples, where density rests
  at ~43%, then a sleeper population 6,400 apart at half the traffic, caught only past a ~64% window.
  Static: 19.9 at the start, 54.1 at 70%; LRU 51.8. The stale-claim family's away-anchor case: the
  calibration audit's down-walk re-syncs the anchor's claim to phase 1's rate as it passes 20%, density
  holds the window at 43%, and the shift lands with the window still and off the anchor, so the
  stand-down keeps the claim (the carve-out). Seeded 1–8: 41.88 ± 0.17 at 64 samples (the s37
  up-audit sits at the top for ten samples at +30pp and fails at budget against the claim), 31.24 ± 2.10
  at 128 (the same claim then vetoes the window to the phase-1 anchor and its hold, the down alternation
  and the deepest-rung wait pin it), reactive 37.96 (bimodal), noaudit 30.30. Knife edges by
  construction: the stand-down's band (a phase-1 band share of 0.16 lands the shift on the anchor and the
  claim is discarded) and the prize's rate against the claim (a sleeper referenced three times clears it).
  Class 6 with class 4 as the pin. **Repaired**: the audit's walk is measured against the smoothed rate it leaves rather than the anchor's
  claim (31.24 → 48.45 at 128 samples, above the reactive law; the witness 41.88 → 46.86; `cp_w100` +2.0
  on the corpus). The discard shape died on the moat rows (§5). What remains is the shift landing on the
  audit's arm sample or during its walk (`ghostclaim_p35..p40`), where the walk crashes on a
  contaminated base and the stale veto's hold pins the machine, and an audit arming inside the smoothing
  horizon after a shift, which measures against a blend of the regimes.

- **crestpast** (spec `audit-regret/specs/crestpast.json`): a
  uniform bulk over 1.2·max at 72% of traffic (main's population, its hits linear in main's capacity)
  and a two-reference band 1,100 requests apart at 25%, caught by an 8% window. Static: 45.8 at 1%,
  44.7 at 5%, 62.5 at 8%, 64.4 at 10%, then a decline of ~0.9pp per 6.25% stride to 54.3 at 80%; LRU
  52.3. A crest 17pp high, one stride from the floor, with a cliff on its near side and a slope on its
  far side. The calibration audit crosses the crest on its first stride and confirms on its fifth
  (`AUDIT_CONFIRM_STREAK` completes at stride 4, `AUDIT_COMMITMENT` holds the verdict to stride 5),
  so it parks at 33% earning 3.2pp less than the crest it walked over; the park's audit walks back
  down, has the crest under foot at its fourth stride and its fifth on the cliff side, where the level
  test crash-aborts before `auditEnding` can be reached; the undo's arrival discards the anchor (the
  C2 shape) and density's average law, which reads the caught band as a dense window against a thin
  main, drifts the window up and away from the crest until the next audit fails at the corner and
  the rail vetoes back. Seeded 1–8: 57.93 ± 0.04 at 132 samples against 64.9 at 10%, reactive 46.5
  and noaudit 46.2 both pinned at the floor (the hit-rate law steps into the 1–7% decline and never
  crosses; density steers the sighted window down), and the same product planted at 10% leaves the
  crest (62.4). The verdict's position is the mechanism: the confirm lands at stride max(k + 3, 5) for
  a crest crossed at stride k, from either side, so the two endpoints the machine visits (the floor
  and the floor's fifth stride) can neither of them confirm the crest, and what the machine holds
  between audits is the average law's rest point above it. Planted at 40% the down-audit's fifth
  stride lands on the crest and confirms there, and the row still reads 59.7 (gap 5.0): a confirmed
  crest is lost after one audit cycle (the corner audit's fail, the veto, the second park's crash,
  density's drift), so the retention half is the larger part of the steady loss and the verdict's
  position decides where the first park lands. Each stride's sample is an arrival transient, so on
  most cells the walk's best sample is one to two strides past the static crest, which bounds what a
  verdict at the walk's best could recover.
  The far side's slope prices it: bisecting the bulk's size finds the knee at 0.68·max, main's capacity
  at the park, below which the bulk still fits from the parked position and the overshoot lands on a
  shelf (`demoflood`'s recorded overshoot, which the C2 discard there accidentally corrects because
  density's rest point sits at that crest). Robust across the band's distance (800–2,200 requests),
  the bulk's size (1.0–1.5) and 16384 (ten cells at 4.4–8.0). Class 9 (the audit verdict,
  `Walk.isConfirmed`) with class 1 holding the loss and the C2 discard handing it over. With the crest
  beyond the fifth stride (the band 4,000–4,400 requests apart), the same verdict parks the window in
  the valley on the cold reference instead, `regimeramp`'s recorded misconfirm at ten times its
  sentinel's dose (`s_fatbase`: 29.6 against 44.3 at 45%, LRU 37.2, at 72 samples).
- **hazefloor** (`/audit-regret` round 3's note, rowed by round 4 at 320 samples; spec
  `audit-regret/specs/hazefloor.json`): a uniform haze over 2·max, a zipf core, a band 7,200 requests
  apart caught only past ~60% and a scan; static flat 33.7 → 32.5 to 50% then 52.3 at 65% (a 60/65%
  re-sweep), LRU 49.2. The calibration audit crosses and parks at the top by s24, and the rest of the
  run is the top-corner residual by itself with a ~105-sample period: the corner's audit is forced down,
  crashes at the cliff, the undo's arrival discards the anchor, density slides off the cliff, the
  floor's ×1 walks fail against the haze, and the rung-16 audit re-crosses. Seeded 1–8 at 320
  samples: 41.09 ± 1.80 (38.3–42.0), 8pp below LRU, against the reactive law's 36.5 and noaudit's
  37.5, so the layer earns its keep and loses most of it every cycle. A sentinel with per-seed bars,
  not a pass bar; the row exists so a change to the corner audit, the C2 discard or density's slide is
  measured against it.

- **parkveil** (specs `audit-regret/specs/parkveil.json` at
  240 samples and `parkveil_min.json` at 60): a zipf core (0.5·max, α 1.05) and a two-reference
  band (pairs at 0.45·max) whose shares alternate in a square wave at period 13 samples, plus a
  10% one-shot scan. The fixed mid window wins by construction (ceiling 66.82 at 50%, an interior
  plateau wholly above LRU 66.04; the product planted at 50% holds 67.03), and every adaptive arm
  sits below LRU: seeded 1–8 the machine reads 53.13 ± 4.09 (a 6-vs-2 basin split), `noaudit`
  55.73 ± 0.55, the reactive law 59.91 ± 0.92. Density's rest point alternates with the mix (the
  ratio's within-sample phase immunity holds for rate swings, not mix swings), each rec phase
  blanks the window into a blind corner, walks crash on the phase boundaries or confirm on the
  flips as wedges (the ladder escalates 2 → 64 as designed), the rung-64 walk parks at the top
  corner as an audit-grade confirm earning within ~2pp of the plateau, and the park survives
  exactly one fresh-park shield: flips inside the shield are absorbed, the park's own audit crash
  returns to it (the cover), and the first post-shield flip fires `isWorkloadShift`,
  whose stand-down discards the claim at the anchor and re-seeds the goal metric, so the position
  is re-derived through the full ladder every cycle. The guard rail never fires on any seed: the
  phase swing holds the deviation at 0.21–0.34 and the veto margin at 62–101pp. The cycle is
  arm-independent (`noaudit` parks at s134 through the ladder alone and re-parks more reliably;
  the audit layer's margin here is delay from a calibration flip-misconfirm's reward reset, plus
  variance), so the loss lives in the shared stand-down and shield machinery. The last-third gap
  is 6.0 of the whole-trace 13.7 (half the headline is one-time ascent); a ~2pp per-cycle
  re-derivation cost and a 2-of-8 lost-seed tail persist. Robust across the period axis (gaps
  27/27/7/27/12/17/12/13 at periods 2–32, a resonance structure with one narrow escape at 6),
  amplitude, band distance (deepens to 22.1 at d=0.6), 16384 (21.2), phase offsets (6.7–20.2,
  none clean) and trace regeneration (~2pp milder). Class 5 (the chase; controller) with class 6
  (the discard; recovery layer); the census C3 observation's first cost-bearing cell, and the
  mirror of `flashpark` (there the shield holds a wrong park through a regime end; here the
  shield's expiry ends a right one).

- **latebloom** (specs `audit-regret/specs/latebloom.json` at
  262 samples and `latebloom_min.json` at 187): a zipf core (0.15·max, α 1.0) at 60% of traffic
  and a scan for 112 samples, flat within the audit's level test at every window, then a
  two-reference band 7,000 requests apart at a third of traffic for 150 samples, caught only from
  a 50% window. Static: 52.4 from 1% to 40%, 66.4 at 50%, 66.2 at 70%, 65.9 at 80%; LRU 64.5.
  The prelude spends the audit schedule: the calibration audit parks the floor on the cold-start
  lag, the park's two audits walk their full budgets to the 80% cap on flat terrain and end
  failed rather than crashed, and the second failure at rung 64 doubles the clock to 128
  (`AuditClock.reschedule`). When the band arrives the floor's rate drops 15pp and the stand-down
  fires, but a stand-down leaves the audit rung and the clock's wait untouched, so nothing is
  rescheduled; at the base's alignment the arrival lands inside the undo's retreat, where
  `isParkTest` covers it, and the held floor survives the change outright. Either way the
  machine sits at the floor against its own law (rest_err +1.3) until the 128-sample wait runs
  out and the next audit catches the band in two strides. Seeded 1–8 the base reads 54.90 ± 0.04
  against the 66.4 ceiling (gap 11.5, 18% of the prize, 123 samples parked), `noaudit` 53.39
  pinned at density's ~19% rest point (the core's head keeps the window sighted at ~4,000 hits a
  sample and worthless at the margin, and the band is a +19pp step between 27% and 51% that a
  marginal law cannot see) and `reactive` 53.44; planted at 50% it reads 57.85, since density
  slides off the flat prelude before the prize exists. The loss is the latency and nothing else
  (gap ≈ (confirm − arrival)/n × 23.9pp fits every cell within 0.5pp), and the phase-1 ladder is
  the family's shape: an arrival during an audit's cap-sit is confirmed at once (56/104 samples:
  0.9/0.8), one on a walk's early stride crashes it and is found at the next wait (48/96:
  3.5/4.4), one just after a budget failure releases the park by the stand-down, lets density
  rest at ~19%, and the next audit alternates down from that interior position and fails into
  the 128 wait (64/80: 16.7/15.4, found at ~s247 when the horizon allows), and one during or
  after the retreat is found ~125 samples late (112–132: 9.8–11.5). Over arrival times the mean
  is 7.2, the median 6.2, the worst 17.1, and about a tenth are found at once; the base is the
  76th percentile. Identical at 16384 and flat in the band's distance down to 3,500 requests,
  where the held floor still catches nothing although density's rest would; the minimal witness
  (187 samples) is the never-found form, 55.78 ± 0.02 against `noaudit` 55.79. Class 4 (the
  exploration cadence, `auditWait`, and `chooseDirection`'s side choice from an interior rest
  point) on a class-3 terrain; the retreat cover's hold is incidental above the family's
  boundary. Nearest recorded: H4-C1 and `metronome`, which reach wait 128 through crashes on
  trap terrain; this reaches it through the designed path and prices a detected regime change
  that reschedules nothing. The latency face of that residual, whose
  re-arm was priced dead (§5's re-arm entry: the stand-down alignments' gaps fall to
  5–6 and the price lands on the F1 sentinels).
- **mainsat** (`/audit-regret` round 7; spec `audit-regret/specs/mainsat.json`, 72 samples): the
  main-side masked signal, the sighted lane's directed slot for seven rounds. A zipf core over
  0.45·max at 30% of traffic (recurrence ~12k requests, longer than the window's residency at
  every window, so main holds it and the window never takes it), a band 5,200 requests apart at
  18% caught from ~50%, a 52% scan that keeps the window's residency short enough for the core to
  stay main-resident even at 80%, and a gap-1 trickle at 0.3% that keeps the window sighted.
  Static, dense-swept: 20.3 flat to 40%, 27.2 at 50%, 32.0 at 52%, 35.8 at 58%, 35.4 at 62%,
  34.6 at 70%, 33.5 at 80%; LRU 30.5. Main's 7,000 hits a sample are real and worth nothing at
  the margin (main holds 8,028 entries for a 3,686-key set), so `steeringError` commands a
  down-step every sample, no probe can arm (a dense main is not a blind corner by design), and
  the raw law pins the floor: `noaudit` 21.41 ± 0.48 against the 35.8 ceiling. The equilibrium
  audit is the only explorer and does the work: the calibration audit confirms a real cold-start
  gain at 32% (+3.3pp over the floor while main warms faster under the larger window; the band
  is five strides past the floor, beyond its reach) and parks for 32 samples, the park's audit
  walks up through the band and confirms at s48 on the peak (six seeds at 56–57%, two at
  68–69%), and the loss is a ~48-sample approach at any horizon: 28.04 ± 0.18 at 72 samples (gap
  7.8), 32.63 ± 0.30 at 160 (gap 3.7, ~1pp of audit duty permanent). Planted above the cliff the
  product does not hold: density slides the window off the cliff at ~49%, the rail returns it,
  and on the landing sample the rate recovers by 11pp, which `isWorkloadShift` reads as a
  workload change, so the stand-down discards the claim with the window on the anchor and the
  slide repeats every ~19 samples with the anchor creeping down (planted at 70% over 160
  samples: 29.99, ten vetoes, `closed` −5.2; at 55%: 27.95). Implemented as the return
  cover (`isReturnTest`): a return's landing and settle samples wait for the retest, which
  breaks the cycle (160 samples: 70% 30.06 → 35.30, 55% 26.13 → 35.08, two vetoes each; the
  pre-cover baseline sat below LRU on both), `noveto` reads 33.75 / 30.82 there, and the cover is
  bit-identical on the unplanted path, where the anchor is a park and the rail never fires.
  Release posture is **HOLD** on the repeated-real-trace recheck; the planted
  synthetic prize remains valid, but the original one-pass gate was not the full price. Class 3
  owned by the audit layer, as `whisper` is on the window side, with
  class 6 on the plants: the C2 arrival-discard shape on the rail's return, which the
  re-price found unreachable because the rail never fired across the battery. Rowed as the
  main-side mask's sentinel.
- **sidecliff** (spec `audit-regret/specs/sidecliff.json`, 96
  samples): a zipf core (0.15·max, α 1.0) at half of traffic, a near band 300 requests apart at 8%
  that the 2% floor already catches in full, and a far band 7,000 apart at 30% caught only from a
  55% window. Static, dense-swept: 36.4 at 1%, a 42% shelf from 5% to 40%, 44.4 at 50%, 63.4 at
  55–65%, 62.3 at the cap; LRU 61.6. The near band's fixed hits spread over a growing window pin
  the average law's rest point at ~14%, sighted and worthless at the margin, so `noaudit` and
  `reactive` sit there (41.85 / 41.80) and the audit layer is the only explorer. The calibration
  audit arms at s7 with `AuditClock.down` still at its initial value and a stride of room below the
  rest, walks down, loses half the near band at the floor, reverses and fails through its own base
  (`arung` 32, `auditWait` 32), and the machine then holds the shelf for 33 samples until the
  rung-32 up-audit crosses the step at s47 and parks at the cap at s50: 52.51 ± 0.03 on the parent
  cell at seeds 1–8 (identical event sequences), a gap of 10.9 that is latency alone (5.32 at 192
  samples with the prefix identical; trace seeds 11 / 13 read 8.57 / 9.02). Two sibling
  constructions from the other lanes price the shape below the rest: `s_faderail` (a trickle-pinned
  rest at 13%, the down walk wall-sits the floor for fourteen samples and fails at budget, 46.12 ±
  0.06 against 55.02) and `s_sidebet` (a starvation probe finds a mid band at 28% and plants a claim
  without a park, so the follow rule has nothing to follow and the calibration audit still opens
  down, crashing at the mid band's cliff with a wait of 16; 45.31 ± 0.15 against 53.30). Planted at
  the peak the product holds the cap (61.35), so the whole loss is the approach. The opening side
  from an interior rest point is the latency-face residual and `absolve`'s
  entry names as a knife edge; this is its stationary witness at the opening rung, with no prelude
  and the prize present from the first sample, filed under class 4 with that residual (the
  signature reads class 2, walk-paced). The symmetric cells, a prize below an interior rest where
  down-first is right (`crestpast`'s drift, `cp_w097`), are the other side of the same coin. The
  guard rail also loses here rather than helping: `noveto` reads 60.62 against 59.28 on all eight
  seeds, and the retest is what bounds the loss, since `noretest` takes seed 2 to 51.63
  (`/climber-minimize`, §5).
- **jumpslide** (spec `audit-regret/specs/jumpslide.json`, 130
  samples): a hot set of 0.5·max keys replaced wholesale every 13 samples at 52% of traffic, a band
  5,600 requests apart at 30%, a scan at 18%. Static, dense-swept: 35.5 flat from 1% to 40%, 50.3 at
  45%, 53.4 at 55%, 54.8 at 65%; LRU 49.5. Between jumps the hot set is main-resident and the miss
  fraction ~0.4, so the band is caught from ~43%, a residency knife edge (catching it lengthens
  residency, losing it shortens it) that collapses in one sample. Seeded 1–8 the machine reads
  38.70 ± 2.79, `noaudit` 44.23 ± 4.07 and `reactive` 41.21 ± 3.64, every arm 5–11 under LRU. The
  route: the calibration park's 32-sample expiry lands two samples after a jump, the recovery
  stands it down and the s42 audit goes down to the floor, so the starvation ladder takes over; a
  first-round probe confirms at 43.2% on the stride where the band is first caught, since 4× the
  bar is first earned exactly there, at the catch region's near edge, and a seven-stride confirm is
  under `PROBE_COMMITMENT_DEEP`, so no park rule applies and the edge position is handed to density
  with a claim planted and nothing held. Density's rest point is below the edge (0.7–1.0% a sample
  down); each jump lands the fresh set in the window and kicks it up 7–8%, and the drift returns it
  to the edge as the next jump lands: seeds 4 and 8 hold that sawtooth at hr 60–62 between jumps,
  while a landing under ~48% lets the drift cross the edge first, the band collapses, density sends
  the window to the floor, and the blind corner re-climbs seven strides of `wh` 0 or a jump crashes
  the climb. The rail never fires, since a one-sample collapse inflates the deviation its margin is
  priced from, `shallowmoat`'s sentence, with or without jumps. Period 17 reads 5.72 (the calibration
  audit goes up instead and parks audit-grade), 26 8.2, 6 / 11 18–20, 40 15.0; trace seed 11 16.7;
  260 samples 14.8; a static uniform hot set in place of the jumps 52.27 against 61.63 (the slide
  released there by a stand-down on the sample after a capped retreat's landing, which the retreat
  cover does not span); planted at 60% the product holds. What the record did not have: a
  first-round starvation confirm's verdict position is the near edge of what it found, by
  construction, and nothing parks it; class 9's second clause feeding class 1 (the retention
  sentence, the parent) and the class-5 sawtooth, with class 4 on the re-climbs. A sentinel row.
- **lowbar** (spec `audit-regret/specs/lowbar.json`, 96
  samples): a uniform bulk over 2·max at 42%, a band 6,000 requests apart at 34% that only a ~68%
  window catches, a scan at 24%, and a gap-1 trickle at 0.4% that keeps the window sighted so no
  probe arms and every seed takes the audit path. Static: 13.0 at 1% declining to 7.8 at 65%, 32.5
  from 70% to the cap; LRU 30.6; a 9% rate. The audit's crash bar is `AUDIT_BAR_FRACTION` of the
  rate frozen at the arm where that is under the 5pp cap, 1.37pp here, and the calibration walk's
  cumulative drawdown along the bulk's decline crosses it at the tenth stride, one short of the
  catch point; the tolerant retry's three sub-bar samples run out at 44%, the rung-32 walk's at
  63% with its next stride on the plateau. Seeded 1–8: 13.40 ± 7.48, seven seeds pinned at
  11.3–12.1 under the 13.0 start (three crashed walks' duty) and one at 26.3 whose tenth stride
  cleared the threshold by 0.07pp where seed 1's missed it by 0.20 (trace seed 11: 26.30 / 11.61);
  `noaudit` 12.72 and `reactive` 12.49 are the terrain's floor at every window short of the catch
  point, the same loss as the 17pp under LRU. With the crash fraction at 0.25, or the absolute 5pp
  bar, three of four seeds arrive at the first walk (26.3) and the fourth takes the reversal exit.
  The moat family's mechanism (the abort at the −bar contour one stride short of the far bank, the
  persistence a stride short on the retry) at a fraction-bar dose: the bar set by the rate rather
  than the terrain, and a sub-bar region wider than the persistence. `s_nearbasin` is the same
  exit at a 16% rate behind a calibration park (about 7 of its 23; the crossing seeds clear the
  level test by 0.02–0.03pp, and `nocal` recovers only +0.35 because its own first walk crashes on
  the same bar). The first measured cell on which the fraction's level is not a tie (the record
  holds it inert at 0.10 and 0.25 on the sub-third holdout and on `arc_S3`'s plateau); its cost
  side is unmeasured where it binds (the `lowmix` rows, `arc_S3` and the thin-signal floors,
  `cp_w050`, a fresh sub-third holdout), and the carve-out the cell resembles is closed (the
  split), its deviation floor dead. Narrow by construction: 5,500 apart catches at the
  tenth stride, 6,600 passes the cap; at 16384 the band fits at 40% and the cell reads 4.8. Class
  4 (reach), the walk's exit pricing; a sentinel row beside the moat rows.
- **moatweather** (`/audit-regret` round 9; spec `audit-regret/specs/moatweather.json` with its
  dose-matched and peak-level controls, 96 samples, trace seed 11): the calibration audit's value
  reverses sign under a level change that lands on its first strides. A zipf over 1.5·max at α 0.95
  (50%) fills main, a band 6,400 requests apart (30%) is the prize, an 8% scan keeps probation thin,
  and the attack is a fourth member, a pure one-shot scan at 12% of traffic square-modulated at
  period 3 samples with amplitude 1, so its share steps between 0 and 24%. Static over 22 windows:
  30.4 at 1% declining to 28.8 at 52%, a flat region spanning 1.6pp across 51 points of window, then
  a 23pp step to a plateau, 51.80 at 64% and 51.19 at the cap; LRU 50.2. Reaching the plateau from
  the floor takes ten consecutive strides. Against the unmodulated control the curve is the same
  within 0.9pp over the whole operating region, the same argmax and the same plateau within 0.5pp;
  only the 55–58% transition differs, three times the machine's window away and in the direction
  that would help a walk. Seeded 1–8 the machine reads 31.20 ± 0.31 against the control's
  47.30 ± 0.54, 16.1pp on 8 of 8 seeds. Undosed the calibration audit is the whole answer: it walks
  ten strides across the flat region while the rate moves 0.294 → 0.302, catches the band on the
  eleventh and parks at 65%, where `noaudit` must first escalate the starvation ladder to rung 64
  and does not arrive before s47. Dosed it crashes on its first stride (a 5.57pp step against
  `min(RESTART_THRESHOLD, AUDIT_BAR_FRACTION · base)` = 4.87pp), its retry's bold driver reverses on
  a step and its raw-sample streak confirms at 675 entries on terrain flat within 1.6pp, and that
  confirm's bridge write resets `starvation.rung` 32 → 1, so the ladder never reaches the rung-64 ×4
  walk that crosses. `nocheapen` recovers +1.3 of the loss, so the bridge write is the smaller half.
  The audit is worth −10.2 / −8.0 / −1.6 on trace seeds 11 / 7 / 13 and +7.5 undosed, with seed 7
  bimodal (6 of 8 trapped) and the other two unimodal; the dose is a bar crossing, the audit's value
  passing zero in 0.5–0.6 and stepping to −10 between amplitude 0.85 and 0.95 on both instances.
  What the mechanism needs is the coincidence, not the noise: the same square wave at period 24
  costs −9.4 when its first flip lands on the audit's first stride and +0.1 when a quarter-period
  shift moves the flips six samples away, and period 3 is the period that makes the coincidence
  unavoidable at every phase. Two controls separate it from the terrain: the scan held constant at
  the attack's high level of 24% reads 42.97 ± 0.16 with the audit earning +6.8, and moving the
  weather to samples 0–19 alone (21% of the trace) reproduces 79% of the loss while the same insult
  moves `nocal` by 0.1. The second axis is the traverse length: with the band 3,200 requests apart a
  first-round walk catches it and the cell is solved (gap 1.4), 4,800 costs 14.4 and 8,000 19.4.
  Doubling the horizon widens the loss (33.27 against `nocal`'s 45.07) and 16384 keeps half. Class 9
  (the abort and the verdict both decided by an exogenous step against a frozen reference) paid as
  class 4, with class 6 as the secondary channel; owned by the recovery layer, responder
  `Walk.crashBar`'s audit arm and `Walk.isConfirmed`'s streak. `b_moatnoise`, the blind lane's
  independent construction (a sine modulation of a resident zipf on a gradient terrain), reproduces
  the escalation block but not the reversal, since the calibration audit earns +1.7 there. Nearest
  recorded: the F1 dose instruments, whose rows sit at LRU parity on shallow bases and whose largest
  audit-layer cost was `flashpark`'s −3.8, and `shallowmoat`/`lowbar`, whose terrain this is; what
  is new is a cell the machine solves undosed and loses entirely dosed, and a coincidence axis the
  record does not price. No arm recovers it: nothing tested exceeds 41.8 against the 51.8 ceiling
  under the modulation, so deferring or removing the audit is not the repair.


- **ramp_retest** (spec `audit-regret/specs/ramp_retest.json`, 72 samples): a zipf core over
  0.58·max at α 1.08 and a two-reference band at 0.31·max. The core's weight is 0.4 while the
  band's weight rises linearly from 0.0304 toward 0.7296, so an initially valid high-rate anchor
  becomes obsolete. At 8192, seed 1's rail returns the window from 4,123 to 1,055 at s24–25
  against a frozen claim of 0.8422. The two arrival samples score 0.4032 and 0.3910; at the
  completed retest, smoothed rate 0.5939 exceeds `claim − 3·deviation` = 0.5038 because the
  arrival raised the live deviation to 0.1128. The retest accepts the claim, the window holds
  at 12.9% for s28–38, and the s39 audit recovers the band at s44. The pre-return deviation
  was 0.0239, which would give a 0.7705 cutoff at that same measurement. This is the return's
  own change widening the tolerance that judges it, at the completed retest rather than the
  walk's crash bar.
  Seeded 1–8: hybrid 61.48 against the best tested static point 68.54 at 26%, LRU 65.43,
  reactive 39.28 and noaudit 45.69. Removing the rail earns +5.54pp, while forcing completed
  retests to reject earns +2.25pp (2.18–2.30 on all eight seeds). The latter keeps the return
  and its observations unchanged through the completed verdict, then releases the hold and
  re-seeds the goal metric. It prices that intervention, not the whole gap or a viable repair.
  The same intervention earns +3.87pp at 16384 and +4.75 on trace seed 13; trace seed 11 takes
  a clean route and is unchanged. A 32-sample stationary prelude preserves the interaction
  (+6.57pp), while repeating the ramp twice reduces its price to +0.25pp. This is a finite
  recovery delay, not a permanent pin. The measured neighborhood is conditional: at 8192 the
  amplitude ladder changes from a correct rejection at 0.86 to a passed retest and eleven held
  samples at 0.88; at 16384 that passed-retest route already exists at 0.84. The rail-path cost remains at distances
  0.25–0.37; a 56-sample ramp triggers no veto, and the 88-sample ramp at 16384
  rejects correctly. A gap-only bisection at much smaller amplitudes instead finds the known
  audit-approach cost, with the retest working, and is not this boundary.
  Class 6 (an obsolete claim surviving its retest), with class 3 (arrival-induced masking) and
  class 4 (the held position waits for the audit); owner `Anchor.retestFails` with `Rates`' live
  margin. Nearest recorded: `ghostclaim`'s obsolete return and `sidecliff`'s rail cost, where the
  retest limits the loss. Here the existing retest is reached and passes because of the
  arrival's tolerance increase. The two size cells are finite-horizon sentinels. No production
  rule was proposed or priced against the corpus; unconditional rejection would also reject
  the valid returns the rail exists to defend. The best-sample rule adds a basin selector on this
  terrain: in an independent construction (round 10's blind lane), two basins identical through s33
  split when the recovery audit's first sample read 0.24pp above its second, so
  `Walk.rememberBest` parked on the catch region's near edge instead of one stride inside, and the
  ramp eroded that edge under the fresh-park shield (parked 56.7 against 73.6).
- **deadpark** (spec `audit-regret/specs/deadpark.json`, 120 samples, trace seed 11): the moat
  terrain the calibration audit solves (a zipf core fills main, a two-reference band is caught
  only past a ~55% window; static 30.6 flat to 50% and 55.0 at 60–65%, LRU 53.1), with two
  samples of pure scan at s36–37, so s37 is dead. The audit parks at 72% at s21 against a density
  law that wants a smaller window. On the dead sample `route` tests `hasBlindCorner` before
  `anchor.held`, and the audit confirm's bridge write zeroed `refractoryLeft`, so a starvation
  down-probe arms from the park inside its fresh shield (the shield guards `isWorkloadShift`
  only). The next live sample confirms the stride by average density; a first-round starvation
  confirm is not audit-grade, so `keepConfirmedPosition` re-plants the claim at 66% with the
  smoothed rate the dead samples dragged down and releases the hold, and density walks the window
  to the floor. Seeded 1–8: 38.43 ± 0.22 against the no-burst control's 48.90 ± 2.59. An arm
  that holds the park on a blind corner (`parkblind`) reads 50.80 ± 0.34, unpriced on the gate
  battery and on the phase-alternation rows where §5's arming guards lost (`shiftdead`). One
  percent of live traffic in the burst defeats the trigger, and after the shield the recorded
  stand-down applies instead. Class 6, class 4 in the recovery. Real corpus: one blind-corner arm
  from a held park in 10,538 density samples (ConCat), which failed. Constructed-only; a sentinel
  row.
- **needle** (specs `audit-regret/specs/needle.json` and `needle_wide.json`, 288 samples): a narrow
  static peak with a loss on both sides. A loop (a cyclic scan) over 0.55·max at 28% of requests
  (period ~16k requests, longer than the window's residency at every window up to the cap, since a
  54-58% one-shot scan holds residency near W/0.7 requests, so only main can hold it, by frequency,
  and only while main has room: window ≤ 45%), a two-reference band 0.57·max apart caught from ~41%,
  and the one-shot scan. Static, dense-swept at 1%: flat within 0.4pp from 1% to 40%, a peak at
  44-45%, then ~-0.5pp per 1% of window to 17-21 at the cap; LRU 8-11. The product held at a fixed
  window matches the reference within 0.25pp over 21 (cell, window) pairs, 0.12 around the peak. An
  audit confirms on four consecutive samples above its frozen reference plus 1pp ending on or after
  the walk's fifth sample (`AUDIT_CONFIRM_STREAK` with `AUDIT_COMMITMENT`) and `nextStride` never
  commands zero, so the machine keeps a peak only where the curve stays above the reference across
  four consecutive stride positions reached late enough in the walk. The calibration audit parks at
  32.0% on every cell, seed and size, so the park's up-audit samples 38.2 / 44.3 / 50.6 / 56.7 /
  62.7%, the first stride lands just under the catch point, and the verdict is one reading: whether
  the 62.7% sample, three strides past the peak, clears the bar. The band's share sets that margin
  (-5.70 -4.54 -3.67 -2.08 -1.25 -0.99 -0.54 at shares 0.04-0.10, +1.08 at 0.12), a gradient that is
  a threshold in the outcome. **Narrow** (shares ≤ 0.09): at 0.08 the rung-16 audit reads 33.89 /
  32.83 / 30.13 against a 28.58 bar, misses on the fifth, and walks on up the far slope to a crash;
  the rung-32 one reads 34.11, overshoots, reverses, and re-crosses the peak at 27.28 because its
  far-side excursion evicted main's loop, then fails through its base (audit rung 64, wait 128, then
  256); at 576 samples the rung-64 one strides 25% across it (57.0% above, 80.2% a crash). 27.15
  against 33.73 at 44%, `reactive` 26.98 and `noaudit` 26.88 pinned as well (the flat approach has
  no gradient for either law, so the arms read the flat's rate from different windows), `closed` ≤
  0.05 while the gap grows 3.65 → 7.71 with the prize; pinned on 12 of 12 admission seeds (one
  decision path: the seeds move sample rates by up to 2.6pp and flip no decision), at 7 of 7
  alignments reached by planting the start so the calibration parks at 33-38% or not at all, on
  three trace seeds, at 16384 and at 576 samples (27.31 against 34.51). A run of four is collected
  and voided where the walk enters the region on its first stride (the band 0.52·max apart:
  38.2-56.6% above, 62.5% not), and the same four positions confirm when reached later in a walk, so
  the region's width alone does not decide it. A share of 0.10 is a 3-in-12 coin (a third pass
  clears the bar by 0.13pp) and is not a row. **Wide** (0.11-0.12): the audit confirms on its fifth
  sample on the far slope and the best-sample rule returns it to the peak, where it parks at the
  ceiling's rate; the park's next up-audit is the tolerant retry and rides three below-bar strides
  up the far slope, the restored park reads 24.5 against a parked 38.0, and the recovery's second
  sample (a +8.6pp first difference) lands one sample after `RETREAT_COVER`, so `isWorkloadShift`
  reads the machine's own recovery as a new workload and the stand-down discards a correct claim on
  its anchor; density then slides off (its rest point is the floor, main's hits being real and worth
  nothing at the margin below the knee) and the starvation ladder cycles near-edge confirms 1 → 64
  without a park. 28.56 against 36.37 at 45%, `noaudit` 26.15, and `pricedshift` **34.45**: the two
  arms are identical in every dumped field until the one sample where the shipped machine stands
  down, on every seed checked, so +5.9 is that decision. The same responder fires on the narrow face
  at a share of 0.04 and costs nothing, the park it discards being on the flat. At 96 samples the
  wide face reads solved: the discard lands at ~s116. At the restored park the deficit is -13 then
  -4.5 after the up-audit's crash and -1.2 after a down-audit's (4 of 4 each, parked sd ~0.2, ~10:1
  on every seed of both wide cells): class 7's first synthetic reading above scatter, and small by
  itself. Robust to the band's distance (0.52-0.62), the loop's size (0.50 flips the narrow cell
  wide by moving the knee out, the fifth sample clearing by +0.44), two regenerated traces and 16384
  (14 of 14 neighborhood cells at 4.9-8.2). Class 4 in the verdict's geometry (narrow), and class 7
  feeding class 6 (wide), both owned by the recovery layer; class 1 is the co-requisite of each. The
  narrow face needs narrowness only, the run breaking where the far slope has merely returned to the
  flat's level; the far side's depth is what class 7 and the wide face's route need. Nearest
  recorded: `crestpast`, whose far side declines slowly enough for the fifth sample to clear, and
  every far-band family, whose plateau runs to the cap where a walk wall-sits for its streak. Round
  8's `pinnacle` (record only; its workspace is gone) overshoots by one stride on arrival, which the
  narrow face does not. Constructed; the narrow face's prize is bounded near 8pp by the generator (a
  steeper far side lets the window hold the loop itself), and no real instance is claimed. The
  ten-point anchor grid has no point between 40% and 50% and under-reads this ceiling by 1.2-3.6pp;
  sweep it densely.


### Terrain classes, and what reachability means

The families above are *shapes that break a climber*. These are properties of the **terrain**
itself, which is a different axis: they classify the hit-rate-versus-window curve rather than a
trajectory over it, and they are what decides whether a constructed trap is worth chasing.

**A non-convex window response is a real class, and on it the rest point is the deficit, not the
travel.** Sweeping static windows, some cells have a trough deeper than 0.5pp on the way from a 1%
window to their optimum, and several are real traces rather than synthetics. The average law's zero
crossing then sits in the wrong basin: on two deep-trough cells it rests at a 1.4% and a 6.9% window
against 80% and 90% optima, and the rest error is 75% and more than 100% of the whole deficit — one
of them runs *above* its own static value at that rest point. The unimodal control is what makes
this trustworthy: there the average law rests 1.9pp of window from the peak for zero hit-rate loss.
So "the climber holds a small window because it has not travelled" is wrong on this class; it holds
a small window because that is where its steering law rests. This is invisible from the canonical
published curves, none of which is non-convex, and it is the terrain half of the marginal-steering
thread (§5).

Classifying a large loss corpus by whether a *fixed* window could have won it: **two** are
structural (no fixed window wins, a real W-TinyLFU limit), **none** are ceiling-bound, and the
rest are **steering** — a reachable fixed window ties or beats the competitor and the climber misses
it. Over 80% of the conceded margin is recoverable inside the reachable window range, so raising the
structural ceiling wins nothing. Terrain is concentrated: most of the steering cells are block I/O,
the family with flat (2–6pp) and sometimes non-convex responses.

**Where the real-trace loss goes, by mechanism.** `/audit-regret`'s teacher splits the climber's
costly samples (those losing at least 1pp to that sample's best static-window copy) by the mode
the machine was in. Over seventeen real density-tier cells at one seed, each weighted equally, the
split is steering 46%, audits 22%, parks and holds 15% and probes 13%, and the families differ.
On cloud-physics (nine cells) steering is 66%: the average law resting at the wrong window, its
error pointing away from each sample's best window on most costly samples, with parks holding a
wrong window on the `w097` family (15–40% of its loss). On the ARC cells, whose optimum is the 1%
floor, the exploration machinery leaves it: audits 33% and parks or holds 27% on `S3`, audits
37–45% on `P1` and `P8`, and probes 65% on `DS1` at 1M and 2M. The two problems want different
repairs, the rest point on block I/O and the exploration price on floor-optimal storage traces.
`DS1` at 4M is excluded, since its sampled copies score below the climber there.

**Do not tune the audit schedule for this class.** The equilibrium audit exists for a sighted false
equilibrium, which is exactly this shape, and its cycle costs more samples than these short cells
have — which invites the conclusion that the clock is too slow. The ablation refutes it: against
`noaudit` the mean delta is under 0.06pp, the significant cells split evenly between cost and help,
one cell of twenty-two moves more than a point, and the mean flipped sign as cells accumulated,
which is what noise looks like. The layer is neither the cause of this class nor its cure, and
shortening its schedule would buy a measured zero while disturbing a constant calibrated on the gate
battery.

**Low audit exposure only bites where the terrain has something to find.** This is the rule that
decides reachability, and it is why several deterministic constructed regressions are parked rather
than chased. The stillness-starved traps (`posjam_d0`, `bandtrap2`, `phases_d050`) arm zero or
almost zero audits, and the reachability scans come back clean on every real cell. The nearest real
approach is always the same cell, `corda`, which calibrates once and never re-audits — the known
blind hold — and it costs nothing there, because its terrain is flat and it already scores within a
third of a point of its ceiling. A cell that never re-audits on flat terrain has lost nothing.

**High per-sample scatter and wide terrain are anti-correlated in practice**, which is the
structural reason the modulation traps are hard to reach. Every real cell with scatter at trap
strength has under 3pp of terrain; every cell with wide terrain has scatter well below it. The
constructed traps hold both at once, and no real cell in the corpus does. The one real cell carrying
trap-strength scatter on wide terrain has an optimum that *moves* — which makes its scatter
window-informative, precisely the case the goal-metric layer exists to answer.

**LLM prefix-cache traces hold both, and the machine climbs on them slowly** (the Qwen Bailian
traces, read by `bailian`; `/climber-gate`'s prefix-cache row). A sample is a few hundred requests
that each re-read a session's whole prefix, so on Thinking@16384 the per-sample rate swings between
14% and 60%, while the static 1%-to-80% span is 4.4pp there, 6.9pp at 65536 and 18pp on
To-C@185364. The response rises toward the 80% ceiling on every such cell, a window planted at 80%
holds there (Thinking@16384 36.99 against LRU 36.26, To-C@185364 43.97 against the static 43.96),
and the MiniSim leader reaches the 80% value within one pass, so the loss is steering into that
basin, not missing information. The window earns nothing until it spans one inter-turn reuse
interval (about a fifth of the cache on Thinking@16384), so it starts starved, and three things
keep it there. Growing the window demotes protected's coldest entries and hands the window
probation's coldest, so the hot set stays in main and keeps hitting there: a walk to 80% scores
what the planted run does at the same point in the trace, but its density error reads shrink until
the hot set migrates (on Thinking@16384, a median of −3.9 at windows of 40% and up on a seed whose
walks stayed 23 samples there and +0.3 on one that stayed 140, against the planted window's +0.6).
On the static anchor the average law is bistable there, negative from 1% to 40% and positive from
50%, where the window takes over main's hits, so the floor and the ceiling both attract and a climb
confirmed below about 45% is reversed. The marginal form crosses up near 9% and rests near 71%
instead; on Coder@65536 the average law rests at the floor or near 40% (1.1pp under the peak) and
the marginal form at the ceiling, and on the To-B control, whose average law rightly rests at the
floor, the marginal form rests near 13% for 0.2pp. And probes and audits are judged across phase
swings several times the window's effect, so most fail and their backoffs escalate.

Two properties matter when interpreting these comparisons (§1). A prefix lookup stops at a
request's first missing block, so only the leading run of hits is usable, while the simulator
counts every block hit. LRU's usable and block hits were equal in the studied replay; that is
not a universal LRU guarantee. A small window streams a long request through TinyLFU's admission
one block at a time, and a rejected shallow block can strand the hits behind it. The shipped
climber's usable hits run 0.8–2.8pp below its block hits in these measurements (To-C@185364 23.4
against 26.2; 0.5–1.7 at an 80% window). The source study's
[workload-aware policy](https://arxiv.org/html/2506.02634v5#S4.SS2) ranks blocks using request
category and turn, idle time and prefix depth, preferring deep blocks for eviction on a priority
tie. These fields can be recovered from the released request records, but the flattened generic
access event and cache maintenance stream do not carry them. That richer input is distinct from
the key-only steering loss above.

**A bounded information study did not qualify a generic trigger.** Across sixteen cells and eight
sampling seeds, two 64-key banks predicted reuse over the next C block events, where C is the
declared cache capacity, using a fixed learner and an authentic demand-fed aged-frequency sketch. On
the seven primary cells' held-out key Brier, bounded reuse history improved 1.63% relative to age
plus frequency (AF), but lost 0.68% to an equally deep AF-child control with no new feature.
Type/turn improved 0.40% over AF child, below the 1% bar, and failed request-weighted and control
checks. Frequency alone had lower primary key Brier than any tested extension. The observer and its
core learner occupied 31,872 native bytes per replica, excluding the shared sketch and offline
evaluator; no production CPU or JVM-size claim was tested. All six history/metadata extensions
failed the preregistered gate. This is a limit of that finite learner, budget and horizon, not proof
that context or inexpensive inference is useless. Prediction or ranking gains do not establish a
harmful-mode trigger, a window direction, benefit from invoking MiniSim, or a qualified policy
change.

**On these traces the large cells' loss is mostly sample budget, and the HBM-size cells' is not.**
A pass holds 8.5 samples on To-C@185364, 12 on Thinking@65536 and 49–59 on Thinking@16384 and
Coder@65536, so one pass of a large cell is warm-up: To-C@185364 reads 26.2 against LRU's 45.8.
Replaying the trace lengthens the run, but replaying its keys also lets the frequency sketch
remember every key from the previous pass. LRU reads the same on every pass, yet that memory lifts a
static 1% window by 4.8pp on To-C@185364 and keeps the climber small: under plain replay To-C@185364
closes only to about 6.5 below LRU from the fifth pass, and Thinking@32768 replayed four times reads
32.95–36.97 across eight seeds, an apparent escape lottery. With fresh keys each pass (each pass's
keys mapped onto new ones) the climber reaches LRU or the static 80% window within two to four
passes on every cell (passes 2–8: To-C@185364 41.3, Thinking@32768 38.8, Thinking@65536 41.6,
Coder@262144 53.4, against LRU's 45.8 / 39.6 / 42.3 / 55.4), as the 20-day AgentX corpus, scheduled
at a steady 16 sessions, converges to within 1.8pp of LRU at every size from 8192. The HBM-size
cells climb slowly even with samples to spare: Thinking@16384 reaches LRU's 36.3 only on its fourth
fresh pass, about 150 samples in. Under plain replay the reactive law looked the better of the two
given time (+1.3 on Thinking@32768 ×4, +4.4 on To-C@185364 ×8); on single passes it is −1.7 to +1.2
against the shipped machine, and −7.2 on Coder at 699051.

**The tier boundary.** At the 4096 switch the discontinuity is the policy, not the terrain: on a
constructed straddle one entry of capacity moves the answer by several points while LRU steps zero
and both single-law arms are continuous. On real traces the sign inverts, so the cost is borne
*below* the boundary and the reactive tier is the loser there. Half the cliff is not in the steering
law at all — the tier gate switches the sample period along with it. The placement is set by the
worst case rather than the mean, and it currently has a single defending cell; if that trace were
ever judged unrepresentative the interval below the boundary reopens at once.

What the boundary also gates is the *layer*, and that is worth more than the difference between the
two laws. A terrain whose response is **flat then a cliff** carries no gradient at all in the region
a climber searches from its start, so no local law can cross it and only the equilibrium audit can —
and below the threshold there is no audit. Constructed at a maximum of 4096, the hybrid, reactive
and `noaudit` arms are one code path and read ~23 with an 11pp spread across seeds, while the
density machine forced at that same size reads 45.8 ± 0.02 against a static ceiling of 48.2. The
tier gate, not the workload, is worth 23pp there. That cell is constructed and its realism is
unestablished: it bounds what the placement can cost, and is not a case for moving it.

**Path-dependent rest points.** At least one real cell settles somewhere different depending on
where it starts, and not because it is sample-starved. `cp_w050@123038` holds 54 samples; from the
shipped 1% start it settles at a 20–21% window, and from a planted 30% start it settles at 14–15%
for **+4.65pp** on the same trace, landing 1.6pp off its ceiling where the shipped start lands 6.3pp
off. The trajectories name the mechanism: from 1% the *first* command leaves the optimum, because at
a 1% window the window is trivially the denser region, so the law commands a large upward step and
the run then oscillates for the whole trace, with both of its audits aborting during low-rate
portions of the trace. From the 30% plant a down-audit instead runs four clean strides,
confirms at a 2% window and parks. The descent machinery is fine here; the ascent out of the shipped
start is not, and the audit layer that should catch it crashes on the trace's own phase structure.
One cell of ten, reproducible in both directions, not traced to a defect. The September 2026 replay
does not support a short-exposure explanation: the first audit's launch and crash both contain
492,152 observations, while the hit rate falls from 48.8% to 4.0%. Granting that first crash the
existing bounded retry saves 1.50pp over the next 64·max requests and 0.99pp over 128·max on
admission seeds 1–8, but the walk still aborts two samples later and returns to the same base.
Suppressing the retry's two extra output strides retains 0.92pp at 128·max. The gain does not
require those strides, but later observations and decisions also change; this does not isolate
a timing-only effect or demonstrate a false rejection.
The same delay loses on the `cp_w081` and planted `shallowmoat` controls. A pre-existing rate
decline at arm does not distinguish the useful continuation from costly ones after invalidation;
no production rule was established. These are finite-horizon, request-indexed replay comparisons,
not a generalization to unseen workloads or a change to the crash policy.

**Descent has to be measured deliberately, because no natural row exercises it.** Every gate row and
every real-corpus cell starts the cache where the product starts it, at a 1% window, and on the
frequency-favorable traces that is already the static optimum — so those rows pass without the
climber having to move, and a machine that could not *descend* would read clean on the whole real
corpus. Planting the initial window is the instrument. Measured, the descent is the density law
unobstructed at **4–6% of the maximum per sample**: the log-ratio error saturates near 1.5–2 nats
even at a badly wrong window, so the step never approaches its 30% cap, and a full walk down from
80% costs 13–16 samples. Recovery is therefore a function of the **sample budget and nothing else** —
replaying a trace four times takes an 80% plant from −16.05 to −3.63. Against the reactive law the
density tier is the better descender (its period is 4× the maximum against reactive's 10×, and its
step is proportional to how wrong the position is rather than a fixed decaying 6.25%), though
reactive holds the higher level at the shipped start on these cells. Two observations recorded
alongside: a failed probe undoes a correct descent in one capped move, which is deliberate and which
the cells absorb; and what a plant prices is the walk back after a regime change, which is tens of
millions of requests for a large cache.

## 4. The shipped design (the probe machine, >4096)

Non-starved samples are the pure density step. The additions:

1. **Starvation bar**: a region is starved when its sample hits < `max(4, requestCount >> 10)`
   (0.1%, floored at 4 — the floor binds whenever the sketch's entry-denominated cap makes the
   sample shorter than 4096 requests).
2. **Blind corner gate**: probe only when the starved window is small (window starved at ≤max/4 →
   probe up) or the sample is dead (both starved; direction away from the nearer bound, deciding
   at half). A starved main beside a large window arms nothing: the equilibrium audit owns that
   terrain (the upper-corner probe, main starved with window ≥3·max/4 → down, was deleted
   §5's corner entry; the `cornerprobe` harness arm restores it). A *large* region
   earning nothing is visible to density and must not trigger a probe (probing "for" a
   scan-filled main destroyed corda_large).
   The gate outranks the goal-metric branches, which is right for the guard rail (it adjudicates a
   shortfall *on* the starved sample) and was wrong for the audit (it adjudicates over the samples
   that follow): a blind corner that never clears served its whole refractory motionless while the
   clock said the position was due, since `refractoryLeft` is armed by a starvation walk's
   `undoProbe` and decremented only inside the hold (an audit's undo left it alone from
   before that every undo re-armed it, which deferred the corner's next probe by the
   whole rung after an audit that was not the probe's doing). A **due** clock now pre-empts that hold, a sample the machine
   was otherwise spending on nothing.
3. **The walk**: bold-driver seeded at the 6.25% restart magnitude in the probe direction,
   **scaled by the refractory rung — ×2 at rung 32, ×4 at rung 64, capped at the 30% max step**
   (a bound the current tables sit under — ×4 · 6.25% = 25% — so it binds only under retune)
   (2026-07 study: deep rungs previously bought *permission* to walk deeper but not *speed*, so
   escapes crept; rung-scaled strides punch through deep stray walls the flat seed stalled in —
   straywall2 +5..+11 across seeds and scales, d050 escapes fuller, corpus 205/205 ties since
   natural workloads never reach the deep *starvation* rungs — the audit ladder shares this ×4
   stride scaling and is routinely deep on real traces (`audit.rung = 64` on 5 of 12
   real density cells, `auditWait = 128` on 4), so an argument from rung unreachability must say
   which ladder it means; the full return command on failure is unchanged, which is what
   separates this from the rejected v7 travel-budget family);
   direction flips only on |ΔHR| ≥ the **reversal** bar, which is not the crash abort's (see the
   crash-abort ending): priced for starvation probes, and for audits
   `min(5pp, AUDIT_BAR_FRACTION · max(baseHitRate, noiseBand))` — so plateau crossings survive
   workload jitter.
   Walks honor the 2% floor (via an else-if after the reversal check, so an undo-to-base wins over
   the floor clamp), and carry a **sample budget of 16** in `Walk.samples`, which counts up from
   the arm (it once borrowed the refractory countdown; the two uses never overlap, but
   sharing one field made the walk's depth an inverted expression at each use and forced both
   oracles to bound it by mode) so no walk is unbounded — the review proved the crash veto alone cannot
   bound walks (below a 5% base rate the absolute form was unsatisfiable, which `AUDIT_BAR_FRACTION` now
   covers, and it stays blind to damage under its own bar). Two
   subtleties the code carries: the walk's anti-freeze re-seed (`|step| < 2` re-seeds toward the
   probe direction) cannot be reached by decay in the density tier — the seed is ≥6.25% of a >4096
   maximum and sixteen 0.98-decays cannot fall below 2 — but the floor clamp assigns
   `step = min(-0.0, floor − window)` directly (the negative zero keeps a wall-blocked restart
   striding into the wall rather than flipping upward off positive zero's sign), so a
   floor-blocked down-walk zeroes its step, re-seeds into the wall each sample, and is
   re-clamped, burning budget in place (the stall note under State); and
   only samples routed to the blind-corner refractory hold decrement its countdown.
   A due audit takes the sample without decrementing it. The hold returns without a steering
   step (only the below-floor lift, item 7 — one definition,
   `Reading.atLeastFloor`, shared with the steering step so the two cannot drift apart). It used to fall
   through to the density arm, which is what the dead-phase rider exploited — a handful of
   window hits in an otherwise blank sample authorized the maximum step while the probe
   machinery was backing off.
4. **Walk endings** (each maps to a verified failure without it):

   A failed walk's return commands sum to its distance from the base. Applied movement can finish
   short when a new sample replaces an unfulfilled transfer; later walks use that landing as their
   base. This approximation is retained after [pricing reconciliation](#reconciling-failed-probe-returns).

   - **Crash-abort** (hit rate ≤ probe-start − the walk-interior bar): undo to the probe's start.
     Starvation probes re-arm their refractory; audits leave it unchanged. The first crash keeps
     the owning ladder's rung unchanged because an exogenous phase shift can resemble probe
     damage, and increasing backoff would delay re-exploration. Consecutive crashes escalate that
     ladder at `PROBE_CRASH_ESCALATION`. A **starvation probe's bar is priced** against the workload's own
     scatter — `min(max(5pp, 3·rateDeviationEma), 15pp)`, the adv3 study: the fixed 5pp sat
     below real per-sample noise and severed the blind corner's only exit (the dosed mixture
     trap crash-cycled one stride off the floor for half its run, −3.4 below LRU, healed to
     +4.8 over by the pricing), while the 15pp cap keeps the abort's second job — bounding a
     damaging walk's excursion — after uncapped pricing let walks roam the all-blind families
     at a measured cost (metronome −0.6, recovered by the cap). An **audit's crash abort keeps the
     absolute 5pp**: a priced audit bar lets audits survive to confirm and park more, which is
     the R4-F1 amplification dial (zigzag −10.5, widepin below bar, demoflood −5.2 under
     `devaudit`). Its **depth** stays absolute, but
     the threshold is a level test against the rate frozen at the arm, so where the whole rate is
     smaller than 5pp the test is unsatisfiable and only the budget bounds a walk doing real
     damage (`arc_S3`: 0.0442 → 0.0216 while the window marched to the structural ceiling).
     `AUDIT_BAR_FRACTION` of the starting rate floors it there. That is not a depth pricing —
     every dead candidate *widened* the bar so walks survive longer — and it binds only below a
     third of the rate the threshold prices, leaving every family in the graveyard untouched.
     **The reversal does not share this bar**: the two exits read different
     statistics, so the crash abort is priced by the level alone and the reversal by
     `AUDIT_BAR_FRACTION · max(baseHitRate, noiseBand)`. `AUDIT_BAR_FRACTION` therefore carries two
     jobs, capping one exit and pricing the other, and the two share a fraction without sharing a
     bar.
   - **Reversal-through-base**: a bold-driver reversal that would cross back through the probe's
     own starting window found nothing — finish as a failed experiment (undo + ladder ×2) instead
     of walking out the other side (a ±5pp blip once converted an up-probe into an unterminated
     down-walk that pinned the window at a single entry).
   - **Adjudication**, when the watched region earns ≥ 4× the bar AND the walk has taken at least
     its **committed depth** (see 5). An up-probe confirms iff
     `ln((windowDensity+ε)/(walk.baseProbationDensity+ε)) > 0` — the probed window must out-earn
     main's MARGINAL rate, the probation density **frozen when the probe armed** — while
     down-probes keep the average-density test (`error·dir > 0`; the window has no marginal
     substructure to price against). Why marginal: climbing donates only the protected
     allocation, whose squeeze demotes into probation and displaces probation's coldest entries,
     so probation is the boundary a window grow actually taxes; adjudicating against main's
     AVERAGE (the rejected simpler form — four simpler verdict shapes were measured, each trading
     a family, see §5) vetoes genuinely-winning positions because the protected core dominates it
     (ablation: trickle 56.1→69.5/73.2/73.1 across seeds, bandtrap2 69.05→72.55, widepin
     45.5→57.7, sample-aligned phases 46.9→60.1). Why frozen: the walk's own demotions enrich live
     probation (a protected-saturating hot core demoted mid-walk earns furiously there), and live
     adjudication is an ABSORBING false-veto (demoflood: 58.94 pinned with zero confirms vs 67.17
     frozen) — the freeze's own cold-start-transient baseline is bounded and self-heals because
     every re-arm re-snapshots. Confirmation keeps the position and resets the ladder to 1
     (control passes to the density arm in the SAME sample); anything else fails (undo + ladder
     ×2) — neutral verdicts must fail or density walks the window home and the probe refires (S3).
     A zero baseline (probation earning nothing at arm) means any ≥4×bar earnings confirm —
     intended, the opportunity cost of a dead boundary is ~zero (nullchurn stays harmless). The
     named trade, priced and journaled: on low-HR bistable shapes whose probation concentrates the
     reuse band while an LRU-ward escape needs a diluted start (lowmix family, 2–7% HR), the
     frozen baseline false-vetoes an escape the old diluted-average verdict luckily confirmed —
     v5a lands the bad basin 3/3 seeds vs ship 1/3, with ZERO real-trace echo across the 32-cell
     defended set. The walk's exit bars are settled: a starvation
     probe's exits are priced against the live scatter, the audit's crash abort keeps its
     absolute depth capped at `AUDIT_BAR_FRACTION` of the frozen rate, and the audit's
     reversal — a first-difference test, not a level test — takes the same fraction of the
     larger of that rate and the noise band; every widening of the crash abort is dead (§5).
     Four simpler verdict forms were measured and each traded a distinct family — see the
     local study archives.
   - **Budget expiry**: failed experiment (undo + ladder ×2).
5. **Escalating commitment** — the resolution of the stray-exit dilemma. Stray and transferred
   hits scale ~linearly with the region's size while their *density* does not, so an absolute exit
   ends deep walks a stride short of a dense reuse band (making that trap absorbing), while
   removing the exit re-creates deep-walk displacement on thin-signal floors (w50/S-class, real
   traces). Neither rule wins both — four rule families were measured (§5). The shipped answer:
   first-round probes exit cheaply (commit 0), and each adjudicated failure lengthens the ladder,
   whose deeper rungs commit the next walk past the stray zone before the adjudication exit may
   fire (length 32 → commit 2 samples, 64 → commit 10). Confirmation is NOT exempt from the gate:
   early confirms on false mid-depth bands reset the ladder and neuter the escalation (measured:
   the long-trace escape fell 53.3 → 38.7 with the exemption). Short traces never reach the deep
   rungs, so thin-signal floors keep the cheap behavior exactly; steady deep-band traps escape by
   ~round 3 (≈100 samples) — an absorbing pin becomes a bounded, temporary dip (demonstrated:
   mixture_d050 scores 33.5 on a dip-length trace and 53.3 on a 3× trace as round 3 escapes).
   A confirm the density arm reverses in the same sample also deepens the ladder:
   the confirmed position is walked home at once and the corner re-arms, so a reset there restarts
   the ladder on every cycle of a dither that never reaches the band it is looking for. That was
   the majority ending of a starvation confirm (668 of 881 across the battery and corpus dumps),
   and pricing it as the completed experiment it is closes the reach half of `shallowmoat` and lifts
   `bandtrap2` +4.1, `trickle_s7` +2.8 and `trickle_s11` +0.9 (seeded, 8 of 8 on each),
   `strad_p8@4097` +1.9, `widepin` +1.7 (7 of 8) and `phases_d050@32k` +4.8 (4 up, 4 unchanged);
   `phases_d050` +0.3 with two seeds falling to its low basin, the N=8 mean above its bar. The
   confirm's handoff and its zero refractory are unchanged, so nothing is undone and
   the escalation is fast: rung 64 in ~10 samples on a steady dither. Its price is on the
   thin-signal floors with a dither in a low phase, where a deep walk armed in a trough has no
   crash abort (its base is the trough, §5's v7): `arc_DS1@1051635` −0.7 on a 10-sample trace and
   `deadphase` −0.2, accepted (§5 has the guard that removes both and
   what it costs), and on `norank_rep_r6` one seed of eight (41.1 → 20.3): a reversed confirm on
   a rewarded ladder deepens it, so the fail that follows waits 16 samples where the reward's
   ladder waited 2, and the ×1 walk that reached that seed's escape re-arms 25 samples later.
   A reversed confirm at the deepest commitment that the goal metric confirms (the audit's own
   streak and beat-base test, `Walk.isAuditGrade`) is an audit in all but name and is kept as an
   audit's confirm is: it parks. Any other starvation confirm still hands to density,
   so the cheap re-probing that phase alternation relies on is untouched. The park's own audits
   are covered by it: a crash-scale move while an audit walks out of a park is what that re-test
   produces and its ending returns to the park, so it does not stand the park down
   (`isWorkloadShift`); a shift with no walk in flight still does. Measured on shallowmoat, where
   a deep walk finds a band density then dismantles (36.4 on the wedge seeds against 30.7), and
   on `flood_j100` (+1.1 at N=8); bit-identical on the rest of the seeded battery and corpus.
   A kept confirm at or short of the farthest window the ladder's walks have already confirmed is
   a repeat: the machine found that ground and lost it, so a walk that only finds it
   again has not earned the reward, and the confirm keeps its handoff and its zero refractory but
   deepens the ladder as a reversed one does. The ladder remembers the farthest confirmed window
   per direction (`Ladder.remember`/`isRepeat`, kept and reversed confirms alike) and forgets it
   when a walk fails or crashes, since a walk that keeps nothing shows the terrain it was found on
   has moved. On `absolve_p8` the reward had pinned the rung at 1 on every lure period (a
   first-round stride catches the lure's +5pp shelf, density dismantles it when the lure stops, the
   corner re-arms); with repeats priced the ladder reaches 64 in six periods and the ×4 walk
   crosses the valley in two strides, parking as an audit-grade confirm at the top: 27.95 → 46.29
   at 8 seeds. Bit-identical to ship on every other cell of the seeded battery (64 cells at seeds
   1–2, the movers at 3–8) and the corpus (`arc_ConCat` −0.03 over four seeds, noise): the census
   of the shipped machine (15 cells × 8 seeds) found no kept confirms at all on `bandtrap2`,
   `trickle`, `deadphase`, `strad_p8` and `shallowmoat` (all wedges), and the kept confirms on
   `rep_r6`, `widepin` and `phases_d050` are followed by crashes, which forget. The memory is not
   cleared on the anchor's discard: the confirm sample is itself the lure's on-step, and where the
   walk lands at the previous anchor that step discards the anchor on the very sample the confirm
   is judged (measured 41.4 against 46.4 on `absolve_p8` seed 1).
6. **Refractory ladder**: length starts 16 and doubles per adjudicated failure to 64 (the arming
   doubles first, so effective waits are 32 → 64; after a kept confirm the next failure waits 2;
   a confirm the density arm reverses doubles the rung without a wait).
7. **Below-floor lift**: the density arm's clamp raises a sub-floor window to the 2% floor (the
   initial window is 1% and otherwise wedges under the documented floor). The blind-corner
   refractory hold honors the lift too — a workload whose every sample is blind never reaches
   the steering step, and the bare hold wedged the initial 1% window for the life of the run
   (adversary F4, metronome; enforcing the floor there costs the one thin-signal
   cell that was enjoying the wedge its documented price, w50 −0.5pp, siblings unmoved).
8. `setMaximumSize` resets the probe state, the pending `adjustment` (a probe-scale residue
   must not crush a fresh partition), and `sample.previousHitRate` (a baseline earned under the old
   geometry inverts the seeded direction of the first sample under the new one, at
   restart magnitude), and the sample period is
   `min(saturated 4·maximum, sketch.sampleSize)` — the sketch's sample is entry-denominated, so
   weighted maxima cannot inflate the period by the mean entry weight (a weight-units period froze
   weighted adaptation entirely), and a near-Long.MAX maximum cannot overflow it. The sketch's
   sample can sit far below the maximum whenever the sketch was sized for something smaller: an
   `initialCapacity` hint sizes it at construction, and growing the maximum leaves the old sample
   in place until the lazy half-full `ensureCapacity`. The period then runs well below 4·maximum
   while the steps already scale to the new maximum.

   That used to be accepted as a transient that heals when the cache fills. The period does heal;
   the window it drove to does not. `climb()` therefore skips `determineAdjustment` until
   `weightedSize() >= maximum() >>> 1`, the same half of the maximum that sizes the sketch,
   instead of adapting the moment the sketch exists. A cold cache is not evicting, so its sampled
   hit rate does not describe the region split; before the gate, the compressed cadence read that
   signal often enough to walk the window across the partition. The cold branch discards a *full*
   sample rather than resetting every cycle: resetting every cycle is not available, since several
   pins record into the sample on caches that never fill, one of them
   (`asyncBulkCompletion_doesNotRecordAccess`) on ten in-flight futures that weigh nothing until
   they complete, so no maximum both opens the gate and holds the entries. Leaving the sample
   untouched is not available either, because it then grows without bound and breaks
   `LocalCacheSubject`'s mirror of the period gate (`HashClashTest` reached 233,364 requests
   against a 2,572 period). `WindowClimber.discardSample` closes it on the same period
   `determineAdjustment` uses, so the invariant holds unchanged and the cold prefix a first real
   sample can inherit is bounded by one period. Pinned by
   `BoundedLocalCacheTest.adapt_partiallyFilled_holdsWindow` and its `adapt_filled_climbsWindow`
   pair.

   Priced 2026-09-08 against `product.Caffeine`, hit rate %, `maximumSize` with an
   `initialCapacity` of 1000 and of 10000 beside the un-hinted arm: `arc/S3@400k` 29.27 / 40.59
   against 41.04, `arc/DS1@1M` 8.76 / 8.97 against 12.48, `arc/P3@100k` 33.50 / 39.63 against
   40.43; `metaCDN@250k`, `wiki1190@250k`, `cs@8192` and `corda@8192` unmoved. The loss tracks the
   compression ratio `0.4 × maximum / hint` rather than the hint, so 16× is free, 40× costs 3.4pp
   and 160× costs 11.8pp, and no cell at `maximum` 8192 can show it because `MIN_SKETCH_SIZE`
   floors the sample at 2560. With the gate every hint arm lands within 0.4pp of the un-hinted
   one. A patched `samplePeriod` returning `4 × maximum` unclamped recovers the same loss and is
   inert un-hinted, which is what separates the cadence from the sketch's own density; the two
   share `sampleSize` in production, so no public API separates them. The `setMaximum` growth arm
   measured the same −3.4pp before the gate and is closed by it.

Shape of the code: `determineAdjustment` gates on the tier's sample period, dispatches to the
bound tier, and closes the sample; the density tier's `route` is a **router** — handed one
`Reading` (the sample's derived view: both densities, the rate and its change, the starvation
bar, the geometry every branch clamps against), it advances the always-on observers, then
dispatches down the priority lattice to one named branch method per outcome. Each branch owns the rationale that used
to sit inline as a comment block, and the conditions are questions (`Reading.hasBlindCorner`,
`AuditClock.isDue`, `isBackingOff`, `Walk.shouldCrashAbort`, `Walk.isConfirmed`,
`Walk.canAdjudicate`)
rather than expressions needing a comment to read. An ending is priced by the layer that owns it:
`probeEnding` asks the crash question of every walk and then hands to `auditEnding` or
`starvationEnding`. Reading order for the whole machine is the router first, then the branch it
sends you to; sectional comments (`Exceptional Scenarios`, `Probe Walk`) carry that order inside
the density tier, and the nested types read in dependency order: the `Climber` base class →
`Sample` → `Reading` → `Step` → the two tiers → `Walk`/`Ladder` → `AuditClock` →
`Anchor`/`Rates` → `ProbeEnding`. Behavior lives with the state it reads (Beck's tests, applied mechanically in the
2026-08 refactor sweeps); repeated quantities have one definition each — `Ladder.stride` (the
rung-scaled stride the walk and the audit's room rule both read), `Reading.maxStep`,
`Reading.upperCorner` (the wall line that the blind corner, the probe direction, and the
audit's direction rule all test), `Step.restartMagnitude`, `Reading.cappedStride`, and
`sample.hitRateChange` (the cross-sample difference the reactive law, the walk's bold driver,
and the stand-down's shift trigger all steer by) — because two spellings of one quantity is how
the room-rule drift happened.

State: fields on the package-private `WindowClimber` (reached through the `climber` field that
`AddMaximum` generates), eviction-locked, cold path only. The climber itself holds the
observation state (`Sample`), the carry-over `adjustment`, and the bound `tier`. The tier owns
its `Step`; everything density-only lives on the `DensityClimber` that tier names. The flat fields
are grouped into small objects — `Sample`, `Step`, `ReactiveClimber`, `DensityClimber`, `Walk`,
two `Ladder`s, `AuditClock`, `Anchor`, `Rates` — each carrying the behavior that reads only its
own state. The
grouping is the write-owner map made structural: each row below is one object, so a cross-layer
write has to name the other owner to happen at all (landed object-by-object, each stage verified
bit-close against the pre-refactor tree by the paired battery). The tuning constants live inside
the class that reads them; the one exception is `RESTART_THRESHOLD`, kept on `WindowClimber`
deliberately because four mechanisms in four classes read it (the reactive restart, the walk's
audit-side bars, the stand-down's shift trigger, the audit direction's stale test) and giving it to
any one would misstate who owns it. Where a constant has one owner but a second reader, the second names it
(`Rates.VETO_MARGIN_MIN` floors the audit confirm; the fresh-park shield is
`AuditClock.AUDIT_WAIT_INITIAL` long), which keeps the sharing visible.

**The tiers are types, and the density tier carries its own supervisor.** Both extend the
`Climber` base class (`climb` over the open sample, and `samplePeriod`), and a (re)size constructs
the one its maximum falls in: `DensityClimber.appliesTo` is the tier boundary, written once in
`resized`, and construction is the reset, so a reactive-tier cache never allocates the density
machinery. `ReactiveClimber` holds the regime predicate, step rule, decay and period;
`DensityClimber` holds the proportional step and its period, and with them the probe machine, the
audit layer and the anchor, the exception machinery §2.1's supervisory switching arms when its
law cannot be trusted. Each strategy chooses its initial direction; `Climber` constructs its `Step`
from that direction and the maximum, so replacing the tier also replaces and re-seeds the step.
The two laws keep different verbs on purpose (the reactive one *climbs*, the density one *steers*),
and the two bold drivers (`ReactiveClimber.climb` and
`nextStride`) stay separate: same algorithm, but their bars, reversal conditions, and return
conventions differ, and §4/§5 record each difference as load-bearing.

**Declined refactors, decided rather than deferred:** no `WalkMachine` and no method object for
the router — the branches write `refractoryLeft`, `undoRemaining`, `walk`, the anchor, the audit
schedule and the *other* layer's ladder (the audit-confirmation exception), so either extraction
needs a back-reference and would move the cross-layer writes out of the class that owns them.
`refractoryLeft` cannot join `Ladder` (the shared type would give the audit's instance a
permanently dead field), and `undoRemaining` deliberately *outlives* the walk, which is why it is
a density-tier field rather than `Walk` state.

The **write-owner map** — which layer's endings may move which fields — is the boundary the
H4-C1/F2 repairs restored; keep new writes inside their owner:

| Owner | Fields | Notes |
|---|---|---|
| Observation | `sample` (a `Sample`: `hits`, `misses`, `windowHits`, `probationHits`, `previousHitRate`) | the counters are zeroed at sample close; `previousHitRate` deliberately keeps a different lifetime, since it is the memory ACROSS samples that the reactive climber's direction and the walk's bold driver compare against — `close(hitRate)` carries it forward while zeroing the rest, and only `reset` (a resize) discards it. `WindowClimber` keeps thin `recordHit`/`recordMiss`/`resetSample` delegates because `BoundedLocalCache` calls them on the read and write paths |
| The active walk | `walk` (a `Walk`, null while none is in flight: `ladder`, `isAudit`, `down`, `baseWindow`, `baseHitRate`, `baseSmoothedRate`, `baseProbationDensity`, `samples`, `belowBarStreak`, `aboveStreak`, `beatBase`), `undoRemaining` | one walk at a time. It is an object rather than eleven flat fields, so "dead state while not probing" is the absent object rather than a comment, and a reader must hold a walk to ask it anything — `armProbe` is the complete constructor, `endWalk` clears the field, and the router keeps the ended walk in a local because the undo that prices it still reads the bases. The bases are `final`: frozen-at-arm is the property the verdict studies keep re-deriving (§4's "why frozen"), so the compiler now holds it. `walk.ladder` is the arming layer's ledger, which makes "an ending may only deepen the machine that produced it" a reference rather than a lookup; `refractoryLeft` is the starvation refractory alone and belongs to the row below |
| Starvation retry | `starvation` (a `Ladder`: `rung`, `crashStreak`), `refractoryLeft` | moved by starvation endings only (an audit confirm cheapens `starvation.rung` and zeroes `refractoryLeft`, the one journaled bridge write, spelled out at its site rather than hidden behind `Ladder`'s methods; an audit's undo leaves the refractory alone) |
| Audit retry + schedule | `audit` (a `Ladder`), `auditClock` (an `AuditClock`: `down`, `waitSamples`, `stillSamples`, `lastWindow`) | moved by audit endings and the position-stillness clock only. The clock owns `tick`/`isDue`/`restart`/`reset`, so the stillness rule (a moving sample **decays** the run, it does not zero it) lives with the counter it governs rather than in a climber method. `reset` preserves the preferred direction, but a resize constructs a new clock with `down = true`; direction alternation does not survive the resize. `AuditClock.reschedule` is handed the audit ladder's rung by the ending that prices it, so the clock never reads the other layer's ledger itself |
| Goal guard | `anchor` (an `Anchor`: `window`, `rate`, `held`, `freshLeft`, `returning`, `returnLeft`, `retestClaim`, `settleLeft`, `shortfallStreak`), `rates` (a `Rates`: `smoothed`, `deviation`) | anchor/park/veto authority and the rate references. `Anchor` is the memory *and* its defense in one object because the layer's three invariants run between those parts, and it now holds them by construction rather than by assertion: a shield lives and dies with its park (`park`/`hold`/`release` are the only writers — an audit's confirm arms a shield, a rail veto holds without arming or spending one, since the shield's clock belongs to the confirm that armed it), a park defends only a planted anchor (`discard` takes the hold with it), and a return implies the park that follows it (`beginReturn` arms both). `isAt`/`isAwayFrom` give the band test one definition instead of three inline copies, and they are deliberately not each other's negation — an unplanted anchor is neither at nor away, and there is no claim to veto against. `Rates` owns the EMA pair and the two bars priced off it — `noiseBand` is the three-deviation width, `vetoMargin` is that floored at `VETO_MARGIN_MIN` — so the rail's margin and the starvation probe's walk-interior bar read one definition instead of recomputing `VETO_MARGIN_SCALE * deviation` apiece. The deviation is read LIVE, and the audit's confirming streak is deliberately not priced off it; both notes live on `noiseBand` itself. A stand-down that discards the claim re-seeds the pair (below): the event that invalidates a claim invalidates the reference the claim would be re-planted from, and the two are one layer's state |
| Motion out | `tier` (the bound `Climber`, owning a `Step`: `size`), `adjustment` | the single per-sample command. `step.size` and `adjustment` have different owners and lifetimes: `adjustment` belongs to `WindowClimber` and is drained by `BoundedLocalCache` across maintenance cycles as the transfer carry-over, so it changes at the cycle rate while the strategy's step changes at the sample rate. A resize constructs a new strategy with a freshly seeded `Step` |

An audit confirmation is the exception to retry-state ownership: it sets `starvation.rung` to
one, clears `starvation.crashStreak`, and clears `refractoryLeft` to allow cheap re-probing.
A reset ladder carrying a live crash streak would re-escalate on the next crash. An audit undo
preserves the starvation retry state, including its current refractory countdown. Re-arming
that countdown would defer the next starvation probe for an audit's failure, as pinned by
`WindowClimberTest.undoProbe_auditRetreat_leavesTheStarvationRefractoryAlone`.
Starvation endings leave `auditClock`, `audit.rung`, and `audit.crashStreak` untouched;
audit endings never deepen `starvation.rung`.

Each layer's rung and crash streak live in its own `Ladder`. Apart from the audit-confirmation
exception, endings change only their own layer's retry state and schedule. This includes
*non-crash* endings: an audit's budget expiry leaves
`starvation.crashStreak`, and a reversal-through-base leaves whichever streak it does not own
(pinned by `audit_budgetExpiry_leavesTheStarvationLedger` and
`walkStep_reversalThroughBase_leavesTheOtherLayersLedger`). Both sites previously cleared
both streaks, which disarmed the *other* layer's two crash responses — the escalation at
`PROBE_CRASH_ESCALATION` and, for audits, the `AUDIT_CRASH_PERSISTENCE` tolerance that arms
at a streak of one. A starvation probe ending between two audit crashes therefore restored
the one-sample abort that the moat and the H4-C1 pulse train specifically need it not to
have: the crash-semantics fix was reachable around by an interleaved blind corner.
`setMaximumSize` additionally zeroes the sample counters and re-seeds the step direction; it
drops the walk, which is how the direction and bases that used to linger as dead state are now
discarded with it. A deep walk that reaches `mainProtectedMaximum == 0` stalls (transfers have
nothing to take) while its budget keeps burning — measured 7 of 16 budget samples at the ceiling
in one blind-review round; benign but known. The floor-side mirror exists and rung-scaled strides
sharpen it: a rung-64 down-walk reaches the 2% floor in ~4 strides and spends the remaining ~12
samples standing still before budget expiry prices it as a failed experiment (2026-07 post-ship
review). Adjudicated as intended: the wall IS the probed extreme awaiting adjudication (main
earning ≥4× the bar there confirms), the budget must keep burning or a wall-sit is unbounded,
wall samples counting toward the commitment depth is what lets a wall position adjudicate at all,
and expiry pricing stays uniform because ladder escalation on walled families is load-bearing for
the deep-band escapes. That adjudication covers walks whose wall-ward direction is *forced* (a
starvation probe's direction comes from the starved region); an **audit chooses its direction**,
and `auditDirection` refuses one with less than a full stride of room — a window in
(2·floor, floor+stride] used to be sent into the floor with sub-stride room by the first
interior toggle, clamping on the entry stride and burning 15/16 budget samples motionless
before the information-free expiry doubled both the refractory ladder and the audit clock
(adversary round, F2: mixture d025@32768 spent 54% of the trace at its pin; the
room rule is worth +8.1/+9.7/+11.7pp on the mixture-audit family with every other gate row
unchanged). What was a real bug — a restart-scale improving sample at the wall
striding upward off positive zero's sign — is fixed by the negative-zero clamp and
regression-tested in `WindowClimberTest` (the wall-sit scenario replays the review's repro).

**Why the tiers stay:** the `hybrid-all` rejection at cs@563 (−1.27) rules out density at *every*
size, and the D2 study closes the weaker question of where the boundary belongs:
`corda`'s own tier crossover sits between 2048 and 4096 (density − reactive reads −0.61 / −0.86 /
−0.50 / **+0.43** at 512 / 1024 / 2048 / 4096), so the threshold is within one octave of the
sensitive trace's crossover even though the aggregate crossover is between 563 and 1024. The
placement is set by the worst case, not the mean (§3, the tier boundary). The weighted threshold
also remains in the weigher's units; the tested [entry-count alternatives](#entry-count-tier-selection-for-weighted-caches)
did not establish a reason to change it.

**The goal-metric layer (the F4 answer; shipped, then hardened).**
The probe machine's trigger taxonomy is starvation-only, and a workload can hold the density arm
at a **sighted false equilibrium** — unstarved, earning enough to be believed, and wrong (F4:
whisper's trickle keeps the window sighted; the mixture family re-pins quietly at scale). The
layer that closes it: an **equilibrium audit** arms the walk machinery from a position held
still for `AUDIT_WAIT_INITIAL` samples — the clock counts positional stillness only, decays by
one on a moving sample rather than resetting, and its schedule is owned by audit endings alone;
the FIRST audit after a (re)size is a calibration probe at `AUDIT_WAIT_FIRST` = 4 — and is
judged by the **goal metric**, `AUDIT_CONFIRM_STREAK` consecutive raw samples above the
reference frozen at arm plus the one-shot `auditBeatBase` gate, because density holds the
equilibrium under test and would veto every walk away from it. A confirm plants the **anchor**
and **parks** without a parting density step, shielded from crash-scale weather for one initial
audit wait; the **guard rail** vetoes a noise-cleared sustained shortfall back to the anchor.
The margins are deliberately split (rail 3·dev; confirm run-length plus beat-base). Each layer
owns its ladder, crash streak and schedule, with the audit-confirmation exception described above.
Audit crashes price persistence in **time** (`AUDIT_CRASH_PERSISTENCE` on the retry) rather than bar
depth. A park's first audit follows the walk that confirmed it: the confirm ends a
walk on evidence of improvement rather than its exhaustion, so the ground beyond is unexplored
while the ground behind was just covered, and the alternation was sending that audit back through
it (`absolve` p16: down from the 32% park into the lure's knee, a crash, the undo's arrival
discarding the anchor; `shallowmoat` basin B and `veilmoat`: down from the calibration park toward
the floor it came from). It follows only while the claim stands, the park still held at the arm
and the smoothed rate within a restart threshold of its value at the confirm
(`AuditClock.settle`/`chooseDirection`); a rate that moved that much with the window still says
the workload moved and the walk's direction says nothing about the terrain. Unguarded, the rule
swept `climbtrend_up`'s trend misconfirms wall to wall (−0.9 at N=8, its sentinel: the 6570 park
starves main of the growing hot set) and re-phased `whisper_mod_p6`'s re-escape (−1.76, its
sentinel), and on `moat_h5000` its far-slope walk (five strides down a 1.5pp-per-stride decline
before the 5pp bar trips) made the next audit the tolerant retry, whose undo evicted the band
(−1.75); the guard returns all three to ship. It also fires, incidentally, after a step confirm
whose smoothed rate at the confirm still reflected the walk's path (`moat_h3000/h4000`,
`flood_j100` on seven seeds of eight, `shieldtrap_s13` seed 1), where the continued direction had
been a coin flip (moat_h3000 +1.6, h4000 +0.8, h5000 −1.75, flood +4.1); those stay on the
alternation. Priced at N=8, arms rotated: `absolve` 24.77 → 41.46, `absolve_x4` 36.45 → 46.89,
`veilmoat` +3.7, `shallowmoat` basin B +2.7, `flood_j100` +0.81 (one seed +6.5), `saw_p40` +0.77
(8 of 8), `whisper_mod_a12` +0.24, `shieldtrap` +0.2, `scarburst` +0.19; `mixture_d010_long`
−0.27 (8 of 8), `demoflood` −0.16 (8 of 8; the calibration audit's confirm overshoots the walk's
own best sample and the continued audit wastes two samples before the crash returns it),
`whisper_mod_p12` −0.05; the corpus `cp_w050` +1.07 and `cp_w044` −0.17 on one seed each, the
rest bit-identical. Alternation resumes after that audit. Invariants and don't-harmonize warnings
live in `rules/design-decisions.md`.

**What the guard rail is for, and what it is not.** It fires rarely, and for two distinct reasons
that were once recorded as one. On blind-corner cells it is **not reached** at all: the refractory
hold outranks its branch, and the starved samples the hold claims are exactly the stalest ones.
Where it is reached, which test binds depends on the population — on constructed cells the
**margin** does, with the shortfall present and large while the streak never reaches one; on real
cells the **streak** does, the shortfall holding for many consecutive rail evaluations while one
clearing evaluation resets it (walk, undo, return, retest and blind-corner samples neither count
nor reset it). The corollary worth keeping: between the veto's `rate − 3·dev` and the confirm's
`rate + VETO_MARGIN_MIN` lies a band in which **neither goal-metric layer can act**, and the
machine spends real time there.

Deleting the rail is rejected by the shallow moat doses, where it is the only mechanism that
recovers the window and several audits arm over the run without rescuing it. Everywhere else
removal is free, and on the real cells where it fires it is worth about −0.01pp. So it is kept
because it is free and is the sole exit on a rowed adversarial family — which makes the case
conditional: **if the moat rows were ever retired, the case for the rail goes with them**, and §5's
`agedown` kill, which rests on disarming the rail, would need re-grounding. Two facts not to
re-derive: the streak is not consecutive **in time** (walk, undo, return and blind samples neither
increment nor reset it), and `beginReturn`'s hold is cleared only incidentally, when the return's
own rate move trips `isWorkloadShift` — which suppresses density steering, not exploration, since a
due clock outranks the hold.

**`AUDIT_CRASH_PERSISTENCE`'s tolerance is one-shot by construction.** Any non-crash ending,
including the tolerant retry's own budget expiry, retires the crash streak. A workload that runs
crash → fail → crash → fail for its whole trace therefore ratchets the ladder while learning
nothing; the streak legitimately measures *consecutive* crashes, and the one-shot form is
deliberate rather than an oversight.

## 5. The graveyard (do not re-explore without new insight)

2026-05 sweeps (~40 variants on real BLC, see `large_cache_climber_findings` memory): PID and all
descendants (integrator + no setpoint = corda catastrophe), feedback/ghost signals (break on loop),
Borkar, Smith predictor, RLS parabola, shadow/probe/MAB state-replacement (noise-driven switching),
EWMA smoothing alone, dead-zone smoothing (`ap_dz`, 28 cells, 6 big wins / 3 big losses — "each
fixes some, breaks others"), Gini/Indicator classifiers (skew does not predict the optimal
window), POSINIT. That campaign is the reason the density tier exists: it was a two-session
attempt to improve the **reactive** law at >512 and its verdict was "ship nothing". Its metric was
relative %, which the project later disowned, so the individual rows are weak evidence — the
dead-zone row in particular was re-tested properly in 2026-08 (below).

2026-07 density-era escapes (Opus session): kickoff-to-center (false-fires on w50 −8), EWMA-regret
(transient — re-traps after the EWMA catches up), annealing HR-walk (inconsistent), wide start
(fixes stress, destroys frequency traces −15), lowering/raising the floor alone (symptom patch).

2026-07 probe-machine ablations (this design's own dead ends, all measured — the tradeoff surface
is real, not tunable away):
- **v6 small-entry probe + absolute exits**: stray hits scale ~linearly with window size while
  their *density* does not, so an absolute hit-count exit fires just short of the reuse band and
  fail-adjudicates the probe (mixture_d025's escape died from a one-step stride change).
- **v7 density-competitive exits + travel budget** (¼ max when the other region is healthy,
  uncapped when dead): principled, kept every escape — but deep walks displace healthy-main
  workloads (w50 doubled to −2.1) because a probe that starts in a low workload-phase never
  triggers the crash-abort.
- **v8 give-up-jumps-ladder-to-max**: no help; the *first* deep round is the cost.
- Verdict logic weaker than confirm-or-fail, and any threshold-only starvation test: see §3/§4.
- **v9 slam-continue** (walks continue past unconfirmed 4×bar hits under the budget): escaped
  every deep synthetic (d050 +25, widepin +15, ratchet +3) but re-imported v7's deep-walk damage
  on real thin-signal traces (w50 −2.2, S3 −1.5 vs the locked design) — the ablation that proved
  the two-class tradeoff is rule-independent and motivated escalating commitment.
- **Early-confirmation exemption from the commitment gate**: false mid-depth confirms reset the
  ladder and neuter the escalation (d050_long 53.3 → 38.7 on the original binary; the exemption's
  harm did not reproduce at N=3 on the shipped binary — era-scoped, the gate stays strict on the
  measured tradeoff surface). The gate is strict.
- **Live-probation adjudication (the marginal verdict's unfrozen variant)**: measured as an ABSORBING
  false-veto on the demoflood construct — a protected-saturating hot core demoted mid-walk earns
  ~11.8/slot in probation, so every walk is vetoed and the machine pins at the floor with zero
  confirms (58.94 vs 67.17 frozen vs 74.10 incumbent-on-that-shape). The pre-walk freeze is
  earned, not decorative; on every non-saturating family live ≡ frozen (mixture d010/d025/
  d050_long identical).
- **Average-density up-probe verdict** (the rejected simpler adjudication): vetoed real
  reuse bands whenever the squeezed main stayed denser on average (F3 trickle −14..17, B1
  bandtrap2 ×3 discard, widepin/aligned-phases pins) — replaced by the probation-marginal frozen
  baseline; its one superiority (lucky diluted-average confirms on lowmix-class bistable low-HR
  shapes) is the named, journaled trade.

2026-07 fresh-eyes study kills (all pre-registered, data in the
local hill-climber-study workspace ledger): per-sample goal-metric triumph
confirm (spoofed
by phase-rise traps; ratchet/troughwalk2); verdict-gated triumph (widepin +9.7 but two defended
traps regress and the pin persists); probe-time sample compression (halved-sample verdict noise
re-pins mixture_d040 −24); sketch-as-ghost repeat-miss lean (null — never engaged its cells);
confirmed-window warm start (null — widepin never first-confirms); boundary marginal-cost damping
(moved the give-backs +1.5 for the first time, but the admission-boundary comparison is degenerate
by construction — victims are always sketch-hotter — and the equilibrium-gated variant is inert);
and four honest-window-hits verdict forms (absolute / vs-main-average / vs-own-baseline /
vs-baseline-with-bar-floor), each trading a distinct family — the verdict-design tradeoff surface
in that study's report. The one survivor shipped: the rung-scaled walk stride (§4, the walk).

### What each step is worth (the ablation prices)

Every step against the whole battery, 91 cells at seeds 1 and 2, beside the firing counts from a
ship run of each cell. Nothing is dead: `corner` reads 0 because that step was deleted, and every
other site fires. What each step buys where it acts, against what it spends elsewhere: the
retest 33:1, a repeat confirm's escalation 22:1, a park's first audit following its walk 20:1,
the frozen probation baseline 17:1, the audit layer 13.5:1, the return and retreat cover 9.3:1, a
reversed confirm's escalation 7.6:1, the walk's commitment depth 5.3:1, a blind corner's probe
4.9:1, the fresh-park shield 4.1:1, the refractory ladder 3.9:1, the rung-scaled stride 3.7:1.

Both candidates are closed. Restoring the deleted upper-corner probe (`cornerprobe`)
costs 9.61 across 18 rows against 4.32 across 5, and 27:1 against on that cell set
itself, so the deletion holds on the evidence that flagged it. The walk's commitment depth read
0.23:1 at N=8 then and 5.3:1 now.

**The guard rail's veto is the one price that inverted, and the battery cannot decide it.** On the
that cell set at the same seed it fell from 11:1 to 1.86:1; over the whole battery it reads
0.28:1, and at N=8 across the eight cells either rail arm moves it reads 0.33:1 with 10.95 of its
12.87pp cost on `sidecliff` alone (−1.34 to −1.42 on all eight seeds, while `cp_w015` splits by
basin, +0.26 on five seeds against −0.26 to −0.37 on three). Every battery cell starts where the
product starts it, and a rail that returns the window to an anchor has little to defend from a 1%
start. Planted, it is worth **+6.3 to +6.6pp on four of four seeds** on `mainsat` at a 55% window
(32.42–32.68 against `noveto`'s 25.90–26.65) and +0.15 to +0.52 at 70%. The step stays and the
`mainsat` plants are what holds it; without a planted cell the battery reads it deletable. On
`sidecliff` the rail is a net cost on every seed and the retest is what bounds it, since
`noretest` there takes seed 2 from 59.25 to 51.63.

**The retreat cover's widening is real and small.** The commit that moved the retreat's
cover out of `isParkTest`, so it now runs without a held park. `nowidecover` (the cover scoped
back to a held park, the return half kept) is bit-identical on 66 of 68 rows and costs 0.07 and
0.08 on `cp_w081`'s two seeds. So `noreturncover`'s 44.12pp belongs to the held-park retreat cover
and the return half rather than to the widening, which is why the moat and `hazefloor` rows move
under that arm while the commit landing the return half read them bit-identical.

### A detected regime change must not restart the audit schedule

The latency face of the top-corner residual: a discarding stand-down (`isWorkloadShift` and `standDown` with the
window on the anchor) restarts the audit schedule, so a detected regime change reschedules
exploration instead of leaving the rung and the wait the ended regime earned. Seven forms of the
one candidate, each read from trajectories on `latebloom`'s alignment ladder at seed 1 and priced
on the rows that wanted the backoff seeded 1–8 (`metronome`, `h4c1_attack`, `h4c1_reverse`,
`whisper_mod_p6`, `whisper_mod_p12`, `whisper_mod_a12`); the reset forms on the moat rows seeded
1–8; the two live forms on the unseeded battery with every mover re-adjudicated at seeds 1–8
with the arms rotated inside each seed, the floors and the corpus:
- **rearm** (the wait drops to `AUDIT_WAIT_FIRST`; stillness and ladder untouched) and
  **rearmstill** (the stillness run restarts as well): inert at the 64 alignment, since the first
  re-armed audit goes down from the interior rest (the alternation state) and its failure at rung
  64 re-doubles the wait to 128; `rearm` reads `whisper_mod_p6` −5.35 and `phases_d050` −1.9
  unseeded.
- **rearmreset** (`auditClock.reset` and `audit.reset`, the cold schedule a resize gives) and
  **rearmboth** (the retest's discard as well): the round-3 `hardreset` kill re-entered through
  the discard. `whisper_mod_p6`'s period-6 swing is ±6–7pp, every swing at the floor anchor
  discards it, the discard zeroes the stillness run, and with discards every three samples the
  clock never reaches its four-sample wait: no audit arms in the whole run, the window sits at
  the floor throughout, 55.58 against 66.01 on every seed (the F4 pin). `whisper_mod_a12` −2.9 at
  seeds 1–8, `mixmod_a010` −12.2, `widepin` −5.7 and `phases_d050@32k` −5.3 unseeded, `moat_h7800`
  −1.1 at seeds 1–8. The pin `auditClock_rateSwings_doNotResetStillness` fails on this form.
- **rearmcold** (every discard; the wait drops and the ladder resets, stillness kept): a
  redistribution across the battery rather than a refinement. `slowswap_step` 40.41 → 31.23 (−7 to
  −13 on seven seeds), `absolve_p8` 45.95 → 42.50 (−5.7 on four), `mixnoise_a10` 60.70 → 59.37 on
  every seed (its bar is no drift below 60.4), `h4c1_reverse` below LRU on three of eight seeds
  (65.39 → 62.84), `parkveil` −5.2 on one seed; against `whisper_mod_a12` 63.59 → 66.03 on every
  seed (above LRU, and deterministic where the row was bimodal), `crashnoise_a12` +3.0 / +5.0 on
  every seed, `parkveil_min` +5.0, `mixmod_a010` +2.1, `whisper_mod_p6` +0.83, the witness +8.75 on
  five seeds. The moat rows bit-identical; floors and corpus within noise. On `h4c1_reverse` the
  low-phase pulse discards the unheld floor anchor, the wait drops with the stillness run intact,
  so the audit arms on the discard sample with the pulse's dip as its base, reads the pulse's end
  as its own reversal, and each such walk spends the rung: the 63% confirm the shipped machine
  reaches at s46 comes at s101.
- **rearmheld** (the cold schedule only where the discarded anchor was a held park, read before
  the stand-down releases the hold) and **rearmheld2** (the same with `settledRate` kept, which
  changes nothing on a12 and costs 0.5 on p6): the closest form. Battery 59 of 75 unmoved and
  ten of the sixteen movers bit-identical at seeds 1–8 (the bimodal rows' draws); the moat rows
  bit-identical at seeds 1–8; `metronome` and the h4c1 rows bit-identical; floors within 0.2;
  corpus within the spread (`cp_w100` 45.77 → 45.23 against a 0.36 spread, `cp_w098` −0.30). It
  earns `whisper_mod_p6` +0.32, `scarburst` +0.54 and the witness +3.79 on five seeds, and it
  pays on the F1 pair: `whisper_mod_a12` 63.59 → 61.72, −2.2 on seven seeds against the row's
  ≥ 62.8 sentinel and 2.65 below LRU, and `crashnoise_a12` 61.69 → 60.13, the high basin lost on
  four seeds. The post-shield swing discards the park, the restart arms an audit four samples
  later from the park's position, and on the ±12pp swings those walks crash and spend the rung
  where the kept wait spaces the audits out.
- **rearmheldcold** (the held gate with the stillness kept): `whisper_mod_p6` 66.01 → 63.31 on
  every seed, since the post-shield discard arms the audit on the swing sample itself; `a12`
  −0.39; `metronome` and `h4c1_reverse` bit-identical.

The latebloom earnings were the same in every reset form and are the candidate's measure. Where
the stand-down fires the gap to the ceiling falls 16.7 → 6.4 (phase 1 = 64), 15.4 → 5.6 (80),
11.3 → 4.9 (120), 9.8 → 4.9 (132) and 14.9 → 5.2 (64 with a 220-sample phase 2); the base is
bit-identical at every seed, since its arrival lands inside the retreat cover and no stand-down
fires; the witness rises on the five seeds whose arrival straddles the cover's end (−8pp on the
landing sample, covered, and −6pp on the next, not); the 48 alignment pays 3.5 → 6.9, an audit
fired from a still-good unheld position four samples after the undo-landing discard walking onto
the flat shelf and failing at budget; and the residual in every form is the alternation's side
choice from the interior rest point, the first re-armed audit spending sixteen samples walking
down to the floor before the next one goes up. The clock alone cannot carry the deep rung, and
the ladder reset is load-bearing: with the rung kept, the first re-armed audit's failure at rung
64 re-doubles the wait.

No form gates clean because the discarding stand-down is the machine's one regime-change
detector and it fires on every crash-scale move at the anchor, which the periodic families
produce every few samples (the track-planted anchor at every swing, the park at the first
post-shield swing) and the pulse family at every dip. The shipped machine pays those discards
with the anchor alone and keeps the schedule. Letting them touch the schedule loses whichever row
the form's shape exposes: the stillness restart starves the clock on the swings, the kept
stillness arms on the dip, and the held gate trades one whisper_mod row for the other. The
distinction a landable form needs, a level change that persists against a swing that returns, is
the signal-classification wall (§5, *Ageing an off-anchor claim*); a restart deferred until the rate has stayed a margin
below the discarded claim through a settle is a new state inside the confidence-gate graveyard
and was not built. The seven arms stay in the harness under one flag table (`rearm*`), so the
square can be re-priced after an alternation change.

A regime change can also miss the detector. `standDown` discards only with the window on the
anchor, so a step that lands while an audit's undo is still carrying the window home keeps the
stale claim; its veto later drags the window back, and the next audit waits out the ended regime's
clock. `slowswap_step` shows the size: moving its swap 16 samples later (`slowswap.py --p1 166`)
puts it inside such an undo, and the shipped climber reads 33.3 against 40.3 at the committed
timing, holding near a 25% window with a 1% hit rate through a 64-sample wait while the new regime
wants 80%. Discarding on an off-anchor step meets the same wall, since the controller's own moves
also step the rate.

### Guarding the probe's arming, and re-reading its verdict, on a regime-change sample

Four guards on the starvation probe's arming, built for `arc_DS1`'s trough-armed confirms at a
1M maximum (§3's thin-signal floors; the v7 shape above), each priced on the unseeded battery,
the floors and the corpus, with the movers re-adjudicated at seeds 1–8 and the arms rotated
inside each seed:
- **shiftarm** (a blind corner on a sample whose rate moved by a restart threshold from the last
  closed sample holds one sample before arming): DS1 11.99 → 13.74 at 1,000,000 and 13.45 →
  14.77 at 1,051,635, floors and corpus within noise, and the phase-alternation families lose
  their escape, since the flip sample is the blind sample and a hold on it arms nothing when the
  next sample is sighted: `phases_d050` −10.6, `widepin` −9.4, `phases_d050@32k` −4.4,
  `shallowmoat` −2.7 (unseeded N=8/8/5/2), against `mixture_d050` +5.7, `blindlock_blind` +5.4
  and `metronome` +2.3, where a shift-armed probe was failing and imposing its refractory.
- **shiftarm0** (shiftarm, and the first sample holds as well): the above plus `demoflood`
  −10.6, `absolve` −5.2 and `slowswap_ramp` −2.9. The first-sample probe is the escape on those
  rows, so a first-sample hold on its own (`coldhold`) was not run.
- **shiftdead** (the hold only where the sample is dead, both regions starved, and a shift: the
  v7 walk armed in a trough and nothing wider): DS1 +1.73 at 1,000,000 and +0.42 at 1,051,635,
  since two of the four arms there are on samples with main at 8k–146k hits against a bar near
  4k (the wedge round's "low samples straddle the starvation bar"); 14 of 16 re-adjudicated
  cells bit-identical at every seed, `widepin` 55.48 → 47.80 (five of eight seeds at −8.4 to
  −13.7; its bar is no per-seed drift) and `phases_d050` −0.69 on four seeds.
The rows that pay are the ones whose escape is a probe armed on exactly the sample the guard
holds.

The verdict side was then exhausted on Ben's ask, three forms on the same cells and battery:
- **rebase** (a starvation walk whose sample rises by a restart threshold re-freezes its base,
  rate and probation density, on that sample once and walks on; **rebasepos** returns the confirm
  to the rebase sample's window): DS1 13.83 / 14.62 at the two sizes, 27.44 at 2M (an extra
  stride), and `phases_d050@32k` −13.7, `phases_d050` −12.3, `widepin` −5.9, `norank_rep_r6`
  −3.2. Re-freezing the probation baseline on a live sample is the `nofreeze` failure for the
  escape: the walk's own demotions enrich live probation into a veto.
- **deadbase** (a walk armed on a dead sample is adjudicated against the live probation density):
  DS1 14.07 / 13.59 (alignment-limited as `shiftdead`), `slowswap_step` −6.7, `widepin` −5.2,
  `phases_d050@32k` −3.5.
- **deferrise** (a crash-scale rise defers the verdict one sample, holding the position, with only
  the crash reference moved and the verdict baseline kept frozen at the arm): DS1 13.91 / 15.12
  (above the hit-rate law's 14.91 at the gate's size), 2M −0.09, S3 +0.13, and `phases_d050@32k`
  −13.2, `widepin` −9.4, `phases_d050` −7.4.
Every form that refuses or defers a verdict on a regime-change sample loses the phase-alternation
escapes, since on those rows the flip sample is the only sample the verdict can be read in: the
regime alternates at the sample cadence as DS1's does, and there the larger window is right. At
the decision the two are one signal. DS1's cost at this size stays the wedge round's accepted
price; `wedgeshift` (above) remains the recorded guard for the shape, still not
landed; the untried lever is the sample period itself (`dens10x` reads 14.34 at 1M by taking four
samples instead of ten, and on S3 +0.2 / +1.0 / −0.6 / −0.3 at 100k–700k), a cadence question
rather than a rule for this shape.

### Whole alternative controllers

- **Marginal-loss controller: locally right is not globally sufficient.** Exact loser-refault
  attribution was locally informative: all seven screened deltas were non-negative (0 to
  +1.80pp). The controller built around it still failed its final-regret gate. Moving a fixed 1%
  of capacity every 2C requests left the ten-cell nearest-rank p90 regret at least 2.83pp against a
  1.5pp bar. P3 states the limitation cleanly: hit rate falls while moving from the 1% start toward
  roughly 20%, then rises to a remote 80% basin. The local signal correctly points back toward the
  floor and therefore cannot discover that basin. Adjacent-direction accuracy and global search
  regret are separate gates. Keep the signal as a possible diagnostic or local trim input, not as
  evidence for a replacement climber.
- **Replicated MiniSim: useful information, rejected realization.** The first prototype was
  invalid: ordinary static W-TinyLFU arms recomputed probation at each window size, while the live
  host keeps probation fixed and transfers capacity between window and protected. The repaired
  experiment constructed reachable integer segment triples, clamped and deduplicated targets
  before scaling, followed capped moves to the exact approved coordinate, and supplied the same
  request-indexed admission variate to every arm. At the co-minimal 128-entry, three-cohort setting,
  its six-cell median and p90 static-oracle regret were 0.197pp and 0.305pp: remote counterfactual
  ranking did contain useful information. The 27-policy realization then failed every production
  cost bar: 103,816 bytes retained when empty, 314,784 bytes steady, 24 bytes per sampled hit, a
  median 1.96 bytes/request on the natural 95/5 mix, and a 37.52% median hot-hit throughput
  regression. This rejects that realization, not the information source.
  The useful correctness subset was later extracted to main: registered MiniSim now preserves the
  live fixed-probation geometry, removes full and scaled aliases, and computes each integral move
  from the host's actual window coordinate. The expensive replicated controller remains rejected.
  Equal RNG seeds still do not give its arms request-indexed common draws, so MiniSim should not be
  described as a fully paired counterfactual oracle.
- **Learners over sampled miniatures: better on quality, rejected on cost.** A learner over the
  registered MiniSim miniatures (fixed-probation copies of about 100 entries fed a 1/R key sample)
  that keeps a discounted miss total per arm over a 64·maximum horizon and moves the host window to
  the leader gains a median of about 3.7pp over `product.Caffeine` on the gate battery, with no row
  worse than about 1pp, and 0.5 to 1.1pp on eighteen real density-tier cells (simulator host,
  unseeded, one to three draws per cell). Five arms at 1/20/40/60/80% keep the gain, so the prize is
  reaching the right basin rather than resolution. Its real-trace wins are the recorded give-backs:
  the average law's rest point on frequency-favorable traces (the `cp_w097` family 4 to 6pp, `P8`
  and `S3` 1 to 2pp) and `cp_w050@123038`'s start path. Its own failure is miniature fidelity: just
  after `DS1@2M` fills, the miniatures rank a 76% window above 1% where the full cache scores 56.8
  against 61.9, costing 1 to 2pp there and at 4M. Three variants are dead. Hedge at a static-regret
  learning rate (H-MC, Ben Mazziane and Zou 2026) cannot follow a regime change: −5.9 on the corda
  stress at 512, −1.4 and −3.6 on `slowswap`. Its maximal coupling of consecutive picks buys fewer
  moves and no hit rate over a deterministic leader. The registered per-epoch argmin chases
  alternation, losing `phases_d050`, `widepin` and `rep_r6` by 6 to 12pp to either learner with
  memory. Rejected as a product mechanism on costs that grow with the arm count or the maximum:
  per-entry nodes and an eviction loop for each miniature (only the sketch and a key table can be
  shared across arms), miniatures sized in proportion to large maxima, and a grid to choose. Kept
  as `/audit-regret`'s ghost, the simulator's `minisim-leader`, because it is an online policy
  rather than a hindsight one: it can exceed every fixed window on a phased trace (39.3 against
  33.5 on the corda stress at 512), and its copies' misses at each sample say which window that
  sample rewarded, the per-phase reference against which the skill reads the climber's costly
  episodes. As a gap-finder it adds little: its gap to the climber ranks cells as the static gap
  does (rank correlation 0.99 over the gate rows, 0.94 over real density-tier cells), and its own
  window, flat on the traps and dithering among near-equal windows on real traces, localizes
  nothing. Its configuration is measured. With five arms and MiniSim's ~100-entry miniatures it missed narrow optima
  (`crestpast` 62.6 against a 64.7 ceiling), and at finer grids the miniatures mis-ranked near
  neighbors (`moat_h7800` 48.3, where five arms read 49.5); miniature size was the larger lever.
  Seventeen arms at 5% steps with full-size copies up to 20,000 entries read 64.6, 56.8 and 50.8
  on `crestpast`, `metronome` and `moat_h7800` (ceilings 64.7, 55.5 and 49.7), sit a median 0.1pp
  above five arms across eighty cells with one row lower by more than half a point (`widepin`
  −0.8), and forty-one arms add nothing (a mean 55.60 against 55.63 on the twenty cells where the
  configurations differed). It lands below the climber on `DS1` at 2M and 4M at every size tried.
- **Bounded reuse-survival estimator.** A smaller primitive estimator was a credible attempt to
  retain the remote-ranking information without full policies. Its registered realization missed
  both information bars: 0.727pp median winner regret against 0.5pp, and 2.849pp p90 against
  1.5pp. Retire that realization as controller authority. The broader reuse-survival idea was not
  disproved by one resource setting and seed family.
- **Compact Shadow prototype.** Its controller semantics and bounded layout checks passed, and the
  observer allocated nothing in its steady interval. The first pair of the terminal four-cell cost
  gate then measured 32 bytes per candidate command against 16 bytes for the baseline, so the
  frozen stop rule ended the study before quality was scored. Compact state is possible; an
  affordable observation seam and a quality win remain unproved. There is no superiority result.
- **Archive boundary.** The trace-metadata prototype, compact primitives, static oracles, runners,
  manifests, and experiment evidence plumbing have no upstream consumer or shared owner. Keep them
  archived until one exists. Whole-file transplants and a public `SimulatorAdmissionRandom` API
  are rejected. A future consumer needing exact randomized pairing must first show that both the
  draw count and request-index digest match.

### Cheaper forms of the veto's return re-test

- **A hold before the retreat commits.** A completed shortfall streak stands the window still for
  two samples and re-samples, cancelling the retreat if the rate recovers to within the veto
  margin. Endpoint +1.16 against the retest alone's +1.39, three negative seeds of sixteen against
  none, `norank_flood_j100` −0.08 and `shieldtrap_s11` −0.01 where the retest alone is
  bit-identical, and `cp_w081` −0.42 on the corpus where the retest alone is +0.07. The mechanism
  is self-defeating: spreading the arrival drop across the hold's own samples is what puts it
  under the crash-scale threshold, so the hold manufactures the masking the retest then repairs.
  The cancel branch never fired on any cell. Do not re-propose a pre-retreat hold without a cell
  where a veto is shown to fire on noise.

### Other ways to judge an audit's verdict against its walk

- **`bestgap`** (the verdict pulls back only past one stride of separation, the arrival transient's
  own width): `cp_w097` +0.73 against **`demoflood` −3.59** and `mixture_d050` −0.33. `demoflood`'s
  real crest and `cp_w097`'s noise spike are both one stride behind the walk's end, so the
  pull-back's distance does not separate them.
- **`bestrough`** (the pull-back must beat the largest sample-to-sample move of the walk itself):
  restored the then-baseline `cp_w097` value recorded by the `07c6e370c` crestpast study
  (47.61 under its original unseeded simulator protocol; not comparable to the fresh seeded N=8
  mean) and costs **`demoflood` −3.59** again plus `hazefloor` −0.73. Two independent statistics refusing a
  real crest in order to refuse a spike is the shape of item 1's finding on this layer: the signal
  cannot tell a crest from a peak.
- **`monoland`** (the margin plus a monotone decline from the best to the walk's end): **inert**,
  bit-identical to the shipped form on every decisive cell. Where the margin has decided, the shape
  test has nothing left to refuse.
- **`bestflip`** (a park short of the walk's end points the next audit the other way, since the
  ground beyond is now known worse): `hazefloor` −3.92. The follow rule's value is the direction the
  confirm earned, not the unexplored side.
- **`landclaim` / `refclaim`** (a covered landing keeps the claim but releases the hold, so density
  may still improve on it): `moat_h4000` −2.29, a re-derivation of the recorded `arrive` kill —
  without the discard's `Rates.reset` the rail's margin stays at its 1pp floor and `track` re-plants
  the anchor down density's slide.
- **`retreatref`** (the retreat's samples judged against the rate the probe left, the `freshref`
  rule applied to the stand-down rather than to the confirm): weaker than covering the landing
  outright (`crestpast` −0.09, the plant-at-40 cell −0.45), because the landing's recovery is itself
  an arrival transient that takes more than one sample, so a level test against the pre-walk rate
  still fires while a first-difference test no longer does.

### Other ways to un-stale an away-anchor claim

- **Discarding the claim on a crash-scale swing that lands with the window still** (`stilldiscard`,
  `still2`, `still3`, `quiet`): fixes the family exactly as the landed reference change does, and
  costs the rail's control rows because a still swing is not necessarily the workload's. The one-
  boundary form mistakes a retreat's echo (`moat_h4000`: the undo arrives, the collapse lands one
  sample later inside the band, −2.3 / −3.4); two or three boundaries excuse the echo but not the
  terrain's erosion at a still window (`moat_h3000`: fourteen still samples, the window's hits fall
  5,662 → 112 with main's flat, then a −7pp break: −0.7 to −1.9 on eight of eight). The claim the
  swing discards is the machine's memory of the prize the rail then recovers (veto to 0.62 kept,
  0.46 discarded). Region composition would separate the cases seen (a shift moves main's hits, the
  terrain's collapse only the window's) and was not built: a second threshold on a heuristic.
- **Re-seeding the goal metric on a still swing while keeping the claim** (`resetstill`,
  `freshreset`): the re-seeded deviation prices the shortfall against the kept claim as real, the
  rail vetoes into the dead anchor within four samples and the audit that then arms from it confirms
  a mediocre position (`ghostclaim_p30` 41.2 against the reference change's 49.2). The deviation
  spike a shift leaves is what holds the rail off while the audit walks first.

### Variants of the repeat-confirm memory

- **The repeat memory cleared on the anchor's discard**: the confirm sample is the lure's on-step and,
  where the walk lands at the previous anchor, that step discards the anchor on the sample the confirm
  is judged; the chain breaks (`absolve_p8` seed 1 41.4 against 46.4 for the fail/crash-reset form).
- **The park's first audit following the confirmed walk unguarded** (`momentum`): `absolve` +16.7,
  `veilmoat` +3.7, `shallowmoat` basin B +2.7, `flood_j100` +4.1 (N=8), `moat_h3000` +1.6, `moat_h4000`
  +0.8, `crashnoise_a12` +0.5, `shieldtrap_s13` +0.6 against `moat_h5000` −1.75 (four seeds at −3.4: the
  far slope's gentle decline takes five strides to trip the 5pp bar, the next audit is the tolerant retry
  and its undo evicts the band), `whisper_mod_p6` −1.76 (its sentinel), `climbtrend_up` −0.9 (its
  sentinel: trend misconfirms swept wall to wall), `saw_p40` −0.5. The guard on the smoothed rate is what
  ships.
- **The guard on the confirm sample's raw rate against the arm sample's**: fragile to the lure's phase
  (`absolve_p12` seed 1 falls back into the trap, +0.6 against +17.6) and keeps `moat_h5000` −3.4 and
  `saw_p40` −1.4; the smoothed reference is robust to the phase and fires on the trend.

### Releasing a park that the workload has starved

- **`unpark`** (a parked, non-blind window starved for a sample discards the anchor and hands the
  sample to density; `unparkboth` also on a starved main): fires at three sites in 642 audit
  confirms and loses one of them big (`phases_d050@32k` seed 8 −5.2 against `rep_r6` seed 4
  +3.8). The park on a starved position is priced by rarity, and the census is the instrument to
  re-run before proposing any rule about it.
- **`marggate` and `margrest` on `flood_j100`**: both are the average law there. The gate's
  "probation richer than main's average" test is satisfied by the SLRU geometry alone whenever a
  rotation exceeds protected (every promoted key is demoted before its reuse and protected earns
  nothing), 47 of 48 steering samples, and the rest band is entered only at 57–76% windows. The
  marginal signal itself is right on the cell (tail hits 0 at every window; ungated `marghalf`
  63.19 against 51.98) and dead by the frontier above.
- **`arrive` alone** (an undo's or a return's landing sample is not judged for a shift): moat_h4000
  −2.69, h5000 −0.50, h3000 −0.36 on every seed. Without the discard's `Rates.reset` the rail's
  margin stays at its 1pp floor and `track` re-plants the anchor down density's slide into the
  valley, so the rail's moat win turns out to rest on the reset widening its margin. (The
  undo-landing half of this arm is what the verdict covers; the return half, re-expressed through
  the retest state once the retest existed, was implemented as
  `isReturnTest`, and on release HOLD by the repeated-real-trace recheck.)
- **`auditshield` alone** (a shift during an audit walk armed from a park does not stand the park
  down): bit-identical to ship everywhere; the undo's arrival at the anchor discards what the crash
  spared. **`arrive` + `auditshield`** as an unconditional pair: moat +6.5 in total and `flood`
  +2.2 against **`demoflood` −1.88 on every seed (its bar)** and `whisper_mod_a12` −1.83, the park
  held where ship's release let density drift to a better position. The park-retention trade with
  its mechanism; a scoped form on the top-corner residual is where it would be worth something.
- **`pricedshift`** (the stand-down's shift trigger priced as a starvation probe's crash bar,
  clamp(3·dev, [5pp, 15pp])): crashnoise_a12 +1.71 and deterministic, `rep_r6` seed 4 +30.9, and
  **cp_w081 −0.39 on 4 of 4 seeds**, arc_P8 −0.19, arc_ConCat −0.13, moat_h7800 −0.94 (a basin
  collapse). The fixed trigger's churn on noisy cells is inert on the corpus and protective on
  cp_w081, where a persisting anchor lets the rail veto on slow weather. A trade; do not re-derive
  the level, and do not read the C3 churn as a defect without this cell.
- **`nocorner`** was the diagnostic that priced the upper corner's probe here (+2.3 rep_r6, +3.8
  balloonflip against −0.2 on cp_w015 and arc_ConCat, read then as "the probe stays"). The probe
  was deleted and the arm with it; `cornerprobe` restores it, and the
  battery reads the restoration 2.2:1 against.
- **`deferreward`** (the wedge pricing's next-sample half): a redistribution of `rep_r6`'s seeds
  (+3.0 mean, one seed −20.9) and inert or slightly negative elsewhere (`phases_d050` −0.49); the
  ladder's doubling is coarse against a 91-sample trace, so any faster escalation wins the seeds
  where the deeper probe finds the band and loses the one where it hits a thin burst. Not landed;
  the arm is in the workspace if the sentinel's mean is ever worth that.

### Other prices for a confirm the density arm reverses

The round priced a starvation confirm that the density arm reverses in the same sample (a
"wedge"). What shipped is the escalation without a wait; the rest is dead:

- **wedgehold** (the wedge keeps the rung, neither reward nor escalation) and **wedgefail** (the
  wedge is a failed experiment: undo, escalate, refractory = rung): both hold the floor long enough
  for the calibration audit to arm and misconfirm on the warmup trend, so `shallowmoat`'s wedge
  seeds read 27.5 / 27.3 against ship's 28.5, and neither reaches the prize inside 64 samples.
- **wedgewalk** (the wedge does not end the walk): the v9 slam-continue family (§5 above), kept as
  the diagnostic upper bound (33.5 on the wedge seeds) and not promotable.
- **wedgeflat** (only a wedge on flat terrain escalates; one across a restart-magnitude change
  keeps the rung): `trickle_s7` −6.7 (four seeds at −14) because the kept rung makes the next
  fail's refractory 32 samples instead of the reward's 2.
- **wedgedead** (a walk armed on a dead sample is first-round): `arc_DS1@1051635` −1.7; its low
  samples straddle the starvation bar (2,662 main hits against a bar of 4,108 on one, 10,221 on
  the next), so the second is a rung-64 walk to an 80% window.
- **wedgelow** (a walk armed a restart threshold below `rates.smoothed` is first-round): never
  fires on the alternating trace, because every sample is a shift and `rates.reset` re-seeds the
  smoothed rate to the arm sample itself.
- **wedgeshift** (a walk armed on the sample the rate fell into by a restart threshold is
  first-round; the guard the shipped rule can carry): measured and not landed. It restores DS1 and
  `deadphase` bit-for-bit and removes `phases_d050`'s two seed flips, is bit-identical on every
  non-wedge cell run (arc_P8, arc_S3, cp_w015, cp_w050, mixture_d050, straywall2, slowswap, moat),
  and gives back `strad_p8@4097` +1.9 → +0.5, `widepin` +1.7 → −0.8 and `phases_d050@32k` +4.8 →
  +2.7. Its evidence for the floors is one cell (DS1 is the only floor cell with any starvation
  confirm), so the pass is by construction; the instrument that would price it is the unspent
  stillness holdout. Add it if trough-armed deep walks show up at scale.

### The 1% initial window is not worth moving

`PERCENT_MAIN` has never been re-asked since it was chosen. The hostile-window study above gave it
an instrument, so it was swept properly: 77 cells classified by their *measured* static optimum
(43 frequency-favorable at ≤5%, 14 middling, 20 recency-favorable at ≥40% — the axis the hostile
study could not see, since its cells were screened **for** a small optimum), then 37 of them swept
at starts of 1/2/5/10/20%, plus a 30% arm on the 23 density cells.

**The bar was fixed before measuring**: corpus mean ≥ +0.50pp, no stratum mean below zero, no
workload family losing more than 1.0pp, holding at each tier separately. **The best arm returns
+0.049pp**, a tenth of the bar, and no arm satisfies the stratum clause. Applying this study's own
readability floor (≥15 density samples) the whole table collapses further: every arm's corpus mean
lands within ±0.21pp of zero, the best being **+0.018** at a 20% start. The mid stratum's apparent
+0.25..+0.87 is entirely `corda@2k` and `corda@4k` at **3.3 and 6.6 samples**, which price the
start rather than the controller; with the floor applied that stratum is negative at every arm.

**The mechanism is why, and it is the part worth keeping.** The climber absorbs the plant
asymmetrically: on recency workloads it captures **96%** of what a larger start could statically
buy (frozen +2.68 against a delivered +0.11 at a 10% start), because it was walking to a 20–50%
window anyway and the plant only shortens a journey already underway. That assumes the climber
walks; on the LLM prefix-cache cells (§3, terrain classes) it walks too slowly for one pass, where a
plant does pay (one pass of To-C@185364 gains +8.7 from a 10% start and +17.5 from 50%), though with
fresh-key replays it reaches the large window within two to four passes anyway. On
frequency workloads it passes **90% of the harm through** (frozen −0.80, delivered −0.72), since there the plant is pure
displacement to be walked back at the descent rate of 4–6% of the maximum per sample. A larger default therefore buys almost
nothing where it would help and costs nearly full price where it hurts. On two cells it lands in a
*worse* attractor than 1% reaches at all: `wiki_1191a@64k` frozen −0.12 → delivered **−0.99** on
3 of 3 seeds, `arc_P13@64k` frozen −1.59 → **−3.41**.

The storage families are the case against moving it: the ARC set is **16 of 18** frequency-favorable
with an optimum at a 1–2% window, losing monotonically as the start grows (frozen −0.15 / −0.44 /
−1.18 at 5 / 10 / 20%, worst −5.41). A 5–10% default is a straight tax on exactly the large,
frequency-signalled caches the 1% choice was made for.

`cp_w050@123038`'s basin (§3, *Path-dependent rest points*) does **not** argue for a larger default: at N=8 the 2/5/10/20% arms
read +0.02 / −0.01 / +0.04 / −0.21 and all converge to the same ~20.6% attractor, and only the 30%
arm moves (+4.72, and one seed of eight already falls into the *bad* high basin from a 20% plant).
It is a threshold between 20% and 30% that a 5–10% default captures none of, and it remains a
question about the audit layer rather than about the default.

Not decision-bearing but recorded: strata are near-collinear with families in this corpus (ARC 16
of 18 frequency, cloud-physics@16k 12 of 14 mid-or-recency), broken only by `scarab_recs`,
`wiki_1191a`, `msr_prxy_0`/`hm_0` and the lirs reactive cells; `cp_w097/098/100/101@16384` are
near-duplicates and count as one workload; and `cp_w050@2048` has its whole static curve **below**
LRU (ceiling 13.65 against LRU 14.52). No holdout was spent, and none is owed, since nothing was
fitted. Two rest-point give-backs above the documented 0.5–2.5pp band turned up in passing and
belong to the marginal-steering thread (§5) rather than here: `arc_P1@64k` settles at a 30% window against a 1% optimum
for **3.25pp** (45.63 against a 48.88 frozen ceiling). `cp_w097@16k` is the other face, but it does
not settle: its 135-decision endpoint is near 47% against a broad 5–15% optimum, and continuous
270- and 541-decision runs stay near 47% while cycling through seed-dependent walks and parks.

### An LRU estimate as an audit trigger

A sampled estimate of what an LRU of the same maximum would hit (SHARDS-sampled keys in a
fingerprint-and-stamp table of about 25 KB, read through AET with the SHARDS-adj correction, within
about 1pp of exact LRU) can schedule an up-audit when LRU out-hits the cache by more than 3pp for
four samples. Under plain multi-pass replays it brought every LLM prefix row within 2.6pp of LRU at
steady state and lifted the battery by +1.6 with no bar failing, but the steady-state win was the
replays' frequency memory (§6): with fresh keys per pass the shipped climber converges within two to
four passes on its own, and the trigger adds 0.0 to +0.8. It also cost 16% of `EvictionBenchmark`
throughput at 5,000 entries and 9.5% at 10,000, where 2,048 sampled keys cover 41% of a 5,000-entry
cache's requests; capping sampling at 1/16 of keys cut that to 2.7–3.7%, the rest being per-request
work. Early in a run the cold frequency sketch lets LRU lead on frequency-favorable traces too, so a
lead-triggered audit ahead of the climber's own first audit confirms premature windows. What
remains is +1.6 on synthetic traps, which does not pay for the state and the throughput. Price a
per-request sketch at the smallest maximum it serves before validating its hit rates.

### Accelerating warm-up: jumps, momentum and shorter samples

On a recency-favorable trace the climber reaches its large window, but slowly. On the LLM
prefix-cache cells (§3, terrain classes) one pass of a two-hour trace is 8.5 to 25 samples at the
larger sizes, and the climber spends 9 samples (To-C@185364) to 74 (Thinking@32768, about three
passes) at the floor before steering climbs: a grown window reads sparse while main holds the
captured hot set, and probes and audits from the floor fail on phase swings. The rules that shorten
that phase buy +1.6 to +2.8pp on those first passes, and each costs something on every seed: a
frequency-heavy real trace 0.7–2.4pp or, once gated so that no real trace triggers it, the
battery's traps. None passes the bar the start-window ruling above set:

- A geometric up-audit from the floor (stride doubling while the hit rate holds), with dip
  tolerance and arming ahead of the calibration wait: +2.8 on the prefix cells, but To-B −1.2 and
  ARC S1 −2.7, S2 −1.2, S3 −0.9. Gating it on an early sample in which the window out-earned main
  leaves the controls bit-identical but not ARC, since a cold cache makes every window look dense in
  its first sample. Judging it against its base plus the hit rate's prior rise, with arming held two
  samples, keeps +2.4 (To-C@185364 +7.4) and still costs P8 −0.9, S3 −0.8, S1 −0.7.
- The same jump behind a fill-drop gate, which allows it only when the hit rate falls once the
  cache is full (the second and third samples averaging under 70% of the first): every corpus cell
  (seeds 1–2) and control is bit-identical and the prefix cells gain +1.6, but the battery fails
  slowswap's step and ramp (−11.7, −9.3) and mixture_d050_long (−1.3), each on every seed, while
  absolve and absolve_p8 gain +9.2 and straywall2@16k +4.0, for a battery mean of +0.4. On
  mixture_d050_long the doubling strides land at 80% and park at 69%, where the shipped walk parks
  at 56%. The early audit also moves every later one, so on slowswap an audit crashes just before
  the swap; the swap's rate step lands during its undo, away from the anchor, where `standDown`
  ignores it, and the stale anchor vetoes the climb. That trap is timing-sensitive for the shipped
  climber too (33.3–41.8 as `slowswap.py --p1` moves the swap ±24 samples, the row's committed
  timing near its best), but averaged over those timings the jump still costs 2.1. Handing the
  landing to density steering instead of a park does not escape this: at To-C@185364's 80% landing
  density reads the window sparse (log ratio −0.8, main still holding the captured hot set) and
  would give back the gain, while at mixture's 69% overshoot it reads −0.5 and would correct it,
  so the landing carries no signal that tells the two apart.
- Momentum, consecutive same-direction density steps growing ×1.5: +0.3, too small.
- Samples of one maximum instead of four until the first audit: +2.2 (Thinking@65536 +5.1), but
  DS1@1M −2.4 and cp_w097 −1.2. DS1's first audit never runs, its 0–68% per-sample swings drive
  density-judged probes to a 40–62% window, and capping the short samples at 8 or 16 still costs
  DS1 −1.6 or −1.2.

Two things recur. The first samples after the half-full gate are cold: the hit rate rises with the
warming cache (ARC S1 climbs 1.1 → 6.9 over the first ten of its 15 samples) and the empty frequency
sketch makes the window look dense, so a verdict taken then is biased. And an early move persists
through parks, backoffs and the audit schedule (P8's jump crashes cheaply at 20%, yet a later audit
confirms larger windows on every seed). So the ungated rules help the traces whose right move is up
anyway and charge the ones whose right move is to stay; the fill-drop gate removes the first problem
and still fails on the second. Warm-up stays as it is; a rule that could pass needs an early move
that neither parks its landing nor shifts the later audits.

### Neither main-space knob is worth adapting

The main space has two elements the climber never touches: the 80/20 protected/probation split
(`PERCENT_MAIN_PROTECTED`) and the 1-hit promotion rule (`reorderProbation`). Merlin adapts the
analogous quantities, so both were swept as a second dial: P in {0.20, 0.50, 0.65, 0.80, 0.90,
0.95} and a promotion gate requiring the sketch's estimate to reach T in {1, 2, 3, 4} — the
zero-metadata analogue of Merlin's `guard_freq`, needing no new per-entry field and no scan.
Object-count, the 288-cell Merlin matrix minus the two multi-GB traces, one build, the shipped
arm re-measured rather than inherited.

- **There is no better fixed constant, in either knob.** Every alternative loses on the corpus
  mean: p90 −0.051, t2 −0.031, p65 −0.074, p50 −0.155, t3 −0.140, p95 −0.191, p20 −0.249,
  t4 −0.373. The shipped 0.80 / 1-hit is the optimum, not merely a defensible default. This is
  the opposite of Merlin's own region dials, where the ghost budget was worth +1.49 mean and the
  filter ratio +0.83 (`merlin_region_sizing_headroom`) — those were bad defaults; this is not.
- **The hindsight prize is inside the instrument.** Best-of-9-arms per cell is +0.326 mean, but
  `product.Caffeine` is not bit-deterministic and max-of-9 is biased upward even when the arms are
  identical: re-running the **shipped** arm nine times on a representative 26-cell subsample buys
  +0.116 from noise alone. Net **+0.21 pp** for a perfect per-workload oracle. Any real controller
  captures a fraction of an oracle, and would be paying for it with a second extremum seeker.
- **The response is real but its argmax is not learnable.** 16/26 cells show a P spread exceeding
  twice the same-arm noise range, so the surface is not noise. But the winner is scattered almost
  uniformly (p95 57, p90 54, base 50, p20 43, p50 38, p65 34 of 276), **46/46 traces change their
  preferred P across the size ladder**, and the mean response is flat to ±0.1 pp at every size rung
  with the peak at or adjacent to 0.80. There is no stable per-workload optimum to seek.
- **The within-trace pass, which is the one that could have overturned this, closes the same way.**
  A per-cell oracle prices only a per-workload controller, so each arm was re-run on three disjoint
  thirds (`trace.skip`/`.limit`, binding confirmed by segments that genuinely differ). On
  *selected high-spread* cells the per-segment prize looks large — +1.075 mean, up to +4.11 — and
  the per-segment noise floor is small (0.16–0.39), so those gains are real rather than noise. On
  an **unbiased** 14-cell sample it collapses to **+0.225 gross, ≈0 net**. The difference is cell
  selection, not workload regime: splitting by adaptations available in the segment does not
  separate it (<15: +0.184, ≥15: +0.256), so the tempting "it is climber non-convergence" reading
  is **not** supported and should not be repeated.
- **The downside dominates the prize by an order of magnitude, and it is a cliff.** p95 costs
  −1.76 at the 5th percentile and −6.16 worst; t4 costs **−25.72** on `backf@300`. A controller
  would be risking multiples of what a perfect one could win.
- **The promotion gate is inert where theory says it should be.** t2 changes nothing at all
  (|Δ| < 0.01 pp) on 55/220 cells: an entry that won the TinyLFU admission contest already carries
  frequency ≥ 2, so the first threshold step is a no-op. The gate only bites by starting to *deny*
  promotions that should happen, which is the −25.72 tail.

Two properties of the structure are worth keeping in mind for anyone who reopens this. Because
`increaseWindow` takes its quota from `mainProtectedMaximum` and caps there, window + protected is
conserved and **probation's capacity is fixed for the cache's lifetime** at `(1−P)·0.99·max`
≈ 19.8%; the window's reachable range is `[2%, ~80.2%]` and it can never grow into probation. That
also means the low-P arms conflate probation size with window ceiling (p20 caps the window near
20.6%), so their losses are a lower bound on p20's true merit — but p90/p95 carry no such handicap
and fail on the mean anyway, so the verdict does not rest on it.
A constructed two-reference band between the window's reach and LRU's (`s_probcapgeo`) costs every
reachable split 32pp against LRU, and a larger probation at the same window recovers none of it
(the filter refuses once-seen candidates); only a window of 89–92% catches it.

Not covered: `ConCat` and `MergeP` (12 of 288 cells, ~9 GB of trace) were not run.

**Companion result: the SLRU main space itself is still earning its keep against a plain LRU
main.** The 2015 commit that introduced it ("Use Segmented LRU for the main space cache") claimed
0–2% in many workloads, especially small caches, and the question is whether that survived the
sketch fixes since. Asked by setting the promotion threshold to 16: the sketch saturates at 15 and
`reorderProbation` is the only path into protected, so nothing is ever promoted, protected stays
permanently empty, and main degenerates to one LRU queue with the climber's window boundary
intact. (`PERCENT_MAIN_PROTECTED = 0` is **not** the way to ask this — it zeroes
`mainProtectedMaximum`, which `increaseWindow` early-returns on, so it measures a crippled climber
rather than plain LRU.)

Over the same 276 cells, plain LRU main is worth **−93.1 pp net, mean −0.337, median −0.227**.
SLRU wins by >1 pp on 40 cells and >2 pp on 8; LRU wins by >1 pp on 9 and >2 pp on 3; 227 cells
tie within 1 pp. **38 of 46 traces prefer SLRU.** Confirmed at N=3 on the 14 headline cells with
**0 sign flips** (σ ≤ 0.31 on all but `P1@69344`, whose N=1 +1.48 collapses to +0.07 — a noise
cell, not a result). The 2015 claim's size dependence also survives: the advantage concentrates on
the smaller rungs (rung 1 −0.596, rung 2 −0.601) and vanishes at rung 4 (−0.002). So: **keep SLRU,
but it is a modest, concentrated effect** — it is inert on 82% of cells and worth 1–3 pp on the
rest. The traces that genuinely prefer a plain LRU main are a coherent set worth remembering:
`fiu_ikki` +0.851, `fiu_webmail` +0.548, `backf` +0.326, `loop` +0.305, `MergeS` +0.259,
`fiu_homes` +0.186, `ps` +0.136 (and `corda_large` +0.002, a tie). They are the same cells that
wanted a smaller protected region in the P sweep, so the preference is consistent across both
knobs — it just isn't worth a controller, per the headroom result above.

**The promotion curve is non-monotone, and that is the interesting part.** Mean vs the shipped
promote-on-first-hit: @2 −0.031, @3 −0.140, @4 −0.373, @6 −0.640, never −0.337. A *partial* gate is
worse than either extreme, and it owns the catastrophic tails (@4 −25.72 on `backf@300`, @6
−20.33) that never-promote does not have (worst −3.06). Mechanism, read from `evictFromMain`:
`victimQueue` starts at PROBATION and only moves to PROTECTED once probation is exhausted, so
under a high-but-finite gate the few entries that do reach protected are effectively removed from
the victim pool indefinitely — protected never approaches its maximum, so `demoteFromMainProtected`
rarely fires to release them. Never-promote avoids the pathology by keeping protected empty. Any
future proposal to gate promotion must answer this, not just pick a better threshold.

### Reconciling failed-probe returns

`undoRemaining` accounts for issued integral commands; the cache applies them through budgeted
transfers of indivisible entries. A full sample can replace an unapplied remainder from each
stride, so a failed return can finish short and rebase the next probe. This can accumulate beyond
one entry: with maximum 10000 and nine entries weighing 1100, a public-operation witness returns
from 7800 toward base 100 with commanded decreases of 3000 + 3000 + 1700, but applies only
2200 + 2200 + 1100. The shortfall is 2200, two whole entries. A controlled unweighted witness with
8192 residents, a 128-CPU buffer configuration, a queued executor and buffered replacements loses
914 entries over two sample boundaries; its 16-CPU control returns exactly. These establish
reachability, not ordinary-runtime prevalence or hit-rate harm.

Two candidates were priced on 2026-09-13 against the real cache:

- **Bounded reconciliation** retains the base and derives later strides from the actual window,
  while the original command ledger limits the number of return samples. Final carry remains
  best effort. It recovers the two lost 200-unit remainders in a 25-entry/400-weight witness
  within the same three samples, so recovery need not prolong retreat.
- **Acknowledged return** retains the base and preceding window, ending on observed arrival or
  non-improving distance. Completion is checked before rate-reference updates to preserve the
  ordinary arrival sample's anchor tracking. Small positive progress can keep this return alive
  longer; the nine-entry witness needs a fourth sample to recover the whole 2200-unit shortfall.

Ten weighted cells from six traces (`MetaCDN_rprn`, `metaStorage_block_traces_1`, `alibabaBlock_128`,
`tencentBlock_11784`, `cloudphysics_w50` and `w99`, 16 MiB to 1 GiB), paired at admission seeds 1–8,
gave mean object-hit-rate changes of **-0.050 to +0.060pp** across both candidates. Some small
effects repeat: Tencent at 64 MiB gains about 0.04pp; w50 loses 0.016/0.010pp. Neither candidate
earned adoption. After a fixed write-flood prefix exposing the unweighted shortfall, a switch to
two million unit-weight w99 requests costs the candidates 813/834 additional misses (about 0.04pp)
at all eight seeds. Exact return does not imply a better allocation after a workload change.

The acknowledged arm reached 14 return samples against a maximum of three in the baseline and
bounded arm. On metaStorage at 64 MiB, return duty rose from 3.30% to 7.26% of adaptation samples,
with no resolved hit-rate gain. These are sample fractions on each arm's evolving path, not CPU
time. Both arms still often stop short on weighted traces when the final remainder cannot move.
JOL estimates on JDK 26 put the added retained state at 8/16 bytes per density cache, with no extra
objects. A 32-fork uninstrumented JMH screen could not resolve its declared 5% threshold: baseline
spread exceeded it. The acknowledged weighted arm was slower in all four blocks (descriptive mean
-4.68%), an unresolved cost signal rather than a no-cost result. Three unit-weight trace projections
and all 87 gate and
bundled stress cells at eight seeds matched baseline's checked cache outputs and admission
digests; shared baseline bar failures remained. This does not price weighted behavior by itself.

Retain best-effort undo on the measured tradeoff. Reconsider with a repeatable quality benefit
that pays for additional state or retreat time; neither incomplete physical arrival nor a
percentage of displaced capacity establishes that benefit.

### Entry-count tier selection for weighted caches

`resized` selects density above a maximum of 4096 in the weigher's units, however few entries the
cache holds, while `AddTask` sizes a weighted cache's sketch from its live entry count. Density's
period `min(4·maximum, sketch.sampleSize)` matches standard reactive's when the sketch cap binds;
byte units alone do not guarantee that. The comparison prices the whole controller, including
density's floor, probes, audits and anchor.

The 2026-09-13 comparison used 18 byte-weighted oracleGeneral cells (`MetaCDN_rprn`,
`metaStorage_block_traces_1`, `tencentBlock_11784`, `alibabaBlock_128` and `cloudphysics_w99`, from
4 MiB to 16 GiB) replayed through a cache configured like `product.Caffeine`, with three unseeded
repeats per climber arm and static windows at 1/5/20/50/80% as the reference. Entry counts
are medians sampled every 65536 demands after the first half-full observation. Small differences
need seeded pairing before motivating a change. The fixed-window grid is a coarse reference,
not an upper bound on adaptive gains.

**No consistent reactive advantage at low resident counts.** On the seven cells with medians of 20–389,
reactive − density reads −0.21..+0.27pp with density ahead on four, and density trails the best
static window by at most 0.66pp (one cell reads above it), against up to 1.57pp on the cells holding
800 to 72k entries. Larger observed differences do not follow a simple count threshold: reactive +0.96
on `MetaCDN_rprn` @ 4 GiB (800 entries) and +0.45 on `tencentBlock_11784` @ 256 MiB (11k), −1.45 on
`alibabaBlock_128` @ 1 GiB (72k), where its window median falls to 12% against a best static 80%.

**Retain the configured-weight threshold on this evidence.** Grow-only selection from the first
request changed object hit rates by −0.21..+0.26pp across the 18 cells. On five cells whose counts
straddled 4096, delaying that check until half occupancy produced the same first switch. Following
the count both ways on those five cells switched 23–13,434 times per run and changed hit rates by
−1.71..+1.10pp. Each switch replaces the controller and resets its sample and carry; the experiment
did not isolate those effects. Reconsider with a reproducible benefit that includes switching costs.

### Other pricings for the walk's two interior exits

The split's own dead arms, all measured N=8 seeded. Do not re-derive either.

- **A reversal bar with no base-rate term at all** (`revdev`, `min(5pp, k·dev)`) is dead across its
  whole range, at both ends for different reasons. At **k=3σ** (`noiseBand`) it is **bit-identical
  to `floorrev` on every seed of every cell measured** — gain and loss alike — because wherever a
  floor at 3σ binds, `max(fraction, 3·dev)` *is* `3·dev`, so the two derivations are one bar. At
  **k=0.5σ** it resolves `arc_S3` (+0.04) and then costs **`whisper_mod_a12` −2.35** and
  **`crashnoise_a12` −1.77**. Nothing in between serves: `crashnoise` needs ≥5pp at its scatter
  while `arc_S3` needs ≤1pp at its own, and the required multiples are ordered the wrong way. The
  mechanism is general — a scale-free bar is necessarily *small where the scatter is small*, which
  is exactly where the shipped absolute is load-bearing. **A base-rate term must survive on the
  reversal as a lower bound**, which is what shipped.
- **`floorrev` / `revfl300`** (floor both the reversal and nothing else at `3·dev`, under the 5pp
  cap): the right *shape* at the wrong multiple. It buys `shieldtrap` +0.49/+1.81/+1.40 and
  `marggate`'s `slowswap_r20` +1.81, and costs **`arc_S3` −0.45** — 38% of what
  `AUDIT_BAR_FRACTION` was introduced to buy, on the cell it was bought for — plus `cp_w050` −0.27.
  At the shipped 0.45σ the same arm costs `arc_S3` −0.01 and gives the `slowswap` prize back. The
  whole distance between those two outcomes is one multiple inside one family, not two mechanisms.
- **`revfl100`** (the same floor at 1σ): `arc_S3` −0.42, i.e. the full `floorrev` loss. The cliff
  between the shipped 0.45σ and 1.0σ is why the level is not worth re-deriving.

### Marginal steering on both sides

The round-2 kill pointed at `ln(d_windowTail / d_probationTail)`, marginal on **both** sides,
keeping probation as the denominator while measuring its margin rather than its bulk. It was built
in two sizings (`margboth`, proportional `δ·W` against `δ·P`; `margeq`, equal capacity on each
side so the denominators cancel to `ln(H_wTail / H_pTail)`), and both are dead. But the result
that matters is not the arms.

**Neither beats the one-sided form.** Nine real cells at N=8: ship +0.00 / 0 losses / Spearman
+0.950 / 21.3pp level error; **`marghalf` +0.83 / 1 / +0.983 / 11.3pp**; `margboth` +0.66 / 3 /
+0.967 / 14.0pp; `margeq` +0.69 / 1 / +0.967 / 13.0pp. A second band on a second deque, for
strictly worse numbers. The two sizings land together, so proportional-versus-equal-capacity is
not the question.

**The frontier.** Averaging the two `slowswap` rows against that screen, the four denominators
trace **one monotone inverse curve**: `marghalf` 30.40 / +0.83, `margeq` 31.74 / +0.69, `margboth`
34.92 / +0.66, `margtail` 36.92 / +0.43, ship 41.32 / +0.00. Spearman **−1.0**, slope ≈**16pp of
`slowswap` per 1pp of corpus**. A denominator does not escape the trade, it only picks a point on
it.

**Gain is not the lever, and the family has a ceiling.** `marghalf` swept over gain 0.125–1.0 on
`slowswap_r20` reads 32.90 / 30.38 / 30.02 / 29.65 / 36.20±5.23 / 31.12: non-monotone, unordered,
its one high point a lottery. And `margtail`, the family's best `slowswap` point, reaches only
37.52±0.18 at its best gain. Across the ~14 (denominator, gain) points measured on that row the
best mean is **37.52** and the best single seed anywhere is **38.67**, against a **≥40** bar and
ship's 42.19.

So the thread does not close because arms failed; it closes because the family has a **measured
limit short of the gate over both of its free parameters**. What remains untried is outside it:
every remedy so far picks a different denominator, while the `slowswap` diagnosis names probation's
*composition* under heavy inflow. A regime-gated fallback is a different shape from round 1's
rejected starvation-gated one, but price it against ~+0.24pp, the corpus prize the frontier leaves
at the bar, not against +1.22.

### Marginal steering: the signal is real, the controller is not shippable

The gated arm is the family's best point and its residue redirected the thread. `marggate`
(fall back to the average law where `probationDensity` exceeds `mainDensity`, a trustworthiness
test on two terms `Reading` already computes) reads **+0.690 mean over the nine cells with one
−0.04 loss**, rest-point tracking Spearman **+1.000** with 12.2pp level error against ship's
21.3, and sits at the same `slowswap` robustness as dead `margtail` for 60% more prize. Its
structural false positive: on `flood_j100` a rotation exceeding protected demotes every promoted
key before reuse, probation reads richer than main with no inflow at all (47 of 48 steering
samples), and the arm is the average law there. It fixes `slowswap_r1` (5.6pp below LRU →
38.24 ± 0.18, +1.45 above, deterministic) and not `r20` (−6.9 → −0.95, bimodal); neither meets
the ≥40 bar. **The residue is recovery, not detection**: per-seed on `r20` the gate fires 11% of
steering samples in the good basin and 9% in the bad one, firing rate does not predict the
outcome, and what separates the basins is where the window ends up (30.5% against 20.0%) — the
depressed-window thread's territory. Dead within the gate, do not re-derive:
a run-to-enter requirement (streaks 2 and 3 both lose to 1), an exit latch (non-monotone, never
lifts `r20` above LRU by more than 0.00 over 1/2/4/8/16), and the gate ratio (the two rows want
opposite settings: 1.0 gives r1 +1.45 / r20 −0.95, 0.6 gives r1 −0.62 / r20 +1.01).

`margrest` (the marginal law only inside a rest band, `|steeringError| <= REST_BAND`) solves
`slowswap_r20` outright (42.20 ± 0.05 against ship's 42.19, deterministic) **and destroys the
prize** (nine cells +0.133 against `marghalf`'s +0.833, `w098` collapsing +4.33 → +0.35). The
mechanism is the wall the record now names everywhere: `w098`'s win *is* a rest-point
correction, but a large one — the valuable corrections are long relocations, and a long
relocation is indistinguishable from a transition by command magnitude. No size-gated rule can
work, and the same fact underlies the frontier: the signal cannot separate "a big move because
the rest point is wrong" from "a big move because the workload changed". Two structural facts
fell out: the average-law error is bimodal (near zero at rest or large in transit; bands 0.5
through 0.1 are bit-identical), and on the bad `r20` seeds the final third is a park, 95 of 103
samples at a 16.9% window with one audit armed — the marginal law digs a hole, the anchor parks
in it, and nothing in a steering law climbs out of one. The stillness-coupling blocker is
repaired entirely by halving the gain (a threshold on the stable band, not a monotone trade;
0.25 is worse than 0.5); the probation-denominator defect under heavy inflow is what the
denominators above could not fix.

External corroboration (OSDI'26 "Learning-Augmented Heuristics", Xia et al.): their model's
feature importance puts **75% on the three queues' hit-position histograms** — per-region
hit-rate-versus-size curves, i.e. marginal-value estimators — over 4140 production traces.
Corpus-scale evidence for the premise from outside the project. `d_tail` is one bin against
their 20 per region, so bin resolution is the knob if a future arm reads ambiguous; nothing
else in the paper transfers (it is a static per-trace configuration selector). **Screen any
successor on rest-point tracking before spending a battery**: Spearman(meanWindow, peakWindow)
over the nine D3 cells reads ship +0.917, `marghalf` +0.983, `margtail` +0.367, and a candidate
that does not beat +0.917 is buying stability with tracking — trap wins paid for by corpus
losses.

The thread was closed against a corpus prize under a point. The LLM prefix-cache cells (§3, terrain
classes) are real traces where the average law's rest error is most of a 3–7pp loss: on the static
anchor it is bistable on Thinking@16384 and rests at the floor or near 40% on Coder@65536, where the
marginal form rests near 71% and at the ceiling against 80% peaks. A re-opened thread prices
against `/climber-gate`'s prefix-cache row as well as `slowswap`.

### Verdict corroboration, and the delayed entry stride

**Verdict corroboration, the smoothed reference (`corrob2`; the raw form died first).** A
pulled-back confirm must reproduce its own claim (`Walk.bestRate`) in the smoothed rate within a
confirm-streak of samples, or the park relocates to the walk's end. It does what the raw form
did — `crashnoise_a12`'s low basins lifted (+1.03 mean, the row near-deterministic at 62.7) and
`whisper_mod_p12` +0.64 on 8 of 8 — and fails `whisper_mod_a12` the same way (−1.67 mean, seven
seeds at −1.98, seed 7's low draw the one gain): at that amplitude the EMA still carries ~40% of
the modulation, the park's first samples land in the trough, and a correct position fails its
own claim. The reference is not the defect; the depth is. The smoothed form also adds
relocation damage the raw one did not have: `demoflood` **−3.64 on 8 of 8** and `crestpast`
−4.2 / −2.9, where the lagging EMA reads a fresh crest park as unproven and revokes it to the
walk's end, re-deriving the exact overshoot the verdict repair removed, plus `straywall2`
−0.78. Dead. Both measurable references die by the same symmetry, and the third (the fallback
position's own rate) is unmeasurable without going there; the corroboration family is closed.

**The delayed entry stride (`delayentry`, item 8's blank).** Freeze the base at the arm and
take the entry stride on the next sample, the WaveCounter comparison. The prior held and the
mechanism sharpened: the entry's timing is itself phase-load-bearing. `absolve` reads **−15.7
on both seeds**, back at its pre-repair value, because the lure-paced escalation needs the walk
to arm in the off phase and stride before the on phase returns; `straywall2` −0.88;
`norank_rep_r6`'s escape lottery reshuffles violently (seed 3 +48.6 against seed 7 −59.8, mean
−2.2); against `blindlock_blind` +5.8 and `demoflood` +0.7, which do not buy that. Dead; the
record's blank is priced in the direction the prior expected.

### The marginal band priced against main's average

`ln(d_tail/d_main)`, the marginal band priced against main's **average** instead of its probation
margin, was the named answer to the one marginal-steering residue that halving the gain does not
touch. It is dead in both directions, and the reason was derivable from §4 before it was built.

**It halves the prize.** On the nine D3 cells `margtail` is +0.43 mean with **four** losses
(P9 −0.41, P10 −0.55, w081 −0.64, w060 −0.75) against the full form at half gain `marghalf`,
+0.84 with one.

**It fails the gate it existed to pass.** Seeded at N=8, `slowswap_r1` reads 36.31±0.92 and
`slowswap_r20` 37.52±0.59 against a ≥40 bar and ship's 40.44 / 42.19. It recovers ~63% of
`marghalf`'s slowswap residue, not the ~89% the static rest-point anatomy implied (1.69 against
14.63pp), and still lands 4.1–4.7pp low. Two side results: the declared trade was wrong, since
`trickle_s7` **improves** (72.59±1.30 against ship's 70.70±3.63); and `widepin` is the arm's one
real win, 56.32±7.00 against ship 49.58±13.73 and `marghalf` 48.12±14.03.

**The mechanism, which is the part worth keeping.** A steering denominator must move with the
window or the rest point stops depending on the window. `d_prob` moves (a grow squeezes main and
demotes protected entries into probation); `d_main` is dominated by the protected core and barely
moves, so the error degenerates toward `ln(d_tail) − ln(const)`. Measured directly as rest-point
tracking across the nine cells, Spearman(meanWindow, peakWindow) is ship **+0.917**, `marghalf`
**+0.983**, `margtail` **+0.367**, with mean |window − peak| 23.0 / 14.9 / 19.2pp: `margtail`'s
middling level error is range compression, not accuracy, and it rests at 12.6% and 13.0% on the two
cells whose optima are 40% and 70%. That is the same fact §4 records for the sibling mechanism, an
up-probe's verdict being priced against probation "rather than its average", and `widepin` is it
read from the other side, since an arm that does not track cannot chase a pin. **An arm that wins
on the traps and loses on the corpus is buying stability with tracking; screen the tracking
statistic before spending a battery.**

### Repairing the reactive law instead of replacing it

Can the reactive law be repaired instead of replaced — specifically by giving its reversal a
noise band, as `nextStride` gives the walk's? Four forms were built and measured against both
incumbents: a flat 5pp band, the walk's priced band (`clamp(3·dev, 5pp, 15pp)`), the same with
the density tier's 2% floor, and `probeEnding`'s base reference (measure from the run's best
sample, ratchet on improvement) with and without aging. All dead, and the reasons are structural
rather than tuning.

- **A band on the reactive reversal, any form.** Real traces: mean **+0.16pp** vs reactive over
  seven cells, range +4.06 (cp_w061@64k) to −2.81 (arc P8@64k), never closing the gap to the
  shipped machine (mean −0.73). Constructed families: **five of six lose**, mixture d050 long
  −25.89, mixture d025@32768 −25.29, straywall2 −12.24, whisper −10.53, demoflood −3.37. A band
  buys commitment, and commitment without an adjudicator is a direction chosen by early noise
  that nothing can revoke — the same wall v7's travel budget and v9's slam-continue hit from the
  probe side. The reversal *is* the reactive law's "confused but never persistently wrong"
  property; a band spends exactly that.
- **A band with no window floor is catastrophic, and this is a standing constraint on the tier.**
  `BoundedLocalCache.decreaseWindow` floors the window at one entry; the 2% floor is climber-side
  in `Reading.floor` and belongs to the density tier alone. The reactive tier has never needed
  one because a bold driver cannot sustain a run. Widen the band and the window walks to ~0,
  where TinyLFU refuses every new entry against an established victim: corda 30.96 → **1.13**.
  Any future change that lengthens a reactive run — a band, a longer period, a confidence gate —
  must bring a floor with it.
- **Aging the base reference** (decay the peak toward the live rate at 0.2/sample): inert on four
  cells, −0.5 on two, and on arc P8 it collapses to the un-ratcheted band's value exactly
  (42.57 vs 46.90). Third independent kill of reference-aging, after the anchor claim's two.

Two results from that study are **not** kills: the magnitude deficit is
partly a reversal-rule problem rather than purely a signal one, and the trap gallery's value is
partly variance reduction that N=1 comparisons hide.

### Guards derived from the control-theory map

The control-theory map (§2.1) suggested two guards that could be *derived* rather than tuned.
Both were built behind variant knobs and both are dead. The holdout frozen for the study was
never spent — neither arm reached a ship gate.

- **Freezing the walk's deviation reference at arm** (`devfreeze`): `walkInteriorBar` prices a
  starvation probe's crash abort at `3×rateDeviationEma`, and because `updateRateReferences` runs
  before the probing branch, the walk's own transient inflates the very statistic that decides
  whether to abort it. That description is accurate; the inference that it is a defect is not.
  The feedback is **load-bearing**: on a dosed workload the deviation at arm is a *pre-dose*
  value, so a frozen bar sits too low and the walk crash-aborts inside the dose — exactly the
  failure the adv3 probe-side pricing exists to fix. Measured: `mixnoise_a10` 60.70 → **55.97**
  (−4.73, spread 0.01, fails its ≥58 bar — the pricing's own prize row) and `crashnoise_a12`
  63.59 → **62.18** (−1.41, spread 0.01, drifts below its 63.6 floor); `devfreeze_nocap` is worse
  (`widepin` −2.48, `phases_d050` −2.75). The live deviation and `PROBE_BAR_CAP` are a matched
  pair — the feedback keeps the bar above the weather while the walk is exposed to it, the cap
  bounds where that would run away. Don't freeze it, and don't drop the cap.
- **A within-sample confidence dead-band** (`deadband<z>`): suppress a steering step whose error
  is inside `z·√(1/H_w + 1/H_m)` (the delta method on two counts — free from counters in hand),
  as a derived answer to F-2 and, since a step never taken cannot move the window, a derived
  stillness band. Dead twice over. **It barely binds**: at a 4×C sample period each region earns
  thousands of hits, so 3·sd is ≈0.04 nats and the step that commands is ≈0.13% of the maximum —
  measured mean **+0.03** across 40 non-bimodal cells, two cells past ±0.2. **And its motivating
  case is not a significance problem**: on `posjam_d0`, **0 of 121** super-band moves have an
  error inside its confidence interval — the jam's motion is statistically *real* and window-
  *irrelevant*, so no confidence test can act as a stillness measure (the dose-matched flat
  control is the mirror image, 5 of its 11 super-band moves inside the CI — it binds exactly
  where a defense is not needed). The `posjam` residual stays a stillness-*measure* problem, not
  a noise problem.

### Ageing an off-anchor claim instead of freezing it

Both forms of the thread's other named fix direction — letting an off-anchor claim **age** toward
the live rate instead of freezing — fix the target and are dead for different reasons. The landed
fix re-seeds the goal metric on a discarding stand-down instead.

- **Symmetric aging** (`ageclaim<pp>`: `anchor.rate` decays toward `rates.smoothed` while the
  window is away and settled): −11.9 on the `slowswap_r20` ramp control, on every one of eight
  seeds (42.14 → 30.29), while fixing `slowswap_r1` exactly as the landed fix does. A claim that
  chases the live rate upward can never fall a `vetoMargin` *below* it, and that comparison is the
  only thing that moves the anchor to a better position: the anchor freezes at a position the
  window has left for the rest of the run (trajectory: park samples 69 → 1, `AUDITCONFIRM` 1 → 0,
  down-probes 7 → 0, steering 128 → 238). Slowing the decay to 0.05 hides it on that trace without
  changing the mechanism.
- **One-sided aging** (`agedown<pp>`: decay only while the live rate sits under the claim): passes
  both slowswap arms, battery mean −0.067 with no seeded mover, and still dead — it **disarms the
  guard rail**. The rail needs `VETO_STREAK` consecutive rail evaluations of
  `smoothed < rate − vetoMargin`, and aging closes that gap at the same rate `rateDeviationEma`
  decays, so the shortfall and the margin shrink together and the streak never completes; both
  rail pins fail
  (`guardRail_sustainedShortfall_vetoesBackToAnchor`,
  `guardRail_marginTracksNoise_silentWhileWideThenVetoes`). The rail is measured near-dead on this
  battery, so the live cost is small — but a compensating fix that trades a documented mechanism is
  the wrong shape when the same defect has a source-level fix that leaves every pin standing.

### Park persistence, and other confirm-side levers

- **Unconditional park persistence** (a parked confirm never crash-releases): captures the full
  w097/whisper-p6 prizes but converts the tracking controller into hold-and-retest exactly
  where tracking is the cure — regimeramp 54.8 → 48.5, widepin 54.8 → 48.5 (below its bar),
  phases@8192 −4.1. The bounded fresh-park shield (one initial audit wait) keeps the prizes
  with all three at ship values — the shield length is load-bearing, not a tuning residue.
- **Deviation-priced audit margins generally** (the F1 mitigation table: `flat`/`ema`/`cap`
  margin modes recover only 25–35% and trade the rail for the audit) — superseded by the
  run-length streak; kept here so nobody re-prices the confirm by any multiple of
  `rateDeviationEma` again.

### Blending the two steering laws

- **Confidence-gated steering blend** (`(1−w)·density + w·reactive`, `w = W·min(1, |ΔHR|/devEMA)`):
  the T1a form. Whisper −1.1, jam-control −3.4, and **zero rescue on its own targets** — the
  density attractor undoes uncommitted per-sample nudges exactly as §T1's coin-flip analysis
  predicted; a confidence gate does not change the physics. Committed walks (probes, audits) are
  the only instruments that cross basins.
- **Settle-then-judge** (hold a walk's final position ≤5 samples, adjudicate by live-margin rate):
  demoflood +6.6 but phases −8.4/−11.5 dose-responsive — an exogenous phase rise credits the
  held position (misconfirm) and plants a wrong anchor. Any goal-metric confirm must survive the
  phase-alternation families before shipping.
- **Goal-metric senior confirm with margins frozen at arm**: no effect — the margin freezes an
  inflated deviation and the anchor reference is stale by construction on noisy traces (the 3·dev
  ratchet bar never fires at dev 0.03–0.07; measured 130-sample-stale on cp_w097_16384).
- **Dwell-gated anchor ratchet** (`auditClock.stillSamples ≥ 2` before the anchor may move): no
  co-tenant help; widepin re-destabilized.

### Audit attribution: headroom without a deployable rule

These findings constrain future audit and retention changes. Reusable demand inputs and commands
are in `/audit-regret`'s [targeted controls](../skills/audit-regret/SKILL.md#targeted-retention-controls).
Experiment reports, raw runs and controller-specific replay instrumentation are transient;
the design conclusions and controls below are the lasting record.

Paired replay at `0494074819b08f8c6076f40a544c882beeeac2d0` found material avoidable
misses, but neither tested decision-time discriminator justified a production candidate. The
selected real, trap and lifecycle panel used the real cache, fixed-probation geometry, admission
seeds 1–8 and request/contest-indexed randomness; it does not establish corpus-wide saturation.
For the comparisons below, C is capacity and the horizons are demand requests after the decision.

- **Retention can repay an initial loss.** On `jumpslide` at C=8192 from the shipped start,
  routing the first unparked starvation confirmation through the existing park path costs
  4,176 additional misses over 16C (−3.19pp), then saves 93,460 over 128C (+8.91pp) and
  193,000 over 256C (+9.20pp). Each sign holds on all eight seeds. These are paired means for
  one intervention, not a rule for recognizing a persistent reuse band. The `cp_w050` case in
  §3 likewise establishes a feasible gain without establishing a false rejection.
- **A pre-existing decline does not justify tolerating the first audit crash.** The screen
  snapshots whether the arm sample already fell by at least 5pp, then grants the existing
  bounded three-sample retry at the first crash. At 128C it selects `cp_w050`'s +0.99pp and
  also selects −0.73pp after whole-cache invalidation and −1.71pp after hot-key invalidation
  on the 55% `mainsat` plant, each on eight of eight seeds. Weather preceding the walk is
  evidence about cause, not sufficient evidence for continuing it.
- **A raw shortfall does not establish an obsolete return claim.** Rejecting a passing retest
  when its raw rate is below the frozen claim by 1pp and has just fallen by at least 5pp
  costs `cp_w081` 0.23pp and `crashnoise_a12` 7.73pp at 128C, on all eight seeds. Both screens
  were specified from baseline observations before their paired outcomes were disclosed;
  neither was repaired by adding predicates to exclude its losing controls.
- **Some hindsight verdicts require unseen future information.** `jumpslide` from a 60% plant,
  with only its rotation period changed from 13 to 26 nominal samples, has 425,984 identical
  initial requests. Every first audit crash occurs at request 262,152, with identical cache,
  controller and admission history in the two futures. Tolerance at 256C averages −0.65pp
  for period 13 and +5.61pp for period 26; five seeds require opposite actions. The latter
  wins only 5/8 seeds, below the study's 7/8 consistency requirement. No history-only rule
  can distinguish those futures at that decision. This does not rule out acting later, once
  the unchanged cache has received distinguishing evidence.
- **Lifecycle misses can be unavoidable.** At C=8192, empty caches with fixed write TTL covering
  C requests on a request-based clock and loading only on demand misses matched an independent
  TTL-only reference on `mainsat` and a 512-key hot-set/scan control. The reference's live entries
  fit in C, and neither cache performed a size eviction.
  Resizing cannot reduce those misses. This is a scoped lower bound, not an inference from
  underfill or an expiry counter, and it does not price transfer CPU or cover other lifecycle rules.

Keeping an allocation fixed for the whole suffix also suppresses later audits, so its gain
cannot be charged solely to the first excursion. A short-horizon gain can reverse through later
scheduling: first-crash tolerance on `cp_w100` reads +1.16pp at 16C and −1.88pp at 256C.
No production candidate or full adoption-gate pass followed from this study.

### Later retention: useful evidence before the benefit is lost

The focused follow-up at the same commit used independent unchanged-baseline prefixes at
1, 2, 4, 8 and 16 nominal samples after the first unparked starvation confirmation. A nominal
sample is 4C requests. Each action uses the first eligible ordinary density-steering decision
in its registered one-sample window, after higher-priority routes; no earlier alternative park
supplies the later state or evidence. Endpoints remain fixed to the original episode.

- **Waiting need not consume all the benefit.** At the common 128C endpoint on `jumpslide`,
  current-position parking at delay 8 saves 67,242 misses (+6.41pp, 7/8 seeds), about 72% of
  early parking's benefit. Delay 4 saves more than the early action, so remaining benefit is
  not monotone. At delay 16, parking the current allocation saves only 0.36pp, while returning
  through the real capped-transfer path to the earlier confirmed allocation still saves 6.46pp.
- **A later signal can distinguish some harmful parks.** The two initially tested
  earliest-trigger rules failed controls. A single timing hypothesis was then frozen from
  discovery: evaluate persistent regional support only at delay 8. The signal, S1, required both
  current and preceding baseline samples to stay within 0.02C of the confirmed window, with
  window hits per sampled request no more than 1pp below the confirmation's contribution and raw
  hit rate no more than 5pp below its raw rate. Only the current sample's ordinary integer density
  command had to shrink. Confirmation used `jumpslide` workload seeds 11/13 and admission
  seeds 9–16, held out until that rule was frozen. All ten selected actions saved misses at 64C,
  128C and 256C. The complete seed-13 cohort has seven gains and one abstention: +7.46pp at 128C,
  retaining 93% of early benefit.
  Seed 11 has three triggers, four abstentions and one missing eligible window; it is not a
  complete eight-seed pass. The signal rejects seed-11 actions that would lose 2.39 and 1.26pp.
- **Missing eligibility is not discrimination.** Phase durations 20C/24C supplied no eligible
  delay-8 windows. A separately registered 18C/22C extension supplied 13, all rejected by the
  frozen signal; ten forced parks lose misses at 128C, eight by more than 0.5pp. Those individual
  paired losses are valid controls even though missing seeds prevent an eight-seed group gate.
  Across the 48 fresh registered cases, only 28 had eligible observed decisions: ten selected
  gains, thirteen avoided losses, three missed gains and two neutral abstentions. The signal
  recognizes a useful subset, not every retention opportunity.

This establishes a useful tested later point, not a continuous safe interval, a general timing
optimum or a production improvement. The 104-baseline, 403-pair study verifies feasible loss
avoidance and baseline-only information; repeated candidate behavior and the existing adoption
gates must be measured separately. The real `cp_w050` control has no qualifying episode on
any seed, so it supplies no retention-effect or discrimination claim.

The resulting isolated candidate kept the first pending confirmation's independent window,
raw-rate and window-contribution references. Later confirmations did not replace them or postpone
the eighth-completed-sample deadline. Assessment required final ordinary steering, no incoming
transfer adjustment and no pending retest. Priority routes consumed the deadline without a retry;
shifts, walks, parks and releases did not cancel it. `resetSample` canceled it, and an actual
maximum change replaced the tier. A true S1 planted the current window/current smoothed rate,
used the existing `park(32)` shift shield, and preserved the previous step rather than committing
the unexecuted integer shrink. The episode then expired and a later confirmation could rearm.
It preserved the existing router, audit, release and retest machinery. First-episode validation
matched the diagnostic across 104 cases: 67 eligible decisions, 24 ineligible expirations and
13 absent episodes. In the canonical seeded-stream battery, 92 of 94 cells had identical counters
and trajectories. `jumpslide` gained 2.63pp (five gains, three ties), and `whisper_quarter` moved by
less than 0.005pp per seed. These are separate admission
schemes and cohorts; their deltas are not pooled. Three historical bars (`deadphase`,
`sidecliff`'s `noaudit` control and `whisper` at 16384) failed identically on the study baseline
and candidate. The paired comparison found no new behavioral regression; the literal historical
bar results remain separately qualified.

**Longer continuation still matters.** On the previously held-out `jumpslide` workload seed 13,
admission seed 14 parks once, saves 25,085 misses at the original 128C endpoint and 8,926 at 256C, then
ends the 260-sample trace 234,178 misses behind (−2.75pp over the whole trace). Its complete
hit sequence matches the earlier one-action replay, so repeated rearming did not cause that
reversal. All 120 registered TTL/invalidation cases were unchanged, but the matched no-expiry
period-26 `jumpslide` control from a 60% plant lost 35,363 misses on one seed (−0.83pp; the
other seven tie). Its initially beneficial park changed later audit/release behavior and ended
at a ceiling park. A retention action includes those later consequences, not just the time it
keeps the original allocation.

Keep all known full-continuation costs visible, including losses allowed by the group screen:

| Workload / admission seed | Retention parks | Whole-trace extra misses |
|---|---:|---:|
| `jumpslide`, workload seed 13 / admission 14, 260 samples | 1 | 234,178 |
| `jumpslide`, workload seed 13 / admission 10, 260 samples | 1 | 64,698 |
| `jumpslide`, workload seed 11 / admission 14, 260 samples | 1 | 104,524 |
| `jumpslide`, workload seed 11 / admission 15, 260 samples | 2 | 13,505 |
| period 26 `jumpslide`, 60% plant / admission 1, no expiry | 1 | 35,363 |

These are request/contest-PRF, direct-executor results, not canonical seeded-stream or ordinary
ThreadLocalRandom expectations. Both seed 13 losses match their earlier single-action continuations.
The period 26 control's seven peers tie, for a group mean of −0.103769pp. The registered rejection
requires at least 0.5pp mean loss, C/4 mean extra misses and 7/8 negative seeds; none of these groups
meets that rule. That does not erase individual costs or create an any-negative-seed rejection rule.

The candidate was not adopted. On the tested Temurin 25/macOS arm64 JVM, the Java
11-compatible implementation added 32 retained bytes per density-tier cache and no additional
retained object. Direct density-tier JMH measured about 5ns more per ordinary sample and 3ns per
sample over the controlled retention cycle. Whole-cache throughput remained inconclusive: baseline
fork variation and wide paired intervals did not resolve small costs.
Neither those intervals nor a cold-path location establish zero cost or an unacceptable cost.
This is a demonstrated later opportunity with unresolved adoption gates, not saturation or a
production improvement.

The bounded runtime follow-up also ended inconclusively. Its 32 source-identical baseline forks
(four pairs in each of four whole-cache cases) resolved neither the registered ±2% sensitivity
target nor a directional build effect. They were not candidate regression measurements. The
archived 2% sensitivity floor, 5% noise ceiling and 0.01 B/op flag floor are diagnostic rules;
no numeric engineering acceptance budget was found. That budget and a qualifying measurement
setup are prerequisites to new candidate classification. No additional candidate timing or
reserved-holdout outcome checks followed from this unresolved result.

A later integration-only follow-up passed 1,499 unit/integration invocations and one
five-minute fuzz campaign (2,228,167 executions), with no candidate defect found; specific retention
branch exposure in fuzzing was unmeasured. One of two ordinary-admission/direct-executor observer
runs naturally armed, parked at sample 72, audited at 99 and released at 109. Neither of two default-
executor observer runs parked; clean counterpart action counts were unmeasured. Default mode had
one demand producer and background common-pool maintenance, not concurrent demand producers.
Source stripping and controlled observation checks do not establish exact concurrent histories
or CPU neutrality. A same-maximum policy call still runs maintenance and may age a completed
sample; tier preservation does not mean all controller state is unchanged. Likewise, quiet
completion and lifecycle operations add no cancellation rule but may process prior observations.

The 17 real/floor cells and their registered quarter, repeated and planted controls made 42
paired cells at eight admission seeds each. All counters and trajectories matched, and the
retention action never fired. The existing recovery bars held, but dormant behavior supplies
neither a real-corpus gain nor evidence that an active retention decision is safe there.

**S1 was closed without adoption on 2026-09-08.** Its source/test snapshot was discarded; the
decision-time findings, known losses and reusable workload controls remain the lasting result.
Runtime acceptance and final reserved checks were not established: ARC P3 was unspent and the
referenced hash-pinned merge specification was not located. Closing the candidate does not turn
those unknowns into a performance rejection or establish that no useful improvement remains.
A future investigation can recreate an experiment against the current source using the targeted
controls and §6's decision-replay protocol, with its own source binding and validation.

## 6. Methodology (the discipline that kept this honest)

- `product.Caffeine` in the simulator is the arbiter — the sim reference climbers diverge from BLC
  (ps2-vs-x2 history). Randomized admission gives ~0.1–0.8pp run-to-run noise: N≥5 runs for any
  claim under 2.5pp, absolute pp only, no net-sum headlines over noise cells.
- **Where that noise comes from, and why some cells are bimodal.** The source is
  `BoundedLocalCache.admit`: a candidate at or above `ADMIT_HASHDOS_THRESHOLD` that does not beat
  the victim is admitted on a 1-in-128 `ThreadLocalRandom` draw (the HashDoS defense). TLR seeds
  from system entropy per JVM, and the policy runs `.executor(Runnable::run)`, so this is the only
  live nondeterminism. The experiment harness can replace that contest-time draw with a seeded
  stream, which makes each arm independently reproducible. Equal seeds do not make two arms use a
  draw on the same request when their contest schedules differ. The arms are paired by seed, not
  by request-indexed common random numbers, unless both the draw count and request-index digest
  match. On most cells the draws average out
  (measured over five batteries: 28 of 47 cells vary by <0.05pp run-to-run, 37 by <0.20pp). On a
  constructed bistable trap an early draw can tip the region composition into the other attractor
  and the climber's own feedback locks it in, which is what a **lottery cell** is: 6 cells
  (`phases_d050`, `widepin`, `shieldtrap_s7`/`s13`, `balloonflip`, `crashnoise_a12`) vary by
  1.1–5.5pp with the code held fixed. Consequences for reading a gate: a mover inside that set is
  evidence of nothing, interleaving arms limits temporal drift but does not make their draws
  common, and a single-cell regression is adjudicated by basin count at N≥10, never by a mean at
  N=2.
- Adaptive N is sound and ~60% cheaper: N=2 everywhere, N=5 where |Δ| ∈ 0.5–2.5pp or spread >0.5.
- **Lengthen a trace with fresh keys, not a plain replay.** Replaying a trace back to back lets the
  frequency sketch remember every key from the previous pass, which flatters small windows and can
  trap the climber there: on the LLM prefix cells a static 1% window gains 4.8pp on To-C@185364 from
  that memory alone, and under plain replay the shipped climber stays small where with each pass's
  keys mapped onto new ones it reaches LRU or the static 80% window within two to four passes.
  Check a multi-pass claim against a fixed policy over one and over N passes before trusting it.
- **Pair usable-prefix scores with the legacy block scores.** A request's usable hits stop at its
  first missing block (§3, terrain classes). Preserve exact prefix-chain keys and real request
  boundaries, and score both measures on the same paired replay. LRU's equality in the studied
  setup must be checked, not assumed for every workload or serving model. Do not silently stop
  the access stream at the first miss, add request pinning, or replace historical gate bars;
  those are separate replay or acceptance changes.
- **Test fill transitions rather than naming them from a drop.** A hit-rate drop near first
  eviction can motivate a fill-transition hypothesis, but admission, sketch initialization/aging,
  capacity pressure and workload phase can change together. Isolate those effects before
  attribution; a filling cache is not unconditionally equivalent to a large LRU window.
- Trajectories (window size per adaptation) are the diagnostic instrument — every fix in §4 was
  found by reading a trajectory, not a scoreboard. The debug hook pattern: stderr print in
  `determineAdjustment` behind a temporary system property (the simulator forwards `caffeine.*`);
  no hook is checked in — the 2026-07 study's working-tree harness (variant knob + debug + seeded
  admission RNG) is preserved in the
  local hill-climber-study workspace (`experiment-harness.patch`).
- **Separate information, actuation, and deployability.** First ask whether a signal predicts reuse
  or ranks counterfactual window sizes on a declared observation stream. Use past-only features,
  complete negative labels, cold-start accounting, authentic frequency and equally deep no-feature
  controls, and bounded state/work. Check which observations survive generic maintenance; metadata
  recovered offline does not make it available there. Reuse prediction alone establishes neither
  when to trigger a different mode nor which window direction helps. Then ask whether a controller
  moves the live cache safely and converges under clamps, reversals, and phase changes. Only then
  price the actual production host for retained state, steady allocation, command/boundary
  allocation, and CPU. Passing one layer says nothing about the next: replicated MiniSim passed its
  information screen and failed cost, while Compact Shadow passed layout and stopped before quality.
- **Distinguish issued commands from applied movement.** An experiment requiring exact arrival
  must keep its decision pending until the host acknowledges the actual signed setpoint change.
  Reject overlapping acknowledgements and the wrong sign or magnitude; retain geometry-specific
  evidence after a rail-clamped zero; clear it only after a validated nonzero move. Continue a
  capped trip from the acknowledged setpoint, not from a nominal percentage or transient queue
  occupancy. Tests must pin partial and zero clamps, reversal, cancellation, and exact arrival in
  both directions. The shipped undo retains
  [best-effort arrival](#reconciling-failed-probe-returns); ledger closure alone does not prove arrival.
- **Use an oracle ladder, not a self-confirming digest.** Compare a compact estimator first with an
  independent readable model, then request by request with the actual simulator policy under the
  same key, frequency history, and admission variate. Check hits/misses, membership, queue order,
  promotions/demotions, and admission decisions, not only final hit rate. Accumulate live evidence
  independently; copying an expected digest into the measured path proves only that the metadata
  agrees with itself.
- **Low duty cycle is not low request-path cost.** A dormant callback, counter decrement, routing
  indirection, or branch still executes on every request. The compact studies measured zero steady
  allocation alongside material CPU regressions. Dormant observation must be structurally absent,
  and even that is only a hypothesis until the exact product splice passes an order-balanced
  benchmark. Warm the exact root, actor, probe, and branch shape being measured, and make estimator
  work externally observable so dead-code elimination cannot manufacture a win.
- **Establish benchmark repeatability before interpreting small costs.** Use ordered baseline
  pairs before candidate pairs, preserve slow forks and report uncertainty. A stable slow fork is
  not necessarily an unfinished warmup. If baseline variation exceeds the registered sensitivity,
  additional measurements may diagnose it but cannot silently turn it into a permissive pass bar.
  Separate cold ns/sample from whole-cache throughput and fixture allocation from new allocation
  in an existing cache; state the sample exposure used for any per-request amortization.
  Bind the entire cache implementation as well as the controller overlay: class origin proves
  location, not that a supplied JAR contains the pinned baseline. Pin the resolved JDK and keep
  different runtime cohorts separate even when their benchmark names and launcher strings match.
  Check activation outside timing; an initialized sketch can process density samples in an
  underfilled cache, while a six-reader/two-writer JMH group does not fix the request ratio.
- **One clock owns each lifecycle boundary.** Host requests, sampled requests, warmup, measure,
  travel, and cooldown are different quantities. Express each transition as one absolute request
  deadline or one remaining count, not two counters that can both charge the same interval. Pin the
  request immediately before and at every edge, and prove that under-full or dormant requests do
  not advance the estimator's active clock.
- **Classify endpoint state before scoring regret.** A trace that ends in a walk or audit measures
  recovery latency over that horizon; it does not prove a settled rest point. Report the terminal
  phase and pending target beside static-oracle regret. For percentile gates, also pre-register the
  monotone futility bound: stop once the observed order statistics force failure under every
  possible value of the unrun cells.
- **A holdout must be able to exercise the thing under test.** `AUDIT_BAR_FRACTION` is
  `min(RESTART_THRESHOLD, AUDIT_BAR_FRACTION · baseHitRate)`, so above a 1/3 hit rate it equals the
  absolute bar and every arm ties by construction. Selecting by "unspent + density tier + enough
  adaptations" is not sufficient: screen the candidate band against the constant's own binding
  condition first, or the freeze spends cells on a null result that reads like a pass.
- Holdout discipline: freeze a fresh holdout BEFORE tuning, sizes by LRU-only characterization,
  score it exactly once at the ship gate — and screen the candidate cells against the constant's
  own binding condition first (above). Every earlier holdout generation is SPENT (each spend is
  recorded in its study's entry and workspace); the density-climber **merge-gate holdout**
  (frozen with a hash-pinned spec in the merge workspace) remains **UNSPENT** — no
  candidate arm may touch its cells before the final pre-merge gate.
- The simulator has NO reference implementation of this climber (the experimental
  density-arm-only `SimpleDensityPropClimber` was retired rather than shipped
  half-faithful); `product.Caffeine` is the only arbiter until a faithful reference
  lands. The climber-class extraction that was to enable one has happened
  (`WindowClimber`), so a reference is now a port, not a redesign — still unbuilt.
- **Judge climber terrain on the object-rate column, not the byte panel.** The weighted anchor
  degenerates bit-for-bit to the classic form on unit-weight traces, so the unweighted record
  stands, but the byte track answers a different question and its win/loss counts move for reasons
  that are not the climber's. A related trap that inverted a real cell before it was fixed: the
  admitter was sized once, when the cache first filled, and never re-sized, while the cache
  re-calls `ensureCapacity` on every addition. Since the sketch's reset period scales with its
  capacity, a frozen one is TinyLFU aging at near-zero period, which flatters *small* windows and
  washes out at large ones.
- **No duty or dwell bound can detect the deferral defects, and that result is worth not
  re-deriving.** The shapes that motivated the search violate the schedule's **provenance**, not
  its magnitude — `maxOverdue` measures zero straight through a pin that lasts over a hundred
  samples — so no elapsed-time or duty bound can see them. What survives the search is three
  oracle clauses, none of them a duty bound: two ownership/evidence clauses (a layer's rung and
  wait grow only on a sample that ended its own walk; the wait passes its floor only on a walk that
  reached a verdict), which carry zero constants and kill mutations quickly but are regression
  value rather than discovery; and one **progress** clause (a due audit is refused for at most a
  bounded number of consecutive samples). The progress clause has real discovery content — on the
  pre-fix tree it fails in seconds and fires on exactly the cells the fix moves — but it was not
  adopted, because its margin sits one sample under the shallowest defect instance and it is a
  mechanism detector whose firings need a human to price, including where the refusal is
  load-bearing. Killed alternates: upper duty bounds (every defect is duty-too-*low*),
  sliding-window lower bounds (permanent false positives), and "consecutive samples commanding no
  motion" (the progress clause in disguise, or blind at the absolute level a legitimate deep park
  requires).
- Simulator operational gotchas: the default `admission` list includes `Clairvoyant`, which wraps
  the trace in a materializing reader (pass `-Dcaffeine.simulator.admission.0=Always` for LRU
  anchors, or run product-only); `simulator:simulate`'s chart renderer wedges headless (run one
  size per call); Twitter/IBM holdout projections live at `~/projects/merlin-traces/holdout/`.

### Decision attribution and later retention

**Establish the replay before interpreting a verdict.** Independently replay the same prefix
into the real cache; compare full policy/controller state, queue order, sketch, node/lifecycle
metadata and pending maintenance at the intervention. An A/A continuation must reproduce demand
outcomes and state. Check observation neutrality separately against the same admission rule with
diagnostic hooks disabled. Do not add cleanup or cache accesses to obtain a convenient snapshot.
If local instrumentation keys admission by request, event phase, ordered candidate/victim and
contest occurrence, verify shared identities and variates and count branch-only contests
separately. The committed seeded-stream harness does not supply that stronger pairing by itself.
Keep study-only hooks isolated; their logs and allocation costs are not production measurements.

**Price the defined action.** Count cumulative misses from demand outcomes independently of
controller samples. Record actual allocation and occupancy, transfer carry-over, sampled exposure,
decision reason and reference values. Share external demand, ticker and lifecycle schedules on
immutable request indices; regenerate each branch's loads, expirations and completions. Use
several predeclared horizons spanning immediate
damage, settling and repayment. Missing decisions, no-op interventions and censored horizons do
not establish zero headroom. A static optimum or a large expiry count cannot label a false reject.

**Later retention is a separate decision-time diagnostic.** Its question is whether substantial
remaining savings and a cheap distinguishing signal coexist at a later baseline decision:

1. Predeclare an original episode event, a small finite set of later offsets or eligible source
   boundaries, skip rules, permissible actions, materiality and controls. Include cases where
   retention is harmful. Select eligibility from the baseline prefix at the exact pre-action
   boundary, without choosing times from subsequent loss, a future phase label or an alternative
   branch's success.
2. Replay the unchanged controller independently through each later point. No earlier forced
   park, hold, tolerance or other intervention may supply its state or evidence. Waiting follows
   ordinary adaptation, even if it loses the good position. A hold used to gather evidence is
   already a treatment. Log which fields were available before the decision; post-route fields
   overwritten by a plant, release or retest are not automatically valid inputs.
3. From that common state, compare ordinary continuation with one specified bounded action.
   Distinguish retaining the **current** allocation from returning to a previously observed one.
   If the position has already been lost, charge real capped transfers, displaced residents and
   settling; do not restore an earlier cache snapshot or assume an earlier park's occupancy.
4. Compare later times at the **same predeclared episode endpoints**, anchored to the original
   event. Keep the misses incurred while waiting and after acting in the accounting. Equal-length
   postdecision horizons can additionally explain settling, but moving every endpoint later would
   grant the delayed action extra regime time. Report absolute remaining savings first; compare
   them with the early-intervention gain at the same endpoint only when that denominator is
   materially positive. Such a ratio can be negative or exceed one and is not a probability.
5. Construct discriminator inputs from baseline-prefix observations alone. Offline outcome labels
   may guide fitting on discovery cases; freeze the cheap rule before testing held-out cases and
   seeds with both helpful and harmful retention outcomes. Identical observed histories must
   produce identical rule outputs until baseline-observable evidence diverges. A difference in
   counters is not enough: it must distinguish the better action while substantial savings remain.
   Future replay may label outcomes, never become a feature or choose a per-event trigger by its
   realized savings.
6. Report remaining savings and discriminator performance against decision delay, including
   waiting, movement and recovery costs. For the tested actions and discriminator, a useful
   interval exists only where both tests pass. Report no such interval if the signal arrives after
   the residents or most of the benefit are lost.
   Do not assume savings decline monotonically, select the best noisy time as recoverable regret,
   or add overlapping event gains. An unexercised or underpowered interval remains inconclusive.

Keep true abstentions separate from missing opportunities: a false signal at an observed eligible
decision is exact baseline continuation, while an absent episode or eligibility window supplies
no action label. Do not require an entire control group's mean to be negative before recognizing
an individually paired harmful action that the signal rejects. Report both group gates and
individual losses. A failed earliest-trigger rule also does not exhaust the registered later
points; a timing hypothesis chosen from discovery needs its own frozen held-out confirmation.

Price the smallest sufficient history. A conjunction of episode-relative support tests can retain
the preceding sample's boolean verdict instead of all preceding measurements, provided it is
updated on every complete sample and reseeded when the reference changes. Keep the current-only
action predicate out of that history. A sampled clock may reproduce a request-indexed diagnostic
on a fixed unit-weight panel without doing so under buffering, weights or lifecycle changes;
verify the equivalence on the actual targets and state that boundary.

Only a surviving discriminator justifies an end-to-end candidate. Freeze its first-trigger and
subsequent behavior, then replay that candidate on its **own** evolving states: baseline event
tables cannot estimate the reachability or gain after earlier interventions. The existing
behavioral, corpus, holdout, invariant and runtime-cost gates still apply. A useful one-decision
signal justifies measuring that candidate; it does not establish the repeated policy's gain or
make unmeasured runtime cost either free or an obstruction.

---

### .Claude Docs Jsr107 Conformance (`.claude/docs/jsr107-conformance.md`)

# JSR-107 (JCache) Conformance Reference

Sources, audit methods, and standing rulings for the `jcache/` adapter. Read the relevant topic
before raising a finding; these conclusions preserve accepted behavior and resolved defects.
Provider comparisons are recorded evidence, not a substitute for checking current source.

## Sources and method

Use the JSR-107 1.1.1 specification and API javadoc. The 1.0 PDF predates changes to
`getCacheNames` iterator behavior, typed `getCache(String)`, and iteration over expired entries.
Compare the 1.1.1 API javadoc before relying on older text; the specification's revision history
records no 1.1 behavior change.

- [JSR landing page](https://jcp.org/en/jsr/detail?id=107)
- [Specification](https://docs.google.com/document/d/1ijduF_tmHvBaUS7VBBU2ZN8_eEBiFaXXg9OI0_ZxCrA/edit)
- Local `cache-api-*-sources.jar`: `javax/cache/**` javadoc and package documentation.
- [Reference implementation](https://github.com/jsr107/RI), version 1.1.1 sources:
  `https://repo1.maven.org/maven2/org/jsr107/ri/cache-ri-impl/1.1.1/cache-ri-impl-1.1.1-sources.jar`.

Fetch the spec as needed; do not bundle it. Cite section or javadoc names because exports reflow.

```bash
SPEC_DOC=1ijduF_tmHvBaUS7VBBU2ZN8_eEBiFaXXg9OI0_ZxCrA
curl -fsSL "https://docs.google.com/document/d/$SPEC_DOC/export?format=txt" -o /tmp/jsr107_spec.txt
```

For an ambiguous behavior, read the spec, inspect what the TCK actually asserts, compare the RI
and at least three other providers, then check Caffeine's sibling paths. Explicit spec wording
outweighs RI behavior; the RI helps resolve ambiguity but has documented bugs. Preserve TCK
interoperability where its assertions are stricter than 1.1.1, and record the distinction.
Use the other providers to check whether the RI is an outlier. When providers split, favor the
spec text and RI interpretation and record the differing results.
Source comments and search summaries are not verified behavior. In particular, prior summaries
reversed Ehcache 3 and Coherence's synchronous listener exception handling.

| Area | Spec section / API | RI source |
|---|---|---|
| Expiry | Expiry Policies; `ExpiryPolicy` method table | `RICache`, `RICachedValue` |
| Events and filters | Cache Entry Listeners; `CacheEntryListener`, `javax.cache.event` | `RICacheEventDispatcher`, `RICacheEntryEvent`, `RICacheEntryEventFilteringIterator` |
| Counters and timing | Statistics Effects of Cache Operations; `CacheStatisticsMXBean` | `RICacheStatisticsMXBean`, counter call sites in `RICache` |
| Loader and writer | Integration; `CacheLoader`, `CacheWriter` | `RICache.writeCacheEntry`, `deleteCacheEntry`, batch calls |
| Entry processors | Entry Processors; `MutableEntry`, `EntryProcessorResult` | `EntryProcessorEntry`, `MutableEntryOperation` |
| Copying | Store-By-Value and Store-By-Reference | `RISerializingInternalConverter`, `RIReferenceInternalConverter` |
| Configuration | Configuration; factories and type enforcement | `RICache`, configuration classes |
| Lifecycle and JMX | Caching Providers; CacheManager/Cache close contracts | `RICacheManager`, `RICachingProvider`, `RICacheMXBean` |

The spec's Annotations section (CDI/Spring) is outside this adapter's scope. The audit skill's
A–N matrix covers the remaining surface. Expiry, events, statistics, integration, and processors
need event/statistic assertions: the TCK often checks only final contents. Do not infer that an
operation's expiry-event column of “No” forbids an independently timed expiry notification.
For statistics, check the operation table as well as broad counter definitions: a read-through
get is a miss, not a put; `clear` differs from `removeAll`; expiry has no dedicated MXBean counter.

### Provider source map

Fetch individual files or shallow-clone small modules. Locate current paths before citing them;
Hazelcast, Infinispan, and Coherence are large enough that a full clone is usually unnecessary.

| Provider repository | JCache expiry entry point |
|---|---|
| `cache2k/cache2k` | `TouchyJCacheAdapter`, `ExpiryPolicyAdapter.calculateExpiryTime`, `durationToTicks` |
| `oracle/coherence` | `coherence-jcache/.../localcache/LocalCache`, `JCacheEntryMetaInf.modified`, `LocalCacheValue.updateInternalValue` |
| `ehcache/ehcache-jcache` (Ehcache 2) | `JCache.setTimeTo` |
| `ehcache/ehcache3` | `Eh107Cache`, `Eh107Expiry` |
| `hazelcast/hazelcast` | `AbstractCacheRecordStore.updateRecord` |
| `infinispan/infinispan` | `jcache/commons/.../AbstractJCache.put` / `replace` |

Find other domains' writer, MXBean, listener, and copier classes in the same modules. Trace
delivery as well as publication: Coherence's UPDATED notification comes through a backing-map
listener, not an explicit event call in `put`. Distinguish local and partitioned variants.

### Validation pattern

Run `:jcache:test` and `:jcache:tckTest` for conformance changes. The latter unpacks tests into
`jcache/build/tck/org/jsr107/tck/`; its `cache-tests-1.1.1-test-sources.jar` is also in the Gradle
cache. Read `CacheExpiryTest` and `CacheMBStatisticsBeanTest` assertions, not just test names.
`CacheLoaderTest.shouldPropagateExceptionUsingGet` and `shouldPropagateExceptionUsingLoadAll`
assert the `CacheLoaderException` wrapping 1.1.1 still requires;
`CacheMBStatisticsBeanTest.testIterateAndRemove` pins iterator hit/removal accounting.

Use a parameterized parity test when sibling operations should agree on events, counters, and
contents. Examples: `JCacheCreationExpiryTest.writeOp_absent_zeroCreationExpiry` and
`JCacheUpdateExpiryTest.writeOp_present_zeroUpdateExpiry`. Establish the expected direction from
the contract first; an existing sibling can itself be wrong. Add a pin only where siblings diverge
or the TCK is silent on an outcome a caller or listener acts on; the TCK's per-operation checks
count as pins, and statistics vectors or event and processor tables for behavior recorded here are
not added for completeness.

## Expiry

### Zero expiry

Creation and update have different contracts. `getExpiryForCreation() == Duration.ZERO` means
the entry is not added; zero update expiry means it is updated and immediately expires.

| Case | Store | Operation event | Put count | Writer |
|---|---|---|---|---|
| Zero creation | Suppressed | No CREATED; EXPIRED carries the new value | None | Still runs |
| Zero update | Store immediately expired | UPDATED, then independently timed EXPIRED | Counted | Runs before commit |

Do not suppress the writer on zero creation. It persists the caller's write intent even though
the cache stores nothing, matching `RICache.writeCacheEntry` before creation-expiry evaluation.
Do not substitute the prior value or null in its EXPIRED event: that would hide disposal of the
new value from resource-tracking listeners. A read-through load under zero creation expiry follows
the same rule: `get`, `getAll` and a processor's `getValue` publish EXPIRED for the loaded value
and count no eviction. The RI publishes it from `invoke` and `loadAll`, but `RICache.getValue`
returns null silently for `get` and `getAll`.

Recorded creation-path comparison, from source on 2026-08-29:

| Provider | Writer before expiry guard | `putIfAbsent` result | Put counted | EXPIRED |
|---|---|---|---|---|
| RI | yes | false | no | yes |
| Coherence | yes | false | no | no |
| Caffeine | yes | false | no | yes |
| Ehcache 2 | suppressed | true | unverified | unverified |
| Hazelcast | suppressed | unverified | no | no |
| Infinispan | below adapter; inconclusive | unverified | no | unverified |
| cache2k | delegated to core; inconclusive | unverified | unverified | unverified |

The RI and Coherence have comments suggesting suppression that disagree with their writer/event
call order; trace the implementation. `CacheMBStatisticsBeanTest.testExpiryOnCreation` pins zero
puts for `put`/`putAll`; `CacheExpiryTest.expire_whenCreated` discards `putIfAbsent`'s result.
Caffeine records a miss for an absent zero-creation `putIfAbsent`, following the statistics
table's prior-presence rule and `getAndPut`; the RI incorrectly derives a hit from `false`.
Pin: `JCacheCreationExpiryTest.putIfAbsent_absent_zeroCreationExpiry_recordsMissNotHit`.

Recorded zero-update comparison:

| Behavior | UPDATED / put counted | Providers |
|---|---|---|
| Store immediately expired | yes / yes | RI, Coherence |
| Remove or suppress | no / no | Infinispan, Ehcache 2, Hazelcast, cache2k |

Measured cells: Ehcache 3.12.0 splits by path (`put`, `invoke` and `loadAll(replace)` store and
publish UPDATED then EXPIRED; `getAndPut`, `putAll`, both `replace` forms and `getAndReplace`
publish REMOVED for the old value); cache2k 2.6.1 publishes EXPIRED for the old value and counts no
put; Infinispan 16.2.3 publishes REMOVED and calls `writer.delete`. Hazelcast 5.7.0 hung on the
scenario, so its row keeps the recorded reading.

cache2k maps a zero update expiry to `ExpiryTimeValues.NOW` (`ExpiryPolicyAdapter.durationToTicks`),
and core's `EntryAction.checkKeepOrRemove` removes an entry expiring `NOW`.

Caffeine follows the first behavior because the spec distinguishes creation from update and the
RI agrees. All put/replace/invoke siblings gate suppression on creation only
(`expirable == null`). `CacheExpiryTest.expire_whenModified` checks only absence afterward, so
both behaviors pass. The parity tests above check the missing event/statistic distinction.

### Policy defaults

- Creation returning null or throwing, a checked exception included, yields eternal expiry
  (`Long.MAX_VALUE`), consistently in `CacheProxy` and `JCacheLoaderAdapter`. Null creation is
  implementation-defined; the RI would NPE. The normal CREATED/put effects still occur. Pin for a
  checked throw: `CacheProxyTest.put_expiryThrowsCheckedException_storesWithDefaultExpiry`. The
  adapter catches `Exception`, so an `Error` propagates; RI, Ehcache 3, Infinispan, and Coherence
  catch `Throwable` around expiry calls, Hazelcast catches `Exception`, and cache2k does not catch.
  Each write consults the policy before it publishes the attempted write's event, counts its put
  or, in the put family, publishes an expired prior's EXPIRED, so the `Error` leaves no event for
  the aborted write, the order Hazelcast and cache2k use. `invoke` instead publishes an expired
  prior's EXPIRED before its processor and commits that removal if anything throws (see
  [entry processors](#entry-processors)). Pins:
  `JCacheCombinedExpiryTest.*_expired_creationExpiryError_expiresOnce` and
  `JCacheUpdateExpiryTest.writeOp_present_updateExpiryError_publishesNoUpdate`.
- Update/access returning null or throwing leaves expiry unchanged. The loader helper takes a
  `created` flag; reload translates the update `Long.MIN_VALUE` sentinel to the old wrapper's
  deadline. Do not turn a finite deadline eternal or log an NPE for an ordinary null update.
  Pins: `CacheLoaderTest.reload_nullUpdateExpiry_keepsExpiration` and
  `reload_updateExpiryFailure_keepsExpiration`.
- Expiration counts in `CacheEvictions`, whether discovered lazily or through native EXPIRED
  removal. RI, Ehcache 3, cache2k, and Hazelcast exclude it, but the spec is ambiguous and offers
  no expiry counter. This accepted choice makes expiry visible to dashboards; do not remove it
  for ecosystem parity.
- An expired entry found by remove/getAndRemove/removeAll emits EXPIRED and an eviction, not
  REMOVED and a removal, and `containsKey` reaps one the same way where the RI only tests the
  deadline. The RI set overload disagrees with its own single-key/no-argument paths.
  TCK pins: `CacheExpiryTest.testCacheStatisticsRemoveAll` and
  `testCacheStatisticsRemoveAllNoneExpired`; the expired set-overload pairing is pinned by
  `JCacheCombinedExpiryTest.removeAll_setOverload_expired`.

### Deadline conversion

`ExpirableToExpiry` subtracts in milliseconds, then converts the remaining duration once.
The wrapper deadline and ticker share an arbitrary origin. Converting the absolute deadline to
nanoseconds first saturates near the signed-long horizon: at `Long.MAX_VALUE - 100`, a one-minute
deadline formerly became 100 ns. The lazy `Expirable.hasExpired` gate was correct; the native
timer was the affected mirror. Custom tickers can reach this boundary, and
`JCacheFixture.START_TIME` exercises the full long range.

Keep millisecond granularity (the native timer may fire up to about 1 ms later), core's
`MAXIMUM_EXPIRY` clamp, and call-site `Math.max(0L, ...)` handling of expire-now. Keep the ticker
read rather than using core's `currentTime`: dropping it was reverted because auto-increment
tickers shift observed times. Pin: `JCacheExpiryTest.nativeDeadline_nearNanosecondSaturation`.

Loader creation expiry uses the same ±1 correction as write/access expiry when a finite deadline
collides with sentinel `0` or `Long.MAX_VALUE`. Pins:
`CacheLoaderTest.load_adjustedTimeSentinelZero` / `load_adjustedTimeSentinelMax`.

The millisecond deadlines and their sentinels (`0` expire-now, `Long.MIN_VALUE` unchanged) assume a
clock that neither reads below zero nor crosses the signed wrap, and durations far below
`Long.MAX_VALUE` milliseconds. The eternal deadline is the same boundary: at a clock below zero an
unguarded `hasExpired` wraps it into expired, so an eternal replacement or an in-place renewal to
eternal seen by `removeExpired`'s re-check is removed. These boundaries are accepted without a fix.
The recorded witnesses used custom boundary tickers; `System.nanoTime` documents that its values may
be negative, so the system ticker can also read below zero, at a frequency that is unmeasured.
JCache cannot express a lazy expiry below a millisecond: `Duration` rejects smaller units, and
`TypesafeConfigurator` reads a configured duration in milliseconds, so a smaller one becomes
`Duration.ZERO`.

### Access expiry

`getAccessExpireTime` evaluates the policy; `adjustAccessExpireTime` writes the resulting deadline
to the held wrapper. Only lock-free reads call `setVariableExpiration` to update the
native timer by key, after writing the wrapper: any core read re-derives the native deadline from
the wrapper through `ExpirableToExpiry`, so one landing between the two writes would otherwise
restore the older deadline. Pin: `JCacheAccessExpiryTest.get_concurrentRead_extendsNativeExpiration`.
Writes already refresh the native timer through core's `expireAfterUpdate`, including a
same-wrapper return. Calling policy `setExpiresAfter` inside their compute violates the policy
API's atomic-scope restriction and can enter maintenance while holding a bin lock.
Read-path anchors: `getAndFilterExpiredEntries`, `EntryIterator.hasNext`, and
`LoadingCacheProxy.getOrLoad`.

An access whose millisecond deadline equals the wrapper's writes neither the wrapper nor the native
timer. `getExpiryForAccess()` is still called once per access, which the TCK counts, and
`Duration.getAdjustedTime` is millisecond arithmetic, so the skipped writes are unobservable.
Rewriting them made every hit take the node's lock and queue a write-buffer task through
`setExpiresAfter`. Under `AccessedExpiryPolicy` on 8 threads a hit cost ~2.2 us, and ~0.2 us with
the skip. Pin: `JCacheAccessExpiryTest.get_sameMillisecond_keepsNativeExpiration`.

A replacement between a read and its by-key timer update can receive the old read's native deadline.
The old wrapper alone receives the timestamp, so this does not permit a stale value through its
lazy-expiry check, and a replacement that keeps the old deadline loses that read's extension, under
the standard `AccessedExpiryPolicy` too. `getExpiryForAccess()` is parameterless; standard
Accessed/Touched policies use the same access and creation duration. A custom access duration
shorter than creation can cause early native eviction and EXPIRED for the replacement. This race is
accepted; do not add a bin lock or identity guard to every access-expiry read.

`get` and `getAll` are happen-before, so a read whose captured wrapper has expired removes it only
by identity and continues with a live replacement that the removal kept: a key replaced before the
captured deadline was never absent. The removal also rechecks the captured wrapper's deadline at
the read's clock, so a wrapper that another read extended meanwhile is kept as well, `containsKey`'s
removal included. `LoadingCacheProxy.getOrLoad` reaches the same answer by looking the key up again.
That lookup can find a concurrent insertion or replacement and return it without another
`ExpiryPolicy.getExpiryForAccess()` call, as can `getAll`'s lookup of initially missing keys.
Native expiry still applies; `ExpirableToExpiry` mirrors the wrapper's stored creation/update
deadline. Skipping this one access adjustment is an accepted read-through race. Do not add a
special expired-wrapper recovery fix for the same miss-then-insertion pattern. Pinned by
`CacheProxyTest.getLoading_replacedBeforeExpiry_skipsAccessExpiry`, with
`get_replacedBeforeExpiry_appliesAccessExpiry` as the plain-read control.
`containsKey` and `EntryIterator.hasNext` are last-value and may still skip it. A read whose thread
stalls across its own deadline between the two writes still loses its extension, because
`setExpiresAfter` refuses an expired node; closing that needs the per-read key lock declined above.
The reverse ordering is the same accepted race from the other side: `removeExpired`'s
`computeIfPresent` re-reads the wrapper's volatile deadline at removal time, so a renewal already
written before that check runs is respected and the removal backs off. But if the check has
already passed and unlinked the wrapper before the renewal's write lands, that write reaches an
object no longer reachable from the map — a lost update, not a stale read, since the renewing
`get` already returned its value at the linearizable point before the removal. Closing this needs
the same declined per-read key lock.
Pinned by `CacheProxyTest.get_replacedBeforeExpiry`, `getAll_replacedBeforeExpiry`,
`containsKey_extendedBeforeExpiry`, `get_extendedBeforeExpiry`, and `getAll_extendedBeforeExpiry`.

An accepted timestamp-race report combined zero access expiry, an eternal entry, and a concurrent
processor READ to produce `EntryProcessorException`. Its `postProcess` path predates pre-processor
expiry reconciliation and is not a current reproducer; the lock-free boundary remains intentional.

The iterator stamps access expiry in `hasNext()` when staging the entry. Deferring to `next()`
widens its expiry hole or makes `hasNext()` promise an unavailable entry; the iterator contract
already allows `next()` to return null after expiry. Calling `hasNext()` without `next()` extends
one entry's deadline, an accepted cost. TCK: `CacheExpiryTest.iteratorNextShouldCallGetExpiryForAccessedEntry`.

`CacheProxy.get` always takes a fresh `ticker.read()` to derive the clock it checks `hasExpired`
against. `LoadingCacheProxy.getOrLoad` instead reuses the statistics start timestamp
(`nanosToMillis((start == 0L) ? ticker.read() : start)`) when statistics are enabled, so enabling
statistics changes which reading its expiry check uses while `get`'s is unaffected. This is
intentional-enough, not a bug: the reused stamp is taken earlier in the method, so it can only
make the check see an *older* clock, which can only judge an entry live that a fresh read might
have judged expired — expiration is a maximum lifetime, and both readings satisfy that. The
`compute`-based writes, `remove(K,V)` and `replace(K,V,V)` included, read the clock inside the
remapping function instead. A stamp taken before the compute is older by the wait for the entry's
lock, so it would judge an entry that expired during the wait as live and date a mismatch's access
deadline from before the wait. Pinned by `JCacheAccessExpiryTest.removeConditionally_mismatchAfterLockWait`
and `replaceConditionally_mismatchAfterLockWait`.

## Native extensions

Returning the same wrapper from a query still reaches core's native-write metadata. Failed
`putIfAbsent` (including keep-existing loadAll), a NONE-action processor, and eternal-policy
failed conditional writes/processor READ can reset native write time and cancel refresh. Thus
polling may defer vendor `refresh.after-write` or eager native TTL. This is accepted: JCache
expiry comes from `ExpiryPolicy`, while those Caffeine settings are optional extensions.

`containsKey` probes with `getIfPresentQuietly`, as keep-existing `loadAll` does. A recording read
would start a due vendor refresh, which calls the `CacheLoader` that the spec says a read-through
`containsKey` never calls, and it would count as an access, which the `ExpiryPolicy` table does
not give `containsKey`. Pin: `CacheLoaderTest.containsKey_refreshAfterWrite_doesNotCallLoader`.

With a vendor `Expiry` beside a JCache `ExpiryPolicy`, the native timer is not a mirror of the
policy, so its deadline after an access depends on the operation: a read sets it to the JCache
access duration, a compute-based access (`invoke` READ, a failed conditional, `putIfAbsent` on a
present key) leaves the vendor's `expireAfterUpdate`, and `containsKey` leaves it unchanged. Every
read still checks the wrapper's deadline, so only when eager eviction and its EXPIRED happen varies.

Do not expose `RemapHints` as a public no-op escape. Treating an unchanged value as no write
would change `asMap().compute` for all users and leave write-deque reorder paired with a stale
timestamp. A `computeIfAbsent` plus expired fallback splits the atomic operation and does not
solve invoke. These repairs were rejected.

A throwing extension `Weigher` (`setWeigherFactory` / `setMaximumWeight`) or native `Expiry`
(`setExpiryFactory` / `ExpiryAdapter`) runs in core after the adapter's remapping
function has written through and published the event, so it can abort storage after those
effects. Standard `ExpiryPolicy` is different: the adapter catches every `Exception` it throws
and uses the default duration, while an `Error` propagates.
The extension case remains accepted misuse of callbacks whose core contract forbids throwing.
Notifications/writer effects cannot be rolled back, and core has no post-metadata hook. Attempted
creation/update notifications are accepted; store/cache discrepancies need external reconciliation.
Do not move publication outside compute, which would lose per-key order. Unlike invoke,
put/getAndPut do not blanket-clear synchronous futures after arbitrary extension failure; a
pending notification reaching the next operation is also within this accepted boundary. A gate
must still be released from `finally` so dispatch can drain.

## Entry processors

`EntryProcessorEntry.Action` records the dominant operation. Preserve these distinctions:

| Sequence / terminal action | Resulting effects |
|---|---|
| Read-through `getValue()` / LOADED | Miss, no put; follows `Cache.get` and the RI LOAD case |
| Remove an absent or expired entry / DELETED | Writer delete if write-through; no REMOVED or removal count |
| Load, then remove / LOADED → DELETED | Writer delete; intentionally differs from RI's cancellation |
| Create, then remove / CREATED → NONE | Full no-op |
| Load, set a value, then remove / LOADED → CREATED → NONE | Full no-op, including writer and events |
| Update a present entry / UPDATED | Writer must succeed before put count, UPDATED, and store |

The RI counts/fires a removal with null for absent processor removal, unlike its own `remove(K)`.
Caffeine follows the listener table's requirement that an entry was removed; the statistics
table's wording is inconsistent. TCK covers remove-on-present only. For a read-through load,
pin the miss/no-put behavior with `CacheProxyTest.invoke_readThroughLoad_recordsMissNotPut`.

A remove that undoes this invocation's own create returns the entry to its untouched state: a
later `getValue` reads through if the loader has not yet been called in this invocation (as in
the RI), and a remove issued before that create is not written through (as in every provider
compared). A remove of any other state is DELETED and blocks read-through.

This terminal-action model follows the RI and the compared providers rather than the
specification's *Entry Processors* examples, which read more broadly. For expiry the
specification's own `ExpiryPolicy` table is explicit and takes precedence over Example 1's
first-read wording, so a read-modify-write `invoke` under `AccessedExpiryPolicy` keeps its
creation schedule. Example 3's writer delete after load, set and remove is the row above's full
no-op, which the RI shares and the TCK does not test.

`invoke` reconciles a lazily expired prior before the processor: publish EXPIRED, count eviction,
and expose absence. (A prior that core has already expired natively is instead reaped by core
with the quiet EXPIRED of [dispatch and commit](#dispatch-and-commit), which the caller does not
await.) If the processor or writer then throws any `Throwable`, commit removal of
that expired prior, await its synchronous listener, and rethrow after compute via
`processorFailure` (`Error` unchanged; other failures as `EntryProcessorException`), with a
listener failure suppressed onto either. Otherwise the already-published expiry would be orphaned
and could fire again. `postProcess` is expiry-free; READ/UPDATED require a live prior, so do not
re-read the clock or restore an expiry check there. Pins:
`EventDispatcherTest.invoke_expiredAndProcessorThrows_retainsListenerFailure` and
`invoke_expiredAndProcessorThrowsError_retainsListenerFailure`.

Read-through `MutableEntry.getValue()` normalizes the direct loader call before returning control
to the processor: existing `CacheLoaderException` passes through, other `Exception`s are wrapped
in it, and a direct `InterruptedException` first restores interruption. `Error` propagates unchanged.
A processor can therefore catch the loader failure and store a fallback. Keep this guard local;
the storage-oriented loader adapter would duplicate copying, expiry and event publication.
Pins: `CacheLoaderTest.invoke_loaderFailure_canRecover`,
`invoke_loaderInterrupted_restoresInterruptStatus`, and `invoke_loaderError_propagates`.

Exceptions escaping processor invocation, including write-through failures, surface as
`EntryProcessorException`; the spec's “Exceptions in EntryProcessors” section includes failures
from the caching implementation itself. Single-key writer failure therefore surfaces as
`EntryProcessorException(CacheWriterException)`. The RI exposes a raw writer exception because
its writer is outside the catch; cache2k wraps and Ehcache 3 exposes `CacheWritingException`.
TCK accepts all as `CacheException`; explicit spec text governs this corner.

Pre-processor copy failures in single-key invoke, of the key or the prior value, remain a raw
`CacheException`, consistent with the API's general cache-failure surface: invocation begins
when the processor is called. `invokeAll`, however, must isolate each key's
runtime failure in its `EntryProcessorResult`: preserve an existing EPE, wrap a non-EPE once,
and continue. Otherwise one uncopyable key aborts the batch and discards completed results.
RI, Ehcache 3, cache2k, and Hazelcast also isolate per-key failures (some double-wrap EPE).
Pin: `CacheProxyTest.invokeAll_perKeyFailure_isolatedNotAborted`.

Both invoke variants forward an explicitly null varargs array unchanged; only the key/keys and
processor require null rejection. RI, Ehcache 3, cache2k, Hazelcast, and Infinispan forward null;
Coherence indirectly reads its length. Normal omitted varargs remain an empty nonnull array.
Pins: `CacheProxyTest.invoke_nullArgumentsArray_forwarded` and
`invokeAll_nullArgumentsArray_forwarded`.

`getValue` on a DELETED entry returns null without loading. A successful null read-through load
is consumed rather than retried by another `getValue` in the same invocation. A failed load
leaves the loader available for a processor that catches the failure and retries. Keep loader
consumption and LOADED assignment after successful return, not in a finally block. Pins:
`CacheLoaderTest.invoke_removedEntry_doesNotLoad`, `invoke_nullLoad_consumed`, and
`invoke_loaderFailure_canRetry`, each covering `invoke` and `invokeAll`.

## Read-through and event ordering

Read-through `getAll` materializes loaded values with core `put`, replacing a value written
concurrently during the load, and `JCacheLoaderAdapter.loadAll` publishes CREATED for the loaded
value. This accepted choice treats the load as a fresh read of the system of record and preserves
notification for resource-tracking listeners. Do not align it with
`loadAll(replaceExistingValues=false)`, whose explicit contract is to keep existing values. That
separate API uses `loadAllAndKeepExisting` / putIfAbsent. Under `replaceExistingValues=true` a
present key the loader cannot load is kept by the RI, Caffeine, Hazelcast and Infinispan and removed
with a REMOVED event by Ehcache 3.12. The spec's Read-Through table says `getAll` invokes
`loadAll()`, which Caffeine and Ehcache 3 follow (Ehcache once per missing key), while the RI,
cache2k 2.6 and Hazelcast 5.7 load per key through `load`; a loader whose `loadAll` throws (for
example `UnsupportedOperationException`) therefore works with the RI's `getAll` and fails Caffeine's
with `CacheLoaderException`, and only Caffeine's `getAll` sees the bulk shapes. Two CREATED events
without an intervening UPDATED are an accepted concurrent-load outcome. A key the loader returns
outside the request is unspecified by `CacheLoader.loadAll` and the TCK, so it is stored and
published as CREATED like any loaded value, even over an existing mapping; a presence check before
core's `put` would be racy check-then-act. The same side-load does not classify the prior: it
replaces an unrequested key's JCache-expired prior without its EXPIRED event or eviction when native
expiry does not mirror the JCache deadline (a vendor `expireAfterWrite`), and with the default
mirror the EXPIRED arrives after the CREATED; under zero creation expiry the unrequested key's
EXPIRED names the discarded load while the prior stays live. `loadAll` routes such keys through the
write path and announces the expired prior first, as the RI does.

The overwrite holds under write-through and for a concurrent delete as well. A `put` or `remove` that
commits through the writer while the bulk load runs is undone when the loaded value is stored: after
a `put` the store holds the new value and the cache the loaded one, and after a `remove` the deleted
row is back in the cache. Either way the cache trails its store until the entry expires. This is
accepted. The spec gives `getAll` per-key happen-before but also has it invoke `loadAll()`, and no
surveyed provider reconciles the two with a batched load: the RI and Ehcache keep the write by
loading one key at a time under that key's lock, Ehcache through a single-key `loadAll`. A per-load
token that every write discards would keep both the batch and the write, at the cost of a hook on
each adapter write path to make the adapter stricter than any provider; core's own bulk overwrite
stays as its contract states. `Cache.loadAll` has the same shape, and its consistency row is "N/A".
An application that needs the write kept can use Caffeine's `AsyncCache.getAll`, whose bulk load does
not stomp a write to a requested key.

The listener contract does promise per-key event ordering for synchronous and asynchronous
listeners, and says listeners fire after the cache mutation. The accepted read-through behavior
diverges from those promises: loader publication precedes core bulk storage, so CREATED can
precede EXPIRED for an expired prior even without a concurrent writer. Single-key loading first
discards the expired entry. Do not describe the contract itself as lacking an ordering promise.

Recorded evidence: the inversion occurs when the reload lands before the timer wheel has swept
the prior's bucket, whichever executor runs maintenance. On 2026-09-24 a `getAll` about 100 ms
after a 50 ms deadline inverted in 17 of 20 runs on the direct executor and 16 of 20 on the
common pool, and in none at about 1.25 s. The sweep follows a level-0 timer-wheel tick (2^30 ns of
the ticker): on the direct executor the inversion occurs exactly when no tick has passed between the
prior's write and the `getAll`, and on the common pool a tick lets the sweep win only usually, more
rarely under CPU contention. The transitions can also end at a value the cache does not hold: when
a concurrent `put` commits between a bulk load's CREATED and its insert, the load overwrites the
put's value, so a mirroring listener's last event names a value the cache no longer holds and no
later event corrects it. That is the accepted bulk overwrite's event face (see the write-through
paragraph above). The accepted rationale is
the timing/transport behavior of distributed providers, while preserving the contract difference:
Hazelcast 5.7's per-entry cache-event factories did not set the publication order key (bulk
removal used the key-set hash); Coherence localcache dispatched EXPIRED before loading, but its
partitioned expiry used a separate synthetic-event listener and delivered asynchronously even to
synchronous registrations. The RI accumulates per-operation events in one JVM and orders this
case. Arrival order in a distributed replica also cannot cover lost-node or split-brain events;
the recorded decision accepts reconciliation by transition rather than imposing a new adapter
ordering guarantee. These observations are not a claim that the spec permits the divergence.

If a concrete requirement justifies repair, the recorded direction is core `getAll`: discard
expired entries in the miss set before loading, as single-key loading does. The adapter's quiet
accessors expose no expired mapping for a conditional removal. Treat publication-before-storage
and reload-before-prior-expiry as the same accepted design choice, not independent new findings.

## Statistics

### Operation timing

- Loading get/getAll opens a per-thread timing scope after recording a miss.
  `JCacheLoaderAdapter` contributes only time spent in `delegate.load`/`loadAll`; the outer
  operation records total minus loader time once, from `finally`. Copying, expiry evaluation,
  publication, and failure handling stay included. Native loads through
  `unwrap(LoadingCache.class)` have no scope and do not alter JCache get time.
- Do not restore a negative global pre-credit. An output-copy failure or native load without
  an enclosing JCache read left that credit unbalanced and made `AverageGetTime` negative.
  RI, Hazelcast, Infinispan, and cache2k avoid negative means, although some include loader time.
  Pins: `CacheLoaderTest.load_outputCopyFailure_keepsGetTimeNonNegative`,
  `load_failure_excludesLoaderTime`, `load_copierTime_includedInGetTime`, and
  `nativeLoad_doesNotChangeJCacheGetTime` (single and bulk variants).
- Successful plain getAll ends its timer after `copyMap`, matching get and loading reads.
  RI and Coherence include return conversion; Hazelcast's bulk per-key timer and cache2k's
  core load-time metric differ. The MXBean's execution-time definition and internal parity
  support inclusion. Pin: `CacheProxyTest.readOp_outputCopyTime_includedInGetTime`.
  Failed-read hit accounting remains distinct: plain get and the RI count a hit only after
  successful output conversion.
- PutAll starts timing before preparing `CopiedEntry` inputs; all copies still finish before
  writeAll and only successful stores count as puts. RI starts before conversion; Coherence
  delegates to its copy-inclusive put, while Hazelcast times below serialization and cache2k
  reports no put mean. Pin: `CacheProxyTest.writeOp_inputCopyTime_includedInPutTime` across
  put/putAll, create/update, and statistics enabled/disabled.
- `CacheGets = hits + misses`. Caffeine divides each average duration by its own operation
  counter; the RI's three averages all divide by gets, a known RI bug to avoid copying.
- `recordHits`/`recordMisses` and the conditional pairs that call them (`replace(K,V,V)`,
  batch hit/miss splits) are unconditional `LongAdder.add`, unlike `recordPuts`/`recordGetTime`/
  `recordPutTime`/`recordRemoveTime`, which also gate on a nonzero count. The asymmetry is
  accepted under best-effort statistics: the zero count costs one uncontended `LongAdder`
  update, and rewriting the pairs as `if (found) recordHits else recordMisses`, as the sibling
  call sites read, was declined as churn.
- `invoke`, conditional `remove`, and iterator yields count gets without timing them, and an
  empty `getAll` times no gets. The RI times the first three and not the last; these populations
  stay unaligned under best-effort statistics. `putIfAbsent`'s hit or miss is untimed too, as in
  the RI. The same mismatch reaches `invoke`'s put and
  removal counters: `postProcess` calls `recordPuts`/`recordRemovals` for CREATED/LOADED/UPDATED/
  REMOVED actions but never opens a timing scope, so `getAveragePutTime` and
  `getAverageRemoveTime` are diluted by `invoke` traffic the same way `getAverageGetTime` is.

### Commit and failure accounting

Writer failure suppresses the operation's put/removal count as well as its mutation event and
store. This includes invoke UPDATED, whose writer must precede the count. Parity pins:
`CacheWriterTest.writeOp_failingWriter_noPutsRecorded`, `writeOp_writerSucceeds_recordsPut`, and
`removeOp_failingWriter_noRemovalsRecorded` across direct, batch, processor, and iterator paths.

After a committed effect, capture a synchronous listener failure with `awaitSynchronousFailure`,
record the required counters/timers using the operation's existing start-state rules, and then
`rethrowListenerFailure`. Direct operations follow bulk/invoke/loading siblings. Returned-value
copying in getAndRemove/getAndReplace happens after accounting, as in getAndPut, so copier failure
does not erase the committed effect. The spec does not prescribe this ordering against listener
failure; required counters and sibling parity justify it. This covers the specified listener
exception, not arbitrary `Error`; enable/disable races remain spec-undefined. When a synchronous
listener throws an `Error`, `awaitSynchronousFailure` does not capture it, so the statistics block
is skipped: the committed mutation is retained but its counters are not recorded for that
operation. The mutation is not rolled back. Pins:
`EventDispatcherTest.synchronousListenerFailure_committedMutationRetainsStatistics` and
`synchronousExpiredListenerFailure_retainsGetStatistics`.

## Write-through

Single-key writer calls and their cache mutations share the per-key compute lock. This follows
`CacheWriter`'s non-batch atomicity contract. In particular remove/getAndRemove must use `compute`,
not computeIfPresent, because delete must run even for absence. A writer call before compute lets
a same-key put interleave, leaving cache and store inconsistent. Preserve `publishToWriter` so
bulk loops can reuse the mutation without repeating writer calls. Pin:
`CacheWriterTest.removeThrough_racingSameKeyPut_noStoreCacheDivergence`.
RI, Ehcache 3, Hazelcast, cache2k, and Coherence partitioned serialize these effects; recorded
Coherence localcache had the old unlocked writer/removal window.

Bulk writeAll/deleteAll runs once before per-key computations. The spec exempts batch methods
from cross-key atomicity; a racing single-key operation can invert cache/store order across
that window. This is accepted for both putAll and removeAll. RI locks all keys beyond the spec;
Ehcache 3 and Coherence partitioned batch like Caffeine, while cache2k/Infinispan/Coherence local
loop per-key through their persistence SPIs. Do not replace batching with per-key writer calls
solely for RI parity. The Integration contract also bounds write-through guarantees to the cache
being the application's only writer to the external resource.

Treat a non-throwing `writeAll` (`putAll`) or `deleteAll` (`removeAll`) as full success. Honor a
writer's residual collection only on a partial failure, when it identifies entries that failed.
The RI's residual-on-success interpretation made both `putAll` and `removeAll` a no-op for a
non-clearing writer and was rejected for both. Pins:
`CacheWriterTest.removeAll_nonClearingWriter_stillEmptiesCache` and
`putAll_nonClearingWriter_stillStores`.

A partial batch failure reconciles the same way whatever `Exception` the writer throws, including
a checked exception thrown undeclared by another JVM language. A writer `Error` after a partial
`writeAll`/`deleteAll` propagates before the store loop, leaving the external resource changed
for the completed entries and the cache unchanged, as in the RI. Pins:
`CacheWriterTest.putAll_partialSuccess_storesWrittenEntriesOnly` and
`removeAll_partialSuccess_removesDeletedEntriesOnly`. The specification requires wrapping any
`Exception` a writer or loader throws. Recorded from bytecode on 2026-09-14: RI, Ehcache 3,
Hazelcast, Infinispan, and cache2k catch `Exception` or `Throwable` around loader and writer calls
(Ehcache and cache2k in their cores), while Coherence's `writeAll` and `deleteAll` catch only
`RuntimeException`.

The RI swallows a `CacheWriterException` thrown by `writeAll`/`deleteAll`, leaving nothing to
rethrow; Caffeine propagates it, as `CacheWriter.writeAll`'s "if thrown cache mutations will occur
for entries that succeeded" requires. Pins:
`CacheWriterTest.putAll_fails_propagatesSameWriterException` and
`removeAll_fails_propagatesSameWriterException`. When a write-through `put` over a JCache-expired
prior fails, the RI has already removed the prior and skips its EXPIRED, and a read-through `get` or
`getAll` whose loader throws over an expired prior removes it the same way; Caffeine keeps the prior
until a later read or commits and announces it, one EXPIRED either way.

Converting or defaulting a callback's `InterruptedException` first restores the thread's interrupt
status, as core's `Caffeine.toUnchecked` does, since the wrapper or the default would otherwise hide
the interruption. Pins: `CacheLoaderTest.readThrough_loaderInterrupted_restoresInterruptStatus`,
`CacheWriterTest.writeOp_writerInterrupted_restoresInterruptStatus`, and
`CacheProxyTest.put_expiryInterrupted_restoresInterruptStatus`.

The batch exemption does not permit writes the adapter later refuses because copying failed.
Copy all putAll keys/values before writeAll and reuse `CopiedEntry` in the store loop. A copier
failure then writes/caches nothing. Pins: `CacheProxyTest.putAll_writeThrough_copierThrows_doesNotWriteToTheStore`
and `EventDispatcherTest.putAll_copierFails_abortsBeforeAnyCommit`. If a throwing native extension
still aborts a loop after partial writer failure, preserve that `CacheWriterException` as
suppressed, not replaced. Pin: `CacheProxyTest.putAll_writerPartiallyFails_storeThrows_retainsWriterFailure`.

No-argument removeAll delegates to the native map key set, which filters natively expired entries.
Those expired-but-unreaped keys are omitted from deleteAll, unlike the RI. This remains an
accepted reading of an existing mapping, with no TCK assertion resolving it. The opposite half of
the same method has the opposite outcome: a key that is JCache-expired but still natively live is
present in that key set, so it is still passed to `writer.deleteAll` before being reaped as EXPIRED
plus an eviction — not REMOVED. `CacheWriter.deleteAll` allows both halves, since under 1.1.1 its
keys "may include keys even if there is no mapping". What the second half departs from is
`Cache.removeAll()`'s "If the cache is empty, the CacheWriter is not called" when every resident is
JCache-expired, which needs a vendor native expiry that outlives the JCache deadline. Both halves
are recorded together here rather than only the natively-expired half.

## Events and callbacks

### Dispatch and commit

Append events inside the mutating compute to preserve per-key ordering, but stage listener
execution until it returns. `beginComputation` marks the publishing thread; the first publish
lazily creates a gate. `endComputation` releases the gate from `finally`, holding the mark through
release because a caller-runs executor may dispatch there. A throwing Weigher/native Expiry must
not leave the chain blocked. No listeners means no gate allocation, and the release clears the
gate's slot with `set(null)`: `remove()` discarded the thread-local entry, so every computation
allocated a fresh one. A drain likewise empties the thread's pending list in place, except one
holding more than `MAX_RETAINED_PENDING` futures, which it releases: a bulk operation with a
synchronous listener otherwise left each thread holding its largest array until the thread died
(about 4.9 MB after a 1M-entry `removeAll`). Pins:
`EventDispatcherTest.awaitSynchronous_largeBatch_releasesPendingList` and
`ignoreSynchronous_largeBatch_releasesPendingList`. Do not stage an event outside a computation,
such as an unwrap-driven load, because it has no matching release.

This staging makes ordinary listeners observe committed mappings. RI dispatches after put under
its key lock; Ehcache 3 releases a StoreEventSink after compute; Hazelcast publishes after record
mutation; Infinispan forwards post events; Coherence uses post-mutation MapEvents. Recorded cache2k
listeners run before its final value write. The read-through publication exception is described
above. Pins: `EventDispatcherTest.publish_listenerObservesTheCommittedMutation` (a created
listener, observing through the native cache because callback JCache re-entry is refused),
`publish_computationThrowsAfterPublishing_doesNotWedgeTheKey`, and
`CacheProxyTest.unwrap_nativeLoad_dispatchesAndDoesNotStallTheKey`.

Every publication uses a dependent CompletableFuture stage, including the first event for a key.
A source `supplyAsync` calls `executor.execute` synchronously; rejection inside the cache compute
can abort the cache mutation after the writer succeeds. A dependent stage captures rejection in
its future, so commit completes and the synchronous caller receives `CacheEntryListenerException`
afterward. This fixed the earlier first-event/successor difference under the non-batch writer
atomicity rule; the rejecting executor is still misconfigured. The “first” branch is common
because idle-key cleanup empties the slot. Pin:
`CacheProxyTest.put_writeThrough_eventExecutorRejects_doesNotSplitTheStore`.

The queue needs no extra lock: append is an atomic map compute, cleanup is conditional
`remove(key, future)`, and its preceding identity get is only a fast path. A successor installed
before cleanup survives; cleanup before a successor permits a new chain only after the old one
completed. Await/ignore pending synchronous futures on relevant exits and clear in finally.
Bulk operations must drain already-published events even when the loop fails. putAll copying
now happens before any commit, covered by
`EventDispatcherTest.putAll_copierFails_abortsBeforeAnyCommit`; the earlier mid-loop copier test
was removed, but later failures still require draining.

`invoke`'s outer catch discards rather than awaits the pending synchronous future
(`ignoreSynchronous()`) before rethrowing. That choice rests on every `postProcess` branch calling
`publishToCacheWriter` before its `publish*`, so a throwing writer aborts before anything is
published and there is nothing pending to leak. This ordering premise is not restated at the catch
site; a future edit that publishes before the writer would silently leak a synchronous future here
without a failing test, since the accepted native-extension and `Error` boundaries are a separate,
already-scoped exception to the same rule.

An accepted report described duplicate EXPIRED, but exactly-once eviction counts, when a listener
ran before executor rejection aborted a reap. It predates dependent-stage publication. Preserve
the accepted executor-failure boundary, but require a current throwing path before claiming that
mechanism remains reproducible.

Native size/weight eviction publishes quiet REMOVED; native EXPIRED publishes quiet EXPIRED.
Refresh reload publishes quiet UPDATED, EXPIRED for zero update expiry, or REMOVED on a miss.
The last two remove the entry but count neither an eviction nor a removal: core removes it as
an explicit removal, outside `JCacheEvictionListener`, and statistics are best-effort.
Quiet means no synchronous caller await: it informs resource-tracking listeners without blocking
the evicting/refresh thread. Lazy expiry mostly arrives this way on the system ticker: the native
mirror hides an expired entry from reads, and a read in the sub-millisecond gap between the
wrapper's millisecond deadline and the native one re-derives the native deadline from the expired
wrapper, so `removeExpired` finds the entry natively expired. A read, or a `computeIfPresent`-based
replace or conditional remove, that finds an entry expired therefore publishes and counts nothing,
and its EXPIRED and eviction wait for a maintenance cycle after a wheel tick, which an idle cache
without a `Scheduler` does not run. The adapter's own read-side reap runs only when a vendor
native expiry outlives the JCache deadline. For a read that finds an expired entry, the RI 1.1.1,
Ehcache 3.12.0 and Infinispan 16.2.3 deliver EXPIRED to a synchronous listener before the read
returns, cache2k 2.6.1 does not, and Hazelcast 5.7.0 does so only in some JVM histories, so the
deferral is a Caffeine choice (lazy expiry; a `Scheduler` for promptness) rather than ecosystem
parity.
Clearing natively expired residents can produce quiet EXPIRED and eviction counts through core's
removal cause, even though ordinary explicit clear removals are silent. Closed-cache delivery is
separately suppressed by dispatch's closed check. A reload publishes before core decides whether
it commits, and core reports a reload it discards because the entry changed in flight only to a
removal listener, which the adapter does not register. That event stands with no mutation, which
is accepted for a best-effort extension that has no post-commit hook. Native eviction and expiry
publish the same way, from core's eviction listener inside the eviction's compute on an unmarked
maintenance thread, so their dispatch is not staged either: a size-evicted entry's REMOVED listener
can read the mapping back before the removal commits, within microseconds of it, and on a
caller-runs executor the listener runs inside that compute, where writing its own key fails with
`ConcurrentHashMap`'s `Recursive update`. An expired entry is hidden from reads, so EXPIRED shows
nothing stale. Accepted on the same terms: JSR-107 defines no eviction, and staging it would need
core's post-unmap removal callback to release a gate per eviction.

### Listener failures

Filters run inside compute and decide whether an event is published. A filter failure, an
`Error` included, is logged and returns false from `GuardedCacheEntryEventFilter.evaluate`; it
must not abort a mutation after the writer or earlier registrations already took effect. This
differs from post-commit listener failure. RI commits before propagating filter failures;
Hazelcast/cache2k filter at delivery. Pin: `EventDispatcherTest.publishCreated_filterThrows`.

A synchronous listener's `CacheEntryListenerException` passes through; other listener runtime
exceptions are wrapped in it. `javax.cache.event` package-info and `CacheEntryListener` require
propagation. Listener `Error` is logged and rethrown unchanged, not wrapped. Ordinary listener
failure travels as a chain result so it does not break subsequent same-key delivery;
`awaitSynchronous` throws the first failure and suppresses extras. Exceptional executor futures
also become `CacheEntryListenerException`. Async/quiet failures are logged because there is no
synchronous caller. Mutation remains committed. Spec, RI, cache2k, and recorded Infinispan sources
support propagation. Ehcache 3.12.0, Hazelcast 5.7.0's member event path, and the recorded Coherence
implementation log failures. Hazelcast's listener adaptor releases its completion latch in finally and
`StripedExecutor` catches the failure; the waiting cache proxy has no listener-exception result.

Pins: `EventDispatcherTest.put_syncListenerThrows_propagatesToCaller`,
`publishCreated_asyncListenerThrows_swallowed`,
`publishCreated_syncListenerThrows_subsequentEventStillDelivered`,
`awaitSynchronous_listenerException`, `awaitSynchronous_listenerExceptions_suppressed`, and
`EventTypeAwareListenerTest`.

A listener failure must not replace the operation's own failure. Use `awaitAndSuppressFailure`
on the primary failure; a bare finally-await can otherwise replace a copier exception with an
expired listener's exception. Plain getAll must follow loading getAll and the bulk/invoke paths.
Pin: `EventDispatcherTest.getAll_copierThrows_retainsPrimaryFailure`. See statistics above for
accounting before rethrowing a post-commit listener exception.

### Callback re-entry

`requireOperable()` refuses operations on the publishing thread while its computation mark is
set, with `IllegalStateException: Recursive cache operation`. Filter, processor, loader, writer,
expiry-policy, and copier callbacks inside a computation can otherwise acquire another bin while
holding one; caller-runs listeners can await the dispatch future executing themselves. Keep the
mark through gate release and refuse reads too: lazy expiry makes get/containsKey/getAll compute,
so a read-only exemption would depend on whether a key happened to expire. The mark is checked
when an operation begins, so an iterator obtained before a callback and advanced inside it is not
refused and can reschedule native access expiry under the callback's locks. The specification's
reentrancy section permits restricting such use without requiring it, the RI refuses nothing, and
checking every advance would add a thread-local read per staged entry.

`close()` refuses the same way after its idempotent return: its sweep would clear the map under the
callback's bin lock, taking `evictionLock` there and nesting a structural update of the same map,
which deadlocked against maintenance and left a permanent size drift. `destroyCache` clears before
it closes, the spec's order, so a refused `clear()` destroys nothing. A resource whose own `close()`
closes the cache still returns quietly, since the closed flag is set first. `CacheManager.close()`
from a callback logs that cache's refused close as a WARNING and leaves it open while the manager
closes; that is lifecycle re-entry, outside the contract. Pins:
`EventDispatcherTest.invoke_processorClosesTheCache_isRejected` and
`invoke_processorDestroysTheCache_isRejected`.

The mark is per cache/thread, not a general listener ban. Batch writeAll/deleteAll runs before
the per-key loop and may use the cache (`CacheWriterTest.removeAll_racingInsert`). An eviction
filter on an asynchronous maintenance thread has no mark and may read the cache; inline
maintenance inherits an existing mark. Adding a mark there can abort publication before other
listeners receive their events, so do not broaden it merely for symmetry. `CacheEntryEventFilter`
forbids side effects, so a filter whose cache call deadlocks the evicting thread, which holds the
eviction lock and the entry's locks, is outside the contract.

The spec permits implementation-specific deadlock detection. Three accepted hazards remain:
cross-cache listener cycles, a synchronous listener dispatched on another thread that operates on
its own key, chains behind itself, then awaits itself, and the same shape across two distinct keys
in one cache: two threads concurrently mutate keys A and B under one synchronous registration, and
if that listener reciprocally writes the other key from the dispatch thread, the resulting per-key
futures form an unbreakable cycle (A's future awaits B's, B's future awaits A's). Distinct keys are
not a safety guarantee here; the mechanism is identical to the other two hazards; not marking
dispatch threads is what admits all three, so it is not a separate boundary to name or a new
decision to make. A user callback that blocks on a `ForkJoinPool` worker between a publish and its
operation's await (a `join()` inside an `ExpiryPolicy` or `Copier`) can let the worker help run
another same-cache `loadAll`, whose `chainSynchronous()` drains the outer operation's thread-local
pending futures; blocking a pool worker inside a callback is outside the contract, as a waiting
executor is (traced from source, not witnessed). The own-key case was reproduced with the gate both
enabled and disabled; moving execution outside compute did not create it. Do not extend the mark to
dispatch threads. Pins: `EventDispatcherTest.publish_listenerUsesTheCache_isRejected` and
`invoke_processorUsesTheCache_isRejected`. The probe included a positive control and found no
re-entry in the then 493 TCK / 603 unit tests.

## Copying and listener configuration

### Copy boundaries and failures

`CacheProxy.copyOf` passes through `NullPointerException`, `IllegalStateException`,
`ClassCastException`, and `CacheException`, the API's declared failure types; it wraps any other
exception in CacheException across reads, writes, getAnd operations, and iteration.
A user's readObject throwing ISE therefore surfaces raw intentionally. The primitive requires a
nonnull argument; nullable prior-value returns guard it explicitly. Loader copying retains its
contextual `CacheLoaderException`, including the TCK-required wrapping. Pin:
`CacheProxyTest.copierFailure_wrappedInCacheException`.

Under store-by-value with write-through, the no-argument `removeAll()` hands `writer.deleteAll` the
stored key instances and `iterator.remove()` hands `writer.delete` the last entry's stored key,
where the other writes pass the caller's instances and the RI a deserialized copy. A writer that
mutates the keys it is asked to delete is outside the contract. A null `unwrap` argument throws
`NullPointerException` on every surface (cache, manager, entry, mutable entry, event); the RI throws
NPE for the cache and manager and IAE for entries and events.

A replacement copies its new value only once the entry is known to be present, as `replace(K,V,V)`
does, so `replace(K,V)` and `getAndReplace` on an absent key return `false` or `null` without
copying. Pin: `CacheProxyTest.replace_absent_copierFails_isNoOp`. The same rule governs
`putIfAbsent`: it copies the new value inside `compute` only on the branch that stores it, unlike
`put`, which has no branch to not take and copies unconditionally before compute. This follows
from `putIfAbsent` being specified as `if (!containsKey) put(...)`: not copying on an untaken
branch is the contract-faithful reading, not an inconsistency to fix.

`JavaSerializationCopier` reports nonserializable values as CacheException, matching its
deserialize path and the cache API. Do not restore `UncheckedIOException` or follow the RI's
serialize-side `IllegalArgumentException`; cache2k uses CacheException, while Hazelcast also
differs. Pins: `JavaSerializationCopierTest.serializable_fail` and
`CacheWriterTest.putIfAbsent_nonSerializableValue_doesNotWrite`.

`JCacheLoaderAdapter.loadAll` copies values but stores the loader-returned key without another
copy. Requested keys are already copied on input, and application returns (`copyMap`, iterator
EntryProxy) copy on output. This is not equivalent to storing an uncopied caller key in put;
the loader's internal key is not otherwise exposed through those returns. Its CREATED event is
within the event-aliasing exception below. Do not add a loader key copy for false put/load parity.

A read-through `MutableEntry.getValue` likewise hands the processor the loader's instance while
`postProcess` stores a copy, as the RI does. The stored value stays isolated, and copying for the
processor too would serialize every processor load.

### Event aliasing

Store-by-value events expose stored values: CREATED/UPDATED use the stored copy, and old/removed/
expired values come from `Expirable.get()`. A listener can therefore mutate the cached value.
Events also carry uncopied keys; ordinary put publication exposes the caller key, not its stored
copy, so mutation there cannot corrupt the cache's stored-key lookup. Preserve this distinction
when discussing loader events, which can expose the loader's stored instance.

This is accepted: the spec's copy guarantee addresses application mutation, the TCK
`StoreByValueTest` does not check listener payloads, and copying each event would tax every
listener-bearing operation. cache2k also exposes stored values; the RI supplies caller instances
for create/update and deserializes for expiry/removal as a consequence of its serialized store.
Do not add event copies without a concrete requirement.

The key is also the asynchronous dispatch queue's identity. Mutating its hash while dispatch is
pending can strand the completed slot in the old bin. Recorded 2026-09-10: 100 mutated keys kept
100 slots, later publication did not reclaim them, and deregistration/close cleared them. Direct
execution retained none; synchronous writes await before caller mutation. Such caller mutation
is legal under store-by-value (`StoreByValueTest.get_Existing_MutateKey` uses Date), but the
recorded seven-provider comparison found no equivalent protection: cache2k's
`AsyncDispatcher.keyQueue` and Infinispan's `latchesByEventSource` also use key hashes; RI has no
per-key state, Ehcache copies the key into its store, and Hazelcast, Redisson, and Ignite deliver
deserialized listener keys. The report was accepted as an unsupported usage edge, not reclassified
here as invalid application input.

If this needs repair, split dispatch identity from listener payload. Passing the stored
`copiedKey` to both would let listener mutation corrupt the cache lookup, introducing a worse
aliasing path. Preserve this rejected-repair boundary.

### Registration identity and configuration leaves

Registration and deregistration normalize listener settings through a defensive
`MutableCacheEntryListenerConfiguration` copy. Identity uses its specified factory/flag field
equality, not a custom caller configuration's equals. Two field-equal custom objects that call
themselves unequal therefore register once (the RI fires both), and deregistering either removes
that registration while the other stays listed (the RI keeps the other). The listed configuration
then receives no events, and a listener instance the two share is never closed, because it counts
as deregistered. RI parity would need a count on the registration, decremented only when
deregistration removes a listed configuration. The stable copy prevents caller mutation from
changing a registration key; deregister must perform the same normalization. Do not restore
raw-config keys or the old deregistration-key mismatch. Dispatch does not consult
`isOldValueRequired`: UPDATED, REMOVED, and EXPIRED events carry the old value either way.

`getConfiguration()` returns a read-only configuration with an unmodifiable listener iterable,
but retains its live mutable listener-setting leaves. This accepted shallow-immutability reading
is stricter than the RI's live mutable configuration and comparable to Ehcache 3's shared leaves.
Dispatch is insulated by Registration's own copy; leaf mutation can affect reporting and later
deregistration matching. Two registrations mutated into equality make `getConfiguration()` and
the CacheMXBean attributes throw `IllegalArgumentException`, since their copy re-adds each
listener configuration through `MutableConfiguration`'s duplicate check. The typed `getCache`
compares the proxy's configured types instead of a copy, so it still returns the cache
(`CacheManagerTest.getCache_typed_listenerConfigurationMutatedIntoDuplicate_returnsCache`).
`ConfigurationTest.testModifyingConfigurationAfterCreateCacheDoesNotModifyCacheConfiguration`
pins key/value type isolation from changes to the original create configuration; that test does not
cover listener-setting leaf isolation.

Copying every leaf was built and rejected: MCELC's instanceof-based equals did not match a
user-implemented original configuration, while it matched Registration's copy. Deregister then
removed dispatch state but left the configuration entry, so the listener remained listed, was
not closed, and could not be registered again. Do not repeat that repair.

Runtime registration records configuration first so true duplicates fail before factories run. A
configuration with a null listener factory is recorded and listed but registers nothing
(`EventDispatcherTest.register_noListener`); the RI and Ehcache 3 fail it with NPE, and cache2k
with IAE. If a factory throws, roll that entry back so retry succeeds. If filter construction fails
after listener construction, or registration drops a normalized duplicate, close the listener
unless a registration already dispatches to that instance, as `FactoryBuilder.factoryOf(instance)`
shares one, and suppress a close failure onto the primary. The surviving registration owns a shared
instance, so there is no later close to defer: cache close already closes it, deregistration does
not, and a close queued behind dispatch would reach a listener that is still registered. Pins:
`CacheProxyTest.registerCacheEntryListener_factoryThrows_isRetryable`,
`registerCacheEntryListener_filterFactoryThrows_closesTheListener`,
`registerCacheEntryListener_filterFactoryThrows_keepsASharedListenerOpen`,
`registerCacheEntryListener_normalizedDuplicate_keepsTheSharedListenerOpen`,
and `createCache_normalizedDuplicate_keepsTheSharedListenerOpen`.

Deregistration removes a listener without closing it; registration failure and cache close are the
paths that close. The spec's one closing requirement is `Cache.close`'s: the manager closes the
configured loader, writer, and expiry policy and the registered listeners that implement
`java.io.Closeable`. Deregistration's contract says nothing of closing, the RI does not close, and
closing there could interrupt a listener whose published events are still dispatching. Closing a
failed registration's listener is resource hygiene beyond that requirement.

In the recorded twelve-provider comparison, Ehcache 3, Hazelcast, and Ignite close a listener on
deregistration, while the RI, cache2k, Infinispan, Coherence, and Blazing Cache do not. Ehcache 3
closes the listener when its filter fails to build, even when another registration shares the
instance, and Ignite closes it when the listener's query fails to start; the RI, Hazelcast, and
Blazing Cache leave it open. JCS, Redisson, Triava, and the Ehcache 2 adapter close no listener at
all.

Filters are never closed, at cache close, at deregistration, or when registration drops a
duplicate: `Cache.close` does not name them. Of the twelve, only Ehcache 3 and Blazing Cache close
filters, and Infinispan builds a new filter for every event. Cache close closes each role and each
registration's listener, so one object serving as loader and writer, or one listener under two
distinct registrations, is closed twice, as in the RI, Ehcache 3, Hazelcast, Coherence, and Blazing
Cache; cache2k and Infinispan close a shared listener instance once. `Cache.close` names
`java.io.Closeable`, whose close has no effect once closed.

## Configuration

HOCON `application.conf` can supply caches before programmatic creation. Vendor
`CaffeineConfiguration` and shipped `reference.conf` default store-by-value to false; standard
`MutableConfiguration` defaults true and its flag is reapplied by `resolveConfigurationFor`.
This vendor-only difference is intentional; `StoreByValueTest` covers the standard surface.
A HOCON-defined cache is created by its first `getCache`, so until then `getCacheNames()` omits it
(the API allows omitting implementation-defined caches) and `createCache` for its name throws
`CacheException` as configured externally; `destroyCache` lasts only until the next `getCache`
recreates it from the configuration.

Validate resolved readThrough/writeThrough dependencies once in `CacheFactory.createCache`,
before building any ticker, executor, scheduler, copier, policy, listener, loader, or writer.
A missing required factory is an invalid configuration and throws IllegalArgumentException
without publishing the name or causing factory side effects. False flags remain valid without
factories. A required factory that produces null fails the creation with NullPointerException, the
writer's as the loader's, rather than silently disabling write-through. After validation,
`isReadThrough()` alone selects the loading proxy; do not restore an extra factory-present gate
that silently substitutes a non-loading proxy for invalid configuration.

This follows `MutableConfiguration.setReadThrough`/`setWriteThrough` and
`CacheManager.createCache`. RI tolerates missing factories; Ehcache 3's
`ConfigurationMerger.initCacheLoaderWriter` rejects them. TCK's `CacheMXBeanTest.testCustomConfiguration`
uses false flags and does not resolve the split. HOCON read-through with a null loader must fail
from getCache, not degrade silently. Pins: `CacheManagerTest.isReadThrough`,
`invalidThroughConfiguration_hasNoCreationSideEffects`,
`throughConfiguration_nullFactoryProduct_failsFast`, and `createCache_minimalConfiguration`.

`TypesafeConfigurator.from` wraps every ConfigException (BadPath/Missing/WrongType) in
CacheException with its cause, as configuration failures through the cache API; do not restore raw
Typesafe exceptions or an ignored subtype. A valid JCache name need not fit Typesafe's path grammar,
so `cachePath` quotes it with `ConfigUtil.joinPath` and resolves it as a literal key. A BadPath can
then only come from a malformed path within the settings, such as a `listeners` entry, which must
fail rather than hide the cache as undefined. This changed in 3.3.0, deliberately: 3.2.x looked a
name up as a path, so an unquoted dotted key such as `com.example.Foo { ... }`, which HOCON expands
into nested objects, resolved, while `cacheNames` listed only its first segment (`com`). Since 3.3.0
the nested form does not resolve and the cache falls back as unconfigured, so a dotted name must be
written quoted. A Java properties key (a `-D` override or a `.properties` file) cannot quote, so it
cannot configure a dotted name at all. Resolving both forms was declined: it restores the phantom
first-segment cache and makes `createCache`'s externally-configured rejection depend on which form
matched. Hibernate names entity regions by class, and it logs `HHH90001006` when it creates a
missing region on the fly. `defaults`, `cacheNames`, and `CacheFactory`'s read of
the configuration source wrap theirs too, so an unreadable or malformed file surfaces from getCache
and createCache as CacheException. Pins: `TypesafeConfigurationTest.from_malformedSetting`,
`from_dottedCacheName`, `from_malformedListenerPath_throwsCacheException`,
`defaults_malformedSetting`, `cacheNames_malformed`, and
`CacheManagerTest.getCache_unreadableConfiguration_throwsCacheException`.
Separate existing paths remain unchanged: type-resolution CNFE becomes ISE, and bad factory-class
RuntimeException originates in the spec's own FactoryBuilder.

`maximumWeight` without a weigher throws ISE, even though createCache's generic validation
clause uses IAE. These are vendor-extension properties; this difference remains accepted.
Pin: `CacheManagerTest.maximumWeight_noWeigher`. The same holds for maximum size with maximum
weight and for eager after-write or after-access expiry with a variable `Expiry`: the core
builder rejects them with ISE inside `build()`, after the `Builder` constructor created the
configured resources. HOCON rejects size-with-weight earlier, with IAE, in `TypesafeConfigurator`.

### Classloaders

`TypesafeConfigurator.addKeyValueTypes` uses `Class.forName(name, true, tccl)`, falling back to
the adapter loader for a null TCCL. This retains initialization and matches the spec's
`Caching.getDefaultClassLoader` / FactoryBuilder idiom for customization classes. In OSGi,
CacheManagerImpl temporarily sets TCCL to the manager loader on create/get paths. Destroy/close
does not resolve class names and needs no swap; a user close callback's TCCL assumptions remain
the user's responsibility. Ehcache 3 does not swap on these paths.

Passing the manager loader directly only for types was rejected: outside OSGi it would resolve
types differently from the spec's own TCCL-based factories. Do not add manager-loader plumbing
to FactoryCreator alone. Pin: `TypesafeConfigurationTest.resolvesTypesViaContextClassLoader`.

The provider's WeakHashMap registry and manager's weak loader reference are best-effort.
An otherwise empty manager is collectible (`CacheManagerTest.classLoader_readThrough_notRetained`),
but application values and factory/policy/loader/writer/listener instances can retain their
defining loader through the value side, even with no cache entries. The test's Mockito::mock
factory belongs to the test loader, so it does not establish absence of this application pin.
Weakening registry values would allow live managers to disappear. The explicit
`CachingProvider.close(ClassLoader)` lifecycle operation is implemented; use it for unloading.
A cache still used after its manager's loader was collected throws `NullPointerException` from
every copy, since `copyOf` resolves the loader each time; that use outlives the loader, and the
RI's serializing converter holds it weakly as well. Such a manager is also no longer reachable
from `CachingProvider.close()` or `close(ClassLoader)`, since the registry entry left with the
loader, so it must be closed directly. Deactivating the OSGi provider component likewise closes
no manager: closing them stays the consuming bundle's responsibility, through
`CachingProvider.close(ClassLoader)` or the manager.

### Management

JMX ObjectName sanitization replaces `[,:=\n*?"]` with a dot. The RI replaces only `,:=\n`, so it
fails `createCache` for a name containing `*`, `?` or `"`. Distinct names or
manager URIs can collide (a:b and a=b both become a.b): the second registration is skipped by
isRegistered and destroying either unregisters the shared name. This accepted consequence is
also present in RI's `MBeanServerRegistrationUtility`. A registration that loses the race between
that check and registerMBean is skipped the same way, rather than failing createCache after the
cache was published (`JmxRegistrationTest.register_registeredConcurrently`). Switching to
ObjectName.quote would break operator tooling; do not do so solely to eliminate the collision.

enableManagement/enableStatistics on an unknown name is a no-op; RI raw-NPEs. The spec's null-name
NPE and closed-cache ISE do not mandate the RI's behavior for absence, and closed caches are
removed from the registry before these paths can see them. Pins:
`CacheManagerTest.enableManagement_absent` / `enableStatistics_absent`.

## Lifecycle

### Executor ownership and asynchronous work

A configured ExecutorService is cache-owned and is shut down on close (`PMD.CloseResource`
suppression documents this). The default ForkJoinPool common pool ignores shutdown. To share an
executor, supply a plain Executor such as `shared::execute`: it bypasses the ExecutorService
shutdown check and, being non-AutoCloseable, the trailing tryClose. A singleton ExecutorService
returned raw by a factory does not opt out of ownership; do not add a shutdown flag. The JCache spec
does not specifically require executor shutdown; its named Closeable list is loader, writer,
listeners, and expiry policy. Ownership is the adapter's resource policy. The executor is the only
vendor factory product the cache releases; a configured `Scheduler`, `Copier`, `Ticker`, `Weigher`,
or native `Expiry` is borrowed, as in core. `Scheduler` has no close contract and none of its stock
implementations owns a thread (`forScheduledExecutorService` adapts the caller's service), so the
application releases a scheduler that starts one. A scheduler named by class in HOCON is instead
created per cache by `FactoryBuilder.ClassFactory`, so one that starts its own thread leaks it per
cache lifecycle; share one through a factory that returns the application's instance. cache2k
instead closes an `AutoCloseable` scheduler with the cache, as its `Scheduler` interface documents.

`inFlight` tracks explicit asynchronous loadAll work, including its CompletionListener
notification, with a bounded 10-second close await. `loadAllAndNotify` returns the notification
future; admission, synchronous submission-failure handling, and retirement stay in loadAll.
Synchronous executor rejection preserves notification-before-retirement in `loadAll`, ensuring
`close()` awaits `listener.onException` as it does on accepted paths.
Compose every outcome onto `dispatcher.chainSynchronous()`, so a failed load, a loader `Error` or
checked exception included, is notified after its committed entries' synchronous listeners, with
a listener failure suppressed onto the load's. Do not move it to an untracked continuation or
join the chain in the load body: a single-thread executor must be released to run the listener
dispatch. A stuck listener, or a CompletionListener that invokes `close()` directly or indirectly
(self-close), waits out the ten-second timeout, reported as TimeoutException. An `Error` from
`executor.execute` is governed by standing executor/JVM-Error principles; the bounded await ensures
it does not hang indefinitely. The bounded await does not promise that timed-out work has stopped.
A loader `Error` during `Cache.loadAll` never notifies the RI's `CompletionListener` (its runnable
catches only `Exception`), where Caffeine and Hazelcast deliver `onException(CacheLoaderException)`;
a `CompletionListenerFuture` on the RI would block forever.
`close()` sets the closed flag and deregisters under the configuration monitor, which admission and
registration also check, and awaits outside it. A second, concurrent `close()` (directly, or through
`CacheManager.close()` or `CachingProvider.close()`) returns once it sees the flag, possibly before
the first closer has released the executor, loader, writer and listeners, and a manager close may
not see the cache at all once the first closer deregistered it; the spec does not say a close must
wait for another, and this is accepted; an awaited listener that reads the configuration
would otherwise wait out the timeout. Pins: `CacheProxyTest.loadAll_loaderFailure_notifiesListener`,
`loadAll_storeFailsMidway_notifiesAfterSynchronousListeners`,
`close_awaitsInFlightLoadAll`, `close_awaitsRejectedLoadAllNotification`, and
`close_awaitsListenerReadingConfiguration`.

Manager close still holds its monitor across child close. A CompletionListener that looks up
a cache through that manager can therefore wait until the child's ten-second timeout. This
bounded shutdown delay is accepted: neither shutdown latency nor a general callback-disposal
barrier is promised. The cache-level configuration-monitor repair does not establish a broader
manager-close guarantee, and ordinary lookup is not classified as invalid lifecycle reentry.

Native background refresh is best-effort and is not added to inFlight or awaited through
`policy().refreshes()`. Blocking close on arbitrary user-executor refresh work was rejected.
Closed-cache event suppression comes from `EventTypeAwareListener.dispatch` checking the source's
isClosed, independently of this barrier. An owned executor's shutdown rejects new work only through
its rejection handler. With a handler that drops work once the pool is shut down
(`CallerRunsPolicy`, `DiscardPolicy`, `DiscardOldestPolicy`), an operation admitted before `close()`
whose synchronous listener dispatch is submitted after the shutdown never has that dispatch run:
its `put`, read-through `get` or `invoke` waits forever and ignores interrupts, and a `loadAll`
caught the same way never notifies its CompletionListener while close waits out its timeout. This
is accepted. The spec defines neither operations racing close nor executor shutdown, the handler is
the application's choice, and a complete repair needs the operation-versus-close coordination
rejected below (an `isShutdown()` check before submission only narrows the race). `AbortPolicy`
surfaces the accepted `CacheEntryListenerException` instead, and a shared plain `Executor` is not
shut down.

### Racing operations and shutdown

The 1.1.1 “Closing a Cache” contract rejects future operational use but does not synchronize
already-running operations; “Consistency” leaves concurrent behavior implementation-dependent,
and close need not destroy contents. An operation that passed its entry check may therefore
finish after close's invalidateAll. Retained local contents are eventually reclaimed with the
cache. Do not add operation-versus-close locking for this accepted boundary.

Straggler loads, invoke, or background refresh can reach a closed loader/expiry policy.
Recorded refresh handling catches/logs failures and records load failure; policy runtime
failures use their defaults. Close's trailing invalidateAll clears refresh ownership, preventing
a later owned-refresh commit; a refresh committed before the sweep is removed by it. This
refresh-specific guard does not imply all in-progress operations are canceled. In-flight user
I/O remains the user's lifecycle responsibility.

Executor rejection reaches synchronous callers after commit; see [dispatch and commit](#dispatch-and-commit)
for draining requirements and the superseded first-event rejection mechanism. Throwing
[native extensions](#native-extensions) retain their accepted pending-notification residual.

### Destroy and iteration

destroyCache clears before closing; close retains its trailing invalidateAll as a concurrency
sweep. The required clear→close sequence does not require retaining the registry entry until
both complete: removing it first agrees with Ehcache 3, Infinispan, Coherence, and Hazelcast;
RI keeps it until close. Pin: `CacheProxyTest.destroyCache_clearsBeforeClosing`.

Explicit clear removals do not notify listeners/writers: JCache uses core's evictionListener,
not removalListener. Natively expired residents are the previously documented exception because
their cause is EXPIRED rather than EXPLICIT; do not claim clear is unconditionally event-silent.
A resident whose JCache deadline passed but whose native deadline has not (the sub-millisecond
mirror gap, or any vendor native expiry) is cleared silently like a live one, as the RI's `clear()`
is for every resident; `removeAll()` reaps the same prior as EXPIRED with an eviction.

iterator.remove shares remove(K)'s removal path, removing the last-returned key even if its value was
replaced since next. It checks closure and expiry through that operation: an entry expired in
between emits EXPIRED/eviction, not REMOVED/removal. cache2k, Infinispan, Coherence, and Hazelcast
also delegate; RI retains an unconditional inline REMOVED path (source comparison 2026-07-20).

The iterator consumes its removal opportunity after the backing removal succeeds, before
awaiting listeners. A post-commit listener failure must not permit another removal without
`next()`, while a writer failure before commit leaves the original entry and retry opportunity.
Pins: `CacheProxyTest.iterator_remove_listenerFails_doesNotRemoveReplacement`,
`iterator_remove_listenerFails_doesNotDeleteTwice`, and `iterator_remove_writerFails_remainsRetryable`.

`next()` copies an entry before recording it, so one that fails to copy is skipped without counting
a hit or becoming the entry `remove()` deletes; the RI likewise converts before remembering its
last entry. Pin: `CacheProxyTest.iterator_next_copierFails_doesNotReturnTheEntry`. The hit is
counted in `next()`, once a value is returned; the RI and cache2k count it when `hasNext()` stages
the entry, Ehcache 3 and Hazelcast in `next()`.

---

### .Claude Docs Research Foundations (`.claude/docs/research-foundations.md`)

# Research Foundations

Papers and talks that informed Caffeine's design, mapped to implementation.

## Design Philosophy

From the "Design of a Modern Cache" talk:
- Use O(1) algorithms for predictable performance
- Optimize the system, not a single metric
- Tradeoffs based on simulations
- Must be correct & maintainable
- APIs with low conceptual weight

Performance mantras:
 - Don't do it
 - Do it, but don't do it again
 - Do it cheaper
 - Do it less
 - Do it later
 - Do it when they're not looking
 - Do it concurrently

## Core Design

### TinyLFU: A Highly Efficient Cache Admission Policy
Einziger, Friedman, Manes. ACM Trans. Storage, 2017.
https://doi.org/10.1145/3149371

**What it introduced:** Frequency-based admission policy using a compact approximate
LFU structure (counting Bloom filter / Count-Min Sketch). The combined replacement
and eviction policy W-TinyLFU uses a small window (LRU) feeding into a main space
(segmented LRU), with TinyLFU as the admission filter between them.

**Implementation:**
- `FrequencySketch.java` — 4-bit Count-Min Sketch with periodic halving (aging)
- `BoundedLocalCache.java` — W-TinyLFU: window deque → admission filter → probation/protected deques
- Admission decision: `admit()` compares frequency of candidate vs victim
- Window size: ~1% of total by default, adaptive via hill climber

### Adaptive Software Cache Management
Einziger, Eytan, Friedman, Manes. Middleware '18, 2018.
https://doi.org/10.1145/3274808.3274816

**What it introduced:** Two mechanisms for auto-tuning the window-vs-main partition
in W-TinyLFU: hill climbing (gradient-free optimization) and an indicator-based
approach. The hill climber adjusts the partition size to adapt to workload changes
without manual configuration.

**Implementation:**
- `HillClimberWindowTinyLfu` in simulator — the paper's reactive climber (research prototype)
- `WindowClimber.java` — the production climber (applied by `BoundedLocalCache.climb()`). The
  shipped design has advanced beyond the paper: reactive below 4096, and above it a
  within-sample density signal with a starvation-guarded probe machine whose up-probe verdict
  prices capacity at main's probation margin (see `hill-climber.md` and
  `wiki/adaptive-window.html`). The simulator deliberately carries no faithful reference of it —
  `product.Caffeine` (the real cache) is the arbiter
- Adjusts window percentage up/down based on observed hit rate changes

#### What MiniSim actually models

MiniSim's premise is simple: run small, sampled copies of the cache at several window sizes and
let their miss counts choose the full cache's next size. That comparison is meaningful only when
each small copy represents a state the full cache can actually reach.

The 2026 alternative-climber study found two separate implementation problems, and both yielded
small corrections to the registered MiniSim implementation:

1. **The old sampler selected far too many keys.** Java's `%` keeps the sign of its left operand,
   so `hash % R < 1` accepts every negative hash in addition to the intended bucket. At
   `R = 1000`, it selects about 50.05% of the hash space instead of 0.1%. This problem is already
   fixed on main: current `MiniSimClimber` uses `floorMod`.
2. **The old miniature cache shapes did not match the live cache shapes.** It built each arm as an
   ordinary static `WindowTinyLfuPolicy`, which recomputed the protected/probation split whenever
   the window percentage changed. The live climber instead transfers capacity between window and
   protected while leaving probation fixed. In a 1,000-entry cache, the live default starts at
   `(window=10, protected=792, probation=198)` and reaches an 80% window at `(800, 2, 198)`. The
   old 100-entry miniature modeled that arm as `(81, 15, 4)` rather than the reachable
   `(80, 0, 20)`, so it could rank the wrong cache. MiniSim now constructs the full cache's
   reachable integer targets first, keeps probation fixed, applies the live rail, removes aliases,
   and scales those exact shapes into miniature caches. It also moves from the host's actual
   integer window size to the selected target; it no longer assumes that percentage arithmetic
   landed on the requested coordinate. Its epoch clock starts when the host fills (earlier
   requests only warm the miniatures), and an epoch whose miss vector is uniform, such as a
   period with no sampled requests, holds rather than electing the first arm.

This correction deliberately does not transplant the rejected 27-policy experimental controller.
That prototype also gave every arm the same random admission draw for each sampled request. Main's
registered MiniSim still gives arms equal seeds whose conditional admission contests may consume
different draw indices. Its geometry and movement are now faithful, but it is not yet an exact
common-random counterfactual panel.

Keep these rules for any future shadow or miniature simulator:

- Model the exact reachable `(window, protected, probation)` state, not just a window percentage.
- Clamp and deduplicate full-cache targets before scaling them down.
- Compute integral movement from the acknowledged live state, and test arrival and reversal in
  both directions.
- Give every arm the same request, frequency state, and request-indexed random draw. Equal seeds
  alone are not enough when arms consume randomness conditionally.
- Start and score epochs explicitly. A period with no sampled evidence must abstain rather than
  select the first arm because every score tied at zero.
- Judge prediction quality separately from implementation cost. A shadow can identify a good
  target and still be too large or expensive to run in production.

The paper's Mini-Sim result, the 2018 repository version, and current `MiniSimClimber` are therefore
three different artifacts. The paper does not identify the exact binary behind its figures, and
the repository later changed hashing, sampling, fullness timing, and clock behavior. A current run
is evidence about current code, not a paper reproduction, unless those differences are controlled
one at a time.

### Lightweight Robust Size Aware Cache Management
Einziger, Eytan, Friedman, Manes. ACM Trans. Storage, 2022.
https://doi.org/10.1145/3507920

**Relationship:** Co-authored advisory role. Analyzes Caffeine's existing weighted
eviction approach and proposes an alternative size-aware admission extension to
TinyLFU. Caffeine already supports variable-sized entries via its `Weigher` API
and weighted eviction. The paper compares Caffeine's approach against their
proposal and other size-aware algorithms (AdaptSize, LHD, LRB, GDSF),
demonstrating competitive hit ratios and byte hit ratios with 3x lower CPU
overhead. The alternative approach was never adopted into Caffeine — it came
after the fact and there was no need to revisit the existing design.

**In simulator:** Several size-aware policies implemented for comparison (CAMP, GDSF,
GDWheel in `greedy_dual/`).

## Concurrency Architecture

### BP-Wrapper: A System Framework Making Any Replacement Algorithms (Almost) Lock Contention Free
Ding, Jiang, Zhang. ICDE 2009.

**What it introduced:** Batching and prefetching to eliminate lock contention from
replacement algorithms. Instead of acquiring the policy lock on every access, buffer
operations and process them in batches under the lock.

**Implementation:**
- `BoundedBuffer.java` — striped ring buffer for read recording (lossy, lock-free)
- `MpscGrowableArrayQueue.java` — write buffer with guaranteed delivery
- `afterRead()` — offers to read buffer, schedules drain
- `afterWrite()` — offers task to write buffer, inline fallback if full
- `maintenance()` — single-threaded batch processing under evictionLock

## Expiration

### Hashed and Hierarchical Timing Wheels
Varghese, Lauck. IEEE/ACM Trans. Networking, 1997.
https://doi.org/10.1109/90.650142

**What it introduced:** O(1) timer start/stop using circular buffers (timing wheels)
with hierarchical extensions for large time ranges.

**Implementation:**
- `TimerWheel.java` — hierarchical timing wheel with 5 levels
- Bucket widths: ~1s, ~1min, ~1hr, ~1.6d, ~6.5d (powers of 2 in nanos)
- O(1) insert/delete via doubly-linked list per bucket
- Cascading: entries demoted to finer-grained wheels as time approaches
- Used for variable expiration (`expireAfter(Expiry)`)

**Bucket width is the resolution, and that is the structure, not a Caffeine choice.** A wheel
advances in whole ticks, so an entry due sooner than the next tick waits for it. That is what buys
the O(1) insert and delete the paper is about, and it applies to every hashed wheel, not to this
implementation. The level-0 tick here is `ceilingPowerOfTwo(1s)`, `2^30` ns or 1.074s, so variable
expiration is announced within about a second of its deadline rather than at it. See
`design-decisions.md` §TimerWheel for the measurements and for why the cache context accepts it;
the short version is that a cache promises a maximum lifetime, not a scheduling resolution.

## Security

### Denial of Service via Algorithmic Complexity Attacks
Crosby, Wallach. USENIX Security 2003.

**What it introduced:** Demonstrated hash-flooding attacks against hash tables,
causing O(n) degeneration per operation.

**Relevance:** Caffeine delegates to `ConcurrentHashMap` which uses tree bins
(red-black trees) for collision resistance in JDK 8+. The frequency sketch has no such
resilience: `spread` and `rehash` re-mix the key's `hashCode()` to distribute keys across
blocks, but they are functions of it, so keys with equal hash codes share all four counters
and read as one key. The admission filter admits 1 in 128 rejected candidates whose frequency
is at least 6, which answers a victim whose frequency an attacker has raised. It does not act
on a candidate whose frequency collisions have raised, since that candidate beats a colder
victim outright: distinct one-hit keys sharing one `String.hashCode()` enter probation and hold
it as frequency-15 victims. Measured on a Zipf 0.99 workload with a 10,000-entry cache and the
default executor, such keys at 1% of traffic cost about 5 points of hit rate, where the same
volume of distinct-hash one-hit keys cost none; protected was never reached. Not a direct
implementation influence, but informs the threat model.

---

### .Claude Docs Ruled Out (`.claude/docs/ruled-out.md`)

# Ruled Out

Adjudicated mechanisms and their scope. Most are accepted behavior; entries that describe a
repaired mechanism are marked as historical. Use the reason and current code, not an earlier
review's outcome.

**When to read this for an audit**: Phase 1.5, after your own findings are written down, alongside
`design-decisions.md`. Never before Phase 1 analysis. Read your module's section plus
*Standing principles*; the rest is not yours.

Optimization experiments read *Performance* before selecting hypotheses, to avoid repeating
adjudicated experiments. This does not change the independent first pass of an audit.

**What a match means**: label the finding "ruled out: <entry>" and **keep it in the report**.
A ruling is about a mechanism and a consequence. If you have the same mechanism with a
*different* consequence, a reachable trigger the entry does not name, or a configuration it
does not cover, that is a new finding and the entry does not dispose of it. Say which part
differs.

Check claimed public wording mismatches against the public text as well as the implementation
ruling. Acceptance of behavior does not establish that the public text describes it, but silence
alone is not a contradiction. Preserve explicit decisions not to add qualifications or warnings.

Rulings can be overturned. Address the stated reason when new evidence changes the consequence,
trigger, or configuration.

---

## Standing principles

These dispose of whole families. Check them first.

- **A JVM `Error` is not our problem.** OOME and SOE leave the JVM corrupt. A window that
  opens only between an OOME and the next statement is not a defect, and defensive
  containment for it is not wanted.
- **A contract-violating user component is the user's bug.** A hostile `CompletableFuture`
  (throwing `isDone`/`whenComplete`), a throwing `Ticker`, broken `equals`/`hashCode`, a
  mutating `Weigher`. Caffeine breaks reasonably and pushes back; it does not add defensive
  ceremony or a javadoc note. Precedent: Quarkus shipped a broken `CompletableFuture`,
  Caffeine pushed back, Quarkus fixed it.
- **Value-bearing callbacks propagate; fire-and-forget callbacks are guarded.** A loader,
  weigher, expiry or ticker returns a value, so there is no default to recover to and the
  throw propagates. Listeners and `StatsCounter` return nothing, so they are guarded. This
  asymmetry is deliberate; do not report it as inconsistency.
- **A broken or misconfigured executor is user error.** Silent-discard, `AbortPolicy` on a
  bounded pool, `shutdownNow()` dropping accepted tasks, a never-completing loader, or an
  `execute` that waits for its task or for queue room (one that joins the thread it dispatched
  to, a bounded pool whose rejection handler blocks). The executor is user-owned and its
  lifecycle is the user's choice. A waiting `execute` deadlocks because the drain task and
  eviction's removal notifications are submitted under `evictionLock` (`synchronization.md`,
  *notifyRemoval*). Submitting outside the lock would fix only the joining executor: maintenance
  runs on the executor, so a lone worker still waits on its own full queue. The same holds for the
  default common pool disabled JVM-wide (`java.util.concurrent.ForkJoinPool.common.parallelism=0`
  or a common-pool thread factory returning null). Removal notifications are never delivered, and
  each queued one retains its entry. Maintenance still runs inline when the write buffer fills and
  on `cleanUp()`, which expires at most `EXPIRATION_THRESHOLD` entries per call and hands the rest
  to the dead pool. At parallelism 0 async loads keep running: `CompletableFuture` replaces an
  explicit `commonPool()` with a thread per task through JDK 24, and from 25 raises the pool's
  parallelism to 2 when it initializes, which also restores maintenance and notifications if no
  backlog has built up. A null factory stops async loads too. The library does not detect either
  ("Remove detection of JDK-8274349"): the JDK documents a zero setting as leaving unjoined tasks
  unexecuted, and from 25 strongly discourages it. A parallelism check, a caller-runs fallback, the
  `AsynchronousCompletionTask` marker and routing through `CompletableFuture.runAsync` were
  declined. So was more `Caffeine.executor` javadoc for any of these cases, a rejecting or
  caller-runs executor running listeners under the eviction lock included: its existing caution
  and the internal guidance suffice. Such an executor runs the removal listener under the lock and
  inside the expiration and eviction scans, so a listener that waits on another thread's
  `cleanUp()` deadlocks. Deferring those inline notifications to the lock's release fixes that and
  measured performance-neutral, but was declined: the eviction listener still runs inside the
  scans under the lock, so the scans keep their reentrancy defenses, and the tests that guard them
  (`ReentrancyFuzzer`, `BoundedLocalCacheTest.maintenance_recursive*`, the `ExpirationTest` scan
  pins) re-enter through a removal listener on `Runnable::run`. Under the deferral they reach no
  scan: with the deque scans' resume-from-head defense removed, the four deque `maintenance_*`
  pins and the four `ExpirationTest` scan cells fail at HEAD and pass with the deferral. Revisiting
  it starts with moving those tests onto the eviction listener.
- **Statistics are best-effort and lowest priority.** A counter that races, drifts, or
  double-counts is not a correctness defect. `CacheStats.missRate`'s
  `missCount >= loadSuccessCount + loadFailureCount` says a miss need not load (`getIfPresent`);
  a refresh or `asMap` compute counting a load without a miss does not make it a defect, and the
  same reading covers `loadSuccessCount`/`loadFailureCount`'s "always" wording, which the javadoc
  keeps deliberately rather than taking Guava's "usually".
- **Anything reachable only through `Cache.unwrap(...)` is out of scope.** By the JCache
  spec `unwrap` is an ill-defined hack; once used, behaviour is undefined, however real the
  symptom.
- **Do not construct the trigger and then report the result.** A `Factory` that calls
  `getCache`, a `Weigher` that mutates the cache, a loader that throws synchronously.
  Writing the misuse is manufacturing the defect. "Nothing warns the user" is not an
  argument: the omission is deliberate because the callback has no reason to do it.
- **Price severity on a production configuration.** A frozen `FakeTicker` and
  `executor(Runnable::run)` are instruments that manufacture impact nobody can reach. See
  `finding-taxonomy.md`, *Severity must be priced on a realistic configuration*. A real
  mechanism and a reachable impact are two separate claims.
- **Lossy and approximate by design.** Read-buffer drops, sketch counter saturation,
  eventual consistency of weight and size. Sub-1% hit-rate deltas on a probabilistic sketch
  are noise, not signal.

---

## Performance

These rulings concern an optimization mechanism and its evidence, not a permanent claim about
which code can become faster. Add a confirmed negative result with its affected path/configurations,
observations, reason for rejection, and the evidence that would justify reopening it. Include
enough detail to assess the ruling without local artifacts; raw runs stay in the experiment
workspace, and no `.local/` path is a durable evidence reference. Keep inconclusive measurements
labelled inconclusive rather than turning absence of a signal into a dead-code claim.

- **Caller-side relocation of constant-foldable expiration conditions is not demonstrated removed
  work.** `BoundedLocalCache.expireAfterUpdate` returns zero when variable expiry is disabled;
  `exceedsWriteTimeTolerance` returns false when write expiry, refresh, and variable expiry are
  absent. Duplicating those conditions in `put` changes compilation shape, but the disabled paths
  already have constant results. The source transformation and a better stress score do not
  establish useful work eliminated by C2. Reopen only with native-code evidence of an executed
  cost and paired confirmation across the affected read/write cells, not another source-layout
  sweep. A compiler-directed improvement with that evidence is a different claim.
- **Resident overwrite throughput does not establish write-queue contention.** In the size-only,
  strong-reference `GetPutBenchmark` configuration, existing unchanged-weight updates in `put`
  reach `afterRead`, without submitting an `UpdateTask`. Sharding the MPSC write queue cannot
  remove cost from an operation that does not enqueue there. Reopen for a workload that exercises
  actual write-buffer traffic, such as insertion or weight/expiry updates, with queue-specific
  contention or backpressure evidence. This does not rule out write-queue improvements generally.
- **Producer-side MPSC tricks in the read buffer do not help the cache.** Measured on an Apple M3 Max
  (JDK 27, 128-byte lines) with `GetPutBenchmark`-shaped read_only and readwrite cells, each arm a
  separate `RingBuffer`/`StripedBuffer` class selected per fork. On a quiet host, five paired rounds
  put a drain that never reads the write counter, with the array reference on the header's line and
  the array padded off the write counter's line, at +0.5% [-1.4, +2.5] (read_only) and -0.7%
  [-3.4, +2.0] (readwrite gets), and a cached consumer index beside the write counter (JCTools'
  `producerLimit`) at -0.1% [-1.8, +1.6] (readwrite gets). A FastForward slot check in place of the
  read counter also gained nothing and is lossy under a producer stall. The drain runs in scheduled
  batches, so the counter lines move once per batch per stripe; in saturation about 98% of offers
  return `FULL` from L1-resident lines. Only a stress where one producer races a continuously polling
  consumer rewarded keeping the consumer off the producers' line (offers from 33-54 ns to 4 ns).
  Reopen for a design that drains continuously against producers.
- **A different stripe hash only reshuffles which thread ids collide.** A one-multiply Fibonacci hash
  in place of `mix64` confirmed at +5.5% [+0.4, +10.9] on read_only and +6.3% [+2.2, +10.6] on
  readwrite gets (10 pairs), but because JMH's worker ids (31-38) share two home stripes under
  `mix64` and one under the new hash in the 16-stripe table the benchmark settles at. It is not a
  latency effect: the non-recording offer path moved +0.6% [-4.0, +5.4]. Over arbitrary runs of
  consecutive ids both hashes collide equally at 16 stripes, and the new hash is worse for
  power-of-two id strides. Growing the table on the first failed home CAS, which removed every home
  collision for those ids at 64 stripes, moved readwrite gets +3.0% [-1.1, +7.2] (5 pairs):
  inconclusive, and it creates more stripes. Reopen with a per-stripe CAS-failure count showing that
  home collisions cost throughput across thread-id sets, not for one benchmark's ids.
- **Plain slot clears in `BoundedBuffer.drainTo`.** Clearing each slot with a plain store instead
  of `setRelease(null)` is correct, since the release store of `readCounter` after the loop orders
  the clears, and on aarch64 it roughly halves the drain's cost per record: C2 emits each release
  store as a barrier plus a store, and the barrier waits on the previous element's `onAccess`
  writes to node lines that readers hold. Rejected anyway: tooling flags plain stores in the ring,
  and the gain is only maintenance time. When unsaturated, readers are unchanged. When saturated,
  the cheaper drain records more reads, so GetPut read cells score lower. Don't re-raise without
  a user-visible cost that the release clears cause.
- **Avoiding the policy's writes to hot nodes' cache lines.** A hit loads the node's `value`, which
  shares a line with the access-order links that `moveToBack` rewrites on the node, its neighbors,
  and the old tail, so a saturated read benchmark pays coherence misses on its hottest keys (the
  cost was never isolated). Deduplicating reorders within a drain needs a membership structure,
  such as a set of nodes or key hashes, whose allocation and lookup cost more than the moves; the
  sequential check is already there (`moveToBack` skips the tail). Moving the links off the node
  adds a mapping and a structure to size. In applications, the work between reads absorbs the
  invalidations. Accepted as the cost of LRU ordering.
- **Skipping the timer-wheel move for a drained read under `Expiry.creating` or `writing`.** Such a
  read never moves the deadline, so `onAccess`'s `reschedule` only relinks the node within its
  bucket, writing the links of the node and its neighbors. Skipping it measured -2.2 ns/op
  (se 2.3, 8 of 12 pairs faster) on bounded `Expiry.writing` hits with 8 threads (M3 Max, JDK 27,
  a cached-clock ticker, a loaded host). The gain is maintenance time only, which the plain slot
  clears ruling above does not count. `Expiry.accessing` is excluded either way: `oldest()` reads
  the wheel's order, and an extended node would keep its old bucket until the sweep. Reopen with a
  reader-side gain on a quiet host.
- **Padding `StripedBuffer`'s `table` field (inconclusive).** Every hit loads `table` from a
  16-24 B object, a different object from the padded rings, whose neighbours the collector picks.
  After a young or full GC settle, JOL found the climber's `Sample` (written per drained read) or the
  eviction lock's `NonfairSync` (written per drain) on that 128-byte line in 31 of 48 placements
  across JDK 17/25/26/27, 18 of 48 with 64-byte lines. Padding the field by inheritance, BBHeader's
  shape, measured on JDK 27 with 20 natural-placement forks: read_only 15.29 vs 15.52 ns/op (paired
  +0.23, se 0.48), and readwrite 22.09 vs 22.51 with puts unchanged. The lock state beside the field,
  in 8 of 20 base forks, cost nothing measurable. The `Sample` never landed there on JDK 27, so that
  placement is unmeasured, and a padded object's header, which the hit's type check loads, stays
  exposed. Reopen with placement-controlled pairs at the `Sample`-adjacent placement (see
  `/audit-performance`'s pricing traps), on JDK 25 or 26 where it is common.
- **Batching removal notifications, or submitting them after the eviction lock's release.**
  Eviction and `removeNode` submit one executor task per notification while holding `evictionLock`
  (`synchronization.md`, *notifyRemoval*). With a no-op listener on the common pool, saturated
  inserts into a full 10k cache cost about 150 ns wall and 1.5 µs of process CPU each, most of it
  in waking pool workers (JDK 25, M3 Max). Collecting a lock hold's notifications and submitting
  them after the release was built in several shapes, and each trades one regime for another.
  Delivering a hold's calls in one task, or in chunks of 64, cut that CPU about tenfold, but runs a
  burst's calls one after another: an `invalidateAll` of 64 entries whose listener blocks for 1 ms
  finished delivering in 96 ms instead of 8. Chunks of 16 still cut it about sixfold and took
  21-24 ms. One task per notification after the release kept the burst, but doubled saturated CPU
  and raised the writer's p99 at 1M evictions/s from 2.3 to 13 µs. Dividing each hold into at most
  NCPU tasks kept the 64-entry burst, but regressed 1M/s like the per-notification shape and slowed
  a 1,024-entry burst from 100 to 171 ms. No shape holds both high-rate CPU and slow-listener
  parallelism, so the per-notification task stays. The drain task's own submission under the
  `tryLock`, which a worker that starts at once waits out, was not priced and stays too. Reopen
  with a shape that keeps the 64-entry burst and the saturated and 1M/s CPU together, or with a
  production workload whose cost is the in-lock submission. Deferring only the notifications that
  would run inline is under *A broken or misconfigured executor is user error*.
- **Moving the eviction lock away from the cache object.** The constructor allocates the lock
  first, so in allocation order the lock and its `Sync`, whose `state` every lock, unlock and
  `tryLock` CASes, follow the cache object. With the eviction counters padded, only an expiring or
  refreshing cache has fields that a hit loads at its end (its duration and ticker), 24 to 40 bytes
  before the `Sync`. On an Apple M3 Max (JDK 27, eight readers, a writer at 500k put+invalidate/s),
  a reachable 128-byte gap between the cache and the lock, which keeps every later object's
  alignment, left the hit unchanged (-0.21 ns [-0.83, +0.38]) and cut the writer's put+invalidate
  from 351 to 296 ns, most of it at the placements where the `Sync` shares the duration's
  128-byte line (78 ns there): the writer's lock CASes take a line that the readers hold. Allocating
  the write buffer first, which puts its leading padding after the cache at no memory cost, made the
  hit 1 ns slower (23.2 to 24.2 ns) and moved a writer cost of similar size elsewhere, since it also
  rearranges the objects that follow. No collector keeps this neighbour for a cache in use: G1 and
  Parallel put the write buffer after the cache at its first young collection, and ZGC relocates a
  cache that readers are touching on its own (nothing within 128 bytes of 9 of 10 caches). The cost
  lasts only until the cache first moves, so a pad would cost memory for nothing after that. Reopen
  with a collector or workload that keeps the lock beside a cache in use.
- **Opaque reads of the expiration and refresh durations.** The generated duration getters are
  `getAcquire` (the fields were plain `volatile` until #1897 gave `maximum` and `weightedSize`
  lock-free readers), although the durations publish nothing else, so `getOpaque` would keep the
  64-bit atomicity. C2 on JDK 27 emits each acquire load as `ldar`: two per expire-after-access hit
  (`hasExpired`, `setAccessTime`), one per expire-after-write hit, and the opaque build emits plain
  `ldr` instead. On an Apple M3 Max (eight readers and a writer, mean over 16 placements) opaque
  measured +0.34 ns [-0.56, +1.16] per expire-after-access hit and +0.32 [-0.42, +1.15] per
  expire-after-write hit on JDK 27, and +0.40 [-0.28, +0.92] on JDK 25; pooled over 64 pairs, +0.35
  [-0.16, +0.84]. That is within noise, and the `readBuffer` ruling keeps the better-specified mode
  when the weaker one is not measurably cheaper (`design-decisions.md`, "Read recording starts with
  the frequency sketch"). Reopen with a resolved gain, such as on a platform where an acquire load
  costs a fence.
- **Separating `ConcurrentStatsCounter`'s eviction adders from its hit and miss adders.** Under
  compact object headers, the default on JDK 27, a `LongAdder` is 24 bytes and the counter allocates
  its seven back to back, so `hitCount.cells` sits 108 bytes from `evictionCount.base` and
  `missCount.cells` 84: in allocation order they share a 128-byte line at 2 and 5 of the 16
  placements, and maintenance CASes `evictionCount.base` on every eviction. (Default headers give
  32-byte adders, with `hitCount` never on that line and `missCount` at 1 of 16; once the readers'
  contention has inflated `hitCount`, a young collection copies its cells right after it, away from
  the others.) On an Apple M3 Max (JDK 27, eight readers, a writer evicting about 500k entries/s),
  moving the eviction adders 120 bytes further gained nothing at the two placements where `hitCount`
  shares the line (-1.57 and +0.10 ns per lookup, against -0.95 at a control placement). Statistics
  are best-effort and lowest priority. Reopen with a measured cost at the sharing placements under an
  eviction rate an application reaches.
- **Reusing one `ForkJoinTask` as the maintenance task, to skip the pool's per-submission wrapper.**
  `ForkJoinPool.execute(Runnable)` wraps only a non-`ForkJoinTask`, and the maintenance task is one
  object resubmitted for the cache's lifetime, by `scheduleDrainBuffers` and by the pacer. A worker
  stalled between its base recheck and the slot CAS can match a later copy of the same object and
  move the queue's base back by a multiple of the array length; JDK 22 and later then reject every
  submission to that queue, and earlier JDKs lose tasks silently (#2021). `reinitialize()` before
  each submission and a rotated set of tasks do not help, since the race needs only the same
  reference in the same slot. A fresh `PerformCleanupTask` per submission would allocate on every
  executor, where a thread pool queues the raw `Runnable` and needs nothing, and is the same size as
  the pool's wrapper (32 bytes on JDK 25 and 26), so the pool's one wrapper costs no more. Measured
  on JDK 25.0.1 (`GetPutBenchmark` and a `weakKeys()` copy, 3 forks, `-prof gc`): throughput within
  the 99.9% confidence interval, allocation up by at most 0.06 B/op, 0.4 B/op for weak-key
  read/write. `BoundedLocalCacheTest.scheduleDrainBuffers_notForkJoinTask` and
  `pacer_notForkJoinTask` pin the type on both paths. Reopen only for a design that never hands the
  same object to the executor twice; an upstream fix does not reopen it while older JDKs are
  supported. Details in [Known JDK Interactions](design-decisions.md#known-jdk-interactions).

---

## Core

**Eviction and maintenance**

- Colliding keys inflating a new candidate's frequency so that TinyLFU admits it outright, before
  the jitter is consulted. Keys with equal `hashCode()` share every sketch counter, so distinct
  one-shot colliding keys read frequency 15 and displace colder probation victims; at 1% of traffic
  on a Zipf 0.99 workload with a 10,000-entry cache they cost about 4.6 points of hit rate
  (research-foundations, *Security*). The jitter exists to let an inflated entry be evicted, not to
  prevent its admission: an admitted candidate becomes an inflated probation victim like any other,
  and keys that are not reused never reach protected. No seed separates equal hash codes, and
  naming the candidate side in `BoundedLocalCache`'s class comment was declined.
- A weight change replayed after the fact evicting the mapping that replaced it. A weighted
  entry rewritten oversize and then rewritten small again can be removed as SIZE once the
  buffered deltas drain, because `UpdateTask` decides from the accumulated `policyWeight`
  while the node already carries the new weight. Deterministic with a discarding executor:
  under `maximumWeight(100)`, three puts of weight 1, 1000 and 1 leave the cache empty, and
  the notification carries the small value. `AddTask` does the same from its captured insert
  weight when an oversize insert is rewritten small before the drain (puts of 1000 then 1).
  `weight` and `policyWeight` are owned by
  different locking protocols, so the replay is inherent, and the javadoc's "may evict an
  entry before this limit is exceeded" covers the result. The price is an extra miss on a
  self-healing transient. Deciding either task's check from `node.getWeight()` closes only
  that path: the same replay inflates the global `weightedSize` across drain cycles and
  evicts through `evictEntries` instead, measured still losing the entry in 2 of 3
  default-executor runs, so it is a special case rather than a repair. Resurrecting a victim
  when size eviction no longer holds has the same gap, and does nothing for an update that
  is not itself oversize and pushes other entries out. Weight-0 pinning is unaffected, which
  is the case that would have made it more than premature eviction.
- `lock()`'s timed retry never reaching its uninterruptible fallback while another thread
  re-interrupts the writer faster than it loops, since `tryLock` checks the flag before trying.
  The loop rides out ordinary interrupts and restores the flag; a storm is a constructed trigger,
  and the writer acquires once it stops.
- The `Caffeine` class javadoc's list of configurations that "perform periodic maintenance"
  omitting `refreshAfterWrite`. A refresh-only cache is bounded so that it can refresh and
  submits a drain after writes, but that drain has nothing to evict, expire or collect; the list
  names maintenance that does work.
- Replacing `EvictContext.removed` with `ctx.cause != null` at `evictEntry`'s tail, as the
  siblings classify into a local cause. Equivalent today only because the resurrect return
  comes first; the explicit flag keeps the tail correct if an edit sets a cause on a path that
  neither removes nor resurrects, which is worth more than one field.
- Size eviction is uncapped and drains the whole excess in one cycle under `evictionLock`.
  Eager shrink is the published `Policy.Eviction.setMaximum` contract, and the uncapped
  property is load-bearing for the `rescheduleCleanUpIfIncomplete` piggyback: size eviction
  never arms the pacer, so a capped drain would be the one backlog shape with no driver.
- A `setMaximum` shrink below a resident's weight evicting entries that fit before it reaches
  that resident. Only the candidate has an oversize check, and every write that grows a weight
  past the maximum evicts that entry at once, so a shrink is the only trigger. The resident goes
  when the victim cursor reaches it: in probation when it loses a frequency duel, in protected in
  LRU order whatever its frequency, so a hot one can outlast the rest of the cache. The eager
  shrink holds, and weight "has no effect on selecting which entry should be evicted next". With
  Pareto weights capped at 3% of the old maximum no resident was oversized down to a 3% shrink,
  and at 2% and 1% the kept weight matched evicting the oversized residents first; only
  constructed populations flush (20-51 of 500 kept). Mirroring the candidate check on the victim
  fixes a probation victim only; a protected resident needs an O(n) pass per shrink.
- Without a `Scheduler`, maintenance is amortized onto callers and a quiesced cache stays
  over `maximumSize`. A `Scheduler` requests prompt expiration; a size-only cache has no pacer. The
  quiesced excess is capped by the write buffer (`estimatedSize() <= maximum + WRITE_BUFFER_MAX`),
  because a full buffer forces `afterWrite`'s inline assist. The live peak under concurrent
  writers is `maximum + 2 * WRITE_BUFFER_MAX + 1` plus the writers, since a drain applies its
  whole batch before evicting. The same debt can keep a removed value reachable: a removal that
  lands after a cycle's write-buffer drain leaves its `RemovalTask` queued, and the retired node
  holds a strong value until the next operation or `cleanUp()`. The removal is already notified,
  and weak or soft values are cleared at retirement. Do not add a third re-arm and do not move the
  resubmission into `PerformCleanupTask`; both were built and declined.
- `rescheduleCleanUpIfIncomplete`'s `!pacer.isScheduled()` gate deferring a REQUIRED backlog
  to the pacer's horizon is the same design. Same for the executor-reject catch in
  `scheduleDrainBuffers` not calling it.
- `rescheduleCleanUpIfIncomplete` missing a concurrent write's re-arm. Its inspection holds
  `evictionLock`, so a writer that sets REQUIRED after the status read can fail to schedule a
  drain. Accepted: the next write re-arms, and any debt left at quiescence is bounded by the
  write buffer. The controlled witness paused a generated subclass; 60 ordinary-runtime bursts
  did not reproduce it. Size-only caches have no pacer, as described above.
- The expiration and window scans are O(N) in the pending-async population. The walk is real
  (100k pending cost ~410 us per `cleanUp`). The expiration scans relink pending entries to the
  MRU end, so one completed entry takes `cleanUp` from 410,208 ns to 41 ns. `evictFromWindow`
  relinks nothing, so without `expireAfterAccess` a never-completing future or a weight-0 entry
  is walked on every over-budget cycle. Callers are unaffected unless writes saturate the write
  buffer (then 14-35x slower); the price is maintenance that never idles, a full core at 200k
  such entries even at 1k writes/s (18-30% at 10k). Both candidate fixes measured worse, and a
  pending-only relink would not reach weight-0 pinning. Residue that is real and documented:
  under `executor(Runnable::run)` the cost is linear per pending entry, so a burst is quadratic.
- Recursive and nested maintenance in `afterWrite`.
- Maintenance that a read inside a computation or eviction listener runs inline on a caller-runs
  executor, and the corruption or deadlock that follows; see
  [ConcurrentHashMap Constraints](design-decisions.md#concurrenthashmap-constraints).
- Expired entries persisting in an idle cache.
- `FrequencySketch.reset()` sweeping under `evictionLock` (238 ms at 100M). Amortized to
  0.24 ns/read, once per 1e9 reads. Chunking lowers quality; SIMD is the answer.
- `FrequencySketch.reset()`'s signed-shift quirk at extreme `maximumSize`, and
  `ensureCapacity` keeping a stale large `sampleSize` after a `setMaximum` shrink.
- `FrequencySketch` table/blockMask race via the `Policy` API. It is entirely under
  `evictionLock`: single-writer under one lock cannot race, and escalating it is a false
  positive.
- Transient negative `weightedSize`, and transient negative `policyWeight` over-shifting the
  climb transfer quotas or region caps. Convergence is via the telescoping sum; verify that
  instead. Confirming at a negative window can leave an anchor held but unplanted under its
  sentinel test. This lies outside the fuzzer's bounded geometry; no distinct harmful adaptation
  consequence was established, so the flag pairing alone does not justify a policy change.
- `DENSITY_EPSILON` attenuating proportional steering at extreme weight maxima. Production
  consumers use the verdict's sign; `steeringError()` contains no epsilon. Floating-point
  rounding can tie sufficiently close densities, but no harmful reachable tie was established.
- Weight=0 entries are a user-facing pinning feature.
- In-flight async entries are uncounted by any bound (weight 0 plus `ASYNC_EXPIRY`).
- The eviction-listener same-key mutation "corrupting or silently losing the write".
- A loader that joins a default-executor `CompletableFuture` deadlocking with maintenance on JDK 25
  and later at a common-pool parallelism of 1, the default on one or two CPUs. Through JDK 24,
  `CompletableFuture` replaced a common pool below parallelism 2 with a thread per task, including
  an explicit `commonPool()` argument; from 25 it uses the single worker (JDK-8319447; JDK-8360593
  was closed Won't Fix). Maintenance, notifications, async loads and the application's default
  futures then share that worker, so a loader that joins its own `supplyAsync` inside `compute`
  waits for a task queued behind maintenance, which holds `evictionLock` and waits for the loader's
  bin. Blocking async loads also run one at a time. Not ours to control: a loader that blocks a
  pooled thread is the application's problem, and one that must wait belongs on a virtual thread
  or a dedicated executor. The default executor stays the common pool until virtual threads are
  preemptive (#1898).

**Buffers and queues**

- `MpscGrowableArrayQueue.resize` stranding the odd producer-index marker on a non-OOME
  throw. It is a shaded JCTools port and is not wrap-safe; upstream master is identical. It
  needs 2^62 offers on one never-reset queue, millennia at 10-20M put/sec. Do not harden it:
  `(pIndex - cIndex) < bufferCapacity` does not suffice because `producerLimit` stores the
  same wrapping sum, and rolling the index back strands the consumer, which is worse.
- `StripedBuffer.offer` treating `FULL` as success and expanding only on `FAILED`. A failed
  CAS is a contention hint and may be spurious; a full buffer means the drain is behind, which
  striping does not fix. The `FULL` return is the signal that tells `afterRead` to drain.
- Replacing the read buffer's weak CAS solely because it can fail spuriously. No correctness
  failure or material cost was established; stronger CAS needs a measured benefit, not an
  assumption that its cost is identical on every architecture.
- A producer spinning on another producer's write-queue resize, including from an inline removal
  listener holding `evictionLock`. The resizer needs no cache lock and clears the marker before
  scheduling maintenance, so this is an owner-progress stall, not a lock cycle. Growth ends at
  maximum capacity; `onSpinWait()` would retain the dependency and has no demonstrated benefit.
- A thread's starting stripe never moving. `ThreadLocalRandom.getProbe`/`advanceProbe` are
  package-private to `java.util.concurrent`, so the permanent-move search is unavailable.
  The only alternative is a `ThreadLocal` on the read hot path.
- `BoundedBuffer.RingBuffer` stripes unusable during a producer stall, and
  `drainTo` leaving the slot nulled before `consumer.accept` stalls `readCounter` on a
  consumer throw.
- `StripedBuffer.expandOrRetry` attaching a stripe with a plain array store while the `RingBuffer`
  constructor stores its write counter plainly. A producer racing the attach can step that counter
  back and drop a bounded number of read records on the stripe until it passes the read counter;
  the drain does not spin and producers do not block, so the cost is the lossy read buffer's.
- Neither write-buffer consumer waiting for a producer's publication (`relaxedPoll`). The
  task is not lost: `scheduleAfterWrite` runs after `offer` returns and re-arms. Where a
  weak-memory interleaving defeats that re-arm (the IDLE strand below), the task waits in the
  buffer for the next write, `cleanUp`, or a read that fills a stripe.
- `clear()`'s write-buffer drain loop being unbounded under `evictionLock`. Its `AddTask`s can
  accumulate sample observations without `climb`, so sample counters have no all-history bound;
  no practical long-counter overflow or impact was established.
- The write buffer's backpressure is a capacity limit, not a CAS.
- The constructor's read-buffer and access-policy conditions not naming `expiresVariable()`.
  Variable expiry shares the `A` classes with access expiry, and their `expiresAfterAccess()`
  (`timerWheel == null`) answers true inside the `BoundedLocalCache` constructor because the
  subclass assigns `timerWheel` after `super`, so a variable-expiry cache gets both. Naming
  `expiresVariable()` would not help: it reads false at that point.

**Timing and arithmetic**

- `EXPIRE_TOLERANCE` (1s) inexactness. Expiration is a maximum lifetime, not a minimum hold
  time; entries may expire up to 1s early from timestamp tolerance. Applies to `writeTime` reorder
  decisions and read-path `accessTime` and `variableTime` updates; a read that shortens the
  variable deadline is always stored, so the tolerance never makes an entry expire late. Read-extension's accepted over-stay is a
  separate race described in [expiration](design-decisions.md#expiration).
- The fixed and refresh `ageOf` accessors returning empty for a present entry whose timestamp a
  concurrent read or write moved past the query's clock, while `getEntryIfPresentQuietly` and
  the ordered snapshots clamp that negative age to 0 and report the entry. Both are deliberate
  ("hide incomplete mappings from policy metadata queries"; `ageOf_negative`): the accessor
  answers for its own instant, where that stamp is not yet visible, as it does for a pending
  mapping, and the entry view reports a present entry with its full duration.
- Expiration eviction capped at `EXPIRATION_THRESHOLD` (1000) per cycle, re-armed via
  `PROCESSING_TO_REQUIRED`. The wheel rewinds `nanos` and re-links the remainder, so a
  `schedule` inside that window measures against a behind clock. The budget counts only
  evictions, never the cascade, and a cascade cap must not reuse the rewind (it livelocks).
- Collected references drained at `REFERENCE_THRESHOLD` (1000) per queue per cycle, counting
  polls rather than evictions. The cap bounds the lock *hold*, not a waiter's *wait*; the
  lock is not fair, so a backlog still monopolizes it. That residual is not the cap failing.
- `nanoTime` overflow at ~73 years.
- `TimerWheel.advance` returning delta=0 within a tick. The rebased `-1 -> 0` crossing is a
  tick boundary and returns 1; see [TimerWheel](design-decisions.md#timerwheel).
- A dropped read-buffer offer deferring a variable-expiry reschedule. A read that shortens the
  duration CASes `variableTime` but leaves the node in its previous deadline's bucket, so the
  entry stays resident and unreadable until that bucket is swept (measured: 3574s against a 3s
  request, with a `Scheduler` arming 3569s out). Bounded by the previous deadline, so it never
  extends an entry's life; see [TimerWheel](design-decisions.md#timerwheel).
- A dropped read-buffer reorder leaving a live `expireAfterAccess` head in front of expired entries.
  The scan moves a live head to the back only when its access time is newer than the tail's, so
  when the tail was read later, the entries behind a head whose reorder was lost wait for that
  head's next recorded read or its own expiry. Measured with twelve threads keeping their
  read-buffer stripes full, the delay exceeded a second; without that contention it stayed under
  25 ms. The timer wheel sweeps by bucket and has no such stop; see
  [TimerWheel](design-decisions.md#timerwheel). The write-order deque has the same face: a write
  time kept by the tolerance plus a weight change's `UpdateTask` reorder can leave an expired node
  behind a live head until that head expires, under the tolerance.
- `Pacer.calculateSchedule`'s 0L sentinel collision.
- `Pacer.schedule`'s reschedule arm must call `cancel()`, not `future.cancel(...)`: the
  immediate-scheduler recursion guard is `future == null && nextFireTime != 0L` and only
  `cancel()` reaches it. Do not simplify it back. For a custom scheduler executing through an
  inline executor, recursion must terminate; autonomous rearming of a deadline discovered inside
  that callback is not guaranteed. This includes a scheduler that honors the requested delay.
  Later independent scheduling can recover; synchronous scheduling is not itself a contract violation.
- `Pacer` skipping its re-arm when the scheduler fires before the cache ticker reaches
  `nextFireTime`. The executing future still appears pending, so maintenance waits for the next
  operation. `systemScheduler()` completes its future before submitting the task, so this needs a
  scheduler whose future can still be pending while the task runs, such as
  `forScheduledExecutorService`. A one-second-granularity ticker reproduces; `systemTicker` and a
  one-millisecond cached clock did not in 20 trials each. Tickers are expected to advance between
  reads; a cheap clock can increment per read and resynchronise periodically. The `fired` flag was
  declined for this coarse-clock trigger, and for a monotonic `Ticker` that runs slower than the
  scheduler's clock, whose lag grows with the delay: `Caffeine.ticker` states a testing intent,
  the default pairs two `System.nanoTime` clocks, scheduling is best-effort, and the loss ends
  at the next cache operation.
- `Pacer` self-poison ordering (`nextFireTime` committed before `scheduler.schedule()`).
  User schedulers get `GuardedScheduler`'s no-throw/no-null guarantee; built-ins satisfy it
  directly. Do not add a catch for an unreachable synchronous scheduler failure.
- `forScheduledExecutorService` allowing a standalone shutdown-race rejection to escape. Its
  factory description states the task-drop policy, not blanket exception suppression;
  `guardedScheduler` supplies that guarantee and configured caches already use it. The shutdown
  precheck avoids log noise during orderly shutdown (#1449), without making submission atomic.
- A nested wheel sweep transiently cancelling the pacer while a bucket is detached. The normal
  outer pass recomputes scheduling, and listener/counter exceptions cannot bypass that step because
  they are caught. No supported escaping callback was established; broken keys, hostile futures
  and JVM failures remain within the standing exception exclusions.
- `TimerWheel.Traverser` detecting concurrent modification via `nanos` rather than a
  `modCount`, unlike the deque-backed `Policy` families. The "spins forever holding
  `evictionLock`" consequence is a frozen-ticker artifact: `advance()` sets
  `nanos = currentTimeNanos` unconditionally, so under `systemTicker` a re-entrant operation
  reaching maintenance throws CME. Two do not: `clear()`, whose result is a truncated snapshot,
  and one whose maintenance cycle exhausts the expiration budget, since the rewind restores the
  `nanos` the traverser captured. That second route need not truncate: when the write that reached
  maintenance rescheduled the node the traverser returned last, as a per-element `setExpiresAfter`
  does, the walk follows it into its new bucket and yields that bucket again on every
  budget-exhausting cycle, throwing CME on the first cycle that leaves budget unused. Measured on
  the system ticker and common pool, with a mapping function calling `setExpiresAfter` per element
  and 6,000 entries due: 12,294 elements over 2,049 distinct keys (the write buffer's capacity plus
  one), each six times (the backlog over the 1,000-entry expiration budget), then CME. Both results
  fall within the best-effort view the `Policy` snapshot entry under *Views, iteration, and the Map
  contract* accepts, for a computation `Policy` already documents as throwing CME when it
  detectably writes an entry.
- `TimerWheel.expire()`'s catch block holding a stale `prev` pointer.
- A fixed expiry of about 146 years or more freezing the JDK's shared delay queue under
  `systemScheduler()` on JDK 11 through 23. `MAXIMUM_EXPIRY` clamps only variable expiry, so
  `expireAfterWrite` or `expireAfterAccess` of `Long.MAX_VALUE` units or `ChronoUnit.FOREVER`
  hands `CompletableFuture`'s delay queue a delay near `Long.MAX_VALUE`. Before JDK 24 its
  `overflowFree` moves such a delay to `Long.MAX_VALUE` past an overdue head (JDK-8338765), so a
  short task inserted moments later compares after it by overflow and never runs; enough of them
  stop every `orTimeout`, `completeOnTimeout` and `delayedExecutor` task in the JVM. The losses
  need tens of such schedules a second, from building caches or from one cache emptying and
  refilling; on JDK 21 at 10 a second nothing was lost in 300 s. A dropped cache's pending task
  never drains in this configuration, an unbounded case of the retention
  [Pacer](design-decisions.md#pacer) accepts.
  Clamping the delay avoids the freeze (`MAXIMUM_EXPIRY` suffices, and the timer wheel's 6.5-day
  span also bounds the retention) and was declined: the configuration is extremely rare, and the
  bug is the JDK's, fixed in 24, so an affected user should move to a current JDK.

**Node lifecycle and access modes**

- Opaque `accessTime` writes, including "it avoids cache-line invalidation" framings and
  hot-entry true-sharing under `expireAfterAccess`. Deliberate, to avoid contention storms.
- Drain status terminal arms that skip the CAS, and a stale opaque read settling IDLE with a
  buffered task.
- `scheduleAfterWrite`'s weak-memory IDLE strand.
- `maintenance`'s entry store overwriting a writer's `PROCESSING_TO_REQUIRED` while the drain
  passes over that writer's slot, so the exit swap settles `IDLE` with the task buffered. It is the
  executor-run maintenance task's route to the same end state and heals the same way; jcstress
  reached it on aarch64 at about two per million racing pairs, where the direct `cleanUp` route
  measured about four per ten thousand. Two unmeasured routes the JMM permits end the same way or
  milder: `scheduleDrainBuffers`' blind `PROCESSING_TO_IDLE` store erasing a `REQUIRED` that a
  writer set after the store's status read, and `rescheduleCleanUpIfIncomplete`'s post-unlock
  opaque read missing a writer's `REQUIRED`, which stays `REQUIRED` for the next read or write.
- Weak key identity semantics. Historical cleared-reference aliasing was harmless to the
  interner's uniform values; cleared references now compare equal only to themselves. See
  [refresh internals](design-decisions.md#refresh-internals).
- `weakKeys()` spliterators advertising `Spliterator.DISTINCT`. `IdentityHashMap`, the class
  the `weakKeys` javadoc names as its model, does the same on key and entry and omits it on
  values. Removing it makes `distinct()` merge distinct live entries.
- Bulk `Iterable` operations collapsing equal-but-distinct keys under `weakKeys()`. Measured
  2026-09-08 on two live keys equal by `equals` but distinct by identity: `refreshAll` issues one
  load and leaves the second entry stale, `getAll` issues one load and leaves the second key
  absent, `getAllPresent` reports one of the two. None of these methods documents the type of the
  map it returns, and a `Map<K, V>` cannot hold both keys, so the collapse is undefined behaviour
  rather than a defect (Ben, 2026-09-08). A caller who needs both operates per key; `refresh(key)`
  reaches each one. The equals-based dedup in `refreshAll` is not incidental either: dropping it
  costs a second load for a key repeated in the input, on the default and direct executors alike.
  `invalidateAll(Iterable)` returns void, has no such limit, and does reach both keys. A bulk
  loader's result is a `Map` too: the asynchronous `getAll` snapshots it with `Map.copyOf`, which
  rejects an `IdentityHashMap` holding equal-but-distinct keys under either key strength, so that
  load fails loudly while the synchronous path, which iterates without a snapshot, stores both.
  Loaded entries are stored as returned, as Guava's `getAll` stores them: a requested hit the
  loader also returns comes back with its pre-load value while the loaded one replaces it (or is
  evicted as oversized), and under `weakKeys()` a rebuilt key is a distinct entry, so an
  over-delivered hit is duplicated and the synchronous path's requested key misses its own load.
  Weak keys and bulk loading follow Guava's precedent rather than a model of their own (Ben,
  2026-09-16). Guava's `getAll` returns an equals-based, ordered `ImmutableMap` and dedupes by
  `equals`; Caffeine returns a `LinkedHashMap` because users asked for that ordering, an identity
  result would need an `IdentityLinkedHashMap` the JDK lacks, and neither project has had a user
  report of what weak-key users expect. Match Guava's observable behavior before proposing identity
  semantics for a bulk path; the facade's `IdentityHashMap` copy of a loader's result does that for
  the extras Guava stores.
- Weak/soft value wrappers allegedly reaching their reference queue before the bookkeeping key
  is initialized. No supported-runtime null-key dequeue or cache failure has been established;
  the abstract construction window alone does not justify a change. Reconsider on a concrete
  runtime witness. This is separate from the fixed predecessor-clear ordering issue (#1820).
- Weak-key lookups allocating a `LookupKeyReference` (24 B/op). A thread-local mutable
  wrapper pins the instance to the thread, rejected in #294 for virtual threads and
  classloader pinning. Young-gen allocation is the better trade.
- `Interner.newWeakInterner()` losing `ConcurrentHashMap`'s tree-bin ordering for `Comparable`
  elements, so interning many same-hash strings is O(n) each: 4.7 s for 16,384 against 10 ms for
  the strong interner. Guava's weak interner chains its bins and degrades for every key type, and
  no contract promises collision resistance. Comparable wrappers are not a repair: CHM orders only
  a probe and stored key of one class, and a cleared referent cannot be ordered, so later inserts
  are tie-broken around it and, once it drains, live keys sit on the wrong side of each other.
  Measured with one comparable weak class in a plain CHM, that missed live keys and inserted
  duplicate equal keys in every trial; without comparison, or without clearing, never.
- A never-completing async or refresh loader retaining weak keys and values via callback
  capture.
- `BoundedLocalCache.containsKey` and `EntrySetView.contains` lacking an `isAlive()` filter;
  `computeIfPresent`'s fast path bypassing `requireIsAlive` for value==null nodes;
  `getKey(K)` lacking expiry, value and alive filtering.
- `BoundedLocalCache.put` missing an `isAlive()` re-check after the `Expiry` callback.
- `put` and `putIfAbsent` weighing and dating a candidate node before `data.putIfAbsent`, so a
  writer that loses that race has called `Weigher.weigh` and `Expiry.expireAfterCreate` for a node
  it discards (two of each for one installed entry, where `computeIfAbsent` makes one). The node
  must be complete when that call publishes it, and neither callback promises one call per entry.
  The same placement dates a new entry from the call, so an insertion that waits on another
  thread's bin or node monitor publishes it aged by the wait; see
  [expiration](design-decisions.md#expiration).
- `BoundedLocalCache.replace(K, V, V)` calling the weigher before the oldValue check.
- `BoundedLocalCache.getIfPresent` casting the lookup `Object` to `K` for
  `tryExpireAfterRead`.
- Read-path expiry extension resurrecting a just-expired entry.
- A read that returns a value already reported EXPIRED, during a concurrent rewrite. Real and
  historically deferred, then repaired by the timestamps-before-value protocol. The protocol
  orders a timestamp stored by the rewriting thread; a fixed `expireAfterAccess` reader's opaque
  `accessTime` store is ordered after the value only on multi-copy-atomic hardware (x86, ARMv8),
  not on POWER, where a third reader can pair it with the superseded value. Unmeasured and not
  repaired: POWER is not a tested target.

**Refresh**

- `refreshIfNeeded` being lock-free. A stale observation can fire `asyncReload` on a
  just-retired node; the completion-path ABA guards (`currentValue == oldValue` plus
  `(node.getWriteTime() & ~1L) == writeTime`) discard the result. The rare spurious loader
  call is accepted to keep the fast path lock-free.
- The "exceptions logged and swallowed" Javadocs on `LoadingCache.refresh` and the loader
  `asyncReload` methods when producing the future throws synchronously. The notes describe the
  refresh itself, not producing its future. Preserve the Javadocs; a construction-time throw
  remains a distinct caller failure.
- The `refreshes` to `data` lock inversion deadlock.
- `LocalAsyncLoadingCache.refresh(key)` retrying without bound or backoff. Each pass rereads,
  so a retry needs the entry to change between the two probes. The exception was a completed
  future holding no value, which satisfied both probes at once and could not terminate; the
  optimistic path now treats such a mapping as absent.
- Refresh eligibility using strict `>` while expiration uses `>=`.
- Refresh discard notification using the discarded value; refresh commit failure not
  surfaced on the future; `discardRefresh`'s `containsKey` prescreen missing a CHM
  `computeIfAbsent` reservation; `discardRefresh` invalidating a newer refresh generation.
- A rejected reload notifying again for the captured old instance after its removal. Returning
  that instance offers it for retention again, so rejection disposes of the new offer. Disposal
  after a throwing user weigher/expiry aborts installation is outside the cache's responsibilities.
- A successful refresh completion discarding a successor that registered after it published the
  new value, so that successor's reload is declined and the value waits for the next refresh.
  Every bounded update of an existing entry has that overlap between publishing its value and
  releasing the registration. Releasing only the completing token also keeps successors that
  loaded the replaced value, which later refreshes then join; see
  [refresh internals](design-decisions.md#refresh-internals).
- A manual `refresh(k)` that started while the key was absent committing across a racing
  `put` then `invalidate`. Its only ownership test is `currentValue == oldValue`, vacuous at
  `null == null`, and the prescreen cannot see the registration reservation. Reproduced 5/5
  with the loader running inside `refreshes.compute`, discarded 0/5 on a pool executor, and
  the history is still a legal linearization; see [refresh internals](design-decisions.md#refresh-internals).
  The same holds for a present start whose racing writes end on the instance it read, which
  the identity test cannot tell from no write.
- `UnboundedLocalCache.discardRefresh` removing unconditionally, so a write waits out an
  inline refresh load on the same key (measured 2004 ms). It is a light `ConcurrentHashMap`
  wrapper and CHM's `put` is pessimistic regardless, so the bounded cache's prescreen is not
  a repair to port. A write to any key in the same `refreshes` bin also waits, so during k
  inline reloads about k/16 of writes stall in a 16-bin table; with the default executor the bin
  is held only for the submission (76 us measured). The prescreen would also bring the bounded
  cache's reservation-window escape, which the unconditional `remove` waits out.
- Removing the bounded `discardRefresh`'s `containsKey` prescreen as redundant work. `remove`
  reaches CHM's `replaceNode`, which returns without locking only when the target bin is empty,
  so an absent key that collides into an occupied bin still takes the bin monitor and walks the
  chain. The lock-free probe buys that miss, and on a hit it is not wasted either, since it warms
  the bin `remove` then touches. The `RedundantCollectionOperation` suppression is deliberate.
- A same-instance refresh leaking the completed future in `refreshes`.
- A user-initiated `LocalLoadingCache.refresh` lacking the `getWriteTime() == writeTime` ABA
  guard.
- `put()`'s insert path missing `discardRefresh`.
- `doComputeIfAbsent`'s first-load path not calling `discardRefresh` when the mapping
  function throws.
- Quiet refresh or async completion skipping the timer-wheel reschedule under variable
  expiry plus `refreshAfterWrite`.
- Refresh only triggering on access, and returning the stale value rather than the fresh one.
- A computation that blocks in `CompletableFuture.join()` or `get()` on a pool worker running the
  cache's own refresh inside it. A blocked worker runs the `AsynchronousCompletionTask`s at the
  base of its queue on its own stack (`helpAsyncBlocker`, JDK 9 and later). A reload or async load
  that worker submitted earlier is one, so its completion's `compute` runs under the outer
  computation's bin lock. With both keys in one bin and a `null` reload the outer insertion is lost
  (the returned value is not mapped, and the map's count stays one high); the helped completion's
  `afterWrite` can take `evictionLock` under that bin, inverting the lock order; and two workers
  can deadlock on each other's bins. Who helps varies by release: on 11 any caller, from a
  common-pool submission queue; on 17-24 only common-pool workers; on 25.0.0 and 25.0.1 also other
  pools' workers (JDK-8369656); from 25.0.2 every pool's workers, each in its own pool. It is
  helping rather than starvation, so a larger pool does not prevent it; a virtual thread or a
  separate executor for the cache does. Not ours to fix: the trigger is a computation blocking a
  pooled thread.

**Views, iteration, and the Map contract**

- `ValuesView.remove(o)` testing the node's current value and conditionally removing the
  iterator's captured value, so a writer alternating A and B can make `remove(B)` delete a
  mapping holding A. Measured on the bounded cache, 8,254 of 710,733 explicit removals
  against a B-only control of 0. Not a defect: the predicate established the node's value was
  equivalent to what was passed, and a conditional removal against either operand means
  "remove this entry if it still holds an equivalent value". The outcome is the ordinary
  value-changed-mid-call one, and `ConcurrentHashMap` produces it too, more freely, since its
  iterator removes by key unconditionally (`replaceNode(p.key, null, null)`) where ours is
  conditional. Two consequences that look distinguishing are not: failing to remove a
  stably-present B happens under CHM and under a matched-operand variant as well. The
  per-node predicate is deliberate, from "Fix removal in identity views", which gave weak and
  soft value caches `IdentityHashMap` equivalence; do not revert it to `o.equals(value)`. The
  open direction, if this is ever revisited, is the opposite one: removing by key only, to
  match what `AbstractCollection` does for non-concurrent usage, against which CHM's
  conditional `removeIf` is the counterweight.

- Map, entry-set and entry-object equality disagreeing under `weakValues()`/`softValues()`.
  The value-bearing queries are identity-based and `equals`/`hashCode`/emitted entries are
  `equals`-based, so `entrySet().equals` is false one way and true the other while `equals` is
  true both ways. Guava's `weakValues()` cache reproduces the row exactly, and `IdentityHashMap`
  buys coherence only by being asymmetric against a `HashMap` instead. Identity belongs to
  one-sided queries and never to a bilateral contract; see
  [iteration](design-decisions.md#iteration).

- `clear()` attributing `EXPLICIT` where `invalidateAll(keys)` attributes `EXPIRED` for an entry
  crossing its deadline during the sweep. The captured clock amortizes the ticker call under the
  eviction lock, and the straggler fallback reads per key anyway, so the same `clear()` reported 5
  `EXPLICIT` then 199,995 `EXPIRED` under a concurrent writer; see
  [expiration](design-decisions.md#expiration).
- `getAllPresent` and `containsValue` using one scan-wide `now` for every element's expiry
  check. This covers bounded single calls whose staleness window is the call. It does **not**
  extend to a user-paced traversal (an iterator or spliterator), where a slow terminal
  operation makes the window unbounded and `EntryIterator.hasNext()` reads per element.
- `AsMapView.KeySet.remove(k)`, `removeAll`, `removeIf` and `retainAll` bypassing the
  block-on-in-flight contract.
- View bulk-removal infinite-looping with a write-back removal listener.
- `clear()` / `invalidateAll()` preserving nodes recreated after its snapshot. Identity-checked
  removal targets the captured node and avoids reprocessing listener write-backs (#872).
  The by-key straggler pass and `invalidateAll(keys)` can remove newer mappings; these operations
  need not remove the same generation during concurrent writes.
- A prefetched iterator cursor returning an entry a fresh traversal skips.
- `ConcurrentMap.getOrDefault` not being overridden; the inherited default performs one `get` call.
- `ConcurrentMap.remove(k, null)` returning false rather than throwing NPE.
- `entrySet().add` throwing UOE rather than putting through. It matches
  `ConcurrentSkipListMap` and pre-v8 CHM; CHM's put-through violates `Set.add` by returning
  false yet replacing.
- Lazily created views (`AsyncCache.asMap()` and `synchronous()`, the synchronous view's `asMap()`,
  the Guava facade's `asMap()`) stored without synchronization, so racing first callers can
  receive distinct, equivalent instances (1 to 6 trials in 50,000). `ConcurrentHashMap.keySet()`
  and Guava's cache `keySet()` publish the same way; Guava's `asMap()` is the cache itself.
  Capture each lazy holder once and return that local or the newly created instance. Final-field
  initialization protects a view's backing state, but does not make repeated plain holder reads
  coherent; a second read could return null after the check observed a peer's initialized view.
- The access-reset javadoc ("reset by all cache read and write operations") not naming
  `containsKey`, `containsValue`, `forEach`, or a conditional `replace(k, old, new)` or
  `remove(k, v)` whose value does not match. A cache read means get-style access, as its get and
  put examples show: an operation records an access when it returns the entry's value, as
  `putIfAbsent` on a present key does, or writes it. Membership checks, traversal, and a failed
  conditional write do neither and stay quiet by design. Guava records a read on a mismatched
  `replace` but not on a mismatched `remove`. Enumerating the exceptions across the four javadocs
  was declined. Pins: `ExpireAfterAccessTest.replaceConditionally_wrongOldValue` and
  `removeConditionally_wrongValue`.
- Every `Policy` map overload (`coldest`/`hottest`, their weighted forms, `oldest`/`youngest`)
  collapsing equal-but-distinct weak keys. The collecting `LinkedHashMap.put` keeps the first
  key instance with the later key's value, a pair the cache does not hold, and the limit or
  weight budget counts both, so the map can come back short. The `Map` cannot hold both keys,
  so which pair survives is the same undefined collapse; the `Stream` overloads return all.
- `Policy` snapshots pairing a value with a weight from another moment. The snapshot reports the
  policy's weight under `evictionLock` while writers publish values under the node's monitor, so
  a concurrent update can hand `coldestWeighted`/`hottestWeighted` a stale weight, and the
  expiration timestamps are read after the value. It is a best-effort view of what the policy
  sees, which also omits an entry whose `AddTask` is still in the write buffer when the
  snapshot's maintenance pass ends; synchronizing every node to pair them was declined.
  The same replay can hold a weight no entry has, which the snapshot clamps into the `int` range: a
  negative transient reads as 0, and an over-count such as `2W - w` stays within this ruling.
- `expireAfterAccess().oldest()` misordering an evicting cache's entries. The snapshot merges the
  window, probation and protected deques by access time, but probation receives window victims and
  protected demotions out of access order, and a pending async entry's `ASYNC_EXPIRY` access time
  holds back the rest of its deque until the others are exhausted, after which the entry is
  filtered out. The order is the policy's best guess; each entry is still listed once.
- Message-less `requireArgument` on public API.

**Notifications**

- `notifyEviction` to `discardRefresh` ordering, and an exception during user
  `equals`/`hashCode`.
- The unbounded cache notifying with the caller's key instance, losing the notification when a
  cross-type equal key reaches a typed listener. `ConcurrentHashMap` never returns a stored key,
  and recovering it needs a scan or a per-entry node; see
  [CHM constraints](design-decisions.md#concurrenthashmap-constraints).
- `AsyncRemovalListener` notification on executor rejection.
- `LocalCache.notifyOnReplace` dropped when both old and new are async futures and the old
  completed exceptionally.
- A same-instance write over an expired entry notifying `EXPIRED` for the value it reinstalls. The
  mapping expired, and maintenance reaping it before the write delivers the same notification, so
  suppressing it would only make the notification depend on timing. `notifyOnReplace`'s identity
  check is for replacing a live value, which removes nothing, and on `compute` and
  `computeIfAbsent` the eviction listener runs before the function returns its value.
- Historical `afterWrite` inline-fallback loss when maintenance throws. The repair runs
  the write's own task in `maintenance`'s `finally`; buffered work remains deferred.
- `RemovalCause.EXPLICIT` and `REPLACED` saying "by the user" while an automatic refresh notifies
  both (`REPLACED` for a reloaded value, `EXPLICIT` for a `null` reload that removes the entry).
  "By the user" separates the two non-eviction causes from the eviction ones, and
  `Caffeine#refreshAfterWrite` delegates its semantics to `LoadingCache#refresh`, which both lists
  name; Guava's identical wording describes its identical refresh. Naming `refreshAfterWrite` in
  either javadoc was declined.
- Under the value-objects preview (JEP 401), `put`, `putIfAbsent`, `putAll` or
  `VarExpiration.put` over an expired or collected weak- or soft-value entry announcing the
  eviction twice. The eviction listener runs before `setValue`, whose new reference throws
  `IdentityException` for a value object, and nothing commits the eviction, so the next
  maintenance announces it again. `compute` and `get(key, fn)` catch, commit and rethrow around
  the same call. Deferred until the feature leaves preview (#1997); the likely shape is an
  `Objects.requireIdentity` precondition before any notification, as `WeakHashMap` does for its
  keys.

---

## Async

- In-flight entries uncounted by any bound; async sync-view `size()` vs `containsKey()`
  divergence; `size()` counting stale or in-flight entries.
- Async sync-view quiet-read divergences generally, and `remove(k,v)` short-circuiting on
  in-flight while `replace` and compute block.
- `AsyncAsMapView.computeIfAbsent` stats diverging from `AsyncCache.get`.
- `AsyncBulkCompleter.fillProxies` using 3-arg `replace` while `handleCompletion` uses the
  4-arg form with `shouldDiscardRefresh=false`.
- `AsyncBulkCompleter.fillProxies`'s `obtrudeValue` overriding a caller's `cancel()` on a
  shared proxy future.
- `AsyncBulkCompleter` double-evaluating a lazy bulk-load result.
- `LocalAsyncCache.put(k, null-future)` calling unconditional `cache().remove(key)`.
- `AsyncCache.asMap().entrySet()`'s `WriteThroughEntry.setValue(incompleteFuture)`
  divergence, and `WriteThroughEntry.setValue` not being fully atomic.
- Async load-failure WARNING not unwrapping `CompletionException` before the
  `instanceof Timeout/Cancellation` suppression check.
- General containment for `getAll` setup failures caused by a throwing `Ticker` or broken key
  equality: cleanup can fail on the same component. Setup does settle its earlier proxies when
  a later read-expiry callback throws, because conditional removal does not invoke that callback.
  This limited cleanup does not establish recovery from broken clocks, keys, or cleanup itself.
- Async `put(k, future)` completion-handler registration not being contained. For any
  spec-abiding `CompletableFuture`, `whenComplete` never throws at the registration site.
- A failing async finalizer removing a later reinsertion of the same completed future.
  Cleanup is conditional on the future object, not an insertion generation; a distinct
  successor is preserved. See [async re-registration](design-decisions.md#async-put-re-registration).
- A `loadAll` returning a map with null keys or values causing a partial commit, and null
  loader maps giving inconsistent diagnostics across `getAll` paths.
  `NullMapCompletionException` is an internal marker translated to
  `NullPointerException("null map")`; the sync path's JEP 358 helpful NPE names the variable.
  Preserve `LoadingCache.getAll`'s existing exception wording; adding a partial-commit
  qualification was declined.
- `loadAll` retaining the caller's mutable `Set` across the async boundary.
- A dropped or hung async load leaving a permanent in-flight mapping. Cancelling a per-key
  load's future removes its mapping; a cancelled bulk proxy stays mapped until the bulk loader's
  own future completes (see design-decisions, *Async Put Re-registration*), so a hung bulk load
  is released by completing that future or by `invalidate`.
- `synchronous().refresh(k)` on an absent key returning a write that completed between its
  absence check and its load, without calling the loader. A refresh of an absent key is a
  `get(key)`, which adopts the mapping it finds, and a load in that race could equally have been
  discarded by the write.
- An overlapping refresh adopting an older failed future while its original caller is still
  registering the absent-key load. Conditional registry cleanup preserves newer cached mappings
  and distinct refresh tokens; refresh does not promise the newest cached future or an immediate
  retry. Cleanup runs when the original invocation resumes; this is not permanent token poisoning.
- Overlapping `getAll` calls sharing a per-key proxy and its failure or omitted result. Each loader
  owns only the proxies it installed; aggregates retain shared futures even after cache removal.
  A caller that owns additional missing keys records its own bulk load, not another load for the
  shared keys. Outcome sharing is the accepted coalescing policy, not a bulk-atomicity guarantee.
- The synchronous view's `asMap()` being equal to no other map while a load is in flight, and a
  `ConcurrentHashMap` or an unbounded cache comparing equal to it in one direction only. See
  [iteration](design-decisions.md#iteration).
- `synchronous()`'s javadoc that a modification to a loading mapping blocks, while `Cache.put`,
  `invalidate`, `invalidateAll`, `asMap().clear()` and key-set removals return at once: they
  return nothing that needs the loaded value. `asMap().put` and `remove(k)` store first and then
  wait for the displaced value they return; the wait is `join`'s, uninterruptible with the
  interrupt status kept, and a load that fails during it yields null. `WriteThroughEntry.setValue`
  waits the same way although it returns the traversal-time value, which the blanket blocking
  sentence permits.
  Callers occupying every worker of the load executor while waiting create thread starvation,
  even when the executor accepts and queues tasks correctly. That application dependency is
  covered by the executor-responsibility rule; the synchronous view promises no independent
  progress or finite wait for the load.
- `AsyncCache.synchronous().get(k, fn)` and `getAll(keys, fn)` running the function on the cache
  executor through `supplyAsync` (or on the caller when a blocked common-pool worker helps with
  its own task), so it can see none, its own, or another task's leftover thread-bound state, and
  interrupt status the function sets stays on that thread; the same view's
  `asMap().computeIfAbsent` runs it on the caller. The `AsyncLoadingCache` view's loads are
  documented to run on the executor through `CacheLoader.asyncLoad`.
- The synchronous view logging a failed load at WARNING that it also throws to the caller.
  Async load failures are logged because a future may go unobserved; the view reuses that
  completion, and the logger can be configured to drop it.
- `synchronous().get(k)`, `get(k, fn)` and `getAll` waiting on a load in flight without responding
  to interruption, the interrupt status kept. A synchronous cache's caller waits the same way at the
  bin lock for another thread's load, as Guava's does; only a thread running an interruptible
  loader returns early.
- `asMap().putIfAbsent` and `computeIfAbsent` returning an existing value without read expiry when
  they waited on a load or found the value after their first lookup missed. See
  [async synchronous view](design-decisions.md#async-synchronous-view).
- `AsyncCache.get(K, Function)` allocating its function adapter on a hit (16 B) once misses share
  the compiled profile. Avoiding it takes a second hit probe ahead of the one in
  `get(K, BiFunction)`, which a lambda allocation does not justify.
- `synchronous().get(k, fn)` surfacing the cause of a `CompletionException` that the function threw
  itself, where the synchronous cache rethrows it unchanged. `supplyAsync` stores a thrown
  `CompletionException` as the wrapper it would otherwise add, so `resolve` cannot tell them apart.
  The view rethrows a `RuntimeException` or `Error` cause and otherwise retains the wrapper;
  a user-supplied wrapper therefore does not preserve its identity or exception category.

---

## jcache

Read `jsr107-conformance.md`'s topic sections with this section.

- **The 1.0 PDF is not authoritative.** The 1.1 and 1.1.1 maintenance releases revised
  normative behaviour without regenerating the formal PDF. Cross-check the 1.1.1 API javadoc
  before treating a 1.0 sentence as load-bearing; the specification's revision history records
  no 1.1 behaviour change. Confirmed relaxations: the `getCacheNames` iterator's ISE on
  modification removed; `getCache(String)` typed-cache IAE removed; the iterator EXPIRED firing
  requirement removed. Loader exception wrapping was not relaxed: the 1.1.1
  `CacheLoaderException` javadoc still requires it, and the TCK asserts it for `get` and `loadAll`.
- A listener, filter, loader, writer or other configured factory whose `create()` calls back into
  the caching API while the adapter holds a monitor or a registry computation: `getCache` or
  `createCache` on the same manager, or `CachingProvider.getCacheManager`. That is the standing
  principle's constructed trigger whatever the outcome: a deadlock against destroy or close, a
  loud `Recursive update`, or, when the nested insert resizes the registry under the outer
  computation's bin, a silently corrupted registry that loses a cache or holds two for one name.
  Moving construction out of the registry's mapping function was not proposed.
- Operations racing `close()`. The spec explicitly permits a closed cache to retain
  contents, governs only *future* use, and punts concurrent behaviour to implementation
  dependent. Local in-memory means no OS resource leaks.
- Manager close holding its monitor while awaiting child close, so an ordinary completion
  callback's manager lookup can wait for the ten-second child timeout. Accepted bounded
  shutdown delay, not an unbounded deadlock or a promised callback-disposal barrier.
- `CacheProxy.close()` calling `executor.shutdown()` and `tryClose`. Spec-silent rather than
  spec-required; defensible as a cache-owned resource.
- A jcache proxy "leaking" when abandoned without `close()`.
- Lifecycle-changing reentry from a user resource's `close()`, including creating another
  cache while its manager closes or reentering provider lifecycle under manager close. These
  user-created shutdown dependencies fall under the callback-trigger boundary; this does not
  classify an ordinary CompletionListener manager lookup as invalid.
- `CacheFactory` construction orphaning an owned executor, expiry, writer, loader or
  listeners when a later config validation throws. The trigger is a config error plus a user
  factory creating an owned resource. The centralize-ownership refactor was built, verified
  green, and discarded.
- The JMX `ObjectName` sanitize collision (`a:b` and `a=b` both to `a.b`). Inherent to any
  lossy sanitize and matches the RI. Never switch to `ObjectName.quote()`: the TCK's
  `TestSupport.calculateObjectName` hardcodes the unquoted RI-style format and looks the
  MBean up by it, so quoting fails the TCK.
- OSGi TCCL swap missing on `destroyCache` and `close`.
- `putNoCopyOrAwait` copying the value under the CHM bin lock, and lazy-expire
  `recordEvictions` drift.
- `CacheManagerImpl.getCache(String)` not throwing IAE for typed caches (relaxed in 1.1.1),
  and `getCacheNames()`'s iterator throwing UOE rather than ISE on `remove()` (relaxed).
- `LoadingCacheProxy.getAll` skipping access-expiry on loaded entries, unlike `get`.
- Read-through `get`/`getAll` finding a concurrent insertion or replacement on the loading
  lookup and skipping one JCache access-policy call. Native expiry still applies; includes
  `getOrLoad`'s expired-wrapper recovery. See [access expiry](jsr107-conformance.md#access-expiry).
- `CacheProxy.EntryIterator.hasNext` skipping expired entries without firing EXPIRED
  (requirement removed in 1.1.1).
- `JCacheLoaderAdapter.expireTimeMillis` and `CacheProxy.getWriteExpireTimeMillis` falling back
  to `Long.MAX_VALUE` (eternal) when `getExpiryForCreation` throws, and to `Long.MIN_VALUE`
  (unchanged) when `getExpiryForUpdate` throws.
- The provider's `WeakHashMap` ClassLoader retention. Proven, and not fixable: the value
  chain reaches its own key, and weak values would collect a live manager. JSR-107 provides
  `CachingProvider.close(ClassLoader)` for exactly this. Documentation only.
- `CacheManagerImpl.close()` deregistering by URI after it releases its lock. A lookup
  overlapping the close can return the closing manager, and a close racing a provider close and a
  fresh lookup can close the successor. Deregistering under the lock deadlocked against the
  provider's registry monitor, and the RI also releases by URI. Removing only this manager by
  identity, as the manager removes its caches, needs a hook from `CacheManagerImpl` into the
  provider's private registry across the `jcache` and `jcache.spi` packages. Its other half, a
  lookup that replaces a closed manager, must not land alone: `close()` sets `closed` before it
  deregisters, so the replacement would be the mapping its by-URI release removes and closes.
- `AbstractCopier` trusting an array's declared component type, so a `BigDecimal[]` holding a
  mutable subclass is copied shallowly. The subclass breaks the declared type's immutability.
- `JavaSerializationCopier` defining a proxy class in the manager's loader, which fails for a
  non-public interface from another loader when Caffeine is loaded above that loader. No report.
- A deserialized `CaffeineConfiguration` not equal to its original, as the default factories are
  method-reference lambdas with identity equality. Nothing compares a round-tripped configuration.

---

## guava adapter

- Guava-facade exception-translation divergences.
- Guava-facade statistics divergences, under the best-effort-stats rule.
- `CacheLoader.asyncReloading` making a scalar loader appear bulk-capable. Unsupported bulk
  loading falls back to direct per-key loader calls, followed by bulk insertion. It can duplicate
  concurrent loads or replace concurrent writes; these follow the adapter's bulk semantics.
- `caffeinate()`'s `ExternalBulkLoader` returning the loader's map uncopied, so a lazy view is
  evaluated more than once. Core tries to evaluate once, but Guava promises no single evaluation
  and itself evaluates twice in rare cases.
- The facade overrides only where native diverges from Guava. It overrides `contains`,
  `containsKey` and `remove` because native `contains(null)` throws NPE; it does not override
  `containsAll`, because native `containsAll` is null-lenient. `containsKey(null)` throwing
  NPE is deliberate null-hostility on a direct query.
- A loader that loads another key, deadlocking two threads on the bin lock or failing with
  CHM's `Recursive update`, where native Guava releases its segment lock before loading.
  Caffeine's `LoadingCache.get` forbids modifying this cache during the computation, and its
  documented `IllegalStateException` covers only detectably recursive updates. The facade
  retains this restriction as an accepted compatibility limit, including a loader or `Callable`
  that writes or invalidates its own key. Detection is best-effort, not a safety guarantee.
  Guava's `LoadingCache.get` and `Cache.get(Callable)` do not state Caffeine's prohibition.
  Guava does detect some same-entry recursive loads: `LocalCache.waitForLoadingValue` checks
  `Thread.holdsLock(e)` and throws `Recursive load of: ...` when the caller holds that entry's
  monitor. This does not reject a different key solely for sharing a segment, and plain `put`
  does not use that loading path. Guava's successful cases do not change the facade's accepted
  restriction. Migrating users do hit it. A `reload` that refreshes its own key fails the
  same way, because the facade calls `reload` inside the refresh registration, while native
  Guava's loading reference turns the nested refresh into a no-op. Three further outcomes are
  covered by the same acceptance. A nested insert that crosses the map's resize threshold runs
  the resize on the loading thread and re-enters the held bin, so the resize is abandoned for the
  life of the map and misplaced keys are iterated but not gettable and survive `invalidateAll()`,
  leaving a bounded cache with an entry outside the policy and a `weightedSize` below the true
  weight. JDK 11 livelocks the loading thread in that resize instead. In a size-bounded cache a
  nested write that finds the write buffer full waits for `evictionLock` while holding the outer
  bin, which `evictEntry` waits on under that lock, so one thread stops eviction cache-wide. Under
  `refreshAfterWrite`, Guava's default `reload` runs a `load` that reads other keys inside the
  refresh registration, so readers can deadlock in the refresh map; `CacheLoader.asyncReloading`
  avoids it. Supporting nested loads means loading outside the bin lock as Guava does, which would
  rebuild the facade on another loading model.

---

## simulator

The simulator is a testing tool. Its correctness matters only to avoid misleading benchmark
claims, not user harm. Weight effort toward the core and the adapters; sibling-divergence is
the one lens that stays productive here. A mechanism that no bundled trace, default run, or
reported issue reaches is won't-do until one does, however cleanly it reproduces. A loud abort
takes no number with it, so it waits for a report too. A documented but non-default setting is
not reach either.

- Approximate and lossy policies are intentional. `membership.bloom.FastFilter` is opt-in.
- `product.*` policies inheriting third-party libraries' wall-clock expiry defaults. This does not
  extend to a clock that ranks eviction victims, which the adapter drives with the event count
  (simulator rules, *Hit-Rate Validation*).
- The feedback TinyLFU policies' `Admit rate` leaving out a candidate their feedback override
  admits after `TinyLfu` rejected it (under half a point on the bundled traces). Counting it would
  need a way to undo the admitter's recorded rejection, for a secondary column of two experimental
  policies. GDSF's admissions are its own accounting, as its evictions are.
- `ClockProSimplePolicy` omitting the CLOCK-Pro hot-warmup phase, or collapsing on scan-loop
  workloads.
- `Cache2kPolicy.finished()` not failing; LIRS and LIRS2 at `percent-hot = 1.0`;
  `(int) (percentSample * maximumSize)` truncating to zero in eight climbers, which is
  already loud.
- `GDWheelPolicy` reading `event.missPenalty()` and degenerating to LRU on equal or uniform
  penalties. Verified against both GD-Wheel papers; a mixed penalty/none trace is undefined
  input for a penalty-aware policy.
- Synthetic workloads being unseeded, so `random-seed` does not cover them.
- Dedup-of-duplicates in ClockPro is author-sanctioned (confirmed with Song Jiang).
- A multi-member gzip or bzip2 trace (`pbzip2`, `bgzip`, `cat`) reading only its first member, and
  a truncated gzip or xz trace of 4- or 8-byte records ending at a 64 KiB buffer boundary without an
  error. Neither has a use case; revisit when a real trace needs one.
- The TinyCache policies, Gil Einziger's contribution, are kept as contributed: neither repaired
  nor removed. That includes building `ceil(maximumSize / 64)` sets of 64 entries, so a cell runs
  at the next multiple of 64.

---

## examples

The examples are simple, illustrative starting points that show how to think about a problem, so
a need can be met without adding a feature to the library. They should not be bad code, but they
are not production code the project owns. A defect is an example failing at what it shows; missing
lifecycle handling, incomplete READMEs, extreme inputs, and unused configuration are not.

- The RxJava and Reactor examples having no backpressure or an unbounded buffer under a slow
  sink.
- `IndexedCache` enforcing unique secondary keys only sequentially: two values sharing a unique key
  are user error. A key held by an expired row that maintenance has not yet removed still counts as
  shared, so a put that claims it is rejected until then; invalidating the key releases it. Its
  alias lookups and `invalidate` read two maps under no shared lock, so a concurrent same-primary
  key change can briefly return or remove the entity through the alias it just left; an exact check
  would run every indexer on each hit.

---

## build and CI

- `tests-latest` (`LATEST_JDK`) gated to default-branch push only.
- `build.yml`'s `attempt-limit` retries through `run-gradle`. The weekly canaries (`build-arm.yml`,
  `build-ea.yml`) run their suites once: the intermittent failure a retry absorbs is what they are
  scheduled to catch. Their retry was for flaky network calls, and from June to October 2026 it
  absorbed five test failures and no network failure; bring it back if that noise returns.
- `rerun-cancelled` re-running a Build job that hit its `timeout-minutes`. Every timeout it has
  re-run was the Fray leg stalling after its tests finished, on a remote build-cache store that
  hung (gradle/gradle#39064, a temporary network problem), and a re-run recovers it. A test hang
  that does not recur therefore ends green; one that does ends red at the third attempt.
- A jcstress run that tests nothing (no matching tests, or no JVM configurations) exiting 0. The
  weekly workflow passes no test filter, so it would take an empty test jar or a platform without
  configurations, and a grading failure already fails the task.
- The generated sources stamping the build year into their license header, so a release rebuilt
  in a later year has a different sources jar (the class files are identical). The generated code
  gains files and changes over time, so the header keeps the current year rather than a fixed one;
  a reproducibility rebuild of the sources jar holds within the release's year.
- A re-run of an older `master` Build publishing its snapshot after a newer run's. The re-run
  keeps its own concurrency group so that it cannot cancel the newer run, `rerun-cancelled`
  re-runs only the newest run, and the next push republishes the snapshot.
- jcstress, lincheck, `test`, `fuzzTest` and `frayTest` tasks being cacheable rather than
  `cacheIf { false }`, so identical inputs (one tree pushed to two branches, a re-run, a
  version-only change) replay a cached pass without re-fuzzing or re-exploring. The weekly canaries
  run with `--rerun-tasks` for fresh executions.
- `EclipseJavaCompile`'s `argumentProviders.add { lambda }` emitting absolute paths.
- `ShardedTestFilter` running non-`MethodSource` descriptors in every shard.
- `ShardedTestFilter` dropping every method without `@CacheSpec` when a `-P` filter is set. The
  filters select local subsets of the parameterized matrix; CI shards without them, so the plain
  tests run there.
- `configureondemand` is intentional.
- `run-gradle` and `build-ea.yml` expanding the suite list unquoted, so a `--tests` wildcard would
  glob against the repository root. Nothing there matches either wildcard, and a match would most
  likely fail loudly.
- The `Stress Tests` job asserting nothing: `Stresser` prints status and throughput and exits 0
  after `--duration`, so it is a smoke run that fails only if the JVM dies, not an invariant check.
- jcache publishing the OSGi DS annotations as `api` although bnd needs them only at build time.
  Like core's jspecify and Error Prone annotations, they stay on consumers' classpath because
  some compilers (scalac, kotlinc) have failed on a missing annotation class even though the JVM
  spec makes a CLASS-retention annotation optional.
- Dependency verification is not wanted; the egress allowances are intentional.
- The release restoring caches that other jobs write (the Develocity remote build cache, the
  Gradle home, the JDKs). The workflows are guarded against compromised actions and the
  configuration cache is encrypted, so a cache-free release is excessive for now; if that
  changes, disable caching for the release job only (`--no-build-cache` and no cache restores).
- `coverage` and `test-results` skipping after a failed `tests-minimum` matrix. The failed shard
  makes the Build fail; fix that failure. Keep downstream summaries skipped, since publishing
  partial results lowers test counts and coverage. Do not add an always-running downstream result
  gate solely because the required summary checks accept a skipped conclusion. The same holds when
  the early-access compile leg fails upstream (an EA JDK checksum mismatch in `setup-java`) and
  cancels the test matrix: the Build is red, the outage is temporary, and the next run tests every
  commit pushed during it.
- The cacheable `jmh` task behind the benchmark gists, `analysis.yml`'s SARIF merge keeping only
  the first input's `tool`, the `git diff` metadata freshness check missing a deleted or added file,
  and the opt-in `-Pjfr` profile (JDK 16+ event settings, no declared recording output).
- Moving or deleting an IntelliJ token in `@SuppressWarnings` for an inspection that `qodana.yaml`
  excludes (`ConstantValue`, `DataFlowIssue`, `NullableProblems`), even one that suppresses nothing
  where it sits. Only the IDE reports those inspections, so no build or CI step shows what the
  change does, and checking it by hand costs more than the token. Take such a change only with an
  IDE result in hand.

---

## Code generation

- `AddFastPath.java` emitting `fastpath()` without `final`.
- Dropping `WEAK_VALUES` and `SOFT_VALUES` from `Feature.fastPathIncompatible`, which the
  local-cache generator never passes, and `AddFastPath.execute` recomputing the predicate
  `applies` evaluated. Output-neutral, with no clarity gain worth the edit; the `Rule` split
  between `applies` and `execute` is deliberate.
- Field declarations and method shapes for evicting caches live in the generators, not in
  `BoundedLocalCache`. Trace a generated field back to its `AddX.java` before drawing a
  conclusion about its type or storage.

---

## Serialization

- The serialization proxy is internal. Its wire format is not a compatibility surface, and
  disclosure of it was declined.
- Serialization of an executor, scheduler, or non-serializable `BiFunction`.
- The proxy's `asMap()` view not being `Serializable`.
- A serializable component that refers back to its own cache. The component is read before
  `readResolve` rebuilds the cache, so its field receives the proxy: a cache-typed field throws
  `ClassCastException` and an untyped one keeps the stale proxy. It fails only when the stream
  reaches the cache before the rest of the cycle. Guava's proxy extends `ForwardingCache` for
  this case; both that shape and a javadoc qualifier were declined.
- Hand-built streams that no `writeReplace` produces: an internal async adapter
  (`AsyncRemovalListener`, `AsyncEvictionListener`, `AsyncWeigher`, `AsyncExpiry`) placed in a
  proxy's callback field, or a Guava facade whose `cache` is null or not a `LoadingCache`. A
  crafted stream already chooses its callbacks, so an adapter or a broken facade gives it nothing
  a hostile callback lacks; it fails on use rather than on read. `readObject` guards were
  declined.

---

## Low-yield lenses

Recorded so a run order can price them, not to discourage a fresh look.

- Re-entrancy as a whole has closed at zero for a full pass. Callback re-entrancy warnings
  are explicitly not wanted, which removes the remedy from most of what it finds.
- Simulator periphery, per the section above.
- Formal-shape lenses (jmm, linearizability, arithmetic, correctness-proof, map-contract)
  have gone several passes without a defect on the core. They are cheap; run them, but do
  not spend a scarce second model on them first.

---

### .Claude Docs Synchronization (`.claude/docs/synchronization.md`)

# Synchronization Reference

## Lock Hierarchy (strict total order)

```
evictionLock (outer, ReentrantLock)
  └─ CHM bin lock (implicit in compute/computeIfPresent)
      └─ synchronized(node)
          └─ refreshes bin lock (inner, discardRefresh)
```

Never acquire in reverse order. evictionLock is NEVER acquired while holding
synchronized(node). synchronized(node) is acquired in various contexts: inside
CHM compute lambdas, under evictionLock outside compute (makeDead, AddTask),
or standalone (put fast path, Policy API methods).

`discardRefresh` takes a `refreshes` bin under the `data` bin lock, the node monitor, or both
(the node monitor alone on `put`'s fast path), and under `evictionLock` when maintenance evicts.
Nothing in the library takes another lock while holding a `refreshes` bin; a user reload that
re-enters the cache does (see *CacheLoader.asyncReload / asyncLoad* below).

**Never hold `synchronized(node)` across a CHM mutating call** (`data.putIfAbsent`/
`compute`/`computeIfPresent`/`merge`). CHM takes bin-head locks internally not just to
link the entry but also for a follow-on table resize (`addCount`→`transfer`, which runs
*after* the node is already visible). Holding a node monitor across the call is therefore a
`node`→bin acquisition that deadlocks against a concurrent `remove`/`compute`'s standard
bin→node order (a real `put`-insert vs `computeIfPresent`-remove deadlock was shipped and
reverted this way). To mutate the map and node-scoped state together, do it *inside* the CHM
lambda — which holds only the bin lock, no node monitor — e.g.
`data.computeIfAbsent(ref, k -> { discardRefresh(k); return node; })`.

## What Each Lock Protects

### evictionLock
- Window/probation/protected deques
- Timer wheel
- Frequency sketch mutations
- Weight counters (weightedSize, windowWeightedSize)
- Drain status transitions (under lock)
- policyWeight, split across its own field and the node's metadata word
- metadata field on nodes (queue type, and a weighted node's policy weight high half)

### CHM bin lock (held implicitly during compute/computeIfPresent/merge)
- Atomic read-modify-write on map entries
- Ensures "at most once" for mapping functions

### synchronized(node)
- Value field mutations (setValue)
- Weight field mutations (setWeight)
- Access time / write time updates
- Node lifecycle transitions (alive → retired → dead)
- Expiration re-validation during eviction

## Node Field Access Modes

| Field | Read Mode | Write Mode | Guard |
|-------|-----------|------------|-------|
| value (strong) | getAcquire | setRelease + storeStoreFence; `die()` plain-sets null | synchronized(node) for mutations; fence orders the store before subsequent timestamp stores |
| value (weak/soft) | getAcquire | setRelease + storeStoreFence | synchronized(node); the release store publishes the new reference, and the fence orders it before the old reference's `clear()` and subsequent timestamp stores |
| key (strong) | getOpaque | set (retire/die only) | synchronized(node) for retire/die; otherwise immutable after construction |
| key (weak) | plain | set (retire/die only) | synchronized(node) for retire/die; otherwise immutable after construction; getRef uses getOpaque |
| key reference held in a weak/soft value reference (W/D nodes, which have no key field) | getOpaque, after the value reference's getAcquire | plain in the constructor; `setValue` copies it into the replacement reference; retire/die setOpaque a sentinel on the current reference | synchronized(node); `setValue` runs only after an `isAlive` check, so it never copies a sentinel |
| accessTime | getOpaque | setOpaque | benign races acceptable |
| writeTime | getOpaque | setOpaque; CAS of the low bit | synchronized(node), except `refreshIfNeeded`'s lock-free `casWriteTime` that sets and clears the refresh soft-lock bit; `BoundedLocalCache.setWriteTime` always stores an even value (`toWriteTime`) |
| variableTime | getOpaque | setOpaque, CAS | synchronized(node), except `tryExpireAfterRead`'s lock-free CAS |
| weight | plain | plain | synchronized(node); unlocked reads accept staleness |
| policyWeight | plain | plain | evictionLock; unlocked reads accept staleness |
| metadata | plain | plain | evictionLock |

`variableTime` has no field of its own. A variable-expiry node stores it in `writeTime` (`…W…`
classes without refresh) or in `accessTime` (`…A…R` classes, variable expiry plus refresh), and the
timer-wheel links reuse the write-order or access-order links. `NodeFactory.getClassName` picks
the class so that the aliased feature is disabled, and `setAccessTime`/`setWriteTime` gate on
`expiresAfterAccess()`/`expiresAfterWrite() || refreshAfterWrite()`, so two features never write one
field.

Values are release/acquire, not volatile, so two readers reading two keys in opposite orders may
disagree on which of two independent writes came first (IRIW) on the bounded cache, which
`ConcurrentHashMap`'s `volatile val` forbids on the unbounded one. `ConcurrentMap` promises
happens-before per key only, and HotSpot cannot show it: x86 and ARMv8 are multi-copy atomic, and
on POWER the CHM volatile load between the two value loads is a full barrier.

`getPolicyWeight` reads two plain fields, its own and `metadata`, so an unlocked reader can
see them from different moments. Maintenance callers hold `evictionLock`. A `Policy` snapshot's
caller holds it through the mapping function; parallel workers do not acquire it themselves.
The snapshot clamps the result into the `int` range for `CacheEntry.weight`. That is the policy's
view, not what a writer last published under the node's monitor, so a concurrent update can pair a
value with an older weight; the snapshot is best effort rather than synchronizing every node. A
replayed update can also leave the policy weight negative or beyond the `int` range, a weight no
entry has. `WeightLimiter` reads a negative running sum as overflow and stops, so without the clamp
a finite `coldestWeighted` or `hottestWeighted` limit ends at that entry.

Note: the expiry read protocol pairs these modes. A rewrite stores the value and then the
timestamps, with `setValue`'s trailing `storeStoreFence` holding that order; a lock-free
reader calls `hasExpired` (timestamps, ending in a `loadLoadFence`) and then loads the value.
A reader that observes a timestamp written by the rewriting thread is therefore guaranteed that
thread's value, which closes the tear. `tryExpireAfterRead` repeats the reader's half between the
deadline its CAS expects and its value-identity check. It does not follow that an already-announced EXPIRED value
is never returned. A reader that judged the entry live at its own clock reading can park in
`Expiry.expireAfterRead` and land `tryExpireAfterRead`'s CAS after a writer has announced the
entry's EXPIRED eviction but before it stores the replacement, leaving the announced value looking
fresh to a later reader. The opaque `setAccessTime` store for a fixed `expireAfterAccess` has the
same window with no callback, needing only a pause between `hasExpired` and the store. That is
the read-extension resurrection in `design-decisions.md`, and it is a benign race: any lock-free
read can be invalidated an instruction after it returns, so closing it would require synchronized
reads. Readers must not load the value before `hasExpired`, and writers must not store a
timestamp before `setValue`; a caller acting on an expired verdict exempts an in-flight async load
via `isComputingAsync` on a value loaded after the check.

## Drain Status State Machine

```
IDLE(0) ──CAS──► REQUIRED(1) ──schedule──► PROCESSING_TO_IDLE(2)
  ▲                                              │
  └──────────CAS success──────────────────────────┘
                                                  │
                                           CAS from REQUIRED
                                                  │
                                                  ▼
                                    PROCESSING_TO_REQUIRED(3)
                                           │
                                           └──► set REQUIRED, loop
```

- IDLE → REQUIRED: CAS by `scheduleAfterWrite()` (lock-free)
- REQUIRED → PROCESSING_TO_IDLE: set by `scheduleDrainBuffers()` or `maintenance()` (under evictionLock)
- PROCESSING_TO_IDLE → IDLE: CAS by `maintenance()` exit (under evictionLock)
- PROCESSING_TO_IDLE → PROCESSING_TO_REQUIRED: CAS by `scheduleAfterWrite()` (lock-free)
- PROCESSING_TO_REQUIRED → set REQUIRED: by `maintenance()` exit (under evictionLock)

Access modes: opaque and acquire reads, release and opaque writes, CAS for transitions.

**Both processing states are terminal under an executor that accepts a task and drops it.**
`scheduleDrainBuffers` sets `PROCESSING_TO_IDLE` *before* `executor.execute(drainBuffersTask)`, so
a silently-discarding executor leaves the machine parked there with no maintainer. Every exit is
then closed: `shouldDrainBuffers` returns false for both processing states, `scheduleDrainBuffers`
returns early on `>= PROCESSING_TO_IDLE`, and both of `scheduleAfterWrite`'s processing arms
return. The only way out is `afterWrite`'s inline assist, which needs the write buffer to fill, so
a cache that goes quiescent *or* read-only stays parked. This is by design and is what
`Caffeine.executor`'s javadoc means by an executor "that discards tasks or never runs them may
experience non-deterministic behavior"; `BoundedLocalCacheTest.scheduleDrainBuffers_discarded_quiescent`
pins the parked state deliberately. Don't add a recovery path for a broken executor.

## User Callback Invocation Points

The lock contexts below describe locks acquired by each cache path; callbacks can also inherit
locks held by their callers.

### notifyEviction — INSIDE synchronized(node)
Irrevocable. In `remap()` and `doComputeIfAbsent()` it precedes the mapping function, weigher,
and expiry callbacks; `put()` calls its weigher (outside all locks) and `expireAfterCreate` first.
- `put()`, `evictEntry()`, `remove()`, `removeNode()`
- `remap()`, `doComputeIfAbsent()` (before try block with user code)

### notifyRemoval — OUTSIDE synchronized(node) and CHM bin lock, AFTER mutations
Wraps the listener call in a task submitted to the executor. The actual
`onRemoval` normally runs in the executor thread outside all locks. Note: in
`evictEntry` and `removeNode`, notifyRemoval is called while evictionLock is
still held (but outside synchronized(node) and CHM bin lock).

**Inline delivery**: a caller-runs executor such as `Runnable::run` runs the task on the caller
thread, and if `executor.execute(task)` throws (such as `RejectedExecutionException`) the task is
invoked inline (`task.run()`). From `evictEntry`/`removeNode` that caller thread is holding
`evictionLock`, so the user's `RemovalListener` runs synchronously under the eviction lock. A
listener that re-enters the cache via a write can deadlock in that path.

### Mapping functions (compute, merge, etc.)
Lock context depends on path:
- **Existing node**: INSIDE CHM bin lock + synchronized(node)
- **New node (n == null)**: INSIDE CHM bin lock only (no node to synchronize on)
Re-entrant cache operations from these callbacks can deadlock, and on a caller-runs executor a
read that schedules maintenance can silently corrupt the computation.

### Weigher.weigh
Lock context depends on path:
- **put()/replace()**: OUTSIDE all locks (called before CHM operation)
- **compute/merge (existing node)**: INSIDE CHM bin lock + synchronized(node)
- **compute/merge (new node)**: INSIDE CHM bin lock only
Can throw (caught by catch-commit-rethrow in compute paths).

### Expiry callbacks
Lock context depends on path:
- **put()**: INSIDE synchronized(node) only (no CHM bin lock)
- **compute/merge (existing node)**: INSIDE CHM bin lock + synchronized(node)
- **New node creation**: varies by call site
Can throw (caught by catch-commit-rethrow in compute paths).

### StatsCounter
`LocalCache.statsAware` records misses and load outcomes inside the computation, under its
CHM bin lock and, for an existing bounded node, its node monitor. During maintenance,
`recordEviction` runs under `evictionLock`. `GuardedStatsCounter` suppresses exceptions; it
does not defer these calls.

### CompletableFuture dependent actions
Non-async actions may run inline on a thread invoking a completion method, or on the
registering thread when the future is already complete. They inherit any locks that thread
holds. `LocalAsyncCache.handleCompletion` can remove the mapping or finalize its weight and
expiration, so completing a cached future inside a mapping function can recursively modify
the cache. `AsyncBulkCompleter.fillProxies` acquires no surrounding lock before completing
proxies, but its caller may already hold one.

### CacheLoader.load
Lock context depends on path:
- **Initial load (new node)**: INSIDE CHM bin lock, NOT inside synchronized(node)
- **Expired/collected existing node**: INSIDE CHM bin lock + synchronized(node)

### CacheLoader.asyncReload / asyncLoad — INSIDE the `refreshes` CHM bin lock
Refresh registration invokes the loader inside a `refreshes.compute*` bin lambda, so the
loader dispatch (including the default `CompletableFuture.supplyAsync`'s `executor.execute`)
runs under the **refreshes** bin lock. Two consequences, both bounded to user misuse:
- **Caller-runs executor** (`Runnable::run`, a saturated `CallerRunsPolicy` pool, or a
  synchronous prefix) runs the entire user reload body under that lock — a slow reload stalls
  other refreshes on the same bin and every discard of a refresh registered there, each waiting
  while it holds its own lock: a completion releasing its registration (its `data` bin), a write
  of that key (its bin or node monitor), `clear()`'s up-front purge of the table, or maintenance
  evicting or expiring it, which waits holding `evictionLock`, so all writers stall once the write
  buffer fills. The Guava facade's default `reload` is this case on any executor. The table cannot
  be presized. Moving the loader call out of the registration needs a placeholder token, which
  §*Refresh Internals* rules out, and deferring the discard past the eviction would still stall the
  thread running maintenance while separating the discard from the write that requires it.
- The only closing leg of a `refreshes-bin → data-bin` order (which would invert `discardRefresh`'s
  `data-bin → refreshes-bin`), or of a `refreshes-bin → refreshes-bin` order across two reloads, is
  a user loader re-entering the cache, where reading a stale key is enough — the documented
  callback-re-entrancy hazard, not an in-library cycle. See design-decisions, *ConcurrentHashMap
  Constraints*.

### RemovalListener.onRemoval — OUTSIDE all locks (normal path)
Delivered asynchronously via executor. Safe for re-entrant cache operations on
the normal path. Exception: see inline delivery under `notifyRemoval`; a caller-runs or
rejecting executor delivers on the caller thread, which holds `evictionLock` for
eviction-triggered removals.

### EvictionListener — INSIDE synchronized(node), varies for other locks
- In `evictEntry`/`removeNode`: evictionLock + CHM bin lock + synchronized(node)
- In `put()`: synchronized(node) only
- In `remove()`/compute paths: CHM bin lock + synchronized(node)
- Writers to the key wait for the listener; lock-free readers do not, and return the value until the
  computation completes. That is `ConcurrentHashMap.compute`'s atomicity and what
  `Caffeine.evictionListener` means by "the atomic operation to remove the entry". A listener that
  disposes of the value must tolerate a reader still holding it, as it must after any removal.
Synchronous. Re-entrant cache operations risk deadlock or corruption, including a read that runs
maintenance inline on a caller-runs executor.

### Other user components under evictionLock
These also run while `evictionLock` is held: `Scheduler.schedule` (through `Pacer`),
`Executor.execute` (the maintenance task and eviction's removal notifications),
`StatsCounter.recordEviction`, `Ticker.read`, and `Policy` snapshot mapping functions (after
maintenance). One that blocks stalls maintenance, then writers once the write buffer fills.

## Buffer Semantics

### Read buffer (BoundedBuffer — striped ring buffer)
- Lossy: drops are benign (affects eviction quality, not correctness)
- Lock-free CAS on per-stripe slots
- Drained under evictionLock during maintenance (single consumer)

### Write buffer (MpscGrowableArrayQueue)
- Guaranteed delivery: if offer fails, acquires evictionLock
  and runs maintenance inline
- Tasks: AddTask, UpdateTask, RemovalTask
- Drained under evictionLock during maintenance (single consumer)

---

### .Claude Docs Testing (`.claude/docs/testing.md`)

# Test Infrastructure Guide

## Parameterized Test System

Tests use a custom JUnit Jupiter parameterization framework built on `@CacheSpec`.

### Writing a test

```java
@ParameterizedTest
@CacheSpec(population = Population.FULL, expireAfterWrite = Expire.ONE_MINUTE)
void myTest(Cache<Int, Int> cache, CacheContext context) {
  // cache is pre-populated per @CacheSpec
  // context holds configuration + test utilities
  assertThat(cache).hasSize(context.original().size());
}
```

### How it works

1. **`@CacheSpec`** declares test dimensions as enum values. Each dimension
   (implementation, compute, keys, values, maximum, expiry, loader, etc.)
   has a set of enum variants.

2. **`CacheGenerator`** computes the Cartesian product of all declared enum
   values, filtered by compatibility rules.

3. **`CacheProvider`** (implements `ArgumentsProvider`) feeds combinations to
   JUnit. It inspects the test method's parameter types to filter further —
   a method taking `AsyncCache` won't receive sync-only configurations.

4. **`CaffeineCacheFromContext`** constructs a `Caffeine` builder from each
   `CacheContext` configuration, populates it, and passes it to the test.

5. **`CacheValidationInterceptor`** runs post-test validation on every test
   via JUnit's `InvocationInterceptor`:
   - Validates internal cache structure (`CacheSubject.isValid()`)
   - Checks executor completion
   - Verifies statistics consistency
   - Validates removal/eviction notifications

### Key classes

| Class | Location | Purpose |
|-------|----------|---------|
| `CacheSpec` | testFixtures | Annotation declaring test dimensions |
| `CacheProvider` | testFixtures | JUnit ArgumentsProvider |
| `CacheGenerator` | testFixtures | Cartesian product + filtering |
| `CacheContext` | testFixtures | Configuration holder + utilities |
| `CaffeineCacheFromContext` | testFixtures | Context → Caffeine builder factory |
| `CacheValidationInterceptor` | testFixtures | Post-test validation |
| `Options` | testFixtures | `-P` system property filters |

### CacheContext utilities

- `context.ticker()` — FakeTicker for time control (`ticker.advance(Duration)`)
- `context.original()` — pre-populated entries (insertion order)
- `context.absentKey()` / `absentValue()` — guaranteed-absent values
- `context.firstKey()`, `middleKey()`, `lastKey()` — keys from population
- `context.intern(T)` — thread-local interning for weak/soft reference tests
- `context.evictionListener()` — cast to `ConsumingRemovalListener` for assertions

### Population sizes

- `EMPTY` = 0, `SINGLETON` = 1, `PARTIAL` = 25, `FULL` = 50
- Pre-populated caches never exceed max size (no evictions during setup)

### Filtering with -P flags

```bash
-Pimplementation=caffeine  # caffeine or guava
-Pkeys=strong              # strong or weak
-Pvalues=strong            # strong, weak, or soft
-Pcompute=sync             # sync or async
-Pstats=enabled            # enabled or disabled
```

Processed by `Options.java`. Case-insensitive enum matching.

### Sharding

```bash
-PshardCount=40 -PshardIndex=0
```

`ShardedTestFilter` partitions at method granularity (FarmHash) for balanced CI.

## Assertion Patterns

### Truth subjects

```java
import static com.github.benmanes.caffeine.testing.CacheSubject.assertThat;

assertThat(cache).isEmpty();
assertThat(cache).hasSize(5);
assertThat(cache).containsKey(key);
assertThat(cache).containsExactlyKeys(list);
```

Custom subjects: `CacheSubject`, `AsyncCacheSubject`, `CacheContextSubject`,
`MapSubject`, `CollectionSubject`, `FutureSubject`. `LocalCacheSubject` (reached via
`CacheSubject.isValid()`) is the deep internal oracle; its climber checks assert the probe-state
invariants — ladder bounds, `refractoryLeft ≤ starvation.rung` and the walk budget it was split
from (`Walk.samples`, on an object that exists only while walking), sample-counter sanity
including `sample.probationHits ≤ sample.hits − sample.windowHits`, and a finite, non-negative
frozen probation baseline.

### Removal listener verification

```java
var listener = (ConsumingRemovalListener<Int, Int>) context.evictionListener();
// ... cache operation ...
assertThat(listener.removed()).containsExactly(
    new RemovalNotification<>(key, value, RemovalCause.EXPIRED));
```

### Async coordination

```java
import static com.github.benmanes.caffeine.testing.Awaits.await;

await().until(() -> cache.estimatedSize() == expected);
await().until(() -> executor.submitted() == executor.completed());
```

#### Pinning a non-termination bug

`@Timeout` cannot catch one. Jupiter's default thread mode is the same thread and the project sets
no `junit.jupiter.execution.timeout.thread.mode.default`, so the deadline is only checked after the
method returns and a spinning test hangs the build instead of failing it. The suite's futures are
also joined without a bound, so `FutureSubject.succeedsWith` blocks forever on its own.

Drive the completion that triggers the spin from `ConcurrentTestHarness.executor` and gate the
assertions behind a bounded `await()`, which fails with `ConditionTimeoutException` at Awaitility's
default. `AsyncLoadingCacheTest.refresh_bulkAbsentKey` is the worked example: the `await()` before
its `succeedsWith` calls is what makes a regression fail rather than hang, so do not remove it as
redundant.

### GC testing

```java
var ref = new SoftReference<>(target);
GcFinalization.awaitClear(ref);  // Forces GC + reference processing
```

Use `Awaits.awaitFullGc()` for full GC with cleaner registration.
Inherently flaky — use `GcFinalization` utilities, not `System.gc()`.

## Race Testing Patterns (BoundedLocalCacheTest)

These patterns force specific interleavings without Fray. Used for white-box
testing of internal paths.

### Forcing the slow path (synchronized(node) block)

Hold the node monitor on the main thread, start a writer on another thread,
wait for it to block, then mutate state while it's blocked:

```java
synchronized (node) {
    var future = CompletableFuture.runAsync(() -> {
        cache.asMap().putIfAbsent(key, value);
    });
    await().until(() -> writerThread.getState() == BLOCKED);
    // node state is now accessible while writer is blocked
    node.setAccessTime(now);
}
// writer proceeds after releasing the monitor
```

A white-box race test that parks a thread should advance the ticker while it is parked when the
path under test stamps timestamps; a frozen `FakeTicker` hides any defect that needs time to pass
during a lock wait.

### Observing retired node state

Hold `evictionLock` to prevent maintenance, run a removal on another thread,
observe the intermediate retired state:

```java
cache.evictionLock.lock();
try {
    CompletableFuture.supplyAsync(() -> cache.remove(key)).join();
    assertThat(node.isRetired()).isTrue();
    cache.cleanUp();
    assertThat(node.isDead()).isTrue();
} finally {
    cache.evictionLock.unlock();
}
```

### Thread state polling for race coordination

Use Awaitility + `Thread.getState()` instead of latches:

```java
await().until(() -> {
    var thread = writer.get();
    return thread != null && EnumSet.of(BLOCKED, WAITING).contains(thread.getState());
});
```

### State reset after intentional corruption

When tests corrupt internal state (retiring nodes, clearing references),
always reset to prevent `CacheValidationInterceptor` failures:

```java
// after intentionally corrupting state
requireNonNull(context.build(key -> key));
```

### Accessing internals

Cast to `BoundedLocalCache` for white-box testing:

```java
var localCache = asBoundedLocalCache(cache);  // private helper
```

## Issue Regression Test Patterns

Tests in `issues/` use different interleaving techniques:

- **Issue568Test** (value visibility): 10 threads x 1000 iterations with random
  50% put/get probability. Detects stale null reads via brute-force contention.
- **Issue412Test** (incorrect cause): Zipfian key distribution + nanos-level TTL
  to maximize expiration-vs-invalidation race window.
- **Issue859Test** (liveness): 100K iterations with real `Scheduler` (wall-clock,
  not FakeTicker) to test that maintenance scheduling is not lost.
- **Solr10141Test** (eviction+update race): 64 threads x 156K reads each with
  `AtomicBoolean` per-value liveness flag to detect double-removal.

## Choosing the Dynamic Tool for an Escalated Finding

Static analysis escalates interleavings it cannot adjudicate. Pick the tool by
what the hypothesis needs, then port the skeleton into the source tree and run
it — an unported skeleton is a TODO, not a resolution.

| Hypothesis | Tool | Why |
|---|---|---|
| Interleaving of operations at synchronization points | Fray (`caffeine/src/frayTest/`) | Systematic scheduler exploration; cannot see plain-field data races (sync-point granularity) |
| Plain/opaque-field data race, SC-reachable | LinCheck model checking (`caffeine/src/lincheckTest/`) | Field-access granularity catches races Fray cannot |
| Weak-memory publication/reordering (store-buffer etc.) | jcstress (`caffeine/src/jcstress/`) | The only tool that exercises the hardware memory model |

jcstress caveat: model the real structure, not an isolated primitive — isolated
shapes over-state reachability (a single-slot repro can be unreachable once the
real ring buffer's wrap gap is modeled). A reproduced result on a simplified
shape justifies a fix only if the real structure also reproduces or the fix is
free. For suspected JIT-dependent interleavings a single run is not evidence of
absence — vary seeds across many short launches instead of one long run.

These tools answer an escalated interleaving. A path that sequential tests pin and that no report
left ambiguous does not get a Fray, LinCheck or jcstress test for coverage.

Two instruments hold a transient state deterministically in a standalone witness. An executor
that queues tasks without running them (`deferred::add`, drained afterwards) keeps expired or
collected entries unreaped while view paths are compared; `CacheExecutor.DISCARDING` is the
in-suite form. A dependent registered with `whenComplete` after the cache inserts a future runs
before the cache's completion handler, so it observes a failed future that is still mapped. Both
are instruments: price a finding they expose on the default executor.

## Fray Concurrency Tests

Location: `caffeine/src/frayTest/`

```java
@ExtendWith(FrayTestExtension.class)
final class EvictionFrayTest {
  @FrayTest(iterations = 10_000, resetClassLoaderPerIteration = false)
  void concurrentPutAndEvict() throws InterruptedException {
    Cache<Integer, Integer> cache = Caffeine.newBuilder()
        .maximumSize(1).executor(Runnable::run).build();
    var t1 = new Thread(() -> cache.put(1, 1));
    var t2 = new Thread(() -> cache.put(2, 2));
    t1.start(); t2.start();
    t1.join(); t2.join();
    cache.cleanUp();
    assertThat(cache).isValid(); // CacheSubject / AsyncCacheSubject deep oracle
  }
}
```

- 10,000 iterations with systematic interleaving exploration
- Direct thread creation (no parameterization framework)
- `executor(Runnable::run)` for inline execution
- The `frayTest` source set has no `CacheValidationInterceptor`, so each test must
  call `assertThat(cache).isValid()` itself (the suite depends on `testFixtures`;
  use `AsyncCacheSubject.assertThat` for async caches). This is the deep oracle —
  it drains to quiescence and checks deque↔data consistency, the telescoping weight
  sum, and node lifecycle.
- Do NOT use `estimatedSize() == asMap().size()` as the consistency oracle: both
  read the same `ConcurrentHashMap` (`data.mappingCount()` vs `data.size()`), so the
  assertion is a tautology that cannot detect a node stranded or duplicated in a
  deque. Keep specific assertions (exact counts, weight bounds, values) alongside
  `isValid()`.

## Test Utilities

- **`TrackingExecutor`** — wraps executor with `pause()`/`resume()` gate
  and `submitted()`/`completed()`/`failed()` counters
- **`ConcurrentTestHarness.timeTasks(nThreads, task)`** — synchronized start
  via CountDownLatch
- **`FakeTicker`** (Guava) — `advance(Duration)` for time control
- **Validation annotations**: `@CheckNoEvictions`, `@CheckMaxLogLevel(WARN)`,
  `@CheckNoStats`

## Coverage (JaCoCo)

The aggregate `jacocoFullReport` merges every `**/*<module>*/**/jacoco/*.exec` across
`:caffeine`, `:guava`, `:jcache` (main source sets only — not simulator/examples). Target is
100% line and branch, with one sanctioned exception: `StripedBuffer.expandOrRetry`'s spin-lock
retry, which no test can deterministically drive.

To re-measure a specific class without the full CI matrix, run the covering test class(es) then
that module's report, e.g. `:jcache:test --tests 'CacheLoaderTest' :jcache:jacocoTestReport`,
and read `<module>/build/reports/jacoco/test/jacocoTestReport.xml` (per-line `mi`/`ci`/`mb`/`cb`
counters). The merged CI HTML can show *internally inconsistent* line vs. branch markers when
shards drift — trust a fresh local run over a stale merged report.

### `throw alwaysThrows()` is uncoverable — a helper that never returns

A call site like `throw processorFailure(e)`, where the helper *always* throws (every path ends
in `throw`), reads as **fully uncovered** even when a test drives it. JaCoCo infers a line's
coverage from a probe placed *after* it; the `invokestatic` unwinds before that probe fires, so
the call site's instructions never count — and paradoxically the always-throwing helper itself
shows covered while its only callers show `mi>0, ci=0`. That impossible combination (a covered
callee with uncovered call sites) is the tell.

Fix by making the helper **return** the exception for the caller to throw (rethrowing only the
one case it must, e.g. `Error`), so `throw helper(e)` becomes a direct throw JaCoCo can probe:

```java
private static RuntimeException processorFailure(Throwable e) {
  if (e instanceof Error) {
    throw (Error) e;                        // must rethrow: can't return an Error as RuntimeException
  } else if (e instanceof EntryProcessorException) {
    return (EntryProcessorException) e;     // return, don't throw
  }
  return new EntryProcessorException(e);
}
```

Watch for this whenever an audit fix replaces direct `throw`s with a throw-helper — the behavior
is identical but the coverage silently drops. (A separate way audit fixes shed coverage: an added
upstream short-circuit makes a downstream guard dead — e.g. `remap` skipping a vanished key before
the `replaceAll` lambda runs left that lambda's `oldValue == null` branch unreachable. There the
fix is to delete the now-dead branch, not to test it.)

## Window Climber Test Policy

- Climber behavioral pins — `WindowClimberGateTest`: a four-cell deterministic subset of the
  `/climber-gate` battery (whisper escape, position-jam control, demoflood adjudication, moat
  valley crossing) run as plain JUnit in the standard suite (~7s, seeded synthetic streams,
  generous bars). Each cell's bar is calibrated against a measured *broken* value, not against
  drift: the moat cell reads 58.2 healthy and 46.2 with the audit layer ablated, and is barred at
  53. A cell whose healthy-to-broken separation is under ~5pp belongs in the manual battery
  instead, where it is adjudicated on an N=8 mean (`whisper_mod_a12` is the worked example: 2pp
  of separation and already below LRU, so any CI bar there is loose or flaky). It exists so
  audit-clock liveness, trap escape, and probe-adjudication regressions fail CI; the full
  battery, its sentinels, and the real corpus remain `/climber-gate` (manual). Kept separate
  from `WindowClimberTest` on purpose: pitest's `targetTests` allowlist names that class, so
  folding the workload cells in would run them against every WindowClimber mutant (tens of
  minutes) and distort the calibrated ~88% kill baseline (396/451 over the nested-class scope,
  re-baselined 2026-08-07). The survivor population is five documented equivalence classes
  (min/max/abs boundary mutants; unreachable float-threshold edges; veto band comparisons
  shadowed by the on-band resync; ±1 divide-vs-multiply; removed layer `reset()`s that are
  no-ops on an already-reset machine) plus three honest soft spots queued for a pinning pass:
  mid-flight state across `resized`, the starvation confirm's release, and the reactive
  tier's period read. `WindowClimberFuzzer` scenarios: random samples, teleporting positions,
  fuzzed region geometry, partial adjustment application, cross-tier resizes
- Behavioral regret (does the climber close the gap on a workload) is not the unit suite's job:
  `/climber-gate` holds the known traps and `/audit-regret` searches for new ones, both in the
  simulator against `product.Caffeine`
- `WindowClimber` state or schedule changes must ALSO run
  `./gradlew :caffeine:fuzzTest --tests 'WindowClimberFuzzer'` — its oracle (mirrored by
  `LocalCacheSubject.checkHillClimber`) pins the state-machine invariants and CI runs it; a
  stale bound there shipped as a red CI job in 2026-07 because class-scoped `test` runs never
  touch the `fuzzTest` source set
- **New climber state must arrive with its invariants, or the fuzz run is theatre.** The oracle
  lives in `ClimberInvariants` (shared by the fuzzer and `LocalCacheSubject`), and it only pins
  the fields someone wrote a line for. The retest shipped 2026-08-19 with two new fields and no
  invariants; the fuzzer then passed 808,898 runs against a reachable defect (issue #2002) purely
  because nothing was looking at them. Add the well-formedness pins to `ClimberInvariants` and any
  cross-sample ones to the fuzzer, which is the only oracle that sees two consecutive states.
- **The guard rail's recovery path is close to unreachable by fuzzing, and that is by design.**
  Reaching it needs the smoothed rate to cross from a full veto margin *below* the anchor's claim
  to a full margin *above* it inside one sample. The margin is `max(1pp, 3·MAD)` priced from the
  rate's own scatter, so any input distribution lively enough to produce the four-sample shortfall
  that arms the veto also inflates the margin past what an α=0.2 EMA can cross in a step; the two
  conditions are anti-correlated by the machine's own noise pricing. Measured 2026-08-20 over four
  generator variants and ~2.4M executions against a tree with the #2002 defect present, all clean.
  Pin that layer's lifetimes with deterministic tests that seed `rates.smoothed`, and do not
  re-tune the fuzzer's rate generator hoping to reach it

## Fuzz Testing (Jazzer)

- Jazzer cannot run 2+ fuzz tests in the same JVM process
  ([jazzer#599](https://github.com/CodeIntelligenceTesting/jazzer/issues/599))
- `forkEvery = 1` is set in `build.gradle.kts` so each test class gets its own fork
- A file with several `@FuzzTest`s gives each its own `static final` nested holder class, as
  `PacerFuzzer` and `CaffeineSpecFuzzer` do:
  ```java
  final class MyFuzzer {
    static final class FuzzA {
      @FuzzTest(maxDuration = "5m")
      void fuzz(FuzzedDataProvider data) { ... }
    }
    static final class FuzzB {
      @FuzzTest(maxDuration = "5m")
      void fuzz(FuzzedDataProvider data) { ... }
    }
  }
  ```
- **Take a fuzzer's `--tests` pattern from `.github/workflows/build.yml`, don't guess it.** Each
  fuzzer is listed there with the form it needs: `PacerFuzzer*` and `CaffeineSpecFuzzer*` carry a
  trailing wildcard because their `@FuzzTest`s sit in nested holder classes, while
  `TimerWheelFuzzer` and the rest match bare. The bare name of a holder fuzzer matches no test and
  fails with "No tests found for given includes"
- Fuzz tests require `JAZZER_FUZZ=1` environment variable (set by the Gradle task)
- The current `fuzzTest` JaCoCo compatibility guard checks the configured Java toolchain
  language version, not a separately selected test launcher. With `-PjavaVersion=11
  -PjavaTestVersion=25`, it leaves JaCoCo enabled despite running Jazzer on JDK 25. The frozen
  retention study used a separate `-PjavaVersion=25 -PjavaTestVersion=25` fuzz build to follow
  that guard, preserving its Java 11 compile/unit evidence before the rebuild.
  Record each build's source identity and class-file version; a Java 25 fuzz pass is not
  Java 11 bytecode evidence.

## PIT Mutation Testing

- `./gradlew :caffeine:pitest` runs mutation testing on self-contained data structures:
  TimerWheel, FrequencySketch, Pacer, BoundedBuffer, StripedBuffer, MpscGrowableArrayQueue,
  AbstractLinkedDeque, Interner, Async, Scheduler, Caffeine (builder), CaffeineSpec (parser),
  WindowClimber
- `BoundedLocalCache` and `UnboundedLocalCache` are NOT in scope — the `@CacheSpec`
  parameterized test suite makes PIT's main process OOM during coverage collection,
  regardless of heap size (`mainProcessJvmArgs` doesn't effectively bump the forked JVM).
  Line coverage on those classes is already 100% via JaCoCo, and concurrency bugs aren't
  caught by mutation testing anyway
- `./gradlew :guava:pitest` runs it on the whole adapter package, which is at 120/120 killed
  (100% test strength) — a confirmation that the translation logic is asserted rather than
  merely executed, so re-running it can only show a regression
- **`testSourceSets` must name every suite that covers the target.** PIT runs the `test` task
  only, so a forked or spec suite registered as its own `JvmTestSuite` is invisible to it and
  everything that suite covers reports as `NO_COVERAGE` — a report full of phantoms rather than
  gaps. Scoped to `test` alone the guava adapter scored 85% with 17 "uncovered" cache methods;
  adding `compatibilityTest` gave 100%. The `pitest` block must also sit **after**
  `testing.suites` in the build file, or the source set does not exist yet
- **jcache is deliberately not wired up.** Its TCK sets `testClassesDirs` to a jar unzipped into
  `build/tck` rather than to a source set's output, so `testSourceSets` has nothing to point at
  and the TCK's ~493 tests cannot run under PIT: a run without them scores a misleading 77% (no
  kill is attributed to `org.jsr107.tck.*`), and roughly a quarter of the survivors are the
  statistics and JMX calls that are best-effort by policy. It also needs `skipFailingTests`,
  since a few tests reach unknown-enum branches through Mockito's static mocking, which fights
  PIT's agent. If jcache signal is ever wanted, scope `targetClasses` to the classes the unit
  suite owns outright (`CaffeineConfiguration`, `Expirable`, `EntryProcessorEntry`,
  `TypesafeConfigurator`), where the absent TCK does not distort the result
- Runtime: ~30-60 minutes. Use for ad-hoc runs, not CI
- Concurrency bugs aren't caught by mutation testing — rely on Fray/LinCheck/JCStress for that

---

--- METRICS ---
- Files Extracted: 11
- Estimated Token Budget: ~168190 tokens
- Canonical Reference: https://codewiki.google/github.com/ben-manes/caffeine
