implements packer PR-01 bootstrap

This commit is contained in:
bQUARKz 2026-03-11 17:34:26 +00:00
parent 839c7d244f
commit a8fa659762
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
38 changed files with 600 additions and 0 deletions

View File

@ -0,0 +1,7 @@
plugins {
id("gradle.java-library-conventions")
}
dependencies {
implementation(project(":prometeu-infra"))
}

View File

@ -0,0 +1,7 @@
package p.packer.api;
public enum PackerOperationClass {
READ_ONLY,
REGISTRY_MUTATION,
WORKSPACE_MUTATION
}

View File

@ -0,0 +1,7 @@
package p.packer.api;
public enum PackerOperationStatus {
SUCCESS,
PARTIAL,
FAILED
}

View File

@ -0,0 +1,17 @@
package p.packer.api;
import java.nio.file.Path;
import java.util.Objects;
public record PackerProjectContext(
String projectId,
Path rootPath) {
public PackerProjectContext {
projectId = Objects.requireNonNull(projectId, "projectId").trim();
rootPath = Objects.requireNonNull(rootPath, "rootPath").toAbsolutePath().normalize();
if (projectId.isBlank()) {
throw new IllegalArgumentException("projectId must not be blank");
}
}
}

View File

@ -0,0 +1,24 @@
package p.packer.api.assets;
import p.packer.api.diagnostics.PackerDiagnostic;
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
import java.util.Objects;
public record PackerAssetDetails(
PackerAssetSummary summary,
String outputFormat,
String outputCodec,
Map<String, List<Path>> inputsByRole,
List<PackerDiagnostic> diagnostics) {
public PackerAssetDetails {
Objects.requireNonNull(summary, "summary");
outputFormat = Objects.requireNonNullElse(outputFormat, "unknown").trim();
outputCodec = Objects.requireNonNullElse(outputCodec, "unknown").trim();
inputsByRole = Map.copyOf(Objects.requireNonNull(inputsByRole, "inputsByRole"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
}
}

View File

@ -0,0 +1,20 @@
package p.packer.api.assets;
import java.nio.file.Path;
import java.util.Objects;
public record PackerAssetIdentity(
Integer assetId,
String assetUuid,
String assetName,
Path assetRoot) {
public PackerAssetIdentity {
assetUuid = assetUuid == null ? null : assetUuid.trim();
assetName = Objects.requireNonNull(assetName, "assetName").trim();
assetRoot = Objects.requireNonNull(assetRoot, "assetRoot").toAbsolutePath().normalize();
if (assetName.isBlank()) {
throw new IllegalArgumentException("assetName must not be blank");
}
}
}

View File

@ -0,0 +1,7 @@
package p.packer.api.assets;
public enum PackerAssetState {
MANAGED,
ORPHAN,
INVALID
}

View File

@ -0,0 +1,20 @@
package p.packer.api.assets;
import java.util.Objects;
public record PackerAssetSummary(
PackerAssetIdentity identity,
PackerAssetState state,
String assetFamily,
boolean preloadEnabled,
boolean hasDiagnostics) {
public PackerAssetSummary {
Objects.requireNonNull(identity, "identity");
Objects.requireNonNull(state, "state");
assetFamily = Objects.requireNonNullElse(assetFamily, "unknown").trim();
if (assetFamily.isBlank()) {
assetFamily = "unknown";
}
}
}

View File

@ -0,0 +1,22 @@
package p.packer.api.diagnostics;
import java.nio.file.Path;
import java.util.Objects;
public record PackerDiagnostic(
PackerDiagnosticSeverity severity,
PackerDiagnosticCategory category,
String message,
Path evidencePath,
boolean blocking) {
public PackerDiagnostic {
Objects.requireNonNull(severity, "severity");
Objects.requireNonNull(category, "category");
message = Objects.requireNonNull(message, "message").trim();
evidencePath = evidencePath == null ? null : evidencePath.toAbsolutePath().normalize();
if (message.isBlank()) {
throw new IllegalArgumentException("message must not be blank");
}
}
}

View File

@ -0,0 +1,9 @@
package p.packer.api.diagnostics;
public enum PackerDiagnosticCategory {
STRUCTURAL,
HYGIENE,
VERSIONING,
MIGRATION,
BUILD
}

View File

@ -0,0 +1,7 @@
package p.packer.api.diagnostics;
public enum PackerDiagnosticSeverity {
INFO,
WARNING,
ERROR
}

View File

@ -0,0 +1,6 @@
package p.packer.api.doctor;
public enum PackerDoctorMode {
MANAGED_WORLD,
EXPANDED_WORKSPACE
}

View File

@ -0,0 +1,16 @@
package p.packer.api.doctor;
import p.packer.api.PackerProjectContext;
import java.util.Objects;
public record PackerDoctorRequest(
PackerProjectContext project,
PackerDoctorMode mode,
boolean includeSafeFixes) {
public PackerDoctorRequest {
Objects.requireNonNull(project, "project");
Objects.requireNonNull(mode, "mode");
}
}

View File

@ -0,0 +1,24 @@
package p.packer.api.doctor;
import p.packer.api.PackerOperationStatus;
import p.packer.api.diagnostics.PackerDiagnostic;
import java.util.List;
import java.util.Objects;
public record PackerDoctorResult(
PackerOperationStatus status,
String summary,
List<PackerDiagnostic> diagnostics,
List<String> safeFixes) {
public PackerDoctorResult {
Objects.requireNonNull(status, "status");
summary = Objects.requireNonNull(summary, "summary").trim();
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
safeFixes = List.copyOf(Objects.requireNonNull(safeFixes, "safeFixes"));
if (summary.isBlank()) {
throw new IllegalArgumentException("summary must not be blank");
}
}
}

View File

@ -0,0 +1,9 @@
package p.packer.api.doctor;
import p.packer.api.PackerOperationClass;
public interface PackerDoctorService {
PackerOperationClass operationClass();
PackerDoctorResult doctor(PackerDoctorRequest request);
}

View File

@ -0,0 +1,31 @@
package p.packer.api.events;
import java.time.Instant;
import java.util.List;
import java.util.Objects;
public record PackerEvent(
String projectId,
String operationId,
long sequence,
PackerEventKind kind,
Instant timestamp,
String summary,
PackerProgress progress,
List<String> affectedAssets) {
public PackerEvent {
projectId = Objects.requireNonNull(projectId, "projectId").trim();
operationId = Objects.requireNonNull(operationId, "operationId").trim();
Objects.requireNonNull(kind, "kind");
timestamp = Objects.requireNonNull(timestamp, "timestamp");
summary = Objects.requireNonNull(summary, "summary").trim();
affectedAssets = List.copyOf(Objects.requireNonNull(affectedAssets, "affectedAssets"));
if (projectId.isBlank() || operationId.isBlank() || summary.isBlank()) {
throw new IllegalArgumentException("projectId, operationId, and summary must not be blank");
}
if (sequence < 0L) {
throw new IllegalArgumentException("sequence must not be negative");
}
}
}

View File

@ -0,0 +1,15 @@
package p.packer.api.events;
public enum PackerEventKind {
ASSET_DISCOVERED,
ASSET_CHANGED,
DIAGNOSTICS_UPDATED,
BUILD_STARTED,
BUILD_FINISHED,
CACHE_HIT,
CACHE_MISS,
PREVIEW_READY,
ACTION_APPLIED,
ACTION_FAILED,
PROGRESS_UPDATED
}

View File

@ -0,0 +1,11 @@
package p.packer.api.events;
@FunctionalInterface
public interface PackerEventSink {
void publish(PackerEvent event);
static PackerEventSink noop() {
return ignored -> {
};
}
}

View File

@ -0,0 +1,12 @@
package p.packer.api.events;
public record PackerProgress(
double value,
boolean indeterminate) {
public PackerProgress {
if (!indeterminate && (value < 0.0 || value > 1.0)) {
throw new IllegalArgumentException("value must be between 0.0 and 1.0 when determinate");
}
}
}

View File

@ -0,0 +1,35 @@
package p.packer.api.mutations;
import p.packer.api.PackerOperationStatus;
import p.packer.api.diagnostics.PackerDiagnostic;
import java.util.List;
import java.util.Objects;
public record PackerMutationPreview(
PackerOperationStatus status,
String summary,
String operationId,
PackerMutationRequest request,
List<PackerProposedAction> proposedActions,
List<PackerDiagnostic> diagnostics,
List<String> blockers,
boolean highRisk) {
public PackerMutationPreview {
Objects.requireNonNull(status, "status");
summary = Objects.requireNonNull(summary, "summary").trim();
operationId = Objects.requireNonNull(operationId, "operationId").trim();
Objects.requireNonNull(request, "request");
proposedActions = List.copyOf(Objects.requireNonNull(proposedActions, "proposedActions"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
blockers = List.copyOf(Objects.requireNonNull(blockers, "blockers"));
if (summary.isBlank() || operationId.isBlank()) {
throw new IllegalArgumentException("summary and operationId must not be blank");
}
}
public boolean canApply() {
return blockers.isEmpty();
}
}

View File

@ -0,0 +1,23 @@
package p.packer.api.mutations;
import p.packer.api.PackerProjectContext;
import java.nio.file.Path;
import java.util.Objects;
public record PackerMutationRequest(
PackerProjectContext project,
PackerMutationType type,
String assetReference,
Path targetRoot) {
public PackerMutationRequest {
Objects.requireNonNull(project, "project");
Objects.requireNonNull(type, "type");
assetReference = Objects.requireNonNull(assetReference, "assetReference").trim();
targetRoot = targetRoot == null ? null : targetRoot.toAbsolutePath().normalize();
if (assetReference.isBlank()) {
throw new IllegalArgumentException("assetReference must not be blank");
}
}
}

View File

@ -0,0 +1,26 @@
package p.packer.api.mutations;
import p.packer.api.PackerOperationStatus;
import p.packer.api.diagnostics.PackerDiagnostic;
import java.util.List;
import java.util.Objects;
public record PackerMutationResult(
PackerOperationStatus status,
String summary,
String operationId,
List<PackerProposedAction> appliedActions,
List<PackerDiagnostic> diagnostics) {
public PackerMutationResult {
Objects.requireNonNull(status, "status");
summary = Objects.requireNonNull(summary, "summary").trim();
operationId = Objects.requireNonNull(operationId, "operationId").trim();
appliedActions = List.copyOf(Objects.requireNonNull(appliedActions, "appliedActions"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
if (summary.isBlank() || operationId.isBlank()) {
throw new IllegalArgumentException("summary and operationId must not be blank");
}
}
}

View File

@ -0,0 +1,11 @@
package p.packer.api.mutations;
import p.packer.api.PackerOperationClass;
public interface PackerMutationService {
PackerOperationClass operationClass();
PackerMutationPreview preview(PackerMutationRequest request);
PackerMutationResult apply(PackerMutationPreview preview);
}

View File

@ -0,0 +1,10 @@
package p.packer.api.mutations;
public enum PackerMutationType {
REGISTER_ASSET,
ADOPT_ASSET,
FORGET_ASSET,
REMOVE_ASSET,
QUARANTINE_ASSET,
RELOCATE_ASSET
}

View File

@ -0,0 +1,20 @@
package p.packer.api.mutations;
import p.packer.api.PackerOperationClass;
import java.util.Objects;
public record PackerProposedAction(
PackerOperationClass operationClass,
String verb,
String target) {
public PackerProposedAction {
Objects.requireNonNull(operationClass, "operationClass");
verb = Objects.requireNonNull(verb, "verb").trim();
target = Objects.requireNonNull(target, "target").trim();
if (verb.isBlank() || target.isBlank()) {
throw new IllegalArgumentException("verb and target must not be blank");
}
}
}

View File

@ -0,0 +1,18 @@
package p.packer.api.workspace;
import p.packer.api.PackerProjectContext;
import java.util.Objects;
public record GetAssetDetailsRequest(
PackerProjectContext project,
String assetReference) {
public GetAssetDetailsRequest {
Objects.requireNonNull(project, "project");
assetReference = Objects.requireNonNull(assetReference, "assetReference").trim();
if (assetReference.isBlank()) {
throw new IllegalArgumentException("assetReference must not be blank");
}
}
}

View File

@ -0,0 +1,25 @@
package p.packer.api.workspace;
import p.packer.api.PackerOperationStatus;
import p.packer.api.assets.PackerAssetDetails;
import p.packer.api.diagnostics.PackerDiagnostic;
import java.util.List;
import java.util.Objects;
public record GetAssetDetailsResult(
PackerOperationStatus status,
String summary,
PackerAssetDetails details,
List<PackerDiagnostic> diagnostics) {
public GetAssetDetailsResult {
Objects.requireNonNull(status, "status");
summary = Objects.requireNonNull(summary, "summary").trim();
Objects.requireNonNull(details, "details");
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
if (summary.isBlank()) {
throw new IllegalArgumentException("summary must not be blank");
}
}
}

View File

@ -0,0 +1,13 @@
package p.packer.api.workspace;
import p.packer.api.PackerProjectContext;
import java.util.Objects;
public record InitWorkspaceRequest(
PackerProjectContext project) {
public InitWorkspaceRequest {
Objects.requireNonNull(project, "project");
}
}

View File

@ -0,0 +1,25 @@
package p.packer.api.workspace;
import p.packer.api.PackerOperationStatus;
import p.packer.api.diagnostics.PackerDiagnostic;
import java.nio.file.Path;
import java.util.List;
import java.util.Objects;
public record InitWorkspaceResult(
PackerOperationStatus status,
String summary,
Path registryPath,
List<PackerDiagnostic> diagnostics) {
public InitWorkspaceResult {
Objects.requireNonNull(status, "status");
summary = Objects.requireNonNull(summary, "summary").trim();
registryPath = Objects.requireNonNull(registryPath, "registryPath").toAbsolutePath().normalize();
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
if (summary.isBlank()) {
throw new IllegalArgumentException("summary must not be blank");
}
}
}

View File

@ -0,0 +1,13 @@
package p.packer.api.workspace;
import p.packer.api.PackerProjectContext;
import java.util.Objects;
public record ListAssetsRequest(
PackerProjectContext project) {
public ListAssetsRequest {
Objects.requireNonNull(project, "project");
}
}

View File

@ -0,0 +1,25 @@
package p.packer.api.workspace;
import p.packer.api.PackerOperationStatus;
import p.packer.api.assets.PackerAssetSummary;
import p.packer.api.diagnostics.PackerDiagnostic;
import java.util.List;
import java.util.Objects;
public record ListAssetsResult(
PackerOperationStatus status,
String summary,
List<PackerAssetSummary> assets,
List<PackerDiagnostic> diagnostics) {
public ListAssetsResult {
Objects.requireNonNull(status, "status");
summary = Objects.requireNonNull(summary, "summary").trim();
assets = List.copyOf(Objects.requireNonNull(assets, "assets"));
diagnostics = List.copyOf(Objects.requireNonNull(diagnostics, "diagnostics"));
if (summary.isBlank()) {
throw new IllegalArgumentException("summary must not be blank");
}
}
}

View File

@ -0,0 +1,13 @@
package p.packer.api.workspace;
import p.packer.api.PackerOperationClass;
public interface PackerWorkspaceService {
PackerOperationClass operationClass();
InitWorkspaceResult initWorkspace(InitWorkspaceRequest request);
ListAssetsResult listAssets(ListAssetsRequest request);
GetAssetDetailsResult getAssetDetails(GetAssetDetailsRequest request);
}

View File

@ -0,0 +1,21 @@
package p.packer.testing;
import org.junit.jupiter.api.Test;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.junit.jupiter.api.Assertions.assertTrue;
final class PackerFixtureCatalogTest {
@Test
void exposesWorkspaceFixturesForFuturePackerTests() {
final Path emptyWorkspace = PackerFixtureLocator.fixtureRoot("workspaces/empty");
final Path managedWorkspace = PackerFixtureLocator.fixtureRoot("workspaces/managed-basic");
assertTrue(Files.isDirectory(emptyWorkspace));
assertTrue(Files.isDirectory(managedWorkspace));
assertTrue(Files.isRegularFile(managedWorkspace.resolve("assets/.prometeu/index.json")));
assertTrue(Files.isRegularFile(managedWorkspace.resolve("assets/ui/atlas/asset.json")));
}
}

View File

@ -0,0 +1,23 @@
package p.packer.testing;
import java.net.URL;
import java.nio.file.Path;
import java.util.Objects;
public final class PackerFixtureLocator {
private PackerFixtureLocator() {
}
public static Path fixtureRoot(String relativePath) {
final String normalized = Objects.requireNonNull(relativePath, "relativePath").replace('\\', '/');
final URL resource = PackerFixtureLocator.class.getClassLoader().getResource("fixtures/" + normalized);
if (resource == null) {
throw new IllegalArgumentException("fixture not found: " + normalized);
}
try {
return Path.of(resource.toURI()).toAbsolutePath().normalize();
} catch (Exception exception) {
throw new IllegalStateException("unable to resolve fixture: " + normalized, exception);
}
}
}

View File

@ -0,0 +1,11 @@
{
"schema_version": 1,
"next_asset_id": 2,
"assets": [
{
"asset_id": 1,
"asset_uuid": "fixture-uuid-1",
"root": "ui/atlas"
}
]
}

View File

@ -0,0 +1,15 @@
{
"schema_version": 1,
"name": "ui_atlas",
"type": "image_bank",
"inputs": {
"sprites": ["sprites/confirm.png"]
},
"output": {
"format": "TILES/indexed_v1",
"codec": "RAW"
},
"preload": {
"enabled": true
}
}

View File

@ -5,6 +5,7 @@ plugins {
rootProject.name = "prometeu-studio"
include("prometeu-infra")
include("prometeu-packer")
include("prometeu-compiler:frontends:prometeu-frontend-pbs")
include("prometeu-compiler:prometeu-compiler-core")