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
@@ -0,0 +1,108 @@
import type {
JavaScriptSemanticProperty,
JavaScriptSemanticValue,
} from "./javascriptSemanticValueTypes.js";
/** A container's own slot, using the value lattice's canonical presence model. */
export type JavaScriptSemanticSlot = JavaScriptSemanticProperty;
/** Static container inventory; partial siblings do not weaken retained slots. */
export interface JavaScriptSemanticContainer {
readonly slots: readonly JavaScriptSemanticSlot[];
readonly coverage:
| { readonly status: "complete"; readonly omitted: 0 }
| { readonly status: "partial"; readonly omitted: number | null };
}
/** Read the same slot inventory for object, array, and return projections. */
export function semanticContainer(
value: Extract<
JavaScriptSemanticValue,
{ readonly status: "object" | "array" }
>,
): JavaScriptSemanticContainer;
export function semanticContainer(
value: JavaScriptSemanticValue,
): JavaScriptSemanticContainer | null;
export function semanticContainer(
value: JavaScriptSemanticValue,
): JavaScriptSemanticContainer | null {
if (value.status === "object")
return {
slots: value.properties,
coverage: value.unknownProperties
? { status: "partial", omitted: value.omittedProperties }
: { status: "complete", omitted: 0 },
};
if (value.status === "array")
return {
slots: value.items,
coverage: value.unknownItems
? { status: "partial", omitted: value.omittedItems }
: { status: "complete", omitted: 0 },
};
return null;
}
/** Resolve a full static own-property path without conflating missing siblings. */
export const semanticSlotAtPath = (
value: JavaScriptSemanticValue,
path: readonly string[],
): JavaScriptSemanticSlot => {
const leafName = path.at(-1) ?? "";
let current: JavaScriptSemanticSlot = {
name: "",
presence: "present",
value,
};
for (const [index, name] of path.entries()) {
const container = semanticContainer(current.value);
if (current.presence !== "present" || container === null)
return {
name: leafName,
presence: "unknown-coverage",
value:
"reason" in current.value
? {
...current.value,
reason: `${current.value.reason} Cannot continue ${semanticPropertyPointer(path)} through receiver ${semanticPropertyPointer(path.slice(0, index))}.`,
}
: {
status: "unknown",
reason: `Cannot resolve ${semanticPropertyPointer(path)} through ${current.value.status} receiver ${semanticPropertyPointer(path.slice(0, index))}.`,
},
};
// Array position inventories omit the intrinsic non-enumerable length slot.
// Its own presence is guaranteed even when no exact length is retained.
if (current.value.status === "array" && name === "length") {
current = {
name,
presence: "present",
value: {
status: "unknown",
reason: "Array length is present but its value is not retained.",
},
};
continue;
}
const observed = container.slots.find((slot) => slot.name === name);
current = observed ?? {
name,
presence:
container.coverage.status === "complete"
? "absent"
: "unknown-coverage",
value: {
status: "unknown",
reason: `Own property ${name} was not observed.`,
},
};
}
return current;
};
/** Encode path segments as an unambiguous JSON Pointer, preserving empty keys. */
export const semanticPropertyPointer = (path: readonly string[]): string =>
path
.map((key) => `/${key.replaceAll("~", "~0").replaceAll("/", "~1")}`)
.join("");