implements packer PR-06 doctor diagnostics baseline
This commit is contained in:
parent
c4fc6e0041
commit
f03cbb28af
@ -0,0 +1,153 @@
|
||||
package p.packer.doctor;
|
||||
|
||||
import p.packer.api.PackerOperationClass;
|
||||
import p.packer.api.PackerOperationStatus;
|
||||
import p.packer.api.PackerProjectContext;
|
||||
import p.packer.api.assets.PackerAssetState;
|
||||
import p.packer.api.diagnostics.PackerDiagnostic;
|
||||
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
||||
import p.packer.api.diagnostics.PackerDiagnosticSeverity;
|
||||
import p.packer.api.doctor.PackerDoctorMode;
|
||||
import p.packer.api.doctor.PackerDoctorRequest;
|
||||
import p.packer.api.doctor.PackerDoctorResult;
|
||||
import p.packer.api.doctor.PackerDoctorService;
|
||||
import p.packer.api.workspace.GetAssetDetailsRequest;
|
||||
import p.packer.api.workspace.ListAssetsRequest;
|
||||
import p.packer.declarations.PackerAssetDetailsService;
|
||||
import p.packer.workspace.FileSystemPackerWorkspaceService;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
|
||||
public final class FileSystemPackerDoctorService implements PackerDoctorService {
|
||||
private final FileSystemPackerWorkspaceService workspaceService;
|
||||
private final PackerAssetDetailsService detailsService;
|
||||
|
||||
public FileSystemPackerDoctorService() {
|
||||
this(new FileSystemPackerWorkspaceService(), new PackerAssetDetailsService());
|
||||
}
|
||||
|
||||
public FileSystemPackerDoctorService(
|
||||
FileSystemPackerWorkspaceService workspaceService,
|
||||
PackerAssetDetailsService detailsService) {
|
||||
this.workspaceService = Objects.requireNonNull(workspaceService, "workspaceService");
|
||||
this.detailsService = Objects.requireNonNull(detailsService, "detailsService");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PackerOperationClass operationClass() {
|
||||
return PackerOperationClass.READ_ONLY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public PackerDoctorResult doctor(PackerDoctorRequest request) {
|
||||
final PackerDoctorRequest doctorRequest = Objects.requireNonNull(request, "request");
|
||||
final PackerProjectContext project = doctorRequest.project();
|
||||
final var snapshot = workspaceService.listAssets(new ListAssetsRequest(project));
|
||||
final List<PackerDiagnostic> diagnostics = new ArrayList<>();
|
||||
final Set<String> safeFixes = new LinkedHashSet<>();
|
||||
final Set<String> seenDiagnostics = new LinkedHashSet<>();
|
||||
|
||||
for (PackerDiagnostic diagnostic : snapshot.diagnostics()) {
|
||||
addDiagnostic(diagnostics, seenDiagnostics, diagnostic);
|
||||
}
|
||||
|
||||
for (var asset : snapshot.assets()) {
|
||||
if (!includeAsset(doctorRequest.mode(), asset.state())) {
|
||||
continue;
|
||||
}
|
||||
final String assetReference = asset.identity().assetId() == null
|
||||
? relativeAssetRoot(project, asset.identity().assetRoot())
|
||||
: Integer.toString(asset.identity().assetId());
|
||||
final var details = detailsService.getAssetDetails(new GetAssetDetailsRequest(project, assetReference)).details();
|
||||
for (PackerDiagnostic diagnostic : details.diagnostics()) {
|
||||
addDiagnostic(diagnostics, seenDiagnostics, diagnostic);
|
||||
}
|
||||
|
||||
if (doctorRequest.mode() == PackerDoctorMode.EXPANDED_WORKSPACE && asset.state() == PackerAssetState.ORPHAN) {
|
||||
addDiagnostic(diagnostics, seenDiagnostics, new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.WARNING,
|
||||
PackerDiagnosticCategory.HYGIENE,
|
||||
"Orphan asset is valid but not registered in the managed build set: " + relativeAssetRoot(project, asset.identity().assetRoot()),
|
||||
asset.identity().assetRoot(),
|
||||
false));
|
||||
if (doctorRequest.includeSafeFixes() && details.summary().state() != PackerAssetState.INVALID) {
|
||||
safeFixes.add("register_asset " + relativeAssetRoot(project, asset.identity().assetRoot()));
|
||||
}
|
||||
}
|
||||
|
||||
if (doctorRequest.mode() == PackerDoctorMode.EXPANDED_WORKSPACE && isInsideQuarantine(project, asset.identity().assetRoot())) {
|
||||
addDiagnostic(diagnostics, seenDiagnostics, new PackerDiagnostic(
|
||||
PackerDiagnosticSeverity.INFO,
|
||||
PackerDiagnosticCategory.HYGIENE,
|
||||
"Asset root is currently quarantined and excluded from the active workspace surface.",
|
||||
asset.identity().assetRoot(),
|
||||
false));
|
||||
}
|
||||
|
||||
details.inputsByRole().forEach((role, inputs) -> inputs.forEach(input -> {
|
||||
if (Files.isRegularFile(input)) {
|
||||
return;
|
||||
}
|
||||
final boolean managed = asset.state() == PackerAssetState.MANAGED;
|
||||
addDiagnostic(diagnostics, seenDiagnostics, new PackerDiagnostic(
|
||||
managed ? PackerDiagnosticSeverity.ERROR : PackerDiagnosticSeverity.WARNING,
|
||||
managed ? PackerDiagnosticCategory.STRUCTURAL : PackerDiagnosticCategory.HYGIENE,
|
||||
"Declared input is missing for role '" + role + "': " + relativeEvidence(project, input),
|
||||
input,
|
||||
managed));
|
||||
}));
|
||||
}
|
||||
|
||||
final long blockingCount = diagnostics.stream().filter(PackerDiagnostic::blocking).count();
|
||||
final long warningCount = diagnostics.stream().filter(diagnostic -> diagnostic.severity() == PackerDiagnosticSeverity.WARNING).count();
|
||||
final PackerOperationStatus status = blockingCount > 0L ? PackerOperationStatus.FAILED
|
||||
: diagnostics.isEmpty() ? PackerOperationStatus.SUCCESS : PackerOperationStatus.PARTIAL;
|
||||
final String summary = blockingCount > 0L
|
||||
? "Doctor found " + blockingCount + " blocking diagnostics and " + warningCount + " warnings."
|
||||
: diagnostics.isEmpty()
|
||||
? "Doctor found no diagnostics."
|
||||
: "Doctor found " + diagnostics.size() + " diagnostics with no blockers.";
|
||||
return new PackerDoctorResult(status, summary, diagnostics, List.copyOf(safeFixes));
|
||||
}
|
||||
|
||||
private boolean includeAsset(PackerDoctorMode mode, PackerAssetState state) {
|
||||
if (mode == PackerDoctorMode.EXPANDED_WORKSPACE) {
|
||||
return true;
|
||||
}
|
||||
return state == PackerAssetState.MANAGED || state == PackerAssetState.INVALID;
|
||||
}
|
||||
|
||||
private void addDiagnostic(List<PackerDiagnostic> diagnostics, Set<String> seenDiagnostics, PackerDiagnostic diagnostic) {
|
||||
final String key = diagnostic.category() + "|" + diagnostic.severity() + "|" + diagnostic.message() + "|" + diagnostic.evidencePath() + "|" + diagnostic.blocking();
|
||||
if (seenDiagnostics.add(key)) {
|
||||
diagnostics.add(diagnostic);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isInsideQuarantine(PackerProjectContext project, Path assetRoot) {
|
||||
return assetRoot.toAbsolutePath().normalize()
|
||||
.startsWith(project.rootPath().resolve("assets/.prometeu/quarantine").toAbsolutePath().normalize());
|
||||
}
|
||||
|
||||
private String relativeAssetRoot(PackerProjectContext project, Path assetRoot) {
|
||||
return project.rootPath().resolve("assets").toAbsolutePath().normalize()
|
||||
.relativize(assetRoot.toAbsolutePath().normalize())
|
||||
.toString()
|
||||
.replace('\\', '/');
|
||||
}
|
||||
|
||||
private String relativeEvidence(PackerProjectContext project, Path evidencePath) {
|
||||
final Path normalizedEvidence = evidencePath.toAbsolutePath().normalize();
|
||||
final Path projectRoot = project.rootPath().toAbsolutePath().normalize();
|
||||
if (normalizedEvidence.startsWith(projectRoot)) {
|
||||
return projectRoot.relativize(normalizedEvidence).toString().replace('\\', '/');
|
||||
}
|
||||
return normalizedEvidence.toString();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,160 @@
|
||||
package p.packer.doctor;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import p.packer.api.PackerOperationStatus;
|
||||
import p.packer.api.PackerProjectContext;
|
||||
import p.packer.api.diagnostics.PackerDiagnosticCategory;
|
||||
import p.packer.api.doctor.PackerDoctorMode;
|
||||
import p.packer.api.doctor.PackerDoctorRequest;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
final class FileSystemPackerDoctorServiceTest {
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
@Test
|
||||
void managedWorldReportsBlockingDiagnosticsForMissingManagedInputs() throws Exception {
|
||||
final Path projectRoot = createManagedProjectWithMissingInput();
|
||||
final FileSystemPackerDoctorService service = new FileSystemPackerDoctorService();
|
||||
|
||||
final var result = service.doctor(new PackerDoctorRequest(
|
||||
new PackerProjectContext("main", projectRoot),
|
||||
PackerDoctorMode.MANAGED_WORLD,
|
||||
false));
|
||||
|
||||
assertEquals(PackerOperationStatus.FAILED, result.status());
|
||||
assertTrue(result.diagnostics().stream().anyMatch(diagnostic ->
|
||||
diagnostic.category() == PackerDiagnosticCategory.STRUCTURAL
|
||||
&& diagnostic.blocking()
|
||||
&& diagnostic.message().contains("Declared input is missing")));
|
||||
assertTrue(result.safeFixes().isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
void expandedWorkspaceReportsOrphanAssetsAndRegisterSafeFixes() throws Exception {
|
||||
final Path projectRoot = createExpandedWorkspace();
|
||||
final FileSystemPackerDoctorService service = new FileSystemPackerDoctorService();
|
||||
|
||||
final var result = service.doctor(new PackerDoctorRequest(
|
||||
new PackerProjectContext("main", projectRoot),
|
||||
PackerDoctorMode.EXPANDED_WORKSPACE,
|
||||
true));
|
||||
|
||||
assertEquals(PackerOperationStatus.PARTIAL, result.status());
|
||||
assertTrue(result.diagnostics().stream().anyMatch(diagnostic ->
|
||||
diagnostic.category() == PackerDiagnosticCategory.HYGIENE
|
||||
&& diagnostic.message().contains("Orphan asset is valid but not registered")));
|
||||
assertTrue(result.safeFixes().contains("register_asset orphans/ui_sounds"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void managedWorldIgnoresOrphanOnlyHygieneFindings() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("orphan-only");
|
||||
final Path orphanRoot = projectRoot.resolve("assets/orphans/ui_sounds");
|
||||
Files.createDirectories(orphanRoot);
|
||||
Files.writeString(orphanRoot.resolve("asset.json"), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "ui_sounds",
|
||||
"type": "sound_bank",
|
||||
"inputs": { "sources": ["confirm.wav"] },
|
||||
"output": { "format": "SOUND/bank_v1", "codec": "RAW" },
|
||||
"preload": { "enabled": false }
|
||||
}
|
||||
""");
|
||||
Files.writeString(orphanRoot.resolve("confirm.wav"), "beep");
|
||||
|
||||
final FileSystemPackerDoctorService service = new FileSystemPackerDoctorService();
|
||||
|
||||
final var result = service.doctor(new PackerDoctorRequest(
|
||||
new PackerProjectContext("main", projectRoot),
|
||||
PackerDoctorMode.MANAGED_WORLD,
|
||||
true));
|
||||
|
||||
assertEquals(PackerOperationStatus.SUCCESS, result.status());
|
||||
assertTrue(result.diagnostics().isEmpty());
|
||||
assertTrue(result.safeFixes().isEmpty());
|
||||
}
|
||||
|
||||
private Path createManagedProjectWithMissingInput() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("managed-missing-input");
|
||||
final Path assetRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
Files.createDirectories(assetRoot);
|
||||
Files.createDirectories(projectRoot.resolve("assets/.prometeu"));
|
||||
Files.writeString(assetRoot.resolve("asset.json"), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "ui_atlas",
|
||||
"type": "image_bank",
|
||||
"inputs": { "sprites": ["sprites/confirm.png"] },
|
||||
"output": { "format": "TILES/indexed_v1", "codec": "RAW" },
|
||||
"preload": { "enabled": true }
|
||||
}
|
||||
""");
|
||||
Files.writeString(projectRoot.resolve("assets/.prometeu/index.json"), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"next_asset_id": 2,
|
||||
"assets": [
|
||||
{
|
||||
"asset_id": 1,
|
||||
"asset_uuid": "uuid-1",
|
||||
"root": "ui/atlas"
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
return projectRoot;
|
||||
}
|
||||
|
||||
private Path createExpandedWorkspace() throws Exception {
|
||||
final Path projectRoot = tempDir.resolve("expanded");
|
||||
final Path managedRoot = projectRoot.resolve("assets/ui/atlas");
|
||||
Files.createDirectories(managedRoot);
|
||||
Files.createDirectories(projectRoot.resolve("assets/.prometeu"));
|
||||
Files.createDirectories(managedRoot.resolve("sprites"));
|
||||
Files.writeString(managedRoot.resolve("asset.json"), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "ui_atlas",
|
||||
"type": "image_bank",
|
||||
"inputs": { "sprites": ["sprites/confirm.png"] },
|
||||
"output": { "format": "TILES/indexed_v1", "codec": "RAW" },
|
||||
"preload": { "enabled": true }
|
||||
}
|
||||
""");
|
||||
Files.writeString(managedRoot.resolve("sprites/confirm.png"), "png");
|
||||
Files.writeString(projectRoot.resolve("assets/.prometeu/index.json"), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"next_asset_id": 2,
|
||||
"assets": [
|
||||
{
|
||||
"asset_id": 1,
|
||||
"asset_uuid": "uuid-1",
|
||||
"root": "ui/atlas"
|
||||
}
|
||||
]
|
||||
}
|
||||
""");
|
||||
final Path orphanRoot = projectRoot.resolve("assets/orphans/ui_sounds");
|
||||
Files.createDirectories(orphanRoot);
|
||||
Files.writeString(orphanRoot.resolve("asset.json"), """
|
||||
{
|
||||
"schema_version": 1,
|
||||
"name": "ui_sounds",
|
||||
"type": "sound_bank",
|
||||
"inputs": { "sources": ["confirm.wav"] },
|
||||
"output": { "format": "SOUND/bank_v1", "codec": "RAW" },
|
||||
"preload": { "enabled": false }
|
||||
}
|
||||
""");
|
||||
Files.writeString(orphanRoot.resolve("confirm.wav"), "beep");
|
||||
return projectRoot;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user