diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 64fcae64cd..23bc2eb264 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -32,6 +32,9 @@ jobs:
- name: Build with Maven
run: mvn -B -f VotingPlugin/pom.xml package
+ - name: Build standalone vote core
+ run: mvn -B -f VotingPluginCore/pom.xml clean package
+
# Only submit dependency snapshot on push (not on pull_request)
- name: Submit Dependency Snapshot
if: github.event_name == 'push'
diff --git a/VotingPluginCore/.gitignore b/VotingPluginCore/.gitignore
new file mode 100644
index 0000000000..b83d22266a
--- /dev/null
+++ b/VotingPluginCore/.gitignore
@@ -0,0 +1 @@
+/target/
diff --git a/VotingPluginCore/pom.xml b/VotingPluginCore/pom.xml
new file mode 100644
index 0000000000..0b186b35ae
--- /dev/null
+++ b/VotingPluginCore/pom.xml
@@ -0,0 +1,73 @@
+
+ 4.0.0
+ com.bencodez
+ votingplugin-vote-core
+ 7.1.2-SNAPSHOT
+ jar
+ VotingPlugin Vote Core
+
+
+ 21
+ UTF-8
+
+
+
+
+ ${project.basedir}/../VotingPlugin/src/main/java
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.13.0
+
+ 21
+
+ com/bencodez/votingplugin/core/vote/**/*.java
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+ 3.5.4
+
+
+ default-test
+
+ **/VoteCoreArtifactTest.java
+
+
+
+ packaged-artifact-test
+ package
+ test
+
+ **/VoteCoreArtifactTest.java
+ ${project.build.directory}/packaged-artifact-reports
+
+ ${project.build.directory}/${project.build.finalName}.jar
+
+
+
+
+
+
+
+
+
+
+ org.junit.jupiter
+ junit-jupiter-api
+ 5.12.2
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ 5.12.2
+ test
+
+
+
diff --git a/VotingPluginCore/src/test/java/com/bencodez/votingplugin/core/vote/VoteCoreArtifactTest.java b/VotingPluginCore/src/test/java/com/bencodez/votingplugin/core/vote/VoteCoreArtifactTest.java
new file mode 100644
index 0000000000..48df7dda39
--- /dev/null
+++ b/VotingPluginCore/src/test/java/com/bencodez/votingplugin/core/vote/VoteCoreArtifactTest.java
@@ -0,0 +1,32 @@
+package com.bencodez.votingplugin.core.vote;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.net.URLClassLoader;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.jar.JarFile;
+
+import org.junit.jupiter.api.Test;
+
+class VoteCoreArtifactTest {
+ @Test
+ void packagedVoteCoreContainsOnlyStandaloneSharedClasses() throws Exception {
+ Path path = Path.of(System.getProperty("votingplugin.voteCoreJar"));
+ try (JarFile jar = new JarFile(path.toFile());
+ URLClassLoader loader = new URLClassLoader(new java.net.URL[] { path.toUri().toURL() }, null)) {
+ List entries = jar.stream().filter(entry -> !entry.isDirectory())
+ .map(entry -> entry.getName()).toList();
+ assertTrue(entries.contains("com/bencodez/votingplugin/core/vote/SharedVoteProcessor.class"));
+ assertTrue(entries.contains("com/bencodez/votingplugin/core/vote/SharedVotePolicy.class"));
+ assertFalse(entries.contains("plugin.yml"));
+ for (String entry : entries) {
+ if (entry.startsWith("META-INF/")) continue;
+ assertTrue(entry.startsWith("com/bencodez/votingplugin/core/vote/") && entry.endsWith(".class"),
+ () -> "Unexpected vote core JAR entry: " + entry);
+ Class.forName(entry.substring(0, entry.length() - 6).replace('/', '.'), false, loader);
+ }
+ }
+ }
+}
diff --git a/docs/platform-core.md b/docs/platform-core.md
new file mode 100644
index 0000000000..af13b76e48
--- /dev/null
+++ b/docs/platform-core.md
@@ -0,0 +1,27 @@
+# Standalone vote core
+
+`VotingPluginCore/pom.xml` builds a JDK 21-only JAR from the same
+`com.bencodez.votingplugin.core.vote` Java sources compiled into the existing
+`VotingPlugin` plugin. The sources stay in one place so the two artifacts cannot
+silently acquire different vote policies. The standalone artifact has no Bukkit,
+proxy, or AdvancedCore compile dependency.
+
+```sh
+mvn -B -f VotingPlugin/pom.xml clean package
+mvn -B -f VotingPluginCore/pom.xml clean package
+```
+
+The first command remains the Bukkit/proxy plugin build. The second produces
+`VotingPluginCore/target/votingplugin-vote-core-7.1.2-SNAPSHOT.jar`; its package
+test checks that only shared vote classes are present and that they load without
+platform libraries. CI builds both artifacts. Keep the version in both POMs
+aligned when changing the plugin version.
+
+This JAR is a library boundary for future platform adapters. It contains no
+Fabric or NeoForge entry point, vote ingress, storage adapter, or reward executor.
+The Bukkit listener still owns its existing vote and reward behavior. In
+particular, `SharedVoteProcessor` is not the production Bukkit path; its
+prepared-plan and keyed-delivery contracts must not be mistaken for guarantees
+of the existing reward system. Future platform work must preserve live reward
+configuration behavior and verify the native adapter against current code and
+tests before changing production ownership.