Skip to content

[codex] Safe TypeScript generic emission#210

Merged
bbopen merged 4 commits intomainfrom
codex/safe-typescript-generic-emission
Mar 20, 2026
Merged

[codex] Safe TypeScript generic emission#210
bbopen merged 4 commits intomainfrom
codex/safe-typescript-generic-emission

Conversation

@bbopen
Copy link
Copy Markdown
Owner

@bbopen bbopen commented Mar 19, 2026

Closes #209.

Summary

This PR implements safe TypeScript generic emission for issue #209. Before this change, tywrap did not preserve enough Python generic metadata end-to-end, so generated wrappers either dropped generics entirely or leaked raw placeholder syntax such as ~T, P.args, and Unpack[Ts]. That made generated .ts and .d.ts output lossy and, in some cases, invalid or misleading for downstream consumers.

Root cause

The Python IR did not carry ordered generic parameter metadata for functions, classes, and type aliases, and the local cache key did not distinguish IR schema versions. On the TypeScript side, the internal model dropped exported type aliases, the annotation parser treated generic placeholders as opaque names, and declaration files were derived after generation instead of being rendered from the same generic-aware pass.

Fix

This change adds ordered type_params extraction to the IR for functions, classes, and top-level assigned type aliases, and bumps the IR version to 0.2.0. The cache key now includes the IR version so stale cached JSON cannot hide the new schema.

On the TypeScript side, the model now carries declaration-level typeParameters and module-level typeAliases, the annotation parser understands ~T, ~P, P.args, P.kwargs, Unpack[Ts], and module-qualified generic references, and the generator emits declarations directly through GeneratedCode.declaration.

The generator now emits simple unconstrained TypeVar parameters as declaration-local generics, emits callable ParamSpec packs as P extends unknown[], makes generic class static create and fromHandle methods generic themselves, normalizes same-module dotted generic references to local names, and falls back to unknown or unknown[] when a Python construct cannot be represented safely. Bound or constrained TypeVars, TypeVarTuple, and Unpack remain conservative by design in this change.

The PR also adds regression coverage for IR extraction order, generic alias extraction, declaration output, cache invalidation, and an integration test that generates .ts and .d.ts files for a local generic Python module and typechecks them with a real TypeScript compile.

Validation

  • ./node_modules/.bin/tsc --noEmit
  • ./node_modules/.bin/vitest --run test/ir_cache_filename.test.ts test/ir_extraction_advanced.test.ts test/generator.test.ts test/integration.test.ts test/type_mapping_enhanced.test.ts test/type_mapping_edge_cases.test.ts

Notes

package-lock.json changed because the repository lockfile was out of sync and npm install was required before the validation commands could run.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 19, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

This PR introduces TypeScript generic type-parameter emission for Python generic constructs (TypeVar, ParamSpec, TypeVarTuple, Unpack). It extends the IR to track type-parameter metadata, updates annotation parsing to recognize generic constructs, and modifies code generation to emit TypeScript generic declarations while safely falling back to unknown for unsupported cases.

Changes

Cohort / File(s) Summary
Type System & IR Definition
src/types/index.ts, tywrap_ir/tywrap_ir/ir.py, tywrap_ir/tywrap_ir/optimized_ir.py
Extended Python IR with new generic-parameter metadata (PythonGenericParameter, PythonTypeAlias) and new typing constructs (ParamSpecType, ParamSpecArgsType, ParamSpecKwargsType, TypeVarTupleType, UnpackType). Python IR extraction now collects and preserves type-parameter information on functions, classes, and type aliases. IR version bumped to 0.2.0.
Annotation Parsing
src/core/annotation-parser.ts, test/annotation-parser.test.ts
Added parsing for ParamSpec, TypeVarTuple, and Unpack constructs with generic-parameter lookup table. Extended annotation parser to recognize ~<Name>.args/~<Name>.kwargs, Callable with paramspec, typing_extensions forms, and bracketed generics. Tests updated to validate ParamSpec argument-pack parsing behavior.
Code Generation & Type Mapping
src/core/generator.ts, src/core/mapper.ts, test/type_mapping_enhanced.test.ts, test/mapper.test.ts
Generator now computes per-function/class/method generic context and emits <...> type-parameter declarations in both overload signatures and implementations. Added generateTypeAlias(...) for generic type-alias output. Mapper extended to handle paramspec, paramspec_args, paramspec_kwargs, typevartuple, unpack kinds, with mapGenericType now returning broader TypescriptType to support non-generic TS shapes for known typing constructs. Tests validate generic type preservation and mapping of new constructs.
Integration & Wiring
src/tywrap.ts, src/index.ts, src/utils/ir-cache.ts, src/utils/runtime.ts
Updated tywrap IR consumption to thread type-parameter metadata through annotation parsing and apply inherited/local generics to function/class/type-alias outputs. Expanded public type exports to include new generic-parameter types. IR cache key now includes irVersion field; version set to 0.2.0. Minor whitespace adjustment in runtime hash utilities.
CLI & Python IR Version
tywrap_ir/tywrap_ir/__main__.py
Updated default --ir-version CLI argument from 0.1.0 to 0.2.0.
Comprehensive Test Coverage
test/generator.test.ts, test/integration.test.ts, test/ir_cache_filename.test.ts, test/ir_extraction_advanced.test.ts
Added unit tests for generic function/class/type-alias rendering, integration tests verifying TypeScript compilation of generated generic code with ParamSpec/TypeVarTuple/Unpack, protocol-method generics preservation, and IR cache filename sensitivity to version changes. Tests gate features on Python version/typing availability.

Sequence Diagram(s)

sequenceDiagram
    participant Extractor as IR Extractor<br/>(Python)
    participant Parser as Annotation Parser<br/>(TypeScript)
    participant Generator as Code Generator<br/>(TypeScript)
    participant Mapper as TypeMapper<br/>(TypeScript)
    participant Output as Generated<br/>Output

    Extractor->>Extractor: Extract type_params from<br/>functions/classes/aliases
    Extractor->>Extractor: Discover TypeVar,<br/>ParamSpec, TypeVarTuple
    Extractor->>Output: Emit IR 0.2.0 with<br/>type_params metadata

    Parser->>Parser: Build knownTypeParameters<br/>lookup from options
    Parser->>Parser: Parse annotation,<br/>consult generic registry
    Parser->>Parser: Convert generic references<br/>to PythonType nodes
    Parser->>Generator: Return annotated types

    Generator->>Generator: Compute per-function<br/>genericContext
    Generator->>Generator: Select which generics<br/>are safe to emit
    Generator->>Mapper: Request TypeScript types<br/>for sanitized Python types
    
    Mapper->>Mapper: Map paramspec →<br/>TSCustomType
    Mapper->>Mapper: Map generic constructs<br/>via mapKnownGenericType
    Mapper->>Mapper: Return mapped TS type
    Mapper->>Generator: TS type representation

    Generator->>Generator: Emit export function<br/><T>(...) signature
    Generator->>Generator: Render overloads &<br/>declarations
    Generator->>Output: Generate .ts & .d.ts<br/>with generic parameters
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~55 minutes

Possibly related PRs

Poem

🐇 Generics bloom in TypeScript's garden fair,
Where TypeVars dance and ParamSpecs declare,
From Python's depths to TypeScript's typed shore,
Generic wrappers rise forevermore!
Safe fallbacks guard when unknowns appear,
Making typed magic crystal clear.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.68% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title '[codex] Safe TypeScript generic emission' clearly and concisely summarizes the main change: implementing safe TypeScript generic emission for Python generics.
Description check ✅ Passed The description comprehensively explains the PR's purpose, root causes, fixes, and validation approach, all directly related to the changeset for safe generic emission.
Linked Issues check ✅ Passed The PR satisfies issue #209's acceptance criteria by emitting TypeScript generics for simple TypeVar cases across functions, classes, and aliases, supporting ParamSpec in callable signatures, safely falling back for TypeVarTuple/Unpack, and adding compile-verified integration tests.
Out of Scope Changes check ✅ Passed All changes are directly aligned with safe generic emission: IR version bump and type_params extraction, TypeScript model/parser enhancements, generator generic rendering, and comprehensive test coverage including integration tests with TypeScript typechecking.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/safe-typescript-generic-emission

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@bbopen bbopen marked this pull request as ready for review March 19, 2026 20:36
@bbopen
Copy link
Copy Markdown
Owner Author

bbopen commented Mar 19, 2026

@coderabbitai review

Please run a full review on this PR.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 19, 2026

@bbopen Sure! I'll kick off a full review of this PR right away.

✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot added documentation Improvements or additions to documentation enhancement New feature or request codex area:tooling Area: tooling and CLI priority:p2 Priority P2 (medium) labels Mar 19, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 180263c403

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread tywrap_ir/tywrap_ir/ir.py Outdated
returns = hints.get("return", sig.return_annotation)
is_async = inspect.iscoroutinefunction(obj) or inspect.isasyncgenfunction(obj)
is_generator = inspect.isgeneratorfunction(obj)
type_params = _collect_type_params_from_annotations(*annotations_for_params, returns) if include_type_params else []
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve PEP 695 function type parameters

On Python 3.12, generic functions declared with PEP 695 syntax (for example def identity[T](x: T) -> T) expose their type parameters on __type_params__, not in the annotations list. _extract_function currently derives type_params only from parameter/return annotations, so these functions end up with type_params=[]; downstream parsing then treats T as an undeclared custom type and generates wrappers like identity(x: T): Promise<T>, which do not type-check.

Useful? React with 👍 / 👎.

Comment thread tywrap_ir/tywrap_ir/ir.py Outdated
Comment on lines +325 to +326
type_params = _collect_type_params_from_annotations(value)
definition = _stringify_annotation(value) or annotation_str or str(value)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Unwrap PEP 695 type aliases before stringifying

For Python 3.12 type aliases, value is a typing.TypeAliasType and str(value) is just the alias name. In _extract_type_aliases, that means type Pair[T] = tuple[T, T] is serialized as definition='Pair' with no type_params, so the generator later emits export type Pair = Pair, which TypeScript rejects as a circular alias. This needs to read the alias payload/type params from the TypeAliasType wrapper instead of stringifying the wrapper itself.

Useful? React with 👍 / 👎.

