mirror of
https://github.com/morluto/rea.git
synced 2026-10-26 17:35:11 +00:00
fix(artifacts): report unknown inventory traversal in inspection coverage (#5867)
fix(artifacts): keep unknown inventory traversal out of complete inspection coverage Artifact inspection derived its coverage status only from always-zero omission counts, so an inventory substep with unknown container-entry traversal (for example, an omitted FIFO) yielded complete-within-substeps. Analysis-view summaries also exposed the inventory's typed source_coverage only for PCK and PyInstaller roots. Derive the inspection status from the inventory substep's coverage (unknown traversal is partial), and carry the producer's source_coverage plus the traversal-unknown caveat in every artifact summary. Hash-count fields keep their existing format gating. Fixes #5837 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d124809950
commit
ce1f4b1bba
4 files changed
+145
-14
No files matched your search
@@ -355,7 +355,9 @@ page size or export retained Evidence if the view is too large.
|
||||
Completed `inspect_artifact` and atomic `inventory_artifact` Evidence also
|
||||
support summary and inventory views. A summary reports the manifest's expected
|
||||
counts separately from available record counts, observed entry kinds, inventory
|
||||
Evidence IDs and inspection coverage. An inventory view selects `nodes`,
|
||||
Evidence IDs, the inventory producer's traversal `source_coverage` and
|
||||
inspection coverage. Inspection coverage is `partial` when that traversal
|
||||
coverage is unknown. An inventory view selects `nodes`,
|
||||
`occurrences`, `edges`, or `integrity_contradictions`, with `offset` and `limit`
|
||||
defaulting to 0 and 64. For occurrences, optional `logical_paths` matches exact
|
||||
recorded strings: omission selects all available records; `[]` selects none.
|
||||
|
||||
@@ -245,17 +245,14 @@ export const projectArtifactInventoryView = (
|
||||
inventory.occurrences.find((row) => row.logical_path === ".")
|
||||
?.format_metadata ?? null;
|
||||
const rootMetadata = source.rootMetadata;
|
||||
const includeCoverage =
|
||||
// Producer traversal coverage is the owner's fact for every format; missing
|
||||
// coverage would read as unknown, so every view carries it inline.
|
||||
const sourceCoverage = { source_coverage: inventory.coverage };
|
||||
const includeHashCounts =
|
||||
view.kind === "inventory" ||
|
||||
inventory.manifest.root_format === "godot-pck" ||
|
||||
inventory.manifest.root_format === "pyinstaller";
|
||||
const sourceCoverage = includeCoverage
|
||||
? { source_coverage: inventory.coverage }
|
||||
: {};
|
||||
const counts =
|
||||
includeCoverage || view.kind === "summary"
|
||||
? inventoryCounts(source)
|
||||
: undefined;
|
||||
const counts = includeHashCounts ? inventoryCounts(source) : undefined;
|
||||
const limitations = uniqueSorted([
|
||||
...parent.limitations,
|
||||
...inventory.limitations,
|
||||
@@ -272,13 +269,12 @@ export const projectArtifactInventoryView = (
|
||||
limitations,
|
||||
unknowns: [
|
||||
"Inventory does not establish application runtime behavior or complete format-specific analysis.",
|
||||
...(includeCoverage && inventory.coverage.status === "unknown"
|
||||
...(inventory.coverage.status === "unknown"
|
||||
? [
|
||||
"Producer traversal coverage is unknown; missing matches do not prove source absence.",
|
||||
]
|
||||
: []),
|
||||
...(includeCoverage &&
|
||||
counts !== undefined &&
|
||||
...(counts !== undefined &&
|
||||
(counts.hashes.unavailable > 0 || counts.hashes["not-hashed"] > 0)
|
||||
? [
|
||||
"Content hashes are unknown for some recorded files; missing node IDs do not prove content absence.",
|
||||
@@ -325,7 +321,7 @@ export const projectArtifactInventoryView = (
|
||||
? {}
|
||||
: { metadata: counts.kinds.metadata }),
|
||||
},
|
||||
...(includeCoverage
|
||||
...(includeHashCounts
|
||||
? { available_hash_status_counts: counts.hashes }
|
||||
: {}),
|
||||
source_complete: inventory.complete,
|
||||
|
||||
@@ -427,10 +427,13 @@ const branch = (
|
||||
};
|
||||
};
|
||||
|
||||
// A completed substep establishes only the coverage its producer examined, so
|
||||
// unknown inventory traversal keeps the inspection partial, never complete.
|
||||
const inspectionCoverage = (
|
||||
_inventory: ArtifactInventoryResult,
|
||||
inventory: ArtifactInventoryResult,
|
||||
omissions: Readonly<Record<string, number>>,
|
||||
): ArtifactInspectionResult["coverage"]["status"] => {
|
||||
if (Object.values(omissions).some((count) => count > 0)) return "truncated";
|
||||
if (inventory.coverage.status !== "complete") return "partial";
|
||||
return "complete-within-substeps";
|
||||
};
|
||||
@@ -0,0 +1,130 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { mkdir, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { createArtifactBinarySession } from "../../../src/composition/binary.js";
|
||||
import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js";
|
||||
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
|
||||
import { parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createServer } from "../../../src/server/createServer.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
|
||||
const TRAVERSAL_UNKNOWN =
|
||||
"Producer traversal coverage is unknown; missing matches do not prove source absence.";
|
||||
|
||||
const writeBundle = async (withFifo: boolean): Promise<string> => {
|
||||
const root = await createTestTempDirectory("rea-inspection-coverage-");
|
||||
const bundle = join(root, "Fixture.app");
|
||||
await mkdir(bundle);
|
||||
await writeFile(
|
||||
join(bundle, "Info.plist"),
|
||||
"<plist><dict><key>CFBundleExecutable</key><string>main.js</string></dict></plist>",
|
||||
);
|
||||
await writeFile(join(bundle, "main.js"), "export const value = 1;\n");
|
||||
if (withFifo) execFileSync("mkfifo", [join(bundle, "omitted.fifo")]);
|
||||
return bundle;
|
||||
};
|
||||
|
||||
const connect = async () => {
|
||||
const session = await createArtifactBinarySession(process.env);
|
||||
const server = createServer({ kind: "session", session });
|
||||
const client = new Client({ name: "inspection-coverage", version: "1" });
|
||||
const [clientTransport, serverTransport] =
|
||||
InMemoryTransport.createLinkedPair();
|
||||
await server.connect(serverTransport);
|
||||
await client.connect(clientTransport);
|
||||
const definition = (await client.listTools()).tools.find(
|
||||
(tool) => tool.name === "inspect_analysis_view",
|
||||
);
|
||||
expect(definition?.outputSchema).toBeDefined();
|
||||
const outputSchema: Record<string, unknown> = definition!.outputSchema!;
|
||||
const viewOutput = new Ajv2020({
|
||||
strict: false,
|
||||
validateFormats: false,
|
||||
}).compile(outputSchema);
|
||||
return { client, server, viewOutput };
|
||||
};
|
||||
|
||||
describe.skipIf(process.platform === "win32")(
|
||||
"inspect_artifact traversal coverage",
|
||||
() => {
|
||||
it.each([
|
||||
{
|
||||
withFifo: true,
|
||||
status: "partial",
|
||||
sourceCoverage: { scope: "container-entries", status: "unknown" },
|
||||
},
|
||||
{
|
||||
withFifo: false,
|
||||
status: "complete-within-substeps",
|
||||
sourceCoverage: { scope: "container-entries", status: "complete" },
|
||||
},
|
||||
] as const)(
|
||||
"derives inspection and summary coverage from inventory traversal (FIFO: $withFifo)",
|
||||
async ({ withFifo, status, sourceCoverage }) => {
|
||||
const path = await writeBundle(withFifo);
|
||||
const { client, server, viewOutput } = await connect();
|
||||
try {
|
||||
const opened = await client.callTool({
|
||||
name: "open_binary",
|
||||
arguments: { path },
|
||||
});
|
||||
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
|
||||
const inspected = await client.callTool({
|
||||
name: "inspect_artifact",
|
||||
arguments: {},
|
||||
});
|
||||
expect(inspected.isError, JSON.stringify(inspected)).not.toBe(true);
|
||||
const parent = parseEvidence(inspected.structuredContent);
|
||||
const inspection = artifactInspectionResultSchema.parse(
|
||||
parent.normalized_result,
|
||||
);
|
||||
const inventory = inspection.substeps[0]!.evidence.normalized_result;
|
||||
expect(inventory.coverage).toEqual(sourceCoverage);
|
||||
expect(inspection.coverage.status).toBe(status);
|
||||
|
||||
const summaries = [
|
||||
await client.callTool({
|
||||
name: "inspect_analysis_view",
|
||||
arguments: {
|
||||
source: {
|
||||
kind: "retained-evidence",
|
||||
evidence_id: parent.evidence_id,
|
||||
},
|
||||
view: { kind: "summary" },
|
||||
},
|
||||
}),
|
||||
await client.callTool({
|
||||
name: "inspect_artifact",
|
||||
arguments: { detail: "summary" },
|
||||
}),
|
||||
];
|
||||
for (const reply of summaries) {
|
||||
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
||||
expect(
|
||||
viewOutput(reply.structuredContent),
|
||||
JSON.stringify(viewOutput.errors),
|
||||
).toBe(true);
|
||||
const view = analysisViewResultSchema.parse(
|
||||
parseEvidence(reply.structuredContent).normalized_result,
|
||||
);
|
||||
if (view.kind !== "summary" || !("source_complete" in view.summary))
|
||||
throw new Error("Expected an artifact inventory summary");
|
||||
expect(view.summary.source_complete).toBe(true);
|
||||
expect(view.summary.source_coverage).toEqual(sourceCoverage);
|
||||
expect(view.summary.inspection?.coverage.status).toBe(status);
|
||||
expect(view.unknowns.includes(TRAVERSAL_UNKNOWN)).toBe(withFifo);
|
||||
}
|
||||
} finally {
|
||||
await client.close();
|
||||
await server.close();
|
||||
}
|
||||
},
|
||||
30_000,
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user