refactor(analysis): remove caller-selected result caps

This commit is contained in:
morluto
2026-09-29 11:22:31 +08:00
parent f55c2b9267
commit a8c58c6605
23 changed files with 120 additions and 476 deletions
+7 -33
View File
@@ -255,18 +255,14 @@ def _containing_procedure(document, address):
return (procedure, None) if procedure is not None else (None, "not_in_procedure")
def _instruction_addresses(procedure, limit=None):
def _instruction_addresses(procedure):
result = []
seen = set()
segment = procedure.getSegment()
truncated = False
for block in procedure.basicBlockIterator():
address = block.getStartingAddress()
end = block.getEndingAddress()
while address < end and address not in seen:
if limit is not None and len(result) >= limit:
truncated = True
return result, truncated
seen.add(address)
instruction = segment.getInstructionAtAddress(address)
if instruction is None:
@@ -276,7 +272,7 @@ def _instruction_addresses(procedure, limit=None):
if length <= 0:
break
address += length
return result, truncated
return result
def _procedure_references(document, params):
@@ -284,7 +280,7 @@ def _procedure_references(document, params):
direction = params.get("direction", "outgoing")
if direction not in ("incoming", "outgoing"):
raise ValueError("direction must be incoming or outgoing")
addresses, _ = _instruction_addresses(procedure)
addresses = _instruction_addresses(procedure)
edges = set()
for address in addresses:
segment = _segment(document, address)
@@ -408,26 +404,6 @@ def _unavailable(reason):
return {"available": False, "reason": reason}
def _offset(params, name):
value = params.get(name, 0)
if not isinstance(value, int) or isinstance(value, bool) or value < 0:
raise ValueError("%s must be a non-negative integer" % name)
return value
def _bounded(items, offset, limit, total=None, scan_truncated=False):
selected = items[offset:offset + limit]
known_total = len(items) if total is None and not scan_truncated else total
has_more = offset + len(selected) < len(items)
return {
"items": selected,
"total": known_total,
"returned": len(selected),
"truncated": scan_truncated or has_more,
"next_offset": offset + len(selected) if has_more else None,
}
def _name_map(document):
result = {}
for segment in document.getSegmentsList():
@@ -454,8 +430,8 @@ def _render_instruction(segment, address):
)
def _assembly(procedure, limit=None):
"""Render bounded assembly while guarding against malformed instruction cycles."""
def _assembly(procedure):
"""Render assembly while guarding against malformed instruction cycles."""
lines = []
segment = procedure.getSegment()
seen = set()
@@ -470,8 +446,6 @@ def _assembly(procedure, limit=None):
line = _render_instruction(segment, address)
if line is not None:
lines.append(line)
if limit is not None and len(lines) >= limit:
return "\n".join(lines)
length = instruction.getInstructionLength()
if length <= 0:
break
@@ -482,7 +456,7 @@ def _assembly(procedure, limit=None):
def _read_function_instructions(document, params):
"""Read all raw instructions without invoking decompilation."""
procedure = _procedure(document, params.get("procedure"))
addresses, _ = _instruction_addresses(procedure)
addresses = _instruction_addresses(procedure)
segment = procedure.getSegment()
items = []
for address in addresses:
@@ -502,7 +476,7 @@ def _read_function_instructions(document, params):
def _analyze_function(document, params):
"""Collect the complete function dossier for agent callers."""
procedure = _procedure(document, params.get("procedure"))
addresses, instruction_scan_truncated = _instruction_addresses(procedure)
addresses = _instruction_addresses(procedure)
blocks = []
all_blocks = list(procedure.basicBlockIterator())
for block in all_blocks:
@@ -165,9 +165,6 @@ export const resolveCompareApplicationVersionsRequestValidated = (
...input.right_native_observations,
...rightNative.value,
],
...(input.unknown_registry_approved === undefined
? {}
: { unknown_registry_approved: input.unknown_registry_approved }),
};
const parsed = compareApplicationVersionsInputSchema.safeParse(raw);
return parsed.success
@@ -196,10 +193,6 @@ export const resolveCompareSourceToBundleRequestValidated = (
const raw = {
reference: input.reference,
application: application.value,
limits: input.limits,
...(input.unknown_registry_approved === undefined
? {}
: { unknown_registry_approved: input.unknown_registry_approved }),
};
const parsed = compareSourceToBundleInputSchema.safeParse(raw);
return parsed.success
@@ -244,9 +237,6 @@ export const resolveCompareJavaScriptExportShapesRequestValidated = (
left_export_name: input.left_export_name,
right_module_path: input.right_module_path,
right_export_name: input.right_export_name,
...(input.unknown_registry_approved === undefined
? {}
: { unknown_registry_approved: input.unknown_registry_approved }),
};
const parsed = compareJavaScriptExportShapesInputSchema.safeParse(raw);
return parsed.success
@@ -32,8 +32,6 @@ const status = (
availability_code: capability.available
? null
: (capability.availability_code ?? null),
input_contract_version: 1,
output_contract_version: 1,
effects: {
mutates_artifact: false,
launches_process: false,
@@ -154,14 +154,12 @@ export const compareSourceToBundleEvidenceValidated = (
rootArtifactSha256: application.rootArtifactSha256,
graph: application.graph,
},
limits: input.limits,
});
return ok(
createSourceToBundleComparisonEvidence(
{
reference_root_sha256: input.reference.root_sha256,
application_evidence_id: application.evidence.evidence_id,
limits: input.limits,
},
result,
),
@@ -147,7 +147,7 @@ describe("managed decompiler reconstruction import", () => {
).toThrow(/does not match/u);
});
it("wraps imported reconstruction in Evidence v2", () => {
it("wraps imported reconstruction in Evidence", () => {
const evidence = importManagedReconstructionEvidence(exampleInput());
expect(evidence.ok).toBe(true);
+1 -1
View File
@@ -8,7 +8,7 @@ export const firstProcedureAddress = (input: unknown): string => {
.array(z.object({ address: addressSchema }))
.min(1)
.parse(input)[0];
if (first === undefined) throw new TypeError("Procedure page was empty");
if (first === undefined) throw new TypeError("Procedure inventory was empty");
return first.address;
};
+3
View File
@@ -28,6 +28,9 @@ describe("Hopper boundary values", () => {
expect(parseNames({ names: [{ address: "0x2", name: "label" }] }).ok).toBe(
true,
);
expect(parseNames({ items: [{ address: "0x2", value: "label" }] }).ok).toBe(
false,
);
expect(parseRelatedAddresses({ callers: ["0x3"] }, "callers")).toEqual({
ok: true,
value: ["0x3"],
-9
View File
@@ -221,15 +221,6 @@ export const parseNames = (
value: JsonValue,
): Result<readonly AddressedName[], HopperProtocolError> => {
const unwrapped = unwrapProperty(value, "names");
const page = z
.object({
items: z.array(z.object({ address: z.string(), value: z.string() })),
})
.safeParse(unwrapped);
if (page.success)
return ok(
page.data.items.map(({ address, value: name }) => ({ address, name })),
);
const records = addressedNamesSchema.safeParse(unwrapped);
if (records.success) return ok(records.data);
const map = addressedNameMapSchema.safeParse(unwrapped);
@@ -15,7 +15,6 @@ export const compareApplicationVersionsInputSchema = z
right: evidenceSchema,
left_native_observations: z.array(evidenceSchema).default([]),
right_native_observations: z.array(evidenceSchema).default([]),
unknown_registry_approved: z.literal(true).optional(),
})
.superRefine((input, context) => {
if (input.left.evidence_id === input.right.evidence_id)
@@ -63,7 +63,6 @@ export const compareJavaScriptExportShapesInputSchema = z
left_export_name: selectorTextSchema,
right_module_path: selectorTextSchema,
right_export_name: selectorTextSchema,
unknown_registry_approved: z.literal(true).optional(),
})
.superRefine((input, context) => {
if (input.left.evidence_id === input.right.evidence_id)
@@ -252,7 +252,7 @@ it("returns every relevant unknown inline", () => {
direction: "forward-influence",
});
expect(result).toMatchObject({
summary: { relevant_unknowns: 1, retained_unknowns: 1 },
summary: { relevant_unknowns: 1 },
});
expect(result.unknowns).toHaveLength(1);
});
@@ -274,6 +274,51 @@ it("returns the complete deterministic forward influence result inline", () => {
expect(first.relations).toHaveLength(3);
});
it("traverses paths beyond the former fixed depth ceiling", () => {
const original = fixtureGraph();
const request = original.nodes.find(({ kind }) => kind === "request");
if (request === undefined) throw new TypeError("Missing request node");
const chain = Array.from({ length: 70 }, (_, index) =>
node("binding", `long-chain-${index}`),
);
const chainRelations = chain.map((target, index) =>
createJavaScriptSemanticGraphRelation({
source_node_id: chain[index - 1]?.node_id ?? request.node_id,
target_node_id: target.node_id,
relation: "reads",
resolution: "resolved",
properties: {},
evidence: evidence("inferred"),
}),
);
const { graph_id: _graphId, ...input } = original;
const graph = createJavaScriptSemanticGraph({
...input,
nodes: [...original.nodes, ...chain],
relations: [...original.relations, ...chainRelations],
coverage: {
...original.coverage,
families: original.coverage.families.map((family) =>
family.family === "data-flow"
? {
...family,
retained_relations:
family.retained_relations + chainRelations.length,
}
: family,
),
},
});
const result = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "literal", value: "TOKEN" },
direction: "forward-influence",
});
expect(result.summary.traversed_nodes).toBe(74);
expect(result.nodes).toHaveLength(74);
});
it("keeps relevant dynamic frontiers unknown", () => {
const result = queryJavaScriptSemanticGraph(fixtureGraph(true), {
seed: { kind: "literal", value: "TOKEN" },
+9 -155
View File
@@ -12,44 +12,26 @@ import {
type JavaScriptSemanticQueryInput,
type JavaScriptSemanticQueryResult,
} from "./javascriptSemanticQuerySchemas.js";
import { assessJavaScriptSemanticQuery } from "./javascriptSemanticQueryAssessment.js";
import {
assessJavaScriptSemanticQuery,
type JavaScriptSemanticQueryFrontier,
} from "./javascriptSemanticQueryAssessment.js";
import {
canonicalJavaScriptSemanticQueryJson as canonicalJson,
javaScriptSemanticQueryIdentifier as queryIdentifier,
resolveJavaScriptSemanticQuerySeeds as resolveSeeds,
} from "./javascriptSemanticQueryIdentity.js";
const TRAVERSAL_LIMITS = {
max_seed_matches: 1_000,
max_nodes: 50_000,
max_edges: 100_000,
max_depth: 64,
max_functions: 10_000,
max_modules: 10_000,
max_unknowns: 10_000,
} as const;
interface TraversalEntry {
readonly relation: JavaScriptSemanticGraphRelation;
readonly nextNodeId: string;
}
type QueryFrontier = JavaScriptSemanticQueryFrontier;
interface Traversal {
readonly nodeIds: Set<string>;
readonly relationIds: Set<string>;
readonly functionIds: Set<string>;
readonly modules: Set<string>;
readonly frontier: QueryFrontier[];
}
interface SeedAdmission {
readonly nodeIds: string[];
readonly frontier: QueryFrontier[];
}
interface QueryResult {
@@ -57,7 +39,7 @@ interface QueryResult {
readonly retainedRelations: JavaScriptSemanticGraphRelation[];
}
/** Run one deterministic bounded traversal over a verified semantic graph. */
/** Run one deterministic traversal over a verified semantic graph. */
export const queryJavaScriptSemanticGraph = (
graph: JavaScriptSemanticGraph,
rawInput: unknown,
@@ -65,24 +47,17 @@ export const queryJavaScriptSemanticGraph = (
const input = javaScriptSemanticQueryInputSchema.parse(rawInput);
const queryId = queryIdentifier(graph, input);
const seeds = resolveSeeds(graph, input);
const admission = admitSeeds(graph, seeds, input);
const admission = admitSeeds(graph, seeds);
const retainedSeeds = admission.nodeIds;
const adjacency = buildAdjacency(graph.relations, input);
const traversal = traverse(graph, retainedSeeds, adjacency, input);
const frontier = uniqueFrontier([
...traversal.frontier,
...admission.frontier,
]);
const traversal = traverse(graph, retainedSeeds, adjacency);
const result = createQueryResult(graph, traversal);
const allRelevantUnknowns = relevantUnknownFrontiers(
graph,
traversal.nodeIds,
input,
);
const relevantUnknowns = allRelevantUnknowns.slice(
0,
TRAVERSAL_LIMITS.max_unknowns,
);
const relevantUnknowns = allRelevantUnknowns;
const candidateRelations = relevantCandidateRelationCount(
graph,
traversal.nodeIds,
@@ -96,13 +71,10 @@ export const queryJavaScriptSemanticGraph = (
const assessment = assessJavaScriptSemanticQuery({
graph,
totalSeeds: seeds.length,
retainedSeeds: retainedSeeds.length,
expectedMatches: expectedMatches.length,
hasExpectation: input.expected !== null,
frontier,
unknowns: relevantUnknowns,
candidateRelations,
unknownsTruncated: allRelevantUnknowns.length > relevantUnknowns.length,
});
return javaScriptSemanticQueryResultSchema.parse({
query_id: queryId,
@@ -117,13 +89,11 @@ export const queryJavaScriptSemanticGraph = (
expected_match_node_ids: expectedMatches.map(({ node_id }) => node_id),
summary: {
total_seed_matches: seeds.length,
retained_seed_matches: retainedSeeds.length,
traversed_nodes: traversal.nodeIds.size,
traversed_relations: traversal.relationIds.size,
traversed_functions: traversal.functionIds.size,
traversed_modules: traversal.modules.size,
relevant_unknowns: allRelevantUnknowns.length,
retained_unknowns: relevantUnknowns.length,
},
coverage: assessment.coverage,
limitations: assessment.limitations,
@@ -146,68 +116,15 @@ const createQueryResult = (
const admitSeeds = (
graph: JavaScriptSemanticGraph,
seeds: readonly string[],
input: JavaScriptSemanticQueryInput,
): SeedAdmission => {
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
const retained: string[] = [];
const functions = new Set<string>();
const modules = new Set<string>();
const frontier: QueryFrontier[] = [];
for (const nodeId of seeds) {
const node = nodes.get(nodeId);
if (node === undefined) continue;
const reason = seedBlockedReason(node, {
retained,
functions,
modules,
input,
});
if (reason !== null) {
frontier.push({ node_id: nodeId, depth: 0, reason });
continue;
}
retained.push(nodeId);
retainOwners(node, functions, modules);
}
return { nodeIds: retained, frontier: uniqueFrontier(frontier) };
};
interface SeedBounds {
readonly retained: readonly string[];
readonly functions: ReadonlySet<string>;
readonly modules: ReadonlySet<string>;
readonly input: JavaScriptSemanticQueryInput;
}
interface TraversalBounds {
readonly nodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>;
readonly nodeIds: ReadonlySet<string>;
readonly relationIds: ReadonlySet<string>;
readonly functionIds: ReadonlySet<string>;
readonly modules: ReadonlySet<string>;
readonly input: JavaScriptSemanticQueryInput;
}
const seedBlockedReason = (
node: JavaScriptSemanticGraphNode,
bounds: SeedBounds,
): QueryFrontier["reason"] | null => {
if (bounds.retained.length >= TRAVERSAL_LIMITS.max_seed_matches)
return "max-seed-matches";
if (bounds.retained.length >= TRAVERSAL_LIMITS.max_nodes) return "max-nodes";
const functionId = functionOwner(node);
if (
functionId !== null &&
!bounds.functions.has(functionId) &&
bounds.functions.size >= TRAVERSAL_LIMITS.max_functions
)
return "max-functions";
if (
!bounds.modules.has(node.identity.module_path) &&
bounds.modules.size >= TRAVERSAL_LIMITS.max_modules
)
return "max-modules";
return null;
return { nodeIds: retained };
};
const buildAdjacency = (
@@ -300,43 +217,25 @@ const traverse = (
graph: JavaScriptSemanticGraph,
seeds: readonly string[],
adjacency: ReadonlyMap<string, TraversalEntry[]>,
input: JavaScriptSemanticQueryInput,
): Traversal => {
const nodes = new Map(graph.nodes.map((node) => [node.node_id, node]));
const nodeIds = new Set(seeds);
const relationIds = new Set<string>();
const functionIds = new Set<string>();
const modules = new Set<string>();
const frontier: QueryFrontier[] = [];
const queue = [...nodeIds];
for (const nodeId of nodeIds)
retainOwners(nodes.get(nodeId), functionIds, modules);
const depths = new Map([...nodeIds].map((nodeId) => [nodeId, 0]));
const queue = [...nodeIds];
const bounds: TraversalBounds = {
nodes,
nodeIds,
relationIds,
functionIds,
modules,
input,
};
for (let offset = 0; offset < queue.length; offset += 1) {
const current = queue[offset];
if (current === undefined) continue;
const depth = depths.get(current) ?? 0;
for (const entry of adjacency.get(current) ?? []) {
const reason = blockedReason(entry, depth, bounds);
if (reason !== null) {
frontier.push({ node_id: current, depth, reason });
continue;
}
relationIds.add(entry.relation.relation_id);
if (nodeIds.has(entry.nextNodeId)) continue;
const node = nodes.get(entry.nextNodeId);
if (node === undefined) continue;
nodeIds.add(entry.nextNodeId);
retainOwners(node, functionIds, modules);
depths.set(entry.nextNodeId, depth + 1);
queue.push(entry.nextNodeId);
}
}
@@ -345,66 +244,21 @@ const traverse = (
relationIds,
functionIds,
modules,
frontier: uniqueFrontier(frontier),
};
};
const blockedReason = (
entry: TraversalEntry,
depth: number,
bounds: TraversalBounds,
): QueryFrontier["reason"] | null => {
if (depth >= TRAVERSAL_LIMITS.max_depth) return "max-depth";
if (
!bounds.relationIds.has(entry.relation.relation_id) &&
bounds.relationIds.size >= TRAVERSAL_LIMITS.max_edges
)
return "max-edges";
if (bounds.nodeIds.has(entry.nextNodeId)) return null;
if (bounds.nodeIds.size >= TRAVERSAL_LIMITS.max_nodes) return "max-nodes";
const node = bounds.nodes.get(entry.nextNodeId);
if (node === undefined) return null;
const functionId = functionOwner(node);
if (
functionId !== null &&
!bounds.functionIds.has(functionId) &&
bounds.functionIds.size >= TRAVERSAL_LIMITS.max_functions
)
return "max-functions";
if (
!bounds.modules.has(node.identity.module_path) &&
bounds.modules.size >= TRAVERSAL_LIMITS.max_modules
)
return "max-modules";
return null;
};
const retainOwners = (
node: JavaScriptSemanticGraphNode | undefined,
functions: Set<string>,
modules: Set<string>,
): void => {
if (node === undefined) return;
const functionId = functionOwner(node);
const functionId =
node.kind === "function" ? node.node_id : node.function_node_id;
if (functionId !== null) functions.add(functionId);
modules.add(node.identity.module_path);
};
const functionOwner = (node: JavaScriptSemanticGraphNode): string | null =>
node.kind === "function" ? node.node_id : node.function_node_id;
const uniqueFrontier = (frontier: readonly QueryFrontier[]): QueryFrontier[] =>
[
...new Map(
frontier.map((item) => [
`${item.node_id}\0${String(item.depth)}\0${item.reason}`,
item,
]),
).values(),
].sort((left, right) =>
compareCodePoints(canonicalJson(left), canonicalJson(right)),
);
const relevantUnknownFrontiers = (
graph: JavaScriptSemanticGraph,
nodeIds: ReadonlySet<string>,
+10 -43
View File
@@ -3,40 +3,24 @@ import type { JavaScriptSemanticGraph } from "./javascriptSemanticGraph.js";
import type { JavaScriptSemanticGraphUnknown } from "./javascriptSemanticGraphSchemas.js";
import type { JavaScriptSemanticQueryResult } from "./javascriptSemanticQuerySchemas.js";
/** One caller-limit frontier retained by deterministic query traversal. */
export interface JavaScriptSemanticQueryFrontier {
readonly node_id: string;
readonly depth: number;
readonly reason:
| "max-depth"
| "max-edges"
| "max-functions"
| "max-modules"
| "max-nodes"
| "max-seed-matches";
}
/** Inputs needed to classify one completed semantic traversal. */
export interface JavaScriptSemanticQueryAssessmentInput {
readonly graph: JavaScriptSemanticGraph;
readonly totalSeeds: number;
readonly retainedSeeds: number;
readonly expectedMatches: number;
readonly hasExpectation: boolean;
readonly frontier: readonly JavaScriptSemanticQueryFrontier[];
readonly unknowns: readonly JavaScriptSemanticGraphUnknown[];
readonly candidateRelations: number;
readonly unknownsTruncated: boolean;
}
/** Status, coverage, limits, and limitations derived from one traversal. */
/** Status, coverage, and limitations derived from one traversal. */
export interface JavaScriptSemanticQueryAssessment {
readonly status: JavaScriptSemanticQueryResult["status"];
readonly coverage: JavaScriptSemanticQueryResult["coverage"];
readonly limitations: string[];
}
/** Classify a bounded semantic traversal without promoting unknowns to absence. */
/** Classify a complete semantic traversal without promoting unknowns to absence. */
export const assessJavaScriptSemanticQuery = (
input: JavaScriptSemanticQueryAssessmentInput,
): JavaScriptSemanticQueryAssessment => ({
@@ -51,28 +35,26 @@ const queryCoverage = (
status:
input.graph.coverage.status === "unavailable"
? "unavailable"
: input.frontier.length > 0 || input.unknownsTruncated
? "truncated"
: input.unknowns.length > 0 ||
input.candidateRelations > 0 ||
input.graph.coverage.status !== "complete"
? "partial"
: "complete",
frontier: [...input.frontier],
: input.unknowns.length > 0 ||
input.candidateRelations > 0 ||
input.graph.coverage.status !== "complete" ||
input.graph.coverage.truncated
? "partial"
: "complete",
});
const queryStatus = (
input: JavaScriptSemanticQueryAssessmentInput,
): JavaScriptSemanticQueryResult["status"] => {
if (input.graph.coverage.status === "unavailable") return "unsupported";
if (input.frontier.length > 0 || input.unknownsTruncated) return "truncated";
if (input.candidateRelations > 0) return "ambiguous";
if (
input.totalSeeds === 0 ||
(input.hasExpectation && input.expectedMatches === 0)
)
return input.unknowns.length > 0 ||
input.graph.coverage.status !== "complete"
input.graph.coverage.status !== "complete" ||
input.graph.coverage.truncated
? "partial"
: "no-match";
return input.totalSeeds > 1 ? "ambiguous" : "found";
@@ -84,26 +66,11 @@ const queryLimitations = (
uniqueSorted([
"Semantic graph reachability is a static inference and does not prove runtime execution.",
...input.graph.limitations,
...(input.totalSeeds === input.retainedSeeds
? []
: [
"Seed matches exceeded the internal traversal safety ceiling; omitted starts remain unexplored.",
]),
...(input.frontier.length === 0
? []
: [
"Traversal stopped at an internal resource ceiling; frontier facts remain unknown.",
]),
...(input.unknowns.length === 0
? []
: [
"Relevant dynamic, unsupported, incomplete, or ambiguous semantics remain unknown.",
]),
...(input.unknownsTruncated
? [
"Relevant unknown frontiers exceeded the internal result safety ceiling; omitted unknowns remain unresolved.",
]
: []),
...(input.candidateRelations === 0
? []
: [
+7 -30
View File
@@ -86,51 +86,28 @@ export const javaScriptSemanticQueryInputSchema = z.strictObject({
source_map_authority: sourceMapAuthoritySchema.default({ authority: "none" }),
});
const queryFrontierSchema = z.strictObject({
node_id: semanticNodeIdSchema,
depth: z.number().int().min(0).max(65),
reason: z.enum([
"max-depth",
"max-edges",
"max-functions",
"max-modules",
"max-nodes",
"max-seed-matches",
]),
});
/** Deterministic semantic trace result. */
export const javaScriptSemanticQueryResultSchema = z.strictObject({
query_id: z.string().regex(/^jsrq_[a-f0-9]{64}$/u),
source_graph_id: z.string().regex(/^jsrg_[a-f0-9]{64}$/u),
seed: javaScriptSemanticQuerySeedSchema,
direction: javaScriptSemanticQueryInputSchema.shape.direction,
status: z.enum([
"found",
"no-match",
"ambiguous",
"partial",
"truncated",
"unsupported",
]),
seed_node_ids: z.array(semanticNodeIdSchema).max(1_000),
nodes: z.array(javaScriptSemanticNodeSchema).max(3_000),
relations: z.array(javaScriptSemanticRelationSchema).max(1_000),
unknowns: z.array(javaScriptSemanticUnknownSchema).max(100_000),
expected_match_node_ids: z.array(semanticNodeIdSchema).max(50_000),
status: z.enum(["found", "no-match", "ambiguous", "partial", "unsupported"]),
seed_node_ids: z.array(semanticNodeIdSchema),
nodes: z.array(javaScriptSemanticNodeSchema),
relations: z.array(javaScriptSemanticRelationSchema),
unknowns: z.array(javaScriptSemanticUnknownSchema),
expected_match_node_ids: z.array(semanticNodeIdSchema),
summary: z.strictObject({
total_seed_matches: z.number().int().min(0),
retained_seed_matches: z.number().int().min(0),
traversed_nodes: z.number().int().min(0),
traversed_relations: z.number().int().min(0),
traversed_functions: z.number().int().min(0),
traversed_modules: z.number().int().min(0),
relevant_unknowns: z.number().int().min(0),
retained_unknowns: z.number().int().min(0).max(10_000),
}),
coverage: z.strictObject({
status: z.enum(["complete", "partial", "truncated", "unavailable"]),
frontier: z.array(queryFrontierSchema).max(100_000),
status: z.enum(["complete", "partial", "unavailable"]),
}),
limitations: z.array(z.string().min(1).max(4_096)).max(1_000),
});
-1
View File
@@ -28,7 +28,6 @@ export const compareManagedMembersInputSchema = z
.strictObject({
left: evidenceSchema,
right: evidenceSchema,
unknown_registry_approved: z.literal(true).optional(),
})
.superRefine((input, context) => {
if (input.left.evidence_id === input.right.evidence_id)
@@ -91,7 +91,6 @@ export const managedNativeVerificationInputSchema = z
.strictObject({
managed_boundaries: evidenceSchema,
native_observations: z.array(evidenceSchema).min(1),
unknown_registry_approved: z.literal(true).optional(),
})
.superRefine((input, context) => {
const ids = new Set<string>();
-1
View File
@@ -49,7 +49,6 @@ export const managedReconstructionImportInputSchema = z.strictObject({
decompiler: decompilerSchema,
methods: z.array(managedReconstructionMethodInputSchema).min(1).max(50),
notes: z.array(z.string().min(1).max(4_096)).max(100).default([]),
unknown_registry_approved: z.literal(true).optional(),
});
const importedMethodSchema = z.strictObject({
-1
View File
@@ -53,7 +53,6 @@ export const managedRuntimeCorrelationInputSchema = z.strictObject({
allow_network: false,
allow_ui: false,
}),
unknown_registry_approved: z.literal(true).optional(),
});
export const managedRuntimeCorrelationResultSchema = z.strictObject({
+17 -26
View File
@@ -6,7 +6,10 @@ import {
} from "./javascriptApplicationGraph.js";
import { createHistoricalSourceGraph } from "./referenceSourceGraph.js";
import { compareSourceToBundle } from "./sourceToBundleComparison.js";
import { sourceToBundleComparisonResultSchema } from "./sourceToBundleComparisonSchemas.js";
import {
compareSourceToBundleInputSchema,
sourceToBundleComparisonResultSchema,
} from "./sourceToBundleComparisonSchemas.js";
import { artifactEvidence } from "./javascriptApplicationGraph.fixture.js";
const HASH = {
@@ -31,7 +34,6 @@ describe("historical source to bundle comparison", () => {
rootArtifactSha256: HASH.artifact,
graph,
},
limits: limits(),
};
const first = compareSourceToBundle(input);
@@ -104,7 +106,6 @@ describe("historical source to bundle comparison", () => {
rootArtifactSha256: HASH.artifact,
graph,
},
limits: limits(),
});
expect(result.summary).toMatchObject({ removed: 0, unknown: 1 });
@@ -116,7 +117,7 @@ describe("historical source to bundle comparison", () => {
expect(result.coverage.status).toBe("partial");
});
it("keeps exact mappings unknown when candidate output is truncated", () => {
it("retains every candidate without caller-selected caps", () => {
const result = compareSourceToBundle({
reference: historicalGraph("complete"),
application: {
@@ -124,16 +125,15 @@ describe("historical source to bundle comparison", () => {
rootArtifactSha256: HASH.artifact,
graph: applicationGraph("complete"),
},
limits: { ...limits(), max_candidate_nodes: 1 },
});
expect(item(result, "src/duplicated.ts").status).toBe("unknown");
expect(
item(result, "src/duplicated.ts").omitted_candidates,
).toBeGreaterThan(0);
expect(item(result, "src/duplicated.ts")).toMatchObject({
status: "duplicated",
candidates: [{}, {}],
});
});
it("reports the candidate frontier instead of deciding past a limit", () => {
it("evaluates all indexed candidates for each source file", () => {
const result = compareSourceToBundle({
reference: historicalGraph("complete", ["src/split.ts"]),
application: {
@@ -141,20 +141,18 @@ describe("historical source to bundle comparison", () => {
rootArtifactSha256: HASH.artifact,
graph: applicationGraph("complete"),
},
limits: { ...limits(), max_candidate_evaluations: 1 },
});
expect(result.items[0]).toMatchObject({
status: "unknown",
confidence: "unknown",
omitted_candidates: 1,
});
expect(result.items[0]).toMatchObject({ status: "split" });
expect(result.coverage).toMatchObject({
status: "truncated",
candidate_evaluations: 1,
omitted_candidate_evaluations: 1,
status: "complete-within-inputs",
candidate_evaluations: 2,
});
});
it("does not expose caller-selected comparison limits", () => {
expect(compareSourceToBundleInputSchema.shape).not.toHaveProperty("limits");
});
});
const historicalGraph = (
@@ -300,13 +298,6 @@ const sourceDigest = (path: string): string => {
return HASH.unchanged;
};
const limits = () => ({
max_source_files: 100,
max_application_nodes: 100,
max_candidate_nodes: 100,
max_candidate_evaluations: 10_000,
});
const item = (
result: ReturnType<typeof compareSourceToBundle>,
sourcePath: string,
+7 -105
View File
@@ -7,7 +7,6 @@ import type { HistoricalSourceGraph } from "./referenceSourceGraph.js";
import {
SOURCE_TO_BUNDLE_SIGNAL_WEIGHTS,
sourceToBundleComparisonResultSchema,
type CompareSourceToBundleInput,
type SourceToBundleCandidate,
type SourceToBundleComparisonItem,
type SourceToBundleComparisonResult,
@@ -28,7 +27,6 @@ interface SourceToBundleComparisonInput {
readonly rootArtifactSha256: string;
readonly graph: JavaScriptApplicationGraph;
};
readonly limits: CompareSourceToBundleInput["limits"];
}
interface ClassifiedMapping {
@@ -49,8 +47,6 @@ interface ComparisonProjection {
interface MappingProjection {
readonly items: SourceToBundleComparisonItem[];
readonly candidateEvaluations: number;
readonly omittedCandidateEvaluations: number;
readonly omittedCandidateReferences: number;
}
/** Compare historical source files to static source-bearing application nodes. */
@@ -61,8 +57,6 @@ export const compareSourceToBundle = (
const mappings = compareMappings(input, projection);
const factors = {
...projection.factors,
omittedCandidateEvaluations: mappings.omittedCandidateEvaluations,
omittedCandidateReferences: mappings.omittedCandidateReferences,
};
const mappedCurrentIds = new Set(
mappings.items.flatMap(({ current_node_ids: nodeIds }) => nodeIds),
@@ -99,13 +93,8 @@ const projectComparison = (
): ComparisonProjection => {
const allSources = historicalSourceFiles(input.reference);
const allCurrentNodes = sourceBearingNodes(input.application.graph.nodes);
const sources = allSources.slice(0, input.limits.max_source_files);
const currentNodes = allCurrentNodes.slice(
0,
input.limits.max_application_nodes,
);
const omittedSourceFiles = allSources.length - sources.length;
const omittedApplicationNodes = allCurrentNodes.length - currentNodes.length;
const sources = allSources;
const currentNodes = allCurrentNodes;
const index = buildSourceToBundleCandidateIndex(currentNodes);
return {
sources,
@@ -114,17 +103,9 @@ const projectComparison = (
absenceComplete:
input.reference.inventory_state === "complete" &&
input.application.graph.coverage.status === "complete" &&
!input.application.graph.coverage.truncated &&
omittedSourceFiles === 0 &&
omittedApplicationNodes === 0 &&
!index.pathIndexTruncated,
!input.application.graph.coverage.truncated,
factors: {
input,
omittedSourceFiles,
omittedApplicationNodes,
omittedCandidateEvaluations: 0,
omittedCandidateReferences: 0,
pathIndexTruncated: index.pathIndexTruncated,
},
};
};
@@ -133,19 +114,12 @@ const compareMappings = (
input: SourceToBundleComparisonInput,
projection: ComparisonProjection,
): MappingProjection => {
let evaluationsRemaining = input.limits.max_candidate_evaluations;
let candidateEvaluations = 0;
let omittedCandidateEvaluations = 0;
let omittedCandidateReferences = 0;
const provisionalItems: SourceToBundleComparisonItem[] = [];
for (const source of projection.sources) {
const candidateIds = candidateIdsForSource(source, projection.index);
const retainedIds = candidateIds.slice(0, evaluationsRemaining);
const omittedEvaluations = candidateIds.length - retainedIds.length;
evaluationsRemaining -= retainedIds.length;
candidateEvaluations += retainedIds.length;
omittedCandidateEvaluations += omittedEvaluations;
const scored = retainedIds
candidateEvaluations += candidateIds.length;
const scored = candidateIds
.map((nodeId) =>
scoreSourceToBundleCandidate(source, nodeId, projection.index),
)
@@ -153,18 +127,7 @@ const compareMappings = (
(candidate): candidate is SourceToBundleCandidate => candidate !== null,
)
.sort(compareCandidates);
const retainedCandidates = scored.slice(
0,
input.limits.max_candidate_nodes,
);
const omittedCandidates =
omittedEvaluations + scored.length - retainedCandidates.length;
omittedCandidateReferences += omittedCandidates;
const classification = classifyMapping(
retainedCandidates,
omittedCandidates === 0,
projection.absenceComplete,
);
const classification = classifyMapping(scored, projection.absenceComplete);
const semantic = {
source_path: source.path,
source_sha256: source.sha256,
@@ -172,16 +135,10 @@ const compareMappings = (
status: classification.status,
confidence: classification.confidence,
current_node_ids: classification.currentNodeIds,
candidates: retainedCandidates,
omitted_candidates: omittedCandidates,
candidates: scored,
limitations: uniqueSorted([
...source.limitations,
...classification.limitations,
...(omittedCandidates > 0
? [
`${String(omittedCandidates)} candidate references were omitted by comparison limits.`,
]
: []),
]),
};
provisionalItems.push({
@@ -192,8 +149,6 @@ const compareMappings = (
return {
items: markMergedMappings(input, provisionalItems),
candidateEvaluations,
omittedCandidateEvaluations,
omittedCandidateReferences,
};
};
@@ -206,13 +161,8 @@ const comparisonCoverage = (
reference_inventory_state: factors.input.reference.inventory_state,
application_graph_status: factors.input.application.graph.coverage.status,
retained_source_files: projection.sources.length,
omitted_source_files: factors.omittedSourceFiles,
retained_application_nodes: projection.currentNodes.length,
omitted_application_nodes: factors.omittedApplicationNodes,
candidate_evaluations: mappings.candidateEvaluations,
omitted_candidate_evaluations: mappings.omittedCandidateEvaluations,
omitted_candidate_references: mappings.omittedCandidateReferences,
omitted_unmapped_current_nodes: 0,
});
const scoringModel = (): SourceToBundleComparisonResult["scoring"] => ({
@@ -226,18 +176,8 @@ const scoringModel = (): SourceToBundleComparisonResult["scoring"] => ({
const classifyMapping = (
candidates: readonly SourceToBundleCandidate[],
evaluationComplete: boolean,
absenceComplete: boolean,
): ClassifiedMapping => {
if (!evaluationComplete)
return {
status: "unknown",
confidence: "unknown",
currentNodeIds: [],
limitations: [
"Candidate evaluation stopped at the declared limit; a stronger match may be omitted.",
],
};
const exact = candidates.filter((candidate) =>
hasSignal(candidate, "exact-source-digest"),
);
@@ -387,24 +327,11 @@ const countStatus = (
interface CoverageFactors {
readonly input: SourceToBundleComparisonInput;
readonly omittedSourceFiles: number;
readonly omittedApplicationNodes: number;
readonly omittedCandidateEvaluations: number;
readonly omittedCandidateReferences: number;
readonly pathIndexTruncated: boolean;
}
const coverageStatus = (
factors: CoverageFactors,
): SourceToBundleComparisonResult["coverage"]["status"] => {
if (
factors.omittedSourceFiles > 0 ||
factors.omittedApplicationNodes > 0 ||
factors.omittedCandidateEvaluations > 0 ||
factors.omittedCandidateReferences > 0 ||
factors.pathIndexTruncated
)
return "truncated";
return factors.input.reference.inventory_state === "complete" &&
factors.input.application.graph.coverage.status === "complete"
? "complete-within-inputs"
@@ -426,31 +353,6 @@ const comparisonLimitations = (factors: CoverageFactors): string[] =>
: [
`Application graph coverage is ${factors.input.application.graph.coverage.status}; unmatched files remain unknown.`,
]),
...(factors.omittedSourceFiles > 0
? [
`${String(factors.omittedSourceFiles)} historical source files were omitted by max_source_files.`,
]
: []),
...(factors.omittedApplicationNodes > 0
? [
`${String(factors.omittedApplicationNodes)} source-bearing application nodes were omitted by max_application_nodes.`,
]
: []),
...(factors.omittedCandidateEvaluations > 0
? [
`${String(factors.omittedCandidateEvaluations)} candidate evaluations were omitted by max_candidate_evaluations.`,
]
: []),
...(factors.omittedCandidateReferences > 0
? [
`${String(factors.omittedCandidateReferences)} candidate references were omitted from item output.`,
]
: []),
...(factors.pathIndexTruncated
? [
"At least one current path exceeded the 128-segment suffix index bound.",
]
: []),
]);
const hasSignal = (
+7 -32
View File
@@ -27,36 +27,17 @@ const sourceToBundleSignalKindSchema = z.enum([
"language-extension",
]);
const sourceToBundleLimitsSchema = z.strictObject({
max_source_files: z.number().int().min(1).max(100_000).default(10_000),
max_application_nodes: z.number().int().min(1).max(100_000).default(20_000),
max_candidate_nodes: z.number().int().min(1).max(1_000).default(100),
max_candidate_evaluations: z
.number()
.int()
.min(1)
.max(10_000_000)
.default(1_000_000),
});
/** Historical source inventory, authenticated application Evidence, and bounds. */
/** Historical source inventory and authenticated application Evidence. */
export const compareSourceToBundleInputSchema = z.strictObject({
reference: historicalSourceGraphSchema,
application: evidenceSchema,
limits: sourceToBundleLimitsSchema.default({
max_source_files: 10_000,
max_application_nodes: 20_000,
max_candidate_nodes: 100,
max_candidate_evaluations: 1_000_000,
}),
unknown_registry_approved: z.literal(true).optional(),
});
const sourceToBundleSignalSchema = z.strictObject({
kind: sourceToBundleSignalKindSchema,
weight: z.number().int().min(1).max(100),
source_value: boundedTextSchema,
current_values: z.array(boundedTextSchema).min(1).max(64),
current_values: z.array(boundedTextSchema).min(1),
});
const sourceToBundleCandidateSchema = z.strictObject({
@@ -82,9 +63,8 @@ const sourceToBundleItemSchema = z.strictObject({
"unknown",
]),
confidence: z.enum(["exact", "high", "medium", "unknown"]),
current_node_ids: z.array(nodeIdSchema).max(1_000),
candidates: z.array(sourceToBundleCandidateSchema).max(1_000),
omitted_candidates: z.number().int().min(0),
current_node_ids: z.array(nodeIdSchema),
candidates: z.array(sourceToBundleCandidateSchema),
limitations: z.array(boundedTextSchema).max(100),
});
@@ -119,10 +99,10 @@ export const sourceToBundleComparisonResultSchema = z.strictObject({
duplicated: z.number().int().min(0),
unknown: z.number().int().min(0),
}),
items: z.array(sourceToBundleItemSchema).max(100_000),
unmapped_current_node_ids: z.array(nodeIdSchema).max(100_000),
items: z.array(sourceToBundleItemSchema),
unmapped_current_node_ids: z.array(nodeIdSchema),
coverage: z.strictObject({
status: z.enum(["complete-within-inputs", "partial", "truncated"]),
status: z.enum(["complete-within-inputs", "partial"]),
reference_inventory_state: z.enum(["complete", "partial", "unknown"]),
application_graph_status: z.enum([
"complete",
@@ -131,13 +111,8 @@ export const sourceToBundleComparisonResultSchema = z.strictObject({
"unavailable",
]),
retained_source_files: z.number().int().min(0),
omitted_source_files: z.number().int().min(0),
retained_application_nodes: z.number().int().min(0),
omitted_application_nodes: z.number().int().min(0),
candidate_evaluations: z.number().int().min(0),
omitted_candidate_evaluations: z.number().int().min(0),
omitted_candidate_references: z.number().int().min(0),
omitted_unmapped_current_nodes: z.number().int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).min(1).max(1),
limitations: z.array(boundedTextSchema).max(1_000),
+5 -19
View File
@@ -35,7 +35,6 @@ export interface SourceToBundleCandidateIndex {
readonly byDigest: ReadonlyMap<string, ReadonlySet<string>>;
readonly byPathSuffix: ReadonlyMap<string, ReadonlySet<string>>;
readonly byBasename: ReadonlyMap<string, ReadonlySet<string>>;
readonly pathIndexTruncated: boolean;
}
const RELEVANT_NODE_KINDS = new Set([
@@ -51,7 +50,6 @@ const PATH_PROPERTIES = [
"resolved_path",
"declared_path",
] as const;
const MAX_PATH_SUFFIXES = 128;
const signalWeight = (kind: SourceToBundleSignal["kind"]): number => {
const entry = SOURCE_TO_BUNDLE_SIGNAL_WEIGHTS.find(
@@ -81,7 +79,7 @@ export const historicalSourceFiles = (
)
.sort((left, right) => compareText(left.path, right.path));
/** Build bounded-key indices without assigning fuzzy matches. */
/** Build deterministic candidate indices without assigning fuzzy matches. */
export const buildSourceToBundleCandidateIndex = (
nodes: readonly ApplicationNode[],
): SourceToBundleCandidateIndex => {
@@ -89,14 +87,11 @@ export const buildSourceToBundleCandidateIndex = (
const byDigest = new Map<string, Set<string>>();
const byPathSuffix = new Map<string, Set<string>>();
const byBasename = new Map<string, Set<string>>();
let pathIndexTruncated = false;
for (const projection of projections) {
for (const digest of projection.digests)
addIndexValue(byDigest, digest, projection.node.node_id);
for (const path of projection.paths) {
const suffixes = pathSuffixes(path.value);
pathIndexTruncated ||= suffixes.truncated;
for (const suffix of suffixes.values)
for (const suffix of pathSuffixes(path.value))
addIndexValue(byPathSuffix, suffix, projection.node.node_id);
addIndexValue(
byBasename,
@@ -112,7 +107,6 @@ export const buildSourceToBundleCandidateIndex = (
byDigest,
byPathSuffix,
byBasename,
pathIndexTruncated,
};
};
@@ -238,7 +232,7 @@ const signal = (
kind,
weight: signalWeight(kind),
source_value: sourceValue,
current_values: [...new Set(currentValues)].sort(compareText).slice(0, 64),
current_values: [...new Set(currentValues)].sort(compareText),
});
const candidateConfidence = (
@@ -294,17 +288,9 @@ const uniquePaths = (paths: readonly CurrentPath[]): CurrentPath[] =>
compareText(`${left.kind}\0${left.value}`, `${right.kind}\0${right.value}`),
);
const pathSuffixes = (
path: string,
): { readonly values: readonly string[]; readonly truncated: boolean } => {
const pathSuffixes = (path: string): string[] => {
const parts = path.split("/");
const start = Math.max(0, parts.length - MAX_PATH_SUFFIXES);
return {
values: parts
.slice(start)
.map((_, index) => parts.slice(start + index).join("/")),
truncated: start > 0,
};
return parts.map((_, index) => parts.slice(index).join("/"));
};
const addIndexValue = (
-1
View File
@@ -103,7 +103,6 @@ const sourceMapSchema = z.union([
]);
const webTextArtifactSummarySchema = z.object({
uri: z.string().regex(/^rea:\/\/web-content\/sha256\/[a-f0-9]{64}$/u),
sha256: z.string().regex(/^[a-f0-9]{64}$/u),
bytes: z.number().int().min(0),
media_type: z.string().min(1).max(256),