Comment thread src/core/generator.ts
Comment on lines +158 to +160
if (param.kind === 'paramspec' && callableParamSpecs.has(param.name)) {
emitted.push({ name: param.name, declaration: `${param.name} extends unknown[]` });
emittedNames.add(param.name);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Emit ParamSpec when a generic alias carries it

This only emits a ParamSpec if it was seen inside a raw Callable[P, ...] node. When a public API uses a generic alias such as Transform[P, T] = Callable[P, T], P appears only as a generic type argument, so it is dropped during sanitization and the generated wrapper ends up using Transform<unknown, T>. That violates the alias’s P extends unknown[] constraint and makes the generated .ts/.d.ts fail to type-check.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/type-mapping-matrix.md`:
- Line 378: Update the footer date string "Last updated: January 2026." to the
current modification date (e.g., "Last updated: March 2026") in the
docs/type-mapping-matrix.md content so the document reflects the actual last
update; locate and replace the exact string "Last updated: January 2026." with
the correct month and year.

In `@test/generator.test.ts`:
- Around line 900-903: The `not.toContain('Ts')` assertion is too broad and can
false-positive when "Ts" appears inside other identifiers; update that assertion
in test/generator.test.ts to a more specific check targeting the actual type
token (e.g., assert absence of the token or generic usage like "Ts<" or use a
word-boundary regex such as /\bTs\b/ via expect(...).not.toMatch) so you only
fail when the standalone `Ts` type appears rather than any substring of other
identifiers.

In `@test/integration.test.ts`:
- Around line 163-175: The runtime-stub.d.ts used in the test is intentionally
minimal and currently omits RuntimeBridge.dispose(); update the string written
in the writeFile call (the template that defines RuntimeBridge and
getRuntimeBridge) to include a short comment above the interface explaining that
this stub only contains the methods used by generated wrappers and that the real
RuntimeExecution includes additional methods (e.g., dispose) which the stub
intentionally omits; mention that if future generated code calls other bridge
methods the stub must be expanded.

In `@tywrap_ir/tywrap_ir/ir.py`:
- Line 199: The expression computing paramspec_match uses two separate endswith
calls; replace the boolean OR pattern with a single endswith call that accepts a
tuple so it reads like text.endswith((".args", ".kwargs")) while preserving the
existing ternary logic that returns text.split(".", 1)[0] when matched and None
otherwise (i.e., update the paramspec_match assignment to use the tuple-based
endswith).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 36ebfc4e-5209-4c36-acf6-a0215bcfcd74

📥 Commits

Reviewing files that changed from the base of the PR and between 494cbf7 and 180263c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • docs/type-mapping-matrix.md
  • src/core/annotation-parser.ts
  • src/core/generator.ts
  • src/core/mapper.ts
  • src/index.ts
  • src/types/index.ts
  • src/tywrap.ts
  • src/utils/ir-cache.ts
  • test/generator.test.ts
  • test/integration.test.ts
  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • tywrap_ir/tywrap_ir/__main__.py
  • tywrap_ir/tywrap_ir/ir.py
  • tywrap_ir/tywrap_ir/optimized_ir.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
test/**/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use NODE_OPTIONS=--expose-gc environment variable for GC-sensitive tests

Files:

  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • test/integration.test.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript strict mode; avoid any — use unknown and type guards instead

Files:

  • src/utils/ir-cache.ts
  • src/index.ts
  • src/tywrap.ts
  • src/core/mapper.ts
  • src/core/generator.ts
  • src/core/annotation-parser.ts
  • src/types/index.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode and avoid any — use unknown and type guards instead

Files:

  • src/utils/ir-cache.ts
  • src/index.ts
  • src/tywrap.ts
  • src/core/mapper.ts
  • src/core/generator.ts
  • src/core/annotation-parser.ts
  • src/types/index.ts
tywrap_ir/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Python code must follow PEP 8 and use type hints

Follow PEP 8 and use type hints in Python code

Files:

  • tywrap_ir/tywrap_ir/__main__.py
  • tywrap_ir/tywrap_ir/optimized_ir.py
  • tywrap_ir/tywrap_ir/ir.py
🧠 Learnings (17)
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to test/runtime_*.test.ts : Include tests for new runtime behavior following the `test/runtime_*.test.ts` patterns

Applied to files:

  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • test/integration.test.ts
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Applies to test/runtime_*.test.ts : Include tests for new runtime behavior following patterns in `test/runtime_*.test.ts`

Applied to files:

  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • test/integration.test.ts
📚 Learning: 2026-01-19T21:48:27.823Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: test/runtime_bridge_fixtures.test.ts:59-66
Timestamp: 2026-01-19T21:48:27.823Z
Learning: In fixture-based tests (e.g., test/runtime_bridge_fixtures.test.ts) and similar tests in the tywrap repository, prefer early returns when Python or fixture files are unavailable. Do not rely on Vitest dynamic skip APIs; a silent pass is intentional for environments lacking Python/fixtures. Treat missing fixtures as optional soft-guards and ensure the test remains non-disruptive in non-availability scenarios.

Applied to files:

  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • test/integration.test.ts
📚 Learning: 2026-01-20T01:34:07.064Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 136
File: src/runtime/node.ts:444-458
Timestamp: 2026-01-20T01:34:07.064Z
Learning: When reviewing promise-based polling patterns (e.g., recursive setTimeout in waitForAvailableWorker functions), ensure that any recursive timer is tracked and cleared on timeout or promise resolution to prevent timer leaks. Do not rely on no-op checks after settlement; verify clearTimeout is invoked in all paths and consider using an explicit cancellation (e.g., AbortController) or a guard to cancel pending timers when the promise settles.

Applied to files:

  • test/ir_cache_filename.test.ts
  • src/utils/ir-cache.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • src/index.ts
  • src/tywrap.ts
  • test/integration.test.ts
  • src/core/mapper.ts
  • src/core/generator.ts
  • src/core/annotation-parser.ts
  • src/types/index.ts
📚 Learning: 2026-01-20T18:37:05.670Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: src/runtime/process-io.ts:37-40
Timestamp: 2026-01-20T18:37:05.670Z
Learning: In the tywrap repo, ESLint is used for linting (not Biome). Do not flag regex literals that contain control characters (e.g., \u001b, \u0000-\u001F) as lint errors, since the current ESLint configuration allows them. When reviewing TypeScript files (e.g., src/**/*.ts), rely on ESLint results and avoid raising issues about these specific control-character regex literals unless there is a competing config change or a policy requiring explicit escaping.

Applied to files:

  • test/ir_cache_filename.test.ts
  • src/utils/ir-cache.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • src/index.ts
  • src/tywrap.ts
  • test/integration.test.ts
  • src/core/mapper.ts
  • src/core/generator.ts
  • src/core/annotation-parser.ts
  • src/types/index.ts
📚 Learning: 2026-01-20T16:00:49.829Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: docs/adr/002-bridge-protocol.md:203-211
Timestamp: 2026-01-20T16:00:49.829Z
Learning: In the BridgeProtocol implementation (tywrap), reject Map and Set explicitly before serialization (e.g., in safeStringify or the serialization entrypoint) with a clear error like "Bridge protocol does not support Map/Set values". Do not rely on post-hoc checks of non-string keys at the point of JSON.stringify, since Maps/Sets cannot round-trip. This should be enforced at the boundary where data is prepared for serialization to ensure deterministic errors and prevent invalid data from propagating.

Applied to files:

  • test/ir_cache_filename.test.ts
  • src/utils/ir-cache.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • src/index.ts
  • src/tywrap.ts
  • test/integration.test.ts
  • src/core/mapper.ts
  • src/core/generator.ts
  • src/core/annotation-parser.ts
  • src/types/index.ts
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Applies to tywrap_ir/**/*.py : Follow PEP 8 and use type hints in Python code

Applied to files:

  • src/tywrap.ts
  • tywrap_ir/tywrap_ir/ir.py
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to tywrap_ir/**/*.py : Python code must follow PEP 8 and use type hints

Applied to files:

  • src/tywrap.ts
  • tywrap_ir/tywrap_ir/ir.py
📚 Learning: 2026-01-19T21:14:37.032Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:375-385
Timestamp: 2026-01-19T21:14:37.032Z
Learning: In tywrap (src/runtime/bridge-core.ts and similar), environment variable parsing follows a tolerant/best-effort policy. For example, `TYWRAP_CODEC_MAX_BYTES=1024abc` should be accepted as 1024. Only reject clearly invalid values (non-numeric start or <=0). This avoids surprising failures from minor typos.

Applied to files:

  • src/tywrap.ts
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Applies to src/**/*.ts : Use TypeScript strict mode and avoid `any` — use `unknown` and type guards instead

Applied to files:

  • docs/type-mapping-matrix.md
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to src/**/*.{ts,tsx} : TypeScript strict mode; avoid `any` — use `unknown` and type guards instead

Applied to files:

  • docs/type-mapping-matrix.md
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to src/runtime/**/*.ts : Runtime bridges code should be located in `src/runtime/` with corresponding tests in `test/runtime_*.test.ts`

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-20T16:01:14.323Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: docs/adr/002-bridge-protocol.md:599-602
Timestamp: 2026-01-20T16:01:14.323Z
Learning: In `src/runtime/node-bridge.ts` (NodeBridge), a test message is sent to the Python subprocess to confirm the bridge is responsive before marking it as ready.

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Place runtime bridge implementations in `src/runtime/` with corresponding tests in `test/runtime_*.test.ts`

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-19T21:14:40.872Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:260-263
Timestamp: 2026-01-19T21:14:40.872Z
Learning: In `src/runtime/bridge-core.ts` and similar hot request/response loop implementations in the tywrap repository, avoid adding extra defensive validation (e.g., runtime shape checks on error payloads) in tight loops unless the protocol boundary is untrusted or there's a concrete bug report. The Python bridge protocol is controlled and validated via tests, so defensive checks would add unnecessary branching overhead without meaningful benefit.

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-19T21:00:30.825Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: test/adversarial_playground.test.ts:339-360
Timestamp: 2026-01-19T21:00:30.825Z
Learning: In adversarial tests for the bridge (test/adversarial_playground.test.ts), recovery assertions should be kept in the finally block adjacent to cleanup to verify resilience under stress, even if that means post-timeout checks might mask the original error. The goal is to surface recovery failures as legitimate test failures.

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-19T21:48:45.693Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:260-298
Timestamp: 2026-01-19T21:48:45.693Z
Learning: In `src/runtime/bridge-core.ts`, keep `normalizeErrorPayload` to validate error payloads from the Python subprocess. The subprocess boundary is effectively untrusted, and normalizing error responses prevents `undefined: undefined` errors on malformed payloads. Error responses are not the hot path, so the small validation overhead is acceptable for the added resilience.

Applied to files:

  • test/integration.test.ts
🪛 Ruff (0.15.6)
tywrap_ir/tywrap_ir/ir.py

[warning] 13-13: typing.Dict is deprecated, use dict instead

(UP035)


[warning] 13-13: typing.List is deprecated, use list instead

(UP035)


[warning] 17-17: Do not catch blind exception: Exception

(BLE001)


[warning] 121-121: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 130-130: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 166-166: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 178-178: Dynamically typed expressions (typing.Any) are disallowed in annotation

(ANN401)


[warning] 186-186: Do not catch blind exception: Exception

(BLE001)


[warning] 193-193: Do not catch blind exception: Exception

(BLE001)


[warning] 199-199: Call endswith once with a tuple

Merge into a single endswith call

(PIE810)


[warning] 208-208: Dynamically typed expressions (typing.Any) are disallowed in *annotations

(ANN401)


[warning] 216-216: Dynamically typed expressions (typing.Any) are disallowed in module

(ANN401)


[warning] 219-219: Do not catch blind exception: Exception

(BLE001)


[warning] 224-224: Do not catch blind exception: Exception

(BLE001)


[warning] 236-236: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)


[warning] 243-243: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[error] 254-255: try-except-pass detected, consider logging the exception

(S110)


[warning] 254-254: Do not catch blind exception: Exception

(BLE001)


[warning] 259-259: Dynamically typed expressions (typing.Any) are disallowed in module

(ANN401)


[warning] 259-259: Unused function argument: module_name

(ARG001)


[warning] 259-259: Boolean-typed positional argument in function definition

(FBT001)


[error] 270-271: try-except-continue detected, consider logging the exception

(S112)


[warning] 270-270: Do not catch blind exception: Exception

(BLE001)


[warning] 288-288: Do not catch blind exception: Exception

(BLE001)


[warning] 303-303: Dynamically typed expressions (typing.Any) are disallowed in module

(ANN401)


[warning] 303-303: Unused function argument: module_name

(ARG001)


[warning] 303-303: Boolean-typed positional argument in function definition

(FBT001)


[error] 316-317: try-except-continue detected, consider logging the exception

(S112)


[warning] 316-316: Do not catch blind exception: Exception

(BLE001)


[warning] 340-340: Dynamically typed expressions (typing.Any) are disallowed in obj

(ANN401)


[warning] 347-347: Do not catch blind exception: Exception

(BLE001)


[warning] 352-352: Do not catch blind exception: Exception

(BLE001)


[warning] 355-355: Do not catch blind exception: Exception

(BLE001)


[error] 532-533: try-except-pass detected, consider logging the exception

(S110)


[warning] 532-532: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (32)
test/ir_cache_filename.test.ts (1)

24-44: LGTM!

The new test case correctly validates that the cache filename changes when only irVersion differs. The use of a shared keyBase object keeps the test focused and readable.

src/utils/ir-cache.ts (1)

3-22: LGTM!

Adding irVersion to the cache key interface ensures cache invalidation when the IR schema changes. This prevents stale cached JSON from being used after schema updates.

tywrap_ir/tywrap_ir/__main__.py (1)

13-13: LGTM!

Default IR version bumped to 0.2.0 to align with the schema changes for generic type parameters.

test/ir_extraction_advanced.test.ts (1)

178-250: LGTM!

Comprehensive test coverage for the new type_params IR field. The test verifies:

  • Function-level generic parameters (identity with T)
  • Class-level generic parameters (Container, KeyValueStore)
  • Type alias generic parameters with different kinds (typevar, paramspec, typevartuple)

The summarizeParams helper keeps assertions focused on the essential fields.

docs/type-mapping-matrix.md (1)

120-151: LGTM!

Clear documentation of the new generic type emission behavior, including:

  • Declaration-level generic parameters for TypeVar
  • Conservative fallback for bounds/constraints
  • ParamSpec handling with P extends unknown[]
  • Safe fallbacks for TypeVarTuple/Unpack to unknown
test/generator.test.ts (1)

632-906: LGTM!

Thorough test coverage for generic emission:

  • Function overloads with generic parameters
  • Generic class declarations with static create<T> and static fromHandle<T>
  • Type alias emission including ParamSpec modeling as P extends unknown[]
  • Verification that unsafe placeholders are not emitted in output
src/index.ts (1)

86-133: LGTM!

The public type exports are expanded to include the new generic-related IR types. This allows consumers to work with the full type schema when needed.

test/integration.test.ts (1)

83-238: LGTM!

Excellent end-to-end integration test that:

  1. Generates TypeScript from a Python module with full generic coverage (TypeVar, ParamSpec, TypeVarTuple)
  2. Verifies generated .ts and .d.ts content includes expected generic signatures
  3. Asserts absence of unsafe placeholder patterns (~T, ~P, Unpack[)
  4. Validates that the output compiles with tsc --strict

The use of a consumer file that actually instantiates and uses the generic types ensures the emitted TypeScript is practically usable, not just syntactically valid.

tywrap_ir/tywrap_ir/optimized_ir.py (1)

161-161: LGTM! IR version bump is consistent.

The default ir_version is correctly updated to "0.2.0" in both the class method and module-level function signatures, aligning with the version bump in ir.py and the CLI.

Also applies to: 409-409

src/tywrap.ts (3)

25-26: LGTM! IR version constant centralized.

Good practice to define TYWRAP_IR_VERSION as a constant and use it consistently in CLI invocations and cache key computation.


428-499: Well-structured generic parameter extraction.

The mapTypeParameters, mapParam, and mapFunc helpers cleanly propagate type parameters from IR to the TypeScript model. The pattern of passing typeParameters context through the parsing pipeline ensures annotations are resolved correctly against their declaring scope.


541-559: Type alias extraction correctly implemented.

The mapTypeAlias function properly extracts generic parameters and parses the definition with the correct type parameter context, and typeAliases is correctly populated from the IR.

src/core/mapper.ts (3)

71-80: LGTM! Exhaustive handling of new Python type kinds.

The switch statement now handles all new type variants (paramspec, paramspec_args, paramspec_kwargs, typevartuple, unpack) with appropriate delegations to dedicated mapping methods.


346-378: Conservative fallback strategy correctly implemented.

The new mapping helpers follow the documented approach:

  • mapParamSpecType returns a custom type placeholder
  • mapParamSpecArgsType falls back to unknown[]
  • mapParamSpecKwargsType falls back to Record<string, unknown>
  • mapTypeVarTupleType and mapUnpackType fall back to unknown

This ensures generated TypeScript remains valid even when Python typing constructs cannot be fully represented.


291-327: Callable mapping correctly handles ParamSpec.

The refactored mapCallableType properly:

  1. Uses mapParamSpecType when a parameterSpec is present
  2. Preserves the ellipsis special case for Callable[..., R]
  3. Falls back to explicit parameter mapping otherwise
tywrap_ir/tywrap_ir/ir.py (4)

21-28: LGTM! IRTypeParam dataclass well-designed.

The IRTypeParam dataclass captures all necessary metadata for Python generic parameters: name, kind (typevar/paramspec/typevartuple), optional bound, optional constraints list, and optional variance. This enables the TypeScript side to make informed emission decisions.


121-174: Type parameter serialization correctly implemented.

The helper functions properly:

  • Detect TypeVar/ParamSpec/TypeVarTuple via module and class name inspection
  • Extract bounds, constraints, and variance from TypeVar instances
  • Strip the ~ prefix from names for normalization

339-386: Function extraction correctly collects type parameters.

The include_type_params flag allows method extraction within classes to skip redundant type parameter collection (since class methods inherit the class's type parameters). The use of get_type_hints(..., include_extras=True) preserves Annotated metadata.


580-580: IR version bump to 0.2.0 is consistent.

The default ir_version is correctly updated in both extract_module_ir and emit_ir_json, matching the changes in optimized_ir.py and the TypeScript side.

Also applies to: 624-624

src/core/generator.ts (5)

19-30: LGTM! Generic rendering context well-designed.

GenericRenderContext cleanly tracks:

  • currentModule for same-module reference normalization
  • declaration and typeArguments strings for emission
  • emittedNames and emittedParamSpecs sets for safe type variable resolution

This enables the generator to emit only declared generics and fall back to unknown for undeclared ones.


134-173: Generic emission rules correctly implemented.

buildGenericRenderContext follows the documented emission rules:

  • Simple unconstrained TypeVars (no bound, no constraints, invariant) → emit as-is
  • ParamSpecs used in callable positions → emit as P extends unknown[]
  • Bounded/constrained TypeVars, TypeVarTuple → not emitted (will fall back to unknown)

211-281: Safe type sanitization ensures valid TypeScript output.

sanitizeType correctly:

  • Passes through primitives, literals, and custom types
  • Recursively sanitizes nested types (collections, unions, generics, callables)
  • Replaces unemitted typevar/paramspec with Any (→ unknown in TS)
  • Falls back paramspec_args to list[Any], paramspec_kwargs to dict[str, Any]
  • Falls back typevartuple and unpack to Any

This guarantees generated TypeScript never references undeclared type parameters.


1085-1095: Type alias generation correctly implemented.

generateTypeAlias properly:

  • Builds a generic context from the alias's type parameters
  • Preserves the original casing via preserveCase: true
  • Emits export type Alias<T> = ... with sanitized body

1173-1185: wrap method signature updated for declaration output.

The method now accepts and returns the declaration string, enabling proper .d.ts file generation separate from runtime code.

src/core/annotation-parser.ts (5)

1-5: LGTM! Parser options extended for type parameter context.

The new typeParameters option allows the parser to resolve type variable references against their declaring scope, enabling correct emission of typevar, paramspec, and typevartuple nodes.


45-65: Type parameter resolution correctly implemented.

mapKnownTypeParameter:

  • Normalizes names by stripping ~ prefix
  • Returns the appropriate PythonType variant based on parameter kind
  • Preserves bound/constraints/variance for TypeVars

213-243: Balanced bracket parsing ensures correct generic extraction.

splitGenericInvocation correctly:

  • Validates the string ends with ]
  • Uses depth tracking to find the matching [
  • Rejects malformed cases (unbalanced brackets, extra content after ])

315-357: Callable parsing handles all parameter forms.

The refactored callable parsing correctly handles:

  1. Ellipsis (...) → single { kind: 'custom', name: '...' } parameter
  2. ParamSpec reference → sets parameterSpec field
  3. Explicit parameter list → parses individual types

381-403: Unpack and generic invocation support added.

  • Unpack[...] correctly emits { kind: 'unpack', type: ... }
  • Generic invocations like Foo[T, U] are parsed with qualified module extraction
src/types/index.ts (3)

39-53: LGTM! Generic parameter types well-defined.

PythonTypeAlias and PythonGenericParameter properly model:

  • Type alias with optional generic parameters
  • Generic parameter kind (typevar/paramspec/typevartuple)
  • Optional bound, constraints, and variance for TypeVars

101-105: New PythonType variants complete the type union.

The five new variants (ParamSpecType, ParamSpecArgsType, ParamSpecKwargsType, TypeVarTupleType, UnpackType) are:

  • Correctly added to the PythonType union
  • Consistently structured with kind discriminators
  • Properly handled in mapper.ts and generator.ts switch statements

Also applies to: 169-192


133-133: Extended type interfaces support qualified generics and ParamSpec callables.

  • GenericType.module enables same-module reference normalization
  • CallableType.parameterSpec enables Callable[P, R] representation

Also applies to: 146-146

Comment thread docs/type-mapping-matrix.md Outdated
Comment thread test/generator.test.ts Outdated
Comment thread test/integration.test.ts
Comment thread tywrap_ir/tywrap_ir/ir.py Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/core/annotation-parser.ts`:
- Around line 257-265: The code incorrectly treats any X.args / X.kwargs as
ParamSpec packs because of the `known === null` branch; narrow the fast path by
removing the `|| known === null` condition so only when
`mapKnownTypeParameter(baseName)` returns a known paramspec (`known?.kind ===
'paramspec'`) you return the `paramspec_args` / `paramspec_kwargs` variants;
update the logic around `paramspecArgsMatch` handling (and its returned
`PythonType` shapes) to avoid rewriting module-qualified names like
`pkg.Request.args` into ParamSpec packs.

In `@src/core/mapper.ts`:
- Around line 186-191: mapGenericType currently emits raw generic names (e.g.,
Sequence, Awaitable) and loses prior typing-to-TS conversions; update
mapGenericType to call normalizeCustomType or reuse mapCustomType on the
generic's base identifier so known typing conversions are preserved.
Specifically, inside mapGenericType (and when computing normalized from { name:
type.name, module: type.module }), ensure you use the normalized result's name
(and any mapped module/alias) for the returned TSGenericType.name rather than
the raw type.name, and keep mapping typeArgs via this.mapPythonType as before so
typing.Sequence[T] and typing.Awaitable[T] become the previously-mapped
TypeScript identifiers instead of undeclared bare names.

In `@test/ir_extraction_advanced.test.ts`:
- Around line 179-216: The test uses TypeVarTuple/Unpack which require Python
3.11+, so add a pre-check before calling createTempModule in the 'extracts
ordered type parameters for functions, classes, and type aliases' test: run a
short Python command (e.g., via child_process.execSync) or inspect
sys.version_info to detect if the interpreter is < 3.11 and if so call the test
skip/early return; update the test surrounding extractIR, createTempModule, or
the it(...) block to bail out when Python < 3.11 so the test only runs when
TypeVarTuple/Unpack are available. Ensure you reference the same test name and
keep createTempModule and extractIR calls unchanged when the guard passes.

In `@tywrap_ir/tywrap_ir/ir.py`:
- Around line 121-127: The function _type_param_kind currently only recognizes
TypeVar/ParamSpec/TypeVarTuple when their class __module__ is "typing"; update
its module check to also accept "typing_extensions" (or otherwise treat the name
match independent of module) so that backported generic markers from
typing_extensions are recognized; modify the conditional in _type_param_kind to
consider module in {"typing", "typing_extensions"} (or simply match the __name__
set {"TypeVar", "ParamSpec", "TypeVarTuple"} regardless of module) so type
parameters are detected and emitted correctly.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 03d4cd4d-f225-4cac-9962-0810bb035292

📥 Commits

Reviewing files that changed from the base of the PR and between 494cbf7 and 180263c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (15)
  • docs/type-mapping-matrix.md
  • src/core/annotation-parser.ts
  • src/core/generator.ts
  • src/core/mapper.ts
  • src/index.ts
  • src/types/index.ts
  • src/tywrap.ts
  • src/utils/ir-cache.ts
  • test/generator.test.ts
  • test/integration.test.ts
  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • tywrap_ir/tywrap_ir/__main__.py
  • tywrap_ir/tywrap_ir/ir.py
  • tywrap_ir/tywrap_ir/optimized_ir.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript strict mode; avoid any — use unknown and type guards instead

Files:

  • src/utils/ir-cache.ts
  • src/index.ts
  • src/tywrap.ts
  • src/core/generator.ts
  • src/core/mapper.ts
  • src/core/annotation-parser.ts
  • src/types/index.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode and avoid any — use unknown and type guards instead

Files:

  • src/utils/ir-cache.ts
  • src/index.ts
  • src/tywrap.ts
  • src/core/generator.ts
  • src/core/mapper.ts
  • src/core/annotation-parser.ts
  • src/types/index.ts
test/**/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use NODE_OPTIONS=--expose-gc environment variable for GC-sensitive tests

Files:

  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • test/integration.test.ts
tywrap_ir/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Python code must follow PEP 8 and use type hints

Follow PEP 8 and use type hints in Python code

Files:

  • tywrap_ir/tywrap_ir/__main__.py
  • tywrap_ir/tywrap_ir/optimized_ir.py
  • tywrap_ir/tywrap_ir/ir.py
🧠 Learnings (18)
📚 Learning: 2026-01-20T01:34:07.064Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 136
File: src/runtime/node.ts:444-458
Timestamp: 2026-01-20T01:34:07.064Z
Learning: When reviewing promise-based polling patterns (e.g., recursive setTimeout in waitForAvailableWorker functions), ensure that any recursive timer is tracked and cleared on timeout or promise resolution to prevent timer leaks. Do not rely on no-op checks after settlement; verify clearTimeout is invoked in all paths and consider using an explicit cancellation (e.g., AbortController) or a guard to cancel pending timers when the promise settles.

Applied to files:

  • src/utils/ir-cache.ts
  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • src/index.ts
  • test/generator.test.ts
  • test/integration.test.ts
  • src/tywrap.ts
  • src/core/generator.ts
  • src/core/mapper.ts
  • src/core/annotation-parser.ts
  • src/types/index.ts
📚 Learning: 2026-01-20T18:37:05.670Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: src/runtime/process-io.ts:37-40
Timestamp: 2026-01-20T18:37:05.670Z
Learning: In the tywrap repo, ESLint is used for linting (not Biome). Do not flag regex literals that contain control characters (e.g., \u001b, \u0000-\u001F) as lint errors, since the current ESLint configuration allows them. When reviewing TypeScript files (e.g., src/**/*.ts), rely on ESLint results and avoid raising issues about these specific control-character regex literals unless there is a competing config change or a policy requiring explicit escaping.

Applied to files:

  • src/utils/ir-cache.ts
  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • src/index.ts
  • test/generator.test.ts
  • test/integration.test.ts
  • src/tywrap.ts
  • src/core/generator.ts
  • src/core/mapper.ts
  • src/core/annotation-parser.ts
  • src/types/index.ts
📚 Learning: 2026-01-20T16:00:49.829Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: docs/adr/002-bridge-protocol.md:203-211
Timestamp: 2026-01-20T16:00:49.829Z
Learning: In the BridgeProtocol implementation (tywrap), reject Map and Set explicitly before serialization (e.g., in safeStringify or the serialization entrypoint) with a clear error like "Bridge protocol does not support Map/Set values". Do not rely on post-hoc checks of non-string keys at the point of JSON.stringify, since Maps/Sets cannot round-trip. This should be enforced at the boundary where data is prepared for serialization to ensure deterministic errors and prevent invalid data from propagating.

Applied to files:

  • src/utils/ir-cache.ts
  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • src/index.ts
  • test/generator.test.ts
  • test/integration.test.ts
  • src/tywrap.ts
  • src/core/generator.ts
  • src/core/mapper.ts
  • src/core/annotation-parser.ts
  • src/types/index.ts
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to test/runtime_*.test.ts : Include tests for new runtime behavior following the `test/runtime_*.test.ts` patterns

Applied to files:

  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • test/integration.test.ts
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Applies to test/runtime_*.test.ts : Include tests for new runtime behavior following patterns in `test/runtime_*.test.ts`

Applied to files:

  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • test/integration.test.ts
📚 Learning: 2026-01-19T21:48:27.823Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: test/runtime_bridge_fixtures.test.ts:59-66
Timestamp: 2026-01-19T21:48:27.823Z
Learning: In fixture-based tests (e.g., test/runtime_bridge_fixtures.test.ts) and similar tests in the tywrap repository, prefer early returns when Python or fixture files are unavailable. Do not rely on Vitest dynamic skip APIs; a silent pass is intentional for environments lacking Python/fixtures. Treat missing fixtures as optional soft-guards and ensure the test remains non-disruptive in non-availability scenarios.

Applied to files:

  • test/ir_cache_filename.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/generator.test.ts
  • test/integration.test.ts
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to tywrap_ir/**/*.py : Python code must follow PEP 8 and use type hints

Applied to files:

  • tywrap_ir/tywrap_ir/__main__.py
  • src/tywrap.ts
  • tywrap_ir/tywrap_ir/ir.py
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Applies to tywrap_ir/**/*.py : Follow PEP 8 and use type hints in Python code

Applied to files:

  • tywrap_ir/tywrap_ir/__main__.py
  • docs/type-mapping-matrix.md
  • src/tywrap.ts
  • tywrap_ir/tywrap_ir/ir.py
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Applies to src/**/*.ts : Use TypeScript strict mode and avoid `any` — use `unknown` and type guards instead

Applied to files:

  • docs/type-mapping-matrix.md
  • test/generator.test.ts
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to src/**/*.{ts,tsx} : TypeScript strict mode; avoid `any` — use `unknown` and type guards instead

Applied to files:

  • docs/type-mapping-matrix.md
  • test/generator.test.ts
📚 Learning: 2026-01-19T21:14:40.872Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:260-263
Timestamp: 2026-01-19T21:14:40.872Z
Learning: In `src/runtime/bridge-core.ts` and similar hot request/response loop implementations in the tywrap repository, avoid adding extra defensive validation (e.g., runtime shape checks on error payloads) in tight loops unless the protocol boundary is untrusted or there's a concrete bug report. The Python bridge protocol is controlled and validated via tests, so defensive checks would add unnecessary branching overhead without meaningful benefit.

Applied to files:

  • test/generator.test.ts
  • test/integration.test.ts
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Place runtime bridge implementations in `src/runtime/` with corresponding tests in `test/runtime_*.test.ts`

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to src/runtime/**/*.ts : Runtime bridges code should be located in `src/runtime/` with corresponding tests in `test/runtime_*.test.ts`

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-20T16:01:39.136Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: docs/adr/002-bridge-protocol.md:826-830
Timestamp: 2026-01-20T16:01:39.136Z
Learning: In the tywrap BridgeProtocol architecture (ADR-002), `HttpIO` (and other Transport implementations like `ProcessIO`, `PyodideIO`) implements only the `Transport` interface (methods: `send`, `isReady`, `dispose`). The `RuntimeExecution` interface methods (`call`, `instantiate`, `callMethod`, `disposeInstance`) are implemented by `BridgeProtocol`, which delegates to `Transport` instances. Transport implementations should not include RuntimeExecution methods.

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-20T16:01:14.323Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: docs/adr/002-bridge-protocol.md:599-602
Timestamp: 2026-01-20T16:01:14.323Z
Learning: In `src/runtime/node-bridge.ts` (NodeBridge), a test message is sent to the Python subprocess to confirm the bridge is responsive before marking it as ready.

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-19T21:00:30.825Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: test/adversarial_playground.test.ts:339-360
Timestamp: 2026-01-19T21:00:30.825Z
Learning: In adversarial tests for the bridge (test/adversarial_playground.test.ts), recovery assertions should be kept in the finally block adjacent to cleanup to verify resilience under stress, even if that means post-timeout checks might mask the original error. The goal is to surface recovery failures as legitimate test failures.

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-19T21:48:45.693Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:260-298
Timestamp: 2026-01-19T21:48:45.693Z
Learning: In `src/runtime/bridge-core.ts`, keep `normalizeErrorPayload` to validate error payloads from the Python subprocess. The subprocess boundary is effectively untrusted, and normalizing error responses prevents `undefined: undefined` errors on malformed payloads. Error responses are not the hot path, so the small validation overhead is acceptable for the added resilience.

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-19T21:14:37.032Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:375-385
Timestamp: 2026-01-19T21:14:37.032Z
Learning: In tywrap (src/runtime/bridge-core.ts and similar), environment variable parsing follows a tolerant/best-effort policy. For example, `TYWRAP_CODEC_MAX_BYTES=1024abc` should be accepted as 1024. Only reject clearly invalid values (non-numeric start or <=0). This avoids surprising failures from minor typos.

Applied to files:

  • src/tywrap.ts
🪛 Ruff (0.15.6)
tywrap_ir/tywrap_ir/ir.py

[warning] 13-13: typing.Dict is deprecated, use dict instead

(UP035)


[warning] 13-13: typing.List is deprecated, use list instead

(UP035)


[warning] 17-17: Do not catch blind exception: Exception

(BLE001)


[warning] 121-121: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 130-130: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 166-166: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 178-178: Dynamically typed expressions (typing.Any) are disallowed in annotation

(ANN401)


[warning] 186-186: Do not catch blind exception: Exception

(BLE001)


[warning] 193-193: Do not catch blind exception: Exception

(BLE001)


[warning] 199-199: Call endswith once with a tuple

Merge into a single endswith call

(PIE810)


[warning] 208-208: Dynamically typed expressions (typing.Any) are disallowed in *annotations

(ANN401)


[warning] 216-216: Dynamically typed expressions (typing.Any) are disallowed in module

(ANN401)


[warning] 219-219: Do not catch blind exception: Exception

(BLE001)


[warning] 224-224: Do not catch blind exception: Exception

(BLE001)


[warning] 236-236: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)


