mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
fix(artifacts): preserve inventory uncertainty in application projections
This commit is contained in:
@@ -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: [],
|
||||
}),
|
||||
},
|
||||
|
||||
@@ -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
|
||||
? [
|
||||
|
||||
@@ -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}`),
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user