MCDiffuse - Architecture & Design


A Minecraft AI block-generation plugin running on Bukkit, Fabric, and NeoForge — Bukkit is the full-feature tier; the two vanilla loaders share one code tree and run the generation core (see the platform feature matrix below). The core design philosophy: commands produce neutral data (lists of actions); platform adapters execute that data. No command ever directly touches a Bukkit or Fabric API — machine-checked by ModuleSurfaceTest (neutral sources may not import platform packages).

Contents

Design Philosophy

Commands Return Data

Every command returns List<Action> — plain records describing intent. No command ever calls a Bukkit or Fabric API directly. Actions are the boundary between neutral logic and platform execution.

Narrow Module Interfaces

Each module exposes a small published surface — often 2-3 entry points, though a few are wider (the commands/core surface is ~16 classes, the widest). Internal files talk only to siblings; cross-module communication flows through that surface: a store, a preparer, or an interface. The exact allowlist is machine-checked by ModuleSurfaceTest, so the numbers here mirror an enforced table rather than aspiration.

State in Stores

All mutable per-player state lives in instance stores keyed by player UUID, owned by one GenerationState aggregate that each platform bootstrap constructs and threads explicitly (constructor or parameter — no static state, no service locator). Commands read state via the injected GenerationStateView; they never hold state themselves. Stores are main-thread confined: async producers (HTTP streaming sinks) hop to the server thread before mutating them. PreviewOverlayStore and SelectionStore are the deliberate synchronized exceptions: packet listeners touch overlays off-thread, and loader event threads record selection clicks directly. Module-internal per-player registries (render handles, open-UI maps) are stores too: private maps, narrow API, quit eviction, never public fields — machine-checked by ModuleSurfaceTest.noPublicStaticMutableCollections.

Platform Boundary by Package

Anything in bukkit.* is Bukkit-only. Everything else is neutral and compiles into the :core Gradle module, which Fabric/NeoForge depend on without Bukkit on the classpath — a Bukkit import in neutral code fails the :core compile. Caveat: sources still live in one physical tree (srcDir + exclude), so the IDE won't stop the import at edit time.

File Names = Control Flow

Reading the file names in a module top-to-bottom should describe what it does: ModelResponseListeners calls HistoryActionPreparer which reads HistoryStore and produces CommitWorldBlocksAction.

Inject, Don't Import

World access, player selection, generation state, and platform services are injected via GenerationWorldSnapshotProvider, GenerationStateView, and MCDiffuseCommandEnv. Commands never import a world class or a store directly — machine-checked by ModuleSurfaceTest.commandsDoNotImportStores.


System Context

At the highest level, MCDiffuse sits between the Minecraft player and a remote Python diffusion model server. The plugin receives a text prompt, captures the world snapshot, sends a request, and renders the streamed response as holographic previews and final world edits.

230Java source files
34neutral action types
26player commands
3platforms
(1 full, 2 core-tier)
553tests

Counts as of 2026-07-05 (src/main 191 + vanilla 9 + app 9 + fabric 2 + neoforge 1; @Test methods). These numbers rot — recount before quoting, or treat as order-of-magnitude.

flowchart LR
  Player["Player\n/mcdiffuse ..."] -->|"typed command"| Platform["Platform Bootstrap\nBukkit / Fabric / NeoForge"]
  Platform -->|"CommandContext"| Router["Command Router"]
  Router -->|"routes to"| Cmd["Command Impl"]
  Cmd -->|"Actions"| Adapter["Action Executor\nplatform adapter"]
  Adapter -->|"block edits"| World["Minecraft World"]
  Adapter -->|"HTTP POST"| ModelSrv["Model Server\nPython"]
  ModelSrv -->|"NDJSON stream"| Listeners["Model Response\nListeners"]
  Listeners -->|"Actions"| Adapter
        
drag to pan · scroll to zoom
Fig 1. End-to-end system context. The Action Executor is the only component that touches platform APIs.

Module Map

230 files collapse into 14 named modules. Each module has a designated public surface — the handful of files that other modules are allowed to import. Everything else is module-internal.

