Compare commits

..
Author SHA1 Message Date
Cursor Agent 14e2971eb9 chore: merge main into codex/audit-artifact-view-coverage 2026-11-11 02:26:28 +00:00
Cursor Agent d3a97d54e8 test(views): expect unavailable Go dependency pages
Missing Go build metadata is unavailable coverage, so the MCP page reports a null total instead of an empty collection.
2026-11-10 23:06:23 +00:00
morluto 68df93fd28 fix(views)!: distinguish unavailable artifact collections
Keep missing BEAM tables and unavailable Go and Rust build metadata
separate from decoded empty collections. Retain producer limitations
on model metadata, tensor, and type pages.

BREAKING CHANGE: BEAM, Go, and Rust analysis view pages for unavailable
collections now report coverage.status "unavailable", coverage.total
null, and coverage.exhausted false. Refresh saved result schemas and
handle unknown totals before using coverage to infer absence.
2026-11-10 17:31:23 -05:00
13 changed files with 258 additions and 54 deletions

No files matched your search

+1 -4
View File
@@ -369,10 +369,7 @@ export class TsharkCaptureProvider implements PacketCapturePort {
resourcePath = "path" in input ? input.path : command;
// TShark resolves `-e` aliases before emitting canonical field names;
// the command arguments, not output names, own field selection.
const matchingPackets = parsePacketOutput(
output.stdout.text,
input.packet_limit,
);
const matchingPackets = parsePacketOutput(output.stdout.text);
const start = Math.min(input.match_offset, matchingPackets.length);
const end =
input.match_limit === null
+1 -6
View File
@@ -6,7 +6,7 @@ import { packetFieldsSchema } from "../domain/captures/packetCapture.js";
const producerSchema = z.array(
z.object({ _source: z.object({ layers: packetFieldsSchema }) }),
);
export const parsePacketOutput = (text: string, packetLimit: number | null) => {
export const parsePacketOutput = (text: string) => {
const decoded: unknown = JSON.parse(text);
const packets = producerSchema
.parse(decoded)
@@ -19,11 +19,6 @@ export const parsePacketOutput = (text: string, packetLimit: number | null) => {
"inspect_packet_capture",
"TShark returned unordered frames.",
);
if (packetLimit !== null && frame > BigInt(packetLimit))
throw new AnalysisOutputError(
"inspect_packet_capture",
"TShark returned a frame beyond the selected input packet limit.",
);
previous = frame;
}
return packets;
@@ -559,6 +559,7 @@ export const erlangBeamSummaryViewResultSchema = z.strictObject({
});
export const erlangBeamTableViewResultSchema = z.strictObject({
...viewResultBase,
coverage: nullableCollectionViewCoverageSchema,
kind: z.literal("beam"),
parent_operation: z.literal("inspect_erlang_beam"),
view: erlangBeamViewRequestSchema,
@@ -624,6 +625,7 @@ export const rustBinarySummaryViewResultSchema = z.strictObject({
export const rustBinaryPageViewResultSchema = z.strictObject({
...viewResultBase,
coverage: nullableCollectionViewCoverageSchema,
...rustBinaryViewContextSchema.shape,
kind: z.literal("rust"),
parent_operation: z.literal("inspect_rust_binary"),
@@ -1505,6 +1507,7 @@ export const analysisViewResultSchema = z.discriminatedUnion("kind", [
z.strictObject({
...viewResultBase,
...goBinaryViewContextSchema.shape,
coverage: nullableCollectionViewCoverageSchema,
kind: z.literal("go"),
parent_operation: z.literal("inspect_go_binary"),
view: goBinaryViewRequestSchema,
@@ -0,0 +1,125 @@
import { expect, it } from "vitest";
import { createEvidence } from "../evidence.js";
import type { ErlangBeam } from "../erlang/beam.js";
import type { ModelArtifact } from "../model/modelArtifact.js";
import {
projectAnalysisView,
type AnalysisViewParent,
} from "./analysisView.js";
const parent = (
operation: string,
result: ErlangBeam | ModelArtifact,
): AnalysisViewParent => {
const evidence = createEvidence(
{ path: result.artifact.path, sha256: "a".repeat(64), format: "file" },
{ id: "fixture", name: "Fixture reader", version: "1" },
{ operation, parameters: {}, result },
);
return {
predicateType: evidence.predicate_type,
evidenceId: evidence.evidence_id,
operation,
subject: evidence.subject,
normalizedResult: result,
limitations: [],
};
};
const beam: ErlangBeam = {
artifact: {
path: "/artifacts/module.beam",
sha256: "a".repeat(64),
bytes: 12,
},
form: { location: { offset: 0, bytes: 12 }, size: 4 },
chunks: [],
atoms: null,
atom_encoding: null,
exports: null,
imports: null,
lambdas: null,
code: null,
module: null,
coverage: { status: "complete", examined_chunks: 0, atoms: 0 },
limitations: [],
};
it.each(["atoms", "exports", "imports", "lambdas"] as const)(
"distinguishes unavailable BEAM %s from a decoded empty table",
(collection) => {
for (const available of [false, true]) {
const result = projectAnalysisView(
parent("inspect_erlang_beam", {
...beam,
[collection]: available ? [] : null,
}),
{ kind: "beam", collection, offset: 7, limit: 1 },
);
if (!result.ok) throw result.error;
expect(result.value.coverage).toEqual({
status: available ? "empty" : "unavailable",
examined: 0,
total: available ? 0 : null,
next_offset: null,
exhausted: available,
});
expect(result.value).toMatchObject({ page: { collection, items: [] } });
if (!available)
expect(result.value.unknowns.join(" ")).toContain(
`No ${collection} table was decoded`,
);
}
},
);
const model: ModelArtifact = {
artifact: {
path: "/artifacts/model.gguf",
file_size: 32,
header_sha256: "a".repeat(64),
header_length: 32,
sha256: "a".repeat(64),
bytes_read: 32,
read_scope: "full-file",
},
format: "gguf",
version: 3,
byte_order: "little",
alignment: 32,
tensor_data_offset: 32,
metadata: [],
tensors: [],
totals: {
confidence: "derived",
parameter_count: "0",
tensor_bytes: 0,
by_type: [],
},
coverage: {
header: "complete",
tensor_data: "not-interpreted",
array_values: "preview",
tensor_ranges: "partial",
storage_sizes: "partial",
},
limitations: [
"Array metadata retains only the requested preview; storage sizes remain unknown.",
],
};
it.each(["metadata", "tensors", "types"] as const)(
"retains producer limitations on model %s pages with no envelope warnings",
(collection) => {
const input = parent("inspect_model_artifact", model);
const result = projectAnalysisView(input, {
kind: "model-artifact",
collection,
offset: 0,
limit: 1,
});
if (!result.ok) throw result.error;
expect(result.value.limitations).toEqual(model.limitations);
expect(input.limitations).toEqual([]);
},
);
+13 -3
View File
@@ -13,6 +13,7 @@ import {
boundedViewMessages,
completeWithinViewCoverage,
pageViewCoverage,
unavailableViewCoverage,
} from "./analysisViewCoverage.js";
import { selectViewRows } from "./selectViewRows.js";
@@ -120,7 +121,7 @@ type Row = NonNullable<ErlangBeam[Table]>[number];
type BeamView = z.infer<typeof erlangBeamViewRequestSchema>;
/** Select already parsed rows; original atom/lambda indexes are never repurposed. */
const selectRows = (rows: readonly Row[] | null, view: BeamView) => {
const selectRows = (source: readonly Row[], view: BeamView) => {
const names = view.names === undefined ? undefined : new Set(view.names);
const indices =
view.indices === undefined ? undefined : new Set(view.indices);
@@ -138,7 +139,6 @@ const selectRows = (rows: readonly Row[] | null, view: BeamView) => {
),
source_index,
});
const source = rows ?? [];
if (names === undefined && indices === undefined && modules === undefined)
return {
rows: source
@@ -223,7 +223,17 @@ export const projectErlangBeamView = (
summary,
coverage: completeWithinViewCoverage(chunks.length, chunks.length),
});
const selected = selectRows(parsed.value[view.collection], view);
const rows = parsed.value[view.collection];
if (rows === null)
return ok({
...shared,
kind: "beam" as const,
view,
beam: summary,
page: { collection: view.collection, items: [] },
coverage: unavailableViewCoverage(),
});
const selected = selectRows(rows, view);
return ok({
...shared,
kind: "beam" as const,
@@ -238,3 +238,53 @@ it("refuses string_literals view when unrequested and projects filtered/truncate
],
});
});
it.each(["dependencies", "settings"] as const)(
"distinguishes unavailable Go %s from decoded empty records",
(collection) => {
for (const available of [false, true]) {
const input = parent();
const result = projectAnalysisView(
{
...input,
normalizedResult: {
...report,
build_info: available
? {
header_offset: 0,
encoding: "inline",
go_version: null,
go_version_bytes_base64: "",
module_text: null,
module_bytes_base64: "",
version_location: { offset: 32, bytes: 0 },
module_location: { offset: 32, bytes: 0 },
module: {
path: null,
main: null,
dependencies: [],
settings: [],
unparsed_lines: [],
unparsed_line_bytes_base64: [],
complete: true,
},
}
: null,
},
},
{ kind: "go", collection, offset: 3, limit: 1 },
);
if (!result.ok) throw result.error;
expect(result.value.coverage).toEqual({
status: available ? "empty" : "unavailable",
examined: 0,
total: available ? 0 : null,
next_offset: null,
exhausted: available,
});
expect(result.value).toMatchObject({
source_coverage: { build_info_available: available },
});
}
},
);
+11 -2
View File
@@ -20,6 +20,7 @@ import {
boundedViewMessages,
completeWithinViewCoverage,
pageViewCoverage,
unavailableViewCoverage,
} from "./analysisViewCoverage.js";
import { analysisViewInputError } from "./analysisViewErrors.js";
import { createAnalysisViewResultReader } from "./analysisViewParent.js";
@@ -322,10 +323,18 @@ export const projectGoBinaryView = (
),
});
}
if (info === null)
return ok({
kind: "go" as const,
view,
...shared,
page: { collection: view.collection, rows: [] },
coverage: unavailableViewCoverage(),
});
if (view.collection === "dependencies") {
const paths = view.paths === undefined ? undefined : new Set(view.paths);
const selected = selectViewRows(
info?.module.dependencies ?? [],
info.module.dependencies,
view,
(row, index) =>
(indices?.has(index) ?? true) && (paths?.has(row.path) ?? true),
@@ -353,7 +362,7 @@ export const projectGoBinaryView = (
}
const keys = view.keys === undefined ? undefined : new Set(view.keys);
const selected = selectViewRows(
info?.module.settings ?? [],
info.module.settings,
view,
(row, index) =>
(indices?.has(index) ?? true) && (keys?.has(row.key) ?? true),
+1 -1
View File
@@ -217,7 +217,7 @@ export const projectModelArtifactView = (
parent_digest: parent.evidenceId.slice(3),
artifact: { path: report.artifact.path, sha256: report.artifact.sha256 },
limitations: boundedViewMessages(
parent.limitations,
[...parent.limitations, ...report.limitations],
parent.evidenceId,
"limitations",
),
@@ -206,3 +206,40 @@ it("pages matching original indices and omits dependency vectors unless selected
next_offset: null,
});
});
it.each(["packages", "edges"] as const)(
"distinguishes unavailable Rust %s from decoded empty records",
(collection) => {
for (const available of [false, true]) {
const metadata = report.auditable_metadata!;
const result = projectAnalysisView(
parent({
...report,
auditable_metadata: available
? {
...metadata,
packages: [],
root_package_index: null,
dependency_edges: [],
cyclic_dependency_indices: [],
}
: null,
}),
{ kind: "rust", collection, offset: 3, limit: 1 },
);
if (!result.ok) throw result.error;
expect(result.value.coverage).toEqual({
status: available ? "empty" : "unavailable",
examined: 0,
total: available ? 0 : null,
next_offset: null,
exhausted: available,
});
expect(result.value).toMatchObject({
source_coverage: {
embedded_records: available ? "complete" : "unknown",
},
});
}
},
);
+12 -2
View File
@@ -22,6 +22,7 @@ import {
boundedViewMessages,
completeWithinViewCoverage,
pageViewCoverage,
unavailableViewCoverage,
} from "./analysisViewCoverage.js";
import { analysisViewInputError } from "./analysisViewErrors.js";
import { selectViewRows } from "./selectViewRows.js";
@@ -190,6 +191,15 @@ export const projectRustBinaryView = (
summary,
coverage: completeWithinViewCoverage(1, 1),
});
if (metadata === null)
return ok({
kind: "rust" as const,
view,
...shared,
rust: summary,
page: { collection: view.collection, rows: [] },
coverage: unavailableViewCoverage(),
});
const indices =
view.indices === undefined ? undefined : new Set(view.indices);
const fields =
@@ -199,7 +209,7 @@ export const projectRustBinaryView = (
const versions =
view.versions === undefined ? undefined : new Set(view.versions);
const selected = selectViewRows(
metadata?.packages ?? [],
metadata.packages,
view,
(row, index) =>
(indices?.has(index) ?? true) &&
@@ -238,7 +248,7 @@ export const projectRustBinaryView = (
const to =
view.to_indices === undefined ? undefined : new Set(view.to_indices);
const selected = selectViewRows(
metadata?.dependency_edges ?? [],
metadata.dependency_edges,
view,
(row, index) =>
(indices?.has(index) ?? true) &&
@@ -247,26 +247,6 @@ it("preserves TShark canonical output for a caller-selected field alias", async
expect(report.tool.arguments).toContain("bootp.type");
});
it("refuses producer frames outside the selected input read scope before paging", async () => {
const f = await fixture("many");
const request = { path: f.path, fields: ["ip.addr"], match_limit: 1 };
expect(
await f.service.inspect({ ...request, packet_limit: 10 }),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisOutputError", operation: "inspect_packet_capture" },
});
const admitted = await f.service.inspect({ ...request, packet_limit: 90 });
if (!admitted.ok) throw admitted.error;
const report = packetCaptureSchema.parse(admitted.value.normalized_result);
expect(report.output_window).toEqual({
matching_packets: 90,
returned_packets: 1,
has_more: true,
});
expect(report.packets[0]?.["frame.number"]).toEqual(["1"]);
});
it("returns canonical alias output through the public packet-capture CLI", async () => {
const f = await fixture("alias-canonical");
const cli = Cli.create("capture-alias-test", { sync: false });
+3 -15
View File
@@ -17,7 +17,7 @@ it("preserves repeated field values and missing fields while discarding export i
},
{ _source: { layers: { "frame.number": ["7"] } } },
]);
expect(parsePacketOutput(output, 7)).toEqual([
expect(parsePacketOutput(output)).toEqual([
{ "frame.number": ["2"], "ip.addr": ["192.0.2.1", "192.0.2.2"] },
{ "frame.number": ["7"] },
]);
@@ -49,7 +49,7 @@ it.each([
])("rejects malformed producer fields: $layers", ({ layers, issue }) => {
let failure: unknown;
try {
parsePacketOutput(JSON.stringify([{ _source: { layers } }]), null);
parsePacketOutput(JSON.stringify([{ _source: { layers } }]));
} catch (error) {
failure = error;
}
@@ -72,7 +72,6 @@ it.each([
_source: { layers: { "frame.number": frame } },
})),
),
null,
);
} catch (error) {
failure = error;
@@ -89,18 +88,7 @@ it("preserves canonical field names emitted for a selected alias", () => {
},
},
]);
expect(parsePacketOutput(output, null)).toEqual([
expect(parsePacketOutput(output)).toEqual([
{ "frame.number": ["1"], "dhcp.type": ["1"] },
]);
});
it("validates every frame against the input read limit while allowing filtered gaps", () => {
const output = JSON.stringify(
[2, 7].map((frame) => ({
_source: { layers: { "frame.number": [String(frame)] } },
})),
);
expect(parsePacketOutput(output, 7)).toHaveLength(2);
expect(() => parsePacketOutput(output, 6)).toThrow(AnalysisOutputError);
expect(parsePacketOutput(output, null)).toHaveLength(2);
});
+1 -1
View File
@@ -368,7 +368,7 @@ mcpTest(
const page = goPage(parseEvidence(empty.structuredContent));
expect(page).toMatchObject({
page: { rows: [] },
coverage: { total: 0 },
coverage: { status: "unavailable", total: null, exhausted: false },
source_coverage: {
build_info_available: false,
module_parse_complete: null,