fix(javascript)!: preserve property paths and uncertain slot presence

Use one explicit object/array slot model and JSON-pointer property identity across mutation analysis, return projection, and semantic graphs. Require observed presence for discriminants and resolve writes only through retained object or array receivers.

BREAKING CHANGE: projected return fields require explicit presence and semantic object operations carry full property paths.
This commit is contained in:
morluto
2026-10-09 20:03:19 +08:00
parent 63bf91405c
commit 475a0f84da
23 changed files with 831 additions and 560 deletions
@@ -1,4 +1,8 @@
import type { JsonValue } from "../../domain/jsonValue.js";
import {
semanticContainer,
semanticPropertyPointer,
type JavaScriptSemanticSlot,
} from "../../domain/javascript/javascriptSemanticSlots.js";
import type {
JavaScriptSemanticBinding,
JavaScriptSemanticValue,
@@ -24,7 +28,7 @@ export const projectSemanticValues = (
binding,
value: binding.value,
target: bindingNode,
role: "binding",
path: [],
});
}
};
@@ -34,11 +38,13 @@ interface ValueProjectionInput {
readonly binding: JavaScriptSemanticBinding;
readonly value: JavaScriptSemanticValue;
readonly target: ReturnType<typeof semanticPropertySlot>;
readonly role: string;
readonly path: readonly string[];
}
const projectValue = (input: ValueProjectionInput): void => {
const { context, binding, value, target, role } = input;
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);
@@ -58,31 +64,33 @@ const projectValue = (input: ValueProjectionInput): void => {
resolution: "candidate",
});
}
else if (value.status === "object")
for (const property of value.properties) {
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,
property.name,
propertyPath,
property,
);
if (property.presence === "absent") continue;
addSemanticGraphRelation(context.state, {
source: target,
target: slot,
relation: "writes-property",
resolution:
value.unknownProperties || property.presence === "unknown-coverage"
? "candidate"
: "resolved",
});
projectValue({
context,
binding,
value: property.value,
target: slot,
role: `property:${property.name}`,
resolution: property.presence === "present" ? "resolved" : "candidate",
});
if (property.presence === "present")
projectValue({
context,
binding,
value: property.value,
target: slot,
path: propertyPath,
});
}
else if (value.status === "unknown" && value.resourceLimit !== undefined) {
} else if (value.status === "unknown" && value.resourceLimit !== undefined) {
const location = binding.definitions[0]?.location ?? null;
const evidence = observedSemanticEvidence(context.file, location);
const isPropertyValue = role.startsWith("property:");
@@ -123,21 +131,27 @@ const addLiteralNode = (
properties: { value },
});
/** Create one canonical property slot for a binding/property identity. */
/** Create one canonical slot for a root binding and exact static property path. */
export const semanticPropertySlot = (
context: SemanticFlowProjectionContext,
objectBindingId: string,
name: string,
path: readonly string[],
fact: JavaScriptSemanticSlot,
) =>
retainSemanticGraphNode(context.state, context.file, {
kind: "property-slot",
roleKey: `property:${objectBindingId}:${name}`,
roleKey: `property:${JSON.stringify([objectBindingId, path])}`,
location: null,
label: name,
label: fact.name,
functionNodeId:
context.bindingNodes.get(objectBindingId)?.function_node_id ?? null,
properties: {
name,
name: fact.name,
property_path: [...path],
property_pointer: semanticPropertyPointer(path),
object_binding_id: objectBindingId,
} satisfies Readonly<Record<string, JsonValue>>,
presence: fact.presence,
value_status: fact.value.status,
value_coverage: semanticContainer(fact.value)?.coverage ?? null,
},
});