Module Key Files Neutral? Public Surface Internal Concerns
Platform Bootstrap MCDiffuse, Config, MCDiffuseFabric, MCDiffuseNeoForge, VanillaCommandEnv (shared) No MCDiffuseCommandEnv interface startup, config binding, lifecycle
Type Bridge abstraction/ (9 neutral types), BukkitAbstractionAdapter, VanillaAbstractionAdapter (shared Fabric+NeoForge) Types yes; adapters no MCDiffusePlayer, MCDiffuseWorld, MCDiffuseBlockData, SelectionRegion adapter logic, UUID derivation
Command Router CommandRouter, CommandRegistry, CommandDispatcher, CommandContext, CommandConstants, MCDiffuseCommandEnv, RuntimeSettings, ReloadCoordinator, ConfigSchema Yes CommandRouter, MCDiffuseCommandEnv, CommandContext, CommandConstants (+CommandAction/QueuePolicy), CommandInvocation, MCDiffuseCommand, RuntimeSettings, ReloadCoordinator, ConfigSchema, generation-arg records (enforced by ModuleSurfaceTest) dispatch table, queue policy, reload steps
Commands 26 files: TerrainGenerateCmd, ModelCmd, ShapeCmd, UndoCmd, RedoCmd, CommitCmd, SelectCmd, ... Yes Each command's execute(CommandContext) arg parsing, store reads, preparer calls
Action Catalog MCDiffuseAction base + 34 subclasses (action/) Yes All action types — plain data records with accessors only (purity machine-checked by ModuleSurfaceTest.actionsArePlainRecords) none
Action Executor BukkitActionAdapter, VanillaActionAdapter (Fabric + NeoForge) No executeAll(actions) per-action dispatch switch
Generation State GenerationState (bootstrap-owned aggregate), ContainerStore, HistoryStore, ModelResponseState, PreviewOverlayStore (+BlockKey), Holo3DObjectStates, SelectionStore, GenerationCommandQueue, GenerationContainer, GenerationSettings, GenerationRequestContext Yes GenerationState accessors containers(), histories(), modelResponseState(), previewOverlays(), holo3dObjects(), selections(), queue(); GenerationRequestOptions lives in generation/data history ring, overlay tracking
Generation Logic HistoryActionPreparer, HistoryDisplayPreparer, LocalFillPreparer, ModelResponseListeners, GenerationCommandQueue Yes HistoryActionPreparer, HistoryDisplayPreparer, ModelResponseListeners, LocalFillPreparer, HistoryPreviewRestorePreparer, GenerationCommandQueue action assembly, queue drain events
World Snapshot GenerationWorldSnapshotProvider (interface), BukkitGenerationWorldSnapshotProvider, VanillaGenerationWorldSnapshotProvider (Fabric + NeoForge) Interface yes; impls no GenerationWorldSnapshotProvider world-height lookup, block snapshot reads
Model Client StreamingModelServerClient (neutral), SharedModelServerClient (:app), thin sinks: ModelServerClient (Bukkit), VanillaStreamingModelServerClient (Fabric + NeoForge) Clients yes; sinks no stream(request, sink), requestFinalChunk(...), sendRequest(...) HTTP streaming, NDJSON parse, error classification
Holographic 3D holo3d/ (11 neutral) + bukkit/holo3d/ (5 Bukkit): Holo3DOperationPreparer, Holographic3DUI, Holo3DObjectState, Holo3DListeners Partial Holo3DOperationPreparer, PrepareHolo3DOperationAction cell layout, object state store (Holo3DObjectStates), armor stand packets, hit testing, render handle
Holographic 2D bukkit/holo2d/ (12 files): Holo2DUI, widget tree, render pipeline No Holo2DUI entry point widget tree, block-face display
Inventory UI bukkit/ui/ (6 files): VirtualUI, VirtualUICallback, VirtualUIListeners No VirtualUI click handler, slot routing
Platform Infrastructure bukkit/packet/ (8), bukkit/protocol/ (2), bukkit/render/ (5), bukkit/selection/ (3), bukkit/handler/ (10), bukkit/event/ (1), bukkit/generation/ (1), bukkit/modelmanager/ (5) No Called only by Action Executor and Display modules packet wrappers, render scheduler, state persistence, model file management
flowchart TD
  PB["Platform Bootstrap"] -->|"implements"| CR["Command Router"]
  CR -->|"routes"| C["Commands"]
  C -->|"reads"| GS["Generation State"]
  C -->|"calls"| GL["Generation Logic"]
  GL -->|"produces"| AC["Action Catalog"]
  C -->|"produces"| AC
  AC -->|"executed by"| AE["Action Executor"]
  AE -->|"world reads via"| WS["World Snapshot"]
  AE -->|"sends to"| MC["Model Client"]
  MC -->|"response via"| GL
  AE -->|"renders"| H3D["Holo 3D"]
  AE -->|"renders"| H2D["Holo 2D"]
  AE -->|"opens"| UI["Inventory UI"]
  H3D -->|"packets"| PI["Platform Infra"]
  H2D -->|"packets"| PI
  UI -->|"packets"| PI
        
drag to pan · scroll to zoom
Fig 2. Module dependency graph, drawn for the Bukkit full tier. Top half is neutral and shared by all three platforms; the bottom half (Holo 3D/2D, Inventory UI) is Bukkit-only. The vanilla loaders (Fabric/NeoForge, shared vanilla/ tree) reuse the entire neutral top half but their action executor (VanillaActionAdapter) implements only generation, selection, preview, and world edits — the Holo/UI branches are unsupported no-ops there (see the platform feature tiers table).

Command Dispatch Flow

Every player command follows the same path regardless of platform. The queue is the only branching point: if the model is already running for this player, the command waits; otherwise it runs immediately.

