Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
3b84b6f
docs(codec): specify bounded value extension protocols
bbopen Sep 20, 2026
98645b2
test(codec): prototype exact integers and dataclass output
bbopen Sep 20, 2026
5ea7635
test(codec): prototype cross-language value extensions
bbopen Sep 20, 2026
d7c2412
test(codec): reject ambiguous prototype envelopes
bbopen Sep 20, 2026
a367777
test(codec): decode exact requests with declared float nodes
bbopen Sep 20, 2026
bba1e66
test(codec): align prototype Unicode and version boundaries
bbopen Sep 20, 2026
8342221
test(codec): emit the counted Unicode wire form
bbopen Sep 20, 2026
11f4fde
test(codec): match prototype output bytes to checked limit
bbopen Sep 20, 2026
42ef2e4
test(codec): reject ordinary class as dataclass output
bbopen Sep 20, 2026
4f48192
test(codec): show serializable unsupported dataclass field
bbopen Sep 20, 2026
5893212
test(codec): prove generated Point through dataclass adapter
bbopen Sep 20, 2026
3862ebd
test: use generated camelCase Point callable
bbopen Sep 20, 2026
8a1d624
test: derive Point proof from analyzed importable fixture
bbopen Sep 20, 2026
27a5f3f
docs: record exact integer migration costs
bbopen Sep 20, 2026
0954106
docs: separate proposed policy from prototype checks
bbopen Sep 20, 2026
b22bdfa
test: bind generated Point call to explicit value policy adapter
bbopen Sep 20, 2026
9fb5457
docs: record generated Point proof and remaining boundary
bbopen Sep 20, 2026
9a4fc75
test: pin Point adapter rejection causes
bbopen Sep 20, 2026
c31157f
docs: mark bigint generator contract gap
bbopen Sep 20, 2026
f41bb44
test: pin dataclass input and depth rejection
bbopen Sep 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 131 additions & 0 deletions docs/maintainers/value-contract-prototypes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# Value extension prototypes

These rules are candidates for #337 and #339. They do not change the 0.11 codec
policy. The prototypes live under `test/prototypes`. Shipped wrappers cannot
import them.

## Exact integer option

The generation option is `integerMode: 'bigint'` for one callable. It maps every
Python `int` parameter and result to TypeScript `bigint`. Safe integers use the
same type. Python `bool` remains `boolean`. Calls without the option keep the
safe `number` rule.

The current value-contract revision represents Python `int` only as a safe JSON
`number`. The compiler and return validator follow that rule. This prototype
does not generate `bigint` wrappers; that needs a deliberate contract extension.

The proposed generated wrapper must require `exactIntegerDecimalV2` in the
bridge's `meta.valueCapabilities`. It must send
`params.valuePolicy.integer = 'bigint-v2'` on each call. A new bridge must
reject unknown policies. An old bridge lacks the capability, so the wrapper must
reject the call before sending a tag. The prototype tests this policy in an
isolated adapter. The shipped wrapper does not implement it.

The version 2 integer envelope is
`{"__tywrap__":"integer","codecVersion":2,"encoding":"decimal","value":"18446744073709551617"}`.
`codecVersion` uses the parsed JSON numeric value. The lexemes `2`, `2.0`, and
`2e0` all mean version 2. Booleans and strings do not. The decimal must match
`^(?:0|[1-9][0-9]*|-[1-9][0-9]*)$`. This excludes `-0`, leading zeros, plus
signs, spaces, exponents, and Unicode digits. The value permits at most 4096
digits, excluding the sign. It uses at most 4097 ASCII bytes. Whole request and
response byte limits still apply.

The TypeScript encoder tags every `bigint`, including safe values. The Python
request decoder requires the tag at each declared integer node. It rejects raw
JSON numbers there. It uses the trusted resolved value contract to distinguish
integer nodes from float nodes. A generated wrapper must not let a client supply
or change that contract.

`JSON.stringify(1.0)` emits `1`. The Python decoder converts that token to
`float` only at a declared float node. `JSON.stringify(-0)` emits `0`, so the
TypeScript encoder uses
`{"__tywrap__":"float","codecVersion":2,"encoding":"negative-zero"}` for
negative zero. The Python decoder accepts that tag only at a float node. It
rejects malformed float tags and nonfinite numbers.

