Skip to content

feat(infra): wire ts-rs + specta codegen pipeline (T-117)#51

Merged
mpiton merged 3 commits into
mainfrom
feat/task-T-117-codegen-types
May 10, 2026
Merged

feat(infra): wire ts-rs + specta codegen pipeline (T-117)#51
mpiton merged 3 commits into
mainfrom
feat/task-T-117-codegen-types

Conversation

@mpiton
Copy link
Copy Markdown
Owner

@mpiton mpiton commented May 10, 2026

Summary

  • Replace Sprint 0 stub scripts/codegen-types.ts with a deterministic Rust → TypeScript codegen pipeline (ts-rs export tests + new specta-export bin + sorted barrel index.ts).
  • Annotate Task, Phase, Status, Priority, AppEvent, the uuid_newtype! / i64_newtype! macros in domain/shared/ids.rs with #[derive(TS)] + #[ts(export, ...)]; field-level overrides preserve the wire contract (OffsetDateTimestring, i64number, transparent newtypes flatten via #[ts(type = "string"|"number")]).
  • Add CI gate git diff --exit-code src/shared/types/ after pnpm run codegen so any Rust → TS drift fails the frontend-checks job.
  • Vitest smoke suite (src/shared/types/__tests__/codegen.test.ts) asserts file shapes (Task fields, Phase literal union, AppError adjacent {kind, detail}, AppEvent snake_case discriminator, TaskId flat string, commands.ts header).

Why

T-117 is P0 in sprint 2026-05-04 — blocks T-121 (final commit step) and T-126 (frontend wrapper imports Task). The renderer cannot consume IPC DTOs as long as src/shared/types/ is empty; CI cannot gate type drift while the codegen script is a no-op.