[warning] 243-243: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[error] 254-255: try-except-pass detected, consider logging the exception

(S110)


[warning] 254-254: Do not catch blind exception: Exception

(BLE001)


[warning] 259-259: Dynamically typed expressions (typing.Any) are disallowed in module

(ANN401)


[warning] 259-259: Unused function argument: module_name

(ARG001)


[warning] 259-259: Boolean-typed positional argument in function definition

(FBT001)


[error] 270-271: try-except-continue detected, consider logging the exception

(S112)


[warning] 270-270: Do not catch blind exception: Exception

(BLE001)


[warning] 288-288: Do not catch blind exception: Exception

(BLE001)


[warning] 303-303: Dynamically typed expressions (typing.Any) are disallowed in module

(ANN401)


[warning] 303-303: Unused function argument: module_name

(ARG001)


[warning] 303-303: Boolean-typed positional argument in function definition

(FBT001)


[error] 316-317: try-except-continue detected, consider logging the exception

(S112)


[warning] 316-316: Do not catch blind exception: Exception

(BLE001)


[warning] 340-340: Dynamically typed expressions (typing.Any) are disallowed in obj

(ANN401)


[warning] 347-347: Do not catch blind exception: Exception

(BLE001)


[warning] 352-352: Do not catch blind exception: Exception

