Files
rea/src/application/javascript/JavaScriptSemanticGraphValueProjection.ts
T
morluto 95f0625781 fix(javascript): preserve uncertain with-scope member writes (#7914)
* fix(javascript): preserve uncertain with-scope member writes

* fix: retain nested initializer candidates

* fix(javascript): admit initializer values in worker transport

* fix(tests): remove duplicate MCP test imports
2026-10-29 09:23:34 +08:00

332 lines
10 KiB
TypeScript

import {
semanticContainer,
semanticPropertyPointer,
} from "../../domain/javascript/javascriptSemanticSlots.js";
import { digestCanonicalValue } from "../../domain/canonicalDigest.js";
import type { JavaScriptSemanticBinding } from "../../domain/javascript/javascriptSemanticIr.js";
import type {
JavaScriptSemanticProperty,
JavaScriptSemanticValue,
} from "../../domain/javascript/javascriptSemanticValueTypes.js";
import {
retainSemanticGraphNode,
addSemanticGraphRelation,
addSemanticGraphUnknown,
} from "./JavaScriptSemanticGraphConstruction.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
import {
observedSemanticEvidence,
unknownSemanticEvidence,
} from "./JavaScriptSemanticGraphEvidence.js";
/** Project bounded literal values and object slots for exact query seeds. */
export const projectSemanticValues = (
context: SemanticFlowProjectionContext,
): void => {
for (const binding of context.ir.bindings) {
const bindingNode = context.bindingNodes.get(binding.bindingId);
if (bindingNode === undefined) continue;
projectValue({
context,
binding,
value: binding.value,
target: bindingNode,
path: [],
});
if (binding.initializerValue !== undefined)
projectInitializerCandidates(
context,
binding,
binding.initializerValue,
binding.value,
[],
);
}
};
/** Preserve possible initializer flow when later effects make a slot unknown. */
const projectInitializerCandidates = (
context: SemanticFlowProjectionContext,
binding: JavaScriptSemanticBinding,
initial: JavaScriptSemanticValue,
current: JavaScriptSemanticValue,
path: readonly string[],
): void => {
const initialContainer = semanticContainer(initial);
const currentContainer = semanticContainer(current);
if (initialContainer === null || currentContainer === null) return;
const currentByName = new Map<string, JavaScriptSemanticProperty>();
for (const property of currentContainer.slots)
if (!currentByName.has(property.name))
currentByName.set(property.name, property);
for (const initialProperty of initialContainer.slots) {
const currentProperty = currentByName.get(initialProperty.name);
if (
initialProperty.presence !== "present" ||
currentProperty === undefined ||
currentProperty.presence === "absent"
)
continue;
const propertyPath = [...path, initialProperty.name];
if (currentProperty.value.status === "unknown") {
const slot = semanticPropertySlot(
context,
binding.bindingId,
propertyPath,
currentProperty,
);
projectUnknownInitializer({
context,
binding,
initial: initialProperty.value,
unknown: currentProperty.value,
target: slot,
path: propertyPath,
});
} else {
projectInitializerCandidates(
context,
binding,
initialProperty.value,
currentProperty.value,
propertyPath,
);
}
}
};
/** Retain initializer descendants as candidates without asserting the unknown receiver still exists. */
const projectUnknownInitializer = ({
context,
binding,
initial,
unknown,
target,
path,
}: {
context: SemanticFlowProjectionContext;
binding: JavaScriptSemanticBinding;
initial: JavaScriptSemanticValue;
unknown: Extract<
JavaScriptSemanticValue,
{ status: "unknown" | "ambiguous" | "cycle" }
>;
target: ReturnType<typeof semanticPropertySlot>;
path: readonly string[];
}): void => {
if (target === null) return;
const values =
initial.status === "literal"
? [initial.value]
: initial.status === "union"
? initial.values
: [];
for (const value of values) {
const literal = addLiteralNode(
context,
binding,
value,
`property:${semanticPropertyPointer(path)}`,
);
addSemanticGraphRelation(context.state, {
source: literal,
target,
relation: "defines",
resolution: "candidate",
});
if (literal !== null)
addSemanticGraphUnknown(context.state, {
node_id: literal.node_id,
family: "data-flow",
relation_kinds: ["defines"],
reason: "unknown-value",
detail: `The initializer may no longer determine this property: ${unknown.reason}`,
candidate_node_ids: [target.node_id],
evidence: unknownSemanticEvidence(
context.file,
binding.definitions[0]?.location ?? null,
),
});
}
const container = semanticContainer(initial);
if (container === null) return;
for (const property of container.slots) {
if (property.presence !== "present") continue;
const propertyPath = [...path, property.name];
const slot = semanticPropertySlot(
context,
binding.bindingId,
propertyPath,
{
name: property.name,
presence: "unknown-coverage",
value: unknown,
},
);
addSemanticGraphRelation(context.state, {
source: target,
target: slot,
relation: "writes-property",
resolution: "candidate",
});
projectUnknownInitializer({
context,
binding,
initial: property.value,
unknown,
target: slot,
path: propertyPath,
});
}
};
interface ValueProjectionInput {
readonly context: SemanticFlowProjectionContext;
readonly binding: JavaScriptSemanticBinding;
readonly value: JavaScriptSemanticValue;
readonly target: ReturnType<typeof semanticPropertySlot>;
readonly path: readonly string[];
}
const projectValue = (input: ValueProjectionInput): void => {
const { context, binding, value, target, path } = input;
const role =
path.length === 0 ? "binding" : `property:${semanticPropertyPointer(path)}`;
if (target === null) return;
if (value.status === "literal") {
const literal = addLiteralNode(context, binding, value.value, role);
addSemanticGraphRelation(context.state, {
source: literal,
target,
relation: "defines",
resolution: "resolved",
});
} else if (value.status === "union")
for (const primitive of value.values) {
const literal = addLiteralNode(context, binding, primitive, role);
addSemanticGraphRelation(context.state, {
source: literal,
target,
relation: "defines",
resolution: "candidate",
});
}
else if (value.status === "object" || value.status === "array") {
const container = semanticContainer(value);
for (const property of container.slots) {
const propertyPath = [...path, property.name];
const slot = semanticPropertySlot(
context,
binding.bindingId,
propertyPath,
property,
);
if (property.presence === "absent") continue;
addSemanticGraphRelation(context.state, {
source: target,
target: slot,
relation: "writes-property",
resolution: property.presence === "present" ? "resolved" : "candidate",
});
if (property.presence === "present")
projectValue({
context,
binding,
value: property.value,
target: slot,
path: propertyPath,
});
}
if (container.coverage.status === "partial")
addSemanticGraphUnknown(context.state, {
node_id: target.node_id,
family: "object-flow",
relation_kinds: ["writes-property"],
reason: "ambiguous-target",
detail: `Initializer container has unknown ${
value.status === "object" ? "properties" : "items"
}${container.coverage.omitted === null ? " with an unknown omitted count" : `; ${container.coverage.omitted} omitted`}.`,
candidate_node_ids: [target.node_id],
evidence: unknownSemanticEvidence(
context.file,
binding.definitions[0]?.location ?? null,
),
});
} else if (
value.status === "unknown" ||
value.status === "ambiguous" ||
value.status === "cycle"
) {
const location = binding.definitions[0]?.location ?? null;
const evidence = observedSemanticEvidence(context.file, location);
addSemanticGraphUnknown(context.state, {
node_id: target.node_id,
family: "data-flow",
relation_kinds: ["defines"],
reason:
value.resourceLimit === undefined ? "unknown-value" : "resource-limit",
detail: `${value.reason} Unknown value at ${role}.`,
candidate_node_ids: [target.node_id],
evidence: {
...evidence,
authority: "unknown",
state: "unknown",
confidence: "unknown",
limitations: [value.reason],
},
});
}
};
const addLiteralNode = (
context: SemanticFlowProjectionContext,
binding: JavaScriptSemanticBinding,
value: string | number | boolean | null,
role: string,
) => {
const encodedValue = JSON.stringify(value);
const valueDigest = `value-sha256:${digestCanonicalValue(value, "Semantic literal")}`;
const valueKey =
encodedValue.length <= valueDigest.length ? encodedValue : valueDigest;
const stringLabel = "string literal";
return retainSemanticGraphNode(context.state, context.file, {
kind: "literal",
// Identity needs an exact value commitment, not another payload copy.
// Keep the complete literal in properties for queries and Evidence export.
roleKey: `literal:${binding.bindingId}:${role}:${valueKey}`,
location: binding.definitions[0]?.location ?? null,
label:
typeof value === "string" && encodedValue.length > stringLabel.length
? stringLabel
: encodedValue,
functionNodeId:
context.bindingNodes.get(binding.bindingId)?.function_node_id ?? null,
properties: { value },
});
};
/** Create one canonical slot for a root binding and exact static property path. */
export const semanticPropertySlot = (
context: SemanticFlowProjectionContext,
objectBindingId: string,
path: readonly string[],
fact: JavaScriptSemanticProperty,
) =>
retainSemanticGraphNode(context.state, context.file, {
kind: "property-slot",
roleKey: `property:${JSON.stringify([objectBindingId, path])}`,
location: null,
label: fact.name,
functionNodeId:
context.bindingNodes.get(objectBindingId)?.function_node_id ?? null,
properties: {
name: fact.name,
property_path: [...path],
property_pointer: semanticPropertyPointer(path),
object_binding_id: objectBindingId,
presence: fact.presence,
value_status: fact.value.status,
value_coverage: semanticContainer(fact.value)?.coverage ?? null,
},
});