Skip to content

TS emitter correctness + repo hygiene (10-item sweep) - #4

Merged
amafjarkasi merged 8 commits into
masterfrom
fix/ts-emitter-correctness
Jul 31, 2026
Merged

amafjarkasi merged 8 commits into
masterfrom
fix/ts-emitter-correctness

Conversation

@amafjarkasi

Copy link
Copy Markdown
Owner

Worked through the 10-item focus list from the prior review. Headline: 3 real TS emitter bugs fixed (found via new big-spec coverage), clippy at 0 warnings, version drift reconciled.

TS emitter correctness (#1–3)

  • Barrel gap — index.ts re-exported 12 of 18 runtime modules, omitting streaming, cache, concurrency, dedup, idempotency, middleware. import { streamSse } from "./index" failed. All 18 now re-exported.
  • Reserved-name collisions — ported the Go/Rust ValidationError-style guard to TS. A spec model named like a built-in (ApiClient, Logger, etc.) now gets a Model suffix. Verified: GitHub TS (which has a ValidationError model) now generates ValidationErrorModel.ts and typechecks.
  • Big-spec regression coverage — added github_generates_and_typechecks_ts + stripe_generates_and_typechecks_ts. This immediately caught a real bug: Stripe's DeletedExternalAccount and DeletedPaymentSource unions both contain DeletedBankAccount/DeletedCard arms, so both emitted top-level isDeletedBankAccount guards that collided under export * (TS2308). Fixed by making TS guards union-scoped (is{Union}{Arm}); Rust unaffected (its guards are enum methods). Updated 3 unit tests + docs.

Repo hygiene (#4–8)

  • Version drift — Cargo.toml was 0.8.0 while docs/tags were 1.6.x; this also corrupted every generated specforge-version.json stamp. Bumped to 1.6.1 everywhere.
  • generate-examples.sh — always rebuilds the CLI now (was silently regenerating with a stale binary).
  • Orphaned website/index.html — a 220-insertion landing-page revamp was sitting uncommitted; preserved on feat/landing-page-revamp-uncommitted (needs review before merge).
  • Clippy → 0 (was 120 at session start): documented #[allow] on the Model enum (boxing would touch 70+ sites for no win), replaced a needless clone with slice::from_ref.
  • docs-site changelog — was stuck at 1.2.0 (missing 1.3–1.6); synced to the canonical root CHANGELOG.md.

Go emitter (#9)

  • Escaped spec-derived text in the remaining inline // Deprecated: Use {alt} and // Returns {desc} lines via a new go_inline() helper (property/param/body/returns docs were already escaped).

#10 — Deno

  • Verified Deno is not a specforge target (the TS SDK is a dual ESM/CJS Node package; "deno" only appears in third-party fixture prose). No action — the tsc --noEmit gate from docs: Deep README accuracy review #3 is the right coverage.

Verification (all run, exit 0)

  • cargo build --workspace ✓
  • cargo clippy --workspace --all-targets → 0 warnings
  • lib tests: 259 pass (242 core + 17 TS)
  • regression: 13/13 pass (rust+go+ts × github+stripe+petstore)
  • examples: rust cargo check + go go build clean

root added 8 commits July 31, 2026 02:39
The generated index.ts re-exported 12 of 18 runtime modules, omitting
concurrency, dedup, idempotency, middleware, streaming, and cache. So
'import { streamSse } from "./index"' failed (the documented entry
point), forcing consumers to know internal module paths. All 18 are
always emitted, so the re-exports are always valid; verified the SDK
still typechecks (no duplicate-export conflicts).
Same bug class as the Rust/Go ValidationError collision: a spec model
named like a built-in SDK type (ApiClient, ResponseCache, ValidationError,
Logger, Middleware, ...) would redeclare it and break compilation with a
duplicate-export error.

Add safe_model_name() in name.rs, applied consistently at all three
model-name derivation sites (emit_object, emit_enum, and the file/barrel
paths in lib.rs) so the filename, export, and barrel re-export all agree.
Colliding names get a 'Model' suffix.

Verified: the GitHub TS SDK (which has a ValidationError model) now
generates ValidationErrorModel.ts and typechecks clean; petstore
unaffected (no colliding names).
#3: Added github_generates_and_typechecks_ts + stripe_generates_and_typechecks_ts
to regression.rs (generate → npm install → tsc --noEmit), mirroring the
existing go/rust gates. assert_tsc helper skips gracefully when node/npm
are absent.

This immediately caught a real bug: Stripe's DeletedExternalAccount and
DeletedPaymentSource unions both contain DeletedBankAccount/DeletedCard
arms, so both emitted top-level isDeletedBankAccount/isDeletedCard guards
that collided under the barrel's 'export *' (TS2308).

Fix: TS oneOf guards are now union-scoped (is{Union}{Arm}, e.g.
isPetEventPetCreated) instead of arm-only (isPetCreated). Rust is
unaffected (its guards are enum methods, inherently scoped). Updated the
3 affected unit tests + README/docs-site examples.
Cargo.toml was stuck at 0.8.0 while README/CHANGELOG/tags were at 1.6.x.
This also corrupted every generated specforge-version.json stamp (it
uses env!(CARGO_PKG_VERSION), inheriting the workspace version).

Bumped [workspace.package].version + the 5 internal-crate dependency
versions. Now Cargo.toml, cargo build, specforge --version, and the
generated version stamp all agree on 1.6.1.
It skipped 'cargo build' when the binary already existed, so it silently
regenerated the example SDKs with a STALE binary (this bit us: examples
missing emit_readme changes). Now always rebuilds; incremental build is
fast and guarantees examples reflect the current emitter.
- ir.rs: Model enum large_enum_variant — boxed ObjectModel would force
  deref at 70+ match sites for no real win (Model values aren't
  stack-hot); documented #[allow] with rationale instead
- merge.rs: replace spec.clone() with std::slice::from_ref(&spec) in the
  test (merge_specs takes &[Value] by reference, so the clone was
  unnecessary)

Clippy now: 0 warnings (was 120 at the start of this work).
The docs-site reference/changelog.md was stuck at 1.2.0 (missing 1.3.0,
1.4.0, 1.5.0, 1.6.0) and had drifted out of sync with the root
CHANGELOG.md. Regenerated it from the root (the single source of truth),
preserving the Docusaurus frontmatter. Now covers 0.1.0 -> 1.6.1.
The // Deprecated: Use {alt} and // Returns {desc} lines still inlined
spec text directly (alt from deprecation-alternative extraction, desc
from response descriptions). Both can contain embedded newlines or */
that break the generated Go. Added go_inline() (escape */ + collapse
newlines) and routed all 4 at-risk sites through it.

Property/param/body/returns/enum/object docs already used go_doc/
go_doc_label (fixed earlier); this closes the remaining inline gaps.
@gemini-code-assist

Copy link
Copy Markdown

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@github-actions

Copy link
Copy Markdown
specforge: OpenAPI spec diff (4 files)

Breaking changes detected. This PR modifies OpenAPI specs in ways that may break existing clients.

crates/specforge-cli/tests/regression.rs

Non-breaking changes:

�[2m2026-07-31T03:19:19.735810Z�[0m �[32m INFO�[0m reading old spec: /tmp/specforge-old-regression.rs
error: failed to parse old spec at /tmp/specforge-old-regression.rs: failed to parse spec as YAML: could not find expected ':' at line 3 column 1, while scanning a simple key at line 2 column 1: could not find expected ':' at line 3 column 1, while scanning a simple key at line 2 column 1
�[2m2026-07-31T03:19:19.736024Z�[0m �[33m WARN�[0m failed to parse old spec at /tmp/specforge-old-regression.rs: failed to parse spec as YAML: could not find expected ':' at line 3 column 1, while scanning a simple key at line 2 column 1: could not find expected ':' at line 3 column 1, while scanning a simple key at line 2 column 1

crates/specforge-core/src/merge.rs

Non-breaking changes:

�[2m2026-07-31T03:19:19.757028Z�[0m �[32m INFO�[0m reading old spec: /tmp/specforge-old-merge.rs
error: failed to parse old spec at /tmp/specforge-old-merge.rs: failed to parse spec as YAML: could not find expected ':' at line 3 column 1, while scanning a simple key at line 2 column 1: could not find expected ':' at line 3 column 1, while scanning a simple key at line 2 column 1
�[2m2026-07-31T03:19:19.757240Z�[0m �[33m WARN�[0m failed to parse old spec at /tmp/specforge-old-merge.rs: failed to parse spec as YAML: could not find expected ':' at line 3 column 1, while scanning a simple key at line 2 column 1: could not find expected ':' at line 3 column 1, while scanning a simple key at line 2 column 1

crates/specforge-ts/src/name.rs

Non-breaking changes:

�[2m2026-07-31T03:19:19.780137Z�[0m �[32m INFO�[0m reading old spec: /tmp/specforge-old-name.rs
error: failed to parse old spec at /tmp/specforge-old-name.rs: failed to parse spec as YAML: mapping values are not allowed in this context at line 10 column 10: mapping values are not allowed in this context at line 10 column 10
�[2m2026-07-31T03:19:19.780317Z�[0m �[33m WARN�[0m failed to parse old spec at /tmp/specforge-old-name.rs: failed to parse spec as YAML: mapping values are not allowed in this context at line 10 column 10: mapping values are not allowed in this context at line 10 column 10

docs-site/docs/intro.md

Non-breaking changes:

�[2m2026-07-31T03:19:19.803747Z�[0m �[32m INFO�[0m reading old spec: /tmp/specforge-old-intro.md
error: failed to parse old spec at /tmp/specforge-old-intro.md: failed to parse spec as YAML: deserializing from YAML containing more than one document is not supported: deserializing from YAML containing more than one document is not supported
�[2m2026-07-31T03:19:19.803957Z�[0m �[33m WARN�[0m failed to parse old spec at /tmp/specforge-old-intro.md: failed to parse spec as YAML: deserializing from YAML containing more than one document is not supported: deserializing from YAML containing more than one document is not supported

Powered by specforge

@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

Fix TS emitter collisions, expand TS regression gates, and align versions to 1.6.1

🐞 Bug fix 🧪 Tests 📝 Documentation ⚙️ Configuration changes 🕐 40+ Minutes

Grey Divider

AI Description

• Fix TypeScript emitter export/name collisions and make oneOf guards union-scoped.
• Add GitHub/Stripe TS generate+typecheck regression gates (skips if Node/npm missing).
• Reconcile repo versioning to 1.6.1 and sync docs/examples/scripts accordingly.
Diagram

graph TD
  SPEC["OpenAPI spec (fixture)"] --> CORE["specforge-core (resolve)"] --> TS["specforge-ts (emit)"] --> SDK["Generated TS SDK"] --> TSC(["tsc --noEmit gate"])
  CORE --> GO["specforge-go (emit)"]
  CLI["specforge-cli regression"] --> TS
  CLI --> TSC
  DOCS["README + docs-site"] --> TS
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Avoid re-exporting per-arm guards from model modules
  • ➕ Eliminates TS2308 collision class without renaming guards
  • ➕ Keeps existing guard names stable
  • ➖ Reduces discoverability/ergonomics (users must know internal paths)
  • ➖ Still risks collisions when consumers barrel-export everything themselves
2. Emit guards under a per-union namespace/object (e.g., PetEvent.isPetCreated)
  • ➕ Namespacing prevents collisions while keeping short arm names
  • ➕ Clear grouping of union-specific helpers
  • ➖ API change (call-site shape changes)
  • ➖ More codegen complexity and potentially less tree-shaking friendliness
3. Rename built-in SDK types instead of models (prefix internal types with Sdk*)
  • ➕ Avoids having to maintain a reserved-name list
  • ➕ User model names remain unmodified
  • ➖ Large breaking change across the runtime/public API
  • ➖ Higher churn and migration cost for existing SDK consumers

Recommendation: Keep the PR’s approach: union-scoped guard names (is{Union}{Arm}) and model-name hardening via safe_model_name() are low-risk, local changes that preserve the overall SDK structure and prevent real-world collisions surfaced by the new GitHub/Stripe TS gates. The reserved-name list is a maintenance cost, but it’s far cheaper than renaming the runtime API or weakening exports.

Files changed (17) +436 / -277

Enhancement (1) +61 / -0
name.rsAdd reserved TS SDK type list and safe_model_name() +61/-0

Add reserved TS SDK type list and safe_model_name()

• Introduces 'is_ts_sdk_builtin_type()' and 'safe_model_name()' to suffix colliding model names with 'Model', preventing duplicate-export/redeclaration errors when specs define models like 'ValidationError' or 'ApiClient'.

crates/specforge-ts/src/name.rs

Bug fix (4) +39 / -15
lib.rsEscape spec-derived text for inline Go // comments +11/-4

Escape spec-derived text for inline Go // comments

• Adds 'go_inline()' to sanitize single-line comment inlines (escapes '*/' and collapses newlines) and applies it to remaining inline 'Deprecated' and 'Returns' comment sites.

crates/specforge-go/src/lib.rs

lib.rsHarden model naming and fix missing runtime barrel re-exports +8/-2

Harden model naming and fix missing runtime barrel re-exports

• Applies 'safe_model_name()' when choosing model filenames and barrel exports so built-in name collisions don’t redeclare runtime types. Expands 'src/index.ts' generation to re-export all runtime modules (concurrency/dedup/idempotency/middleware/streaming/cache).

crates/specforge-ts/src/lib.rs

models.rsMake oneOf guard names union-scoped and apply safe model naming +14/-9

Make oneOf guard names union-scoped and apply safe model naming

• Updates object/enum emission to use 'safe_model_name()' for filenames/types. Renames emitted oneOf guard functions to 'is{Union}{Arm}' and updates 'narrow{Union}' accordingly; adjusts unit tests to match the new names.

crates/specforge-ts/src/models.rs

index.tsRe-export all TS runtime modules from example barrel +6/-0

Re-export all TS runtime modules from example barrel

• Updates the generated example 'src/index.ts' to export additional runtime modules (concurrency/dedup/idempotency/middleware/streaming/cache), matching the fixed emitter behavior.

examples/petstore-ts/sdk/src/index.ts

Refactor (2) +7 / -1
ir.rsSuppress clippy large enum variant warning with rationale +6/-0

Suppress clippy large enum variant warning with rationale

• Adds a documented '#[allow(clippy::large_enum_variant)]' to the 'Model' enum to avoid widespread boxing/deref churn across many match sites for minimal benefit.

crates/specforge-core/src/ir.rs

merge.rsReplace single-item slice construction to avoid clone +1/-1

Replace single-item slice construction to avoid clone

• Uses 'std::slice::from_ref' in a test to avoid cloning the spec into a temporary vec/slice, addressing a clippy concern.

crates/specforge-core/src/merge.rs

Tests (1) +107 / -0
regression.rsAdd TS generate+typecheck regression gates for GitHub/Stripe +107/-0

Add TS generate+typecheck regression gates for GitHub/Stripe

• Introduces TS regression tests that generate a TS SDK into a temp directory and run 'npm install' + 'tsc --noEmit' via the generated 'typecheck' script. Skips gracefully when Node/npm are unavailable; otherwise fails on TS type errors.

crates/specforge-cli/tests/regression.rs

Documentation (3) +202 / -241
README.mdDocument union-scoped TS oneOf guard names +5/-3

Document union-scoped TS oneOf guard names

• Updates examples and feature table to reflect the new TS guard naming convention ('is{Union}{Arm}') and explains why guards are union-scoped to avoid collisions.

README.md

intro.mdSync TS oneOf guard naming in docs-site intro +3/-3

Sync TS oneOf guard naming in docs-site intro

• Updates the oneOf guard example and feature table entry to use union-scoped guard names consistent with the TS emitter changes.

docs-site/docs/intro.md

changelog.mdSync docs-site changelog with canonical history and links +194/-235

Sync docs-site changelog with canonical history and links

• Refreshes the docs-site changelog content to include later releases (through 1.6.1), improves formatting, and replaces the old bottom link block with a consolidated 'Version Links' section.

docs-site/docs/reference/changelog.md

Other (6) +20 / -20
Cargo.lockBump workspace crate versions to 1.6.1 in lockfile +7/-7

Bump workspace crate versions to 1.6.1 in lockfile

• Updates locked versions for specforge workspace crates from 0.8.0 to 1.6.1 to match the reconciled release versioning.

Cargo.lock

Cargo.tomlAlign workspace/package and internal dependency versions to 1.6.1 +6/-6

Align workspace/package and internal dependency versions to 1.6.1

• Updates workspace package version and internal crate dependency versions from 0.8.0 to 1.6.1 to eliminate version drift across crates.

Cargo.toml

specforge-version.jsonUpdate generated example version stamp to 1.6.1 +1/-1

Update generated example version stamp to 1.6.1

• Regenerates the example metadata stamp to reflect specforge 1.6.1 and updates the generation timestamp.

examples/petstore-go/sdk/specforge-version.json

specforge-version.jsonUpdate generated example version stamp to 1.6.1 +1/-1

Update generated example version stamp to 1.6.1

• Regenerates the example metadata stamp to reflect specforge 1.6.1 and updates the generation timestamp.

examples/petstore-rust/sdk/specforge-version.json

specforge-version.jsonUpdate generated example version stamp to 1.6.1 +1/-1

Update generated example version stamp to 1.6.1

• Regenerates the example metadata stamp to reflect specforge 1.6.1 and updates the generation timestamp.

examples/petstore-ts/sdk/specforge-version.json

generate-examples.shAlways rebuild CLI before regenerating examples +4/-4

Always rebuild CLI before regenerating examples

• Removes the conditional build and always runs 'cargo build -p specforge-cli' to prevent silently generating examples with a stale binary.

scripts/generate-examples.sh

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. TS rename breaks references 🐞 Bug ≡ Correctness
Description
TypeScript model/enum names are now rewritten via safe_model_name() (e.g.
ValidationError→ValidationErrorModel), but TypeScript reference rendering and import generation
still use plain pascal(), so the generated SDK can import non-existent files/types and fail tsc
for any spec that contains one of these reserved names.
Code

crates/specforge-ts/src/models.rs[R51-52]

+    let name = crate::name::safe_model_name(&pascal(&o.name));
    let mut refs = BTreeSet::new();
Evidence
The PR changes model/enum declaration names to safe_model_name(...), but multiple other code paths
still convert schema references with pascal(...) when rendering TypeScript types and when
generating import type {X} from "../models/X" lines, which will not match the renamed
filenames/exports for reserved schemas.

/crates/specforge-ts/src/name.rs[39-98]
/crates/specforge-ts/src/models.rs[49-111]
/crates/specforge-ts/src/models.rs[352-390]
/crates/specforge-ts/src/types.rs[55-63]
/crates/specforge-ts/src/operations.rs[82-96]
/crates/specforge-ts/src/runtime.rs[765-798]
/crates/specforge-ts/src/runtime.rs[1158-1164]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The TS emitter now renames some schema model identifiers (and filenames/exports) using `safe_model_name()`, but other emitter components still assume the original `pascal()` name for schema references. This creates inconsistencies such as:
- file emitted as `ValidationErrorModel.ts` exporting `ValidationErrorModel`
- other generated files importing `../models/ValidationError` and using type `ValidationError`

This causes TypeScript compilation failures and/or wrong type bindings whenever a spec contains a schema whose PascalCase name matches a TS runtime builtin (e.g. `ValidationError`, `ApiClient`, `Logger`, etc.).

## Issue Context
`safe_model_name()` is intended to avoid collisions with hardcoded TS runtime exports, but it must be the single source of truth for *all* schema-reference naming (type rendering, imports, validator naming, schema descriptor refs, union guard arm names).

## Fix Focus Areas
- crates/specforge-ts/src/types.rs[55-63]
- crates/specforge-ts/src/models.rs[49-111]
- crates/specforge-ts/src/models.rs[115-177]
- crates/specforge-ts/src/models.rs[352-390]
- crates/specforge-ts/src/operations.rs[82-96]
- crates/specforge-ts/src/operations.rs[401-418]
- crates/specforge-ts/src/runtime.rs[765-812]
- crates/specforge-ts/src/runtime.rs[1158-1164]

### Implementation notes
- Introduce a helper like `safe_schema_type_name(name: &str) -> String` that does `safe_model_name(&pascal(name))`, and use it for every `Type::Reference` rendering/import/function naming.
- Update union guard generation so both `union_name` and `arm_ty` use safe names.
- Update model import generation (`build_imports`) and API imports (`operations.rs`) to import from the safe filename stem.
- Ensure `validate{Model}()` and `{Model}Schema` names in `validate.ts` are also based on the safe name so runtime validation wiring continues to compile.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. TS gate needs network 🐞 Bug ☼ Reliability
Description
The new TS regression gate always runs npm install in a fresh temp directory, which introduces a
network/package-registry dependency and can make CI slow or flaky even when the generator output is
correct.
Code

crates/specforge-cli/tests/regression.rs[R513-525]

+    // Install devDependencies (typescript) quietly.
+    let install = std::process::Command::new("npm")
+        .args(["install", "--no-audit", "--no-fund", "--loglevel=error"])
+        .current_dir(dir)
+        .output()
+        .map_err(|e| format!("spawn npm install: {e}"))?;
+    if !install.status.success() {
+        return Err(format!(
+            "npm install failed (exit {:?}):\n{}",
+            install.status.code(),
+            String::from_utf8_lossy(&install.stderr)
+        ));
+    }
Evidence
assert_tsc() installs dependencies via npm install for every run, and only skips when node/npm
are missing—not when npm can’t reach the registry—so CI can fail for reasons unrelated to codegen
correctness.

/crates/specforge-cli/tests/regression.rs[497-525]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`assert_tsc()` unconditionally executes `npm install` in the generated temp package directory. This may require registry access and can fail due to transient network/registry issues, turning the regression suite into a flaky, externally-dependent test.

## Issue Context
This is a regression test, so failures should ideally reflect generator correctness rather than external package-manager availability.

## Fix Focus Areas
- crates/specforge-cli/tests/regression.rs[497-525]

### Possible approaches (pick one)
- Add `--prefer-offline`/`--no-progress` and configure an explicit npm cache directory to reduce network reliance.
- Make the TS gate opt-in via env var (e.g. `SPECFORGE_ENABLE_TSC_GATE=1`) so default CI remains hermetic.
- Vendor a minimal toolchain approach (e.g., run `tsc` from a repo-managed dependency/tooling) so the test doesn’t need to install from the network each time.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Informational

3. TSC stderr not reported 🐞 Bug ◔ Observability
Description
On TypeScript typecheck failure, assert_tsc() only returns tsc.stdout and drops tsc.stderr,
which can hide important diagnostics and makes failures harder to debug.
Code

crates/specforge-cli/tests/regression.rs[R537-541]

+        Err(format!(
+            "tsc --noEmit failed (exit {:?}):\n{}",
+            tsc.status.code(),
+            String::from_utf8_lossy(&tsc.stdout),
+        ))
Evidence
The failure branch formats the error with only String::from_utf8_lossy(&tsc.stdout) and omits
tsc.stderr.

/crates/specforge-cli/tests/regression.rs[527-542]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
When `npm run typecheck` fails, the error string only includes stdout. If stderr contains useful output (npm wrapper errors, node runtime errors, or diagnostics), it will be lost.

## Issue Context
Other compile gates in this file include stderr in failure messages (e.g., cargo check); TS should match for debuggability.

## Fix Focus Areas
- crates/specforge-cli/tests/regression.rs[527-542]

### Suggested change
Include both stdout and stderr in the returned error message, similar to `assert_cargo_check()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

To customize comments, go to the Qodo configuration screen, or learn more in the docs.

Qodo Logo

Comment on lines +51 to 52
let name = crate::name::safe_model_name(&pascal(&o.name));
let mut refs = BTreeSet::new();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Ts rename breaks references 🐞 Bug ≡ Correctness

TypeScript model/enum names are now rewritten via safe_model_name() (e.g.
ValidationError→ValidationErrorModel), but TypeScript reference rendering and import generation
still use plain pascal(), so the generated SDK can import non-existent files/types and fail tsc
for any spec that contains one of these reserved names.
Agent Prompt
## Issue description
The TS emitter now renames some schema model identifiers (and filenames/exports) using `safe_model_name()`, but other emitter components still assume the original `pascal()` name for schema references. This creates inconsistencies such as:
- file emitted as `ValidationErrorModel.ts` exporting `ValidationErrorModel`
- other generated files importing `../models/ValidationError` and using type `ValidationError`

This causes TypeScript compilation failures and/or wrong type bindings whenever a spec contains a schema whose PascalCase name matches a TS runtime builtin (e.g. `ValidationError`, `ApiClient`, `Logger`, etc.).

## Issue Context
`safe_model_name()` is intended to avoid collisions with hardcoded TS runtime exports, but it must be the single source of truth for *all* schema-reference naming (type rendering, imports, validator naming, schema descriptor refs, union guard arm names).

## Fix Focus Areas
- crates/specforge-ts/src/types.rs[55-63]
- crates/specforge-ts/src/models.rs[49-111]
- crates/specforge-ts/src/models.rs[115-177]
- crates/specforge-ts/src/models.rs[352-390]
- crates/specforge-ts/src/operations.rs[82-96]
- crates/specforge-ts/src/operations.rs[401-418]
- crates/specforge-ts/src/runtime.rs[765-812]
- crates/specforge-ts/src/runtime.rs[1158-1164]

### Implementation notes
- Introduce a helper like `safe_schema_type_name(name: &str) -> String` that does `safe_model_name(&pascal(name))`, and use it for every `Type::Reference` rendering/import/function naming.
- Update union guard generation so both `union_name` and `arm_ty` use safe names.
- Update model import generation (`build_imports`) and API imports (`operations.rs`) to import from the safe filename stem.
- Ensure `validate{Model}()` and `{Model}Schema` names in `validate.ts` are also based on the safe name so runtime validation wiring continues to compile.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +513 to +525
// Install devDependencies (typescript) quietly.
let install = std::process::Command::new("npm")
.args(["install", "--no-audit", "--no-fund", "--loglevel=error"])
.current_dir(dir)
.output()
.map_err(|e| format!("spawn npm install: {e}"))?;
if !install.status.success() {
return Err(format!(
"npm install failed (exit {:?}):\n{}",
install.status.code(),
String::from_utf8_lossy(&install.stderr)
));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Ts gate needs network 🐞 Bug ☼ Reliability

The new TS regression gate always runs npm install in a fresh temp directory, which introduces a
network/package-registry dependency and can make CI slow or flaky even when the generator output is
correct.
Agent Prompt
## Issue description
`assert_tsc()` unconditionally executes `npm install` in the generated temp package directory. This may require registry access and can fail due to transient network/registry issues, turning the regression suite into a flaky, externally-dependent test.

## Issue Context
This is a regression test, so failures should ideally reflect generator correctness rather than external package-manager availability.

## Fix Focus Areas
- crates/specforge-cli/tests/regression.rs[497-525]

### Possible approaches (pick one)
- Add `--prefer-offline`/`--no-progress` and configure an explicit npm cache directory to reduce network reliance.
- Make the TS gate opt-in via env var (e.g. `SPECFORGE_ENABLE_TSC_GATE=1`) so default CI remains hermetic.
- Vendor a minimal toolchain approach (e.g., run `tsc` from a repo-managed dependency/tooling) so the test doesn’t need to install from the network each time.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +537 to +541
Err(format!(
"tsc --noEmit failed (exit {:?}):\n{}",
tsc.status.code(),
String::from_utf8_lossy(&tsc.stdout),
))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Informational

3. Tsc stderr not reported 🐞 Bug ◔ Observability

On TypeScript typecheck failure, assert_tsc() only returns tsc.stdout and drops tsc.stderr,
which can hide important diagnostics and makes failures harder to debug.
Agent Prompt
## Issue description
When `npm run typecheck` fails, the error string only includes stdout. If stderr contains useful output (npm wrapper errors, node runtime errors, or diagnostics), it will be lost.

## Issue Context
Other compile gates in this file include stderr in failure messages (e.g., cargo check); TS should match for debuggability.

## Fix Focus Areas
- crates/specforge-cli/tests/regression.rs[527-542]

### Suggested change
Include both stdout and stderr in the returned error message, similar to `assert_cargo_check()`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@amafjarkasi
amafjarkasi merged commit 739d405 into master Jul 31, 2026
3 of 5 checks passed
@amafjarkasi
amafjarkasi deleted the fix/ts-emitter-correctness branch July 31, 2026 03:30
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.

1 participant