Files
rea/src/domain/managed/managedApplicationGraph.ts
T
morluto b582b6ecb4 fix(managed): enforce module identity before joining token facts
Validate declared artifact and MVID scopes at managed Evidence admission, and reject conflicting known module declarations across graph inputs. Join boundary tokens to members only under matching known MVID scopes; preserve facts and report partial coverage when scope is unknown.

Prefer an observed MVID-bearing declaration with its supplying Evidence. Normalize GUID casing for module node identity while preserving original strings in observations.

Validation: 285 tests across 34 focused suites passed, followed by the final 12-test managed graph suite and managed CLI/MCP regressions. check:fast and architecture guards passed; no real native providers were run.
2026-10-11 02:52:11 +08:00

344 lines
12 KiB
TypeScript

import { createHash } from "node:crypto";
import canonicalize from "canonicalize";
import { z } from "zod";
import { evidenceSchema, type Evidence } from "../evidence.js";
import {
createJavaScriptApplicationGraph,
javascriptApplicationGraphSchema,
} from "../javascript/javascriptApplicationGraph.js";
import type {
ApplicationEdge,
ApplicationNode,
} from "../javascript/javascriptApplicationGraphSchemas.js";
import {
managedArtifactInspectionSchema,
managedMemberInspectionSchema,
managedNativeBoundaryInspectionSchema,
type ManagedArtifactInspection,
type ManagedMemberInspection,
type ManagedNativeBoundaryInspection,
} from "./managedArtifact.js";
import type { JsonValue } from "../jsonValue.js";
import {
assessManagedGraphOmissions,
managedGraphEvidenceCoverage,
managedGraphResultCoverage,
} from "./managedApplicationGraphCoverage.js";
import {
addArtifactNode,
addArtifactIdentityNodes,
addBoundaryNodes,
addMemberNodes,
} from "./managedApplicationGraphNodes.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
import { parseManagedInspectionEvidence } from "./managedInspectionEvidence.js";
const evidenceIdSchema = prefixedDigestSchema("ev");
const textSchema = z.string().min(1);
/** Authenticated managed Evidence records projected into the application graph. */
export const projectManagedApplicationGraphInputSchema = z
.strictObject({
managed_artifact: evidenceSchema.optional(),
managed_members: evidenceSchema.optional(),
managed_native_boundaries: evidenceSchema.optional(),
})
.superRefine((input, context) => {
if (
input.managed_artifact === undefined &&
input.managed_members === undefined &&
input.managed_native_boundaries === undefined
)
context.addIssue({
code: "custom",
message:
"At least one managed artifact, member, or native-boundary Evidence record is required",
});
});
/** Managed-code projection result containing a validated application graph. */
export const managedApplicationGraphResultSchema = z.strictObject({
projection_id: prefixedDigestSchema("magp"),
root_artifact_sha256: digestSchema,
source_evidence: z.strictObject({
managed_artifact_evidence_id: evidenceIdSchema.nullable(),
managed_members_evidence_id: evidenceIdSchema.nullable(),
managed_native_boundaries_evidence_id: evidenceIdSchema.nullable(),
}),
summary: z.strictObject({
graph_nodes: z.number().int().min(0),
graph_edges: z.number().int().min(0),
assemblies: z.number().int().min(0),
modules: z.number().int().min(0),
types: z.number().int().min(0),
methods: z.number().int().min(0),
fields: z.number().int().min(0),
pinvoke_imports: z.number().int().min(0),
native_implementations: z.number().int().min(0),
}),
graph: javascriptApplicationGraphSchema,
coverage: z.strictObject({
status: z.enum(["complete-within-inputs", "partial"]),
}),
evidence_links: z.array(evidenceIdSchema).min(1).max(3),
limitations: z.array(textSchema),
});
export type ProjectManagedApplicationGraphInput = z.infer<
typeof projectManagedApplicationGraphInputSchema
>;
export type ManagedApplicationGraphResult = z.infer<
typeof managedApplicationGraphResultSchema
>;
export interface ParsedManagedGraphInput {
readonly artifact: {
readonly evidence: Evidence;
readonly result: ManagedArtifactInspection;
} | null;
readonly members: {
readonly evidence: Evidence;
readonly result: ManagedMemberInspection;
} | null;
readonly boundaries: {
readonly evidence: Evidence;
readonly result: ManagedNativeBoundaryInspection;
} | null;
}
export interface GraphBuildState {
readonly nodes: ApplicationNode[];
readonly edges: ApplicationEdge[];
readonly artifactSha256: string;
readonly artifactPath: string;
readonly evidenceLinks: string[];
readonly typeNodes: Map<string, ApplicationNode>;
readonly methodNodes: Map<string, ApplicationNode>;
readonly fieldNodes: Map<string, ApplicationNode>;
}
const sha256 = (value: JsonValue): string => {
const serialized = canonicalize(value);
if (serialized === undefined)
throw new TypeError("Managed application graph canonicalization failed");
return createHash("sha256").update(serialized).digest("hex");
};
/** Project authenticated managed Evidence records into the application graph. */
export const projectManagedApplicationGraph = (
input: ProjectManagedApplicationGraphInput,
): ManagedApplicationGraphResult => {
const parsed = parseManagedInputs(input);
const artifactIdentity = chooseArtifact(parsed);
assertSameArtifact(parsed, artifactIdentity.sha256);
assertConsistentModuleIdentity(parsed);
const evidenceLinks = [
parsed.artifact?.evidence.evidence_id,
parsed.members?.evidence.evidence_id,
parsed.boundaries?.evidence.evidence_id,
].filter((id): id is string => id !== undefined);
const state: GraphBuildState = {
nodes: [],
edges: [],
artifactSha256: artifactIdentity.sha256,
artifactPath: artifactIdentity.path,
evidenceLinks,
typeNodes: new Map(),
methodNodes: new Map(),
fieldNodes: new Map(),
};
const artifactNode = addArtifactNode(state);
addArtifactIdentityNodes(state, artifactNode, parsed);
addMemberNodes(state, artifactNode, parsed);
addBoundaryNodes(state, artifactNode, parsed);
const omissions = omittedCounts(parsed);
const limitations = projectionLimitations(parsed, omissions);
const graph = createJavaScriptApplicationGraph({
schema: "JavaScriptApplicationGraph",
root_node_ids: [artifactNode.node_id],
nodes: state.nodes,
edges: state.edges,
coverage: managedGraphEvidenceCoverage(omissions),
limitations,
});
const withoutId = {
root_artifact_sha256: state.artifactSha256,
source_evidence: {
managed_artifact_evidence_id:
parsed.artifact?.evidence.evidence_id ?? null,
managed_members_evidence_id: parsed.members?.evidence.evidence_id ?? null,
managed_native_boundaries_evidence_id:
parsed.boundaries?.evidence.evidence_id ?? null,
},
summary: {
graph_nodes: graph.nodes.length,
graph_edges: graph.edges.length,
assemblies: countNodes(graph.nodes, "managed-assembly"),
modules: countNodes(graph.nodes, "managed-module"),
types: countNodes(graph.nodes, "managed-type"),
methods: countNodes(graph.nodes, "managed-method"),
fields: countNodes(graph.nodes, "managed-field"),
pinvoke_imports: countNodes(graph.nodes, "managed-pinvoke-import"),
native_implementations: countNodes(
graph.nodes,
"managed-native-implementation",
),
},
graph,
coverage: managedGraphResultCoverage(omissions),
evidence_links: evidenceLinks,
limitations,
};
return managedApplicationGraphResultSchema.parse({
...withoutId,
projection_id: `magp_${sha256(withoutId)}`,
});
};
const countNodes = (
nodes: readonly ApplicationNode[],
kind: ApplicationNode["kind"],
): number => nodes.filter((node) => node.kind === kind).length;
const parseManagedInputs = (
input: ProjectManagedApplicationGraphInput,
): ParsedManagedGraphInput => ({
artifact:
input.managed_artifact === undefined
? null
: parseManagedInspectionEvidence(
input.managed_artifact,
"inspect_managed_artifact",
managedArtifactInspectionSchema,
),
members:
input.managed_members === undefined
? null
: parseManagedInspectionEvidence(
input.managed_members,
"inspect_managed_members",
managedMemberInspectionSchema,
),
boundaries:
input.managed_native_boundaries === undefined
? null
: parseManagedInspectionEvidence(
input.managed_native_boundaries,
"inspect_managed_native_boundaries",
managedNativeBoundaryInspectionSchema,
),
});
const chooseArtifact = (
parsed: ParsedManagedGraphInput,
): { readonly sha256: string; readonly path: string } => {
const result =
parsed.artifact?.result ??
parsed.members?.result ??
parsed.boundaries?.result;
if (result === undefined)
throw new TypeError("Managed graph projection requires managed Evidence");
return { sha256: result.artifact.sha256, path: result.artifact.path };
};
const assertSameArtifact = (
parsed: ParsedManagedGraphInput,
sha256Value: string,
): void => {
for (const result of [
parsed.artifact?.result,
parsed.members?.result,
parsed.boundaries?.result,
])
if (result !== undefined && result.artifact.sha256 !== sha256Value)
throw new TypeError(
"Managed graph projection inputs must describe the same artifact SHA-256",
);
};
/** Reject contradictory known module identities while leaving null fields unknown. */
const assertConsistentModuleIdentity = (
parsed: ParsedManagedGraphInput,
): void => {
const mvidClaims = [
parsed.artifact?.result.module?.mvid,
parsed.members?.result.module?.mvid,
parsed.members?.result.identity_scope.requires_mvid,
parsed.boundaries?.result.module?.mvid,
parsed.boundaries?.result.identity_scope.requires_mvid,
]
.filter((mvid): mvid is string => mvid !== undefined && mvid !== null)
.map((mvid) => mvid.toLowerCase());
if (new Set(mvidClaims).size > 1)
throw new TypeError(
"Managed graph inputs report conflicting known module MVIDs or token scopes",
);
const modules = [
parsed.artifact?.result.module,
parsed.members?.result.module,
parsed.boundaries?.result.module,
].filter(
(module): module is NonNullable<ManagedArtifactInspection["module"]> =>
module !== undefined && module !== null,
);
for (const field of ["name", "generation", "token", "row_offset"] as const) {
const values = new Set(modules.map((module) => module[field]));
if (values.size > 1)
throw new TypeError(
`Managed graph inputs report conflicting module ${field} values`,
);
}
for (const field of ["enc_id", "enc_base_id"] as const) {
const values = new Set(
modules
.map((module) => module[field]?.toLowerCase())
.filter((value): value is string => value !== undefined),
);
if (values.size > 1)
throw new TypeError(
`Managed graph inputs report conflicting module ${field} values`,
);
}
};
const omittedCounts = (parsed: ParsedManagedGraphInput) =>
assessManagedGraphOmissions({
artifact: parsed.artifact?.result ?? null,
members: parsed.members?.result ?? null,
boundaries: parsed.boundaries?.result ?? null,
});
const projectionLimitations = (
parsed: ParsedManagedGraphInput,
omissions: ReturnType<typeof omittedCounts>,
): string[] => [
"Managed graph projection preserves static metadata observations only; it does not execute managed code, load assemblies, or map managed tokens to native addresses.",
...(parsed.members === null
? [
"Managed member Evidence was not supplied; type, method, and field nodes are absent.",
]
: []),
...(parsed.artifact === null
? [
"Managed artifact Evidence was not supplied; assembly identity observations are absent.",
]
: []),
...(parsed.boundaries === null
? [
"Managed native-boundary Evidence was not supplied; P/Invoke and native-implementation nodes are absent.",
]
: []),
...(omissions.partialInput
? ["At least one supplied managed Evidence record has partial coverage."]
: []),
...(omissions.unscopedTokenRelationships
? [
"Managed boundary tokens were not linked to member nodes because matching module MVID scope was not established.",
]
: []),
];