From c931ba43ab1379d12e2a3dc984f4e902bcc525b2 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sun, 15 Nov 2026 23:34:17 +0800 Subject: [PATCH] fix(managed): report a managed graph over an unavailable source as unknown (#11280) --- docs/guides/managed/managed-code-analysis.md | 5 ++ .../verify/architecture-guard-baselines.json | 2 +- src/domain/managed/managedApplicationGraph.ts | 13 +++- .../managedApplicationGraphCoverage.ts | 73 ++++++++++++------- src/domain/managed/managedArtifact.ts | 29 +++++++- .../managedApplicationGraph.test.ts | 40 ++++++++++ 6 files changed, 128 insertions(+), 34 deletions(-) diff --git a/docs/guides/managed/managed-code-analysis.md b/docs/guides/managed/managed-code-analysis.md index 6a926b265c..9df1841082 100644 --- a/docs/guides/managed/managed-code-analysis.md +++ b/docs/guides/managed/managed-code-analysis.md @@ -724,6 +724,11 @@ type selection. Limitations name each counted class, and graph coverage is incomplete. A missing `calls` edge is therefore not evidence that a method is never called. +A supplied inspection whose parser coverage is `unavailable` examined nothing, +so the graph cannot tell what it omits: the result's `coverage.status` and the +graph coverage are then `unknown`, even beside a known gap in another source. +`partial` is reserved for known gaps. + ## Evidence record shape Member comparison, application graph projection, and managed/native verification diff --git a/scripts/verify/architecture-guard-baselines.json b/scripts/verify/architecture-guard-baselines.json index e2721683dd..8828ea7364 100644 --- a/scripts/verify/architecture-guard-baselines.json +++ b/scripts/verify/architecture-guard-baselines.json @@ -458,7 +458,7 @@ "src/domain/javascript/sourceToBundleComparisonSchemas.ts": 1, "src/domain/javascript/staticObservedEndpointSchemas.ts": 1, "src/domain/jeb/jebAnalysis.ts": 1, - "src/domain/managed/managedArtifact.ts": 6, + "src/domain/managed/managedArtifact.ts": 4, "src/domain/managed/managedMemberComparison.ts": 3, "src/domain/managed/managedNativeVerificationSchemas.ts": 2, "src/domain/managed/managedReconstruction.ts": 1, diff --git a/src/domain/managed/managedApplicationGraph.ts b/src/domain/managed/managedApplicationGraph.ts index a114b1de2f..46e7f3a71f 100644 --- a/src/domain/managed/managedApplicationGraph.ts +++ b/src/domain/managed/managedApplicationGraph.ts @@ -118,7 +118,11 @@ export const managedApplicationGraphResultSchema = z }), graph: javascriptApplicationGraphSchema, coverage: z.strictObject({ - status: z.enum(["complete-within-inputs", "partial"]), + status: z + .enum(["complete-within-inputs", "partial", "unknown"]) + .describe( + "complete-within-inputs when every supplied managed Evidence record is complete and nothing was omitted; unknown when a supplied record is unavailable.", + ), }), evidence_links: z.array(evidenceIdSchema).min(1).max(3), limitations: z.array(nonEmptyTextSchema), @@ -428,9 +432,14 @@ const projectionLimitations = ( "Managed native-boundary Evidence was not supplied; P/Invoke and native-implementation nodes are absent.", ] : []), - ...(omissions.partialInput + ...(omissions.inputStates.includes("partial") ? ["At least one supplied managed Evidence record has partial coverage."] : []), + ...(omissions.inputStates.includes("unavailable") + ? [ + "At least one supplied managed Evidence record is unavailable; what the graph omits is unknown.", + ] + : []), ...(omissions.selectedTypes === null ? [] : [ diff --git a/src/domain/managed/managedApplicationGraphCoverage.ts b/src/domain/managed/managedApplicationGraphCoverage.ts index d7754f14c8..6e8a3ed65c 100644 --- a/src/domain/managed/managedApplicationGraphCoverage.ts +++ b/src/domain/managed/managedApplicationGraphCoverage.ts @@ -1,13 +1,14 @@ +import { joinCoverageStatus, type CoverageStatus } from "../coverage.js"; import type { ApplicationGraphEvidence } from "../javascript/javascriptApplicationEvidenceSchemas.js"; -import type { - ManagedArtifactInspection, - ManagedMemberInspection, - ManagedNativeBoundaryInspection, +import { + managedInspectionCoverage, + type ManagedArtifactInspection, + type ManagedInspectionState, + type ManagedMemberInspection, + type ManagedNativeBoundaryInspection, } from "./managedArtifact.js"; import { managedTokenScopesMatch } from "./managedInspectionEvidence.js"; -type ManagedCoverageState = "complete" | "partial" | "unavailable"; - /** Why a call site did not become a `calls` edge. */ export type OmittedCallReason = | "member-ref" @@ -44,9 +45,10 @@ export interface ManagedGraphCoverageSources { readonly boundaries: ManagedNativeBoundaryInspection | null; } -/** Whether any supplied source has incomplete coverage. */ +/** What the supplied sources and the projection left out. */ export interface ManagedGraphProjectionOmissions { - readonly partialInput: boolean; + /** Parser states of the supplied sources; absent sources are not listed. */ + readonly inputStates: readonly ManagedInspectionState[]; readonly unscopedTokenRelationships: boolean; /** Declaring types the member Evidence selected; null when it inspected every type. */ readonly selectedTypes: readonly string[] | null; @@ -56,9 +58,21 @@ export interface ManagedGraphProjectionOmissions { readonly relations: ManagedRelationOmissions | null; } +/** + * Coverage the supplied sources allow: an unavailable source examined nothing, + * so the graph cannot tell what it omits, even beside a known gap. + */ +const inputCoverage = ( + omissions: ManagedGraphProjectionOmissions, +): CoverageStatus => + joinCoverageStatus([ + "complete", + ...omissions.inputStates.map(managedInspectionCoverage), + ]); + /** Omissions whose size is unknown, unlike counted relation sites. */ const omitsUncounted = (omissions: ManagedGraphProjectionOmissions): boolean => - omissions.partialInput || + inputCoverage(omissions) !== "complete" || omissions.unscopedTokenRelationships || omissions.selectedTypes !== null || omissions.methodRows !== undefined; @@ -75,7 +89,7 @@ const completeCoverage = (): ApplicationGraphEvidence["coverage"] => ({ /** Preserve the source parser's coverage state on projected observations. */ export const managedSourceCoverage = ( - state: ManagedCoverageState, + state: ManagedInspectionState, ): ApplicationGraphEvidence["coverage"] => state === "complete" ? completeCoverage() @@ -103,11 +117,9 @@ export const assessManagedGraphOmissions = ( sources.members.identity_scope.requires_mvid, sources.boundaries.identity_scope.requires_mvid, ), - partialInput: [ - sources.members?.coverage.state, - sources.boundaries?.coverage.state, - sources.artifact?.coverage.state, - ].some((state) => state !== undefined && state !== "complete"), + inputStates: [sources.members, sources.boundaries, sources.artifact].flatMap( + (source) => (source === null ? [] : [source.coverage.state]), + ), selectedTypes: sources.members?.selection?.type_names ?? null, methodRows: sources.members === null || @@ -122,22 +134,27 @@ export const assessManagedGraphOmissions = ( export const managedGraphEvidenceCoverage = ( omissions: ManagedGraphProjectionOmissions, ): ApplicationGraphEvidence["coverage"] => - omitsAny(omissions) - ? { - status: "partial", - truncated: false, - omitted_count: omitsUncounted(omissions) - ? null - : omittedRelationSites(omissions.relations), - limits: [], - } - : completeCoverage(); + inputCoverage(omissions) === "unknown" + ? { status: "unknown", truncated: false, omitted_count: null, limits: [] } + : omitsAny(omissions) + ? { + status: "partial", + truncated: false, + omitted_count: omitsUncounted(omissions) + ? null + : omittedRelationSites(omissions.relations), + limits: [], + } + : completeCoverage(); /** Report whether all supplied source evidence was complete. */ export const managedGraphResultCoverage = ( omissions: ManagedGraphProjectionOmissions, ) => ({ - status: omitsAny(omissions) - ? ("partial" as const) - : ("complete-within-inputs" as const), + status: + inputCoverage(omissions) === "unknown" + ? ("unknown" as const) + : omitsAny(omissions) + ? ("partial" as const) + : ("complete-within-inputs" as const), }); diff --git a/src/domain/managed/managedArtifact.ts b/src/domain/managed/managedArtifact.ts index 77c0e18894..e90c77fd29 100644 --- a/src/domain/managed/managedArtifact.ts +++ b/src/domain/managed/managedArtifact.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import type { CoverageStatus } from "../coverage.js"; import { digestSchema } from "../digests.js"; import { managedFieldConstantSchema } from "./managedFieldConstant.js"; @@ -6,6 +7,28 @@ import { managedFieldConstantSchema } from "./managedFieldConstant.js"; export const managedMetadataTokenSchema = z.string().regex(/^0x[0-9a-f]{8}$/u); const offsetSchema = z.int().min(0); +/** + * Parser coverage of one managed inspection. `unavailable` means the parser + * examined nothing of that kind, unlike `partial`, which records a known gap. + */ +export const managedInspectionStateSchema = z.enum([ + "complete", + "partial", + "unavailable", +]); +export type ManagedInspectionState = z.infer< + typeof managedInspectionStateSchema +>; + +/** + * What a result derived from one managed inspection can claim about its + * coverage: an unavailable inspection examined nothing, so what it would cover + * is unknown rather than partially known. + */ +export const managedInspectionCoverage = ( + state: ManagedInspectionState, +): CoverageStatus => (state === "unavailable" ? "unknown" : state); + /** A 16-byte CLI metadata GUID without RFC 4122 version/variant restrictions. */ export const cliMetadataGuidSchema = z .string() @@ -510,7 +533,7 @@ export const managedArtifactInspectionSchema = z.object({ resources: z.array(managedResourceSchema), attributes: z.array(customAttributeSchema), coverage: z.object({ - state: z.enum(["complete", "partial", "unavailable"]), + state: managedInspectionStateSchema, issues: z.array(managedParseIssueSchema), }), limitations: z.array(z.string()), @@ -582,7 +605,7 @@ export const managedMemberInspectionSchema = z "Examined zero-based MethodDef row interval before declaring-type selection; null means the row scope was not recorded. Row coordinates are build-local and cannot prove cross-build member absence for a page.", ), coverage: z.object({ - state: z.enum(["complete", "partial", "unavailable"]), + state: managedInspectionStateSchema, issues: z.array(managedParseIssueSchema), }), limitations: z.array(z.string()), @@ -629,7 +652,7 @@ export const managedNativeBoundaryInspectionSchema = z.object({ mixed_mode_or_native_header: z.boolean().nullable(), }), coverage: z.object({ - state: z.enum(["complete", "partial", "unavailable"]), + state: managedInspectionStateSchema, issues: z.array(managedParseIssueSchema), }), limitations: z.array(z.string()), diff --git a/tests/composition/analysis-sessions/managedApplicationGraph.test.ts b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts index 7fe5799e07..ddc2175080 100644 --- a/tests/composition/analysis-sessions/managedApplicationGraph.test.ts +++ b/tests/composition/analysis-sessions/managedApplicationGraph.test.ts @@ -921,6 +921,46 @@ describe("managed application graph coverage", () => { } } }); + + it("reports an unavailable source as unknown, not partial", () => { + const bytes = buildManagedPeFixture(); + const binary = managedPeFixtureTarget(bytes, "/fixture/ManagedInterop.exe"); + const members = inspectManagedMembersBytes(bytes, binary); + const evidence = (state: "partial" | "unavailable") => + createEvidence(binary, MANAGED_STATIC_PROVIDER, { + operation: "inspect_managed_members", + parameters: {}, + result: { ...members, coverage: { state, issues: [] } }, + rawResult: null, + limitations: members.limitations, + }); + const project = (state: "partial" | "unavailable") => { + const projection = projectManagedApplicationGraphEvidence({ + managed_members: evidence(state), + }); + if (!projection.ok) throw new Error("projection failed"); + return managedApplicationGraphResultSchema.parse( + parseEvidence(projection.value).normalized_result, + ); + }; + + const unavailable = project("unavailable"); + expect(unavailable.coverage).toEqual({ status: "unknown" }); + expect(unavailable.graph.coverage).toEqual({ + status: "unknown", + truncated: false, + omitted_count: null, + limits: [], + }); + expect(unavailable.limitations).toContainEqual( + expect.stringMatching(/managed Evidence record is unavailable/iu), + ); + expect(unavailable.limitations).not.toContainEqual( + expect.stringMatching(/partial coverage/iu), + ); + // A known gap alone stays partial. + expect(project("partial").coverage).toEqual({ status: "partial" }); + }); }); describe("managed application graph type selection", () => {