flowchart TD
  PE["Platform Event\nonCommand / LiteralArg"] -->|"raw args"| CR["CommandRouter\nparse CommandAction"]
  CR -->|"CommandContext\nplayer, world, args, provider"| CD["CommandDispatcher\nlook up registered command"]
  CD -->|"QueuePolicy START_MODEL"| GCQ["GenerationCommandQueue\nidle?"]
  CD -->|"QueuePolicy RUN_NOW"| CMD["Command.execute()"]
  GCQ -->|"yes"| CMD
  GCQ -->|"no, queue"| Q["Pending Queue\nper player"]
  Q -->|"model done"| CMD
  CMD -->|"Actions"| AE["Action Executor\nBukkitActionAdapter / VanillaActionAdapter"]
  AE -->|"switch on type"| Effects["World edits\nDisplay updates\nMessages\nUI opens"]
        
drag to pan · scroll to zoom
Fig 3. Command dispatch. CommandContext carries everything a command needs — player, args, world snapshot provider, and the injected GenerationStateView; queue interaction happens via each command’s declared QueuePolicy in the dispatcher.

Key design rule: CommandDispatcher is the only place that maps a CommandAction enum to a Command instance. Adding a new command means adding one entry in the dispatch table and one new file in commands/. Nothing else changes.

Queue policies - each command declares one:

PolicyBehaviourExample commands
RUN_NOWExecute immediately, bypass queueselect, undo, redo, info, reload
QUEUE_WHEN_BUSYRun now if idle, else enqueuefill, display, cursor
START_MODEL_REQUESTAlways send to model servergenerate, terrain generate, shape, style
COMPOSITE_GENERATEPrepare sub-commands then dispatchmodel (combines shape + generate)

Generation Request Flow

When a model command runs, it assembles a GenerationRequestContext from the current generation state and world snapshot, then sends it to the model server. Responses arrive as a streaming NDJSON sequence; each chunk becomes another list of actions.

flowchart LR
  MC["ModelCommand\nShapeCommand\nTerrainGenerateCmd"] -->|"read"| GCS["ContainerStore\ncurrent settings"]
  MC -->|"read"| WS["WorldSnapshotProvider\nblock data + height"]
  GCS -->|"assemble"| RC["GenerationRequestContext\nRequestObject"]
  WS -->|"assemble"| RC
  RC -->|"HTTP POST"| MSC["ModelServerClient\nstreaming"]
  MSC -->|"loading"| MRL_S["ModelResponseListeners\nonModelResponse()"]
  MSC -->|"chunk N"| MRL_C["ModelResponseListeners\nonModelResponse()"]
  MSC -->|"done"| MRL_D["ModelResponseListeners\nonModelResponse()"]
  MRL_S -->|"status action"| AE["Action Executor"]
  MRL_C -->|"RenderPreviewAction\nstore preview"| AE
  MRL_D -->|"CommitBlocksAction\nstore history"| AE
  AE -->|"Holo3D render"| Preview["Holographic Preview"]
  AE -->|"world writes"| World["Minecraft World\non commit"]
  AE -->|"update"| HS["HistoryStore\nPreviewOverlayStore"]
        
drag to pan · scroll to zoom
Fig 4. Generation request flow. ModelResponseListeners is the single file that translates raw model output into actions.

Streaming design: Each NDJSON chunk calls onModelResponse() independently. Progress steps produce preview actions; the final step (or any error) produces a terminal action. The caller never polls - it just executes each returned list immediately.

Generation State after a successful generation:

ContainerStoreupdated with final blocks
HistoryStorenew entry pushed (contains undo baseline)
PreviewOverlayStorecleared (preview replaced by world edit)

History & Undo/Redo Flow

History is a per-player stack of GenerationContainer snapshots. Undo/redo/commit are all stateless reads of that stack; HistoryActionPreparer translates a stack position into an action list.

flowchart LR
  UC["UndoCommand"] -->|"prepareUndo"| HAP["HistoryActionPreparer"]
  RC["RedoCommand"] -->|"prepareRedo"| HAP
  CC["CommitCommand"] -->|"prepareCommitSelected"| HAP
  HC["HideCommand"] -->|"prepareHideSelected"| HAP
  SC["ShowCommand"] -->|"prepareShowIndexed"| HAP
  HAP -->|"read"| HS["HistoryStore\nper-player stack"]
  HAP -->|"read"| WS["WorldSnapshotProvider\nbaseline snapshot"]
  HAP -->|"Actions"| AE["Action Executor"]
  AE -->|"RestoreWorldBlocksAction"| World["Minecraft World"]
  AE -->|"RenderGeneratedPreviewAction"| H3D["Holographic 3D Display"]
  AE -->|"ClearHistoryRegionDisplayAction"| H3D
        
drag to pan · scroll to zoom
Fig 5. History flow. Five commands share one preparer; no duplicated history logic.

History entry lifecycle:

