fix(artifacts): compare PyInstaller declaration facts

Compare stable producer-tagged declarations independently of materialized payloads. Preserve missing facts as unknown without applying PyInstaller rules to foreign metadata or nested container entries.

Fixes #6484
Fixes #6485
This commit is contained in:
morluto
2026-10-23 18:55:31 +08:00
parent c70eefad50
commit 142cf188a7
4 changed files with 457 additions and 1 deletions
+9
View File
@@ -50,6 +50,15 @@ table index; original names remain available in the supplied artifact.
Path failures use bounded escaped previews and report the original length;
accepted paths and retained source names are not shortened.
`compare_artifacts` compares these node-less records as declarations using the
container, container name, source name and type code. Equal option or dependency
declarations can therefore compare unchanged without implying that an external
dependency's bytes were found. For materialized PyInstaller members, the same
fields participate in the metadata delta independently of content identity;
archive offsets and table positions remain location facts, not declaration
identity. Missing declaration facts and unavailable non-metadata content remain
unknown.
Request `detail: "summary"` for a compact inspection, then query retained
Evidence with `inspect_analysis_view`. An inventory view can select
`collection: "occurrences"`, `entry_kinds: ["metadata"]`, exact logical paths or
+80 -1
View File
@@ -14,6 +14,7 @@ import {
parseArtifactInventoryEvidence,
type InventorySet,
} from "./artifactInventoryEvidence.js";
import { pyInstallerEntryDeclaration } from "./artifactFormatMetadata.js";
import { prefixedDigestSchema } from "./../domain/digests.js";
import { canonicalJson } from "./comparisonSemantics.js";
@@ -363,14 +364,84 @@ const classifyPath = (input: ClassifyPathInput): ArtifactChange | null => {
const base = buildArtifactChangeBase(input);
const integrity = classifyIntegrity(input, base);
if (integrity !== null) return integrity;
const declaration = classifyPyInstallerDeclaration(input, base);
if (declaration.handled) return declaration.change;
const availability = classifyAvailability(input, base);
if (availability !== null) return availability;
const dimensions = changedDimensions(input);
if (pyInstallerMetadataUnknown(input)) {
if (dimensions.length === 0)
return {
...base,
classification: "unknown",
dimensions: ["availability"],
};
dimensions.push("availability");
}
return dimensions.length === 0
? null
: { ...base, classification: "changed", dimensions };
};
const pyInstallerMetadataUnknown = (input: ClassifyPathInput): boolean =>
(isPyInstallerEntry(input.leftOccurrence) &&
!isPyInstallerEntry(input.rightOccurrence)) ||
(isPyInstallerEntry(input.rightOccurrence) &&
!isPyInstallerEntry(input.leftOccurrence));
const isPyInstallerEntry = (
occurrence: ArtifactOccurrence | undefined,
): occurrence is ArtifactOccurrence & {
readonly format_metadata: Extract<
NonNullable<ArtifactOccurrence["format_metadata"]>,
{ readonly kind: "pyinstaller-entry" }
>;
} => occurrence?.format_metadata?.kind === "pyinstaller-entry";
const classifyPyInstallerDeclaration = (
input: ClassifyPathInput,
base: ReturnType<typeof buildArtifactChangeBase>,
): { readonly handled: boolean; readonly change: ArtifactChange | null } => {
const leftIsDeclaration = input.leftOccurrence?.entry_kind === "metadata";
const rightIsDeclaration = input.rightOccurrence?.entry_kind === "metadata";
if (!leftIsDeclaration && !rightIsDeclaration)
return { handled: false, change: null };
// Metadata is not inherently a PyInstaller declaration. Only apply this
// format-specific comparison when a producer explicitly identified one.
if (
!isPyInstallerEntry(input.leftOccurrence) &&
!isPyInstallerEntry(input.rightOccurrence)
)
return { handled: false, change: null };
if (!leftIsDeclaration || !rightIsDeclaration)
return { handled: false, change: null };
if (
!isPyInstallerEntry(input.leftOccurrence) ||
!isPyInstallerEntry(input.rightOccurrence)
)
return {
handled: true,
change: {
...base,
classification: "unknown",
dimensions: ["availability"],
},
};
const changed =
canonicalJson(
pyInstallerEntryDeclaration(input.leftOccurrence.format_metadata),
) !==
canonicalJson(
pyInstallerEntryDeclaration(input.rightOccurrence.format_metadata),
);
return {
handled: true,
change: changed
? { ...base, classification: "changed", dimensions: ["metadata"] }
: null,
};
};
const changedDimensions = (input: {
readonly leftNode: ArtifactNode | undefined;
readonly rightNode: ArtifactNode | undefined;
@@ -430,7 +501,15 @@ const metadataChanged = (
input.leftOccurrence?.encrypted !== input.rightOccurrence?.encrypted ||
JSON.stringify(left.limitations) !== JSON.stringify(right.limitations) ||
JSON.stringify(input.leftOccurrence?.limitations) !==
JSON.stringify(input.rightOccurrence?.limitations);
JSON.stringify(input.rightOccurrence?.limitations) ||
(isPyInstallerEntry(input.leftOccurrence) &&
isPyInstallerEntry(input.rightOccurrence) &&
canonicalJson(
pyInstallerEntryDeclaration(input.leftOccurrence.format_metadata),
) !==
canonicalJson(
pyInstallerEntryDeclaration(input.rightOccurrence.format_metadata),
));
const nodeFor = (
occurrence: ArtifactOccurrence | undefined,
+11
View File
@@ -88,6 +88,17 @@ export const pyInstallerEntryMetadataSchema = z.strictObject({
export type PyInstallerEntryMetadata = z.infer<
typeof pyInstallerEntryMetadataSchema
>;
/** Declaration identity that is stable across archive layout changes. */
export const pyInstallerEntryDeclaration = (
metadata: PyInstallerEntryMetadata,
) => ({
container: metadata.container,
container_name: metadata.container_name,
source_name: metadata.source_name,
type_code: metadata.type_code,
});
export const artifactFormatMetadataSchema = z.discriminatedUnion("kind", [
godotPckMetadataSchema,
godotPckEntryMetadataSchema,
+357
View File
@@ -3,6 +3,13 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js";
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
import { jsonValueSchema } from "../../../src/domain/jsonValue.js";
import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph.js";
import {
artifactEdgeId,
artifactGraphDigest,
artifactManifestId,
occurrenceIdForLocation,
} from "../../../src/domain/artifactIdentity.js";
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
import { ok as resultOk } from "../../../src/domain/result.js";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
@@ -24,6 +31,8 @@ import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js";
import { ARTIFACT_COMPARISON_EXAMPLE } from "../../../src/contracts/artifactComparisonExample.js";
import { createServer } from "../../../src/server/createServer.js";
import { observed } from "../../fixtures/analysisExecution.js";
import { cArchiveExecutable } from "../../../src/artifacts/pyinstaller/CArchive.fixture.js";
import { compareUnicodeCodePoints } from "../../../src/domain/unicodeCodePointOrder.js";
const integrityMessage = (unpacked: boolean): string =>
`${
@@ -660,6 +669,354 @@ it("compares summary Evidence through retained complete inspection parents", asy
const toolEvidence = (value: unknown) => parseEvidence(value);
const pyInstallerComparisonArchive = (
typeCode: "m" | "s",
includeSymlink: boolean,
includePrefix = false,
) =>
cArchiveExecutable({
layout: "2.1",
pythonVersion: 309,
members: [
...(includePrefix
? [
{
name: "prefix.bin",
typeCode: "x" as const,
payload: Buffer.from("prefix bytes"),
compress: false,
},
]
: []),
{
name: "helper",
typeCode,
payload: Buffer.from("same helper bytes"),
compress: false,
},
{
name: "pyi-contents-directory _internal",
typeCode: "o",
payload: Buffer.alloc(0),
compress: false,
},
{
name: "shared-app:assets/data.bin",
typeCode: "d",
payload: Buffer.alloc(0),
compress: false,
},
...(includeSymlink
? [
{
name: "unavailable-link",
typeCode: "n" as const,
payload: Buffer.from("target"),
compress: false,
},
]
: []),
],
});
const inspectArtifactInventoryThroughMcp = async (
client: Client,
path: string,
) => {
const opened = await client.callTool({
name: "open_binary",
arguments: { path },
});
expect(opened.isError).not.toBe(true);
const reply = await client.callTool({
name: "inspect_artifact",
arguments: {},
});
expect(reply.isError).not.toBe(true);
const inspection = artifactInspectionResultSchema.parse(
toolEvidence(reply.structuredContent).normalized_result,
);
const inventory = inspection.substeps.find(
({ operation }) => operation === "inventory_artifact",
)?.evidence;
if (inventory === undefined) throw new Error("missing inventory Evidence");
return parseEvidence(inventory);
};
const compareArtifactEvidenceThroughMcp = (
client: Client,
left: Record<string, unknown>,
right: Record<string, unknown>,
) => client.callTool({ name: "compare_artifacts", arguments: { left, right } });
const withForeignMetadataAndNestedZipMember = (
evidence: ReturnType<typeof parseEvidence>,
) => {
const inventory = artifactInventoryResultSchema.parse(
evidence.normalized_result,
);
const helper = inventory.occurrences.find(
({ logical_path }) => logical_path === "helper",
);
if (helper === undefined) throw new Error("missing helper occurrence");
const { format_metadata: _formatMetadata, ...helperWithoutMetadata } = helper;
const foreignMetadataId = occurrenceIdForLocation({
rootArtifactId: inventory.manifest.root_artifact_id,
logicalPath: "foreign-meta",
entryKind: "metadata",
});
const foreignMetadata = {
...helperWithoutMetadata,
occurrence_id: foreignMetadataId,
logical_path: "foreign-meta",
entry_kind: "metadata" as const,
artifact_format: "zip" as const,
};
const nestedZipMember = {
...helperWithoutMetadata,
occurrence_id: occurrenceIdForLocation({
rootArtifactId: inventory.manifest.root_artifact_id,
logicalPath: "helper/member.txt",
entryKind: "file",
}),
parent_occurrence_id: helper.occurrence_id,
logical_path: "helper/member.txt",
artifact_format: "zip" as const,
};
const occurrences = inventory.occurrences
.concat(foreignMetadata, nestedZipMember)
.sort((left, right) =>
compareUnicodeCodePoints(left.logical_path, right.logical_path),
);
const edges = inventory.edges
.map((edge) => {
const occurrence = occurrences.find(
({ occurrence_id }) => occurrence_id === edge.occurrence_id,
);
return occurrence === undefined
? edge
: { ...edge, edge_id: artifactEdgeId(edge, occurrence) };
})
.sort((left, right) =>
compareUnicodeCodePoints(left.edge_id, right.edge_id),
);
const graphSha256 = artifactGraphDigest({
nodes: inventory.nodes,
occurrences,
edges,
contradictions: inventory.integrity_contradictions,
});
const manifestId = artifactManifestId(
inventory.manifest.root_artifact_id,
graphSha256,
);
const subject = evidence.subject;
if (subject === null) throw new Error("missing inventory subject");
return createEvidence(
{
path: subject.local_path,
sha256: subject.digest.sha256,
format: subject.format,
},
evidence.provider,
{
operation: "inventory_artifact",
parameters: evidence.parameters,
confidence: evidence.confidence,
authority: evidence.authority,
result: jsonValueSchema.parse({
...inventory,
occurrences,
edges,
manifest: {
...inventory.manifest,
occurrence_count: occurrences.length,
edge_count: edges.length,
graph_sha256: graphSha256,
manifest_id: manifestId,
},
}),
},
);
};
it("does not apply PyInstaller declaration unknowns to foreign metadata or nested ZIP members", async () => {
const root = await createTestTempDirectory("rea-pyinstaller-scope-mcp-");
const archive = join(root, "sample");
await writeFile(archive, pyInstallerComparisonArchive("m", false));
const session = createTestBinarySession(new ArtifactProvider(process.env));
const server = createServer({ kind: "session", session });
const client = new Client({ name: "pyinstaller-scope-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const observed = await inspectArtifactInventoryThroughMcp(client, archive);
const foreign = withForeignMetadataAndNestedZipMember(observed);
const response = await compareArtifactEvidenceThroughMcp(
client,
foreign,
foreign,
);
expect(response.isError).not.toBe(true);
expect(
toolEvidence(response.structuredContent).normalized_result,
).toMatchObject({
status: "unchanged",
summary: { unknown: 0 },
changes: [],
});
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
});
it("compares PyInstaller declaration facts without inventing payload identity", async () => {
const root = await createTestTempDirectory("rea-pyinstaller-compare-mcp-");
const leftPath = join(root, "module");
const rightPath = join(root, "script");
const declarationsPath = join(root, "declarations");
const modulePath = join(root, "module-clean");
const shiftedPath = join(root, "module-shifted");
await Promise.all([
writeFile(leftPath, pyInstallerComparisonArchive("m", true)),
writeFile(rightPath, pyInstallerComparisonArchive("s", true)),
writeFile(declarationsPath, pyInstallerComparisonArchive("s", false)),
writeFile(modulePath, pyInstallerComparisonArchive("m", false)),
writeFile(shiftedPath, pyInstallerComparisonArchive("m", false, true)),
]);
const session = createTestBinarySession(new ArtifactProvider(process.env));
const server = createServer({ kind: "session", session });
const client = new Client({ name: "pyinstaller-compare-test", version: "1" });
const [clientTransport, serverTransport] =
InMemoryTransport.createLinkedPair();
try {
await server.connect(serverTransport);
await client.connect(clientTransport);
const left = await inspectArtifactInventoryThroughMcp(client, leftPath);
const right = await inspectArtifactInventoryThroughMcp(client, rightPath);
const clean = await inspectArtifactInventoryThroughMcp(
client,
declarationsPath,
);
const module = await inspectArtifactInventoryThroughMcp(client, modulePath);
const shifted = await inspectArtifactInventoryThroughMcp(
client,
shiftedPath,
);
const leftInventory = artifactInventoryResultSchema.parse(
left.normalized_result,
);
const rightInventory = artifactInventoryResultSchema.parse(
right.normalized_result,
);
const leftHelper = leftInventory.occurrences.find(
({ logical_path }) => logical_path === "helper",
);
const rightHelper = rightInventory.occurrences.find(
({ logical_path }) => logical_path === "helper",
);
expect(leftHelper?.artifact_id).toBe(rightHelper?.artifact_id);
expect(leftHelper?.format_metadata).toMatchObject({ type_code: "m" });
expect(rightHelper?.format_metadata).toMatchObject({ type_code: "s" });
expect(
leftInventory.occurrences.filter(
({ entry_kind }) => entry_kind === "metadata",
),
).toHaveLength(2);
const changed = await compareArtifactEvidenceThroughMcp(
client,
left,
right,
);
expect(changed.isError).not.toBe(true);
const changedResult = toolEvidence(
changed.structuredContent,
).normalized_result;
expect(changedResult).toMatchObject({
status: "unknown",
summary: { changed: 2, unchanged: 2, unknown: 1 },
changes: expect.arrayContaining([
expect.objectContaining({
logical_path: ".",
classification: "changed",
dimensions: expect.arrayContaining(["content"]),
}),
expect.objectContaining({
logical_path: "helper",
classification: "changed",
dimensions: ["metadata"],
left_occurrence_id: leftHelper?.occurrence_id,
right_occurrence_id: rightHelper?.occurrence_id,
}),
expect.objectContaining({
logical_path: "unavailable-link",
classification: "unknown",
dimensions: ["availability"],
}),
]),
});
expect(
z
.object({ changes: z.array(z.object({ logical_path: z.string() })) })
.parse(changedResult)
.changes.map(({ logical_path }) => logical_path),
).not.toContain("__pyinstaller__/runtime-options/1");
expect(
z
.object({ changes: z.array(z.object({ logical_path: z.string() })) })
.parse(changedResult)
.changes.map(({ logical_path }) => logical_path),
).not.toContain("__pyinstaller__/dependencies/2");
const declarationsOnly = await client.callTool({
name: "compare_artifacts",
arguments: { left: clean, right: clean },
});
expect(declarationsOnly.isError).not.toBe(true);
expect(
toolEvidence(declarationsOnly.structuredContent).normalized_result,
).toMatchObject({
status: "unchanged",
summary: { unchanged: 4, unknown: 0 },
changes: [],
});
const layoutOnly = await client.callTool({
name: "compare_artifacts",
arguments: { left: module, right: shifted },
});
expect(layoutOnly.isError).not.toBe(true);
const layoutChanges = z
.object({ changes: z.array(z.object({ logical_path: z.string() })) })
.parse(toolEvidence(layoutOnly.structuredContent).normalized_result)
.changes.map(({ logical_path }) => logical_path);
expect(layoutChanges).toContain("prefix.bin");
expect(layoutChanges).not.toContain("helper");
const unavailableControl = await client.callTool({
name: "compare_artifacts",
arguments: { left, right: left },
});
expect(unavailableControl.isError).not.toBe(true);
expect(
toolEvidence(unavailableControl.structuredContent).normalized_result,
).toMatchObject({
status: "unknown",
changes: [
expect.objectContaining({
logical_path: "unavailable-link",
classification: "unknown",
}),
],
});
} finally {
await Promise.allSettled([client.close(), server.close(), session.close()]);
}
});
it("discovers artifact target prerequisites and preserves typed MCP refusals", async () => {
const root = await createTestTempDirectory("rea-artifact-availability-mcp-");
const zip = new ZipWriter(new Uint8ArrayWriter());