refactor(runtime): retain complete reconciliation outputs

This commit is contained in:
morluto 2026-09-29 12:16:32 +08:00
parent 7b3c797237
commit 79ea571512
16 changed files with 106 additions and 190 deletions

View File

@ -156,16 +156,18 @@ silently equated with the generated bytes observed by CDP.
## Bounds and determinism
Inputs allow up to eight static layers and 32 passive captures. Runtime entity,
reconciliation item, and static load-state projections have caller-visible
limits and report omitted counts when reached. The merged application graph
retains all selected roots, nodes, edges, and observations from those inputs;
it adds no second aggregate prefix cap. Reaching an input projection limit
reports exact omitted counts and `coverage.status: truncated`.
The workflow does not impose aggregate count caps on static layers, passive
captures, runtime entities, reconciliation items, static load states, runtime
mapping declarations, or candidate references. These collections remain
subject to the structural validation applied to each Evidence record and
application graph; passive capture collection limits are committed by the
observation operation that produced that Evidence. Reconciliation reports zero
omitted items when it projects the complete supplied inputs. Static JavaScript
analysis can still report its own bounded AST coverage as partial or truncated.
Layers, captures, nodes, edges, matches, and evidence links are canonically
ordered. Repeating the same verified Evidence, mappings, and limits produces
the same reconciliation, graph, and Evidence identifiers.
ordered. Repeating the same verified Evidence and mappings produces the same
reconciliation, graph, and Evidence identifiers.
## Non-goals

View File