(BLE001)


[warning] 355-355: Do not catch blind exception: Exception

(BLE001)


[error] 532-533: try-except-pass detected, consider logging the exception

(S110)


[warning] 532-532: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (1)
test/integration.test.ts (1)

92-96: Verify the fixture’s Python-version floor.

This inline module now requires the newer stdlib typing exports: ParamSpec from Python 3.10+, and TypeVarTuple / Unpack from Python 3.11+. If the integration suite still runs on older interpreters, the test will fail before generate() is exercised.

♻️ Portable fixture import
-from typing import Callable, Generic, ParamSpec, TypeVar, TypeVarTuple, Unpack
+from typing import Callable, Generic, TypeVar
+try:
+    from typing import ParamSpec, TypeVarTuple, Unpack
+except ImportError:
+    from typing_extensions import ParamSpec, TypeVarTuple, Unpack

Comment thread src/core/annotation-parser.ts Outdated
Comment thread src/core/mapper.ts Outdated
Comment thread test/ir_extraction_advanced.test.ts
Comment thread tywrap_ir/tywrap_ir/ir.py
@bbopen bbopen force-pushed the codex/safe-typescript-generic-emission branch from c77fff9 to 2538f51 Compare March 20, 2026 22:07
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/type-mapping-matrix.md`:
- Around line 122-126: The table entry for TypeVar is misleading: update the
rows for `TypeVar('T')`, `TypeVar('T', bound=Base)`, and `TypeVar('T', str,
int)` to explicitly state that only invariant TypeVars are emitted as
declaration-level generics and that covariant/contravariant TypeVars (variance)
as well as TypeVars with bounds or constraints currently fall back to `unknown`;
revise the `TypeVar('T')` row to note "invariant only" and add a brief note to
the other two rows that they cover both bounds/constraints and variance
fallbacks so the table no longer over-promises.

In `@src/core/generator.ts`:
- Around line 146-170: The emitted generic identifiers are inserted raw causing
invalid TS for names like "default" — update the code paths that build the
emitted array and emittedNames/emittedParamSpecs to use
this.escapeIdentifier(param.name) instead of param.name: when pushing into
emitted use declaration: `${this.escapeIdentifier(param.name)}${...}` (or
`${this.escapeIdentifier(param.name)} extends unknown[]` for paramspecs) and set
the pushed object's name to the escaped identifier; also add the escaped
identifier to emittedNames and emittedParamSpecs so later sanitizeType checks
match; apply the same replacement wherever emitted/declaration/typeArguments are
composed (including other spots that build emitted.map(param => param.name) and
emitted.map(param => param.declaration)) and ensure all uses of param.name in
these generators are escaped via this.escapeIdentifier.
- Around line 175-185: mergeGenericRenderContexts currently unions emittedNames
across scopes which loses shadowing info and allows inner type parameters to be
resolved to outer names; update mergeGenericRenderContexts to preserve scope
precedence instead of a flat Set (e.g., make emittedNames a stack/array of Sets
or a structure that records origin order so lookups consult inner first), keep
declaration and typeArguments as-is, and adapt sanitizeType (the
ctx.emittedNames.has(...) check) to consult the new precedence-aware
emittedNames structure so inner/declared names shadow outer ones; refer to
mergeGenericRenderContexts, emittedNames, and sanitizeType when making the
change.

In `@test/integration.test.ts`:
- Around line 225-277: The tsconfig written in the test includes the generated
implementation .ts files which causes TypeScript to bind imports to the .ts
sources instead of the .d.ts declarations; update the tsconfig JSON written in
the test (the object passed to writeFile for tsconfig.json under compileDir) to
remove '../generated/generic_module.generated.ts' (and similarly remove
'../generated/protocol_module.generated.ts' in the other tsconfig write) from
the "include" array so the test compiles only against the .d.ts files and
validates the declaration surface.

In `@test/type_mapping_enhanced.test.ts`:
- Around line 232-370: Add a direct conversion in mapPythonType for generic
types named "Coroutine" coming from module "typing": when encountering a
PythonType with kind 'generic', name 'Coroutine', and module 'typing', map it to
a TS generic Promise using the third type argument as the promise value type
(i.e., Promise<T>), making sure to map that T via the existing mapper logic
(call mapper.mapPythonType on typeArgs[2]) and handle the case where fewer than
3 typeArgs exist gracefully (fallback to Promise<any> or similar). Update the
branch that currently preserves third-party Coroutine (module !== 'typing') to
remain unchanged so only the 'typing' module gets the Promise conversion, and
add/adjust tests to cover typing.Coroutine[A,B,T] -> Promise<T>.

In `@tywrap_ir/tywrap_ir/ir.py`:
- Around line 99-104: The current logic strips module qualification from class
annotations by taking class_path.split(".")[-1]; instead, preserve the full
qualified path so type identity is not lost: when handling the string form of an
annotation (variables str_repr, class_path, annotation), return the whole
class_path (e.g., "datetime.datetime") instead of only the last segment; keep
the existing guard that checks str_repr.startswith("<class '") and
str_repr.endswith("'>") and only change the return behavior around class_path
(remove the dot-splitting).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: d6571322-8e25-4c3c-900e-afa84d5a5315

📥 Commits

Reviewing files that changed from the base of the PR and between 180263c and c77fff9.

📒 Files selected for processing (13)
  • docs/type-mapping-matrix.md
  • src/core/annotation-parser.ts
  • src/core/generator.ts
  • src/core/mapper.ts
  • src/runtime/node.ts
  • src/tywrap.ts
  • src/utils/runtime.ts
  • test/annotation-parser.test.ts
  • test/generator.test.ts
  • test/integration.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/type_mapping_enhanced.test.ts
  • tywrap_ir/tywrap_ir/ir.py
📜 Review details
🧰 Additional context used
📓 Path-based instructions (5)
test/**/*.test.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Use NODE_OPTIONS=--expose-gc environment variable for GC-sensitive tests

Files:

  • test/annotation-parser.test.ts
  • test/type_mapping_enhanced.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/integration.test.ts
  • test/generator.test.ts
src/**/*.{ts,tsx}

📄 CodeRabbit inference engine (CLAUDE.md)

TypeScript strict mode; avoid any — use unknown and type guards instead

Files:

  • src/runtime/node.ts
  • src/utils/runtime.ts
  • src/core/annotation-parser.ts
  • src/core/generator.ts
  • src/tywrap.ts
  • src/core/mapper.ts
src/runtime/**/*.ts

📄 CodeRabbit inference engine (CLAUDE.md)

Runtime bridges code should be located in src/runtime/ with corresponding tests in test/runtime_*.test.ts

Files:

  • src/runtime/node.ts
src/**/*.ts

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode and avoid any — use unknown and type guards instead

Files:

  • src/runtime/node.ts
  • src/utils/runtime.ts
  • src/core/annotation-parser.ts
  • src/core/generator.ts
  • src/tywrap.ts
  • src/core/mapper.ts
tywrap_ir/**/*.py

📄 CodeRabbit inference engine (CLAUDE.md)

Python code must follow PEP 8 and use type hints

Follow PEP 8 and use type hints in Python code

Files:

  • tywrap_ir/tywrap_ir/ir.py
🧠 Learnings (20)
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to test/runtime_*.test.ts : Include tests for new runtime behavior following the `test/runtime_*.test.ts` patterns

Applied to files:

  • test/annotation-parser.test.ts
  • src/utils/runtime.ts
  • test/type_mapping_enhanced.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/integration.test.ts
  • test/generator.test.ts
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Applies to test/runtime_*.test.ts : Include tests for new runtime behavior following patterns in `test/runtime_*.test.ts`

Applied to files:

  • test/annotation-parser.test.ts
  • src/utils/runtime.ts
  • test/type_mapping_enhanced.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/integration.test.ts
  • test/generator.test.ts
📚 Learning: 2026-01-19T21:48:27.823Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: test/runtime_bridge_fixtures.test.ts:59-66
Timestamp: 2026-01-19T21:48:27.823Z
Learning: In fixture-based tests (e.g., test/runtime_bridge_fixtures.test.ts) and similar tests in the tywrap repository, prefer early returns when Python or fixture files are unavailable. Do not rely on Vitest dynamic skip APIs; a silent pass is intentional for environments lacking Python/fixtures. Treat missing fixtures as optional soft-guards and ensure the test remains non-disruptive in non-availability scenarios.

Applied to files:

  • test/annotation-parser.test.ts
  • test/type_mapping_enhanced.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/integration.test.ts
  • test/generator.test.ts
📚 Learning: 2026-01-20T01:34:07.064Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 136
File: src/runtime/node.ts:444-458
Timestamp: 2026-01-20T01:34:07.064Z
Learning: When reviewing promise-based polling patterns (e.g., recursive setTimeout in waitForAvailableWorker functions), ensure that any recursive timer is tracked and cleared on timeout or promise resolution to prevent timer leaks. Do not rely on no-op checks after settlement; verify clearTimeout is invoked in all paths and consider using an explicit cancellation (e.g., AbortController) or a guard to cancel pending timers when the promise settles.

Applied to files:

  • test/annotation-parser.test.ts
  • src/runtime/node.ts
  • src/utils/runtime.ts
  • test/type_mapping_enhanced.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/integration.test.ts
  • src/core/annotation-parser.ts
  • src/core/generator.ts
  • src/tywrap.ts
  • src/core/mapper.ts
  • test/generator.test.ts
📚 Learning: 2026-01-20T18:37:05.670Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: src/runtime/process-io.ts:37-40
Timestamp: 2026-01-20T18:37:05.670Z
Learning: In the tywrap repo, ESLint is used for linting (not Biome). Do not flag regex literals that contain control characters (e.g., \u001b, \u0000-\u001F) as lint errors, since the current ESLint configuration allows them. When reviewing TypeScript files (e.g., src/**/*.ts), rely on ESLint results and avoid raising issues about these specific control-character regex literals unless there is a competing config change or a policy requiring explicit escaping.

Applied to files:

  • test/annotation-parser.test.ts
  • src/runtime/node.ts
  • src/utils/runtime.ts
  • test/type_mapping_enhanced.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/integration.test.ts
  • src/core/annotation-parser.ts
  • src/core/generator.ts
  • src/tywrap.ts
  • src/core/mapper.ts
  • test/generator.test.ts
📚 Learning: 2026-01-20T16:00:49.829Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: docs/adr/002-bridge-protocol.md:203-211
Timestamp: 2026-01-20T16:00:49.829Z
Learning: In the BridgeProtocol implementation (tywrap), reject Map and Set explicitly before serialization (e.g., in safeStringify or the serialization entrypoint) with a clear error like "Bridge protocol does not support Map/Set values". Do not rely on post-hoc checks of non-string keys at the point of JSON.stringify, since Maps/Sets cannot round-trip. This should be enforced at the boundary where data is prepared for serialization to ensure deterministic errors and prevent invalid data from propagating.

Applied to files:

  • test/annotation-parser.test.ts
  • src/runtime/node.ts
  • src/utils/runtime.ts
  • test/type_mapping_enhanced.test.ts
  • test/ir_extraction_advanced.test.ts
  • test/integration.test.ts
  • src/core/annotation-parser.ts
  • src/core/generator.ts
  • src/tywrap.ts
  • src/core/mapper.ts
  • test/generator.test.ts
📚 Learning: 2026-01-19T21:14:35.390Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:260-263
Timestamp: 2026-01-19T21:14:35.390Z
Learning: In src/runtime/bridge-core.ts and similar hot request/response loop implementations in the tywrap repository, avoid adding extra defensive validation (such as runtime, shape, or error-payload checks) inside tight loops unless the protocol boundary is untrusted or there is a concrete bug report. The Python bridge protocol is controlled via tests, so these checks add unnecessary branching overhead without meaningful benefit. Apply this guidance to other hot-path runtime loops in src/runtime/**/*.ts, and re-enable additional validations only when a documented risk or failure scenario is identified. Ensure tests cover protocol validation where applicable.

Applied to files:

  • src/runtime/node.ts
📚 Learning: 2026-01-19T21:48:45.693Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:260-298
Timestamp: 2026-01-19T21:48:45.693Z
Learning: In `src/runtime/bridge-core.ts`, keep `normalizeErrorPayload` to validate error payloads from the Python subprocess. The subprocess boundary is effectively untrusted, and normalizing error responses prevents `undefined: undefined` errors on malformed payloads. Error responses are not the hot path, so the small validation overhead is acceptable for the added resilience.

Applied to files:

  • src/runtime/node.ts
  • test/integration.test.ts
📚 Learning: 2026-01-20T16:01:14.323Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: docs/adr/002-bridge-protocol.md:599-602
Timestamp: 2026-01-20T16:01:14.323Z
Learning: In `src/runtime/node-bridge.ts` (NodeBridge), a test message is sent to the Python subprocess to confirm the bridge is responsive before marking it as ready.

Applied to files:

  • src/runtime/node.ts
  • test/integration.test.ts
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Place runtime bridge implementations in `src/runtime/` with corresponding tests in `test/runtime_*.test.ts`

Applied to files:

  • src/runtime/node.ts
  • test/integration.test.ts
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to src/runtime/**/*.ts : Runtime bridges code should be located in `src/runtime/` with corresponding tests in `test/runtime_*.test.ts`

Applied to files:

  • src/runtime/node.ts
  • test/integration.test.ts
📚 Learning: 2026-01-19T21:14:29.869Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:375-385
Timestamp: 2026-01-19T21:14:29.869Z
Learning: In the runtime env-var parsing (e.g., in src/runtime/bridge-core.ts and similar modules), adopt a tolerant, best-effort policy: numeric env values should parse by taking the leading numeric portion (e.g., TYWRAP_CODEC_MAX_BYTES=1024abc -> 1024). Only reject clearly invalid values (non-numeric start or <= 0). This reduces surprising failures from minor typos. Add tests to cover partial numeric prefixes, and ensure downstream logic documents the trusted range; consider a small helper to extract a positive integer from a string with a safe fallback.

Applied to files:

  • src/runtime/node.ts
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Applies to tywrap_ir/**/*.py : Follow PEP 8 and use type hints in Python code

Applied to files:

  • docs/type-mapping-matrix.md
  • src/tywrap.ts
  • tywrap_ir/tywrap_ir/ir.py
📚 Learning: 2026-03-17T05:27:14.946Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: AGENTS.md:0-0
Timestamp: 2026-03-17T05:27:14.946Z
Learning: Applies to src/**/*.ts : Use TypeScript strict mode and avoid `any` — use `unknown` and type guards instead

Applied to files:

  • docs/type-mapping-matrix.md
  • src/core/annotation-parser.ts
  • src/core/mapper.ts
  • test/generator.test.ts
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to src/**/*.{ts,tsx} : TypeScript strict mode; avoid `any` — use `unknown` and type guards instead

Applied to files:

  • docs/type-mapping-matrix.md
  • src/core/annotation-parser.ts
  • src/core/mapper.ts
  • test/generator.test.ts
📚 Learning: 2026-01-19T21:14:40.872Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:260-263
Timestamp: 2026-01-19T21:14:40.872Z
Learning: In `src/runtime/bridge-core.ts` and similar hot request/response loop implementations in the tywrap repository, avoid adding extra defensive validation (e.g., runtime shape checks on error payloads) in tight loops unless the protocol boundary is untrusted or there's a concrete bug report. The Python bridge protocol is controlled and validated via tests, so defensive checks would add unnecessary branching overhead without meaningful benefit.

Applied to files:

  • test/integration.test.ts
  • src/core/annotation-parser.ts
  • test/generator.test.ts
📚 Learning: 2026-01-20T16:01:39.136Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 152
File: docs/adr/002-bridge-protocol.md:826-830
Timestamp: 2026-01-20T16:01:39.136Z
Learning: In the tywrap BridgeProtocol architecture (ADR-002), `HttpIO` (and other Transport implementations like `ProcessIO`, `PyodideIO`) implements only the `Transport` interface (methods: `send`, `isReady`, `dispose`). The `RuntimeExecution` interface methods (`call`, `instantiate`, `callMethod`, `disposeInstance`) are implemented by `BridgeProtocol`, which delegates to `Transport` instances. Transport implementations should not include RuntimeExecution methods.

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-19T21:00:30.825Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: test/adversarial_playground.test.ts:339-360
Timestamp: 2026-01-19T21:00:30.825Z
Learning: In adversarial tests for the bridge (test/adversarial_playground.test.ts), recovery assertions should be kept in the finally block adjacent to cleanup to verify resilience under stress, even if that means post-timeout checks might mask the original error. The goal is to surface recovery failures as legitimate test failures.

Applied to files:

  • test/integration.test.ts
📚 Learning: 2026-01-19T21:14:37.032Z
Learnt from: bbopen
Repo: bbopen/tywrap PR: 127
File: src/runtime/bridge-core.ts:375-385
Timestamp: 2026-01-19T21:14:37.032Z
Learning: In tywrap (src/runtime/bridge-core.ts and similar), environment variable parsing follows a tolerant/best-effort policy. For example, `TYWRAP_CODEC_MAX_BYTES=1024abc` should be accepted as 1024. Only reject clearly invalid values (non-numeric start or <=0). This avoids surprising failures from minor typos.

Applied to files:

  • test/integration.test.ts
  • src/tywrap.ts
📚 Learning: 2026-03-17T05:27:08.277Z
Learnt from: CR
Repo: bbopen/tywrap PR: 0
File: CLAUDE.md:0-0
Timestamp: 2026-03-17T05:27:08.277Z
Learning: Applies to tywrap_ir/**/*.py : Python code must follow PEP 8 and use type hints

Applied to files:

  • src/tywrap.ts
  • tywrap_ir/tywrap_ir/ir.py
🪛 Ruff (0.15.6)
tywrap_ir/tywrap_ir/ir.py

[warning] 13-13: typing.Dict is deprecated, use dict instead

(UP035)


[warning] 13-13: typing.List is deprecated, use list instead

(UP035)


[warning] 17-17: Do not catch blind exception: Exception

(BLE001)


[warning] 121-121: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 130-130: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 166-166: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 181-181: Dynamically typed expressions (typing.Any) are disallowed in obj

(ANN401)


[error] 185-186: try-except-continue detected, consider logging the exception

(S112)


[warning] 185-185: Do not catch blind exception: Exception

(BLE001)


[warning] 191-191: Dynamically typed expressions (typing.Any) are disallowed in obj

(ANN401)


[warning] 199-199: Dynamically typed expressions (typing.Any) are disallowed in annotation

(ANN401)


[warning] 207-207: Do not catch blind exception: Exception

(BLE001)


[warning] 214-214: Do not catch blind exception: Exception

(BLE001)


[warning] 229-229: Dynamically typed expressions (typing.Any) are disallowed in *annotations

(ANN401)


[warning] 250-250: Dynamically typed expressions (typing.Any) are disallowed in obj

(ANN401)


[warning] 250-250: Dynamically typed expressions (typing.Any) are disallowed in *annotations

(ANN401)


[warning] 258-258: Dynamically typed expressions (typing.Any) are disallowed in obj

(ANN401)


[warning] 259-259: Dynamically typed expressions (typing.Any) are disallowed in *annotations

(ANN401)


[warning] 274-274: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[warning] 274-274: Dynamically typed expressions (typing.Any) are disallowed in _unwrap_type_alias_value

(ANN401)


[warning] 280-280: Do not catch blind exception: Exception

(BLE001)


[warning] 284-284: Dynamically typed expressions (typing.Any) are disallowed in module

(ANN401)


[warning] 287-287: Do not catch blind exception: Exception

(BLE001)


[warning] 292-292: Do not catch blind exception: Exception

(BLE001)


[warning] 304-304: Do not call getattr with a constant attribute value. It is not any safer than normal property access.

Replace getattr with attribute access

(B009)


[warning] 311-311: Dynamically typed expressions (typing.Any) are disallowed in value

(ANN401)


[error] 322-323: try-except-pass detected, consider logging the exception

(S110)


[warning] 322-322: Do not catch blind exception: Exception

(BLE001)


[warning] 327-327: Dynamically typed expressions (typing.Any) are disallowed in module

(ANN401)


[warning] 327-327: Unused function argument: module_name

(ARG001)


[warning] 327-327: Boolean-typed positional argument in function definition

(FBT001)


[error] 338-339: try-except-continue detected, consider logging the exception

(S112)


[warning] 338-338: Do not catch blind exception: Exception

(BLE001)


[warning] 356-356: Do not catch blind exception: Exception

(BLE001)


[warning] 371-371: Dynamically typed expressions (typing.Any) are disallowed in module

(ANN401)


[warning] 371-371: Unused function argument: module_name

(ARG001)


[warning] 371-371: Boolean-typed positional argument in function definition

(FBT001)


[error] 384-385: try-except-continue detected, consider logging the exception

(S112)


[warning] 384-384: Do not catch blind exception: Exception

(BLE001)


[warning] 409-409: Dynamically typed expressions (typing.Any) are disallowed in obj

(ANN401)


[warning] 418-418: Do not catch blind exception: Exception

(BLE001)


[warning] 423-423: Do not catch blind exception: Exception

(BLE001)


[warning] 426-426: Do not catch blind exception: Exception

(BLE001)


[error] 613-614: try-except-pass detected, consider logging the exception

(S110)


[warning] 613-613: Do not catch blind exception: Exception

(BLE001)

🔇 Additional comments (5)
src/utils/runtime.ts (1)

718-726: No issues in fallback hash block change.

This edit appears to be formatting-only and preserves existing fallback behavior and fixed-length output guarantees.

src/runtime/node.ts (2)

77-77: Legacy warmup union is correctly preserved for migration-safe errors.

This keeps backwards-compatible typing while still allowing normalizeWarmupCommands to fail fast with a clear migration message at runtime.


612-612: Warmup error message fallback remains safe and clear.

The condensed conditional preserves the same defensive behavior and readable failure output.

src/tywrap.ts (1)

470-476: Add regression test for method generic shadowing.

The implementation correctly handles nested generic shadowing: annotationTypeParameters is built as [...inheritedTypeParameters, ...localTypeParameters], and parseAnnotationToPythonType() uses knownTypeParameters.get() to resolve names—a Map lookup that respects insertion order, so local parameters correctly shadow inherited ones. However, there is no explicit test for this case (e.g., class Box[T]: def method[T](...)). Add a test to prevent regression.

tywrap_ir/tywrap_ir/ir.py (1)

311-324: The suggested fix does not actually address the stated problem.

Removing inspect.isclass from line 312 has no effect on the extraction of simple type aliases like UserId = str. Testing confirms that both the current and proposed implementations produce identical results—they reject simple class assignments because those assignments lack the properties that _is_type_alias_value() checks for (TypeAliasType, GenericAlias, typing.get_origin() result, or __parameters__ attribute).

The review comment's diagnosis is incomplete. The inspect.isclass check is not the barrier preventing UserId = str from being extracted; the barrier is that str has none of the characteristics that identify type aliases. To actually capture undecorated class assignments as type aliases would require either an explicit TypeAlias annotation (UserId: TypeAlias = str) or a substantially different detection approach in _is_type_alias_value().

			> Likely an incorrect or invalid review comment.

Comment thread docs/type-mapping-matrix.md Outdated
Comment on lines +122 to +126
| Python Type | TypeScript Type | Notes |
| -------------------------- | --------------- | --------------------------------------------------------------------------------------------------- |
| `TypeVar('T')` | `T` | Emitted as a declaration-level generic parameter when tywrap can declare it safely |
| `TypeVar('T', bound=Base)` | `unknown` | Bound information is currently treated conservatively instead of emitting an unsupported TS generic |
| `TypeVar('T', str, int)` | `unknown` | Constraints are currently treated conservatively instead of emitting a partial TS equivalent |
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Document variance fallback for TypeVar.

The generator only emits declaration-level generics for invariant TypeVars. Covariant and contravariant params currently fall back too, so this table now over-promises by only calling out bounds and constraints.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/type-mapping-matrix.md` around lines 122 - 126, The table entry for
TypeVar is misleading: update the rows for `TypeVar('T')`, `TypeVar('T',
bound=Base)`, and `TypeVar('T', str, int)` to explicitly state that only
invariant TypeVars are emitted as declaration-level generics and that
covariant/contravariant TypeVars (variance) as well as TypeVars with bounds or
constraints currently fall back to `unknown`; revise the `TypeVar('T')` row to
note "invariant only" and add a brief note to the other two rows that they cover
both bounds/constraints and variance fallbacks so the table no longer
over-promises.

