implements packer PR-06 doctor diagnostics baseline

This commit is contained in:
bQUARKz 2026-03-11 17:59:32 +00:00
parent c4fc6e0041
commit f03cbb28af
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
2 changed files with 313 additions and 0 deletions

View File

@ -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();
}
}

View File

@ -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;
}
}