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 committed 2026-10-09 20:18:52 +08:00
1 parent 63bf91405c
commit 475a0f84da
23 files changed
+831 -560

No files matched your search

@@ -1,5 +1,10 @@
import * as t from "@babel/types";
import {
semanticContainer,
semanticPropertyPointer,
} from "./javascriptSemanticSlots.js";
import type {
JavaScriptSemanticCallable,
JavaScriptSemanticModuleLink,
@@ -244,54 +249,24 @@ const flattenValue = (
});
return;
}
if (value.status === "object") {
coverage.push(
value.unknownProperties
? { path, status: "partial", omitted: value.omittedProperties }
: { path, status: "complete", omitted: 0 },
);
for (const property of value.properties)
flattenValue(
property.value,
`${path}/${escapePointer(property.name)}`,
fields,
coverage,
property.presence ?? "present",
);
if ("reason" in value) {
fields.push({
path,
presence,
state: "unknown",
value: null,
reason: value.reason,
});
return;
}
if (value.status === "array") {
coverage.push(
value.unknownItems
? { path, status: "partial", omitted: value.omittedItems }
: { path, status: "complete", omitted: 0 },
const container = semanticContainer(value);
coverage.push({ path, ...container.coverage });
for (const slot of container.slots)
flattenValue(
slot.value,
`${path}${semanticPropertyPointer([slot.name])}`,
fields,
coverage,
slot.presence,
);
value.items.forEach((item, index) =>
flattenValue(
item,
`${path}/${String(index)}`,
fields,
coverage,
value.itemPresence?.[index] ?? "present",
),
);
// Sparse mutation metadata retains uncertain slots without expanding arrays.
for (const [index, itemPresence] of Object.entries(
value.itemPresence ?? {},
)) {
if (Number(index) < value.items.length) continue;
flattenValue(value, `${path}/${index}`, fields, coverage, itemPresence);
}
return;
}
fields.push({
path,
presence,
state: "unknown",
value: null,
reason: value.reason,
});
};
const escapePointer = (value: string): string =>
value.replaceAll("~", "~0").replaceAll("/", "~1");