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

@@ -71,6 +71,7 @@ it("parses projected fields into literal, union, or unknown values", () => {
projectedExportReturnShapesSchema.safeParse(
projectionWithField({
path: "/kind",
presence: "present",
state: "unknown",
value: "invented",
reason: "Dynamic property",
@@ -81,6 +82,7 @@ it("parses projected fields into literal, union, or unknown values", () => {
projectedExportReturnShapesSchema.safeParse(
projectionWithField({
path: "/kind",
presence: "present",
state: "union",
value: ["success", "failure"],
reason: null,
@@ -88,3 +90,16 @@ it("parses projected fields into literal, union, or unknown values", () => {
).success,
).toBe(true);
});
it("rejects return fields that omit slot presence", () => {
expect(
projectedExportReturnShapesSchema.safeParse(
projectionWithField({
path: "/kind",
state: "literal",
value: "result",
reason: null,
}),
).success,
).toBe(false);
});
@@ -75,7 +75,7 @@ export const compareJavaScriptExportShapesInputSchema = z
const projectedReturnFieldShape = {
path: jsonPointerSchema,
presence: z.enum(["present", "absent", "unknown-coverage"]).optional(),
presence: z.enum(["present", "absent", "unknown-coverage"]),
};
const projectedReturnFieldSchema = z.discriminatedUnion("state", [
z.strictObject({
@@ -10,9 +10,6 @@ import {
import { compareCodePoints } from "../canonicalOrdering.js";
type Shape = ProjectedExportReturnShapes["static_return_shapes"][number];
type Field = Shape["fields"][number];
type NormalizedField = Field & { presence: NonNullable<Field["presence"]> };
type NormalizedShape = Omit<Shape, "fields"> & { fields: NormalizedField[] };
type SelectorResult = JavaScriptExportShapeComparisonResult["left"];
type SelectorBase = Omit<SelectorResult, "status" | "selected_node_id">;
type ReturnShapeSelection =
@@ -58,7 +55,7 @@ export type SelectedJavaScriptExport =
/** Complete return-shape inventory retained from an authenticated projection. */
export interface RetainedJavaScriptExportShapes {
readonly shapes: readonly NormalizedShape[];
readonly shapes: readonly Shape[];
readonly omitted: number;
}
@@ -129,24 +126,13 @@ export const selectJavaScriptExport = (
};
};
/** Retain every variant and normalize legacy presence on comparison-owned copies. */
/** Retain every authenticated variant with its explicit slot presence. */
export const retainJavaScriptExportShapes = (
selection: SelectedJavaScriptExport,
): RetainedJavaScriptExportShapes => {
const shapes = (
selection.projection?.static_return_shapes ?? []
).map<NormalizedShape>((shape) => ({
...shape,
fields: shape.fields.map<NormalizedField>((field) => ({
...field,
// Legacy unknown fields did not distinguish values from uncertain slots.
presence:
field.presence ??
(field.state === "unknown" ? "unknown-coverage" : "present"),
})),
}));
return { shapes, omitted: 0 };
};
): RetainedJavaScriptExportShapes => ({
shapes: selection.projection?.static_return_shapes ?? [],
omitted: 0,
});
const selectorBase = (
side: JavaScriptExportShapeSideInput,
@@ -17,21 +17,21 @@ describe("JavaScript semantic analysis: dataflow 1", () => {
`);
expect(
ir.objectOperations.map(({ kind, propertyName, resolution }) => ({
ir.objectOperations.map(({ kind, propertyPath, resolution }) => ({
kind,
propertyName,
propertyPath,
resolution,
})),
).toEqual(
expect.arrayContaining([
{
kind: "destructure",
propertyName: "token",
propertyPath: ["token"],
resolution: "complete",
},
{ kind: "spread", propertyName: null, resolution: "complete" },
{ kind: "write", propertyName: "count", resolution: "complete" },
{ kind: "read", propertyName: "token", resolution: "complete" },
{ kind: "spread", propertyPath: [], resolution: "complete" },
{ kind: "write", propertyPath: ["count"], resolution: "complete" },
{ kind: "read", propertyPath: ["token"], resolution: "complete" },
]),
);
});
@@ -50,11 +50,11 @@ describe("JavaScript semantic analysis: dataflow 1", () => {
({ kind }) => kind === "destructure",
);
expect(destructures.map(({ propertyName }) => propertyName)).toEqual([
"token",
"token",
"token",
"1",
expect(destructures.map(({ propertyPath }) => propertyPath)).toEqual([
["token"],
["token"],
["token"],
["1"],
]);
expect(
destructures.every(({ resolution }) => resolution === "complete"),
@@ -127,10 +127,19 @@ describe("JavaScript semantic analysis: dataflow 1", () => {
unknownProperties: false,
omittedProperties: 0,
properties: expect.arrayContaining([
{ name: "depth", value: { status: "literal", value: 1 } },
{ name: "type", value: { status: "literal", value: "heading" } },
{
name: "depth",
presence: "present",
value: { status: "literal", value: 1 },
},
{
name: "type",
presence: "present",
value: { status: "literal", value: "heading" },
},
{
name: "text",
presence: "present",
value: {
status: "unknown",
reason: "Unsupported CallExpression value.",
@@ -62,7 +62,11 @@ it("invalidates a deep mutation while retaining unrelated literal properties", (
expect(onlyBinding(ir, "root").value).toEqual({
status: "object",
properties: [
{ name: "untouched", value: { status: "literal", value: "retained" } },
{
name: "untouched",
presence: "present",
value: { status: "literal", value: "retained" },
},
{
name: "next",
presence: "unknown-coverage",
@@ -93,6 +93,9 @@ const unchangedProperties = [
'const source = { nested: { token: "TOKEN", count: 1 } }; const { nested } = source; nested.count = 2; return source.nested.token;',
'const child = { token: "TOKEN", count: 1 }; const source = { child }; source.child.count = 2; return child.token;',
'const source = ["TOKEN", 1]; source[1] = 2; return source[0];',
'const source = ["TOKEN"]; source.extra = 2; return source[0];',
'const source = ["TOKEN"]; source["01"] = 2; return source[0];',
'const source = ["TOKEN"]; source[1000000000] = 2; return source[0];',
'const child = { token: "TOKEN", count: 1 }; const source = [child]; source[0].count = 2; return child.token;',
'const source = { token: "TOKEN" }; source.added = 2; return source.token;',
'const source = { token: "TOKEN", count: 1, other: 1 }; source.count = 2; source.other = 2; return source.token;',
@@ -144,7 +144,11 @@ describe("JavaScript semantic analysis: rejection 1", () => {
expect(recovered.returnSites[0]?.value).toMatchObject({
status: "object",
properties: expect.arrayContaining([
{ name: "type", value: { status: "literal", value: "item" } },
{
name: "type",
presence: "present",
value: { status: "literal", value: "item" },
},
expect.objectContaining({
name: "text",
value: expect.objectContaining({ status: "unknown" }),
@@ -380,7 +380,7 @@ export interface JavaScriptSemanticObjectOperation {
readonly ownerCallableId: string | null;
readonly objectBindingId: string | null;
readonly targetBindingId: string | null;
readonly propertyName: string | null;
readonly propertyPath: readonly string[] | null;
readonly resolution: "complete" | "partial";
}
@@ -6,10 +6,7 @@ import {
} from "./javascriptSemanticState.js";
import { evaluateSemanticBinding } from "./javascriptSemanticValues.js";
import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js";
import {
semanticStaticPropertyKey,
semanticStaticPropertyName,
} from "./javascriptAstValues.js";
import { semanticStaticPropertyKey } from "./javascriptAstValues.js";
type PropertyPath = readonly (string | number | null)[];
@@ -105,13 +102,11 @@ const referencedValues = (
if (t.isSpreadElement(property))
return [{ node: property.argument, path }];
if (!t.isObjectProperty(property)) return [];
const name = semanticStaticPropertyName(
const name = semanticStaticPropertyKey(
property.key,
property.computed,
);
return key === null ||
(name === "" && !t.isStringLiteral(property.key, { value: "" })) ||
String(key) === name
return key === null || name === null || String(key) === name
? [{ node: property.value, path: remaining }]
: [];
});
@@ -32,25 +32,26 @@ export const invalidateSemanticMutationPath = (
return { ...value, properties };
}
if (value.status === "array") {
const index = typeof key === "number" ? key : Number(key);
if (
!Number.isSafeInteger(index) ||
index < 0 ||
String(index) !== String(key)
)
return unknown;
const items = value.items.map((item, position) =>
position === index
? invalidateSemanticMutationPath(item, remaining)
const name = String(key);
// Array length writes can remove every index; ordinary named properties
// and sparse indices affect only their own slot.
if (name === "length") return unknown;
const observed = value.items.some((item) => item.name === name);
const items = value.items.map((item) =>
item.name === name
? {
...item,
presence:
remaining.length === 0
? ("unknown-coverage" as const)
: item.presence,
value: invalidateSemanticMutationPath(item.value, remaining),
}
: item,
);
const itemPresence = {
...value.itemPresence,
...(remaining.length === 0 || value.items[index] === undefined
? { [index]: "unknown-coverage" as const }
: {}),
};
return { ...value, items, itemPresence };
if (!observed)
items.push({ name, presence: "unknown-coverage", value: unknown });
return { ...value, items };
}
return unknown;
};
@@ -38,20 +38,24 @@ export const collectJavaScriptSemanticObjects = (
t.isSpreadElement(node) &&
parent !== null &&
t.isObjectExpression(parent)
)
) {
const identity = expressionObjectIdentity(node.argument, state);
addObjectOperation(
{
node,
kind: "spread",
ownerCallableId,
objectBindingId: expressionBindingId(node.argument, state),
objectBindingId: identity.bindingId,
targetBindingId: null,
propertyName: null,
propertyPath: identity.path,
},
state,
output,
);
else if (t.isVariableDeclarator(node) && t.isObjectPattern(node.id))
} else if (
t.isVariableDeclarator(node) &&
(t.isObjectPattern(node.id) || t.isArrayPattern(node.id))
)
collectDestructuring(node, ownerCallableId, state, output);
},
exit: (node) => {
@@ -75,8 +79,14 @@ const collectMember = (
context: ObjectCollectionContext,
): void => {
const { state, output } = context;
const name = semanticStaticPropertyKey(node.property, node.computed);
if (name === null) return;
// A chain is one property access; retaining every prefix duplicates work and
// would make full paths quadratic in deeply nested producer expressions.
if (
(t.isMemberExpression(parent) || t.isOptionalMemberExpression(parent)) &&
parent.object === node
)
return;
const identity = expressionObjectIdentity(node, state);
const write =
(t.isAssignmentExpression(parent) && parent.left === node) ||
(t.isUpdateExpression(parent) && parent.argument === node);
@@ -87,9 +97,9 @@ const collectMember = (
node,
kind: "read",
ownerCallableId,
objectBindingId: expressionBindingId(node.object, state),
objectBindingId: identity.bindingId,
targetBindingId: null,
propertyName: name,
propertyPath: identity.path,
},
state,
output,
@@ -99,9 +109,9 @@ const collectMember = (
node,
kind: write ? "write" : "read",
ownerCallableId,
objectBindingId: expressionBindingId(node.object, state),
objectBindingId: identity.bindingId,
targetBindingId: null,
propertyName: name,
propertyPath: identity.path,
},
state,
output,
@@ -114,30 +124,41 @@ const collectDestructuring = (
state: JavaScriptSemanticAnalysisState,
output: JavaScriptSemanticObjectOperation[],
): void => {
if (!t.isObjectPattern(node.id)) return;
const objectBindingId = expressionBindingId(node.init, state);
for (const property of node.id.properties) {
if (!t.isObjectProperty(property)) continue;
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name === null) continue;
const target = bindingIdentifier(property.value);
const source = expressionObjectIdentity(node.init, state);
const visit = (pattern: t.Node, path: readonly string[]): void => {
if (t.isObjectPattern(pattern)) {
for (const property of pattern.properties) {
if (!t.isObjectProperty(property)) continue;
const name = semanticStaticPropertyKey(property.key, property.computed);
if (name !== null) visit(property.value, [...path, name]);
}
return;
}
if (t.isArrayPattern(pattern)) {
pattern.elements.forEach((element, index) => {
if (element !== null && !t.isRestElement(element))
visit(element, [...path, String(index)]);
});
return;
}
const target = bindingIdentifier(pattern);
if (target === null) return;
addObjectOperation(
{
node: property,
node: pattern,
kind: "destructure",
ownerCallableId,
objectBindingId,
objectBindingId: source.bindingId,
targetBindingId:
target === null
? null
: (resolveSemanticBindingState(state, target, target.name)
?.bindingId ?? null),
propertyName: name,
resolveSemanticBindingState(state, target, target.name)?.bindingId ??
null,
propertyPath: source.path === null ? null : [...source.path, ...path],
},
state,
output,
);
}
};
visit(node.id, []);
};
interface AddObjectOperationInput {
@@ -146,7 +167,7 @@ interface AddObjectOperationInput {
readonly ownerCallableId: string | null;
readonly objectBindingId: string | null;
readonly targetBindingId: string | null;
readonly propertyName: string | null;
readonly propertyPath: readonly string[] | null;
}
const addObjectOperation = (
@@ -161,27 +182,57 @@ const addObjectOperation = (
ownerCallableId: input.ownerCallableId,
objectBindingId: input.objectBindingId,
targetBindingId: input.targetBindingId,
propertyName: input.propertyName,
propertyPath: input.propertyPath,
resolution:
input.kind === "spread"
? input.objectBindingId === null
? "partial"
: "complete"
: input.objectBindingId === null ||
input.propertyName === null ||
(input.kind === "destructure" && input.targetBindingId === null)
? "partial"
: "complete",
input.objectBindingId === null ||
input.propertyPath === null ||
(input.kind === "destructure" && input.targetBindingId === null)
? "partial"
: "complete",
});
};
const expressionBindingId = (
interface ObjectIdentity {
readonly bindingId: string | null;
readonly path: readonly string[] | null;
}
const expressionObjectIdentity = (
node: t.Node | null | undefined,
state: JavaScriptSemanticAnalysisState,
): string | null =>
t.isIdentifier(node)
? (resolveSemanticBindingState(state, node, node.name)?.bindingId ?? null)
: null;
): ObjectIdentity => {
const path: string[] = [];
let current = node;
let staticPath = true;
while (current !== null && current !== undefined) {
if (
t.isMemberExpression(current) ||
t.isOptionalMemberExpression(current)
) {
const name = semanticStaticPropertyKey(
current.property,
current.computed,
);
if (name === null) staticPath = false;
else path.push(name);
current = current.object;
} else if (
t.isTSAsExpression(current) ||
t.isTSTypeAssertion(current) ||
t.isTSNonNullExpression(current) ||
t.isTSSatisfiesExpression(current)
)
current = current.expression;
else break;
}
return {
bindingId: t.isIdentifier(current)
? (resolveSemanticBindingState(state, current, current.name)?.bindingId ??
null)
: null,
path: staticPath ? path.reverse() : null,
};
};
const bindingIdentifier = (node: t.Node): t.Identifier | null => {
if (t.isIdentifier(node)) return node;
@@ -91,7 +91,7 @@ export const semanticResourceLimitsIn = (
else if (value.status === "object")
for (const property of value.properties) pending.push(property.value);
else if (value.status === "array")
for (const item of value.items) pending.push(item);
for (const item of value.items) pending.push(item.value);
}
return [...found].sort();
};
@@ -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");
@@ -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("");
@@ -23,10 +23,7 @@ type JavaScriptSemanticObjectValue = {
type JavaScriptSemanticArrayValue = {
readonly status: "array";
readonly items: readonly JavaScriptSemanticValue[];
readonly itemPresence?: Readonly<
Record<number, "absent" | "unknown-coverage">
>;
readonly items: readonly JavaScriptSemanticProperty[];
} & (
| {
readonly unknownItems: false;
@@ -56,9 +53,15 @@ export type JavaScriptSemanticValue =
readonly resourceLimit?: JavaScriptSemanticResourceLimit;
};
/** One statically named object-literal property. */
/** One statically named own slot in an object or array. */
export interface JavaScriptSemanticProperty {
readonly name: string;
readonly value: JavaScriptSemanticValue;
readonly presence?: "unknown-coverage";
readonly presence: JavaScriptSemanticSlotPresence;
}
/** Own-slot presence, independent of its value and sibling coverage. */
export type JavaScriptSemanticSlotPresence =
| "present"
| "absent"
| "unknown-coverage";
@@ -1,5 +1,7 @@
import * as t from "@babel/types";
import { semanticSlotAtPath } from "./javascriptSemanticSlots.js";
import { invalidateSemanticMutationPath } from "./javascriptSemanticMutationValues.js";
import type {
@@ -195,9 +197,12 @@ const evaluateTemplate = (
let candidates = [""];
for (let index = 0; index < node.quasis.length; index += 1) {
const quasi = node.quasis[index];
// Semantic template evaluation retains its established raw fallback for
// synthetic AST inputs; exact atom readers remain cooked-only.
const text = quasi?.value.cooked ?? quasi?.value.raw ?? "";
const text = quasi?.value.cooked;
if (text === undefined || text === null)
return {
status: "unknown",
reason: "Template text has no cooked representation.",
};
if (exceedsSemanticTemplateByteBudget(candidates, [text]))
return semanticResourceLimitUnknown("primitive-bytes");
candidates = candidates.map((prefix) => `${prefix}${text}`);
@@ -235,6 +240,7 @@ const evaluateObject = (
for (const [name] of propertiesByName)
propertiesByName.set(name, {
name,
presence: "present",
value: {
status: "unknown",
reason: "A later property may overwrite this value.",
@@ -268,6 +274,7 @@ const evaluateObject = (
}
propertiesByName.set(name, {
name,
presence: "present",
value: t.isObjectProperty(property)
? evaluateExpression(property.value, nestedContext(context))
: {
@@ -298,8 +305,7 @@ const evaluateArray = (
node: t.ArrayExpression,
context: EvaluationContext,
): JavaScriptSemanticValue => {
const items: JavaScriptSemanticValue[] = [];
const itemPresence: Record<number, "absent"> = {};
const items: JavaScriptSemanticProperty[] = [];
let unknownItems = false;
let omittedItems: number | null = 0;
for (const element of node.elements) {
@@ -310,21 +316,24 @@ const evaluateArray = (
break;
}
if (element === null) {
itemPresence[items.length] = "absent";
items.push({
status: "unknown",
reason: "Array hole has no primitive value.",
name: String(items.length),
presence: "absent",
value: { status: "unknown", reason: "Array hole has no own value." },
});
continue;
}
items.push(evaluateExpression(element, nestedContext(context)));
items.push({
name: String(items.length),
presence: "present",
value: evaluateExpression(element, nestedContext(context)),
});
}
return unknownItems
? { status: "array", items, itemPresence, unknownItems: true, omittedItems }
? { status: "array", items, unknownItems: true, omittedItems }
: {
status: "array",
items,
itemPresence,
unknownItems: false,
omittedItems: 0,
};
@@ -436,46 +445,16 @@ const projectValue = (
value: JavaScriptSemanticValue,
projection: readonly (string | number | null)[],
): JavaScriptSemanticValue => {
let current = value;
for (const key of projection) {
if (key === null)
return {
if (projection.includes(null))
return { status: "unknown", reason: "Cannot project a dynamic property." };
const path = projection.map((key) => String(key));
const slot = semanticSlotAtPath(value, path);
return slot.presence === "present"
? slot.value
: {
status: "unknown",
reason: "Cannot project a dynamic property.",
reason: `Own property ${path.join("/")} is ${slot.presence}.`,
};
if (current.status === "object" && typeof key === "string") {
const property = current.properties.find(({ name }) => name === key);
if (property === undefined)
return {
status: current.unknownProperties ? "unknown" : "unknown",
reason: `Object property ${key} was not observed.`,
};
current = property.value;
} else if (current.status === "array") {
const index = typeof key === "number" ? key : Number(key);
if (
!Number.isSafeInteger(index) ||
index < 0 ||
String(index) !== String(key)
)
return {
status: "unknown",
reason: `Array property ${String(key)} is not a canonical index.`,
};
const item = current.items[index];
if (item === undefined)
return {
status: "unknown",
reason: `Array item ${String(key)} missing.`,
};
current = item;
} else
return {
status: "unknown",
reason: `Cannot project ${String(key)} from ${current.status}.`,
};
}
return current;
};
const provenanceForBinding = (