Skip to content

Commit b5d1eb6

Browse files
authored
Merge pull request #256 from bbopen/refactor/0.7.0
feat!: 0.7.0 scientific data plane — foundation (IR 0.3.0 member capture, capabilities, Arrow DX, dev smoke)
2 parents 790dbfb + 3da64b6 commit b5d1eb6

38 files changed

Lines changed: 2764 additions & 430 deletions

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,25 @@
11
# Changelog
22

3+
## [0.7.0](https://github.com/bbopen/tywrap/compare/v0.6.1...v0.7.0) (2026-06-01)
4+
5+
The foundation half of the scientific data plane. It lands the measurement, capability, and Arrow-ergonomics groundwork the large-payload transport work (0.8.0) builds on, and captures Python class members the IR used to drop. The wire protocol is unchanged, so a 0.6.x bridge and a 0.7.0 client still talk.
6+
7+
### Breaking changes
8+
9+
**The IR schema is now `0.3.0` and captures more class members.** `tywrap-ir` reads `@classmethod`, `@property`, and `functools.cached_property` (via `inspect.classify_class_attrs`) and labels `@staticmethod` correctly. Generated wrappers gain `static` members for class/static methods and `readonly` getters for properties, so **generated output changes for any class that uses those decorators** — classes without them are byte-identical. Regenerate your wrappers after upgrading. The TS↔Python IR-version check enforces the bump.
10+
11+
### Features
12+
13+
- Arrow decoding is frictionless: the runtime auto-registers an `apache-arrow` decoder when the package is present, with no manual wiring. JSON fallback stays opt-in (`TYWRAP_CODEC_FALLBACK=json`), and a missing `apache-arrow` fails with a clear, actionable error instead of silent lossy output. ([#232](https://github.com/bbopen/tywrap/issues/232))
14+
- Each backend reports a `TransportCapabilities` descriptor (`backend`, `supportsArrow`, `supportsBinary`, `supportsChunking`, `supportsStreaming`, `maxFrameBytes`), surfaced on the bridge via `capabilities()`, with a documented capability matrix. ([#235](https://github.com/bbopen/tywrap/issues/235))
15+
- `tywrap/dev` gains a watch/reload end-to-end smoke and documented reload failure/recovery behavior. ([#228](https://github.com/bbopen/tywrap/issues/228))
16+
17+
### Internal
18+
19+
- Measure-first data-plane benchmarks (Arrow round-trip, 100k-row decode, size-guard overhead, pool throughput) land as baselines for 0.8.0's perf gates — no gating yet.
20+
- `generate()` and `fetchPythonIr()` are decomposed (cognitive complexity 91→16), and a shared `BasePythonBridge` removes the duplicated RPC delegation across the three bridges.
21+
- The bridge dispatches `@classmethod`/`@staticmethod` through a dotted `call('Class.method')` and reads `@property`/`cached_property` through `call_method`, with the private-attribute guard re-applied per dotted segment.
22+
323
## [0.6.1](https://github.com/bbopen/tywrap/compare/v0.6.0...v0.6.1) (2026-05-31)
424

525
A maintenance release. Nothing you call changes — no API, behavior, or wire-protocol changes.

ROADMAP.md

Lines changed: 66 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,38 @@ The detailed technical appendix for the scientific data plane lives in
88

99
## Recently Shipped
1010

11+
### v0.6.1: maintenance (complexity and dedup)
12+
13+
`v0.6.1` is internal-only — no API, behavior, or wire-protocol changes. It
14+
removed two dead exports, broke the eleven worst complexity hotspots into
15+
smaller output-preserving helpers (cache-key generation, type-hint validation,
16+
the dev watch/reload paths, the subprocess write queue, module discovery, path
17+
and interpreter resolution, an annotation-parser helper), and factored the
18+
duplicated request/response dispatch in the codec and RPC client into one path.
19+
Static-analysis actionable complexity dropped from 14 to 3.
20+
21+
### v0.6.0: one breaking cleanup pass
22+
23+
`v0.6.0` collected the rest of the 0.5.0 refactor plan into a single breaking
24+
release so users take the import and name churn once, before the data plane adds
25+
new surface. The wire protocol did not change, so a 0.5.x client and a 0.6.0
26+
bridge still talk.
27+
28+
- trimmed `src/index.ts` to its real public surface; `SafeCodec` (renamed
29+
`BridgeCodec`) and the `Transport` contract moved to `tywrap/runtime`
30+
- standardized the runtime vocabulary on the four-layer glossary (`*IO`
31+
`*Transport`, `IntelligentCache``ArtifactCache`,
32+
`WorkerPool`/`PooledWorker``TransportPool`/`TransportLease`, `marker`
33+
`typeTag`); on-the-wire keys unchanged
34+
- stricter per-section config validation; dropped the dead per-module `runtime`
35+
field (#230)
36+
- the Python bridge blocks private-attribute access by default
37+
(`TYWRAP_ALLOW_PRIVATE_ATTRS=1` to opt out) and validates module names before
38+
discovery, closing two escape paths
39+
- single-sourced `VERSION` and `IR_VERSION` with a TS↔Python drift check (#229)
40+
- deleted four dead modules and collapsed the generator's three duplicated
41+
call-emission paths into one, output byte-preserved
42+
1143
### v0.5.1: install with no native build (Node 25+)
1244

1345
`v0.5.1` removed the dead TypeScript analyzer and the `tree-sitter`,
@@ -33,70 +65,49 @@ entrypoint, Node watch sessions that regenerate wrappers and swap the active
3365
bridge, and structured generation failures that keep the last known good output
3466
and bridge live.
3567

36-
## Now (0.6.0): one breaking cleanup pass
37-
38-
This is the rest of the 0.5.0 refactor plan that 0.5.0/0.5.1 did not ship —
39-
collected into a single breaking release so users take the import and name churn
40-
once, before the data plane adds new surface. Pre-1.0 with very few users, the
41-
breaking budget is cheap; spend it here and emerge on clean names.
42-
43-
Internal cleanup (invisible to callers):
44-
45-
- delete the four remaining dead modules (`bundle-optimizer`, `memory-profiler`,
46-
`optimized-node`, `protocol` — drain its `PROTOCOL_VERSION` into `transport`
47-
first)
48-
- decompose the live complexity hotspots (`decodeEnvelopeCore`,
49-
`annotation-parser.parse`, `mapPresetType`, the config dispatches) behind
50-
output-preserving characterization snapshots
51-
- extract a shared call-emission path in the generator (three near-identical
52-
copies collapse to one, generated output byte-preserved)
53-
- convert the silent test skips to `it.skipIf` so a missing Python interpreter
54-
skips loudly instead of passing vacuously
55-
- fix `VERSION` (still reports `0.3.0`) by single-sourcing it from a
56-
build-generated module
57-
58-
Breaking surface and naming:
59-
60-
- trim `src/index.ts` to its real public surface and move `SafeCodec` plus the
61-
`Transport` contract to `tywrap/runtime`, locked by a type-level surface test
62-
- standardize the runtime vocabulary on the four-layer glossary: `*IO`
63-
`*Transport`, `SafeCodec``BridgeCodec`, `IntelligentCache`
64-
`ArtifactCache`, `WorkerPool`/`PooledWorker``TransportPool`/`TransportLease`,
65-
`marker``typeTag`; the on-the-wire keys do not change, only code identifiers
66-
- tighten config loading with per-section validators and demote the dead
67-
per-module `runtime` field (#230)
68-
- single-source `IR_VERSION` (today duplicated across six files) and add a drift
69-
check, the foundation of the `tywrap``tywrap-ir` compatibility contract
70-
(#229)
71-
- land the security must-dos that change behavior: the import/`getattr` allowlist
72-
in the Python bridge and the module-name injection fix in discovery
73-
74-
## Next (0.7.0): the scientific data plane
75-
76-
The release that makes large scientific payloads reliable and first-class, built
77-
on the clean foundation 0.6.0 establishes (this is the workstream that was
78-
labeled 0.5.0 before the refactor took that number). Tracked under #237.
79-
80-
- measure first: Arrow, large-payload, and pool benchmarks land before any perf
81-
gate so the gates have real baselines
82-
- add a versioned artifact or chunked transport path so large payloads no longer
83-
depend on single-line JSONL (#231)
84-
- make Arrow registration frictionless across the runtime story (#232)
85-
- expand scientific codec validation and performance gates (#233)
86-
- harden SciPy, Torch, and Sklearn envelope behavior so supported cases are
87-
explicit and unsupported cases fail clearly (#234)
88-
- document transport capability expectations across Node, Pyodide, and HTTP (#235)
68+
## Now (0.7.0): the scientific data plane — foundation
69+
70+
The scientific data plane (tracked under #237) makes large numpy/pandas/Arrow
71+
payloads reliable and first-class. It is large, and the roadmap's own rule is
72+
*measure first: benchmarks land before any perf gate*. The headline chunked
73+
transport (#231) is still undesigned. So the theme ships in two releases.
74+
75+
`0.7.0` is the foundation half — everything buildable today with no
76+
wire-protocol design pass:
77+
78+
- measure first: Arrow round-trip, large-payload decode, size-check overhead,
79+
and pool-throughput benchmarks land against current behavior so 0.8.0's perf
80+
gates have real baselines
81+
- make Arrow registration frictionless — the JS runtime auto-registers an Arrow
82+
decoder when `apache-arrow` is present (#232)
83+
- a `TransportCapabilities` descriptor on each backend, reconciled with the
84+
bridge `meta` report, plus a documented capability matrix across Node,
85+
Pyodide, and HTTP (#235) — the contract #231 chunking keys off
8986
- capture the dropped Python member categories in `tywrap-ir` (`@classmethod`,
9087
`@property`, `cached_property` via `inspect.classify_class_attrs`), bump the IR
91-
schema, and regenerate goldens
88+
schema, and regenerate goldens — the one breaking change
9289
- stabilize the `tywrap/dev` examples with a watch/reload end-to-end smoke (#228)
90+
- fold in the complexity cleanup deferred from 0.6.1 (decompose `generate` and
91+
`fetchPythonIr`; collapse the cross-bridge `call`/`instantiate` boilerplate)
9392

9493
See [docs/codec-roadmap.md](./docs/codec-roadmap.md) for the deeper technical
9594
plan behind this release theme.
9695

96+
## Next (0.8.0): large-payload transport
97+
98+
The design-then-build half, built on 0.7.0's baselines and capability
99+
descriptor:
100+
101+
- design and add a versioned artifact or chunked transport path so large
102+
payloads no longer depend on single-line JSONL (#231)
103+
- expand scientific codec validation and set performance gates from the 0.7.0
104+
baselines (#233)
105+
- harden SciPy, Torch, and Sklearn envelope behavior so supported cases are
106+
explicit and unsupported cases fail clearly (#234)
107+
97108
## Later
98109

99-
These items are intentionally not part of `0.6.0` or `0.7.0`:
110+
These items are intentionally not part of `0.7.0` or `0.8.0`:
100111

101112
- GPU-native transport such as DLPack or Arrow CUDA
102113
- HTTP server lifecycle management owned by Tywrap

docs/.vitepress/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export default defineConfig({
2727
items: [
2828
{ text: 'Getting Started', link: '/guide/getting-started' },
2929
{ text: 'Configuration', link: '/guide/configuration' },
30+
{ text: 'Watch & Reload', link: '/guide/dev-reload' },
3031
{
3132
text: 'Runtime Bridges',
3233
collapsed: false,

docs/codec-roadmap.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ This is a forward-looking plan for adding codecs beyond numpy/pandas. The focus
1616

1717
## DX Defaults (Decisions)
1818

19-
- Arrow is the default for ndarray/dataframe/series. The JS runtime should auto-register an Arrow decoder when `apache-arrow` is installed so users do not have to wire it manually.
19+
- Arrow is the default for ndarray/dataframe/series. The JS runtime auto-registers an Arrow decoder when `apache-arrow` is importable, so users do not have to wire it manually. Node/Bun/Deno and HTTP bridges register eagerly during `init()`; in addition, the codec lazily imports `apache-arrow` on the first Arrow-encoded response (cached for the process) so callers that bypass `init()` still work.
20+
- `apache-arrow` is an **optional** dependency: it is not in tywrap's runtime `dependencies` (declared as an optional peer). Installing tywrap never pulls it in or fails without it.
21+
- No silent lossy fallback: if an Arrow-encoded payload arrives and `apache-arrow` is unavailable, decoding fails with an actionable error telling you to `npm install apache-arrow` or set `TYWRAP_CODEC_FALLBACK=json` on the Python side. tywrap never quietly downgrades Arrow data to JSON.
2022
- JSON fallback is opt-in only (via `TYWRAP_CODEC_FALLBACK=json`) and remains explicitly lossy for dtype/NA fidelity.
2123
- GPU handling stays explicit: no implicit `.cpu()` or contiguous copies. Opt-in copy/transfer remains available, and GPU-native transport is a follow-up track (DLPack/Arrow CUDA).
2224
- Large payloads should not be forced through single-line JSONL forever; add an artifact/chunked transport to keep responses reliable without silent truncation.

docs/guide/dev-reload.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Watch & Reload (Failure / Recovery Contract)
2+
3+
`tywrap/dev` provides development-time wrapper regeneration plus runtime bridge
4+
replacement. It does **not** provide application-level hot module reloading — it
5+
keeps your generated wrappers and the active Python bridge in sync with your
6+
Python sources while your process stays up.
7+
8+
This page documents the reload **lifecycle** and, in particular, the
9+
**failure / recovery contract**: what happens when a reload fails, and what stays
10+
live so your app keeps working.
11+
12+
> Reload is configured in code through `tywrap/dev`, never in `tywrap.config.*`.
13+
> The legacy `development` block and `pythonModules.<module>.watch` fields were
14+
> removed in 0.4.0; using them now raises an explicit migration error.
15+
16+
## The two entry points
17+
18+
| Helper | Use it for |
19+
| --- | --- |
20+
| `startNodeWatchSession(...)` | Node-only. Watches your config file and local Python sources, regenerates wrappers, and swaps the active bridge in place. |
21+
| `createBridgeReloader(...)` | Cross-runtime manual primitive (e.g. Pyodide). You call `reload()` yourself; there is no filesystem watcher. |
22+
23+
```typescript
24+
import { startNodeWatchSession } from 'tywrap/dev';
25+
import { NodeBridge } from 'tywrap/node';
26+
27+
const session = await startNodeWatchSession({
28+
configFile: './tywrap.config.ts',
29+
createBridge: async config =>
30+
new NodeBridge({
31+
pythonPath: config.runtime.node?.pythonPath ?? 'python3',
32+
timeoutMs: config.runtime.node?.timeout ?? 30000,
33+
}),
34+
});
35+
36+
// Force a rebuild now (resolves to `true` on success, `false` on failure):
37+
const ok = await session.reloadNow();
38+
39+
// Stop watching and dispose the active bridge:
40+
await session.close();
41+
```
42+
43+
`createBridge` receives the **freshly resolved config for that reload cycle**, so
44+
config edits (e.g. a changed `timeout`) are picked up on the next reload. By
45+
default the newly created bridge is published to the global runtime registry, so
46+
existing generated wrappers transparently route through the swapped bridge — no
47+
imports change and the process never restarts.
48+
49+
## Reload lifecycle events
50+
51+
Pass an `onEvent` callback to observe the session. Events are emitted in this
52+
order for a successful cycle:
53+
54+
| Event | Meaning |
55+
| --- | --- |
56+
| `watchPaths` | The set of paths the session is currently watching (config file, resolved Python package trees, and `extraWatchPaths`). Re-emitted whenever the watched set changes. |
57+
| `change` | A watched path changed (`manual: false`) — a reload has been scheduled. |
58+
| `reload-start` | A reload cycle began (`manual: true` for `reloadNow()` / initial startup). |
59+
| `reload-success` | Regeneration and bridge swap completed. Carries `written` (relative paths of the generated files now on disk) and `warnings`. |
60+
| `reload-error` | The reload failed. Carries the underlying `error: Error`. |
61+
62+
A reload performs these steps atomically with respect to the live state:
63+
64+
1. **Generate** the next wrapper set into a temporary staging directory.
65+
2. **Promote** the staged files into the real output directory (writing new
66+
files, removing managed files that no longer exist).
67+
3. **Warm and activate** the next bridge, then dispose the previous one.
68+
4. **Commit** the refreshed watcher set and emit `reload-success`.
69+
70+
`reloadNow()` and filesystem changes are serialized: an in-flight reload
71+
finishes before the next one starts, and rapid edits are debounced
72+
(`debounceMs`, default `100`).
73+
74+
## Failure / recovery contract
75+
76+
If **any** step of a reload throws, the session does **not** tear down. The
77+
last-known-good state stays live:
78+
79+
- The **previously generated wrappers remain on disk unchanged.** Staging happens
80+
in a temp directory; the real output directory is only touched once generation
81+
succeeds. If promotion itself fails partway, the previous file contents are
82+
restored.
83+
- The **previously active bridge stays active** in the runtime registry and is
84+
**not disposed.** A bridge that was warmed for the failed reload is disposed
85+
instead, so you never leak the half-prepared one.
86+
- A structured `reload-error` event is emitted with the underlying `Error`, and
87+
the failing call (`reloadNow()` or the debounced auto-reload) resolves to
88+
`false`. The session keeps watching and will attempt the next reload normally.
89+
90+
Two distinct failure sources surface the same way:
91+
92+
| Source | What the `error` looks like |
93+
| --- | --- |
94+
| **Generation failure** (a watched module's IR can't be produced — e.g. a syntax error). This is the `GenerateFailure` path. | `error.message` begins with `Generation failed for N module(s):` followed by per-module detail. |
95+
| **Bridge construction failure** (your `createBridge` throws). | The error your factory threw, propagated verbatim. |
96+
97+
```typescript
98+
const session = await startNodeWatchSession({
99+
configFile: './tywrap.config.ts',
100+
createBridge,
101+
onEvent: event => {
102+
if (event.type === 'reload-error') {
103+
// Last-good wrappers + bridge are still live here.
104+
console.error('[tywrap] reload failed, keeping last-good state:', event.error.message);
105+
}
106+
if (event.type === 'reload-success') {
107+
console.log('[tywrap] reloaded:', event.written.join(', '));
108+
}
109+
},
110+
});
111+
```
112+
113+
> **Startup is the exception.** There is no last-known-good state on the very
114+
> first reload, so if the *initial* `startNodeWatchSession(...)` setup fails, the
115+
> promise rejects (watchers are closed and any partial bridge disposed). After a
116+
> successful start, every subsequent failure is recoverable as described above.
117+
118+
## Other notes
119+
120+
- Watched Python package trees are watched per-directory; new nested directories
121+
are picked up automatically and `__pycache__` / `.pytest_cache` /
122+
`.mypy_cache` / `.ruff_cache` churn is ignored.
123+
- Writes into the output directory and `.tywrap/{cache,reports}` are ignored so
124+
generation does not retrigger itself.
125+
- For Pyodide use `createBridgeReloader(...)` and drive `reload()` from your own
126+
trigger. For the HTTP runtime, reload by restarting/redeploying the remote
127+
server — that is external to tywrap.

docs/guide/getting-started.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -303,7 +303,8 @@ tywrap.
303303
`startNodeWatchSession(...)` watches local package directories as directory
304304
trees, refreshes those trees when nested directories change, and keeps the last
305305
known good wrappers and bridge live if a reload produces structured generation
306-
failures.
306+
failures. See [Watch & Reload](./dev-reload.md) for the reload lifecycle events
307+
and the full failure / recovery contract.
307308

308309
### Build Integration
309310

docs/guide/runtimes/node.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ directory-valued `extraWatchPaths`, ignores Python cache directories, and keeps
5454
the last known good wrappers plus bridge live if a reload returns structured
5555
generation failures.
5656

57+
See [Watch & Reload](../dev-reload.md) for the reload lifecycle events and the
58+
full failure / recovery contract.
59+
5760
## Basic Setup
5861

5962
### Installation

0 commit comments

Comments
 (0)