EventStack change
Generation completesNew entry pushed (contains world baseline + generated blocks)
UndoPointer moves back; RestoreWorldBlocks action reverts world
RedoPointer moves forward; RenderGeneratedPreview action re-displays
CommitPointer entry committed to world; stack truncated above pointer
Hide/ShowDisplay toggled; world unchanged

Display Pipeline

MCDiffuse renders previews as floating holographic displays using Minecraft armor-stand entities (Holo3D) and block-face projections (Holo2D). The neutral half defines what to show; the Bukkit half decides how to show it via packets.

Holo3D Pipeline

Holo3D is split: 11 neutral files compute cell layout and manage object state; 5 Bukkit files send packets to the player.

flowchart TD
  A["PrepareHolo3DOperationAction\nneutral - describes op"] -->|"executed by"| AE["Action Executor"]
  AE --> H3DP["Holo3DOperationPreparer\nneutral - computes cell layout"]
  H3DP -->|"Holo3DOperationRequest"| H3DL["Holo3DListeners\nneutral - manages object state"]
  H3DL -->|"Holo3DObjectState"| H3DUI["Holographic3DUI\nBukkit - sends packets"]
  H3DUI -->|"spawn/move/destroy\narmor stands"| PL["PacketLayer\nProtocolLib"]
  PL --> PC["Player Client"]
        
drag to pan · scroll to zoom
Fig 6a. Holo3D render pipeline - neutral cell computation, Bukkit packet dispatch.

Neutral half: Holo3DOperationPreparer, Holo3DListeners, Holo3DObjectState, Holo3DCellKey, Holo3DPivot, Holographic3DDisplayTransform, Holo3DCommandSpec, Holo3DAction, Holo3DOperationRequest, Holo3DOperationSnapshot, Holo3DBrightnessOverride

Bukkit half: Holographic3DUI (entity management), Holographic3DHitTest (raycasting), Holographic3DRenderedCell (entity handle), Holo3DRenderExecutor (scheduler), Holographic3DRenderHandle (lifecycle)

Holo2D Pipeline

Holo2D renders 2D images as Minecraft block faces. All 12 files are Bukkit-only; no neutral split is needed since no other platform requires this subsystem.

flowchart TD
  A["Holo2D Action\nfrom Action Catalog"] -->|"executed by"| AE["Action Executor"]
  AE --> H2D["Holographic2DUI\nBukkit - widget host"]
  H2D -->|"layout"| W["Widget Tree\nholo2d/widget/"]
  W -->|"block-face frames"| RL["Render Layer\nbukkit/render/"]
  RL -->|"map art packets"| PL["PacketLayer\nProtocolLib"]
  PL --> PC["Player Client"]
        
drag to pan · scroll to zoom
Fig 6b. Holo2D pipeline - all Bukkit-specific.

Inventory UI

A Minecraft chest inventory repurposed as a GUI. Slot clicks re-enter the action loop, allowing menus to compose commands.

flowchart TD
  A["OpenMainMenuAction\nOpenModelManagerAction"] -->|"executed by"| AE["Action Executor"]
  AE --> VUI["VirtualUI\nBukkit - chest inventory"]
  VUI -->|"slot layout"| VCB["VirtualUICallback\nslot click handler"]
  VCB -->|"returns Actions"| AE
  VUI -->|"listens"| VUL["VirtualUIListeners\ninventory close / item drag"]
        
drag to pan · scroll to zoom
Fig 6c. Inventory UI - slot clicks re-enter the action loop.

Platform Boundary

The neutral/platform split is enforced structurally. The :core Gradle module holds all neutral sources (via srcDir("../src/main/java") + exclude("**/bukkit/**")); :bukkit, :fabric, and :neoforge depend on it, so a Bukkit import in neutral code is a compile error for every platform build. The remaining gap is physical: neutral and Bukkit sources share one directory tree, so the boundary is invisible in the IDE until Gradle compiles :core. Target state is moving neutral sources into core/src/main/java outright.

Platform feature tiers

All three platforms ride one command/action pipeline, but they are not feature-equal. Bukkit is the full tier; the two vanilla loaders (Fabric and NeoForge, one shared vanilla/ tree) implement the generation core and answer everything else with an explicit player-visible "not supported on this loader yet" notice — 13 action types route to unsupported arms in VanillaActionAdapter, and four (Save/LoadRuntimeStateAction, RestorePersistentDisplaysAction, FireGenerationQueuesDrainedEventAction) are deliberate no-ops. The unsupported arms are exhaustive sealed-switch cases, so a new action type fails the loader compile instead of silently skipping.

CapabilityBukkitFabric / NeoForge
Command dispatch + generation queue
Terrain / shape / style / condition generation
Selection tool + region store
Generated preview / commit / restore world
Local fill
Undo/redo history + saved-region displays✗ (no-op)
Holographic 2D / 3D displays
Litematic display
Model manager GUI / device switch
Inventory UI (main menu, cursor-correction GUI)✗ (chat fallback)
State persistence across restart✗ (session-only)

