Skip to content

Commit f31e008

Browse files
guitavanoclaude
andcommitted
fix(engine): drop never-gated hidden array items instead of null holes
Hiding an array item wraps it in a multivariate flag gated by a `never` matcher; flag.ts returns `undefined` when no variant matched. The engine left that `undefined` as a hole in the resolved array, which serializes to `null` in JSON and renders as an empty card (blank benefit rows, etc.). Compact `undefined` out of resolved arrays in resolvePropsWithHints. A resolver returning `null` is a legitimate value and is kept — only the `undefined` "not present" sentinel is filtered. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 53410fe commit f31e008

2 files changed

Lines changed: 42 additions & 0 deletions

File tree

engine/core/mod.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,39 @@ Deno.test("resolve", async (t) => {
153153
});
154154
});
155155

156+
await t.step(
157+
"drops hidden array items (resolver -> undefined) but keeps null",
158+
async () => {
159+
// A hidden array item is a multivariate flag gated by a `never` matcher;
160+
// flag.ts returns `undefined` when no variant matched. Such items must be
161+
// dropped from the array, not left as holes (which serialize to `null`
162+
// and render as empty cards). A resolver returning `null` is a legitimate
163+
// value and is kept.
164+
const resolverMap = {
165+
resolve: (data: unknown) => context.resolve(data),
166+
hiddenFlag: (): unknown => undefined,
167+
nullFlag: (): unknown => null,
168+
keep: (p: { label: string }) => p,
169+
};
170+
const result = await resolve<{ items: unknown[] }>(
171+
{
172+
items: [
173+
{ label: "A", __resolveType: "keep" },
174+
{ __resolveType: "hiddenFlag" },
175+
{ label: "B", __resolveType: "keep" },
176+
{ __resolveType: "nullFlag" },
177+
{ __resolveType: "hiddenFlag" },
178+
],
179+
__resolveType: "resolve",
180+
},
181+
{ ...context, resolvers: resolverMap as unknown as ResolverMap },
182+
);
183+
assertEquals(result, {
184+
items: [{ label: "A" }, { label: "B" }, null],
185+
});
186+
},
187+
);
188+
156189
await t.step("resolves object with no resolvable fields", async () => {
157190
type TestType = {
158191
foo: string;

engine/core/resolver.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -424,6 +424,15 @@ const resolvePropsWithHints = async <
424424
}
425425

426426
if (!type) {
427+
// Drop array items that resolved to `undefined` — a hidden array item is a
428+
// multivariate flag gated by a `never` matcher, and flag.ts returns
429+
// `undefined` when no variant matched. Left in place it survives as a hole
430+
// (and serializes to `null` in JSON), which the consuming section renders
431+
// as an empty card. A `null` from a resolver is a legitimate value and is
432+
// kept — only the `undefined` "not present" sentinel is filtered.
433+
if (Array.isArray(mutableProps)) {
434+
return mutableProps.filter((item) => item !== undefined) as T;
435+
}
427436
return mutableProps;
428437
}
429438

0 commit comments

Comments
 (0)