a2ui-project / a2ui-project/a2ui

[BUG]: GenericBinder misclassifies nested dynamic unions as STATIC

オープン
#2,530 コメント 1 件 リアクション 0 件 担当者 1 名 @andrewkolos が担当を希望しています GitHub で見る
status: first-line-handled
主要言語
TypeScript
スター
16.4k
フォーク
1.3k
平均マージ
2日 13時間
マージ済み PR(30日)
134

説明

- [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` decides per property how to treat the incoming JSON value by scraping the component's Zod schema (`getFieldBehavior` in `renderers/web_core/src/v0_9/rendering/generic-binder.ts`). A property classified `DYNAMIC` gets the full data-binding treatment: `{path}` and `{call}` values are resolved through the `DataContext`, subscribed for updates, and a runtime setter is generated. A property classified `STATIC` is passed through untouched.

`getFieldBehavior` recognizes a dynamic property two ways:

- a `REF:` marker in the schema description (`#/$defs/DynamicString` and friends)
- a structural check that scans a `ZodUnion`'s options for the `DataBindingSchema` object shape
(an object with `path` and no `componentId`).

Both checks look exactly one level deep.

### The bug

A schema that composes a `Dynamic*` schema into a wider union defeats both checks. The basic catalog's own `DateTimeInput` does this for `min` and `max` (`renderers/web_core/src/v0_9/basic_catalog/components/basic_components.ts`):

```ts
'min': z
.union([DynamicStringSchema, z.string().date(), z.string().time(), z.string().datetime()])
.describe('The minimum allowed date/time in ISO 8601 format.')
.optional(),
```

~~The outer `.describe()` replaces the description, so the union carries no `REF:` marker.~~

> **Correction (per review on #2531):** `.describe()` is not what hides the marker. `z.union([...])` creates a new schema with no description of its own, so the outer union never had a `REF:` marker to replace. The marker stays on the nested `DynamicStringSchema`; `getFieldBehavior` simply does not read the descriptions of a union's options. Removing the `.describe()` would not change the outcome.

And `DynamicStringSchema` is itself a `ZodUnion`, not a `ZodObject`, so the structural scan never sees the `{path}` branch nested inside it. Both properties scrape as `STATIC`, while the sibling `value`, a bare `DynamicStringSchema`, scrapes as `DYNAMIC`.

The consequences, all silent:

- A bound `min` reaches the component implementation as the raw `{path: '...'}` object: never resolved, never subscribed. The React implementation's defensive check (`typeof props.min === 'string' ? props.min : undefined`) then discards the object, so the constraint is simply dropped.
- The type layer and the runtime disagree about setters. `GenerateSetters` sees the `DataBinding` branch in the union and declares `setMin`/`setMax` on the resolved props type, but the binder only generates runtime setters for `DYNAMIC` properties, so the typed method is `undefined` at runtime.

The trap is not specific to `DateTimeInput`: any catalog schema that wraps a `Dynamic*` schema inside a wider union (hand-written Zod catalogs can) gets the same misclassification.

## Steps to Reproduce

1. Scrape the shipped `DateTimeInput` schema:

```ts
import {scrapeSchemaBehavior} from '@a2ui/web_core/v0_9';
import {DateTimeInputApi} from '@a2ui/web_core/v0_9/basic_catalog';

const behavior = scrapeSchemaBehavior(DateTimeInputApi.schema);
console.log(behavior.shape.min); // {type: 'STATIC'} - wrong
console.log(behavior.shape.value); // {type: 'DYNAMIC'} - correct, same DynamicStringSchema
```

2. Or observe it end to end: render a surface with a `DateTimeInput` whose `min` is `{"path": "/limits/min"}` and any value at `/limits/min` in the data model. The rendered input has no minimum constraint, and updating `/limits/min` has no effect.

## Expected Behavior

`min` and `max` classify as `DYNAMIC` like their sibling `value`: a bound value is resolved, subscribed, and delivered to the component as a string, and the generated setters exist at runtime as the props type promises.

## Environment Details

- **OS**: macOS 15
- **Browser/Platform**: Node.js 22
- **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** - make the structural union check recurse: a union any of whose branches is itself dynamic (nested `Dynamic*` union, or a branch carrying a `Dynamic*`/`DataBinding` `REF:` description) is dynamic. The fix belongs in the scraper rather than in the `DateTimeInput` schema, because third-party catalogs composing `Dynamic*` schemas into wider unions hit the same misclassification.

Prior art on the `v1_0` branch, where this is already solved twice over:

- the v1.0 basic catalog flattened `min`/`max` to a bare `DynamicStringSchema` with the `REF:` marker preserved, removing the trigger
- #2353 rewrites the classifier so the union check reads each option's `REF:` definition name (`getRefDefName(o).startsWith('Dynamic')`), which classifies this exact shape as `DYNAMIC`

コントリビューションガイド

コントリビューションガイドを開く

評価

この issue はまだ評価されていません。

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。