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.
  • processResources now reads gradle properties via providers.gradleProperty(...) — Loom 1.13 configures the task in a context where Project.property(String) throws "unknown property".
  • Yarn-mapping source drift (1.21.11+build.1), fixed against the actual mappings.tiny: RegistryByteBuf moved to net.minecraft.network (was imported from net.minecraft.registry); and the removed static Screen.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 build BUILD SUCCESSFUL in 1m40s in-container, producing papermc-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) + a fonts.css are now vendored into docs-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 Dokka page_metadata.ftl, and the chrome/head.html reference). fonts.css uses relative URLs (the hub master's absolute /assets/fonts/ would break on this Pages subpath), and both build.gradle.kts chrome-vendoring Copy tasks now ship the font files beside main.css. docs-theme/chrome/VERSION → 2.3.0. Verified: assembleDocsSite builds green (5m46s in the Docker container); the built site self-hosts fonts on both hand-authored and Dokka API pages with no fonts.googleapis/gstatic requests (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-level permissions: read-all is now documented in-file as a deliberate, write-free divergence from supply-chain §1's literal contents: 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 compiled VERIFY-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 + .dockerignore for a local-first Linux build/test loop (source bind-mounted, Gradle cache volume, host Docker socket mounted for docker-outside-of-docker with host.docker.internal pinned). Validated end-to-end: docker compose run --rm build./gradlew build BUILD SUCCESSFUL in 12m33s inside the container, and the previously host-skipped MariaDbStorageTest (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.jsonl release 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 settings
  • enable · 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 rebuilt
  • priority <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 with ffmpeg. 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 to viewer on failure rather than failing the build. Each frame records its backend in manifest.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.sh now accepts the latest-stable / latest-experimental tokens 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 to main'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 uuid transitive dep (via mineflayer) 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. Validated npm ci + mineflayer/uuid load.
  • New optional pre-ship stage: scripts/local-playtest.ps1 boots a local real Paper server in YAML storage mode (offline, under git-ignored run/) so the owner can connect their own Minecraft client to localhost and 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 from npm install …@4.37.1 to npm ci. A bare version pin did not satisfy Scorecard (it wants a lockfile); npm ci from a committed lockfile does → Pinned-Dependencies 9→10. Validated locally: npm ci installs 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.jsonl release 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.
  • main required status checks set (build, Analyze (java-kotlin), both Server smoke jobs, In-game client smoke) with strict up-to-date, via gh api — this platform-enforces the owner's full-CI-before-main rule (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 build was green) — the reason main deployments must clear the full CI suite.
  • Full-CI gate enforced (CLAUDE.md + status.md, owner instruction): no merge to main until 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.jsonl release 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.registering Kotlin-DSL usages (Gradle-10 warnings) → tasks.register(...). Remaining ReportingExtension.file deprecation 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 /recycle whose 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 to config.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, console give + /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.
  • copyBlockToLocation branches 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.propertiesapi.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): covers StorageFactory.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 in notes/reference/mockbukkit-harness.md; retest on TC upgrades.)
  • Headless real-server smoke in CI (scripts/server-smoke.sh, server-smoke matrix 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-smoke job): a Mineflayer bot — a real Minecraft client — joins offline-mode Paper 1.21.11, runs /recycle and /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 uploaded CODECOV_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 via api/components/show — the GitHub slug guess 1fairyfox didn'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-hit compileKotlin traces nothing and CodeQL dies with "could not process any code" (regression pin in the workflow comment).
  • codecov.yml added: partials_as_hits so 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: koverVerify fails the build below 90% line coverage. New layers: full /despi + /recycle command 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 (RemoveMaterials walks), data backends (StorageFactory selection + aliases, YAML→SQLite migration incl. idempotence, pool exhaustion, transaction rollback, malformed rows), config (defaults, coercion, live reload, plugin.yml validity), 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 purge never purged containers (Void strategy short-circuited the removal chain → new supportsRemoval flag); the recycle reward pool could pick non-item materials and crash the drop (→ isItem filter); entity purge-by-material never cleared storage-minecart stacks (→ amount-agnostic removal path).
  • Feature: /despi recycle (same behaviour, recycle.use-gated) and renameable commands — new commands: config section renames /despi / /recycle and 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: read on 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 on KotlinInterceptor$KotlinVersionTooRecentError, and build-mode: none extracts 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.yml now runs koverXmlReport and 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 to src, Kover coverage import), documented for Automatic Analysis (default) or a CI scan (for in-Sonar coverage).
  • Naming fix: SECURITY.md no longer used the forbidden bare DespawnedItems form (→ "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 — renderDocsSite emits notes/<section>/index.html for each non-empty notes section), and added a Notes ↗ GitHub link beside Repository ↗. Both chrome surfaces updated (_shell.html hand pages + header.ftl Dokka API pages); verified rendering on both + all links resolve.
  • Raised to the fairyfox hub: notes/fairyfox-reports/2026-07-21-subnav-standard-proposal.md proposes 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 + shared fairyfox.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.io contact; Last updated 2026-07-21. Emitted at legal/<slug>/index.html so the footer's pretty URLs resolve, plus a legal/ 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 from notes/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.png at 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) + assembleDocsSitebuild/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 on releases/latest, install notes, all-releases link; vendored into the docs root by vendorChromeAssets.
  • Release workflow hardened: main branch-protected (solo config — require PR, 0 approvals, strict checks, enforce-admins), dev deletion/force-push protected, branch auto-delete on. Releases now go through the PR path; direct dev → main pushes are retired. release.yml gains build-provenance attestation (supply-chain-hardening §4/§5).
  • Repo hygiene gates: scripts/check-links.mjs doc-link gate wired into CI; scripts/check-tidy.mjs for 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.yml name (Bukkit plugin id), and data folder are now all papermc-despawned-items; the Kotlin main class is PaperMcDespawnedItems; the human-facing name stays "PaperMC Despawned Items". No more bare DespawnedItems. (Breaking for anyone who installed ≤1.2: rename plugins/DespawnedItems/plugins/papermc-despawned-items/.)
  • Kover coverage now works — its plugin marker lives on Maven Central, not the Gradle portal, so pluginManagement now includes mavenCentral() and Kover 0.9.9 (Kotlin-2.4 compatible) generates a coverage report. koverXmlReport green.
  • 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, isHazardous refactored from a complexity-48 boolean chain to an O(1) Set, empty overrides made explicit, the JDBC catch narrowed to SQLException, 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; .editorconfig sets line length 140 and enables no-wildcard-imports.
  • Detekt (static analysis: complexity, smells, potential bugs) — config/detekt/detekt.yml layers on the defaults (line length 140, MagicNumber/ReturnCount tuned 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-version1.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.