mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
feat(javascript): discover semantic call literals in feature traces (#7840)
* feat(javascript): discover semantic call literals in feature traces * fix(javascript): bound retained call literals by transfer capacity * test(javascript): align feature no-match diagnostic
This commit is contained in:
1 parent
685f6db960
commit
f9e7571a3d
20 files changed
+681
-15
No files matched your search
@@ -2,6 +2,8 @@ import { z } from "zod";
|
||||
|
||||
/** Complete static checkpoint / per-script result transport ceiling. */
|
||||
export const JAVASCRIPT_STATIC_FACT_BYTES = 8 * 1024 * 1024;
|
||||
/** Maximum encoded worker transfer record, including the JSON envelope and newline. */
|
||||
export const JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES = 8 * 1024 * 1024;
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
import type { ApplicationCoverage } from "./javascriptApplicationEvidenceSchemas.js";
|
||||
|
||||
|
||||
@@ -71,6 +71,17 @@ export const readExactJavaScriptLiteral = (
|
||||
return { found: true, value: node.value };
|
||||
if (t.isBooleanLiteral(node)) return { found: true, value: node.value };
|
||||
if (t.isNullLiteral(node)) return { found: true, value: null };
|
||||
if (
|
||||
t.isUnaryExpression(node) &&
|
||||
(node.operator === "+" || node.operator === "-")
|
||||
) {
|
||||
const operand = unwrapJavaScriptExpression(node.argument).node;
|
||||
if (t.isNumericLiteral(operand)) {
|
||||
const value = node.operator === "-" ? -operand.value : operand.value;
|
||||
if (Number.isFinite(value) && !Object.is(value, -0))
|
||||
return { found: true, value };
|
||||
}
|
||||
}
|
||||
if (
|
||||
t.isTemplateLiteral(node) &&
|
||||
node.expressions.length === 0 &&
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import * as t from "@babel/types";
|
||||
import { expect, it } from "vitest";
|
||||
import { graphForJavaScript } from "../../../tests/fixtures/javascriptSemanticGraph.js";
|
||||
import { readExactJavaScriptLiteral } from "./javascriptAstValues.js";
|
||||
import { analyzeParsedJavaScriptSemantics } from "./javascriptSemanticAnalysis.js";
|
||||
import { parseJavaScriptSource } from "./javascriptSourceParser.js";
|
||||
import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js";
|
||||
import { javaScriptSemanticQueryInputSchema } from "./javascriptSemanticQuerySchemas.js";
|
||||
|
||||
it("recognizes wrapped unary numeric literals without collapsing negative zero", () => {
|
||||
const wrapped = t.tsAsExpression(t.numericLiteral(7), t.tsNumberKeyword());
|
||||
expect(readExactJavaScriptLiteral(t.unaryExpression("-", wrapped))).toEqual({
|
||||
found: true,
|
||||
value: -7,
|
||||
});
|
||||
expect(
|
||||
readExactJavaScriptLiteral(t.unaryExpression("-", t.numericLiteral(0))),
|
||||
).toEqual({
|
||||
found: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("retains wrapped TypeScript call literals for semantic queries", () => {
|
||||
const source = "rpc(-(7 as number), +(8 as number), -0, variable);";
|
||||
const parsed = parseJavaScriptSource(source, "example.ts");
|
||||
if (parsed === null) throw new Error("Expected TypeScript source to parse");
|
||||
const graph = graphForJavaScript(
|
||||
source,
|
||||
analyzeParsedJavaScriptSemantics(parsed),
|
||||
);
|
||||
const call = graph.nodes.find(({ kind }) => kind === "call-site");
|
||||
expect(call?.properties.arguments).toEqual([
|
||||
expect.objectContaining({ index: 0, value: -7 }),
|
||||
expect.objectContaining({ index: 1, value: 8 }),
|
||||
]);
|
||||
if (call === undefined) throw new Error("Expected call site");
|
||||
const result = queryJavaScriptSemanticGraph(
|
||||
graph,
|
||||
javaScriptSemanticQueryInputSchema.parse({
|
||||
seed: { kind: "literal", value: -7 },
|
||||
direction: "forward-influence",
|
||||
}),
|
||||
);
|
||||
expect(result.seed_node_ids).toContain(call.node_id);
|
||||
});
|
||||
@@ -365,7 +365,7 @@ describe("feature trace projection from contextBridge source members", () => {
|
||||
});
|
||||
expect(result.evidence_links).toEqual([sourceEvidenceId]);
|
||||
expect(result.limitations).toContain(
|
||||
"No graph entity matched the literal seed; this is not evidence that the feature is absent.",
|
||||
"No application graph entity matched the literal seed; this is not evidence that the feature is absent.",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ const seedMatch = (
|
||||
): ApplicationFeatureSeedMatch[] => {
|
||||
const mode = featureSeedMatchMode(seed);
|
||||
const caseSensitive = seed.case_sensitive;
|
||||
const query = normalized(seed.value, caseSensitive);
|
||||
const query = normalizeFeatureSeedText(seed.value, caseSensitive);
|
||||
if (!kindMatches(node, seed.kind)) return [];
|
||||
const requestedEntryPath =
|
||||
seed.kind === "api" && "entry_path" in seed ? seed.entry_path : undefined;
|
||||
@@ -62,7 +62,7 @@ const seedMatch = (
|
||||
)
|
||||
return [];
|
||||
const match = candidateFields(node, seed.kind).find((candidate) => {
|
||||
const value = normalized(candidate.value, caseSensitive);
|
||||
const value = normalizeFeatureSeedText(candidate.value, caseSensitive);
|
||||
return mode === "exact" ? value === query : value.includes(query);
|
||||
});
|
||||
return match === undefined
|
||||
@@ -272,7 +272,11 @@ const propertyFields = (
|
||||
return output;
|
||||
};
|
||||
|
||||
const normalized = (value: string, caseSensitive: boolean): string => {
|
||||
/** Normalize literal matching identically across application and semantic labels. */
|
||||
export const normalizeFeatureSeedText = (
|
||||
value: string,
|
||||
caseSensitive: boolean,
|
||||
): string => {
|
||||
const normalizedValue = value.normalize(caseSensitive ? "NFC" : "NFKC");
|
||||
return caseSensitive
|
||||
? normalizedValue
|
||||
|
||||
@@ -12,14 +12,18 @@ import type {
|
||||
} from "./javascriptApplicationGraphSchemas.js";
|
||||
import {
|
||||
applicationFeatureTraceResultSchema,
|
||||
featureSeedMatchMode,
|
||||
type ApplicationFeatureSeed,
|
||||
type ApplicationFeatureTraceResult,
|
||||
type TraceApplicationFeatureInput,
|
||||
} from "./javascriptFeatureTraceSchemas.js";
|
||||
import {
|
||||
findApplicationFeatureSeeds,
|
||||
normalizeFeatureSeedText,
|
||||
type ApplicationFeatureSeedMatch,
|
||||
} from "./javascriptFeatureSeed.js";
|
||||
import type { JavaScriptSemanticGraph } from "./javascriptSemanticGraph.js";
|
||||
import { semanticCallLiteralValues } from "./javascriptSemanticCallLiterals.js";
|
||||
import {
|
||||
traverseApplicationFeature,
|
||||
type ApplicationFeatureTraversal,
|
||||
@@ -30,6 +34,8 @@ import { buildJavaScriptNativeHandoffs } from "./javascriptNativeHandoff.js";
|
||||
export interface ApplicationFeatureTraceProjectionInput {
|
||||
readonly sourceEvidenceId: string;
|
||||
readonly graph: JavaScriptApplicationGraph;
|
||||
readonly semanticGraph?: JavaScriptSemanticGraph | null;
|
||||
readonly semanticPage?: { readonly offset: number; readonly limit: number };
|
||||
readonly nativeEvidence: readonly Evidence[];
|
||||
readonly seed: ApplicationFeatureSeed;
|
||||
readonly direction: TraceApplicationFeatureInput["direction"];
|
||||
@@ -98,12 +104,14 @@ export const traceApplicationFeature = (
|
||||
const noMatchResult = (
|
||||
input: ApplicationFeatureTraceProjectionInput,
|
||||
): ApplicationFeatureTraceResult => {
|
||||
const discovery = discoverSemanticLabels(input);
|
||||
const semantic = {
|
||||
source_evidence_id: input.sourceEvidenceId,
|
||||
source_graph_id: input.graph.graph_id,
|
||||
seed: input.seed,
|
||||
direction: input.direction,
|
||||
seed_matches: [],
|
||||
...(discovery === undefined ? {} : { semantic_discovery: discovery }),
|
||||
graph: null,
|
||||
paths: [],
|
||||
native_handoffs: [],
|
||||
@@ -125,7 +133,12 @@ const noMatchResult = (
|
||||
},
|
||||
evidence_links: [input.sourceEvidenceId],
|
||||
limitations: uniqueSorted([
|
||||
"No graph entity matched the literal seed; this is not evidence that the feature is absent.",
|
||||
"No application graph entity matched the literal seed; this is not evidence that the feature is absent.",
|
||||
...(discovery === undefined
|
||||
? []
|
||||
: [
|
||||
"Semantic discovery matches retained node labels and direct string call arguments; matches are starting points, not application-graph feature traces. Use trace_javascript_semantics with a returned trace_seed and the same application Evidence.",
|
||||
]),
|
||||
...sourceCoverageLimitations(input.graph),
|
||||
]),
|
||||
};
|
||||
@@ -135,6 +148,56 @@ const noMatchResult = (
|
||||
});
|
||||
};
|
||||
|
||||
/** Semantic discovery is independent of the application graph traversal. */
|
||||
const discoverSemanticLabels = (
|
||||
input: ApplicationFeatureTraceProjectionInput,
|
||||
): ApplicationFeatureTraceResult["semantic_discovery"] => {
|
||||
const graph = input.semanticGraph;
|
||||
const seed = input.seed;
|
||||
if (graph === undefined || graph === null || seed.kind !== "string")
|
||||
return undefined;
|
||||
const query = normalizeFeatureSeedText(seed.value, seed.case_sensitive);
|
||||
const mode = featureSeedMatchMode(seed);
|
||||
const { offset, limit } = input.semanticPage ?? { offset: 0, limit: 64 };
|
||||
const matches: NonNullable<
|
||||
ApplicationFeatureTraceResult["semantic_discovery"]
|
||||
>["matches"] = [];
|
||||
let total = 0;
|
||||
for (const node of graph.nodes) {
|
||||
const { label, node_id, kind } = node;
|
||||
const values = [
|
||||
...(label === null ? [] : [label]),
|
||||
...semanticCallLiteralValues(node).filter(
|
||||
(value): value is string => typeof value === "string",
|
||||
),
|
||||
];
|
||||
if (
|
||||
!values.some((entry) => {
|
||||
const value = normalizeFeatureSeedText(entry, seed.case_sensitive);
|
||||
return mode === "exact" ? value === query : value.includes(query);
|
||||
})
|
||||
)
|
||||
continue;
|
||||
if (total >= offset && matches.length < limit)
|
||||
matches.push({
|
||||
node_id,
|
||||
kind,
|
||||
trace_seed: { kind: "semantic-node", node_id },
|
||||
});
|
||||
total += 1;
|
||||
}
|
||||
return {
|
||||
source_graph_id: graph.graph_id,
|
||||
source_coverage: graph.coverage.status,
|
||||
total,
|
||||
offset,
|
||||
limit,
|
||||
next_offset:
|
||||
offset + matches.length < total ? offset + matches.length : null,
|
||||
matches,
|
||||
};
|
||||
};
|
||||
|
||||
const traceGraph = (
|
||||
input: ApplicationFeatureTraceProjectionInput,
|
||||
seedMatches: readonly ApplicationFeatureSeedMatch[],
|
||||
|
||||
@@ -106,6 +106,15 @@ export const traceApplicationFeatureInputSchema = z
|
||||
application: evidenceSchema,
|
||||
native_observations: z.array(evidenceSchema).default([]),
|
||||
seed: applicationFeatureSeedSchema,
|
||||
semantic_page: z
|
||||
.strictObject({
|
||||
offset: z.int().nonnegative().default(0),
|
||||
limit: z.int().positive().default(64),
|
||||
})
|
||||
.optional()
|
||||
.describe(
|
||||
"Page retained semantic label matches when a string seed has no application match. Defaults to offset 0 and limit 64.",
|
||||
),
|
||||
direction: z.enum(["outgoing", "incoming", "both"]).default("both"),
|
||||
})
|
||||
.superRefine(validateApplicationTraceIdentities);
|
||||
@@ -156,6 +165,34 @@ export const applicationFeatureTraceResultSchema = z.strictObject({
|
||||
seed: applicationFeatureSeedSchema,
|
||||
direction: z.enum(["outgoing", "incoming", "both"]),
|
||||
seed_matches: z.array(seedMatchSchema),
|
||||
semantic_discovery: z
|
||||
.strictObject({
|
||||
source_graph_id: prefixedDigestSchema("jsrg"),
|
||||
source_coverage: z.enum([
|
||||
"complete",
|
||||
"partial",
|
||||
"unknown",
|
||||
"unavailable",
|
||||
]),
|
||||
total: z.int().nonnegative(),
|
||||
offset: z.int().nonnegative(),
|
||||
limit: z.int().positive(),
|
||||
next_offset: z.int().nonnegative().nullable(),
|
||||
matches: z.array(
|
||||
z.strictObject({
|
||||
node_id: prefixedDigestSchema("jsrg_node"),
|
||||
kind: z.string().min(1),
|
||||
trace_seed: z.strictObject({
|
||||
kind: z.literal("semantic-node"),
|
||||
node_id: prefixedDigestSchema("jsrg_node"),
|
||||
}),
|
||||
}),
|
||||
),
|
||||
})
|
||||
.optional()
|
||||
.describe(
|
||||
"On an unmatched string seed, retained semantic labels and direct string call arguments use the requested match mode and case sensitivity. Use a trace_seed with trace_javascript_semantics; source coverage limits absence claims.",
|
||||
),
|
||||
graph: javascriptApplicationGraphSchema.nullable(),
|
||||
paths: z.array(tracePathSchema),
|
||||
native_handoffs: z.array(nativeHandoffSchema),
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import type { JavaScriptSemanticGraphNode } from "./javascriptSemanticGraphSchemas.js";
|
||||
|
||||
/** Read retained direct call literals without interpreting other node properties. */
|
||||
export const semanticCallLiteralValues = (
|
||||
node: JavaScriptSemanticGraphNode,
|
||||
): (string | number | boolean | null)[] => {
|
||||
if (node.kind !== "call-site" || !Array.isArray(node.properties.arguments))
|
||||
return [];
|
||||
return node.properties.arguments.flatMap((argument) => {
|
||||
if (
|
||||
argument === null ||
|
||||
typeof argument !== "object" ||
|
||||
Array.isArray(argument) ||
|
||||
!Object.hasOwn(argument, "value")
|
||||
)
|
||||
return [];
|
||||
const value = argument.value;
|
||||
return value === null ||
|
||||
typeof value === "string" ||
|
||||
typeof value === "number" ||
|
||||
typeof value === "boolean"
|
||||
? [value]
|
||||
: [];
|
||||
});
|
||||
};
|
||||
@@ -12,12 +12,12 @@ import type {
|
||||
import { semanticIdentifierRole } from "./javascriptSemanticProjection.js";
|
||||
import { createSemanticCallableStack } from "./javascriptSemanticCallableStack.js";
|
||||
import {
|
||||
readExactJavaScriptLiteral,
|
||||
semanticStaticPropertyKey,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
import {
|
||||
resolveSemanticBindingState,
|
||||
isUnshadowedGlobal,
|
||||
type JavaScriptSemanticAnalysisState,
|
||||
type JavaScriptSemanticBindingState,
|
||||
} from "./javascriptSemanticState.js";
|
||||
@@ -30,6 +30,7 @@ import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js";
|
||||
import { range } from "./javascriptStaticAnalysisHelpers.js";
|
||||
import { semanticCallableResultKind } from "./javascriptSemanticCallableResults.js";
|
||||
import { assignedSemanticResultBindings } from "./javascriptSemanticDataEffectHelpers.js";
|
||||
import { semanticPrimitiveSet } from "./javascriptSemanticPrimitives.js";
|
||||
|
||||
/** Local call, flow, capture, and unsupported-frontier facts. */
|
||||
export interface JavaScriptSemanticCallAnalysis {
|
||||
@@ -138,7 +139,7 @@ const collectCallSite = (
|
||||
bindingCache: context.bindingResolutionCache,
|
||||
});
|
||||
const callSiteId = semanticCallSiteId(node);
|
||||
const argumentsValue = retainedArguments(node.arguments);
|
||||
const argumentsValue = retainedArguments(node.arguments, state);
|
||||
const site: JavaScriptSemanticCallSite = {
|
||||
callSiteId,
|
||||
kind: t.isNewExpression(node) ? "construct" : "call",
|
||||
@@ -209,6 +210,7 @@ const retainedArguments = (
|
||||
| t.ArgumentPlaceholder
|
||||
| null
|
||||
)[],
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): JavaScriptSemanticCallSite["arguments"] => {
|
||||
const retained: JavaScriptSemanticCallSite["arguments"][number][] = [];
|
||||
for (const [index, node] of nodes.entries()) {
|
||||
@@ -223,11 +225,34 @@ const retainedArguments = (
|
||||
location: range(node),
|
||||
valueExpressionLocation: valueNode === null ? null : range(valueNode),
|
||||
spread: t.isSpreadElement(node),
|
||||
...(valueNode === null ? {} : directLiteral(valueNode, state)),
|
||||
});
|
||||
}
|
||||
return retained;
|
||||
};
|
||||
|
||||
/** Retain direct scalar syntax only; do not infer values through bindings. */
|
||||
const directLiteral = (
|
||||
node: t.Node,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
): {
|
||||
readonly literal?: string | number | boolean | null;
|
||||
readonly literalResourceLimit?: "primitive-bytes";
|
||||
} => {
|
||||
const literal = readExactJavaScriptLiteral(node);
|
||||
if (!literal.found) return {};
|
||||
const parsed = semanticPrimitiveSet([literal.value]);
|
||||
if (parsed.status === "literal") return { literal: parsed.value };
|
||||
if (
|
||||
parsed.status === "unknown" &&
|
||||
parsed.resourceLimit === "primitive-bytes"
|
||||
) {
|
||||
state.valueResourceLimits.add(parsed.resourceLimit);
|
||||
return { literalResourceLimit: parsed.resourceLimit };
|
||||
}
|
||||
return {};
|
||||
};
|
||||
|
||||
const collectArgumentFlows = (
|
||||
site: JavaScriptSemanticCallSite,
|
||||
node: t.CallExpression | t.OptionalCallExpression | t.NewExpression,
|
||||
|
||||
@@ -138,6 +138,10 @@ export interface JavaScriptSemanticCallArgument {
|
||||
/** Unwrapped value expression; null for a spread or non-expression slot. */
|
||||
readonly valueExpressionLocation: JavaScriptSourceRange | null;
|
||||
readonly spread: boolean;
|
||||
/** Direct scalar syntax, before runtime evaluation; omitted for other expressions. */
|
||||
readonly literal?: string | number | boolean | null;
|
||||
/** A direct literal was omitted under a semantic resource bound. */
|
||||
readonly literalResourceLimit?: "primitive-bytes";
|
||||
}
|
||||
|
||||
/** One positional argument that may initialize one local parameter binding. */
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { JavaScriptSemanticGraphNode } from "./javascriptSemanticGraphSchem
|
||||
import { uniqueSorted } from "../canonicalOrdering.js";
|
||||
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
|
||||
import type { JavaScriptSemanticQueryInput } from "./javascriptSemanticQuerySchemas.js";
|
||||
import { semanticCallLiteralValues } from "./javascriptSemanticCallLiterals.js";
|
||||
|
||||
/** Derive the stable identity for one graph-bound query. */
|
||||
export const javaScriptSemanticQueryIdentifier = (
|
||||
@@ -74,8 +75,12 @@ const seedMatches = (
|
||||
if (seed.kind === "application-node")
|
||||
return node.application_node_ids.includes(seed.node_id);
|
||||
if (seed.kind === "function") return fingerprintFunctions.has(node.node_id);
|
||||
if (seed.kind === "literal")
|
||||
return node.kind === "literal" && node.properties.value === seed.value;
|
||||
if (seed.kind === "literal") {
|
||||
if (node.kind === "literal") return node.properties.value === seed.value;
|
||||
return semanticCallLiteralValues(node).some(
|
||||
(value) => value === seed.value,
|
||||
);
|
||||
}
|
||||
if (seed.kind === "property") {
|
||||
if (node.kind === "property-slot")
|
||||
return node.properties.name === seed.name;
|
||||
|
||||
Reference in new issue
Block a user