fix(environment): stop identity checks at the first missing or repeated value

identifies built every value before checking, so a wide first item
followed by many sparse items cost a full scan per suffix-named field.
It now returns at the first missing or repeated value; a regression test
counts reads over a 2,000-field item and 5,000 empty ones.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-19 23:28:15 -07:00
1 parent aa48c746d8
commit 5bb7ca421c
2 files changed
+41 -6

No files matched your search

@@ -398,11 +398,14 @@ export const identifies = (
items: readonly JsonObject[],
name: string,
): boolean => {
const values = items.map((item) => scalarText(item[name]));
return (
values.every((value) => value !== undefined) &&
new Set(values).size === values.length
);
// Stop at the first missing or repeated value, so a sparse list costs one read per field.
const seen = new Set<string>();
for (const item of items) {
const value = scalarText(item[name]);
if (value === undefined || seen.has(value)) return false;
seen.add(value);
}
return true;
};
/** Fields ending in one of these may identify listed items that no identity name identifies. */
@@ -17,7 +17,12 @@ import {
type AnalysisViewResult,
} from "./analysisView.js";
import { EnvironmentStateAccumulator } from "./EnvironmentStateAccumulator.js";
import { at, replaceAt } from "./environmentStateRules.js";
import {
at,
identityCandidates,
replaceAt,
type JsonObject,
} from "./environmentStateRules.js";
type State = Extract<AnalysisViewResult, { kind: "environment-state" }>;
const ALL_RULES = [
@@ -1007,3 +1012,30 @@ it("reads only item totals and next-page markers as signs of a partial list", ()
["recorded", []],
]);
});
it("reads a sparse list once per candidate field when inferring identity", () => {
const fields = 2_000;
const wide: JsonObject = Object.fromEntries(
Array.from({ length: fields }, (_, index) => [
`f${String(index)}Id`,
index,
]),
);
let reads = 0;
const sparse = Array.from(
{ length: 5_000 },
() =>
new Proxy<JsonObject>(
{},
{
get: (target, key, receiver) => {
reads += 1;
return Reflect.get(target, key, receiver) as unknown;
},
},
),
);
expect(identityCandidates([wide, ...sparse])).toEqual([]);
// Each identity name and suffix field stops at the first sparse item.
expect(reads).toBeLessThanOrEqual(fields + 20);
});