The Python response encoder tags every `int`, including safe values. The
TypeScript response decoder returns `bigint` at declared integer nodes. The
generated return validator must require `bigint`. It must reject an ordinary
number and a raw tag. Arrow int64 keeps its current Arrow encoding and decoded
type.

Both prototypes walk nested arrays and string-keyed records within depth and
node limits. They reject unsupported values and malformed envelopes. Encoders
reject cycles with paths. Ordinary records cannot use `__tywrap__` as a key
because the decoder reserves it for envelopes.

The prototype counts UTF-8 bytes of compact JSON. Python emits Unicode
characters directly, as `JSON.stringify` does. Both paths reject unpaired
surrogates. Production must check raw wire bytes before parsing; object-level
checks do not prove equal byte counts for alternate numeric JSON spellings.

The bounded tests must round trip positive and negative values beyond 64 bits.
They must cover floats, booleans, nested records, malformed tags, digit limits,
byte limits, and old bridges. Production work needs separate review of
capability negotiation, generated types, and migration cost.

### Exact integer migration cost

Opting in changes every declared Python `int` at that callable to TypeScript
`bigint`, including values within the safe number range. Callers must update
number arithmetic, comparisons, and stored JSON. `JSON.stringify` rejects a
plain `bigint`, so callers that persist results need an explicit encoding.

Each tagged integer adds 73 JSON bytes beyond its decimal digits. For example,
`7` uses one byte as a JSON number and 74 bytes in the version 2 envelope.
Nested collections multiply that cost by their integer count. Teams must measure
representative call payloads against the byte limit before enabling the option.

The client must check the bridge capability before it sends tagged values. This
requires a coordinated client and bridge rollout. Old bridges reject the opted
call. Arrow int64 columns keep their current type and wire format.

Before production adoption, inventory generated call sites that use `int`,
measure payload growth, and typecheck downstream callers after the opt-in. The
prototype has not measured those application-specific costs.

## Dataclass output option

The generation option is `dataclassReturns: 'validated-v2'` for a callable with
an annotated dataclass result. Dataclass inputs remain unsupported. Ordinary
class instances do not qualify.

The generator compiles `dataclasses.fields` into a structural TypeScript record.
It includes fields with defaults and `init=False`. An instance returns those
fields. `Optional[T]` permits `null`; a default does not make a returned
property optional.

The proposed generated wrapper must require `dataclassFieldsV2` in
`meta.valueCapabilities`. It must send
`params.valuePolicy.dataclass = 'fields-v2'` on each call. The Python prototype
encoder accepts only the declared dataclass type. It emits a version 2 envelope
with `module.qualname` identity and an ordered `fields` record. It rejects
unsupported fields and cycles with paths. It applies depth, node, and byte
limits. It does not call `dataclasses.asdict` or create object handles.

The TypeScript prototype decoder checks marker, version, encoding, type
identity, and exact fields before it removes the envelope. It returns a plain
record. A module-private `WeakMap` stores verified type identity. The prototype
origin check reads that entry. A plain record cannot claim dataclass origin. A
nested dataclass gets its own entry. The current generated return validator
checks required field types but does not check origin or reject extra keys. The
adapter must perform those checks before it calls the validator.

The Python prototype checks the declared root dataclass type. It serializes
nested dataclasses by their observed runtime type and does not check a compiled
field contract. The TypeScript decoder rejects unexpected nested identities or
field values. Server-side field validation remains open.

The bounded Point proof in `test/value_extensions_generated_point.test.ts` uses
analyzer IR from an importable Python module. The compiler emits
`Promise<Point>` with required numeric `x` and `y`, including a defaulted `y`. A
bound prototype adapter returns `{x: 1, y: 2}` and rejects missing, extra, wrong
typed fields, and untagged records before the generated validator runs. The
generated validator independently rejects wrong field types. The binding rejects
a bridge without the Point policy adapter.

The Python prototype tests optional fields, nesting, cycles, unsupported fields,
depth, and byte limits separately. Production still needs bridge capability
negotiation, a server-side compiled field contract, and an independent design
review. The default runtime has no dataclass adapter.
14 changes: 14 additions & 0 deletions test/fixtures/python/value_contract_point.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""Importable Point fixture for the dataclass value proof."""

from dataclasses import dataclass


@dataclass
class Point:
x: int
y: int = 2


def make_point() -> Point:
"""Return a Point with its defaulted field present."""
return Point(1)
Loading
Loading