fix(evidence): explain fixed analysis view page recovery (#7860)
* fix(evidence): explain fixed analysis view page recovery * fix(evidence): explain unpublished view recovery
This commit is contained in:
1 parent
00bd92c56e
commit
fe52ea12c3
10 files changed
+192
-16
No files matched your search
@@ -5,6 +5,13 @@ import {
|
||||
|
||||
export const INSPECT_ANALYSIS_VIEW_OPERATION = "inspect_analysis_view" as const;
|
||||
|
||||
/** Recovery belongs to the view projection, not to the MCP transport. */
|
||||
export const ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY = {
|
||||
retryable: false,
|
||||
remediationAction:
|
||||
"The 1,048,576-byte analysis-view page allowance is fixed and has no runtime override. For a pageable collection, select fewer fields or a smaller selection; one oversized field may not fit even in a one-row page. If the error includes details.partial_observation, use that complete parent Evidence. Otherwise, read the parent directly from the caller-supplied bundle, or retrieve/export the retained parent in the same MCP session. Reading the parent does not increase the page allowance.",
|
||||
} as const;
|
||||
|
||||
/** Construct a typed input failure for one selected-view constraint. */
|
||||
export const analysisViewInputError = (
|
||||
issues: readonly AnalysisInputIssue[],
|
||||
|
||||
@@ -17,7 +17,10 @@ import type {
|
||||
AnalysisViewRequest,
|
||||
UnsignedAnalysisView,
|
||||
} from "./analysisView.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import {
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
analysisViewInputError,
|
||||
} from "./analysisViewErrors.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
import {
|
||||
boundedViewMessages,
|
||||
@@ -260,11 +263,7 @@ export const projectAndroidJniView = (
|
||||
"transport",
|
||||
"JNI view exceeds its 1 MiB serialized allowance.",
|
||||
{ parent_evidence_id: parent.evidenceId, limit_bytes: 1024 * 1024 },
|
||||
{
|
||||
retryable: false,
|
||||
remediationAction:
|
||||
"Select a smaller collection page and requested fields, or read the complete retained Evidence.",
|
||||
},
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
),
|
||||
);
|
||||
return ok(result);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { createEvidence } from "../evidence.js";
|
||||
import { projectAnalysisError } from "../analysisErrorProjection.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import {
|
||||
analysisViewRequestSchema,
|
||||
@@ -219,15 +220,18 @@ it("bounds selected row serialization and lets field selection recover without c
|
||||
component("large.Class", { process: "x".repeat(2 * 1024 * 1024) }),
|
||||
]),
|
||||
);
|
||||
expect(
|
||||
projectAnalysisView(
|
||||
source,
|
||||
analysisViewRequestSchema.parse({ kind: "android", facet: "components" }),
|
||||
),
|
||||
).toMatchObject({
|
||||
const wide = projectAnalysisView(
|
||||
source,
|
||||
analysisViewRequestSchema.parse({ kind: "android", facet: "components" }),
|
||||
);
|
||||
expect(wide).toMatchObject({
|
||||
ok: false,
|
||||
error: { _tag: "AnalysisResourceConstraintError" },
|
||||
});
|
||||
if (wide.ok) throw new Error("Expected an oversized-row refusal");
|
||||
expect(projectAnalysisError(wide.error).remediation.action).toContain(
|
||||
"select fewer fields",
|
||||
);
|
||||
const small = androidComponentsViewResultSchema.parse(
|
||||
project(source, { kind: "android", facet: "components", fields: ["name"] }),
|
||||
);
|
||||
|
||||
@@ -21,7 +21,10 @@ import {
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import {
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
analysisViewInputError,
|
||||
} from "./analysisViewErrors.js";
|
||||
import {
|
||||
analysisTextPage,
|
||||
analysisTextPageItemSchema,
|
||||
@@ -233,6 +236,7 @@ export const projectAndroidPackageView = (
|
||||
limit_bytes: PAGE_BYTES,
|
||||
facet: view.facet,
|
||||
},
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
),
|
||||
);
|
||||
// Admit caller selections and provenance before building filter indexes or output rows.
|
||||
|
||||
@@ -17,7 +17,10 @@ import type {
|
||||
AnalysisViewRequest,
|
||||
UnsignedAnalysisView,
|
||||
} from "./analysisView.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import {
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
analysisViewInputError,
|
||||
} from "./analysisViewErrors.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
import {
|
||||
boundedViewMessages,
|
||||
@@ -305,6 +308,7 @@ export const projectGpuCodeObjectsView = (
|
||||
"memory",
|
||||
"PTX page exceeds the 1 MiB response budget; lower limit",
|
||||
{ maximum_page_bytes: 1024 * 1024 },
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
),
|
||||
);
|
||||
return ok({
|
||||
@@ -411,6 +415,7 @@ export const projectGpuCodeObjectsView = (
|
||||
"memory",
|
||||
"GPU page exceeds the 1 MiB response budget; select fewer fields or rows",
|
||||
{ maximum_page_bytes: 1024 * 1024 },
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
),
|
||||
);
|
||||
return ok({
|
||||
|
||||
@@ -3,6 +3,7 @@ import { analyzeJavaScriptStaticSource } from "../javascript/javascriptStaticAna
|
||||
import { completeSemanticSteps } from "../javascript/javascriptSemanticTraversal.js";
|
||||
import { buildJavaScriptRouteInventorySteps } from "../javascript/javascriptRouteInventory.js";
|
||||
import { freezeJsonSnapshot } from "../immutableJson.js";
|
||||
import { projectAnalysisError } from "../analysisErrorProjection.js";
|
||||
import {
|
||||
projectAnalysisView,
|
||||
analysisViewRequestSchema,
|
||||
@@ -269,6 +270,47 @@ it("returns a typed retained-parent resource error when even a summary identity
|
||||
expect(result.error.message).toContain("cannot fit");
|
||||
});
|
||||
|
||||
it("explains how to recover an oversized route row without changing the parent", () => {
|
||||
const source = {
|
||||
...parent(),
|
||||
normalizedResult: {
|
||||
input_path: "/gone/app",
|
||||
root_artifact_sha256: "c".repeat(64),
|
||||
route_inventory: inventory(`router.push('/${"a".repeat(1_100_000)}');`),
|
||||
},
|
||||
};
|
||||
const selection = { collection: "routes", limit: 1 };
|
||||
const result = projectAnalysisView(
|
||||
source,
|
||||
analysisViewRequestSchema.parse({
|
||||
kind: "javascript-routes",
|
||||
...selection,
|
||||
}),
|
||||
);
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) throw new Error("Expected an oversized-row refusal");
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
retryable: false,
|
||||
details: {
|
||||
reported_limits: {
|
||||
parent_evidence_id: source.evidenceId,
|
||||
limit_bytes: 1024 * 1024,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(projected.remediation.action).toContain("select fewer fields");
|
||||
expect(projected.remediation.action).toContain("no runtime override");
|
||||
expect(projected.remediation.action).toContain("caller-supplied bundle");
|
||||
const narrow = query(source, {
|
||||
...selection,
|
||||
fields: ["classification", "confidence"],
|
||||
});
|
||||
expect(narrow.items).toHaveLength(1);
|
||||
expect(narrow.items[0]).not.toHaveProperty("path");
|
||||
});
|
||||
|
||||
it.each(["summary", "routes"] as const)(
|
||||
"includes the sealed digest in the %s byte ceiling",
|
||||
(collection) => {
|
||||
|
||||
@@ -26,7 +26,10 @@ import type {
|
||||
AnalysisViewRequest,
|
||||
UnsignedAnalysisView,
|
||||
} from "./analysisView.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import {
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
analysisViewInputError,
|
||||
} from "./analysisViewErrors.js";
|
||||
import {
|
||||
boundedViewMessages,
|
||||
completeWithinViewCoverage,
|
||||
@@ -404,6 +407,7 @@ export const projectJavaScriptRoutesView = (
|
||||
limit_bytes: PAGE_BYTES,
|
||||
collection: view.collection,
|
||||
},
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
),
|
||||
);
|
||||
if (view.collection === "summary") {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { expect, it } from "vitest";
|
||||
import type { WebBundleAnalysis } from "../webBundleAnalysis.js";
|
||||
import { AnalysisInputError } from "../analysisErrorCore.js";
|
||||
import { projectAnalysisError } from "../analysisErrorProjection.js";
|
||||
import {
|
||||
analysisViewRequestSchema,
|
||||
projectAnalysisView,
|
||||
@@ -414,6 +415,9 @@ it("returns typed AnalysisResourceConstraintError when a single record exceeds 1
|
||||
if (result.ok) throw new Error("Expected resource error");
|
||||
expect(result.error._tag).toBe("AnalysisResourceConstraintError");
|
||||
expect(result.error.message).toContain("byte budget");
|
||||
expect(projectAnalysisError(result.error).remediation.action).toContain(
|
||||
"select fewer fields",
|
||||
);
|
||||
});
|
||||
|
||||
it.each([undefined, ["call:fetch"]])(
|
||||
|
||||
@@ -19,7 +19,10 @@ import {
|
||||
completeWithinViewCoverage,
|
||||
pageViewCoverage,
|
||||
} from "./analysisViewCoverage.js";
|
||||
import { analysisViewInputError } from "./analysisViewErrors.js";
|
||||
import {
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
analysisViewInputError,
|
||||
} from "./analysisViewErrors.js";
|
||||
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
|
||||
|
||||
const PAGE_BYTES = 1024 * 1024;
|
||||
@@ -187,6 +190,7 @@ export const projectWebBundleView = (
|
||||
limit_bytes: PAGE_BYTES,
|
||||
collection,
|
||||
},
|
||||
ANALYSIS_VIEW_PAGE_BUDGET_RECOVERY,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
|
||||
cliTest(
|
||||
"recovers an oversized retained route page by selecting fields",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-route-budget-recovery-");
|
||||
const source = join(root, "app.js");
|
||||
const bundle = join(root, "parent.json");
|
||||
const route = `/api/${"a".repeat(1_100_000)}`;
|
||||
await writeFile(source, `router.push(${JSON.stringify(route)});\n`);
|
||||
const unpublished = await cli.run({
|
||||
arguments: [
|
||||
"analyze-javascript-application",
|
||||
root,
|
||||
"--detail",
|
||||
"summary",
|
||||
"--evidence_output",
|
||||
bundle,
|
||||
"--view",
|
||||
JSON.stringify({
|
||||
kind: "javascript-routes",
|
||||
collection: "routes",
|
||||
limit: 1,
|
||||
}),
|
||||
"--json",
|
||||
],
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
expect(unpublished.exitCode, unpublished.stderr).toBe(1);
|
||||
expect(unpublished.json).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
details: {
|
||||
partial_observation: {
|
||||
evidence_id: expect.stringMatching(/^ev_/),
|
||||
normalized_result: expect.any(Object),
|
||||
},
|
||||
},
|
||||
});
|
||||
const unpublishedError = unpublished.json as {
|
||||
remediation: { action: string };
|
||||
details: { partial_observation: unknown };
|
||||
};
|
||||
expect(unpublishedError.remediation.action).toContain(
|
||||
"details.partial_observation",
|
||||
);
|
||||
expect(
|
||||
JSON.stringify(unpublishedError.details.partial_observation),
|
||||
).toContain(route);
|
||||
await expect(readFile(bundle)).rejects.toMatchObject({ code: "ENOENT" });
|
||||
const analyzed = await cli.run({
|
||||
arguments: [
|
||||
"analyze-javascript-application",
|
||||
root,
|
||||
"--evidence_output",
|
||||
bundle,
|
||||
"--json",
|
||||
],
|
||||
timeoutMs: 60_000,
|
||||
});
|
||||
expect(analyzed.exitCode, analyzed.stderr).toBe(0);
|
||||
const request = (fields?: readonly string[]) =>
|
||||
JSON.stringify({
|
||||
source: { kind: "evidence-file", path: bundle },
|
||||
view: {
|
||||
kind: "javascript-routes",
|
||||
collection: "routes",
|
||||
limit: 1,
|
||||
...(fields === undefined ? {} : { fields }),
|
||||
},
|
||||
});
|
||||
const wide = await cli.run({
|
||||
arguments: ["inspect-analysis-view", request(), "--json"],
|
||||
});
|
||||
expect(wide.exitCode).toBe(1);
|
||||
expect(wide.json).toMatchObject({
|
||||
code: "resource_constraint",
|
||||
retryable: false,
|
||||
details: { reported_limits: { limit_bytes: 1024 * 1024 } },
|
||||
});
|
||||
const wideError = wide.json as { remediation: { action: string } };
|
||||
expect(wideError.remediation.action).toContain("select fewer fields");
|
||||
expect(wideError.remediation.action).toContain("no runtime override");
|
||||
const narrow = await cli.run({
|
||||
arguments: [
|
||||
"inspect-analysis-view",
|
||||
request(["classification", "confidence"]),
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(narrow.exitCode, narrow.stderr).toBe(0);
|
||||
expect(narrow.json).toMatchObject({
|
||||
normalized_result: {
|
||||
items: [expect.not.objectContaining({ path: expect.anything() })],
|
||||
},
|
||||
});
|
||||
expect(await readFile(bundle, "utf8")).toContain(route);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user