The concrete blocker for vanilla persistence is RuntimeStatePersistenceCodec living in bukkit/generation: its snapshot logic is mostly neutral, but the YAML binding is the Bukkit-specific part that has not been abstracted, so the loaders keep runtime state session-only.

flowchart LR
  subgraph Neutral ["Neutral - shared by all platforms"]
    direction TB
    Types["Type Bridge\nabstraction/"]
    Actions["Action Catalog\naction/"]
    Router["Command Router\ncommands/core/"]
    Cmds["Commands\ncommands/"]
    GenState["Generation State\ngeneration/store/"]
    GenLogic["Generation Logic\ngeneration/logic/"]
    Holo3DNeut["Holo3D neutral\nholo3d/ 11 files"]
  end
  subgraph BukkitPlat ["Bukkit"]
    direction TB
    BPlugin["Plugin Bootstrap\nMCDiffuse + Config"]
    BExec["Action Executor\nBukkitActionAdapter"]
    BWS["World Snapshot\nBukkitGenerationWorldSnapshotProvider"]
    BH3D["Holo3D Bukkit\n5 files"]
    BH2D["Holo 2D\n12 files"]
    BUI["Inventory UI\n6 files"]
    BInfra["Platform Infra\npacket + render + selection\nhandler + modelmanager"]
  end
  subgraph VanillaShared ["Vanilla shared - Fabric + NeoForge"]
    direction TB
    FExec["Action Executor\nVanillaActionAdapter"]
    FWS["World Snapshot\nVanillaGenerationWorldSnapshotProvider"]
    FSM["Model Client sink\nVanillaStreamingModelServerClient"]
    FBrig["Brigadier tree\nVanillaBrigadierCommands"]
  end
  subgraph FabricPlat ["Fabric bootstrap"]
    FPlugin["MCDiffuseFabric"]
  end
  subgraph NeoForgePlat ["NeoForge bootstrap"]
    NPlugin["MCDiffuseNeoForge"]
  end
  FabricPlat -->|"events + metadata"| VanillaShared
  NeoForgePlat -->|"events + metadata"| VanillaShared
  Neutral -->|"compile dep"| BukkitPlat
  Neutral -->|"compile dep"| VanillaShared
        
drag to pan · scroll to zoom
Fig 7. Platform boundary. Neutral code compiles without Bukkit or Fabric on the classpath. Platform code depends on neutral; neutral never imports from platform.

Execution path comparison: same command on two platforms

StepFileLayer
1. Player types /mcdiffuse undoMCDiffuse.onCommand()Bukkit
2. Parse + routeCommandRouter then CommandDispatcherNeutral
3. Execute commandUndoCommand.execute()Neutral
4. Build action listHistoryActionPreparer produces RestoreWorldBlocksActionNeutral
5. Execute actionsBukkitActionAdapter.executeAll()Bukkit
6. Apply world editWorld.setBlock() via Spigot APIBukkit
StepFileLayer
1. Player uses /mcdiffuse undo literalVanillaBrigadierCommands (Brigadier)Vanilla (Fabric/NeoForge)
2. Parse + routeCommandRouter then CommandDispatcherNeutral (same)
3. Execute commandUndoCommand.execute()Neutral (same)
4. Build action listHistoryActionPreparer produces RestoreWorldBlocksActionNeutral (same)
5. Execute actionsVanillaActionAdapter.executeAll()Vanilla (Fabric/NeoForge)
6. Apply world editlevel.setBlock() via official Mojang mappingsVanilla (Fabric/NeoForge)

