Changelog
Every release in plain English, newest first — generated from the project's living notes (notes/version/).
2026-08
2026-08-02 — v1.5.2 — full compliance sweep: gaps closed
Ran a full fairyfox compliance audit (report:
notes/fairyfox-reports/2026-08-02-compliance-audit.md) — node compliant, Scorecard 7.9
(live) — and closed every gap it surfaced:
- Dating fix. This session's artifacts were misdated
2026-07-25; corrected to2026-08-02(adopting report renamed, session log moved tosessions/2026-08/, changelog moved to this file, manifest dates fixed). - Dockerfile base image SHA-pinned —
eclipse-temurin:21-jdk@sha256:da9d3a4f…(Scorecard Pinned-Dependencies 9 → 10; verified the digest against the local image + a cleandocker build). - Dependabot backlog triaged (9 PRs). Merged the green bumps (#27 scorecard-action, #28
codeql-action, #29 attest-build-provenance, #30 deploy-pages, #31 upload-pages-artifact applied
by hand, #35 kotest-property 6.2.3). Closed #33 (paper-api → 26.x — contradicts the
deliberate 1.21.x target) and #32 (bundled Kotlin 2.4.0 → 2.4.10, which breaks the CodeQL
pin). Closed #34 (ktlint 12 → 14 fails the build; needs a dedicated migration). Added
dependabot.ymlignores for paper-api major/minor andorg.jetbrains.kotlin*so neither recurs. The mergedcodeql-action/analyzebump (#28) skewed againstcodeql-action/init(still 4.36.3) and failed the required Analyze check on the release PR — alignedinitto 4.37.3 and grouped all github-actions bumps so a monorepo action can't half-bump again. (The full-CI gate caught this pre-merge — exactly its job.) mainrequired checks now includeServer smoke (Paper purpur-1.21.11)andLatest Paper smoke (stable + experimental)— the full-CI-gate mandate is now platform-enforced across all seven jobs.- Stale
release/1.5.0branch confirmed gone.
VERSION 1.5.1 → 1.5.2.
2026-08-02 — v1.5.1 — adopt hub 1.6.0: README structure & cross-linking standard
Ran the fairyfox check/adopt flow. Hub mirror fast-forwarded a6d7e68 → 8c6a50e (VERSION
1.5.1 → 1.6.0). The only project-facing change in the span is the new readme.md
standard (worded README cross-links — the site chrome doesn't travel with the README to
GitHub/registries, so it must carry its own mesh links); 1.5.2/1.5.3 are hub-side registry
edits. Pre-authorized by the standing adopt-standards-by-default ledger grant → adopted by
default. Applied to README.md:
- a worded docs link near the top (
fairyfox.io/papermc-despawned-items/), alongside the existing docs badge; - an organized "Get it" section (Documentation · Download/Releases · Source), with a note that Modrinth/Hangar/CurseForge rows land when those pages exist (not yet);
- a mesh footer near the bottom linking back into the Fairy Fox mesh.
## Verify run and recorded in adoption-manifest.md (readme → implemented, baseline → 1.6.0).
check-links green (40 files). Report: notes/fairyfox-reports/2026-08-02-adopting-updates.md.
Shipped as PATCH v1.5.1 (docs-only) via PR into main, full CI, tag, back-merge. VERSION
1.5.0 → 1.5.1.
2026-07
2026-07-24 — fix the Fabric client-mod build (green again) — Loom + yarn-mapping drift
The client/fabric companion mod's CI job (Build the Fabric client mod) had gone red from
upstream drift — not from any change here. Fixed it end to end so the full CI suite is green:
- Loom 1.11-SNAPSHOT → 1.13-SNAPSHOT (resolves 1.13.6). fabric-api
0.141.4's modules are now published built with Loom 1.13.3, and Loom refuses a mod built with a newer Loom than the consumer (Mod was built with a newer version of Loom (1.13.3), you are using Loom (1.11.8)). Same MC 1.21.11 / Gradle 9.6.1 / Java 21 toolchain. processResourcesnow reads gradle properties viaproviders.gradleProperty(...)— Loom 1.13 configures the task in a context whereProject.property(String)throws "unknown property".- Yarn-mapping source drift (1.21.11+build.1), fixed against the actual
mappings.tiny:RegistryByteBufmoved tonet.minecraft.network(was imported fromnet.minecraft.registry); and the removed staticScreen.hasShiftDown()is replaced with a GLFW read off the window handle (GLFW.glfwGetKey(client.getWindow().getHandle(), GLFW_KEY_*_SHIFT)) — stable across future mapping churn. - Verified:
client/fabric./gradlew buildBUILD SUCCESSFUL in 1m40s in-container, producingpapermc-despawned-items-client-1.5.0.jar.
2026-07-24 — docs-site chrome 2.2.1 → 2.3.0 (self-hosted fonts) + scorecard hygiene
Completing the fairyfox adoption's remaining items after the owner re-issued the "in full" mandate.
- Docs-site shared chrome bumped 2.2.1 → 2.3.0. The only bundle delta was self-hosted
fonts: the three OFL variable subsets (Fraunces/Inter/JetBrains-Mono latin
.woff2) + afonts.cssare now vendored intodocs-theme/chrome/and served from this project's own docs origin, replacing the Google Fonts hot-link in all three head sites (pages/_shell.html, the Dokkapage_metadata.ftl, and thechrome/head.htmlreference).fonts.cssuses relative URLs (the hub master's absolute/assets/fonts/would break on this Pages subpath), and bothbuild.gradle.ktschrome-vendoring Copy tasks now ship the font files besidemain.css.docs-theme/chrome/VERSION→ 2.3.0. Verified:assembleDocsSitebuilds green (5m46s in the Docker container); the built site self-hosts fonts on both hand-authored and Dokka API pages with nofonts.googleapis/gstaticrequests (the only remaining string hits are standards prose in<code>). This closes the last self-hosted-assets deviation the project carried by hand. The 1.4.0 web-interface rules (three-zone subnav, on-site Notes/Systems/ Reference,coins.js) were already satisfied. - Scorecard workflow hygiene.
scorecard.yml's top-levelpermissions: read-allis now documented in-file as a deliberate, write-free divergence from supply-chain §1's literalcontents: read(the Scorecard analyzer needs broad read; it grants no write) — recorded in the adoption manifest rather than left as an unexplained finding.
2026-07-24 — adopt fairyfox standards 0.20.2 → 1.5.1 (+ Docker local-first)
Ran the fairyfox check/adopt flow (pre-authorized by the standing adopt-standards-by-default
grant). The hub moved three standards releases since this node's last anchor (0.20.2):
0.21.0 (checklists-are-contracts + mandate-ledger, the "done means verified" release),
1.4.0 (complete-by-default + phase-by-default + web-interface enforcement, incl. the
20-badge default set), and 1.5.0 (the new Docker local-first standard).
- Reference standards refreshed to current hub wording: 17 mirrored copies updated (adopting-updates, agent-tooling, badges, coins, compliance, cross-project-sync, dependencies, deployment, engineering-quality, git-workflow, notes-system, planning, process-reports, repo-hygiene, self-hosted-assets, supply-chain-hardening, testing).
- New standards vendored into
notes/reference/:checklists-are-contracts.md,mandate-ledger.md,docker.md,ci-secrets.md,legal-docs.md, plus the hub's compiledVERIFY-INDEX.md(drives the fast/partial compliance mode). The behaviours of checklists-are-contracts and mandate-ledger were already enshrined in CLAUDE.md (the "Checklists Are Contracts" and "Owner Mandates Become Ledgers" standing instructions) — this files the reference copies + manifest rows for them. - Docker adopted for real (1.5.0's headline). Docker 29.5.3 with Linux containers is
present and working on the dev box, so the earlier "Docker 29 incompatible → Testcontainers
can't run locally" write-off is falsified and fixed, not routed around. Vendored a thin
Dockerfile+compose.yaml+.dockerignorefor a local-first Linux build/test loop (source bind-mounted, Gradle cache volume, host Docker socket mounted for docker-outside-of-docker withhost.docker.internalpinned). Validated end-to-end:docker compose run --rm build→./gradlew buildBUILD SUCCESSFUL in 12m33s inside the container, and the previously host-skippedMariaDbStorageTest(Testcontainers) now runs and passes (3/3) over the mounted socket. CLAUDE.md's Build/Run section carries the Docker local-first mesh rule. - Confirmed already-compliant (no change needed): the README badge block already carries
the full 20-badge set in canonical order (done in the v1.5.0 README rewrite); release.yml
already attaches SLSA provenance as a
.intoto.jsonlrelease asset (supply-chain §4). - Full per-standard compliance Verify + manifest row finalisation runs as the paired compliance pass (see the 2026-07-24 process report).
2026-07-22 — v1.5.0 (MINOR, in progress): per-target settings by command, and an optional client-mod protocol
The wand is gone. An earlier pass added a "despawn wand" — a tagged item that made right-click mean something special — plus a chest-inventory options menu. Both are deleted. The owner's objection is the right one: an item that pretends to be a tool, and a container that pretends to be a settings screen, are less predictable than a command, and a chat command is genuinely the more intuitive interface on a vanilla client. Server-side stays commands.
/despi target … is the new per-target surface, operating on the block you are looking at:
info— report the target's settingsenable·disable·toggle— switch a target on or off without unregistering it; the pipeline skips a disabled target but keeps its settings, which is what you want when a chest is temporarily full or being rebuiltpriority <1-10>— how often this target is drawn (a priority-3 target is picked three times as often as a priority-1 one)contraband accept|refuse— whether this target receives banned items instead of them being destroyed
Your own targets by default; anyone's with despi.elevated. Every failure explains itself
rather than doing nothing.
Optional client-mod protocol. A container screen has only real slots, so a plugin cannot put a button in one without replacing the whole screen and risking the player's items — but a client mod can, in a few lines. So the split is: the client draws and clicks, the server owns the truth. The bridge is now bidirectional with a handshake:
→ HELLO <version> ← WELCOME <version> <capabilities…> | UNAVAILABLE <reason>
→ QUERY|MARK|UNMARK|TOGGLE|PRIORITY|CONTRABAND <world> <x> <y> <z> [value]
← TARGET … | ABSENT … | DENIED … <reason>
The server owner is in charge, as a first-class feature: targets.client-mod.enabled: false makes the plugin refuse the handshake, so a conforming mod hides its interface instead
of showing dead controls — and /despi is completely unaffected. On servers that do allow
client mods, the despi.client permission decides who gets one. The handshake reports the
player's actual capabilities so the mod can grey out what the server would refuse.
Nothing the client sends is trusted: permission, reach, ownership and the location limit are all re-checked server-side, so a modified client can do nothing it could not already do by typing a command. Nothing is sent to a client that has not registered the channel, so vanilla players are unaffected. The protocol is plain text on a namespaced channel with no library to link against, so any third-party mod can interoperate.
Also: TargetActions split out of DespiActions (which had grown past its cohesion budget),
and a read-only ContainerOpenListener that pushes state when a modded client opens a
container so its button renders without a round trip.
2026-07-22 — v1.5.0 (MINOR, in progress): automated release screenshots · multi-platform publishing · README rewrite
Automated screenshots (scripts/screenshots.mjs). CI boots a real Paper 1.21.11 server with
the freshly built plugin, joins it with a Mineflayer director bot, builds each scene from the
server console, teleports a spectator camera into position and captures the frame at the exact
tick the subject is on screen. Seven planned scenes: the despawn network, items waiting out their
timer, a group relocating, the landing effect (wide + close), /recycle, the catch-all cluster,
and the pipeline under load. Fully background — no window, no human, no GPU.
Two capture backends behind one interface:
viewer(default) — prismarine-viewer's renderer inside headless Chrome with SwiftShader WebGL. Pure Node + Chromium, so it runs anywhere CI runs. Real block textures, entities and dropped items.client(opt-in) — a real Minecraft client under Xvfb, grabbed withffmpeg. The only way to photograph Minecraft's particle system, because particles are drawn client-side from a network packet and no server-side renderer ever sees them. Falls back tovieweron failure rather than failing the build. Each frame records its backend inmanifest.json, and the gallery page states which produced it — so the limitation is visible, not buried.
Publishing (.github/workflows/screenshots.yml, docs.yml, ci.yml). The capture is a
reusable workflow called by both CI (build artifacts on every push) and Docs (gallery on
Pages), so the artifact images and the site images are produced by identical code. Non-blocking
everywhere and deliberately not a required check on main: screenshots are a deliverable,
not a correctness gate. New /screenshots.html docs page renders screenshots/manifest.json at
runtime, so a newer capture set needs no page rebuild — and says so honestly when a build
produced none. README image URLs point at the published paths and therefore refresh themselves.
Multi-platform publishing + artifact bundle (release.yml). Releases now carry the jar, that
version's default config.yml, zipped API docs, a source archive, the screenshots, provenance,
and SHA256SUMS.txt over the lot. A new publish job pushes to Modrinth and CurseForge
(mc-publish, SHA pinned from the tag on the repo's new canonical owner) and Hangar, each
gated on its token secret so the workflow stays green until the owner creates the projects.
Declared loaders are paper · purpur · spigot · bukkit — deliberately not fabric/neoforge,
because claiming a loader the jar cannot run on is a support trap.
Platform matrix (notes/plans/platform-targets.md). Answers the owner's survey target by
target, sorted into: same jar (Paper/Purpur/Spigot/Bukkit), same codebase but real work (Folia —
needs a PlatformScheduler and a shared-state audit that v1.5.0's throttle maps just made
bigger), and separate implementation (Fabric, NeoForge, Sponge, Velocity, …). Hybrids
(Arclight/Mohist/Magma) explicitly unsupported.
README rewritten end to end — opens on the actual problem ("Minecraft deletes your stuff"), then what it does, a dedicated why an admin would install this section, a why a player would use it section, the permission-driven per-rank tuning table, the honest supported-platform matrix, and auto-refreshing screenshots.
2026-07-22 — v1.5.0 (MINOR, in progress): per-user throttling · void chance · catch-all storage
Two new admin-facing systems, both off by default so upgrading changes no behaviour.
Per-user throttling (throttle:). The existing performance: budget answers "how much
relocation work may the server do this tick". This answers the different question: "how much
of that budget may one player take" — so a single player dumping a double chest of junk can no
longer consume the whole pipeline while everyone else's items quietly expire. Four selectable
strategies:
rate— token bucket per player ("max per chunk of time"); bursts allowed, refills smoothly.concurrent— relocations in flight per player at once ("max per each one").fair-share— weighted round-robin drain; a weight-4 player is drained four times as fast as a weight-1 player. This is what makes "some users get more despawned items than others" true.combined— all three.
Allowances come from permissions, mirroring the precedence despi.limit.<n> already uses:
despi.throttle.rate.<n> · .concurrent.<n> · .weight.<n> · despi.throttle.bypass
(highest node wins; offline throwers fall back to the config defaults). Over-quota items are
deferred (lossless, the default), dropped, or routed to the catch-all (void). Ownerless
drops (mob loot, dispensers) are exempt unless throttle-unowned is set, so a mob farm can't eat
the budget players are meant to share.
Void chance + catch-all (void:). A configurable probability (chance, default 0.0) that
a despawning item is voided rather than relocated — rolled once at enqueue, not re-rolled per
candidate location. An admin-extensible banned-materials list layers on top of the built-in
contraband set. Crucially, both categories can now land somewhere instead of ceasing to exist:
one or more catch-all containers (world;x;y;z, mode: first|random) receive banned and
voided items, giving staff a server-wide lost-and-found and an audit trail. Catch-alls are
config-driven and admin-owned by design — no storage-schema migration, and no way for a player to
register one.
Testing (~60 new assertions across five new files, all layers): TokenBucketTest (pure,
clock-injected — rate behaviour asserted deterministically, never by sleeping),
ThrottleManagerTest (every strategy in isolation then combined, purge, reset, backwards clock),
ThrottleQuotasTest (real MockBukkit permission attachments — precedence, highest-wins, junk
nodes), CatchAllDeliveryTest (delivery, full-chest fallthrough, non-container, unknown world,
malformed config), SchedulerVoidAndThrottleTest (seeded Random, so the probabilistic void
path is asserted exactly rather than statistically), ThrottleAndVoidSettingsTest (clamping and
graceful degradation — a server owner's typo must never abort the load). koverVerify ≥ 90 and
detekt/ktlint stay green with no baseline and no suppression added.
2026-07-22 — Release v1.4.8 (PATCH): rolling latest-stable (hard) + latest-experimental (soft) Paper smoke
New CI job latest-paper proves the plugin still loads on Paper's moving frontier, resolved
live from fill.papermc.io:
- Latest STABLE (a real gate — a red run is investigated before shipping): newest version
whose latest build channel is
STABLE. Currently resolves to 26.1.2. - Latest EXPERIMENTAL (
continue-on-error— soft; dev builds are expected to be flaky, so surface issues without blocking): newest version overall. Currently 26.2 (BETA). scripts/server-smoke.shnow accepts thelatest-stable/latest-experimentaltokens and resolves them (jq over the versions list, newest-first; first STABLE wins). Resolver validated against the live API. Uses Java 25 (the 26.x line). Not added tomain's required status checks — a moving external target shouldn't hard-block an unrelated release. Full in-game (Mineflayer) functional coverage stays pinned to 1.21.11 (node-minecraft-protocol's ceiling), so the rolling jobs are load/enable smokes.
2026-07-22 — Release v1.4.7 (PATCH): uuid advisory cleared + optional local-playtest step
- Security: overrode the
uuidtransitive dep (viamineflayer) to ≥ 11.1.1 to clear Dependabot GHSA-w5hq-g745-h8pq (buffer bounds check, medium). CI-only smoke dep — never in the plugin jar, so no production exposure — but cleared per the no-parked-findings bar. Validatednpm ci+ mineflayer/uuid load. - New optional pre-ship stage:
scripts/local-playtest.ps1boots a local real Paper server in YAML storage mode (offline, under git-ignoredrun/) so the owner can connect their own Minecraft client tolocalhostand hand-test. Documented in CLAUDE.md as opt-in — offered only when previewing or before a ship, never automatic, never a release gate (the CI smoke jobs remain the hard gate).
2026-07-22 — Release v1.4.6 (PATCH): npm lockfile → Scorecard Pinned-Dependencies 9→10
Closes the last agent-doable Scorecard lever from the ship mandate's remaining-headroom list.
- Added a root
package.json+package-lock.json(lockfileVersion 3, integrity-hash pinned) for the CI-only Mineflayer smoke dependency (mineflayer 4.37.1), and switched the ci.yml "Install Mineflayer" step fromnpm install …@4.37.1tonpm ci. A bare version pin did not satisfy Scorecard (it wants a lockfile);npm cifrom a committed lockfile does → Pinned-Dependencies 9→10. Validated locally:npm ciinstalls 90 packages and mineflayer loads.node_modules/stays git-ignored. - Housekeeping: the 7 merged Dependabot branches were auto-pruned by GitHub on merge; stale local tracking refs cleaned.
2026-07-22 — Release v1.4.5 (PATCH): Scorecard 7.1, main required-status-checks (full-CI hard-enforced)
Closes the Scorecard clause of the 2026-07-21 ship mandate and records the result.
- OpenSSF Scorecard 6.9 → 7.1. The lift came from Signed-Releases 0→2 — the v1.4.4
provenance
.intoto.jsonlrelease asset (added in v1.4.4's release.yml) was detected — plus Binary-Artifacts 9→10. The provenance-as-asset change paid off in one release, contra the earlier pessimistic estimate. mainrequired status checks set (build,Analyze (java-kotlin), bothServer smokejobs,In-game client smoke) with strict up-to-date, viagh api— this platform-enforces the owner's full-CI-before-mainrule (GitHub blocks the merge unless all pass), and this PR is the first to ride the new gate end-to-end.- Docs: mandate ledger row 1 → done (7.1) with the honest before/after; status.md records the gate + score. Remaining Scorecard headroom (npm lockfile, more provenance'd releases) logged.
2026-07-21 — Release v1.4.4 (PATCH): ship-contract, phase-by-default, 8 Dependabot PRs handled, Scorecard hardening, tech debt cleared
Owner mandate: raise Scorecard into the 7.x range, remove tech debt, handle the 8 open
PRs, and enforce all of that — plus phase-by-default — in CLAUDE.md and notes. Ledger:
notes/plans/mandate-2026-07-21-ship-contract.md.
- Ship-contract enforced (CLAUDE.md + status.md): a release, by default, also drives OpenSSF Scorecard toward max / never below ≥ 7.0, removes tech debt, and triages + handles every open PR. Phase-by-default standing instruction: any owner/fairyfox request — or a self-set task — is broken into as many phases as needed.
- 8 Dependabot PRs handled: merged setup-java v5.6.0, checkout v7.0.1, upload-artifact
v7.0.1, action-gh-release v3.0.2, setup-gradle v6.2.0 (wrapper-validation bumped to match),
the gradle minor/patch group (sqlite-jdbc 3.53.2.1, testcontainers 1.21.4, mariadb-client
3.5.9, slf4j 2.0.18, kotlinx-coroutines 1.11.0), and HikariCP 5.1.0 → 7.1.0. Closed
#7 (paper-api → 26.1.2 — contradicts the deliberate 1.21.x target). Kotlin held at
2.4.0 — reverted the group's 2.4.10 bump, which would have broken CodeQL SAST (its
extractor rejects 2.4.10). plugin.yml runtime
libraries:synced to the tested versions (HikariCP 7.1.0, mariadb 3.5.9) so production loads what CI validates — sqlite-jdbc held at 3.49.1.0: 3.53.2.1 is on Maven Central but not on Paper's library-loader mirror, so it fails runtime plugin load. The server-smoke CI job caught this (local./gradlew buildwas green) — the reason main deployments must clear the full CI suite. - Full-CI gate enforced (CLAUDE.md + status.md, owner instruction): no merge to
mainuntil every CI job on the release PR is green — build, CodeQL, and all server/in-game smoke jobs, not a subset. - Scorecard hardening (6.9 → target ≥ 7.0): pinned the Mineflayer npm install to an exact
version (Pinned-Deps 9→10); attach the build-provenance bundle as a
.intoto.jsonlrelease asset so Signed-Releases detects it. Branch-protection required-status-check is the remaining reliable lever — owner action (sandbox blocked the settings API); see status.md / ledger. - Tech debt cleared: removed the three deprecated
by tasks.registeringKotlin-DSL usages (Gradle-10 warnings) →tasks.register(...). RemainingReportingExtension.filedeprecation is plugin-sourced (Kover 0.9.9, no build-script line) — recorded, not chased.
Owner-triggered retrospective on why the day's mandate took three prompts. New
CLAUDE.md standing instruction "Owner Mandates Become Ledgers" (verbatim
per-clause transcription, evidence-gated deferral, phase-end diff against the
owner's words, auto-escalation on repetition); hub report
notes/fairyfox-reports/2026-07-21-mandate-execution-failure-analysis.md with five
proposed mesh-wide standard amendments. Docs-only PATCH — shipped to main as
v1.4.3 (full gate: ./gradlew build + check-links.mjs green, PR CI + CodeQL green).
2026-07-21 — Release v1.4.2 (PATCH): exhaustive permission matrix, combined load + throughput proofs, Kotest/Pitest/JMH adopted, spark+JFR profiling, local Docker fixed
Third pass on the owner's mandate — every previously-deferred tool adopted, and the "does the job under load" dimension proven with numbers:
- Exhaustive command × permission matrix (
CommandPermissionMatrixTest): all 41 subcommand shapes × 3 permission levels = 123 individual JUnit test nodes, each asserting allowed AND denied via the command's own reply; plus a multi-player group scenario (player + admin workflows ending with a/recyclewhose stack verifiably lands in the registered chest). - Combined high-load tests (
CombinedLoadTest): despawn storm + 50 players commanding every tick → avg 0.79ms / p95 1.17ms / max 8.2ms per tick; 1000-command burst from 100 players in 39ms; throughput/conservation — a 5000-item burst fully delivered (nothing lost) in 251 ticks vs 250 theoretical minimum; budget scaling proven at 100/tick; persistence consistent while flushing mid-storm. Tuning guidance added toconfig.yml(performance:is budget-bound). - Kotest property tests adopted (
PropertyInvariantsTest): generator-driven roundtrips, garbage resilience, store set-semantics under random sequences, reward arithmetic. Pitest 1.19 adopted (pure core, informational): 107 mutants, 84% test strength, weekly CI artifact. JMH adopted: store ops single-digit ns at 100k entries (random-draw ~7ns, add+remove ~119ns), weekly CI artifact (bench.yml). - In-game CI test deepened to the full permission story: unprivileged denial seen
from the client, console op grant, real look-at
/despi add this, consolegive+/recycle→ "Done!", then a load phase (200×64 forced despawns under live bot traffic) with spark TPS/health numbers in the CI log and a JFR profile uploaded as an artifact from every run; post-load responsiveness asserted. copyBlockToLocationbranches covered further (banner-meta, brewing stand, beacon, beehive, command block, spawner alongside chest/furnace/sign/skull).- Local Windows Docker FIXED (owner was right to push): docker-java's fallback
API 1.32 vs Docker 29's minimum 1.40 — proven with
curl /v1.32/info→ 400. Fix:~/.docker-java.properties→api.version=1.44; MariaDB Testcontainers now run locally too. Harness note updated with the full diagnosis.
2026-07-21 — Release v1.4.1 (PATCH): server + client test layers automated in CI; Sonar scan wired; Testcontainers MariaDB
Second pass on the owner's testing mandate — the layers no mock can prove are now automated, closing the pyramid top to bottom (Kotlin → server → client):
- Real MySQL/MariaDB integration (
MariaDbStorageTest, Testcontainers): coversStorageFactory.buildMysql, CRUD + replace semantics, one-time YAML→MySQL migration, and the full manager lifecycle against a live MariaDB. Runs in CI; disables itself cleanly without Docker. (Local Windows: Testcontainers 1.21.3 cannot negotiate with Docker Desktop 29.5 — documented incompat innotes/reference/mockbukkit-harness.md; retest on TC upgrades.) - Headless real-server smoke in CI (
scripts/server-smoke.sh,server-smokematrix job): boots Paper 1.21.11 (target, Java 21) and 26.1.2 (forward-compat, Java 25) with the built jar on every push/PR; asserts a clean enable, no load errors, no self-disable. Replaces the manual smoke. - In-game client acceptance in CI (
scripts/ingame-smoke.mjs,ingame-smokejob): a Mineflayer bot — a real Minecraft client — joins offline-mode Paper 1.21.11, runs/recycleand/despi exists anywhere owned-by-me, and asserts the plugin's replies reach the client. Validated end-to-end locally first. - SonarQube Cloud CI scan wired (
SonarSource/sonarqube-scan-action, SHA-pinned, token-guarded self-skip): imports the Kover XML as coverage inside Sonar. Owner uploadedCODECOV_TOKEN/SONAR_TOKEN/SCORECARD_TOKEN; CodeFactor already active on PRs. If Sonar reports an Automatic-Analysis conflict, flip the project to CI-based analysis (owner-side UI). - Test-scratch ignores (
smoke-*/,node_modules/), slf4j binding for test logs. - Sonar org key corrected to the minted
junebug12851(verified viaapi/components/show— the GitHub slug guess1fairyfoxdidn't exist, exactly the trap the properties file warned about). - CodeQL cache pitfall fixed: the traced compile now runs
--rerun-tasks --no-build-cache— a cache-hitcompileKotlintraces nothing and CodeQL dies with "could not process any code" (regression pin in the workflow comment). codecov.ymladded:partials_as_hitsso Codecov reports line coverage (same meaning as the Kover gate, ~95%) instead of counting branch-partial lines as misses (~87%); project status target 90% — the owner's floor, now enforced on the Codecov side too.
2026-07-21 — Release v1.4.0 (MINOR): full-layer test suite (44%→~95%, gated ≥90%), 3 bug fixes, /despi recycle + renameable commands, CodeQL restored
Owner mandate: coverage near-100% (hard floor 90%), real tests on every feature at
every layer, Scorecard at the repo's maximum, tech debt zero, full gates before
release. Plan: notes/plans/coverage-max-2026-07-21.md; gap analysis for the hub:
notes/fairyfox-reports/2026-07-21-testing-and-quality-gap-analysis.md.
- Test suite ~49 → ~200 tests; line coverage 44.2% → ~95.6% (branch 20.7% → ~78.6%),
now gated:
koverVerifyfails the build below 90% line coverage. New layers: full/despi+/recyclecommand matrix through real Brigadier dispatch, permission matrix (none/use/elevated/op/revoked), the whole despawn pipeline against mocked worlds (process, effect, all five strategies, event incl. cancellation), management (RemoveMaterialswalks), data backends (StorageFactory selection + aliases, YAML→SQLite migration incl. idempotence, pool exhaustion, transaction rollback, malformed rows), config (defaults, coercion, live reload,plugin.ymlvalidity), and high-load/DoS (10k flood, queue caps both drop policies, per-tick + max-concurrent budgets). - Three shipped bugs found by the new tests, fixed with regression pins (§87):
/despi purgenever purged containers (Void strategy short-circuited the removal chain → newsupportsRemovalflag); the recycle reward pool could pick non-item materials and crash the drop (→isItemfilter); entity purge-by-material never cleared storage-minecart stacks (→ amount-agnostic removal path). - Feature:
/despi recycle(same behaviour,recycle.use-gated) and renameable commands — newcommands:config section renames/despi//recycleand adds aliases (validated names, restart required). README + tutorials updated. - CodeQL SAST restored by pinning Kotlin 2.4.10 → 2.4.0 (patch-level; CodeQL 2.26.0, 2026-07-10, supports exactly up to 2.4.0): traced-compile analysis, dev pushes informational, the release PR run gates. Bump Kotlin only together with CodeQL's supported range.
- Scorecard/workflow hardening: top-level
permissions: contents: readon every workflow (docs/release writes moved to job scope), gradle wrapper-validation step in CI, stale detekt-baseline reference removed (no baseline exists — nothing parked). - Test harness for MockBukkit's three deviations (ray-traced targeting, async
chunks, container-state snapshot wipes):
TestSupport.kt+notes/reference/mockbukkit-harness.md. Quality bar codified in CLAUDE.md as a standing instruction (coverage floor, no parked findings, SAST-outlives-toolchain, full gates before release).
2026-07-21 — Release v1.3.7 (PATCH): badge wall + supply-chain/quality tooling parity with random-ai-prompt
Owner asked to match the sibling project random-ai-prompt's
README badge wall and security tooling, adapted from its JavaScript/Node stack to this
Kotlin/Gradle/Paper one. Plan: notes/plans/badges-and-security-2026-07-21.md.
- README badge wall expanded from 5 → 21 badges: contributors/stars/forks/watchers, last-commit/commits, version, Java 21 + Paper 1.21.x (replacing the sibling's Node badge), CI, Codecov coverage, CodeFactor, Sonar quality gate + tech debt, OpenSSF Scorecard, docs, Pages deploy (replacing the sibling's Netlify badge), open/closed issues, open/closed PRs, license. Plus a commented-out Hangar + Modrinth usage-badge block, ready to enable on publish (both support CI publishing).
- Security workflow (SHA-pinned):
scorecard.yml(OpenSSF Scorecard — weekly + branch-protection + push-to-main). Dependabot already covered gradle + actions. CodeQL deferred (tried, doesn't work yet): GitHub's CodeQL extractor (2.26.x) doesn't support this project's Kotlin (2.4.10) — compiled build modes die onKotlinInterceptor$KotlinVersionTooRecentError, andbuild-mode: noneextracts no Kotlin ("no source code seen"). A CodeQL gate would be permanently red, so it's removed rather than shipped broken. Kotlin SAST/quality is covered meanwhile by SonarQube Cloud + Detekt (both Kotlin-aware); revisit CodeQL when it supports Kotlin 2.4.x. - Coverage → Codecov:
ci.ymlnow runskoverXmlReportand uploads the Kover XML (build/reports/kover/report.xml, verified 156 KB) to Codecov (non-blocking, tokenless-ok). - SonarQube Cloud:
sonar-project.properties(Kotlin, scoped tosrc, Kover coverage import), documented for Automatic Analysis (default) or a CI scan (for in-Sonar coverage). - Naming fix:
SECURITY.mdno longer used the forbidden bareDespawnedItemsform (→ "PaperMC Despawned Items" /plugins/papermc-despawned-items/). - Docs subnav restructured to match
random-ai-prompt(owner-flagged gap): added an Overview pill, broke the single Notes door into Project Notes / Systems / Reference (with new generated section landing pages —renderDocsSiteemitsnotes/<section>/index.htmlfor each non-empty notes section), and added a Notes ↗ GitHub link beside Repository ↗. Both chrome surfaces updated (_shell.htmlhand pages +header.ftlDokka API pages); verified rendering on both + all links resolve. - Raised to the fairyfox hub:
notes/fairyfox-reports/2026-07-21-subnav-standard-proposal.mdproposes making the docs-site subnav standard adaptive (Overview + Project Notes + a door per notes section + Notes ↗) so mesh nodes stop diverging, with a reference implementation and a proposed conformance check.
Needs owner-side external setup for the account-gated badges to go live (Codecov app +
CODECOV_TOKEN; SonarCloud project import + SONAR_TOKEN; CodeFactor repo add; optional
SCORECARD_TOKEN). ./gradlew build + koverXmlReport green; doc-link gate + workflow
YAML validated. VERSION → 1.3.7.
2026-07-21 — Release v1.3.6 (PATCH): self-hosted legal pages + Legal subnav item
Fairy Fox caught the legal pages missing. Two findings: (1) the legal-docs standard is
mandatory for every repo ("every repo ships the three pages, self-hosted") — the
adoption manifest's "N/A — hub serves /legal/" was another wrong lenient read, and the
chrome footer's …/legal/{privacy,terms,cookies}/ links had been 404ing on the live
site since the chrome adoption; (2) the owner wants Legal reachable from the subnav.
- Three self-hosted, code-accurate legal pages (from
hub/templates/legal/, rewritten to reality): no accounts/analytics/cookies; reader-prefs + coins local storage disclosed as device-only with the in-app Reset / Clear-my-data controls; coins no-monetary-value + sharedfairyfox.io/legal/coins/explainer linked; GitHub Pages named as processor; Google Fonts IP exposure flagged; plugin-side reality stated (despawn-location records live in the operator's storage; no telemetry); Apache-2.0 "as is" terms;legal@fairyfox.iocontact; Last updated 2026-07-21. Emitted atlegal/<slug>/index.htmlso the footer's pretty URLs resolve, plus alegal/index page. - Legal subnav item added on every page (hand + API) — an owner-directed addition
to the canonical centre set, recorded in
decisions/architecture.md; sanctioned by legal-docs rule 4 (primary-menu placement). - Manifest legal-docs row corrected N/A → implemented; CLAUDE.md notes-maintenance table gains the legal-docs trigger (data-practice change → update pages + date).
VERSION → 1.3.6. Docs-site only.
2026-07-21 — Release v1.3.5 (PATCH): notes README pages rendered — live 404s fixed
Live-deploy verification of v1.3.4 (fetching the served pages, not trusting the build)
found the notes landing linking sessions/README.html / fairyfox-reports/README.html
— pages the renderer had excluded → 404 on the live site (a no-orphan/quality-bar
violation). Section READMEs are now rendered (links resolve) while staying
out of the sidebar (docs-site 06: the sidebar lists notes, not section overviews).
VERSION → 1.3.5. Docs-build only.
2026-07-20 — Release v1.3.4 (PATCH): docs site rebuilt to the canonical shape — overview landing, Notes/Tutorials/Changelog pages, API boundaried under /api/
Fairy Fox caught that v1.3.3's subnav "fix" had satisfied the letter by shrinking the subnav to what existed instead of building what the standard expects — and that the site's default page was the raw Dokka module page. Rebuilt as docs-site Case A (hand-authored site, generator boundaried):
- Overview landing is the default page (
index.html) — pitch, pipeline, features, section cards; the left subnav pill is now a real overview, not the API root. - Full canonical subnav everywhere:
[PaperMC Despawned Items] · Notes · Tutorials · Changelog · API · Download · [Repository ↗]— every centre link a real, chrome-wearing page; per-page.active+aria-current. - Notes rendered on the site (docs-site 06 shape): single Notes door, landing =
root-note intro, left sidebar listing every note grouped by section (READMEs
excluded), 44 pages, note pages carry
data-read(reading controls live), the landing does not. - Tutorials (getting started / commands / permissions / config,
data-read) and Changelog (generated at build fromnotes/version/*.md— cannot go stale) pages added. - API boundaried under
/api/: the Dokka tree moved off the site root, reached via the subnav API item (active on every generated page), way-back links to the overview (homepageLink+ subnav); generator sidebar/footer rules re-verified on the new output. - Fox brand icon vendored (
fox.pngat site root + api tree) — the last hot-link to fairyfox.io removed (self-hosted-assets gap #94 closed). - Build: new
renderDocsSite(flexmark md→HTML, shell + fragments) +assembleDocsSite→build/docs-site; docs.yml publishes that. Verification caught and fixed a malformed home-pill attribute before commit.
VERSION → 1.3.4. Docs/build/workflow only — no plugin behaviour change.
2026-07-20 — Release v1.3.3 (PATCH): full hub-standards audit — nav/subnav corrected, Downloads page, mandatory branch protection, provenance attestation
Ran the complete hub-standards audit (100+ items, notes/plans/standards-audit-2026-07-20.md)
and applied every gap under the standing adopt-standards-by-default grant:
- Docs chrome corrections (docs-site standard 05/08): primary nav now marks
Projects active (a sub-project is always under Projects — the earlier "Docs active"
was wrong); subnav canonicalized to
[project name] · API (active) · Download · [Repository ↗]— the invented "Guide" item and raw-GitHub centre links removed. - Real Downloads page (
downloads.html, required since we ship release jars): chrome-wearing, perma-latest section onreleases/latest, install notes, all-releases link; vendored into the docs root byvendorChromeAssets. - Release workflow hardened:
mainbranch-protected (solo config — require PR, 0 approvals, strict checks, enforce-admins),devdeletion/force-push protected, branch auto-delete on. Releases now go through the PR path; directdev → mainpushes are retired.release.ymlgains build-provenance attestation (supply-chain-hardening §4/§5). - Repo hygiene gates:
scripts/check-links.mjsdoc-link gate wired into CI;scripts/check-tidy.mjsfor session-end tidiness. - 17 hub standards adopted into
notes/reference/(agent-tooling, planning, process-reports, testing, repo-hygiene, compliance, …); 4 recorded N/A with reasons.
VERSION → 1.3.3. No plugin behaviour change — docs, CI/release workflow, and
repo/notes meta only.
2026-07-20 — Release v1.3.2 (PATCH): docs site wears the shared fairyfox chrome (bundle v2.2.1)
The Dokka API docs now carry the shared fairyfox chrome bundle (VERSION 2.2.1) instead of being a bare generator island — the masthead (Home · Projects · Farms · Docs · Updates · About), this project's subnav (overview · Guide · Changelog · Download · Repository), the reader "Aa"/coins controls, and the shared footer, on every page.
Injected through Dokka's FreeMarker templatesDir (the Doxygen-style "full-page generator"
adapter): docs-theme/dokka-templates/includes/{page_metadata,header,footer}.ftl paste the
bundle's head/header/subnav/footer verbatim with only the FF_* slots filled. The four
master assets (main.css, reader.js, nav.js, coins.js) are vendored under
docs-theme/chrome/ and copied into the docs root at build time (vendorChromeAssets),
referenced per-page via ${'$'}{pathToRoot} so they resolve at any depth — vendored, never
hot-linked, renders with fairyfox.io offline. "Wear the chrome, boundary the reference":
Dokka's API body stays Dokka, harmonised to the palette by dokka-fairyfox.css. Verified
in-browser at root + a deep package page; full build green. Deviations (footer in the
content column; Dokka's own bar kept as API controls) recorded in decisions/architecture.md.
VERSION → 1.3.2. No plugin behaviour/artifact change — docs-only.
2026-07-20 — Release v1.3.1 (PATCH): package namespace → io.fairyfox.papermc.despawneditems
Moved the Kotlin source out of com.popupmc.despawneditems into
io.fairyfox.papermc.despawneditems and set the Gradle group to io.fairyfox
to match. The plugin.yml main: now points at
io.fairyfox.papermc.despawneditems.PaperMcDespawnedItems. ktlint import ordering
re-sorted automatically (io.fairyfox sorts differently than com.popupmc did).
No behaviour, command, config, permission, artifact-name, plugin-id, or data-folder
change — for a server admin the drop-in jar is identical (papermc-despawned-items-*.jar,
plugin id papermc-despawned-items). This is purely the internal package/Maven namespace.
VERSION → 1.3.1. Full build green (ktlint + detekt + kover + tests).
2026-07-20 — Release v1.3.0: consistent naming + real static analysis & coverage
Addressed three follow-ups, none of which cut corners:
- Naming made consistent — the built artifact,
plugin.ymlname(Bukkit plugin id), and data folder are now allpapermc-despawned-items; the Kotlin main class isPaperMcDespawnedItems; the human-facing name stays "PaperMC Despawned Items". No more bareDespawnedItems. (Breaking for anyone who installed ≤1.2: renameplugins/DespawnedItems/→plugins/papermc-despawned-items/.) - Kover coverage now works — its plugin marker lives on Maven Central, not the Gradle
portal, so
pluginManagementnow includesmavenCentral()and Kover 0.9.9 (Kotlin-2.4 compatible) generates a coverage report.koverXmlReportgreen. - Detekt rules re-enabled and the code actually fixed — reverted the earlier mistake of
disabling
MagicNumber/ReturnCount/etc. All default rules are on with no baseline: every magic number extracted to a named constant,isHazardousrefactored from a complexity-48 boolean chain to an O(1)Set, empty overrides made explicit, the JDBC catch narrowed toSQLException, an unused parameter removed, and the few genuinely-branchy strategy methods carry a narrow@Suppress+ reason. The only config overrides are ktlint-aligned line length and documented guard-clause/facade thresholds.
VERSION → 1.3.0. Ktlint + Detekt + Kover all gate the build and CI.
2026-07-20 — Release v1.2.0 (MINOR): Brigadier commands + quality gates
Milestone since 1.1.1: /despi and /recycle rewritten on Paper's Brigadier command
API (typed args, permission-scoped visibility, native tab-completion; logic in testable
DespiActions/RecycleAction) — verified under MockBukkit (dispatch) and enabling on real
Paper 1.21.11. Added Ktlint and Detekt quality gates to the build (JDK-21 daemon
pin so detekt runs; tuned config + baseline). Minor /despi syntax tweaks noted below.
VERSION → 1.2.0.
2026-07-20 — Add Ktlint + Detekt quality gates; format the codebase
Adopted two static-quality tools and wired them into build (they run as part of
check):
- Ktlint (formatting + Kotlin style) — formatted the whole codebase to a consistent
style;
.editorconfigsets line length 140 and enables no-wildcard-imports. - Detekt (static analysis: complexity, smells, potential bugs) —
config/detekt/detekt.ymllayers on the defaults (line length 140,MagicNumber/ReturnCounttuned for the branchy despawn strategies); a 12-entry baseline (config/detekt/baseline.xml) accepts pre-existing findings so detekt gates only new issues.
Detekt 1.23.8 can't run on a JDK 25 daemon, so gradle/gradle-daemon-jvm.properties pins
the Gradle daemon to JDK 21 (foojay-provisioned) — which also makes the whole build
run on the plugin's real target JVM. Kover (coverage) was attempted but is currently
blocked (0.9.1 is Kotlin-2.4-incompatible; 0.9.8's plugin marker isn't on the portal) —
deferred until a working release. Build green with both gates + tests + jar.
2026-07-20 — Modernize commands to Paper's Brigadier API
Replaced the legacy plugin.yml command + hand-rolled string parsing with Paper's modern
Brigadier command API, registered via the lifecycle event system. /despi and
/recycle are now proper command trees with typed arguments, permission-scoped
visibility (.requires), native tab-completion, and client-side validation.
Structure: all behaviour lives in a testable DespiActions (and RecycleAction);
DespiCommand builds the tree and registers it; CommandFeedback holds shared helpers.
Removed the old framework (OnDespiCommand, AbstractDespiCommand, DespiSubcommandsA/B,
OnRecycleCommand) and the plugin.yml commands: block. MockBukkit supports the
lifecycle command API, so the enable test still passes and a new test dispatches /despi
end-to-end through the registration. Minor syntax tweaks: clear player <name> (was
clear <name>) and despawn create-material-amount <name> <amt> alongside
create-material <names> (Brigadier can't follow a greedy string with an int).
2026-07-20 — Release v1.1.1 (PATCH): regression pins + 26.1 forward-compat verified
Post-1.1.0 hardening release. Pinned regression tests for the fixed bugs (recycle reward,
particle data) by extracting RecycleProgress and ParticleData as pure units
(behaviour unchanged), and verified forward-compat by booting the jar on a real Paper
26.1.2 server (enables cleanly, no registry-change breakage). No user-facing behaviour
change. VERSION → 1.1.1.
2026-07-20 — Forward-compat verified on real Paper 26.1.2
Booted a headless Paper 26.1.2 server with the 1.1.0 jar (built against 1.21.11) and
confirmed the forward-compat claim: the api-version: '1.21' descriptor is accepted, the
libraries: (HikariCP + JDBC drivers) load, and onEnable completes cleanly —
DespawnedItems is enabled, Done (10.3s)!, no exceptions despite the 26.x registry
changes (Material iteration in RewardPool, Particle.valueOf in ParticleData, etc.
all work). Closes the last verification gap; the README's "loads on 26.x" is now proven.
2026-07-20 — Regression tests for the fixed bugs (§87): recycle reward + particle data
Post-1.1.0 hardening. Per the testing standard, every fixed bug now has a permanent,
pinned test. Extracted the two bug-prone pieces into pure, server-independent units and
tested them: RecycleProgress (the "reward every 64 recycles" threshold that used to
never fire) and ParticleData (resolves a particle + its required data safely, falling
back with a warning instead of crashing for DUST/BLOCK/ITEM/unknown keys). 12 new pure
tests. Also a small readability win — /recycle and FileConfig now delegate to these
units instead of inlining the logic.
2026-07-20 — Release v1.1.0 (MINOR): full refactor to a modern, tested, scalable plugin
Cut the v1.1.0 milestone from the 2026-07-20 refactor work below. Highlights since
1.0.2: retargeted to Paper 1.21.x / Java 21; fixed real bugs (/recycle rewards,
data-bearing particles, stale static strategy list, RemoveMaterials IOOBE, /despi reload); indexed LocationStore with O(1) lookups; pluggable YAML / SQLite /
MySQL storage with incremental off-thread persistence and migration; throttled
DespawnScheduler for large-server safety; per-user location limits via
despi.limit.<n> group permissions; a JUnit 5 + MockBukkit test suite (~49 tests, CI
green) plus a real-server smoke pass; rewritten README, filled context notes, and a
GitHub Pages Dokka workflow. VERSION → 1.1.0; release.yml/ci.yml on Java 21.
2026-07-20 — Runtime-verified on real Paper 1.21.11 (headless smoke)
Booted a headless Paper 1.21.11 server with the built jar and confirmed a clean
enable: Paper's library loader auto-downloaded and loaded HikariCP 5.1.0, sqlite-jdbc
3.49.1.0, and mariadb-java-client 3.5.3 from Maven Central, then
DespawnedItems is enabled with no stack traces (Done (23.16s)!). The server logged it
was "2 releases behind 26.1.2" — confirming the 1.21.11-vs-26.1 target. The smoke also
caught (and corrected) that VERSION is 1.0.2 (status.md had said 1.0.0) and that the
distributable jar is DespawnedItems-1.0.2.jar. Forward-compat load on an actual 26.1
server is still to verify.
2026-07-20 — Fill in the context notes (project/architecture/principles/overview)
Phase 4 docs. Filled the previously-templated notes/context/{project,architecture, principles}.md and notes/systems/overview.md to describe the current architecture
(the location store/manager/repository seams, the throttled pipeline, limits), the
repo layout, the data-integrity/scale/fail-safe principles, and the end-to-end system
map. Refreshed notes/status.md to reflect the largely-complete refactor.
2026-07-20 — GitHub Pages docs workflow; CI on Java 21
Phase 4. Added .github/workflows/docs.yml — builds the Dokka API docs and deploys them
to GitHub Pages on every release to main (SHA-pinned Pages actions; dokkaGenerate
verified locally, output build/dokka/html). Enabled Pages with the GitHub-Actions build
source. Also fixed ci.yml to use Java 21 (was still 25 after the retarget); the CI
build already runs the full test suite.
2026-07-20 — Rename display name to "PaperMC Despawned Items"; rewrite README
Phase 4. Set the human-facing/listed name to PaperMC Despawned Items in the README
title, Dokka moduleName, and CLAUDE.md heading. The repo slug
(papermc-despawned-items) and the Bukkit plugin id (DespawnedItems) are unchanged —
the latter can't contain spaces. Also set plugin.yml author to Fairy Fox. Rewrote the
README to cover the 1.21.x target (loads on 26.x), the storage backends, per-user limits,
performance/scaling, updated commands (dropped indexes), and the test suite.
2026-07-20 — Per-user location limits with permission/group tiers
Phase 3. Players self-manage their own despawn locations up to a cap. DespawnLimits
resolves the cap as: limits.unlimited / despi.limit.bypass → unlimited; else the
highest despi.limit.<n> permission the player holds (assign these to LuckPerms groups
for per-rank caps); else limits.default (config). /despi add this enforces it for
self-service adds, while admins adding for others (already behind despi.elevated) are
uncapped. New limits: config section, LimitSettings, and the despi.limit.bypass
permission node. Tests: settings parsing/boundary (3) + a full MockBukkit permission
matrix (6). 49 tests green.
2026-07-20 — Throttled despawn scheduler (large-server safety)
Phase 1 scaling, final core piece. The old model started a chunk-loading DespawnProcess
immediately for every despawning item with no upper bound — a flood of despawns could
storm the server. Added DespawnScheduler: despawning items are enqueued and a
once-per-tick drain starts at most performance.max-per-tick new relocations, never
exceeding performance.max-concurrent in flight, with the queue capped at
performance.max-queue and a configurable drop policy. New performance: config section
and PerformanceSettings. The ItemDespawnEvent listener now enqueues; /despi despawn count-ongoing reports active + queued. Tests: config parsing/boundary (3) + MockBukkit
enqueue/drain (2). 40 tests green.
2026-07-20 — Wire the new store + storage into the live plugin
Phase 1 integration. The plugin now actually uses the indexed store and pluggable
storage instead of the old flat list: a new LocationManager (on plugin.locations)
owns the LocationStore + configured LocationRepository, exposes all the read/mutate
operations the pipeline and commands need, and persists incrementally and off-thread
(a mutation marks its owner dirty and debounces a 5s async flush; disable//despi save
flush synchronously). StorageFactory builds the yaml/sqlite/mysql backend from config
and auto-migrates yaml → database on first switch. New storage: config section.
Rewired the despawn pipeline (DespawnProcess draws from the manager and uses an
instance-scoped strategy list — fixing the old static list that captured the first
plugin instance across reloads), decoupled DespawnEffect to take a plain Location,
and updated RemoveMaterials (also fixed a latent index-out-of-bounds when a target
owner had no locations). Updated every /despi subcommand to the manager; removed the
now-obsolete indexes subcommand. Deleted FileLocations, LocationEntry,
DespawnIndexes. Also fixed /despi reload to actually re-read config.yml from disk
(it reused Bukkit's cached config before). Added a MockBukkit LocationManager
integration test (add/remove/query + save-and-reload). 35 tests green; shaded jar builds.
2026-07-20 — Adopt the Kotlin-Paper testing checklist as the project standard
Added notes/plans/testing.md: adopts the comprehensive Kotlin-PaperMC testing checklist
as this project's testing standard, maps its 95 sections onto this plugin (applicable /
partial / N-A with status), sets the testing-pyramid targets and source organisation, and
records the tool stack. A living document the suite is grown against. Current suite: 31
tests across unit, serialization/fuzz, database (SQLite), performance, and MockBukkit.
2026-07-20 — Pluggable storage backends (YAML + SQLite + MySQL/MariaDB) + perf tests
Phase 1 scaling. Turned persistence into a LocationRepository interface with two
implementations: YamlLocationRepository (the flat userdata/<uuid>.yml files —
default, zero-config, unchanged on-disk format, now writing incrementally per owner
instead of rewriting every file on every change) and a dialect-agnostic
JdbcLocationRepository for SQLite (embedded) and MySQL/MariaDB (network-shared),
with transactional per-owner writes. JDBC drivers + HikariCP load at runtime via Paper's
libraries: loader, so the jar stays small.
Tests: YAML roundtrip/resilience/incremental (5), JDBC-over-SQLite CRUD/replace/persist (4), and a performance-guardrail suite that fails the build if the store's inserts, lookups, random draw, or bulk removal regress from O(1)/O(log n) at 100k+ entries (3). 31 tests green.
2026-07-20 — Indexed location data model (DespawnLocation + LocationStore)
Phase 1 scaling foundation. Added an immutable, server-independent DespawnLocation
value type (world name + block coords + owner; x;y;z;world serialisation kept
byte-compatible with the old on-disk format) and a LocationStore that replaces the
old flat ArrayList — the one that was copied and linearly scanned on every query
and rewritten to disk in full on every change. The store gives O(1) add/remove/contains,
a spatial index (block → owners), an owner index (owner → locations, for per-user
limits and bulk ops), an O(1) swap-remove flat bag for uniform random draw, and
dirty-owner tracking so persistence can write only what changed.
Both types are pure and fully unit-tested (roundtrip + fuzz for the value type; add/remove/index-integrity + a 5,000-op randomized integrity fuzz for the store). 19 tests green. Additive for now — wiring the store into the plugin comes next.
2026-07-20 — Stand up the test harness (JUnit 5 + MockBukkit), first tests green
Phase 2 foundation, brought online early so the refactor is tested as it lands. Added
JUnit 5 + MockBukkit (mockbukkit-v1.21:4.110.0) to the build; test uses the JUnit
platform and build runs it. First suite (PluginEnableTest) boots a mocked Paper
1.21 server, loads the plugin, and asserts it enables cleanly, both commands
register, and the reward pool is populated/filtered — all green.
Two changes were needed to make the plugin testable, both improvements in their own
right: DespawnedItems is now open (Kotlin classes are final; MockBukkit subclasses
the main class via ByteBuddy), and RewardPool filters by stable enum name instead of
calling server-backed Material.isAir() (which routed through the block registry —
fragile and slower; MockBukkit can't mock the legacy path at all).
2026-07-20 — Fix fragile particle handling (data-bearing particles crashed)
Phase 1 correctness fix. The effect always called spawnParticle with no data,
so any configured particle that requires data (DUST, BLOCK, ITEM, …) threw at spawn
time. Now the particle's data requirement is resolved and validated once at config
load: data-less particles pass null, DUST-style particles are built from new optional
particles.color (#RRGGBB) and particles.dust-size keys, and any particle needing
data the plugin doesn't model is swapped for the happy_villager default with a
console warning instead of crashing. Unknown particle keys warn and fall back too.
Build green on 1.21.11.
2026-07-20 — Fix silently-broken /recycle rewards; rename reward pool
Phase 1 (refactor) correctness fix. /recycle tracked its reward progress in three
scoreboard objectives (recycleCountPart/recycleCount/recycleCountPaid) that the
plugin never created, so on any normal server the reward path hit a null objective and
silently granted nothing. Rewrote it to store progress in the player's
PersistentDataContainer — persists across sessions, needs no scoreboard setup, and
actually works. Behaviour preserved: one random reward every 64 recycles.
Also renamed the misnamed BlacklistedItems object to RewardPool (it is the
allow-list of reward-eligible materials, not a blacklist) and tidied its filtering.
Build green against Paper 1.21.11.
2026-07-20 — Retarget Paper 26.1 → 1.21.x (Java 21) + refactor plan
Kicked off a full refactor/modernisation/testing/scale-up effort (plan:
notes/plans/refactor-2026-07.md). First step: retargeted the build from Paper
26.1 / Java 25 to Paper 1.21.11 / Java 21. Rationale — the MockBukkit test
framework supports the 1.21.x line but not the newer 26.x line yet, so 1.21.x
unlocks full integration/e2e testing; 1.21.11 is also the single most-installed
version (~25% of tracked public servers, ~47% for the whole 1.21.x line) and a
1.21-built plugin still loads on 26.1 servers via Paper forward-compat (to verify).
Bonus: Mineflayer supports 1.21.11, so in-game client automation is now possible.
Changes: build.gradle.kts Paper API → 1.21.11-R0.1-SNAPSHOT, Kotlin toolchain
and jvmTarget → 21; settings.gradle.kts adds the foojay resolver (1.0.0, the
Gradle-9-compatible release) to auto-provision JDK 21; plugin.yml api-version →
1.21; CLAUDE.md and notes/status.md updated. ./gradlew build is green against
the real 1.21.11 API — the existing code compiled with zero API changes.
2026-07-20 — Project icon
Added the project icon (assets/icon.png, 1024×1024, transparent) to the repo: shown
in the README header and wired as the Dokka docs header logo (via customAssets).
Released as v1.0.2. The GitHub social-preview image still needs a one-time manual
upload in repo Settings — gh/the API can't set it.
2026-07-19 — Make the Gradle wrapper executable for CI
Set the executable bit on gradlew so the Linux GitHub Actions runners can invoke it.
Committed from Windows, the wrapper landed non-executable and CI failed with
./gradlew: Permission denied (exit 126). Released as v1.0.1.
2026-07-19 — Kotlin/Paper 26.1 rewrite + fairyfox mesh onboarding
Modernised the plugin and folded it into the fairyfox mesh in one pass.
Rewrote the entire plugin from Java 11 / Maven / Paper 1.16.5 to Kotlin / Gradle /
Paper 26.1 on Java 25: all ~30 classes (the five-stage despawn pipeline, every
/despi subcommand, /recycle, config and per-player location storage) ported to
idiomatic Kotlin. Modernised churned APIs — ChatColor → Adventure components,
Effect/playEffect → a sound key via playSound(String, …), Particle.VILLAGER_HAPPY
→ a resolved particle key, dropped removed MaterialData calls, and switched
overloaded-setter properties to explicit setX(...) calls. Reconciled the plugin's
own config holder to plugin.settings to avoid clashing with JavaPlugin.getConfig(),
and reconciled plugin.yml permissions to match the code. The build shades the Kotlin
stdlib into a single runnable jar and is wired for Dokka docs and Hangar publishing.
Verified ./gradlew build green against the real Paper 26.1.2 API, and confirmed the
built jar loads and enables cleanly on a headless Paper 26.1.2 / Java 25 server.
Adopted the fairyfox standards: mesh-aware CLAUDE.md, the notes/ living-notes
system, VERSION seeded at 1.0.0, LF .gitattributes, SECURITY.md, Dependabot,
and SHA-pinned CI / branch-sync / release workflows, plus a reconciled README with the
badge block.