a2ui-project / a2ui-project/a2ui

[BUG]: GenericBinder generates an uncallable setter for a binding-only property

Đang mở
#2,528 1 bình luận 0 reaction 1 người được giao Được @andrewkolos nhận Xem trên GitHub
status: first-line-handled
Ngôn ngữ chính
TypeScript
Star
16.4k
Fork
1.3k
Merge trung bình
2 ngày 13 giờ
Pull request đã merge (30 ngày)
134

Mô tả

- [x] I have searched the existing issues to make sure this bug has not already been reported.

## Describe the Bug

### Background

`@a2ui/web_core`'s `GenericBinder` (`renderers/web_core/src/v0_9/rendering/generic-binder.ts`) is
the engine that turns a component's raw A2UI JSON config plus its Zod schema into the resolved,
strongly typed props a renderer hands to a component implementation. The React and Lit renderers
both build their component props on it (Angular does not, since it rolls its own binding).

Two exported utility types in that file describe the result:

- `ResolveA2uiProp` types what a component **reads**: an `Action` becomes `() => void`, a
`DynamicString` becomes `string`, and so on.
- `GenerateSetters` types what a component **writes**: for a `value: DynamicString` property
it generates a `setValue(v: string)` method for two-way binding.

Both start from the same idea. A dynamic property is declared as a union of literal branches plus
the binding shapes, for example `DynamicString = string | DataBinding | FunctionCall`. To find the
resolved type, both subtract the binding shapes: `Exclude`.

### The bug

A property can be declared with **only** binding branches and no literal one, for example
`z.union([DataBindingSchema, FunctionCallSchema])`. For such a property the subtraction removes
everything and leaves nothing, and the two types disagree about what to do next.

`ResolveA2uiProp` guards that case and falls back to `any` (lines 175-177):

```ts
: Exclude extends never
? any
: Exclude;
```

`GenerateSetters` has no equivalent guard (same file, line 185):

```ts
value: Exclude, DynamicTypes>,
```

So for a property declared as `DataBinding | FunctionCall` the read side falls back to `any`,
while the write side resolves to `(value: never) => void`. `never` is uninhabited, so no value
can be passed and the generated setter cannot be called at all. The asymmetry is the whole
defect: the read path has a fallback and the write path was never given one.

No component in the basic catalog declares such a property, and `DataBinding` is absent from
`COMMON_TYPE_SCHEMAS` in `src/v0_9/catalog/schema_loader.ts`, so JSON-defined catalogs cannot
declare one either. The case is reachable from hand-written Zod component schemas, which is
presumably why it has gone unnoticed.

## Steps to Reproduce

1. Declare a component whose schema has a property with only binding branches.
2. Read the generated setter's parameter type.

```ts
import {z} from 'zod';
import {DataBindingSchema, FunctionCallSchema} from '@a2ui/web_core/v0_9';
import type {ResolveA2uiProps} from '@a2ui/web_core/v0_9';

const BindingOnly = z.object({sort: z.union([DataBindingSchema, FunctionCallSchema])});
declare const props: ResolveA2uiProps>;

props.sort; // any - the read-side fallback applies
props.setSort; // (value: never) => void - uncallable

props.setSort({path: '/sortOrder'});
```

3. Run `tsc --noEmit`. The call on the last line is rejected.

## Expected Behavior

The generated setter is callable. The read side already decides what to do when the subtraction
leaves nothing; the write side should make the same decision rather than producing a parameter
no value can satisfy.

## Screenshots / Video / Logs

```
error TS2345: Argument of type '{ path: string; }' is not assignable to parameter of type 'never'.
```

## Environment Details

- **OS**: macOS 15
- **Browser/Platform**: Node.js, TypeScript 5.9.3 (compile-time only, no runtime behaviour involved)
- **SDK/Package Name & Version**: `@a2ui/web_core` 0.10.7 (reproduced against `main` @ `0dc4a30f`)
- **Protocol Version**: v0.9, v0.9.1
- **Agent Framework & LLM Model**: n/a

## Additional Context

**Suggested fix** - give the write side the fallback the read side already has:

```ts
value: [Exclude, DynamicTypes>] extends [never]
? unknown
: Exclude, DynamicTypes>,
```

`unknown` rather than `any`: it accepts every argument while keeping the value checked at the
call site, and does not add to the `any` count tracked in #1296.

This is non-breaking. A parameter of type `never` admits no argument today, so widening it can
only permit calls that previously failed to compile; no existing call site can change meaning.

Hướng dẫn đóng góp

Mở hướng dẫn đóng góp

Đánh giá

Issue này chưa được đánh giá.

Nhận issue mới trong hộp thư của bạn

Bản tóm tắt ngắn những issue GitHub phù hợp với người mới.