Steps 2-4 are identical across all three platforms. Platform code is only ever at the edges: receiving player input (step 1) and executing actions against the world (steps 5-6). NeoForge shares the Fabric implementation via the loader-agnostic vanilla/ source tree (see Blind Spot #2).


Blind Spots & Design Debt

The philosophy above describes the intended shape. This section records where the implementation diverges from it, why that matters for maintainability, and what the fix looks like. Verified against source on 2026-07-05.

1. Action parity is asymmetrically enforced

MCDiffuseAction is a sealed interface. BukkitActionAdapter dispatches with an exhaustive switch, so adding an action without a Bukkit handler is a compile error. FabricActionAdapter dispatches with an if instanceof chain and a fallthrough that string-matches class names (className.contains("Holo"), "History", "Display", ...) to decide whether to log at FINE or WARNING — then skips either way. Consequences:

Fix: exhaustive switch in every adapter (the sealed interface already pays for it). Unsupported actions get explicit case arms that produce a player-visible "not supported on Fabric" message, and the support matrix becomes greppable code instead of string heuristics.

Status: fixed 2026-07-05. The loader adapter (then FabricActionAdapter, since unified into the shared VanillaActionAdapter serving both Fabric and NeoForge — see blind spot 2) now dispatches via exhaustive sealed switch — a new action type without a loader arm is a compile error. Player-initiated unsupported actions (7) send a yellow "not supported on this loader yet" chat notice; downstream Holo3D plumbing actions (6) log at FINE only, since their entry point already messaged the player. Feature coverage is still 21/34 implemented — the parity enforcement is fixed, the missing loader features are separate work (now enumerated in the platform feature-tiers table under Platform Boundary). 2026-07-21 (P11): that adapter also stopped being a static-singleton utility and became a bootstrap-owned instance.

2. Two command architectures coexist

Bukkit and Fabric enter through CommandRouterCommand.execute() → actions. NeoForge bypasses all of it: MCDiffuseNeoForge builds an MCDiffuseApp facade (:app) exposing executeCommand() / executeSetModelDevice() returning CompletableFuture<MCDiffuseCommandResult> — a second, parallel dispatch system with its own result type. "3 platforms" in the KPI overstates parity: only two ride the action pipeline. Every new command now poses the question "which system?", and the facade will accrete duplicates of router features (queueing, arg parsing) one at a time.

Fix: pick a target. Either NeoForge grows a NeoForgeActionAdapter and joins the pipeline, or the facade is documented as a deliberately minimal tier with a frozen command surface. The worst outcome is both systems growing indefinitely.

Status: fixed 2026-07-05. One architecture remains. Key insight: Fabric and NeoForge both use official Mojang mappings, and Fabric's implementation was already ~90% loader-agnostic (zero net.fabricmc imports in the adapter, renderer, snapshot provider, selection state, and streaming sink). Those files moved to a shared vanilla/ source tree (me.kokecacao.mcdiffuse.vanilla, classes Vanilla*) compiled into both loader modules via srcDir — the same sharing mechanism :core uses. Each loader keeps only a thin bootstrap (~1 file): mod init, metadata lookup, event-bus registration (Fabric callbacks vs NeoForge @SubscribeEvent), selection-tool interaction hook. NeoForge now has full Fabric-parity gameplay through CommandRouterVanillaBrigadierCommandsVanillaActionAdapter. The MCDiffuseApp facade and its family (MCDiffuseCommandResult, MCDiffuseAppConfig, MCDiffusePlatform, both platform schedulers, ModelServerDisplayFormatter) were deleted as dead code. ModuleSurfaceTest.vanillaTreeStaysLoaderAgnostic locks the new boundary: any net.fabricmc/net.neoforged import in the shared tree fails the build. 2026-07-06 follow-up (P4): the last duplicated bootstrap logic — the ~30-line selection-tool right-click handler and the 17-line resolveSelectionToolItem, byte-identical in both bootstraps — moved to VanillaSelectionToolHandler in the shared tree; each loader now keeps only its event plumbing (Fabric returns InteractionResult, NeoForge cancels the event).

3. Model client is implemented three times

HTTP + NDJSON streaming exists as ModelServerClient (Bukkit, 409 lines), FabricStreamingModelServerClient (230 lines), and SharedModelServerClient (:app, 164 lines). java.net.http is platform-neutral — nothing about streaming NDJSON requires Bukkit or Fabric. The philosophy constrains what neutral code may touch, but says nothing about what platform code may host, so neutral logic leaked into platform modules where it now drifts (retry policy, error parsing) in triplicate.

Fix: add the missing dual of the boundary rule — platform modules contain only code that needs platform APIs — and collapse the three clients into one neutral streaming client injected with a platform scheduler for thread-hopping.

Status: fixed 2026-07-05. Neutral StreamingModelServerClient (in modelserver/, compiled into :core) now owns the HTTP + NDJSON loop, transport-failure classification, and the error-category constants; platform events alias those constants. Platform classes became thin sinks with unchanged public APIs: Bukkit's ModelServerClient wraps chunks into ModelResponseEvent and delegates JSON endpoints to SharedModelServerClient; Fabric's client converts blocks against its registry and hops via server.execute. Design rule adopted: the neutral client never hops threads — each sink performs its own platform hop. Registry-validating block conversion stays platform-side by necessity (Bukkit.createBlockData / Fabric block registry). Covered by StreamingModelServerClientTest against a local HTTP server. 2026-07-05 follow-up: the vanilla sink originally called ModelResponseListeners.onModelResponse on HTTP client threads, racing main-thread store reads (the neutral stores are unsynchronized by design); the sink now hops to the server thread before the listener call, mirroring Bukkit's scheduler-first order — thread confinement is documented as store precondition.

4. Store lifecycle is unmanaged

"State in Stores" is implemented as static HashMaps with static methods (GenerationContainerStore, GenerationContainerHistoryStore). Three costs the philosophy never mentions:

Fix: per-player eviction hooks on quit (bounded ring per player at minimum), and long-term, instance stores owned by the bootstrap and passed through CommandContext — the injection plumbing already exists.

Status: fixed 2026-07-05 (revised diagnosis). Quit-eviction of history was rejected on purpose: history is designed to survive sessions — undo works after rejoin, and RuntimeStatePersistenceCodec persists containers/history/displays across restarts (Holo3D state is likewise parked, not evicted, on quit). Evicting on quit would silently break that contract. Instead memory is bounded at the source: GenerationContainerHistory.push() now returns dropped container ids (overwritten redo branches were the real leak — dropped ids previously left orphaned block volumes in GenerationContainerStore forever, which had no remove API), the history store evicts unreferenced dropped containers, and a depth cap (HISTORY_MAX_UNDO_DEPTH = 16, baseline never dropped) bounds each region stack. Session-only state is evicted on quit: PreviewBlockOverlayStore.clearPlayer() existed but had zero callers; the quit handler now calls it. Test-isolation and instancing costs remain open. 2026-07-05 P2 follow-up: shadow stores repaired — ModelResponseListeners’ three hidden static maps extracted into ModelResponseStateStore (the listener is now the stateless translator the doc always claimed), Holo3DOperationPreparer’s object registry extracted into Holo3DObjectStateStore (fixing the false claim that Holo3DListeners manages state), VirtualUI’s public static map encapsulated behind register/get/remove, debug test-render handles now evicted on quit, and the remaining module-internal registries were blessed with explicit store-role comments and eviction paths.

P3 stage 1 (2026-07-05): committed long-term direction — one bootstrap-owned runtime aggregate with instance stores behind interfaces, reached in shippable stages. Stage 1 done: commands now read generation state only through the injected GenerationStateView (CommandContext.stores(), store-backed default impl StoreBackedGenerationStateView); the history store’s nested result records moved to generation/data as top-level records, misplaced pure helpers moved out (CommandArgs.parseHistoryIndex, DisplayText); zero generation/store imports remain under commands/ (arch-test enforced); command tests can inject mock views with no store setup or teardown (GenerationStateViewSeamTest). Stage 2: instance stores behind unchanged interfaces. Stage 3: aggregate into listeners/preparers.

P3 stage 2 (2026-07-06): store state is now instance state. New instance classes ContainerStore, HistoryStore (constructor-wired to its container store so eviction frees volumes in the same runtime), ModelResponseState, PreviewOverlayStore, Holo3DObjectStates are owned by a new GenerationState aggregate. The old static store classes remain as thin facades delegating to GenerationState.DEFAULT so the ~60 listener/preparer/adapter/persistence call sites stay put until stage 3, which moves aggregate ownership into the platform bootstraps and deletes the facades. Payoff now: isolated fixtures — new GenerationState() per test, zero clear() discipline (GenerationStateIsolationTest proves container, history-eviction wiring, model-response, and Holo3D isolation).

P3 stage 3 (2026-07-06) — done; item closed. Static generation state is gone. Each platform bootstrap owns one aggregate: MCDiffuse (Bukkit) holds a generationState field (lazy accessor for CALLS_REAL_METHODS partial mocks), MCDiffuseFabric and MCDiffuseNeoForge each construct one and wire it into CommandRouter, ModelResponseListeners, Holo3DListeners, the streaming sink, and VanillaActionAdapter.init. GenerationCommandQueue became an instance owned by the aggregate (state.queue()); preparers take a leading GenerationState parameter; Bukkit handlers reach state via plugin.generationState(); SelectionListeners takes an injected HistoryStore; RuntimeStatePersistenceCodec and Holo3DRenderExecutor.flushPlayerState take explicit state parameters. The five facades, GenerationState.DEFAULT, and StoreBackedGenerationStateView.INSTANCE were deleted; BlockKey moved to its own top-level record in generation/store. Tests seed per-fixture aggregates (mock plugins stub generationState()). Remaining honest caveat: Holo3DRenderExecutor's live render-handle registry and SelectionListeners' cursor registry are still static module-internal stores at the Bukkit edge — session-only packet state, quit-evicted, out of this item's scope.

P5 (2026-07-21): selection state unified. Player selection was implemented twice — Bukkit's SelectionListeners held a static playerCursors map with independent left/right anchors, the loaders' VanillaSelectionState held two static ConcurrentHashMaps with a sequential two-click state machine. Both policies now live in one neutral synchronized SelectionStore owned by the aggregate (state.selections()): updateAnchors (Bukkit independent-anchor pairing), recordSequentialClick (loader sequential clicks; a pending first corner keeps the last complete region readable), setRegion, and currentRegion (absorbs stale-cursor sanitization). Platforms keep only plumbing: event extraction, ray tracing, chat/hints, and fake-display rendering — Bukkit display ids moved off PlayerCursor (now a pure two-anchor value) into a listener-instance CursorDisplayState map, evicted on quit while anchors survive for rejoin restore. VanillaSelectionState is deleted, both GenerationWorldSnapshotProvider impls lost their static INSTANCE and read the store, and the stage-3 caveat about the static cursor registry is closed. Deliberate behavior change: a cross-dimension second click on loaders now restarts the first corner with a chat notice instead of throwing inside the event handler.

P6 (2026-07-21): config schema unified. Shared config was defined twice — bukkit/Config (YAML, validated) and VanillaConfig (JSON via Gson, zero validation) — with live drift: serverPort 8123 vs 8765, maxGenerationSize 64 vs 16, real checkpoint fixture paths vs placeholder strings, selection tool minecraft:string vs minecraft:blaze_rod. One neutral ConfigSchema (commands/core, on the enforced surface) now owns the 8 shared keys, defaults, and bounds; Bukkit keeps only YAML extraction and the ItemStack edge, loaders initialize fields from schema defaults and gained a fail-loud validate() at mod init (invalid config.json throws instead of silently running with garbage). Defaults resolved to the Bukkit values; existing config files keep their explicit values. Bukkit-only keys (ray tracing, render budgets, persistence flags) stay platform-local by design.

P9 (2026-07-21): nullable-plugin dispatch retired. BukkitActionAdapter had a plugin-less execute/executeAll overload that forwarded null, forcing 23 switch arms to re-assert requirePlugin(plugin, action) at runtime and pushing display executors into static lookups. The null overloads and every requirePlugin guard are gone; execute null-checks the plugin once at the switch entry, so a missing wiring fails loud there instead of arm by arm, and a new plugin-backed action can no longer forget the guard. The four event handlers that still dispatched plugin-less (SelectionBukkitHandler, Holographic2DBukkitHandler, VirtualUIBukkitHandler, ReloadBukkitHandler) now take MCDiffuse at construction (all are built in MCDiffuse.onEnable) and thread it through.

P11 (2026-07-21): vanilla loader edge symmetric with Bukkit. VanillaActionAdapter was a utility class holding five static volatile singletons (server, env, model client, selection tool, generation state) wired by a static init() — a hidden init-order contract that made two servers or side-by-side tests share one adapter. It is now a bootstrap-owned instance built with its mod-scope deps, with setServer attaching/detaching the server like the snapshot provider and streaming client; the streaming sink and Brigadier dispatch reach it through a small VanillaActionExecutor interface wired once after construction. The whole vanilla service graph moved into one VanillaBootstrap in the shared tree (attachServer/detachServer fold the three per-start setServer calls), so Fabric and NeoForge keep only loader-specific metadata lookup and event plumbing and a new service is wired once instead of twice.

5. Conventions are prose, not checks

"Narrow module interfaces", "internal files talk only to siblings", and "file names = control flow" are enforced by nothing. The module table's public surface column is aspiration; any file can import any other file inside the source tree and no build step objects. Conventions that survive only by review attention decay at exactly the rate reviewers get busy.

Fix: encode the module table as an architecture test (ArchUnit or a simple import-grep test in CI) that fails when a module is imported outside its declared surface. The table in this document then becomes generated-from or verified-against code instead of hand-maintained.

Status: fixed 2026-07-05. Drift was fixed in code first, then locked: ShowHistoryRegionDisplayAction/ClearHistoryRegionDisplayAction constructors no longer call GenerationContainerHistoryStore.validateHistoryOwner — ownership checks live in the preparers that construct them (HistoryDisplayPreparer validates explicit history args; HistoryActionPreparer only resolves from the player's own stack) — so the "plain data records" claim is true again. New ModuleSurfaceTest (same source-walking pattern as the existing raw-volume-array boundary test) enforces four rules: command implementations importable only by the dispatch table; action catalog may not import behavior modules; cross-module imports must hit each restricted module's declared surface (the table above now mirrors the enforced allowlist); neutral sources may not import platform APIs. Verified by mutation: each forbidden-import injection fails the corresponding check. Still unchecked: bukkit/* internal layering.

6. This document itself

Hand-written counts (file totals, action counts, test counts) and hand-drawn module tables drift from source — several KPIs here were stale until the 2026-07-05 audit, and the platform-boundary section described an already-completed migration as future work. Prefer describing invariants (which are stable) over inventories (which rot), and date-stamp anything counted.

P7 (2026-07-21): platform parity honesty. The lead and the "3 platforms" KPI read as feature parity, but the two vanilla loaders are a lower tier: 13 action types are player-visible "not supported" arms and four are deliberate no-ops (persistence, drain event, display restore). A platform feature-tiers table now sits in the Platform Boundary section stating exactly what each tier does, the lead and KPI are annotated (1 full, 2 core-tier), Fig 2's caption marks the Holo/UI branches as Bukkit-only, and RuntimeStatePersistenceCodec in bukkit/generation is named as the concrete blocker for vanilla persistence (neutral snapshot logic, Bukkit-only YAML binding).

P8 (2026-07-21): drift smalls swept. Source-file KPI 212→230 and intro "180 files" → 230 (module count still 14); test KPI 526→553; the philosophy card's "at most 2-3 entry points" now admits the widest surface is the ~16-class commands/core and cites the enforcing ModuleSurfaceTest; the philosophy lead's "no command touches a Bukkit/Fabric API" cites the same machine check; Fig 3's stale FabricActionAdapter node is now VanillaActionAdapter. Counts re-derived from the working tree on this date.