rea/src/android/JadxDoctor.ts

156 lines
5.3 KiB
TypeScript

import type {
DoctorProviderCheck,
DoctorProviderInspection,
} from "../application/Doctor.js";
import type { ProviderAvailability } from "../application/AnalysisProvider.js";
import type { JsonValue } from "../domain/jsonValue.js";
import {
inspectJadxAvailability,
JADX_FAILURE_KINDS,
projectJadxFailureOutcome,
type JadxFailureKind,
} from "./JadxConfiguration.js";
/** Project the validated JADX configuration into the doctor contract. */
export const inspectJadxProvider = async (
environment: Readonly<Record<string, string | undefined>>,
): Promise<DoctorProviderInspection> => {
const configured = environment.REA_JADX_MCP_JAR !== undefined;
const availability = await inspectJadxAvailability(environment);
// diagnostics is a loosely-typed JsonValue map; guard the published path
// even though the success path always sets jar_path today.
const jarPath =
availability.status === "available" &&
typeof availability.diagnostics["jar_path"] === "string"
? availability.diagnostics["jar_path"]
: undefined;
const available =
availability.status === "available" && jarPath !== undefined;
// Preserve the caller-selected JAR path (it may be a stable symlink) and
// the validated JAVA_HOME selection; the resolved jarPath stays in
// diagnostics as inspection evidence.
const configuredJar = environment.REA_JADX_MCP_JAR;
return {
id: "jadx",
configured,
available,
providerVersion: null,
registrationEnvironment:
jarPath === undefined || configuredJar === undefined
? {}
: {
REA_JADX_MCP_JAR: configuredJar,
...optionalEntry("JAVA_HOME", environment.JAVA_HOME),
...optionalEntry(
"REA_JADX_HEAP_MIB",
environment.REA_JADX_HEAP_MIB,
),
...optionalEntry(
"REA_JADX_ACTIVE_PROCESSOR_COUNT",
environment.REA_JADX_ACTIVE_PROCESSOR_COUNT,
),
},
checks: [configurationCheck(configured, availability, jarPath)],
};
};
/**
* Publish a caller-selected variable only when explicitly set; never persist
* ambient inheritance. Values are validated before this point.
*/
const optionalEntry = (
name: string,
value: string | undefined,
): Record<string, string> => (value === undefined ? {} : { [name]: value });
const configurationCheck = (
configured: boolean,
availability: ProviderAvailability,
jarPath: string | undefined,
): DoctorProviderCheck => {
const code = availability.status === "unavailable" ? availability.code : null;
if (availability.status === "available" && jarPath !== undefined) {
return {
name: "configuration",
ok: true,
code: null,
detail: `JADX MCP bridge JAR validated at ${jarPath}; its path is recorded for agent registrations.`,
remediation: null,
classification: "config_drift",
};
}
// The failure kind is tagged at the configuration boundary; project its
// remediation and classification here instead of reinterpreting the
// phase/setting evidence. Every failed check keeps a remediation.
const kind =
availability.status === "unavailable"
? asFailureKind(availability.diagnostics["failure_kind"])
: null;
const outcome = projectJadxFailureOutcome(kind, configured);
return {
name: "configuration",
ok: false,
code,
detail:
availability.status === "available"
? "Validated JADX configuration reported no JAR path."
: availability.reason,
remediation: outcome.remediation,
classification: outcome.classification,
...(kind === "java_probe" && availability.status === "unavailable"
? { details: probeDetails(availability.diagnostics) }
: kind !== null &&
kind.startsWith("windows_native_") &&
availability.status === "unavailable"
? { details: windowsNativeFailureDetails(availability.diagnostics) }
: {}),
};
};
const windowsNativeFailureDetails = (
diagnostics: Readonly<Record<string, JsonValue>>,
): Record<string, JsonValue> => {
const details: Record<string, JsonValue> = {};
for (const key of ["failure_stage", "remediation"] as const) {
const value = diagnostics[key];
if (value !== undefined) details[key] = value;
}
return details;
};
/** Validate the loose diagnostics tag against the boundary's failure kinds. */
const asFailureKind = (value: JsonValue | undefined): JadxFailureKind | null =>
typeof value === "string" &&
(JADX_FAILURE_KINDS as readonly string[]).includes(value)
? (value as JadxFailureKind)
: null;
/**
* Project bounded Java probe metadata for the doctor report. The full
* stdout/stderr can reach the 1 MiB probe budget, so only the bounded
* outcome metadata is carried; the message already references the retained
* output.
*/
const probeDetails = (
diagnostics: Readonly<Record<string, JsonValue>>,
): Record<string, JsonValue> => {
const details: Record<string, JsonValue> = {};
for (const key of [
"java_executable",
"failure",
"output_truncated",
"exit_code",
"process_error_code",
"signal",
"killed",
"output_limit_bytes",
"deadline_ms",
"jdk_compiler",
"java_major",
] as const) {
const value = diagnostics[key];
if (value !== undefined) details[key] = value;
}
return details;
};