Changes

  • scripts/codegen-types.ts: real three-stage orchestrator (ts-rs tests → specta-export bin → barrel + verify).
  • src-tauri/src/bin/specta-export.rs + [[bin]] in Cargo.toml: deterministic commands.ts placeholder until T-118 wires tasks_list.
  • Domain annotations in domain/tasks/model.rs, domain/shared/events.rs, domain/shared/ids.rs.
  • .github/workflows/ci.yml: add Linux deps + Rust toolchain + Swatinem/rust-cache to frontend-checks job, then pnpm run codegen followed by the diff gate.
  • .oxfmtrc.json: ignore src/shared/types/** (parallels oxlint).
  • scripts/no-manual-deps.sh: scope dep-table detection to [*dependencies*] sections via awk so structural edits like [[bin]] no longer false-positive the lock gate.
  • Generated bindings committed: Task.ts, Phase.ts, Status.ts, Priority.ts, AppEvent.ts, AppError.ts, TaskId.ts, RunId.ts, WorktreeId.ts, ProfileId.ts, PrId.ts, commands.ts, index.ts.

Acceptance criteria

  • pnpm codegen produces src/shared/types/{Task,Phase,Status,Priority,AppEvent,AppError,commands}.ts.
  • Re-running pnpm codegen is byte-deterministic (verified via diff -r on two consecutive runs).
  • CI job fails when a Rust type changes without re-committing TS bindings.
  • pnpm exec tsc -b compiles with the generated types.

Test plan

  • cargo test --workspace — 165 passed (added 11 ts-rs export_bindings_* tests).
  • cargo clippy --workspace --all-targets -- -D warnings — clean.
  • cargo fmt --check — clean.
  • pnpm exec tsc -b — clean.
  • pnpm exec oxlint . --max-warnings=0 — clean.
  • pnpm exec vitest run — 14 / 14 passing.
  • pnpm run codegen twice in a row — no diff (deterministic).

Closes #32.

Summary by CodeRabbit

  • New Features

    • Deterministic Rust→TypeScript codegen added so frontend types (IDs, models, events, errors, commands) are generated from backend definitions.
  • Tests

    • Added tests validating generated type artifacts and shapes.
  • Chores

    • CI now runs codegen and fails on drift; pre-commit dependency checks and formatter ignore updated for generated types.
  • Documentation

    • CHANGELOG updated to document the new codegen pipeline.

Review Change Stack

Replaces the Sprint 0 stub `scripts/codegen-types.ts` with a deterministic
three-stage pipeline that emits TypeScript bindings for the IPC DTOs:
ts-rs `#[derive(TS)]` exports per-type files, the new
`src-tauri/src/bin/specta-export.rs` bin emits a placeholder
`commands.ts` (T-118 will populate it once `tasks_list` lands), and the
script writes a sorted barrel `index.ts`.

Annotates `Task`, `Phase`, `Status`, `Priority`, `AppEvent`, the
`uuid_newtype!` and `i64_newtype!` macros in `domain/shared/ids.rs` with
`#[derive(TS)]` + `#[ts(export, export_to = "../../src/shared/types/")]`,
plus field-level overrides for `time::OffsetDateTime` (string), `i64`
(number), and the transparent newtypes (string / number).

Adds a CI gate (`git diff --exit-code src/shared/types/` after
`pnpm run codegen`) that fails when a Rust type changes without
re-committing the regenerated TS bindings, and a vitest smoke suite
asserting the documented file shapes (Task fields, Phase literal union,
AppError adjacent-tagged kind/detail, AppEvent snake_case discriminator,
TaskId flat string alias, commands.ts AUTO-GENERATED header).

Closes #32.

- `.oxfmtrc.json`: ignore `src/shared/types/**` (parallels oxlint
  ignorePatterns) so ts-rs's fixed output style does not fight oxfmt
  defaults.
- `scripts/no-manual-deps.sh`: scope dep-table detection to
  `[*dependencies*]` sections via awk so adding a `[[bin]]` target no
  longer false-positives the lock-file gate.
- `cargo test --workspace`: 165 passed (11 new ts-rs export tests).
- `cargo clippy --workspace --all-targets -- -D warnings`: clean.
- `pnpm exec tsc -b` / `pnpm exec oxlint . --max-warnings=0` /
  `pnpm exec vitest run`: clean.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 10, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cf9234cf-e9f9-47a1-bd05-068138650eb5

📥 Commits

Reviewing files that changed from the base of the PR and between 4741efa and 670627c.

📒 Files selected for processing (1)
  • src-tauri/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • src-tauri/Cargo.toml

📝 Walkthrough

Walkthrough

Implements deterministic Rust→TypeScript codegen: annotate Rust types with ts-rs, add specta-export binary, add orchestration script to run cargo generation and write a barrel, produce generated TS files under src/shared/types/, add CI drift-check gate, and add validation tests.

Changes

Rust→TypeScript IPC DTO Codegen Pipeline

Layer / File(s) Summary
Rust Type Annotations
src-tauri/src/domain/shared/ids.rs, src-tauri/src/domain/shared/events.rs, src-tauri/src/domain/tasks/model.rs
ID newtypes and domain value objects are augmented with ts_rs::TS derives and #[ts(export, export_to = "../../src/shared/types/")]. Field-level #[ts(type = ...)] annotations map Rust primitives/timestamps/options to TypeScript types.
Codegen Orchestration Script & Binary
scripts/codegen-types.ts, src-tauri/Cargo.toml, src-tauri/src/bin/specta-export.rs
scripts/codegen-types.ts runs cargo test --lib export_bindings and cargo run --bin specta-export, cleans stale outputs, writes src/shared/types/index.ts barrel, and verifies required outputs. Cargo.toml adds [[bin]] specta-export and default-run. specta-export emits a deterministic placeholder commands.ts.
Generated TypeScript Types
src/shared/types/*.ts
Per-type generated modules: ID aliases (TaskId, RunId, WorktreeId, ProfileId, PrId), enums (Phase, Status, Priority), domain objects (Task), discriminated unions (AppEvent, AppError), placeholder commands.ts, generated barrel index.ts, and a Vitest suite validating outputs.
CI Workflow & Drift Gate
.github/workflows/ci.yml
frontend-checks now installs Linux Rust deps, sets up Rust toolchain and cache, runs pnpm run codegen, and fails if git diff --exit-code src/shared/types/ detects changes post-codegen. tsc -b is moved to run after codegen + drift-check.
Infrastructure & Tests
.oxfmtrc.json, scripts/no-manual-deps.sh, src/shared/types/__tests__/codegen.test.ts, CHANGELOG.md
Formatter ignores src/shared/types/**. scripts/no-manual-deps.sh tightens Cargo.toml dependency-add detection using an awk parser. Vitest suite checks generated files and exported type shapes. Changelog documents the pipeline.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related issues

Possibly related PRs

Poem

🐰 Hop hop—the types now flow free,
From Rust to TypeScript with ts-rs decree,
A codegen gate stands steady and tight,
Ensuring generated types are just right! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% 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
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly describes the main change: wiring a ts-rs and specta codegen pipeline, directly matching the primary objective of replacing the Sprint 0 stub with a functional pipeline.
Linked Issues check ✅ Passed All coding requirements from issue #32 are met: pnpm codegen produces required TS files, generation is deterministic, CI gates Rust→TS drift, and tsc -b compiles successfully with generated types.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the codegen pipeline: workflow updates, script implementation, Cargo.toml setup, domain type annotations, and generated outputs. No unrelated modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/task-T-117-codegen-types

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


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

@codspeed-hq
Copy link
Copy Markdown
Contributor

codspeed-hq Bot commented May 10, 2026

Merging this PR will not alter performance

✅ 7 untouched benchmarks


Comparing feat/task-T-117-codegen-types (670627c) with main (c50e18d)

Open in CodSpeed

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 current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 110-115: The echo statements in the "codegen-diff gate (no
uncommitted Rust → TS drift)" run block include backticks inside double-quoted
strings, causing bash command substitution when printing the error; change those
echo lines to use single-quoted strings (or escape the backticks) so the
messages display the literal text "pnpm run codegen" instead of executing it,
e.g., update the echo calls that reference `pnpm run codegen` after the git diff
check.

In `@CHANGELOG.md`:
- Line 9: The changelog contains a stale sentence claiming “No ts-rs in domain”
that contradicts this PR which adds #[derive(TS)] exports (e.g., types in
domain/tasks/model.rs, domain/shared/events.rs, and domain/shared/ids.rs) and
the new codegen pipeline (scripts/codegen-types.ts and
src-tauri/src/bin/specta-export.rs); edit CHANGELOG.md to remove or replace that
phrase with a concise accurate note stating that ts-rs derives were added to
domain models and that generated TS files are created under src/shared/types/ so
the entry matches the actual changes.

In `@scripts/codegen-types.ts`:
- Around line 27-35: REQUIRED currently omits generated ID alias files so
verify() can falsely succeed; update the REQUIRED array in
scripts/codegen-types.ts to include the generated ID alias files for the domain
types (e.g., TaskId.ts and the corresponding ID files for AppError, AppEvent,
Phase/Priority/Status if applicable) so verify() will fail when those generated
type aliases are missing; modify the REQUIRED constant (not the verify logic) by
adding the missing ID filenames like TaskId.ts, AppErrorId.ts, AppEventId.ts,
etc., matching the actual generated filenames used elsewhere.

In `@scripts/no-manual-deps.sh`:
- Around line 23-29: The git diff invocation uses -U0 which removes section
context and causes the awk state machine (the DEP_DIFF capture logic) to miss
additions inside unchanged dependency tables; change the git diff call in
scripts/no-manual-deps.sh (the DEP_DIFF assignment) to include at least one line
of context (e.g., remove -U0 or use -U1) so the /^[@@]/ and dependency section
matches in the awk script can be set correctly, then run quick tests by staging
a change that adds a dependency to an existing [dependencies] table to confirm
DEP_DIFF now captures the addition.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 31859275-6748-431e-9b06-d7ea443b6bc0

📥 Commits

Reviewing files that changed from the base of the PR and between c50e18d and 4f74013.

📒 Files selected for processing (24)
  • .github/workflows/ci.yml
  • .oxfmtrc.json
  • CHANGELOG.md
  • scripts/codegen-types.ts
  • scripts/no-manual-deps.sh
  • src-tauri/Cargo.toml
  • src-tauri/src/bin/specta-export.rs
  • src-tauri/src/domain/shared/events.rs
  • src-tauri/src/domain/shared/ids.rs
  • src-tauri/src/domain/tasks/model.rs
  • src/shared/types/AppError.ts
  • src/shared/types/AppEvent.ts
  • src/shared/types/Phase.ts
  • src/shared/types/PrId.ts
  • src/shared/types/Priority.ts
  • src/shared/types/ProfileId.ts
  • src/shared/types/RunId.ts
  • src/shared/types/Status.ts
  • src/shared/types/Task.ts
  • src/shared/types/TaskId.ts
  • src/shared/types/WorktreeId.ts
  • src/shared/types/__tests__/codegen.test.ts
  • src/shared/types/commands.ts
  • src/shared/types/index.ts

Comment thread .github/workflows/ci.yml
Comment thread CHANGELOG.md
Comment thread scripts/codegen-types.ts
Comment thread scripts/no-manual-deps.sh Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 24 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/codegen-types.ts">

<violation number="1" location="scripts/codegen-types.ts:45">
P2: Codegen does not remove obsolete generated files, so stale TypeScript bindings can remain and continue to be exported after Rust types are removed or renamed.</violation>
</file>

<file name=".github/workflows/ci.yml">

<violation number="1" location=".github/workflows/ci.yml:112">
P2: The drift gate misses untracked generated files; `git diff --exit-code` only checks tracked-file changes, so newly generated type files can bypass CI.</violation>

<violation number="2" location=".github/workflows/ci.yml:113">
P2: Backticks inside double-quoted `echo` strings trigger command substitution in bash, so `` `pnpm run codegen` `` will actually execute rather than print as literal text. Use single quotes to prevent this.</violation>
</file>

<file name="scripts/no-manual-deps.sh">

<violation number="1" location="scripts/no-manual-deps.sh:23">
P1: Using `-U0` with section-tracking awk causes normal dependency additions to be missed, so manual Cargo dependency edits can bypass the lockfile check.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread scripts/no-manual-deps.sh Outdated
Comment thread scripts/codegen-types.ts
Comment thread .github/workflows/ci.yml Outdated
Comment thread .github/workflows/ci.yml Outdated
mpiton added 2 commits May 10, 2026 08:27
CodeRabbit + Cubic-AI bot findings, all VALID:

- ci.yml drift gate: switched from `git diff --exit-code` to
  `git status --porcelain --untracked-files=all` so a brand-new
  `<NewType>.ts` (untracked, no tracked-diff) cannot bypass the gate.
  Single-quoted the `echo '::error::...'` lines so the literal
  backticks around `pnpm run codegen` print instead of triggering
  command substitution (shellcheck SC2006 / actionlint).
- scripts/codegen-types.ts: added `cleanGenerated()` that wipes
  every `*.ts` under `src/shared/types/` (test files live in
  `__tests__/`) before regen, so a renamed/removed Rust type cannot
  leave a stale binding behind. `REQUIRED` now lists every ID alias
  (TaskId, RunId, WorktreeId, ProfileId, PrId) so verify() catches
  missing IDs that Task / AppEvent depend on for compilation.
- scripts/no-manual-deps.sh: replaced `-U0` with `-U999999` so the
  full file context lands in the diff and the awk state machine
  observes the unchanged `[dependencies]` header before any new
  `+key = value` line. Without context lines, additions to existing
  dep tables would slip past the gate. Section-tracker regex relaxed
  from `^[+-]` to `^[ +-]` to match context lines too.
- CHANGELOG.md (T-109 entry): removed the stale "No ts-rs in domain"
  claim that contradicted T-117. Replaced with a note explaining the
  in-place ts-rs derive choice (derive-only crate, no runtime / I/O,
  preserves dependency-rule spirit without a parallel DTO hierarchy).

Verified: cargo test 165, cargo clippy -D warnings, tsc -b, oxlint
--max-warnings=0, vitest 14/14, codegen idempotent after wipe-and-regen.
Awk patch sanity-checked against synthetic dep addition under unchanged
`[dependencies]`: now flagged correctly.
… desktop binary

Adding the `[[bin]] specta-export` codegen helper in 4f74013 left the
Tauri package with two binaries (the implicit `src/main.rs` and the
specta-export bin), so tauri-cli's `cargo metadata` lookup failed with

  failed to find main binary, make sure you have a `package > default-run`
  in the Cargo.toml file

across all three OS matrix builds. Pinning `default-run = "forgent"` in
`[package]` tells cargo / tauri-cli which binary is the app entrypoint.

Verified: `cargo metadata` reports `default_run: forgent`,
`pnpm exec tauri build --debug` exits 0 locally with both bins present.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[T-117] scripts/codegen-types.ts (ts-rs + specta)

1 participant