fix(artifacts): preserve inventory uncertainty in application projections

This commit is contained in:
morluto committed 2026-10-13 07:52:54 +08:00
1 parent 951b095e11
commit d8ceb3f276
11 files changed
+116 -16

No files matched your search

+7 -3
View File
@@ -1,6 +1,9 @@
import { z } from "zod";
import { parseArtifactInventoryEvidence } from "../artifactInventoryEvidence.js";
import {
hasCompleteInventoryCoverage,
parseArtifactInventoryEvidence,
} from "../artifactInventoryEvidence.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
import { nativeRuntimeConvention } from "../nativeConvention.js";
@@ -62,6 +65,7 @@ export const androidApplicationProjectionResultSchema = z.strictObject({
bridge_candidate_coverage: bridgeCandidateCoverageSchema,
coverage: z.strictObject({
status: z.enum(["complete-within-inventory", "partial"]),
// Count completeness is separate from the declared coverage status.
inventory_complete: z.boolean(),
}),
limitations: z.array(z.string().min(1)),
@@ -95,8 +99,8 @@ export const projectAndroidApplication = (
);
const components = classified;
const limitations = [
...(!inventory.complete
? ["Source inventory pages are incomplete; absence is unknown."]
...(!hasCompleteInventoryCoverage(inventory)
? ["Source inventory coverage is partial; absence is unknown."]
: []),
"Manifest, resource, signing, and bytecode semantics require a dedicated Android provider; this projection reports exact inventory paths and hashes only.",
"Runtime families are inferred from inventory formats and paths; filename suffixes do not establish valid DEX or JVM class bytes.",
@@ -138,6 +138,7 @@ const inventoryEvidence = (
occurrences: sortedOccurrences,
edges: [],
provenance: [],
coverage_status: "complete",
limitations: [],
}),
},
+9 -4
View File
@@ -17,7 +17,10 @@ import {
isSwiftRuntimeLibrary,
nativeRuntimeConvention,
} from "../nativeConvention.js";
import { parseArtifactInventoryEvidence } from "../artifactInventoryEvidence.js";
import {
hasCompleteInventoryCoverage,
parseArtifactInventoryEvidence,
} from "../artifactInventoryEvidence.js";
import { evidenceSchema } from "../evidence.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
@@ -88,6 +91,7 @@ export const appleApplicationProjectionResultSchema = z.strictObject({
bridge_candidate_coverage: bridgeCandidateCoverageSchema,
coverage: z.strictObject({
status: z.enum(["complete-within-inventory", "partial"]),
// Count completeness is separate from the declared coverage status.
inventory_complete: z.boolean(),
}),
limitations: z.array(z.string().min(1)),
@@ -165,9 +169,10 @@ export const projectAppleApplication = (
);
const uninventoriedImage =
rootFormat === "dmg" && inventory.occurrences.length === 1;
const complete = inventory.complete && !uninventoriedImage;
const inventoryComplete = hasCompleteInventoryCoverage(inventory);
const complete = inventoryComplete && !uninventoriedImage;
const limitations = projectionLimitations({
complete: inventory.complete,
complete: inventoryComplete,
uninventoriedImage,
roots,
bundles,
@@ -390,7 +395,7 @@ const projectionLimitations = (facts: {
readonly bridgeCandidateCoverage: AppleApplicationProjectionResult["bridge_candidate_coverage"];
}): string[] => [
...(!facts.complete
? ["Source inventory pages are incomplete; absence is unknown."]
? ["Source inventory coverage is partial; absence is unknown."]
: []),
...(facts.uninventoriedImage
? [
+3 -4
View File
@@ -8,6 +8,7 @@ import type {
} from "./artifactGraph.js";
import { evidenceSchema } from "./evidence.js";
import {
hasCompleteInventoryCoverage,
parseArtifactInventoryEvidence,
type InventorySet,
} from "./artifactInventoryEvidence.js";
@@ -103,10 +104,8 @@ export const compareArtifacts = (
const right = parseArtifactInventoryEvidence(rightEvidence);
const leftComplete = left.inventory.complete;
const rightComplete = right.inventory.complete;
const leftCovered =
leftComplete && left.inventory.coverage_status === "complete";
const rightCovered =
rightComplete && right.inventory.coverage_status === "complete";
const leftCovered = hasCompleteInventoryCoverage(left.inventory);
const rightCovered = hasCompleteInventoryCoverage(right.inventory);
const limitations = [
...left.inventory.limitations.map((item) => `Left: ${item}`),
...right.inventory.limitations.map((item) => `Right: ${item}`),
+6
View File
@@ -28,10 +28,16 @@ export interface InventorySet {
readonly edges: readonly ArtifactInventoryResult["edges"][number][];
readonly integrityContradictions: readonly IntegrityContradiction[];
readonly limitations: readonly string[];
/** True when merged node, occurrence, and edge counts match the manifest. */
readonly complete: boolean;
readonly coverage_status: ArtifactInventoryCoverage;
}
/** Whether all graph members are present and the producer reports full coverage. */
export const hasCompleteInventoryCoverage = (
inventory: InventorySet,
): boolean => inventory.complete && inventory.coverage_status === "complete";
export interface ParsedInventorySet {
readonly evidence: readonly Evidence[];
readonly inventory: InventorySet;
+7 -3
View File
@@ -1,6 +1,9 @@
import { z } from "zod";
import { parseArtifactInventoryEvidence } from "../artifactInventoryEvidence.js";
import {
hasCompleteInventoryCoverage,
parseArtifactInventoryEvidence,
} from "../artifactInventoryEvidence.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
import { nativeRuntimeConvention } from "../nativeConvention.js";
@@ -66,6 +69,7 @@ export const harmonyApplicationProjectionResultSchema = z.strictObject({
bridge_candidate_coverage: bridgeCandidateCoverageSchema,
coverage: z.strictObject({
status: z.enum(["complete-within-inventory", "partial"]),
// Count completeness is separate from the declared coverage status.
inventory_complete: z.boolean(),
}),
limitations: z.array(z.string().min(1)),
@@ -108,8 +112,8 @@ export const projectHarmonyApplication = (
classified.native_libraries,
);
const limitations = [
...(!inventory.complete
? ["Source inventory pages are incomplete; absence is unknown."]
...(!hasCompleteInventoryCoverage(inventory)
? ["Source inventory coverage is partial; absence is unknown."]
: []),
...(unavailable.length > 0
? [
@@ -3,7 +3,10 @@ import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { z } from "zod";
import type { ParsedInventorySet } from "./artifactInventoryEvidence.js";
import {
hasCompleteInventoryCoverage,
type ParsedInventorySet,
} from "./artifactInventoryEvidence.js";
import { evidenceSchema } from "./evidence.js";
import { digestSchema } from "./digests.js";
import { prefixedDigestSchema } from "./digests.js";
@@ -49,10 +52,11 @@ export const projectionCoverage = (
budgetComplete: boolean,
): {
status: "complete-within-inventory" | "partial";
/** True when all manifest graph-member counts are present, independent of source coverage. */
inventory_complete: boolean;
} => ({
status:
inventory.complete && budgetComplete
hasCompleteInventoryCoverage(inventory) && budgetComplete
? ("complete-within-inventory" as const)
: ("partial" as const),
inventory_complete: inventory.complete,
@@ -227,6 +227,9 @@ describe("case-distinct APK resource names", () => {
const inventory = parseEvidence(
await runProviderAnalysis(path, "inventory_artifact", {}),
);
expect(inventory.normalized_result).toMatchObject({
coverage_status: "partial",
});
const result = projectAndroidApplicationEvidence({
inventory_evidence: [inventory],
});
@@ -234,6 +237,13 @@ describe("case-distinct APK resource names", () => {
requireSuccessfulProjection(result).normalized_result,
);
expect(projection.root_format).toBe("apk");
expect(projection.coverage).toEqual({
status: "partial",
inventory_complete: true,
});
expect(projection.limitations).toContain(
"Source inventory coverage is partial; absence is unknown.",
);
expect(projection.components.manifests).toHaveLength(1);
expect(projection.components.dex).toHaveLength(1);
expect(JSON.stringify(inventory)).toContain("res/2F.xml");
@@ -75,6 +75,42 @@ async function createCompleteAppleProjection() {
}
describe("Apple application projection", () => {
it("keeps a case-colliding real IPA inventory partial", async () => {
const root = await createTestTempDirectory("rea-apple-case-collision-");
const path = join(root, "CaseCollision.ipa");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add(
"Payload/Example.app/Contents/Info.plist",
new TextReader("upper"),
);
await writer.add(
"Payload/Example.app/contents/Info.plist",
new TextReader("lower"),
);
await writeFile(path, await writer.close());
const inventory = parseEvidence(
await runProviderAnalysis(path, "inventory_artifact", {}),
);
expect(inventory.normalized_result).toMatchObject({
coverage_status: "partial",
});
const result = projectAppleApplicationEvidence({
inventory_evidence: [inventory],
});
const projection = appleApplicationProjectionResultSchema.parse(
requireSuccessfulProjection(result).normalized_result,
);
expect(projection.coverage).toEqual({
status: "partial",
inventory_complete: true,
});
expect(projection.limitations).toContain(
"Source inventory coverage is partial; absence is unknown.",
);
});
it("projects deterministic IPA components and bridge hypotheses from exact inventory Evidence", async () => {
const root = await createTestTempDirectory("rea-apple-");
const path = join(root, "Fixture.ipa");
@@ -172,6 +172,9 @@ describe("artifact inventory coverage", () => {
authority: "shipped-artifact",
},
);
expect(parseArtifactInventoryEvidence(partial).inventory.complete).toBe(
true,
);
expect(compareArtifacts(partial, partial)).toMatchObject({
status: "unknown",
summary: { unchanged: 0, unknown: 1 },
@@ -214,6 +214,34 @@ describe("HarmonyOS packaging boundaries", () => {
});
describe("HarmonyOS inventory coverage", () => {
it("keeps a case-colliding inventory partial in the application projection", async () => {
const path = await writeApplicationZip(
"rea-harmony-case-collision-",
"Fixture.hap",
[
{ path: "module.json", content: '{"module":{}}' },
{ path: "MODULE.JSON", content: '{"module":{}}' },
],
);
const inventory = await inventoryApplicationPackage(path);
expect(inventory.normalized_result).toMatchObject({
coverage_status: "partial",
});
const projection = requireDeterministicProjection(
projectHarmonyApplicationEvidence,
inventory,
harmonyApplicationProjectionResultSchema,
);
expect(projection.coverage).toEqual({
status: "partial",
inventory_complete: true,
});
expect(projection.limitations).toContain(
"Source inventory coverage is partial; absence is unknown.",
);
});
it.each(ZIP_PACKAGE_FORMATS)(
"retains archive identity and ZIP coverage for %s",
async (format) => {