Comment thread src/core/generator.ts
Comment on lines +146 to +170
typeParameters.forEach(param => {
if (
param.kind === 'typevar' &&
!param.bound &&
!(param.constraints && param.constraints.length > 0) &&
(!param.variance || param.variance === 'invariant')
) {
emitted.push({ name: param.name, declaration: param.name });
emittedNames.add(param.name);
return;
}

if (param.kind === 'paramspec' && callableParamSpecs.has(param.name)) {
emitted.push({ name: param.name, declaration: `${param.name} extends unknown[]` });
emittedNames.add(param.name);
emittedParamSpecs.add(param.name);
}
});

return {
currentModule,
declaration:
emitted.length > 0 ? `<${emitted.map(param => param.declaration).join(', ')}>` : '',
typeArguments: emitted.length > 0 ? `<${emitted.map(param => param.name).join(', ')}>` : '',
emittedNames,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp/repo && wc -l src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && sed -n '146,170p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && sed -n '1,50p' src/core/generator.ts | head -30

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && sed -n '100,180p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && rg -n "escapeIdentifier" src/core/generator.ts -A 5

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && sed -n '270,310p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && rg -n "param\.name" src/core/generator.ts -B 2 -A 2

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && rg -n "default|new|extends|class|function" src/core/generator.ts | head -20

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && sed -n '128,200p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && sed -n '200,280p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && rg -n "type GenericRenderContext\|interface GenericRenderContext" -A 10 src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && rg -n "GenericRenderContext" src/core/generator.ts | head -20

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

cd /tmp/repo && sed -n '1,130p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 116


🏁 Script executed:

pwd && ls -la

Repository: bbopen/tywrap

Length of output: 2125


🏁 Script executed:

git ls-files src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 78


🏁 Script executed:

cat -n src/core/generator.ts | sed -n '146,170p'

Repository: bbopen/tywrap

Length of output: 1139


🏁 Script executed:

cat -n src/core/generator.ts | sed -n '1,50p'

Repository: bbopen/tywrap

Length of output: 1332


🏁 Script executed:

cat -n src/core/generator.ts | sed -n '100,180p'

Repository: bbopen/tywrap

Length of output: 3287


🏁 Script executed:

rg -n "escapeIdentifier" src/core/generator.ts -A 5

Repository: bbopen/tywrap

Length of output: 7804


🏁 Script executed:

cat -n src/core/generator.ts | sed -n '270,310p'

Repository: bbopen/tywrap

Length of output: 1578


🏁 Script executed:

rg -n "param\.name" src/core/generator.ts -B 2 -A 2 | head -50

Repository: bbopen/tywrap

Length of output: 856


🏁 Script executed:

cat -n src/core/generator.ts | sed -n '1,180p'

Repository: bbopen/tywrap

Length of output: 5976


Escape emitted generic identifiers before emitting them in TypeScript declarations.

param.name is copied directly into <...> without sanitization, but Python allows names like default or new that are invalid in TypeScript. For example, TypeVar("default") produces <default> which is a syntax error. Use this.escapeIdentifier(param.name) when building the declaration field (lines 153, 159) and name field in the emitted array, and update emittedNames to store the escaped name so that sanitizeType checks match the emitted identifiers.

Also applies to: 274-276, 300-306

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/generator.ts` around lines 146 - 170, The emitted generic
identifiers are inserted raw causing invalid TS for names like "default" —
update the code paths that build the emitted array and
emittedNames/emittedParamSpecs to use this.escapeIdentifier(param.name) instead
of param.name: when pushing into emitted use declaration:
`${this.escapeIdentifier(param.name)}${...}` (or
`${this.escapeIdentifier(param.name)} extends unknown[]` for paramspecs) and set
the pushed object's name to the escaped identifier; also add the escaped
identifier to emittedNames and emittedParamSpecs so later sanitizeType checks
match; apply the same replacement wherever emitted/declaration/typeArguments are
composed (including other spots that build emitted.map(param => param.name) and
emitted.map(param => param.declaration)) and ensure all uses of param.name in
these generators are escaped via this.escapeIdentifier.

Comment thread src/core/generator.ts
Comment on lines +175 to +185
private mergeGenericRenderContexts(
outer: GenericRenderContext,
inner: GenericRenderContext
): GenericRenderContext {
return {
currentModule: inner.currentModule ?? outer.currentModule,
declaration: inner.declaration,
typeArguments: inner.typeArguments,
emittedNames: new Set([...outer.emittedNames, ...inner.emittedNames]),
emittedParamSpecs: new Set([...outer.emittedParamSpecs, ...inner.emittedParamSpecs]),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's locate the file and check its size
find . -type f -name "generator.ts" | head -20

Repository: bbopen/tywrap

Length of output: 80


🏁 Script executed:

# Get the file size to determine if we can read it entirely
wc -l src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 83


🏁 Script executed:

# Read the relevant sections around lines 175-185 and 274-276
sed -n '170,190p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 713


🏁 Script executed:

# Also read the 274-276 area
sed -n '270,280p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 440


🏁 Script executed:

# Search for GenericRenderContext type definition
rg "type GenericRenderContext|interface GenericRenderContext" -A 10

Repository: bbopen/tywrap

Length of output: 592


🏁 Script executed:

# Search for other usages of mergeGenericRenderContexts to understand how it's used
rg "mergeGenericRenderContexts" -B 3 -A 3

Repository: bbopen/tywrap

Length of output: 1168


🏁 Script executed:

# Look for any shadow-related logic or type guards in the file
rg "shadow" -i src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 39


🏁 Script executed:

# Read lines 270-285 to see the context around 274-276
sed -n '270,285p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 567


🏁 Script executed:

# Search for how emittedNames is used in type resolution
rg "emittedNames\.has|emittedNames\." -B 2 -A 2

Repository: bbopen/tywrap

Length of output: 955


🏁 Script executed:

# Look for sanitizeType method and understand its full scope
sed -n '250,300p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 1818


🏁 Script executed:

# Check how the generic contexts are created initially
rg "emittedNames:" -B 3 -A 1

Repository: bbopen/tywrap

Length of output: 730


🏁 Script executed:

# Search for type parameter handling and scope awareness
rg "typevar|paramspec" src/core/generator.ts -B 2 -A 2 | head -80

Repository: bbopen/tywrap

Length of output: 1353


🏁 Script executed:

# Check if there are any TODOs or comments about shadowing or scope
rg "TODO|FIXME|BUG|shadow" -i src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 39


🏁 Script executed:

# Look for how declaration field is used in GenericRenderContext
rg "declaration" src/core/generator.ts -B 2 -A 2 | head -40

Repository: bbopen/tywrap

Length of output: 1304


🏁 Script executed:

# Check if there's any scope tracking mechanism elsewhere
rg "scope|origin|priority" -i src/core/generator.ts | head -20

Repository: bbopen/tywrap

Length of output: 39


🏁 Script executed:

# Examine the complete flow: how contexts are created and what declaration contains
sed -n '100,170p' src/core/generator.ts

Repository: bbopen/tywrap

Length of output: 2474


Make merged generic contexts shadow-aware.

The merged context unions all emitted names into a single set without tracking scope, so an inner (method-local) type parameter can silently resolve as an outer (class-level) parameter. For example, class Box[T]: def f[T: int](x: T) -> T would resolve the method's T through the class parameter instead of recognizing the shadow. The sanitizeType check at line 276 (ctx.emittedNames.has(type.name)) cannot distinguish between scopes. While declaration and typeArguments fields correctly use only the inner scope, emittedNames needs to track scope origin or precedence to prevent shadowed parameters from resolving through outer contexts.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/core/generator.ts` around lines 175 - 185, mergeGenericRenderContexts
currently unions emittedNames across scopes which loses shadowing info and
allows inner type parameters to be resolved to outer names; update
mergeGenericRenderContexts to preserve scope precedence instead of a flat Set
(e.g., make emittedNames a stack/array of Sets or a structure that records
origin order so lookups consult inner first), keep declaration and typeArguments
as-is, and adapt sanitizeType (the ctx.emittedNames.has(...) check) to consult
the new precedence-aware emittedNames structure so inner/declared names shadow
outer ones; refer to mergeGenericRenderContexts, emittedNames, and sanitizeType
when making the change.

Comment thread test/integration.test.ts
Comment on lines +225 to +277
await writeFile(
join(compileDir, 'consumer.ts'),
`import { Container, Pair, Transform, acceptTransform, forward, identity, passthrough } from '../generated/generic_module.generated.js';

const pair: Pair<string> = ['a', 'b'];
const transform: Transform<[number], string> = (...args) => String(args[0]);
const container = Container.fromHandle<number>('handle');
const accepted: Promise<Transform<[number], string>> = acceptTransform<[number], string>(transform);
const forwarded: Promise<Container<number>> = forward<number>(container);
const resolved: Promise<number> = identity<number>(1);
const cloned: Promise<Container<number>> = container.clone();
const identified: Promise<[number, string]> = container.id<string>('value');
const passthroughResult: Promise<void> = passthrough([1, 2], { flag: true });

const checkResult = await generate(options as any, { check: true });
expect((checkResult.outOfDate ?? []).includes(generatedTsPath as string)).toBe(true);
void pair;
void transform;
void accepted;
void forwarded;
void resolved;
void cloned;
void identified;
void passthroughResult;
`,
'utf-8'
);
await writeFile(
join(compileDir, 'tsconfig.json'),
JSON.stringify(
{
compilerOptions: {
target: 'ES2022',
module: 'NodeNext',
moduleResolution: 'NodeNext',
strict: true,
noEmit: true,
skipLibCheck: false,
baseUrl: '.',
paths: {
'tywrap/runtime': ['./runtime-stub'],
},
},
include: [
'./consumer.ts',
'./runtime-stub.d.ts',
'../generated/generic_module.generated.ts',
'../generated/generic_module.generated.d.ts',
],
},
null,
2
),
'utf-8'
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In TypeScript with "moduleResolution": "NodeNext", if a source file imports "../generated/foo.generated.js" and both "../generated/foo.generated.ts" and "../generated/foo.generated.d.ts" exist in the program, which file is used for type checking?

💡 Result:

In TypeScript with "moduleResolution": "NodeNext", when importing "../generated/foo.generated.js" and both "../generated/foo.generated.ts" and "../generated/foo.generated.d.ts" exist in the program, TypeScript uses the foo.generated.ts file for type checking. TypeScript's NodeNext resolution first checks for the source file (foo.generated.ts) matching the import extension pattern, then declaration files (.d.ts), then JS files. Since foo.generated.ts exists and provides full type information (including implementation for emission if needed), it takes precedence over foo.generated.d.ts for type checking. The .d.ts is typically for JS modules without source, but here the source is preferred when present.

Citations:


🏁 Script executed:

# Check the test file and surrounding context
wc -l test/integration.test.ts

Repository: bbopen/tywrap

Length of output: 85


🏁 Script executed:

# Read the specific section mentioned in the review (lines 225-277)
sed -n '225,277p' test/integration.test.ts

Repository: bbopen/tywrap

Length of output: 1815


🏁 Script executed:

# Check the second mentioned section (lines 341-378)
sed -n '341,378p' test/integration.test.ts

Repository: bbopen/tywrap

Length of output: 977


🏁 Script executed:

# Check if generated files directory exists and what's in it
find . -path "*/generated/generic_module.generated.*" -type f 2>/dev/null | head -20

Repository: bbopen/tywrap

Length of output: 39


🏁 Script executed:

# Check the actual structure around these test areas to understand context
sed -n '215,285p' test/integration.test.ts | cat -n

Repository: bbopen/tywrap

Length of output: 3094


Remove generated .ts files from test compiles to actually validate the .d.ts declarations.

Both test programs include the generated .ts implementation alongside the .d.ts declarations in the include array. With moduleResolution: "NodeNext", TypeScript binds the import of ../generated/foo.generated.js to the sibling .ts source file when present, bypassing the declarations entirely. This undermines the declaration validation these tests should provide.

Exclude the .ts files from the include array to compile only against the declarations:

  • Remove '../generated/generic_module.generated.ts' (line 225-277 test, also lines 341-378)
  • Remove '../generated/protocol_module.generated.ts' (lines 341-378 test)

This ensures the consumer actually type-checks against the exported .d.ts surface.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/integration.test.ts` around lines 225 - 277, The tsconfig written in the
test includes the generated implementation .ts files which causes TypeScript to
bind imports to the .ts sources instead of the .d.ts declarations; update the
tsconfig JSON written in the test (the object passed to writeFile for
tsconfig.json under compileDir) to remove
'../generated/generic_module.generated.ts' (and similarly remove
'../generated/protocol_module.generated.ts' in the other tsconfig write) from
the "include" array so the test compiles only against the .d.ts files and
validates the declaration surface.

Comment on lines +68 to +99
async function supportsVariadicTypingFeatures(): Promise<boolean> {
const result = await processUtils.exec(PYTHON_EXECUTABLE, [
'-c',
`
try:
from typing import ParamSpec, TypeVarTuple, Unpack
except ImportError:
try:
from typing_extensions import ParamSpec, TypeVarTuple, Unpack
except ImportError:
raise SystemExit(1)
raise SystemExit(0)
`,
]);
return result.code === 0;
}

async function supportsTypingExtensionsBackports(): Promise<boolean> {
const result = await processUtils.exec(PYTHON_EXECUTABLE, [
'-c',
'from typing_extensions import ParamSpec, TypeVarTuple, Unpack',
]);
return result.code === 0;
}

async function supportsPep695Syntax(): Promise<boolean> {
const result = await processUtils.exec(PYTHON_EXECUTABLE, [
'-c',
'import sys; raise SystemExit(0 if sys.version_info >= (3, 12) else 1)',
]);
return result.code === 0;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Extract the Python feature probes into a shared test helper.

supportsVariadicTypingFeatures() and supportsPep695Syntax() are now duplicated here and in test/integration.test.ts. Moving them to a common helper will keep the gating logic from drifting as more version-specific cases get added.

Comment on lines +232 to +370
test('maps generic typing.Awaitable[T] to Promise<T>', () => {
const awaitableType: PythonType = {
kind: 'generic',
name: 'Awaitable',
module: 'typing',
typeArgs: [{ kind: 'primitive', name: 'str' }],
};

const result = mapper.mapPythonType(awaitableType) as TSGenericType;

expect(result.kind).toBe('generic');
expect(result.name).toBe('Promise');
expect(result.typeArgs).toEqual([{ kind: 'primitive', name: 'string' }]);
});

test('maps generic typing.Sequence[T] to Array<T>', () => {
const sequenceType: PythonType = {
kind: 'generic',
name: 'Sequence',
module: 'typing',
typeArgs: [{ kind: 'primitive', name: 'int' }],
};

const result = mapper.mapPythonType(sequenceType) as TSGenericType;

expect(result.kind).toBe('generic');
expect(result.name).toBe('Array');
expect(result.typeArgs).toEqual([{ kind: 'primitive', name: 'number' }]);
});

test('maps generic collections.abc.Sequence[T] to Array<T>', () => {
const sequenceType: PythonType = {
kind: 'generic',
name: 'Sequence',
module: 'collections.abc',
typeArgs: [{ kind: 'primitive', name: 'bool' }],
};

const result = mapper.mapPythonType(sequenceType) as TSGenericType;

expect(result.kind).toBe('generic');
expect(result.name).toBe('Array');
expect(result.typeArgs).toEqual([{ kind: 'primitive', name: 'boolean' }]);
});

test('maps generic typing_extensions.Mapping[K, V] to an object index signature', () => {
const mappingType: PythonType = {
kind: 'generic',
name: 'Mapping',
module: 'typing_extensions',
typeArgs: [
{ kind: 'primitive', name: 'str' },
{ kind: 'primitive', name: 'int' },
],
};

const result = mapper.mapPythonType(mappingType) as TSObjectType;

expect(result.kind).toBe('object');
expect(result.properties).toEqual([]);
expect(result.indexSignature).toEqual({
keyType: { kind: 'primitive', name: 'string' },
valueType: { kind: 'primitive', name: 'number' },
});
});

test('preserves third-party Sequence generics', () => {
const sequenceType: PythonType = {
kind: 'generic',
name: 'Sequence',
module: 'pkg',
typeArgs: [{ kind: 'primitive', name: 'int' }],
};

const result = mapper.mapPythonType(sequenceType) as TSGenericType;

expect(result.kind).toBe('generic');
expect(result.name).toBe('Sequence');
expect(result.typeArgs).toEqual([{ kind: 'primitive', name: 'number' }]);
});

test('preserves third-party Awaitable generics', () => {
const awaitableType: PythonType = {
kind: 'generic',
name: 'Awaitable',
module: 'pkg',
typeArgs: [{ kind: 'primitive', name: 'str' }],
};

const result = mapper.mapPythonType(awaitableType) as TSGenericType;

expect(result.kind).toBe('generic');
expect(result.name).toBe('Awaitable');
expect(result.typeArgs).toEqual([{ kind: 'primitive', name: 'string' }]);
});

test('preserves third-party Coroutine generics', () => {
const coroutineType: PythonType = {
kind: 'generic',
name: 'Coroutine',
module: 'vendor',
typeArgs: [
{ kind: 'primitive', name: 'int' },
{ kind: 'primitive', name: 'int' },
{ kind: 'primitive', name: 'str' },
],
};

const result = mapper.mapPythonType(coroutineType) as TSGenericType;

expect(result.kind).toBe('generic');
expect(result.name).toBe('Coroutine');
expect(result.typeArgs).toEqual([
{ kind: 'primitive', name: 'number' },
{ kind: 'primitive', name: 'number' },
{ kind: 'primitive', name: 'string' },
]);
});

test('preserves third-party Mapping generics', () => {
const mappingType: PythonType = {
kind: 'generic',
name: 'Mapping',
module: 'vendor',
typeArgs: [
{ kind: 'primitive', name: 'str' },
{ kind: 'primitive', name: 'int' },
],
};

const result = mapper.mapPythonType(mappingType) as TSGenericType;

expect(result.kind).toBe('generic');
expect(result.name).toBe('Mapping');
expect(result.typeArgs).toEqual([
{ kind: 'primitive', name: 'string' },
{ kind: 'primitive', name: 'number' },
]);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add a direct typing.Coroutine[...] conversion case.

The new assertions cover the preserve-as-is branch for third-party Coroutine, but not the new typing.Coroutine[A, B, T] -> Promise<T> mapper path. A regression there would still pass this suite.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@test/type_mapping_enhanced.test.ts` around lines 232 - 370, Add a direct
conversion in mapPythonType for generic types named "Coroutine" coming from
module "typing": when encountering a PythonType with kind 'generic', name
'Coroutine', and module 'typing', map it to a TS generic Promise using the third
type argument as the promise value type (i.e., Promise<T>), making sure to map
that T via the existing mapper logic (call mapper.mapPythonType on typeArgs[2])
and handle the case where fewer than 3 typeArgs exist gracefully (fallback to
Promise<any> or similar). Update the branch that currently preserves third-party
Coroutine (module !== 'typing') to remain unchanged so only the 'typing' module
gets the Promise conversion, and add/adjust tests to cover
typing.Coroutine[A,B,T] -> Promise<T>.

Comment thread tywrap_ir/tywrap_ir/ir.py
Comment on lines 99 to 104
str_repr = str(annotation)

# Clean up class references to show just the class name
if str_repr.startswith("<class '") and str_repr.endswith("'>"):
# Extract class name from <class 'module.ClassName'>
class_path = str_repr[8:-2] # Remove <class ' and '>
if '.' in class_path:
return class_path.split('.')[-1] # Just the class name
class_path = str_repr[8:-2]
if "." in class_path:
return class_path.split(".")[-1]
return class_path
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cd /tmp && find . -name "ir.py" -path "*/tywrap_ir/*" 2>/dev/null | head -5

Repository: bbopen/tywrap

Length of output: 39


🏁 Script executed:

git ls-files | grep -E "tywrap_ir.*ir\.py$"

Repository: bbopen/tywrap

Length of output: 118


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '90,115p'

Repository: bbopen/tywrap

Length of output: 1143


🏁 Script executed:

rg -n "_stringify_annotation" tywrap_ir/tywrap_ir/ir.py

Repository: bbopen/tywrap

Length of output: 1141


🏁 Script executed:

rg -n "_stringify_annotation" tywrap_ir/ --type py

Repository: bbopen/tywrap

Length of output: 1626


🏁 Script executed:

rg -B5 -A10 "def _stringify_annotation" tywrap_ir/tywrap_ir/ir.py

Repository: bbopen/tywrap

Length of output: 588


🏁 Script executed:

find tywrap_ir -name "*test*.py" -o -name "test_*" | head -10

Repository: bbopen/tywrap

Length of output: 39


🏁 Script executed:

git ls-files | grep -i test | grep ir

Repository: bbopen/tywrap

Length of output: 503


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '380,400p'

Repository: bbopen/tywrap

Length of output: 1076


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '215,225p'

Repository: bbopen/tywrap

Length of output: 584


🏁 Script executed:

cat test/__snapshots__/ir_snapshot.test.ts.snap | head -100

Repository: bbopen/tywrap

Length of output: 498


🏁 Script executed:

cat -n test/__snapshots__/ir_snapshot.test.ts.snap | sed -n '1,500p'

Repository: bbopen/tywrap

Length of output: 701


🏁 Script executed:

rg -A5 -B5 "annotation" test/__snapshots__/ir_snapshot.test.ts.snap | head -100

Repository: bbopen/tywrap

Length of output: 39


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '1,40p'

Repository: bbopen/tywrap

Length of output: 1084


🏁 Script executed:

rg -B3 -A3 "datetime" tywrap_ir/ --type py

Repository: bbopen/tywrap

Length of output: 39


🏁 Script executed:

python3 << 'EOF'
import inspect
from datetime import datetime

# Simulate what _stringify_annotation does
annotation = datetime
str_repr = str(annotation)
print(f"str(annotation) = {str_repr}")

if str_repr.startswith("<class '") and str_repr.endswith("'>"):
    class_path = str_repr[8:-2]
    print(f"class_path = {class_path}")
    if "." in class_path:
        shortened = class_path.split(".")[-1]
        print(f"shortened = {shortened}")
    else:
        print(f"no dots, returns = {class_path}")

# Test with a custom class
class MyClass:
    pass

annotation2 = MyClass
str_repr2 = str(annotation2)
print(f"\nstr(MyClass) = {str_repr2}")
EOF

Repository: bbopen/tywrap

Length of output: 197


🏁 Script executed:

rg -B10 -A10 "IRTypeAlias\|definition" tywrap_ir/tywrap_ir/ir.py | head -80

Repository: bbopen/tywrap

Length of output: 39


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '50,75p'

Repository: bbopen/tywrap

Length of output: 706


🏁 Script executed:

python3 << 'EOF'
# Check what happens with nested modules and collisions
test_cases = [
    "<class 'datetime.datetime'>",
    "<class 'collections.defaultdict'>",
    "<class 'MyClass'>",
    "<class '__main__.MyClass'>",
]

for case in test_cases:
    if case.startswith("<class '") and case.endswith("'>"):
        class_path = case[8:-2]
        if "." in class_path:
            shortened = class_path.split(".")[-1]
            print(f"{case:45} -> {shortened}")
        else:
            print(f"{case:45} -> {class_path}")
EOF

Repository: bbopen/tywrap

Length of output: 289


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '73,90p'

Repository: bbopen/tywrap

Length of output: 496


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '360,410p'

Repository: bbopen/tywrap

Length of output: 2133


🏁 Script executed:

grep -r "definition" test/ --include="*.ts" | head -20

Repository: bbopen/tywrap

Length of output: 613


🏁 Script executed:

python3 << 'EOF'
# Scenario: collision risk with module stripping
# If we have both a local datetime and datetime.datetime

print("Scenario 1: Type alias for datetime.datetime")
print("  definition = 'datetime' (current behavior)")
print("  Problem: can't distinguish from local datetime class")
print()

print("Scenario 2: Type alias for collections.defaultdict")  
print("  definition = 'defaultdict' (current behavior)")
print("  Problem: might collide with a local defaultdict")
print()

print("The reviewer's suggestion: keep full path")
print("  definition = 'datetime.datetime'")
print("  definition = 'collections.defaultdict'")
print("  Downstream TypeScript can shorten if needed")
EOF

Repository: bbopen/tywrap

Length of output: 516


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '370,400p'

Repository: bbopen/tywrap

Length of output: 1630


🏁 Script executed:

cat -n test/ir_extraction_advanced.test.ts | sed -n '1,100p'

Repository: bbopen/tywrap

Length of output: 3741


🏁 Script executed:

rg -B5 -A5 "expect.*definition" test/ --type ts

Repository: bbopen/tywrap

Length of output: 965


🏁 Script executed:

rg -B10 -A10 "type_alias" test/ir_extraction_advanced.test.ts | head -80

Repository: bbopen/tywrap

Length of output: 3244


🏁 Script executed:

grep -r "datetime\|collections\|external" test/fixtures/python/ --include="*.py"

Repository: bbopen/tywrap

Length of output: 3143


🏁 Script executed:

python3 << 'EOF'
# Check what str() returns for various type objects
import typing
from datetime import datetime
from collections import defaultdict

test_values = [
    datetime,
    defaultdict,
    str,
    int,
    list,
]

for val in test_values:
    print(f"str({val.__name__:15}) = {str(val)}")
    
# Check what happens with generic aliases
print("\nGeneric aliases:")
print(f"str(typing.List[int]) = {str(typing.List[int])}")
print(f"str(tuple[int, int]) = {str(tuple[int, int])}")
EOF

Repository: bbopen/tywrap

Length of output: 374


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '130,170p'

Repository: bbopen/tywrap

Length of output: 1775


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '200,240p'

Repository: bbopen/tywrap

Length of output: 1596


🏁 Script executed:

python3 << 'EOF'
# Test what annotation strings look like for type aliases
import typing

# Simulate what _stringify_annotation returns for various cases
test_cases = [
    ("bare class", "<class 'datetime.datetime'>"),
    ("bare class no module", "<class 'str'>"),
    ("generic alias", "tuple[int, int]"),
    ("typing generic", "typing.List[int]"),
]

for label, val in test_cases:
    if val.startswith("<class '") and val.endswith("'>"):
        class_path = val[8:-2]
        if "." in class_path:
            shortened = class_path.split(".")[-1]
            print(f"{label:25} {val:40} → {shortened}")
        else:
            print(f"{label:25} {val:40} → {class_path}")
    else:
        print(f"{label:25} {val:40} → (no change)")
EOF

Repository: bbopen/tywrap

Length of output: 369


🏁 Script executed:

rg -B5 -A5 "definition.*datetime\|datetime.*definition" test/ tywrap_ir/

Repository: bbopen/tywrap

Length of output: 39


🏁 Script executed:

python3 << 'EOF'
# Simulate the collision scenario the reviewer mentioned
import sys
from io import StringIO

# Create a scenario where we have local datetime and external datetime.datetime
code = """
from datetime import datetime

# User defines a local wrapper
class datetime:
    '''A local datetime wrapper'''
    pass

# Type alias using the local class
UserId = datetime
"""

# Parse and check what would be extracted
lines = code.strip().split('\n')
for i, line in enumerate(lines, 1):
    print(f"{i:2}: {line}")

print("\nScenario: _stringify_annotation(datetime) where datetime is the LOCAL class")
print("Current behavior would return: 'datetime'")
print("But which datetime? The imported one or the local wrapper?")
print("Information loss creates ambiguity in the IR.")
EOF

Repository: bbopen/tywrap

Length of output: 492


🏁 Script executed:

cat -n tywrap_ir/tywrap_ir/ir.py | sed -n '95,108p'

Repository: bbopen/tywrap

Length of output: 638


🏁 Script executed:

python3 << 'EOF'
# Check if there are any test assertions about class name stripping
import subprocess
result = subprocess.run(
    ['grep', '-r', 'definition.*:.*', 'test/', '--include=*.ts'],
    capture_output=True,
    text=True,
    cwd='/tmp'
)
# Filter for meaningful results
lines = [l for l in result.stdout.split('\n') if 'definition' in l and ('datetime' in l or 'defaultdict' in l or '.' in l)]
for line in lines[:10]:
    print(line)
if not lines:
    print("No test assertions found checking definition strings with module paths")
EOF

Repository: bbopen/tywrap

Length of output: 127


Preserve module qualification in class annotations to maintain type identity.

Stripping the module path from <class 'datetime.datetime'> to datetime loses namespace information and creates collision risk with local classes of the same name. Keeping the full qualified path allows downstream type processing to decide when shortening is safe.

♻️ Suggested fix
         if str_repr.startswith("<class '") and str_repr.endswith("'>"):
             class_path = str_repr[8:-2]
-            if "." in class_path:
-                return class_path.split(".")[-1]
-            return class_path
+            return class_path
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tywrap_ir/tywrap_ir/ir.py` around lines 99 - 104, The current logic strips
module qualification from class annotations by taking class_path.split(".")[-1];
instead, preserve the full qualified path so type identity is not lost: when
handling the string form of an annotation (variables str_repr, class_path,
annotation), return the whole class_path (e.g., "datetime.datetime") instead of
only the last segment; keep the existing guard that checks
str_repr.startswith("<class '") and str_repr.endswith("'>") and only change the
return behavior around class_path (remove the dot-splitting).

@bbopen
Copy link
Copy Markdown
Owner Author

bbopen commented Mar 20, 2026

@coderabbitai review

Please do a full, extensive review of the merged state this PR would introduce against the current base branch, not only the raw diff.

Focus on:

  • blocker correctness bugs or regressions in the final merged result
  • interactions with changes already on main
  • any reason this PR should not be approved if CI becomes green
  • any additional CI coverage or test matrix entries this PR introduces or now requires

If CI gates are green and you do not find blocker issues, please mark the review as passable/approved from your side.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 20, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

I'll do a thorough inspection of the key implementation files before writing the final review.


🏁 Script executed:

#!/bin/bash
# Inspect generator.ts for the wrap signature change and sanitizeType logic
cat src/core/generator.ts | head -200

Length of output: 5408


🏁 Script executed:

#!/bin/bash
# Check sanitizeType and GenericRenderContext in generator.ts
grep -n "sanitizeType\|GenericRenderContext\|GenericRenderParam\|collectCallableParamSpecs\|typeParamDecl\|wrap(" src/core/generator.ts | head -80

Length of output: 3673


🏁 Script executed:

#!/bin/bash
# Check the wrap() call sites to ensure all are updated
grep -n "this\.wrap\|\.wrap(" src/core/generator.ts

Length of output: 284


🏁 Script executed:

#!/bin/bash
# Check annotation-parser for the ~P.args / ~Name.kwargs handling
grep -n "paramspec_args\|paramspec_kwargs\|args\|kwargs\|splitQualified\|splitGeneric" src/core/annotation-parser.ts | head -60

Length of output: 613


🏁 Script executed:

#!/bin/bash
# Check mapper.ts for new type handlers
grep -n "mapParamSpec\|mapTypeVarTuple\|mapUnpack\|mapGenericType\|mapCallableType" src/core/mapper.ts | head -40

Length of output: 956


🏁 Script executed:

#!/bin/bash
# Check tywrap.ts for irVersion cache key and transformIrToTsModel
grep -n "irVersion\|TYWRAP_IR_VERSION\|typeAliases\|type_params\|type_aliases\|renderDts\|declaration" src/tywrap.ts | head -60

Length of output: 832


🏁 Script executed:

#!/bin/bash
# Check ir.py for type_params extraction correctness
grep -n "type_params\|IRTypeParam\|__type_params__\|__parameters__\|TypeVar\|ParamSpec\|TypeVarTuple" tywrap_ir/tywrap_ir/ir.py | head -80

Length of output: 2941


🏁 Script executed:

#!/bin/bash
# Check for any remaining ~T / ~P leakage in generator output tests
grep -n "~T\|~P\|Unpack\[" test/generator.test.ts | head -30

Length of output: 166


🏁 Script executed:

#!/bin/bash
# Check test coverage matrix - what Python versions are being tested
cat .github/workflows/*.yml 2>/dev/null || find . -name "*.yml" -path "*github*" | head -5

Length of output: 17333


🏁 Script executed:

#!/bin/bash
# Check if there are any issues with the declaration emission
grep -n "declaration\|GeneratedCode\|\.declaration" src/core/generator.ts | head -60

Length of output: 2288


🏁 Script executed:

#!/bin/bash
# Check types/index.ts for the new type definitions
cat src/types/index.ts

Length of output: 9678


@bbopen — I've done a thorough inspection of all the key paths this PR touches. Here is the full analysis.


Review: PR #210 — Safe TypeScript Generic Emission

✅ No blocker correctness bugs found


1. wrap() signature change — all call sites updated correctly

The private wrap(typescript, declaration, _sources) refactor has exactly 5 call sites, all correctly threaded:

Call site declaration value Notes
generateFunctionWrapper ${jsdoc}${declarationBody} Proper overload-only declaration
Protocol class fallback ts (same as runtime) Protocol emits a export type = alias — runtime and declaration are identical, correct
generateClassWrapper separate declaration Class with typed members
generateTypeAlias ts (same) A type alias is already a declaration
generateModuleDefinition assembled declaration string Aggregates all three categories

No orphaned or stale call sites.


2. sanitizeType — safe, no silent data loss

sanitizeType replaces only references to undeclared generic identifiers (i.e., typevar/paramspec/typevartuple names not present in emittedNames) with { kind: 'custom', name: 'Any' }. It also strips parameterSpec from callable when the spec wasn't emitted. This is exactly the right conservative behavior to prevent undeclared identifier leakage in generated .ts/.d.ts. Since Any is already the unknown-equivalent fallback documented in the PR, there's no unexpected information loss.


3. buildGenericRenderContext — emission rules are correct and conservative

Only emits:

  • typevar that is unconstrained, unbound, and invariant
  • paramspec that appears in a callable position (detected via collectCallableParamSpecs), emitted as P extends unknown[]

Bound/constrained TypeVar, TypeVarTuple, and Unpack silently fall back — matches the documented non-goals in #209.


4. Annotation-parser P.args / ~P.args / Request.args disambiguation — correct

The regex /^~?([A-Za-z_][A-Za-z0-9_]*)\.(args|kwargs)$/ is broad, but the guard condition that gates whether a match becomes paramspec_args correctly requires either a known paramspec entry in typeParameters or a leading ~. The three new test cases confirm this:

  • P.args with explicit typeParametersparamspec_args
  • ~P.args without typeParametersparamspec_args
  • Request.args (no ~, not in typeParameters) → custom

5. Cache invalidation — correct

TYWRAP_IR_VERSION = '0.2.0' is hardcoded in tywrap.ts, passed to the IR CLI via --ir-version 0.2.0, and included in IrCacheKeyObject.irVersion. Since computeIrCacheFilename SHA-256 digests the entire JSON-stringified key, stale 0.1.0-format caches are correctly invalidated. The new ir_cache_filename.test.ts explicitly asserts that 0.1.0 and 0.2.0 produce different filenames.


6. Declaration emission path — improved, no regression

The removed renderDts regex-based approach was fragile. The new path uses gen.declaration directly from the generator, which is assembled with full generic-context awareness. tywrap.ts line 267 (content: gen.declaration) correctly routes into the output.declaration path.


7. CI matrix — sufficient, new tests are gated correctly

Dimension Coverage
Node 20, 22
Python 3.10, 3.11, 3.12
OS ubuntu (matrix), macOS + Windows (smoke)
Runtime Bun (ubuntu + macOS)

The new integration tests in test/integration.test.ts and test/ir_extraction_advanced.test.ts use supportsVariadicTypingFeatures(), supportsTypingExtensionsBackports(), and supportsPep695Syntax() guards — they skip gracefully on Python 3.10/3.11 where PEP 695 is unavailable, and run fully on 3.12. The tsc --noEmit check inside the integration test provides a meaningful compile-verified regression gate.


8. Minor observations (non-blocking)

  • collectCallableParamSpecs does not recurse into typevar/typevartuple kinds — correct, those are not callable-bearing types.
  • mergeGenericRenderContexts: inner declaration/typeArguments override outer, while emittedNames/emittedParamSpecs are unioned — semantically correct for method-level overriding of class-level context.
  • Python 3.13 not in test matrix — not a blocker; PEP 695 coverage comes from 3.12.
  • python-suite-core/python-suite-data workflows do not exercise the new type_params IR path — but these are library-integration suites, not tywrap-generation tests; end-to-end coverage lives in the vitest integration suite with tsc validation.

Verdict

No blocker issues. The implementation is conservative, well-reasoned, properly tested with feature-detection guards, and the CI matrix is adequate. If CI gates pass, this PR is ready to merge.

[approve]


🧠 Learnings used
Learnt from: bbopen
Repo: bbopen/tywrap PR: 136
File: src/runtime/node.ts:238-249
Timestamp: 2026-01-20T01:33:12.841Z
Learning: In the tywrap repository, when reviewing init/startup patterns, prioritize cases that break core functionality over edge cases with diagnostic-only impact (e.g., missing bridgeInfo metadata). If workers are healthy and operational, defer low-impact defensive checks to follow-up PRs.

@codecov-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 74.93734% with 100 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/core/annotation-parser.ts 63.15% 28 Missing and 21 partials ⚠️
src/tywrap.ts 54.71% 1 Missing and 23 partials ⚠️
src/core/generator.ts 92.12% 9 Missing and 4 partials ⚠️
src/core/mapper.ts 79.06% 9 Missing ⚠️
src/utils/runtime.ts 0.00% 5 Missing ⚠️

📢 Thoughts on this report? Let us know!

@bbopen bbopen merged commit c977786 into main Mar 20, 2026
20 of 21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:codegen area:docs Area: documentation area:runtime area:tooling Area: tooling and CLI area:types codex documentation Improvements or additions to documentation enhancement New feature or request priority:p2 Priority P2 (medium)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(types): emit TypeScript generics for Python TypeVar / ParamSpec / TypeVarTuple

2 participants