@ -12,10 +12,6 @@ import { flattenSemanticReturnValue } from "../domain/javascriptSemanticReturns.
import type { JavaScriptSourceRange } from "../domain/javascriptStaticAnalysisTypes.js";
import type { JavaScriptArtifactGraphCoverage } from "./JavaScriptArtifactGraphContext.js";
const MAX_PROJECTED_RETURN_SHAPES = 32;
const MAX_PROJECTED_RETURN_FIELDS = 64;
const MAX_PROJECTED_PROPERTY_COVERAGE = 64;
export interface JavaScriptReturnShapeProjection {
readonly properties: ReturnType<typeof jsonObjectSchema.parse>;
readonly range: JavaScriptSourceRange;
@ -46,24 +42,7 @@ export const projectJavaScriptExportReturnShapes = (input: {
input.baseCoverage.status === "complete" &&
callable.returnCoverage.status === "complete" &&
projectionOmitted === 0;
const limits = [
...input.baseCoverage.limits,
{
name: "return-shape-sites",
value: MAX_PROJECTED_RETURN_SHAPES,
unit: "items" as const,
},
{
name: "return-shape-fields",
value: MAX_PROJECTED_RETURN_FIELDS,
unit: "items" as const,
},
{
name: "return-shape-property-coverage",
value: MAX_PROJECTED_PROPERTY_COVERAGE,
unit: "items" as const,
},
];
const limits = input.baseCoverage.limits;
const omitted =
semanticOmitted === null ? null : semanticOmitted + projectionOmitted;
const coverage = complete
@ -79,9 +58,6 @@ export const projectJavaScriptExportReturnShapes = (input: {
"No direct return value was retained; runtime return behavior remains unknown.",
]
: []),
...(projectionOmitted > 0
? ["Return-shape graph projection reached explicit retention limits."]
: []),
];
return {
properties: jsonObjectSchema.parse({
@ -110,37 +86,14 @@ export const projectJavaScriptExportReturnShapes = (input: {
};
const projectReturnSites = (callable: JavaScriptSemanticCallable) => {
let retainedFields = 0;
let retainedCoverage = 0;
let omittedFields = 0;
let omittedCoverage = 0;
const retainedSites = callable.returnSites.slice(
0,
MAX_PROJECTED_RETURN_SHAPES,
);
const retainedSites = callable.returnSites;
const shapes = retainedSites.map((site) => {
const flattened = flattenSemanticReturnValue(site.value);
const remainingFields = Math.max(
0,
MAX_PROJECTED_RETURN_FIELDS - retainedFields,
);
const fields = flattened.fields.slice(0, remainingFields).map((field) => ({
const fields = flattened.fields.map((field) => ({
...field,
value: jsonValueSchema.parse(field.value),
}));
retainedFields += fields.length;
omittedFields += flattened.fields.length - fields.length;
const remainingCoverage = Math.max(
0,
MAX_PROJECTED_PROPERTY_COVERAGE - retainedCoverage,
);
const propertyCoverage = flattened.propertyCoverage.slice(
0,
remainingCoverage,
);
retainedCoverage += propertyCoverage.length;
omittedCoverage +=
flattened.propertyCoverage.length - propertyCoverage.length;
const propertyCoverage = flattened.propertyCoverage;
return {
source_range: site.location,
value_status: site.value.status,
@ -150,8 +103,8 @@ const projectReturnSites = (callable: JavaScriptSemanticCallable) => {
});
return {
shapes,
omittedSites: callable.returnSites.length - retainedSites.length,
omittedFields,
omittedCoverage,
omittedSites: 0,
omittedFields: 0,
omittedCoverage: 0,
};
};

View File

@ -39,5 +39,4 @@ const parameters = (
runtime_evidence_ids: input.runtime_observations.map(
({ evidence_id: id }) => id,
),
limits: input.limits,
});

View File

@ -32,11 +32,6 @@ const reconciliationEvidence = createEvidence(
JAVASCRIPT_RUNTIME_RECONCILIATION_EXAMPLE.runtime_observations.map(
({ evidence_id: id }) => id,
),
limits: {
max_runtime_entities: 10_000,
max_reconciliation_items: 20_000,
max_static_load_states: 20_000,
},
},
result: reconciliation,
confidence: "inferred",

View File

@ -208,9 +208,6 @@ export const applicationVersionComparisonResultSchema = z
});
});
export type CompareApplicationVersionsInput = z.infer<
typeof compareApplicationVersionsInputSchema
>;
export type ApplicationVersionComparisonItem = z.infer<
typeof comparisonItemSchema
>;

View File

@ -25,21 +25,17 @@ export const classifyStaticLoadStates = (
runtimeEntities: readonly RuntimeReconciliationEntity[],
matches: readonly JavaScriptRuntimeReconciliationItem[],
options: {
readonly maximumStates: number;
readonly reconciliationComplete: boolean;
},
): StaticLoadStateProjection => {
const states: JavaScriptStaticLoadState[] = [];
let totalStates = 0;
for (const layer of layers) {
const projection = classifyLayer(layer, runtimeEntities, matches, {
reconciliationComplete: options.reconciliationComplete,
maximumStates: Math.max(0, options.maximumStates - states.length),
});
totalStates += projection.total;
states.push(...projection.states);
}
return { states, omittedStates: totalStates - states.length };
return { states, omittedStates: 0 };
};
const classifyLayer = (
@ -48,11 +44,9 @@ const classifyLayer = (
matches: readonly JavaScriptRuntimeReconciliationItem[],
options: {
readonly reconciliationComplete: boolean;
readonly maximumStates: number;
},
): { readonly states: JavaScriptStaticLoadState[]; readonly total: number } => {
): { readonly states: JavaScriptStaticLoadState[] } => {
const nodes = layer.graph.nodes.filter(isLoadStateNode);
if (options.maximumStates === 0) return { states: [], total: nodes.length };
const direct = new Map<string, string[]>();
for (const match of matches)
if (
@ -73,7 +67,7 @@ const classifyLayer = (
scopedEntities.every(({ capture }) =>
captureScriptsComplete(capture.evidence.evidence_id, runtimeEntities),
);
const states = nodes.slice(0, options.maximumStates).map((node) => {
const states = nodes.map((node) => {
const runtimeNodeIds = uniqueSorted(direct.get(node.node_id) ?? []);
if (runtimeNodeIds.length > 0)
return state(layer, node, {
@ -102,7 +96,7 @@ const classifyLayer = (
: "static-or-runtime-coverage-incomplete",
});
});
return { states, total: nodes.length };
return { states };
};
const residentNodes = (

View File

@ -18,23 +18,18 @@ export const reconcileJavaScriptRuntime = (
const parsed = reconcileJavaScriptRuntimeInputSchema.parse(input);
const layers = parseStaticLayers(parsed.static_layers);
const captures = parseRuntimeCaptures(parsed.runtime_observations);
const runtime = projectRuntimeCaptures(
captures,
parsed.limits.max_runtime_entities,
);
const runtime = projectRuntimeCaptures(captures);
const candidates = collectStaticRuntimeCandidates(layers);
const matching = reconcileRuntimeEntities({
entities: runtime.entities,
candidates,
layers,
maximumItems: parsed.limits.max_reconciliation_items,
});
const loadStates = classifyStaticLoadStates(
layers,
runtime.entities,
matching.items,
{
maximumStates: parsed.limits.max_static_load_states,
reconciliationComplete:
runtime.omittedEntities === 0 && matching.omittedItems === 0,
},

View File

@ -67,7 +67,7 @@ export const buildReconciledApplicationGraph = (
: []),
...(facts.outputTruncated
? [
"The reconciled graph omitted entities or classifications at caller limits.",
"The reconciled graph omitted entities or classifications during projection.",
]
: []),
]),

View File

@ -27,24 +27,21 @@ export const reconcileRuntimeEntities = (input: {
readonly entities: readonly RuntimeReconciliationEntity[];
readonly candidates: readonly StaticRuntimeCandidate[];
readonly layers: readonly ParsedStaticLayer[];
readonly maximumItems: number;
}): RuntimeMatchingProjection => {
const index = indexCandidates(input.candidates);
const evaluated = input.entities.map((entity) =>
reconcileEntity(entity, index, input.layers),
);
const retained = evaluated
.sort((left, right) =>
compareCodePoints(
left.item.reconciliation_id,
right.item.reconciliation_id,
),
)
.slice(0, input.maximumItems);
const retained = evaluated.sort((left, right) =>
compareCodePoints(
left.item.reconciliation_id,
right.item.reconciliation_id,
),
);
return {
items: retained.map(({ item }) => item),
edges: retained.flatMap(({ edge }) => (edge === null ? [] : [edge])),
omittedItems: evaluated.length - retained.length,
omittedItems: 0,
};
};
@ -419,11 +416,10 @@ const uniqueCandidateReferences = (
if (seen.has(key)) continue;
seen.add(key);
count += 1;
if (items.length < 1_000)
items.push({
static_layer_id: layer.layerId,
static_node_id: node.node_id,
});
items.push({
static_layer_id: layer.layerId,
static_node_id: node.node_id,
});
}
return { count, items };
};

View File

@ -198,7 +198,7 @@ const reconciliationLimitations = (completion: CompletionFlags): string[] =>
: []),
...(completion.outputTruncated
? [
"Caller limits omitted runtime entities or reconciliation classifications.",
"Runtime reconciliation omitted entities or classifications during projection.",
]
: []),
]);

View File

@ -31,16 +31,9 @@ export interface RuntimeProjection {
/** Project authorized passive capture metadata into capture-scoped JAG nodes. */
export const projectRuntimeCaptures = (
captures: readonly ParsedRuntimeCapture[],
maximumEntities: number,
): RuntimeProjection => {
if (maximumEntities < captures.length)
throw new TypeError(
"Runtime entity limit must admit one target for every capture",
);
const all = captures.flatMap(projectCapture);
const targets = all.filter(({ kind }) => kind === "target");
const nonTargets = all.filter(({ kind }) => kind !== "target");
const retained = [...targets, ...nonTargets].slice(0, maximumEntities);
const retained = all;
const retainedIds = new Set(retained.map(({ node }) => node.node_id));
const nodes = retained.map(({ node }) => node);
const edges = captures
@ -59,7 +52,7 @@ export const projectRuntimeCaptures = (
nodes,
edges,
targetNodeByEvidenceId,
omittedEntities: all.length - retained.length,
omittedEntities: 0,
};
};

View File

@ -80,7 +80,6 @@ const staticLayerSchema = z.strictObject({
analysis: evidenceSchema,
runtime_mappings: z
.array(runtimeLocationMappingSchema)
.max(32)
.overwrite(normalizeRuntimeMappings)
.default([]),
});
@ -103,22 +102,11 @@ function runtimeMappingKey(mapping: RuntimeLocationMapping): string {
: `url-prefix\0${mapping.prefix}\0${mapping.artifact_prefix}`;
}
const reconciliationLimitsSchema = z.strictObject({
max_runtime_entities: z.number().int().min(1).max(50_000).default(10_000),
max_reconciliation_items: z.number().int().min(1).max(50_000).default(20_000),
max_static_load_states: z.number().int().min(1).max(50_000).default(20_000),
});
/** Evidence-backed static layers and passive captures to reconcile. */
export const reconcileJavaScriptRuntimeInputSchema = z
.strictObject({
static_layers: z.array(staticLayerSchema).min(1).max(8),
runtime_observations: z.array(evidenceSchema).min(1).max(32),
limits: reconciliationLimitsSchema.default({
max_runtime_entities: 10_000,
max_reconciliation_items: 20_000,
max_static_load_states: 20_000,
}),
static_layers: z.array(staticLayerSchema).min(1),
runtime_observations: z.array(evidenceSchema).min(1),
})
.superRefine((input, context) => {
if (
@ -148,12 +136,6 @@ export const reconcileJavaScriptRuntimeInputSchema = z
path: ["runtime_observations"],
message: "Runtime observation Evidence must be unique",
});
if (input.limits.max_runtime_entities < input.runtime_observations.length)
context.addIssue({
code: "custom",
path: ["limits", "max_runtime_entities"],
message: "Runtime entity limit must retain one target per observation",
});
});
const layerSummarySchema = z.strictObject({
@ -164,7 +146,7 @@ const layerSummarySchema = z.strictObject({
root_artifact_sha256: digestSchema,
input_path: z.string().min(1).max(16_384),
format: z.enum(["asar", "directory"]),
runtime_mappings: z.array(runtimeLocationMappingSchema).max(32),
runtime_mappings: z.array(runtimeLocationMappingSchema),
});
const captureSummarySchema = z.strictObject({
@ -209,15 +191,13 @@ const reconciliationItemShape = {
]),
confidence: z.enum(["exact", "high", "medium", "low", "unknown"]),
reason: reconciliationReasonSchema,
candidate_static_count: z.number().int().min(0).max(800_000),
candidate_static_nodes: z
.array(
z.strictObject({
static_layer_id: z.string().regex(/^jrl_[a-f0-9]{64}$/u),
static_node_id: nodeIdSchema,
}),
)
.max(1_000),
candidate_static_count: z.number().int().min(0),
candidate_static_nodes: z.array(
z.strictObject({
static_layer_id: z.string().regex(/^jrl_[a-f0-9]{64}$/u),
static_node_id: nodeIdSchema,
}),
),
};
const reconciliationItemSchema = z.union([
z.strictObject({
@ -244,7 +224,7 @@ const staticLoadStateSchema = z.strictObject({
"not-observed-in-capture",
"unknown",
]),
runtime_node_ids: z.array(nodeIdSchema).max(10_000),
runtime_node_ids: z.array(nodeIdSchema),
reason: z.enum([
"runtime-script-correspondence",
"containing-asset-was-loaded",
@ -257,8 +237,8 @@ const staticLoadStateSchema = z.strictObject({
/** Deterministic static/passive-runtime reconciliation with a combined JAG. */
export const javascriptRuntimeReconciliationResultSchema = z.strictObject({
reconciliation_id: z.string().regex(/^jrr_[a-f0-9]{64}$/u),
static_layers: z.array(layerSummarySchema).min(1).max(8),
runtime_captures: z.array(captureSummarySchema).min(1).max(32),
static_layers: z.array(layerSummarySchema).min(1),
runtime_captures: z.array(captureSummarySchema).min(1),
graph: javascriptApplicationGraphSchema,
summary: z.strictObject({
runtime_targets: z.number().int().min(0),
@ -274,8 +254,8 @@ export const javascriptRuntimeReconciliationResultSchema = z.strictObject({
static_not_observed: z.number().int().min(0),
static_unknown: z.number().int().min(0),
}),
reconciliations: z.array(reconciliationItemSchema).max(50_000),
static_load_states: z.array(staticLoadStateSchema).max(50_000),
reconciliations: z.array(reconciliationItemSchema),
static_load_states: z.array(staticLoadStateSchema),
source_map_authority: z.strictObject({
used_for_primary_matching: z.literal(false),
static_layer_count: z.number().int().min(0),
@ -290,8 +270,8 @@ export const javascriptRuntimeReconciliationResultSchema = z.strictObject({
omitted_static_load_states: z.number().int().min(0),
omitted_graph_items: z.number().int().min(0),
}),
evidence_links: z.array(evidenceIdSchema).min(2).max(40),
limitations: z.array(boundedTextSchema).max(1_000),
evidence_links: z.array(evidenceIdSchema).min(2),
limitations: z.array(boundedTextSchema),
});
export type ReconcileJavaScriptRuntimeInput = z.infer<

View File

@ -118,9 +118,6 @@ export const sourceToBundleComparisonResultSchema = z.strictObject({
limitations: z.array(boundedTextSchema).max(1_000),
});
export type CompareSourceToBundleInput = z.infer<
typeof compareSourceToBundleInputSchema
>;
export type SourceToBundleComparisonResult = z.infer<
typeof sourceToBundleComparisonResultSchema
>;

View File

@ -205,7 +205,42 @@ describe("JavaScript export return-shape selection", () => {
});
describe("JavaScript export return-shape projection", () => {
it("marks graph field projection omissions as truncated", async () => {
it("retains more than 64 inferred return fields", async () => {
const properties = Array.from(
{ length: 70 },
(_, index) => `field${String(index)}: ${String(index)}`,
).join(",");
const root = await temporaryRoot();
await writeFile(
join(root, "parser.mjs"),
`export const parse = () => ({ ${properties} });`,
);
const analyzed = await analyzeGraph(root);
const returnShape = analyzed.graph.nodes
.flatMap(({ observations }) => observations)
.find(
({ properties: value }) =>
value.semantic_role === "export-return-shapes",
);
const shapes = returnShape?.properties.static_return_shapes;
expect(Array.isArray(shapes)).toBe(true);
if (!Array.isArray(shapes)) throw new TypeError("Missing return shapes");
const firstShape = shapes[0];
if (
firstShape === null ||
typeof firstShape !== "object" ||
Array.isArray(firstShape)
)
throw new TypeError("Missing first return shape");
expect(firstShape.fields).toHaveLength(70);
expect(returnShape?.properties.return_shape_coverage).toMatchObject({
omitted_fields: 0,
projection_complete: true,
});
});
it("keeps incomplete source property coverage partial", async () => {
const properties = [
'a_type: "item"',
...Array.from(
@ -219,9 +254,9 @@ describe("JavaScript export return-shape projection", () => {
});
const result = compare(left, right);
expect(result.coverage.status).toBe("truncated");
expect(result.coverage.left_omitted_fields).toBeGreaterThan(0);
expect(result.coverage.right_omitted_fields).toBeGreaterThan(0);
expect(result.coverage.status).toBe("partial");
expect(result.coverage.left_omitted_fields).toBe(0);
expect(result.coverage.right_omitted_fields).toBe(0);
});
});

View File

@ -67,7 +67,7 @@ it("keeps byte-identical cross-layer candidates explicitly ambiguous", async ()
).toBe(2);
});
it("retains one target per capture and keeps load-state absence unknown when output is bounded", async () => {
it("retains all runtime entities and static load states without projection caps", async () => {
const fixture = await applicationFixture();
temporary.push(fixture);
const staticEvidence = await analyzeFixture(fixture);
@ -85,11 +85,6 @@ it("retains one target per capture and keeps load-state absence unknown when out
const result = reconcileJavaScriptRuntime({
static_layers: [{ role: "application", analysis: staticEvidence }],
runtime_observations: runtimeObservations,
limits: {
max_runtime_entities: 2,
max_reconciliation_items: 2,
max_static_load_states: 20_000,
},
});
expect(result.runtime_captures).toHaveLength(2);
@ -98,19 +93,22 @@ it("retains one target per capture and keeps load-state absence unknown when out
).toBe(2);
expect(result.summary).toMatchObject({
runtime_targets: 2,
runtime_frames: 0,
runtime_scripts: 0,
runtime_frames: 2,
runtime_scripts: 2,
runtime_workers: 0,
static_not_observed: 0,
static_not_observed: 2,
});
expect(result.coverage).toMatchObject({
status: "truncated",
truncated: true,
omitted_runtime_entities: 4,
omitted_runtime_entities: 0,
omitted_reconciliation_items: 0,
omitted_static_load_states: 0,
});
expect(
result.static_load_states.every(({ status }) => status === "unknown"),
).toBe(true);
expect(result.static_load_states).toHaveLength(
result.summary.static_loaded +
result.summary.static_resident +
result.summary.static_not_observed +
result.summary.static_unknown,
);
});
it("rejects runtime source bytes whose Evidence omits source-capture approval", async () => {
@ -233,15 +231,6 @@ const electronRuntimeEvidence = (
target_id: targetId,
observation_ms: 100,
include_script_sources: sourceIncluded,
limits: {
max_frames: 200,
max_dom_nodes: 2_000,
max_scripts: 500,
max_resources: 2_000,
max_workers: 500,
max_script_source_bytes: 1_048_576,
max_total_script_source_bytes: 4_194_304,
},
});
return createElectronEvidence(
"inspect_electron_page",

View File

@ -354,15 +354,6 @@ const electronRuntimeEvidence = (
target_id: targetId,
observation_ms: 100,
include_script_sources: sourceIncluded,
limits: {
max_frames: 200,
max_dom_nodes: 2_000,
max_scripts: 500,
max_resources: 2_000,
max_workers: 500,
max_script_source_bytes: 1_048_576,
max_total_script_source_bytes: 4_194_304,
},
});
return createElectronEvidence(
"inspect_electron_page",