fix(javascript): report application analysis progress on the CLI (#1008)
* fix(javascript): report application analysis progress on the CLI * test(cli): preserve progress in filtered output checks
This commit is contained in:
parent
51956c1a49
commit
7b18c58140
|
|
@ -31,6 +31,11 @@ command and the generic route return their complete results inline. Native
|
|||
targets, single JavaScript files, `.app` bundles, and explicit deep-provider or
|
||||
snapshot requests retain the native deep-analysis route.
|
||||
|
||||
Both CLI routes emit `rea_progress` JSON lines on stderr as inventory, source
|
||||
parsing, graph construction, validation, and Evidence hashing begin. Stdout
|
||||
remains the selected result document. Completion is reported after Evidence
|
||||
creation; a failed analysis retains its typed diagnostic result.
|
||||
|
||||
Configure an MCP client with the ordinary REA setup command:
|
||||
|
||||
```bash
|
||||
|
|
|
|||
|
|
@ -57,13 +57,27 @@ export const analyzeJavaScriptApplicationValidated = async (
|
|||
format: input.format,
|
||||
},
|
||||
options.signal,
|
||||
options.progress,
|
||||
);
|
||||
const { electron_summary: summary, ...application } = reconstructed;
|
||||
await options.progress?.report({
|
||||
phase: "validate_javascript_application_result",
|
||||
completed: 0,
|
||||
total: 1,
|
||||
message: "Validating the application analysis result",
|
||||
});
|
||||
const result = javascriptApplicationAnalysisResultSchema.parse({
|
||||
...application,
|
||||
summary,
|
||||
limitations: reconstructed.graph.limitations,
|
||||
});
|
||||
await options.progress?.report({
|
||||
phase: "create_javascript_application_evidence",
|
||||
completed: 0,
|
||||
total: 1,
|
||||
message: "Creating and hashing application analysis Evidence",
|
||||
});
|
||||
const evidence = createJavaScriptApplicationEvidence(input, result);
|
||||
await options.progress?.report({
|
||||
phase: "analyze_javascript_application",
|
||||
completed: 1,
|
||||
|
|
@ -71,7 +85,7 @@ export const analyzeJavaScriptApplicationValidated = async (
|
|||
message: "Application graph and Electron boundaries reconstructed",
|
||||
terminal: true,
|
||||
});
|
||||
return ok(createJavaScriptApplicationEvidence(input, result));
|
||||
return ok(evidence);
|
||||
} catch (cause: unknown) {
|
||||
if (cause instanceof ArtifactReaderFailure)
|
||||
return err(
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
import { scanCanonicalArtifactInventory } from "../ArtifactInventory.js";
|
||||
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
|
||||
import { buildJavaScriptSemanticGraph } from "./JavaScriptSemanticGraphBuilder.js";
|
||||
import type { ProgressReporter } from "../ProgressReporter.js";
|
||||
|
||||
/** Application-layer result retaining local diagnostics outside the canonical graph. */
|
||||
export interface JavaScriptArtifactReconstructionResult {
|
||||
|
|
@ -45,7 +46,11 @@ export interface JavaScriptArtifactReconstructionResult {
|
|||
export const reconstructJavaScriptArtifact = async (
|
||||
rawInput: unknown,
|
||||
signal?: AbortSignal,
|
||||
progress?: ProgressReporter,
|
||||
): Promise<JavaScriptArtifactReconstructionResult> => {
|
||||
const reportPhase = async (phase: string, message: string): Promise<void> => {
|
||||
await progress?.report({ phase, completed: 0, total: 1, message });
|
||||
};
|
||||
const input = javascriptArtifactReconstructionInputSchema.parse(rawInput);
|
||||
abortIfNeeded(signal);
|
||||
const path = await realpath(input.input_path);
|
||||
|
|
@ -58,10 +63,26 @@ export const reconstructJavaScriptArtifact = async (
|
|||
);
|
||||
const reader = createReader(path, format);
|
||||
try {
|
||||
await reportPhase(
|
||||
"read_javascript_artifacts",
|
||||
"Reading inventoried JavaScript application sources",
|
||||
);
|
||||
const files = await readJavaScriptArtifactFiles(reader, snapshot, signal);
|
||||
await reportPhase(
|
||||
"parse_javascript_sources",
|
||||
`Parsing ${String(files.files.length)} application source files`,
|
||||
);
|
||||
const analysis = analyzeJavaScriptArtifactFiles(files);
|
||||
abortIfNeeded(signal);
|
||||
await reportPhase(
|
||||
"build_javascript_application_graph",
|
||||
"Constructing application and Electron boundary relationships",
|
||||
);
|
||||
const graph = buildJavaScriptArtifactGraph(snapshot, files, analysis);
|
||||
await reportPhase(
|
||||
"build_javascript_semantic_graph",
|
||||
"Constructing static semantic relationships",
|
||||
);
|
||||
const semanticGraph = buildJavaScriptSemanticGraph({
|
||||
rootArtifactSha256: snapshot.manifest.root_sha256,
|
||||
applicationGraph: graph,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { analyzeJavaScriptApplication } from "../application/javascript/JavaScriptApplicationService.js";
|
||||
import { createProgressReporter } from "../application/ProgressReporter.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
|
|
@ -6,7 +7,13 @@ import type { JsonValue } from "../domain/jsonValue.js";
|
|||
export const runCliJavaScriptApplicationAnalysis = async (
|
||||
input: unknown,
|
||||
): Promise<JsonValue> => {
|
||||
const result = await analyzeJavaScriptApplication(input);
|
||||
const progress = createProgressReporter(
|
||||
async (update) => {
|
||||
process.stderr.write(`${JSON.stringify({ rea_progress: update })}\n`);
|
||||
},
|
||||
{ minimumIntervalMs: 0 },
|
||||
);
|
||||
const result = await analyzeJavaScriptApplication(input, { progress });
|
||||
return result.ok ? result.value : cliError(result.error);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ describe("compiled CLI output boundary", () => {
|
|||
expect(result).toMatchObject({
|
||||
exitCode: 0,
|
||||
stdout: "{}\n",
|
||||
stderr: "",
|
||||
stderr: expect.stringContaining('"rea_progress":'),
|
||||
json: {},
|
||||
});
|
||||
const nested = await cli.run({
|
||||
|
|
@ -260,7 +260,7 @@ describe("compiled CLI output boundary", () => {
|
|||
});
|
||||
expect(nested).toMatchObject({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
stderr: expect.stringContaining('"rea_progress":'),
|
||||
json: { normalized_result: {} },
|
||||
});
|
||||
const selected = await cli.run({
|
||||
|
|
@ -275,7 +275,7 @@ describe("compiled CLI output boundary", () => {
|
|||
});
|
||||
expect(selected).toMatchObject({
|
||||
exitCode: 0,
|
||||
stderr: "",
|
||||
stderr: expect.stringContaining('"rea_progress":'),
|
||||
json: {
|
||||
normalized_result: {
|
||||
summary: expect.objectContaining({ browser_windows: 0 }),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,72 @@
|
|||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { expect } from "vitest";
|
||||
import { z } from "zod";
|
||||
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { analyzeJavaScriptApplication } from "../../../src/application/javascript/JavaScriptApplicationService.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
const progressLine = z.object({
|
||||
rea_progress: z.object({
|
||||
phase: z.string(),
|
||||
completed: z.number(),
|
||||
total: z.number().nullable(),
|
||||
message: z.string(),
|
||||
sequence: z.number(),
|
||||
terminal: z.boolean().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
for (const command of ["analyze-javascript-application", "analyze"]) {
|
||||
cliTest(
|
||||
`${command} reports real analysis phases on stderr and preserves stdout Evidence`,
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-js-cli-progress-");
|
||||
await writeFile(join(root, "main.js"), "fetch('');\n");
|
||||
const direct = await analyzeJavaScriptApplication({ input_path: root });
|
||||
if (!direct.ok) throw direct.error;
|
||||
|
||||
const output = await cli.run({ arguments: [command, root, "--json"] });
|
||||
expect(output.exitCode).toBe(0);
|
||||
expect(parseEvidence(output.json).evidence_id).toBe(
|
||||
direct.value.evidence_id,
|
||||
);
|
||||
const updates = output.stderr
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => progressLine.parse(JSON.parse(line)).rea_progress);
|
||||
expect(updates[0]?.completed).toBe(0);
|
||||
expect(
|
||||
updates.some(({ phase }) => phase === "parse_javascript_sources"),
|
||||
).toBe(true);
|
||||
expect(updates.at(-1)).toMatchObject({ completed: 1, terminal: true });
|
||||
expect(
|
||||
updates.slice(0, -1).every(({ terminal }) => terminal !== true),
|
||||
).toBe(true);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
cliTest(
|
||||
"keeps a failed analysis distinct from a completed result",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-js-cli-progress-failure-");
|
||||
const output = await cli.run({
|
||||
arguments: [
|
||||
"analyze-javascript-application",
|
||||
join(root, "absent"),
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(output.json).toMatchObject({ code: "artifact_operation_failed" });
|
||||
const updates = output.stderr
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => progressLine.parse(JSON.parse(line)).rea_progress);
|
||||
expect(updates[0]?.completed).toBe(0);
|
||||
expect(updates.every(({ terminal }) => terminal !== true)).toBe(true);
|
||||
},
|
||||
);
|
||||
|
|
@ -10,6 +10,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|||
|
||||
import { createTestBinarySession } from "../../fixtures/binarySession.js";
|
||||
import type { ElectronActiveObservationPort } from "../../../src/application/javascript/ElectronActiveObservationPort.js";
|
||||
import { analyzeJavaScriptApplication } from "../../../src/application/javascript/JavaScriptApplicationService.js";
|
||||
import { CdpElectronProvider } from "../../../src/browser/CdpElectronProvider.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { observed } from "../../fixtures/analysisExecution.js";
|
||||
|
|
@ -270,12 +271,18 @@ it("exposes the target-free static JavaScript application workflow", async () =>
|
|||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
|
||||
const analyzed = await client.callTool({
|
||||
name: "analyze_javascript_application",
|
||||
arguments: { input_path: root },
|
||||
});
|
||||
const progress: number[] = [];
|
||||
const analyzed = await client.callTool(
|
||||
{
|
||||
name: "analyze_javascript_application",
|
||||
arguments: { input_path: root },
|
||||
},
|
||||
{ onprogress: (update) => progress.push(update.progress) },
|
||||
);
|
||||
|
||||
expect(analyzed.isError).not.toBe(true);
|
||||
expect(progress[0]).toBe(0);
|
||||
expect(progress.at(-1)).toBe(1);
|
||||
const projected = z
|
||||
.object({
|
||||
evidence_id: z.string(),
|
||||
|
|
@ -314,6 +321,9 @@ it("exposes the target-free static JavaScript application workflow", async () =>
|
|||
expect(projected.result.graph.nodes.length).toBeGreaterThan(0);
|
||||
expect(projected.result.semantic_graph.nodes.length).toBeGreaterThan(0);
|
||||
expect(projected.evidence.operation).toBe("analyze_javascript_application");
|
||||
const direct = await analyzeJavaScriptApplication({ input_path: root });
|
||||
if (!direct.ok) throw direct.error;
|
||||
expect(projected.evidence_id).toBe(direct.value.evidence_id);
|
||||
});
|
||||
|
||||
const evidenceFor = (value: unknown) => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue