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 278 additions and 153 deletions

No files matched your search

@@ -20,11 +20,6 @@ it("reads a Python 3.9 reference-flagged PYZ table", () => {
]);
});
it("reads marshal one-byte string tags as Latin-1", () => {
for (const hex of ["6101000000e9", "4101000000e9", "7a01e9", "5a01e9"])
expect(readMarshalSubset(bytes(hex))).toEqual({ ok: true, value: "é" });
});
it("decodes CPython marshal Unicode with surrogatepass", () => {
expect(
readMarshalSubset(
+8 -2
View File
@@ -168,8 +168,14 @@ const readAsciiBytes = (
): Result<string, PyInstallerFailure> => {
const bytes = readBytes(cursor, length);
if (!bytes.ok) return bytes;
// These tags use CPython's one-byte Unicode constructor, not an ASCII check.
return ok(bytes.value.toString("latin1"));
if (bytes.value.some((byte) => byte > 0x7f))
return err(
pyInstallerFailure(
"format",
"PYZ marshal ASCII string contains a non-ASCII byte",
),
);
return ok(bytes.value.toString("ascii"));
};
const readUtf8 = (
@@ -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) &&
-54
View File
@@ -407,30 +407,6 @@ it("charges long digits to the string budget and keeps large previews bounded",
);
});
it("reads marshal one-byte string tags as Latin-1 with Unicode identity", () => {
for (const tag of [0x61, 0x41, 0x7a, 0x5a]) {
const encoded = Buffer.concat([
Buffer.from([tag]),
tag === 0x61 || tag === 0x41 ? u32(1) : Buffer.from([1]),
Buffer.from([0xe9]),
]);
const sameUnicode = marshalUnicode(Buffer.from("é", "utf8"));
const set = Buffer.concat([
Buffer.from([0x3c]),
u32(2),
encoded,
sameUnicode,
]);
const report = root(parse(withConsts([encoded, set])));
expect(report.consts[0]).toMatchObject({
kind: "str",
value: "é",
bytes: 1,
});
expect(report.consts[1]).toEqual({ kind: "set", length: 1 });
}
});
it("decodes marshal strings with CPython surrogatepass rules", () => {
const report = parse(
withConsts([marshalUnicode(Buffer.from("efbbbf73eda080efbbbf", "hex"))]),
@@ -1558,36 +1534,6 @@ it("hashes a long TYPE_REF tuple chain without recursing", () => {
expect(pythonPycSchema.safeParse(report).success).toBe(true);
});
it("hashes shallow TYPE_REF code chains without using the JavaScript stack", () => {
const links = 3000;
const codes = Array.from({ length: links }, (_, index) =>
marshalCode("py311", {
flagged: true,
name: "f",
consts: index === 0 ? [Buffer.from([0x4e])] : [marshalRef(index - 1)],
}),
);
const bytes = Buffer.concat([
pythonPycHeader(3531),
marshalCode("py311", {
constsObject: Buffer.concat([
Buffer.from([0x28]),
u32(links + 1),
...codes,
Buffer.from([0x3c]),
u32(1),
marshalRef(links - 1),
]),
}),
]);
const report = parseWith(bytes, { max_objects: 65_536 });
expect(root(report).consts.at(-1)).toEqual({ kind: "set", length: 1 });
expect(report.coverage).toMatchObject({
status: "complete",
code_objects: links + 1,
});
});
it("rejects code CPython treats as corrupt instead of complete coverage", () => {
const file = (
layout: "py37" | "py38" | "py311",
+17 -83
View File
@@ -332,7 +332,7 @@ class MarshalReader {
readonly #codeKeys = new WeakMap<CodeNode, string>();
readonly #codeIdentities = new WeakMap<CodeNode, number>();
readonly #codeEquality = new WeakMap<CodeNode, CodeEquality>();
/** Completed Python hashability checks per code object. */
/** Python hashability per code object. False while that check is in progress. */
readonly #codeHashable = new WeakMap<CodeNode, boolean>();
/** Tuples whose items are still being read; CPython cannot hash them yet. */
readonly #readingTuples = new WeakSet<MarshalValue>();
@@ -436,19 +436,13 @@ class MarshalReader {
};
case TYPE_INTERNED:
case TYPE_UNICODE:
return this.string(
this.nonNegativeInt32("Marshal string length"),
"utf8",
);
return this.utf8(this.nonNegativeInt32("Marshal string length"), false);
case TYPE_ASCII:
case TYPE_ASCII_INTERNED:
return this.string(
this.nonNegativeInt32("Marshal string length"),
"latin1",
);
return this.utf8(this.nonNegativeInt32("Marshal string length"), true);
case TYPE_SHORT_ASCII:
case TYPE_SHORT_ASCII_INTERNED:
return this.string(this.u8(), "latin1");
return this.utf8(this.u8(), true);
case TYPE_TUPLE:
return this.sequenceValue(
"tuple",
@@ -1315,70 +1309,13 @@ class MarshalReader {
#isCodeHashable(node: CodeNode): boolean {
const known = this.#codeHashable.get(node);
if (known !== undefined) return known;
const frames: { value: MarshalValue; finish: boolean }[] = [
{ value: { kind: "code", value: node }, finish: false },
];
const gray = new Set<MarshalValue | CodeNode>();
const complete = new Set<MarshalValue>();
const reject = (): false => {
this.#codeHashable.set(node, false);
return false;
};
while (frames.length > 0) {
this.#takeKeyStep();
const frame = frames.pop()!;
const value = frame.value;
const identity = value.kind === "code" ? value.value : value;
if (frame.finish) {
gray.delete(identity);
if (value.kind === "code") {
const material =
this.#codeEquality.get(value.value) ??
malformed("Code object is missing equality material.");
// Child code checks are now complete, so the existing tuple key
// walker preserves its equality and expansion budgets without recursion.
const hashable =
this.#equalityKey(material.constsTuple, "hash") !== null;
this.#codeHashable.set(value.value, hashable);
if (!hashable) return reject();
} else complete.add(value);
continue;
}
if (value.kind === "code") {
const cached = this.#codeHashable.get(value.value);
if (cached === false) return reject();
if (cached === true) continue;
} else {
const cached = this.#keyCache.get(value);
if (cached === null) return reject();
if (cached !== undefined || complete.has(value)) continue;
}
if (
gray.has(identity) ||
this.#readingTuples.has(value) ||
value.kind === "list" ||
value.kind === "set" ||
value.kind === "dict" ||
value.kind === "null"
)
return reject();
const children =
value.kind === "code"
? [
(
this.#codeEquality.get(value.value) ??
malformed("Code object is missing equality material.")
).constsTuple,
]
: value.kind === "tuple" || value.kind === "frozenset"
? value.items
: [];
gray.add(identity);
frames.push({ value, finish: true });
for (let index = children.length - 1; index >= 0; index -= 1)
frames.push({ value: children[index]!, finish: false });
}
return true;
this.#codeHashable.set(node, false);
const material =
this.#codeEquality.get(node) ??
malformed("Code object is missing equality material.");
const hashable = this.#equalityKey(material.constsTuple, "hash") !== null;
this.#codeHashable.set(node, hashable);
return hashable;
}
#codeIdentity(node: CodeNode): number {
@@ -1555,20 +1492,17 @@ class MarshalReader {
);
}
string(length: number, encoding: "latin1" | "utf8"): MarshalValue {
utf8(length: number, ascii: boolean): MarshalValue {
const bytes = this.blob(length);
if (encoding === "latin1") {
// CPython reads the ASCII-tagged forms as one-byte Unicode, even when
// high bytes appear. Identity must agree with the UTF-8 string form.
const value = bytes.toString("latin1");
if (ascii) {
if (bytes.some((byte) => byte > 0x7f))
malformed("Marshal ASCII string contains a non-ASCII byte.");
return {
kind: "str",
value,
value: bytes.toString("latin1"),
previewEnd: Math.min(length, PYTHON_PYC_CONST_PREVIEW_LIMIT),
bytes: length,
raw: bytes.some((byte) => byte > 0x7f)
? Buffer.from(value, "utf8")
: bytes,
raw: bytes,
};
}
const decoded = decodeMarshalUtf8(bytes, PYTHON_PYC_CONST_PREVIEW_LIMIT);
+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,