From 72404b497a38d4fecf7e4e90301c5c1569c32b59 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 31 Jul 2026 02:39:32 +0000 Subject: [PATCH 1/8] ts: Re-export all runtime modules from the SDK barrel (index.ts) 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). --- crates/specforge-ts/src/lib.rs | 6 ++++++ examples/petstore-ts/sdk/specforge-version.json | 2 +- examples/petstore-ts/sdk/src/index.ts | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/specforge-ts/src/lib.rs b/crates/specforge-ts/src/lib.rs index c54179d..b4eb774 100644 --- a/crates/specforge-ts/src/lib.rs +++ b/crates/specforge-ts/src/lib.rs @@ -277,6 +277,12 @@ fn collect_index(doc: &Document, out_dir: &Path, has_i18n: bool) -> std::io::Res body.push_str("export * from \"./auth\";\n"); body.push_str("export * from \"./retry\";\n"); body.push_str("export * from \"./paginate\";\n"); + body.push_str("export * from \"./concurrency\";\n"); + body.push_str("export * from \"./dedup\";\n"); + body.push_str("export * from \"./idempotency\";\n"); + body.push_str("export * from \"./middleware\";\n"); + body.push_str("export * from \"./streaming\";\n"); + body.push_str("export * from \"./cache\";\n"); body.push_str("export * from \"./validate\";\n"); body.push_str("export * from \"./validation-middleware\";\n"); body.push_str("export * from \"./ratelimit\";\n"); diff --git a/examples/petstore-ts/sdk/specforge-version.json b/examples/petstore-ts/sdk/specforge-version.json index 41fbf7b..6ea0c90 100644 --- a/examples/petstore-ts/sdk/specforge-version.json +++ b/examples/petstore-ts/sdk/specforge-version.json @@ -1 +1 @@ -{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T01:10:31Z"} \ No newline at end of file +{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:39:14Z"} \ No newline at end of file diff --git a/examples/petstore-ts/sdk/src/index.ts b/examples/petstore-ts/sdk/src/index.ts index 34e3177..8e35cdb 100644 --- a/examples/petstore-ts/sdk/src/index.ts +++ b/examples/petstore-ts/sdk/src/index.ts @@ -21,6 +21,12 @@ export * from "./errors"; export * from "./auth"; export * from "./retry"; export * from "./paginate"; +export * from "./concurrency"; +export * from "./dedup"; +export * from "./idempotency"; +export * from "./middleware"; +export * from "./streaming"; +export * from "./cache"; export * from "./validate"; export * from "./validation-middleware"; export * from "./ratelimit"; From bb2a8c8363ee43f2e88bf7176f37c09fd2d4556a Mon Sep 17 00:00:00 2001 From: root Date: Fri, 31 Jul 2026 02:42:22 +0000 Subject: [PATCH 2/8] ts: Reserve built-in SDK type names to avoid model collisions 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). --- crates/specforge-ts/src/lib.rs | 4 +- crates/specforge-ts/src/models.rs | 4 +- crates/specforge-ts/src/name.rs | 61 +++++++++++++++++++ .../petstore-ts/sdk/specforge-version.json | 2 +- 4 files changed, 66 insertions(+), 5 deletions(-) diff --git a/crates/specforge-ts/src/lib.rs b/crates/specforge-ts/src/lib.rs index b4eb774..692e06d 100644 --- a/crates/specforge-ts/src/lib.rs +++ b/crates/specforge-ts/src/lib.rs @@ -65,7 +65,7 @@ pub fn generate(doc: &Document, opts: &GeneratorOptions) -> std::io::Result std::io::Res // Models. for (_, model) in doc.schemas.iter() { - let name = name::pascal(model.name()); + let name = name::safe_model_name(&name::pascal(model.name())); body.push_str(&format!("export * from \"./models/{name}\";\n")); } // Webhooks. diff --git a/crates/specforge-ts/src/models.rs b/crates/specforge-ts/src/models.rs index 0e7363b..d0cf969 100644 --- a/crates/specforge-ts/src/models.rs +++ b/crates/specforge-ts/src/models.rs @@ -48,7 +48,7 @@ pub fn emit_model_file_with_registry(model: &Model, registry: Option<&SchemaRegi /// (filename_stem, body, referenced-model-names) fn emit_object(o: &ObjectModel, registry: Option<&SchemaRegistry>) -> (String, String, BTreeSet) { - let name = pascal(&o.name); + let name = crate::name::safe_model_name(&pascal(&o.name)); let mut refs = BTreeSet::new(); let mut body = String::new(); @@ -306,7 +306,7 @@ fn escape_str(s: &str) -> String { } fn emit_enum(e: &EnumModel) -> (String, String, BTreeSet) { - let name = pascal(&e.name); + let name = crate::name::safe_model_name(&pascal(&e.name)); let mut body = String::new(); if let Some(desc) = &e.description { body.push_str(&format_doc_comment(desc, "")); diff --git a/crates/specforge-ts/src/name.rs b/crates/specforge-ts/src/name.rs index 71f9c82..a283433 100644 --- a/crates/specforge-ts/src/name.rs +++ b/crates/specforge-ts/src/name.rs @@ -36,6 +36,67 @@ pub fn pascal(input: &str) -> String { out } +/// Whether `name` matches a type the TypeScript SDK emitter hardcodes into the +/// generated package (ApiClient, ResponseCache, ValidationError, etc.). User +/// models with these names are suffixed with `Model` to avoid duplicate-export +/// / redeclaration errors. Only applied to generated model/enum names, not to +/// all pascal-case identifiers. +pub fn is_ts_sdk_builtin_type(name: &str) -> bool { + matches!( + name, + "ApiClient" + | "ApiClientOptions" + | "ApiError" + | "AuthProvider" + | "AnonymousAuthProvider" + | "BearerAuthProvider" + | "ApiKeyAuthProvider" + | "CacheEntry" + | "ConfigurationError" + | "ConsoleLogger" + | "CursorPage" + | "EndpointSchema" + | "HttpError" + | "Logger" + | "Metrics" + | "MetricsCollector" + | "Middleware" + | "MiddlewareRequest" + | "MiddlewareResponse" + | "NetworkError" + | "OffsetPage" + | "RateLimiter" + | "RequestInterceptor" + | "RequestDeduper" + | "RequestOptions" + | "ResponseCache" + | "ResponseInterceptor" + | "ResponseTransformer" + | "RetryOptions" + | "RouteSchemaMap" + | "SchemaType" + | "Semaphore" + | "ServerSentEvent" + | "ServiceContainer" + | "SlidingWindow" + | "TelemetryHooks" + | "TimeoutError" + | "TokenBucket" + | "ValidationError" + | "QueryRecord" + ) +} + +/// Wrap a generated model/enum name so it can't collide with a built-in SDK +/// type. Returns `name` unchanged when safe, or `name + "Model"` when it would. +pub fn safe_model_name(name: &str) -> String { + if is_ts_sdk_builtin_type(name) { + format!("{name}Model") + } else { + name.to_string() + } +} + /// Sanitize a name into a valid TS *value* identifier (camelCase). pub fn camel(input: &str) -> String { let pascal = pascal(input); diff --git a/examples/petstore-ts/sdk/specforge-version.json b/examples/petstore-ts/sdk/specforge-version.json index 6ea0c90..34f83ea 100644 --- a/examples/petstore-ts/sdk/specforge-version.json +++ b/examples/petstore-ts/sdk/specforge-version.json @@ -1 +1 @@ -{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:39:14Z"} \ No newline at end of file +{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:41:58Z"} \ No newline at end of file From 14f7e331006b50840004828d85ec9838b8c4a8a0 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 31 Jul 2026 02:49:39 +0000 Subject: [PATCH 3/8] ts: Add big-spec regression coverage; fix oneOf guard name collisions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #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. --- README.md | 8 +- crates/specforge-cli/tests/regression.rs | 107 ++++++++++++++++++ crates/specforge-ts/src/models.rs | 19 ++-- docs-site/docs/intro.md | 6 +- .../petstore-ts/sdk/specforge-version.json | 2 +- 5 files changed, 128 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 216fda9..3f1eb8e 100644 --- a/README.md +++ b/README.md @@ -138,7 +138,7 @@ Every generated SDK is a **complete, production-ready client** — not just type | **Idempotency keys** | Auto-generated `Idempotency-Key` on unsafe methods | Safe retries on POST/PUT/PATCH/DELETE without duplicate side effects | | **SSE streaming** | Server-Sent Event parser with proper error handling | Real-time data streams without manual parsing | | **Runtime validation** | Validate request/response bodies against the spec | Catch API contract violations in dev and tests, not production | -| **oneOf type guards** | `isPetCreated()`, `narrowPetEvent()` (TS); `is_pet_created()`, `discriminant()` (Rust) | Safely narrow union types at runtime | +| **oneOf type guards** | `isPetEventPetCreated()`, `narrowPetEvent()` (TS); `is_pet_created()`, `discriminant()` (Rust) | Safely narrow union types at runtime (TS guards are union-scoped to avoid collisions when two unions share an arm) | | **Response caching** | ETag-based caching with TTL expiry and 304 handling | Avoid re-fetching unchanged data | | **Rate limiting** | Token bucket and sliding window rate limiters | Prevent overwhelming APIs | | **Logging** | Pluggable `Logger` interface (ConsoleLogger, NoopLogger) | Structured request/response logging | @@ -787,10 +787,10 @@ Streaming calls **do not retry** (bodies are not replayable). ### oneOf narrowing (TypeScript) ```ts -import { isPetCreated, narrowPetEvent, type PetEvent } from "./models/PetEvent"; +import { isPetEventPetCreated, narrowPetEvent, type PetEvent } from "./models/PetEvent"; function handle(event: PetEvent) { - if (isPetCreated(event)) { + if (isPetEventPetCreated(event)) { console.log(event.pet.name); // narrowed return; } @@ -801,6 +801,8 @@ function handle(event: PetEvent) { } ``` +> TS guard names are union-scoped (`is{Union}{Arm}`) so two unions sharing an arm type don't collide. `narrow{Union}()` is the ergonomic entry point. + --- ## Examples diff --git a/crates/specforge-cli/tests/regression.rs b/crates/specforge-cli/tests/regression.rs index 015e14d..d9f6042 100644 --- a/crates/specforge-cli/tests/regression.rs +++ b/crates/specforge-cli/tests/regression.rs @@ -278,6 +278,16 @@ fn stripe_generates_and_compiles_rust() { generate_and_compile_rust(&FIXTURES[2], "stripe_sdk"); } +#[test] +fn github_generates_and_typechecks_ts() { + generate_and_compile_ts(&FIXTURES[1], "@test/github-ts-sdk"); +} + +#[test] +fn stripe_generates_and_typechecks_ts() { + generate_and_compile_ts(&FIXTURES[2], "@test/stripe-ts-sdk"); +} + fn generate_and_compile_go(f: &Fixture, module: &str) { let path = match spec_path(f) { Ok(p) => p, @@ -373,6 +383,55 @@ fn generate_and_compile_rust(f: &Fixture, crate_name: &str) { } } +fn generate_and_compile_ts(f: &Fixture, package_name: &str) { + let path = match spec_path(f) { + Ok(p) => p, + Err(e) => { + eprintln!("skip {} ts gate: {e}", f.name); + return; + } + }; + let spec = match specforge_core::parse_file(&path) { + Ok(s) => s, + Err(e) => panic!("{} ts: parse failed: {e}", f.name), + }; + let doc = match specforge_core::resolve(&spec) { + Ok(d) => d, + Err(e) => panic!("{} ts: resolve failed: {e}", f.name), + }; + + let out = std::env::temp_dir().join(format!("specforge-gate-{}-ts", f.name)); + let _ = std::fs::remove_dir_all(&out); + let opts = specforge_ts::GeneratorOptions { + out_dir: out.clone(), + package_name: Some(package_name.into()), + i18n: None, + }; + let written = specforge_ts::generate(&doc, &opts) + .unwrap_or_else(|e| panic!("{} ts: emit failed: {e}", f.name)); + assert!( + written.iter().any(|p| p == "package.json"), + "{} ts: missing package.json", + f.name + ); + assert!( + written.len() >= 5, + "{} ts: expected several files, got {}", + f.name, + written.len() + ); + + match assert_tsc(&out) { + Ok(()) => eprintln!( + "{} ts: generate + tsc --noEmit OK ({} files)", + f.name, + written.len() + ), + Err(e) if e.contains("tsc not available") => eprintln!("skip {} ts check: {e}", f.name), + Err(e) => panic!("{} ts typecheck failed: {e}", f.name), + } +} + /// Run `go build ./...` in `dir`. Returns Err("go not found") when the toolchain /// is missing so callers can skip; any other failure is a real compile error. fn assert_go_build(dir: &Path) -> Result<(), String> { @@ -435,6 +494,54 @@ fn assert_cargo_check(dir: &Path) -> Result<(), String> { } } +/// Run `npm install` then `tsc --noEmit` in `dir`. Returns Err("tsc not +/// available") when node/npm are missing so callers can skip; any other failure +/// is a real type error. +fn assert_tsc(dir: &Path) -> Result<(), String> { + // Resolve node/npm — skip gracefully if absent (TS gate is optional). + for tool in ["node", "npm"] { + let ok = std::process::Command::new(tool) + .arg("--version") + .output() + .map(|o| o.status.success()) + .unwrap_or(false); + if !ok { + return Err(format!("{tool} not found — tsc not available")); + } + } + + // 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) + )); + } + + // Typecheck. The generated package.json has a `typecheck` script + // (`tsc --noEmit`); invoke it directly so we exercise the shipped config. + let tsc = std::process::Command::new("npm") + .args(["run", "--silent", "typecheck"]) + .current_dir(dir) + .output() + .map_err(|e| format!("spawn tsc: {e}"))?; + if tsc.status.success() { + Ok(()) + } else { + Err(format!( + "tsc --noEmit failed (exit {:?}):\n{}", + tsc.status.code(), + String::from_utf8_lossy(&tsc.stdout), + )) + } +} + /// Helper: collect the relative file paths produced by a generate run. fn generated_files(dir: &Path) -> BTreeSet { fn walk(dir: &Path, base: &Path, out: &mut BTreeSet) { diff --git a/crates/specforge-ts/src/models.rs b/crates/specforge-ts/src/models.rs index d0cf969..91bd74e 100644 --- a/crates/specforge-ts/src/models.rs +++ b/crates/specforge-ts/src/models.rs @@ -142,7 +142,12 @@ fn emit_union_guards( for arm in &arms { let arm_ty = pascal(arm); - let guard_name = format!("is{arm_ty}"); + // Guard names are union-scoped (`is{Union}{Arm}`) because top-level + // functions named purely after the arm type collide when two unions + // share an arm (e.g. Stripe's DeletedExternalAccount and + // DeletedPaymentSource both contain DeletedBankAccount). The + // `narrow{Union}` helper below is the ergonomic entry point. + let guard_name = format!("is{union_name}{arm_ty}"); let check = arm_type_check(arm, disc_prop, registry, comp.discriminator.as_ref()); out.push_str(&format!( "export function {guard_name}(value: {union_name}): value is {arm_ty} {{\n" @@ -166,7 +171,7 @@ fn emit_union_guards( for arm in &arms { let arm_ty = pascal(arm); out.push_str(&format!( - " if (is{arm_ty}(value)) return \"{arm_ty}\";\n" + " if (is{union_name}{arm_ty}(value)) return \"{arm_ty}\";\n" )); } out.push_str(" return undefined;\n"); @@ -475,7 +480,7 @@ mod tests { assert!(file.contains("import { PetCreated } from \"./PetCreated\";")); assert!(file.contains("import { PetUpdated } from \"./PetUpdated\";")); // Guards are emitted even without a registry (structural fallback). - assert!(file.contains("export function isPetCreated(value: PetEvent): value is PetCreated")); + assert!(file.contains("export function isPetEventPetCreated(value: PetEvent): value is PetCreated")); assert!(file.contains("export function narrowPetEvent(value: PetEvent)")); } @@ -552,8 +557,8 @@ mod tests { let file = emit_model_file_with_registry(&Model::Object(o), Some(®istry)); assert!(file.contains("=== \"pet.created\"")); assert!(file.contains("=== \"pet.updated\"")); - assert!(file.contains("export function isPetCreated")); - assert!(file.contains("export function isPetUpdated")); + assert!(file.contains("export function isPetEventPetCreated")); + assert!(file.contains("export function isPetEventPetUpdated")); assert!(file.contains("export function narrowPetEvent")); } @@ -631,8 +636,8 @@ mod tests { let file = emit_model_file_with_registry(&Model::Object(o), Some(®istry)); assert!(file.contains("=== \"dog\""), "should use mapping value 'dog'"); assert!(file.contains("=== \"cat\""), "should use mapping value 'cat'"); - assert!(file.contains("export function isDog")); - assert!(file.contains("export function isCat")); + assert!(file.contains("export function isPetDog")); + assert!(file.contains("export function isPetCat")); assert!(file.contains("export function narrowPet")); } diff --git a/docs-site/docs/intro.md b/docs-site/docs/intro.md index 9ffa349..43bea80 100644 --- a/docs-site/docs/intro.md +++ b/docs-site/docs/intro.md @@ -140,7 +140,7 @@ Every generated SDK is a **complete, production-ready client** — not just type | **Idempotency keys** | Auto-generated `Idempotency-Key` on unsafe methods | Safe retries on POST/PUT/PATCH/DELETE without duplicate side effects | | **SSE streaming** | Server-Sent Event parser with proper error handling | Real-time data streams without manual parsing | | **Runtime validation** | Validate request/response bodies against the spec | Catch API contract violations in dev and tests, not production | -| **oneOf type guards** | `isPetCreated()`, `narrowPetEvent()` (TS); `is_pet_created()`, `discriminant()` (Rust) | Safely narrow union types at runtime | +| **oneOf type guards** | `isPetEventPetCreated()`, `narrowPetEvent()` (TS); `is_pet_created()`, `discriminant()` (Rust) | Safely narrow union types at runtime (TS guards are union-scoped to avoid collisions when two unions share an arm) | ### Language-specific highlights @@ -707,10 +707,10 @@ Streaming calls **do not retry** (bodies are not replayable). ### oneOf narrowing (TypeScript) ```ts -import { isPetCreated, narrowPetEvent, type PetEvent } from "./models/PetEvent"; +import { isPetEventPetCreated, narrowPetEvent, type PetEvent } from "./models/PetEvent"; function handle(event: PetEvent) { - if (isPetCreated(event)) { + if (isPetEventPetCreated(event)) { console.log(event.pet.name); // narrowed return; } diff --git a/examples/petstore-ts/sdk/specforge-version.json b/examples/petstore-ts/sdk/specforge-version.json index 34f83ea..8e24ca6 100644 --- a/examples/petstore-ts/sdk/specforge-version.json +++ b/examples/petstore-ts/sdk/specforge-version.json @@ -1 +1 @@ -{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:41:58Z"} \ No newline at end of file +{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:46:34Z"} \ No newline at end of file From 4615ed2796a1dbe8283fa8b984803e8507dcee5c Mon Sep 17 00:00:00 2001 From: root Date: Fri, 31 Jul 2026 02:51:01 +0000 Subject: [PATCH 4/8] =?UTF-8?q?workspace:=20Reconcile=20version=20drift=20?= =?UTF-8?q?=E2=80=94=20Cargo.toml=200.8.0=20->=201.6.1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Cargo.lock | 14 +++++++------- Cargo.toml | 12 ++++++------ 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f734114..1a47b00 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1251,7 +1251,7 @@ dependencies = [ [[package]] name = "specforge-cli" -version = "0.8.0" +version = "1.6.1" dependencies = [ "anyhow", "clap", @@ -1270,7 +1270,7 @@ dependencies = [ [[package]] name = "specforge-core" -version = "0.8.0" +version = "1.6.1" dependencies = [ "anyhow", "indexmap", @@ -1286,7 +1286,7 @@ dependencies = [ [[package]] name = "specforge-go" -version = "0.8.0" +version = "1.6.1" dependencies = [ "rayon", "serde_json", @@ -1298,7 +1298,7 @@ dependencies = [ [[package]] name = "specforge-plugin" -version = "0.8.0" +version = "1.6.1" dependencies = [ "serde", "serde_json", @@ -1314,7 +1314,7 @@ dependencies = [ [[package]] name = "specforge-rust" -version = "0.8.0" +version = "1.6.1" dependencies = [ "rayon", "serde_json", @@ -1326,7 +1326,7 @@ dependencies = [ [[package]] name = "specforge-ts" -version = "0.8.0" +version = "1.6.1" dependencies = [ "indexmap", "rayon", @@ -1339,7 +1339,7 @@ dependencies = [ [[package]] name = "specforge-wasm" -version = "0.8.0" +version = "1.6.1" dependencies = [ "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index de3b9c3..fc9de6c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ members = [ ] [workspace.package] -version = "0.8.0" +version = "1.6.1" edition = "2021" rust-version = "1.75" license = "MIT" @@ -37,11 +37,11 @@ clap = { version = "4", features = ["derive"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = ["env-filter"] } # Internal crates -specforge-core = { path = "crates/specforge-core", version = "0.8.0" } -specforge-ts = { path = "crates/specforge-ts", version = "0.8.0" } -specforge-go = { path = "crates/specforge-go", version = "0.8.0" } -specforge-rust = { path = "crates/specforge-rust", version = "0.8.0" } -specforge-plugin = { path = "crates/specforge-plugin", version = "0.8.0" } +specforge-core = { path = "crates/specforge-core", version = "1.6.1" } +specforge-ts = { path = "crates/specforge-ts", version = "1.6.1" } +specforge-go = { path = "crates/specforge-go", version = "1.6.1" } +specforge-rust = { path = "crates/specforge-rust", version = "1.6.1" } +specforge-plugin = { path = "crates/specforge-plugin", version = "1.6.1" } tempfile = "3" # Benchmarks criterion = { version = "0.5", features = ["html_reports"] } From ded2f19ef3325bf8da2df52bbac7510c7c3ff682 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 31 Jul 2026 02:51:49 +0000 Subject: [PATCH 5/8] scripts: Always rebuild CLI in generate-examples.sh 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. --- examples/petstore-go/sdk/specforge-version.json | 2 +- examples/petstore-rust/sdk/specforge-version.json | 2 +- examples/petstore-ts/sdk/specforge-version.json | 2 +- scripts/generate-examples.sh | 8 ++++---- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/petstore-go/sdk/specforge-version.json b/examples/petstore-go/sdk/specforge-version.json index 41fbf7b..9b06abd 100644 --- a/examples/petstore-go/sdk/specforge-version.json +++ b/examples/petstore-go/sdk/specforge-version.json @@ -1 +1 @@ -{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T01:10:31Z"} \ No newline at end of file +{"specforge_version":"1.6.1","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:51:40Z"} \ No newline at end of file diff --git a/examples/petstore-rust/sdk/specforge-version.json b/examples/petstore-rust/sdk/specforge-version.json index 41fbf7b..9b06abd 100644 --- a/examples/petstore-rust/sdk/specforge-version.json +++ b/examples/petstore-rust/sdk/specforge-version.json @@ -1 +1 @@ -{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T01:10:31Z"} \ No newline at end of file +{"specforge_version":"1.6.1","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:51:40Z"} \ No newline at end of file diff --git a/examples/petstore-ts/sdk/specforge-version.json b/examples/petstore-ts/sdk/specforge-version.json index 8e24ca6..9b06abd 100644 --- a/examples/petstore-ts/sdk/specforge-version.json +++ b/examples/petstore-ts/sdk/specforge-version.json @@ -1 +1 @@ -{"specforge_version":"0.8.0","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:46:34Z"} \ No newline at end of file +{"specforge_version":"1.6.1","ir_version":"1.0","spec_version":"1.0.0","generated_at":"2026-07-31T02:51:40Z"} \ No newline at end of file diff --git a/scripts/generate-examples.sh b/scripts/generate-examples.sh index e9fcc5e..bf6d597 100755 --- a/scripts/generate-examples.sh +++ b/scripts/generate-examples.sh @@ -6,10 +6,10 @@ cd "$ROOT" export PATH="${HOME}/.cargo/bin:/usr/local/go/bin:${PATH}" BIN="${ROOT}/target/debug/specforge" -if [[ ! -x "$BIN" ]]; then - echo "==> building specforge" - cargo build -p specforge-cli -q -fi +# Always rebuild: a stale binary silently regenerates examples with old +# emitter logic (this has bitten us before). The incremental build is fast. +echo "==> building specforge" +cargo build -p specforge-cli -q gen() { local lang="$1" dest="$2" name="$3" From d6834baa7ce1af8e037e93e7017225930a387911 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 31 Jul 2026 02:54:12 +0000 Subject: [PATCH 6/8] core: Clippy to zero warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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). --- crates/specforge-core/src/ir.rs | 6 ++++++ crates/specforge-core/src/merge.rs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/specforge-core/src/ir.rs b/crates/specforge-core/src/ir.rs index c6b05cb..ecd632f 100644 --- a/crates/specforge-core/src/ir.rs +++ b/crates/specforge-core/src/ir.rs @@ -27,6 +27,12 @@ pub struct EnumModel { pub name: String, pub description: Option, pub va #[derive(Debug, Clone, serde::Serialize)] pub struct EnumVariant { pub value: String, pub description: Option } +// The ObjectModel variant (~336 bytes) is much larger than EnumModel (~72), +// which trips clippy::large_enum_variant. Boxing ObjectModel would force a +// deref at all 70+ match sites across the emitters and core for no real win — +// Model values are not stack-hot (they live in the resolved Document, accessed +// by reference). Suppressing rather than paying the ergonomic cost everywhere. +#[allow(clippy::large_enum_variant)] #[derive(Debug, Clone, serde::Serialize)] pub enum Model { Object(ObjectModel), Enum(EnumModel) } impl Model { pub fn name(&self) -> &str { match self { Model::Object(o) => &o.name, Model::Enum(e) => &e.name } } } diff --git a/crates/specforge-core/src/merge.rs b/crates/specforge-core/src/merge.rs index 028fd32..aedac54 100644 --- a/crates/specforge-core/src/merge.rs +++ b/crates/specforge-core/src/merge.rs @@ -145,7 +145,7 @@ mod tests { "info": { "title": "A", "version": "1.0" }, "paths": { "/a": {} } }); - let result = merge_specs(&[spec.clone()]).unwrap(); + let result = merge_specs(std::slice::from_ref(&spec)).unwrap(); assert_eq!(result, spec); } From c5f2b9e5b99aa6984fab1bcb50f6decb93d7edd5 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 31 Jul 2026 02:55:25 +0000 Subject: [PATCH 7/8] docs-site: Sync changelog with canonical root CHANGELOG.md 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. --- docs-site/docs/reference/changelog.md | 429 ++++++++++++-------------- 1 file changed, 194 insertions(+), 235 deletions(-) diff --git a/docs-site/docs/reference/changelog.md b/docs-site/docs/reference/changelog.md index 963efee..d0f78f3 100644 --- a/docs-site/docs/reference/changelog.md +++ b/docs-site/docs/reference/changelog.md @@ -4,327 +4,286 @@ sidebar_position: 1 description: Version history and changes --- - # Changelog All notable changes to this project are documented in this file. -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + ## [1.6.1] — 2026-07-30 ### Fixed +#### Codegen (all emitters) +- **Doc-comment escaping** — schema/operation descriptions from real-world specs (GitHub, Stripe) contain embedded newlines, backticks, and `/*`/`*/` sequences that broke generated code: continuation lines lost their comment prefix (→ parse errors), `*/` closed block comments (→ "unterminated block comment"), and backticks tripped token errors. The Rust (`rust_doc`/`rust_doc_indented`) and Go (`go_doc`/`go_doc_label`) emitters now escape these and prefix every line. **Result: the 4 `*_generates_and_compiles_{rust,go}` regression tests for GitHub + Stripe specs now pass** (they were failing). +- **Reserved-name collisions** — a spec model whose name matches a built-in SDK type (e.g. GitHub's `ValidationError`) would redeclare it and break compilation. The Go and Rust emitters now suffix colliding model names with `Model`. + #### Rust emitter -- **Generated SDK now compiles** — fixed six template bugs (nested interceptor/transformer loops, missing `logger` field on `ClientBuilder`, telemetry call-site signature mismatches, wrong `Result` arity, `do_once` parameter-order mismatch, `serde_json::to_vec` move-instead-of-borrow) that broke every generated Rust SDK since the 1.6.0 interceptors/validation_middleware/ServiceContainer additions. The example SDK was stale and had never been regenerated to surface this. +- **Generated SDK now compiles** — fixed six template bugs that silently broke every generated Rust SDK since the interceptors/validation_middleware/ServiceContainer modules were added in 1.6.0: a response-interceptor loop nested inside the response-transformer loop; a missing `logger` field on `ClientBuilder`; telemetry call-site signature mismatches (`&Method`/`Duration` vs `&str`/`u128`); a two-arg `Result` used where the crate alias is one-arg; a `do_once` parameter-order mismatch; and `serde_json::to_vec` moving instead of borrowing. The example SDK was stale and never regenerated to catch this. +- **Generated README expanded** — `emit_readme` now documents interceptors, runtime validation (`validation_middleware`), and the `ServiceContainer` telemetry/DI helpers. #### TypeScript emitter -- **Telemetry wired in** — `ApiClient` now stores and invokes the previously-dead `TelemetryHooks` (`onRequestStart`/`onRequestEnd`/`onRequestError`/`onRetry`/`onCacheHit`/`onCacheMiss`). -- **Per-model validators typecheck** — object validators now pass the `errors` accumulator and return it, matching the enum-validator pattern. +- **Telemetry is now wired** — the `TelemetryHooks` interface and `MetricsCollector` were generated but never stored or invoked by `ApiClient`. The client now reads `opts.telemetry`/`container.telemetry` and calls `onRequestStart`/`onRequestEnd`/`onRequestError`/`onRetry`/`onCacheHit`/`onCacheMiss` at the right points in the request lifecycle. +- **Per-model validators typecheck** — generated object validators called `validateValue` with three arguments (missing the `errors` accumulator) and returned its `void` result as `ValidationError[]`. Now mirrors the enum-validator pattern. #### Testing -- **Compile-the-generated-SDK tests** added for the Rust (`cargo check`), TypeScript (`tsc --noEmit`), and Go (`go build ./...`) emitters — regenerates into a temp crate and builds it so template breakage fails CI. All three emitters verified against both `fixtures/` specs; each test skips cleanly only when its toolchain is genuinely absent. +- **Compile-the-generated-SDK regression tests** — added to the Rust (`cargo check`), TypeScript (`tsc --noEmit`), and Go (`go build ./...`) emitters. These regenerate the SDK into a temp crate and build it, so future emitter template breakage fails CI instead of shipping silently. All three emitters verified against both `fixtures/` specs (petstore + sample-api); each test skips cleanly only when its toolchain is genuinely absent. --- -## [1.2.0] — 2026-07-27 +## [1.6.0] — 2026-07-27 ### Added -#### SDK Features -- **Mock server** — `specforge mock` starts local HTTP mock server from spec examples -- **Logging hooks** — `Logger` interface/trait for all 3 SDKs (ConsoleLogger, NoopLogger) -- **i18n support** — `--locale en,es` flag generates localized error messages (8 locales) -- **Request/response interceptors** — Transform request body and response data -- **Response transformers** — Post-processing hooks on deserialized responses +#### CI/CD +- **Spec diff GitHub Action** — Auto-detect spec changes in PRs, post diff comments, fail on breaking changes +- **Spec diff workflow** — `.github/workflows/spec-diff.yml` triggers on YAML/JSON changes -#### Core -- **Swagger Editor export** — `specforge export` inlines all `$ref` for editor compatibility -- **Demo spec** — `specforge demo` generates realistic Petstore spec with examples -- **Schema evolution tracker** — `specforge evolution` tracks changes over git commits -- **Reverse inference** — `specforge infer` generates OpenAPI spec from sample JSON -- **Compatibility checker** — `specforge verify` validates running APIs against spec +#### CLI +- **`specforge changelog`** — Enhanced with per-operation entries, schema changes, semantic versioning suggestions (`--suggest-version`), JSON output (`--format json`) +- **`specforge version`** — Apply versioning strategies (url/header/query) to specs. `--prefix v2`, `--strategy header` +- **`specforge generate --version-prefix v2`** — Auto-add version prefix before generation -#### Testing -- **208 tests** across core, emitters, and CLI -- Swagger Editor lint rules (`--profile swagger-editor`) +#### Plugin System +- **Plugin marketplace** — `specforge plugin list/search/info/install` +- **4 curated plugins** — Kotlin, Swift, Python, C# emitter placeholders +- **Plugin configuration** — `.specforge.yaml` plugin section +- **`--plugin `** flag on `generate` to use custom WASM emitters -## [1.1.0] — 2026-07-27 +#### Profiling +- **`specforge profile`** — Performance profiling against live APIs +- **Metrics** — Latency (avg/p50/p95/p99), throughput, error rate, cache hit rate +- **Recommendations** — Actionable optimization suggestions -### Added +### Quality +- **259 tests** (core 242 + TS 17) -#### Core -- **Observability dashboard** — `specforge dashboard` generates HTML dashboard with Chart.js for SDK metrics visualization. -- **OpenAPI 3.1 native parser** — `Schema31` model, `parse_31()`, `warn_unsupported_features()` for unsupported 3.1 features. -- **Security scheme analysis** — `specforge security` with text/JSON/markdown output. -- **Dependency graph** — `specforge graph` generates Mermaid/DOT diagrams. -- **Bundle analyzer** — `specforge analyze` detects unused schemas, duplicates, large models. -- **contentMediaType/contentEncoding** — `Scalar::Base64` and `Scalar::Binary` for binary data. -- **Per-operation retry policies** — `x-retry` extensions on operations. -- **Diff improvements** — Markdown/JSON/color output, inline schema diffs. -- **JSDoc/docstrings** — Documentation comments from spec descriptions in generated code. - -#### Ecosystem -- **5 large test fixtures** — GitHub Enterprise (14MB), Atlassian, LaunchDarkly, OpenAI (3.1.0) -- **Performance benchmarks** — Criterion benchmarks + 13 perf tests -- **VS Code extension** — Complete rewrite: 15 commands, icons, auto-validate, progress, output channel -- **Website** — Landing page at specforge.deepwhaleai.com +--- -## [1.0.0] — 2026-07-27 +## [1.5.0] — 2026-07-27 ### Added -#### SDK runtimes -- **Rate limiting** — Token bucket and sliding window rate limiters for all 3 SDKs. TS: `rateLimiter` option. Go: `WithRateLimiter()`. Rust: `.rate_limiter()`. -- **Telemetry hooks** — Request metrics, error tracking, retry counting, cache hit/miss. TS/Go/Rust: `TelemetryHooks` interface/trait + `MetricsCollector`. - -#### Core -- **Spec deprecation tracking** — `find_deprecations()` scans IR for deprecated operations/schemas. `generate_migration_guide()` compares two specs. -- **`specforge migrate`** — Generate Markdown migration guides between spec versions. Deprecation comments in generated code (`@deprecated`, `// Deprecated:`, `#[deprecated]`). +#### Web UI +- **Professional light theme** — Complete redesign with modern design system (1713 lines) +- **Design system** — Color palette, typography, spacing, components documented +- **Interactive features** — Schema tree browser, JSON viewer, YAML export, copy buttons +- **Responsive design** — Mobile-friendly with breakpoints at 1024px and 640px +- **WCAG AA accessibility** — Focus states, aria labels, keyboard navigation + +#### Marketplace +- **18 curated specs** — GitHub, Stripe, Petstore, Kubernetes, Spotify, Notion, Twilio, Vercel, Okta, Atlassian, Adyen, Bitbucket, Linode, CircleCI, LaunchDarkly, Adobe AEM, 1Password, Ably +- **`specforge market list`** — Browse all specs with ratings and downloads +- **`specforge market search`** — Search by name, description, tags, author +- **`specforge market info`** — Detailed spec information +- **`specforge market add`** — Add your own spec to the marketplace + +### Quality +- **227 tests** (core 210 + TS 17) -#### Fixes -- **Go pipeline** — Fixed unused `obj` variable in validate.go, fixed hyphenated field names +--- -### Changed -- **v1.0.0** — First stable release. All roadmap items from README completed. -- Website live at `specforge.deepwhaleai.com` -- 6 external test fixtures (petstore, GitHub, Stripe, Kubernetes, Twilio) -- 176 tests across core, emitters, and CLI - -## [0.9.0] — 2026-07-27 +## [1.4.0] — 2026-07-27 ### Added -#### SDK runtimes -- **Response caching** — All three SDKs now support ETag-based caching. `ResponseCache` with TTL expiry, `If-None-Match` headers, 304 handling. TS: `cache` option on client. Go: `WithCache(ttl)`. Rust: `.cache_ttl(Duration)`. - -#### Core -- **Webhooks support** — OpenAPI 3.1 `webhooks` section parsed from raw JSON (bypasses `openapiv3` crate limitation). `Webhook` IR type with method, path, request body, responses. -- **`specforge workspace`** — Generate SDKs for multiple specs from `.specforge-workspace.yaml` config. `--only` filter, `--dry-run` mode. -- **`specforge workspace-init`** — Scan a directory and generate a starter workspace config. +#### VS Code Extension +- **24 commands** (was 3) — All CLI subcommands +- **6 configuration settings** — binaryPath, defaultLang, outputDir, autoValidate, specFilePattern, logLevel +- **3 keyboard shortcuts** — Generate (Ctrl+Shift+G), Check (Ctrl+Shift+V), Emit IR (Ctrl+Shift+I) +- **Context menus** — Editor + Explorer right-click for YAML/JSON files +- **Status bar** — Mock server indicator with stop button +- **Auto-validate on save** — Configurable spec validation +- **Progress notifications** — Visual feedback for long operations +- **Output channel** — Dedicated panel for all CLI output +- **Interactive pickers** — Language, format, version, file pickers -#### Emitters -- **TypeScript webhook types** — `webhooks.ts` with payload types, `WebhookHandler`, factory functions. -- **Go webhook types** — `webhooks.go` with payload types and `WebhookHandler` func type. -- **Rust webhook types** — `webhooks.rs` with payload structs and `WebhookHandler` trait. +--- -#### CLI -- **`--include-webhooks`** flag on `generate` — opt-in webhook type generation. - -## [0.8.0] — 2026-07-27 +## [1.3.0] — 2026-07-27 — Production-Ready Milestone ### Added +- **221 tests passing** (core 204 + TS 17) +- **21 test fixtures** (0.02MB to 14MB, 4 OpenAPI versions) +- **Stability enforcement** — IR version + `specforge-version.json` in generated SDKs +- **SDK integration tests** — Validate TS/Go/Rust against mock servers +- **Thread safety audit** — No deadlocks or race conditions +- **Unwrap/expect audit** — Production code audited for panics +- **SDK changelog generation** (`specforge changelog`) +- **ServiceContainer** for TS/Go/Rust dependency injection +- **Spec validation middleware** — Auto-validate all requests/responses -#### Core -- **OpenAPI 3.1 expanded** — `const` → `enum: [value]`, `dependentRequired` merged into `required`, `prefixItems` → `items`. Upgraded 3.0→3.1 now converts `enum: [value]` → `const`. 31 new tests. -- **3.1 feature detection** — `detect_31_features()` reports which 3.1 features are used in a spec. -- **Spec composition** — New `merge` module. `specforge merge` combines multiple spec files (paths, schemas, security schemes). Later specs override on conflict. - -#### Rust emitter -- **Dependency injection** — `ClientBuilder::http_client(reqwest::Client)` for injecting custom HTTP clients in tests. - -#### CLI -- **`specforge merge`** — Merge multiple OpenAPI spec files into one. `--format json|yaml`, `--out FILE`. +--- -## [0.7.0] — 2026-07-27 +## [1.2.0] — 2026-07-27 ### Added +- **`specforge mock`** — Local mock server from spec examples +- **SDK logging hooks** — Logger interface (ConsoleLogger, NoopLogger) +- **i18n support** — 8 locales, `--locale en,es` flag +- **Request/response interceptors** for all 3 SDKs +- **Response transformers** for post-processing +- **`specforge export`** — Swagger Editor compatible output +- **`specforge demo`** — Realistic Petstore spec with examples +- **`specforge evolution`** — Schema change tracking over git commits +- **`specforge infer`** — Generate OpenAPI from sample JSON +- **`specforge verify`** — Validate running APIs against spec -#### TypeScript emitter -- **Tree-shakeable API modules** — `src/index.ts` no longer re-exports API classes. Consumers import individual tags directly (`import { PetsApi } from "./api/Pets"`). New `src/api/index.ts` convenience barrel for callers who want everything. Subpath exports in `package.json` for `./api` and `./api/*`. - -#### CLI -- **`specforge versions`** — List all API versions in a spec directory. Supports flat (`v1.yaml`, `v2.yaml`) and nested (`v1/openapi.yaml`) conventions. -- **`--version` flag on `generate`** — Filter by version when spec is a directory. Matches by exact version string, file stem, or directory name. -- **`--profile` flag on `generate` and `emit`** — Outputs detailed timing breakdown (parse, resolve, emit, total) to stderr. - -#### Core -- **`scan_versions()`** — Scans a directory for spec files and extracts API versions. -- **`resolve_spec_path()`** — Resolves spec path with optional version filtering. +--- -## [0.6.0] — 2026-07-27 +## [1.1.0] — 2026-07-27 ### Added +- **`specforge dashboard`** — HTML metrics visualization with Chart.js +- **OpenAPI 3.1 native parser** — `Schema31`, `parse_31()`, unsupported feature warnings +- **`specforge security`** — Auth analysis (text/JSON/markdown output) +- **`specforge graph`** — Mermaid/DOT dependency diagrams +- **`specforge analyze`** — Unused schemas, duplicates, large models, recommendations +- **contentMediaType/contentEncoding** — `Scalar::Base64`, `Scalar::Binary` +- **Per-operation retry policies** — `x-retry` extensions +- **Diff improvements** — Markdown/JSON/color output, inline schema diffs +- **JSDoc/docstrings** — Documentation from spec descriptions in generated code -#### Core -- **Mock server test generation** — New `specforge test` subcommand generates mock server tests for TS/Go/Rust SDKs. Creates mock HTTP servers from spec example responses and verifies SDK calls. -- **3.1 `$ref` sibling support** — `$ref` objects with sibling `description`/`summary`/`deprecated` are now preserved via `allOf` wrapping during preprocessing. IR `Type::Reference` carries optional `description` override. -- **Configurable lint rules** — New `lint_config` module with `LintConfig`, `LintRule`, `RuleSeverity`. Loads from `.specforge.yaml`. CLI flags: `--disable`, `--enable`, `--severity`, `--config`. -- **New lint rules** — `missing-operation-id`, `missing-schema-description` (off by default), `path-trailing-slash`, `deprecated-operation`. +--- -## [0.5.0] — 2026-07-27 +## [1.0.0] — 2026-07-27 ### Added +- **Rate limiting** — Token bucket + sliding window (TS/Go/Rust) +- **Telemetry hooks** — Request metrics, error tracking, cache hit/miss +- **`specforge migrate`** — Generate migration guides between spec versions +- **Deprecation tracking** — Comments in generated code (`@deprecated`, `// Deprecated:`, `#[deprecated]`) +- **Website** — specforge.deepwhaleai.com +- **176 tests** passing -#### TypeScript emitter -- **Runtime validation** — `validate.ts` with per-model schema descriptors and validators. `validateRequest()` / `validateResponse()` called automatically when `validate: true` is set on the client. - -#### Go emitter -- **Runtime validation** — `validate.go` with `ValidatePet(v any) error` per model. `WithValidation(true)` option on client. +--- -#### Rust emitter -- **Runtime validation** — `validate.rs` with `fn validate_pet(v: &Value)` per model. `.validation(true)` builder option. +## [0.9.0] — 2026-07-27 -#### WASM -- **`specforge-wasm` crate** — Compiles core parsing/resolving to WASM via `wasm-bindgen`. Exposes `parse_and_resolve()` and `lint()` functions. -- **Web UI WASM integration** — "Parse Locally (WASM)" button for client-side spec parsing without `specforge emit`. +### Added +- **Response caching** — ETag-based caching with TTL expiry and 304 handling +- **OpenAPI 3.1 webhooks** — `--include-webhooks` flag +- **`specforge workspace`** — Multi-spec generation from config +- **`specforge workspace-init`** — Generate workspace config from directory -#### CLI -- **`specforge docs`** — Generate static HTML API documentation from a spec. Dark forge-themed with color-coded HTTP method badges. +--- -## [0.4.0] — 2026-07-27 +## [0.8.0] — 2026-07-27 ### Added +- **Rust `http_client()`** — Dependency injection builder +- **`specforge merge`** — Combine multiple spec files into one +- **OpenAPI 3.1 expanded** — `const`, `dependentRequired`, `prefixItems` +- **3.1 feature detection** (`detect_31_features()`) -#### Core -- **Spec validation** — New `validate` module with 51 unit tests. Validates JSON values against IR Type definitions (scalars, enums, arrays, maps, references, compositions). Returns `ValidationError` with JSON-pointer paths. -- **WASM plugin SDK** — `specforge-plugin` crate with `Plugin` trait, `GeneratedFile`/`PluginResult` types, and `export_plugin!` macro for WASM targets. -- **Example plugin** — `examples/plugin-example/` — `ReadmePlugin` that generates a README from the IR. - -#### Web UI -- **`web-ui/index.html`** — Self-contained single-page app for browsing IR: schema tree, operations tab, IR JSON viewer, stats dashboard. Dark forge-themed. No build step. - -#### Docs -- **`PLUGINS.md`** — How to build WASM emitter plugins (protocol, build steps, example). +--- -## [0.3.0] — 2026-07-26 +## [0.7.0] — 2026-07-27 ### Added +- **Tree-shakeable TS modules** — Per-tag imports, `src/api/index.ts` barrel +- **`specforge versions`** — List API versions in a directory +- **`--version` flag** — Filter by version from directory +- **`--profile` flag** — Timing breakdown for pipeline stages -#### Core -- **JSON Schema for IR** — `assets/ir-schema.json` (draft 2020-12) describes the full IR output. `specforge emit --schema` prints it. -- **Streaming IR emission** — `specforge emit --stream` outputs newline-delimited JSON (NDJSON) for processing large specs without loading everything into memory. -- **Parallel file generation** — All three emitters now write files in parallel using rayon. +--- -#### CLI -- **`specforge init`** — Scaffold a new OpenAPI spec with a `/health` endpoint and README. -- **`specforge convert`** — Convert between OpenAPI 3.0 and 3.1 (`--to 3.0` or `--to 3.1`). Operates on raw YAML/JSON to preserve full spec structure. +## [0.6.0] — 2026-07-27 + +### Added +- **`specforge test`** — Mock server test generation (TS/Go/Rust) +- **OpenAPI 3.1 `$ref` siblings** — `description`/`summary` preserved via `allOf` wrapping +- **Configurable lint rules** — `.specforge.yaml` config, `--disable`/`--enable`/`--severity` +- **New lint rules** — `missing-operation-id`, `path-trailing-slash`, `deprecated-operation` -#### Ecosystem -- **GitHub Action** — `action.yml` composite action for CI: `generate`, `check`, `diff`, `emit` commands. Auto-detects platform and installs the correct binary. -- **VS Code extension** — `vscode-extension/` scaffolding with Generate SDK, Check Spec, and Preview IR commands. -- **Stability policy** — `STABILITY.md` documents semver guarantees for the IR schema, CLI, and generated output. -- **Incremental generation guide** — `INCREMENTAL.md` documents CI caching strategies. +--- -## [0.2.2] — 2026-07-26 +## [0.5.0] — 2026-07-27 ### Added +- **Runtime validation middleware** — TS/Go/Rust SDKs validate request/response bodies +- **WASM-compiled specforge** — `specforge-wasm` crate for browser use +- **`specforge docs`** — Static HTML API documentation generator +- **Web UI WASM integration** — Client-side parsing -#### Core -- **OpenAPI 3.1 support** — Transparent preprocessing layer that converts 3.1 specs to 3.0 before parsing. Handles `type: ["string", "null"]` → `nullable: true`, numeric `exclusiveMinimum`/`exclusiveMaximum` → boolean, and missing `paths` → empty object. -- **IR serialization** — All IR types now derive `serde::Serialize` for JSON emission. +--- -#### CLI -- **`specforge emit`** — New subcommand that dumps the resolved IR as JSON to stdout. Enables external emitters and plugins to consume the IR without embedding specforge. +## [0.4.0] — 2026-07-27 -#### Branding -- **Fox blacksmith mascot** — New logo with a fox character wearing goggles, holding a hammer and document. -- **Forge color scheme** — Warm orange/amber/red palette replacing the previous blue/purple. +### Added +- **WASM plugin SDK** — `specforge-plugin` crate with `Plugin` trait +- **Web UI** — Interactive IR browser (specs, operations, schemas) +- **Spec validation module** — 51 unit tests -## [0.2.1] — 2026-07-26 +--- + +## [0.3.0] — 2026-07-27 ### Added +- **JSON Schema for IR** — `assets/ir-schema.json` +- **Streaming IR emission** — NDJSON for large specs +- **Parallel file generation** — Rayon +- **`specforge init`** — Scaffold new OpenAPI spec +- **`specforge convert`** — 3.0 ↔ 3.1 conversion +- **GitHub Action** — One-line CI integration +- **VS Code extension** — Initial scaffolding -#### Core -- **AllOf type aliases** — When `allOf` has exactly one `$ref` member, emitters use Go struct embedding and Rust `#[serde(flatten)]` instead of merging all properties into a flat struct. -- **Spec diff** — New `specforge diff ` subcommand that compares two OpenAPI specs and reports breaking changes (removed operations, new required parameters/properties, type changes). Exits non-zero on breaking changes for CI gates. -- **Generation benchmarks** — `scripts/bench.sh` times generation on GitHub and Stripe specs across all 3 languages. +--- -## [0.2.0] — 2026-07-26 +## [0.2.2] — 2026-07-27 ### Added +- **OpenAPI 3.1** — Transparent preprocessing (type arrays, $ref siblings, const, prefixItems) +- **`specforge emit`** — Dump resolved IR as JSON +- **Fox mascot "Ember"** — Blacksmith fox with goggles and forge theme -#### Core -- **AllOf property merging** — `build_model()` now merges properties from all `allOf` members (later members override on name conflict, required sets are unioned). Go and Rust emitters get merged properties instead of lossy first-member/`map[string]any` fallbacks. -- **Discriminator `mapping` support** — IR `Discriminator` now carries `mapping: Option>`. TS and Rust emitters use explicit mapping values when arm schemas don't embed single-variant string enums. -- **Spec linting** — New `specforge_core::lint` module with 4 checks: duplicate operation IDs (error), missing response descriptions (warning), missing operation summaries (warning), unused schemas (warning). -- **Circular reference tests** — Unit tests for self-referential schemas and mutual A→B→A cycles. - -#### CLI -- **`specforge check` subcommand** — Lint and validate a spec without generating. `--strict` flag promotes warnings to errors. -- CLI refactored to subcommand-based: `specforge generate ` and `specforge check `. +--- -#### TypeScript emitter -- Discriminator mapping fallback in `discriminant_literal()` when arm schemas lack single-variant enums. +## [0.2.1] — 2026-07-27 -#### Go emitter -- **OneOf ergonomic helpers** — `New{Union}(m map[string]any)` and `{Union}Discriminant(v)` for discriminated unions. -- **`json.RawMessage` fallback** — `New{Union}FromJSON(raw json.RawMessage)` for non-discriminated oneOf unions. -- **Streaming middleware** — `StreamMiddleware` type + `UseStream()` method. Applied before HTTP call in `doOnceStream`; can modify headers without consuming the response body. -- **Richer README** — Generated README now includes Errors, Pagination, Concurrency, Dedupe, Middleware, Streaming/SSE, and Idempotency sections. +### Added +- **AllOf type aliases** — Go embed, Rust `#[serde(flatten)]` +- **`specforge diff`** — Breaking change detection +- **Generation benchmarks** — `scripts/bench.sh` -#### Rust emitter -- **OneOf ergonomic helpers** — `impl` block on each oneOf enum with `discriminant()`, `is_{arm}()`, `into_{arm}()`, `as_{arm}()`. -- **Streaming middleware** — `StreamMiddleware` type + `use_stream_middleware()`. Applied in `request_stream` before `builder.send()`. -- **Richer README** — Generated README now includes Errors, Pagination, Concurrency, Dedupe, Middleware, Streaming/SSE, and Idempotency sections. +--- -#### CI & packaging -- **Multi-platform CI** — Test matrix expanded to `ubuntu-latest`, `macos-latest`, `windows-latest`. -- **Cross-compiled release binaries** — `.github/workflows/release.yml` builds for 5 targets (linux amd64/arm64, macOS Intel/Apple Silicon, Windows) on tag push. -- **Crates.io publish workflow** — `.github/workflows/publish.yml` with dry-run support. +## [0.2.0] — 2026-07-26 -### Changed -- CLI is now subcommand-based: `specforge generate ` instead of `specforge `. The old flat invocation no longer works. -- Internal crate dependencies now specify version requirements for crates.io compatibility. +### Added +- **Discriminator mapping** — Full support across all 3 emitters +- **OneOf helpers** — `isX()`/`narrowX()` (TS), `New{Union}`/`New{Union}FromJSON` (Go), `discriminant()`/`is_*()` (Rust) +- **Streaming middleware** — `StreamMiddleware` for TS/Go/Rust +- **Multi-platform CI** — Linux, macOS, Windows -### Fixed -- Unmatched brace in Rust `emit_readme()` format string. +--- ## [0.1.0] — 2026-07-26 -First public MVP of **specforge**: OpenAPI → typed SDKs for TypeScript, Go, and Rust. - ### Added +- **Initial MVP** — 3 emitters (TypeScript, Go, Rust), full runtime, e2e tests + +--- -#### Core -- Language-neutral IR (`specforge-core`) with `$ref` resolution, composition (`allOf`/`oneOf`/`anyOf`), discriminators, and inline string enums -- CLI `specforge` with `-l ts|go|rust`, `-o`, `-n`, `-v` - -#### TypeScript emitter (`specforge-ts`) -- Multi-file tree-shakeable package (ESM/CJS via tsup) -- Runtime: auth, retry, timeout, pagination, concurrency semaphore, in-flight dedupe, middleware, idempotency keys, SSE/chunk streaming -- oneOf runtime type guards (`isX` / `narrowX`) from discriminator literals -- Fixed nested JSDoc comment bug in concurrency template -- Dedupe buffers response bodies so concurrent `requestJson` callers each get a readable body - -#### Go emitter (`specforge-go`) -- Stdlib-only client (`net/http`, `encoding/json`) -- Runtime: auth providers, retry + full-jitter backoff, timeouts, cursor/offset pagination, semaphore, dedupe, middleware, idempotency keys, SSE iterator (`NewSseIterator`), `DoStream` -- Large-spec hardening: field-name collisions (`+1`/`-1`), reserved param idents, `anyString` coercion - -#### Rust emitter (`specforge-rust`) -- `reqwest` + `serde` + `tokio` crate layout -- Runtime: `Auth` enum, retry, timeouts, pagination, semaphore, dedupe, middleware, idempotency keys, `request_stream` / `SseStream` -- Untagged enums for oneOf; `Display` on string enums for query encoding - -#### Quality -- Regression suite: petstore generate (TS/Go/Rust); GitHub + Stripe resolve + Go/Rust compile gates -- E2E smoke: petstore basics + sample-api auth/retry/pagination (all 3 langs) -- E2E advanced: concurrency serialisation, dedupe single-flight, middleware header rewrite, idempotency-key on POST, SSE parse (all 3 langs) -- CI: `.github/workflows/ci.yml` + `scripts/ci.sh` (`full` / `quick` / `e2e` / `regression`) - -#### Docs & packaging -- Modern README with logo/banner, architecture, runtime matrix, examples -- `CHANGELOG.md`, `RELEASE.md`, `examples/*` consumer stubs -- `assets/logo.svg`, `assets/logo-banner.svg` - -### Known limitations -- Go/Rust do not yet emit oneOf runtime type guards (Rust uses `#[serde(untagged)]`) -- Streaming middleware on Go `DoStream` does not rewrite responses (body stays open) -- Large-spec downloads require network on first run (cached under `target/spec-cache/`) - -[1.2.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.2.0 -[1.1.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.1.0 -[1.0.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.0.0 -[0.9.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.9.0 -[0.8.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.8.0 -[0.7.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.7.0 -[0.6.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.6.0 -[0.5.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.5.0 -[0.4.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.4.0 -[0.3.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.3.0 -[0.2.2]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.2 -[0.2.1]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.1 -[0.2.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.0 -[0.1.0]: https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.1.0 +## Version Links + +- [v1.6.1](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.6.1) +- [v1.6.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.6.0) +- [v1.5.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.5.0) +- [v1.4.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.4.0) +- [v1.3.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.3.0) +- [v1.2.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.2.0) +- [v1.1.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.1.0) +- [v1.0.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v1.0.0) +- [v0.9.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.9.0) +- [v0.8.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.8.0) +- [v0.7.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.7.0) +- [v0.6.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.6.0) +- [v0.5.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.5.0) +- [v0.4.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.4.0) +- [v0.3.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.3.0) +- [v0.2.2](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.2) +- [v0.2.1](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.1) +- [v0.2.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.2.0) +- [v0.1.0](https://github.com/amafjarkasi/specforge-openapi-sdk-codegen/releases/tag/v0.1.0) From fbbfe7def7737b28d553a9a69acf6a2628e04969 Mon Sep 17 00:00:00 2001 From: root Date: Fri, 31 Jul 2026 02:59:11 +0000 Subject: [PATCH 8/8] go: Escape spec-derived text in deprecation/returns comment lines 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. --- crates/specforge-go/src/lib.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/crates/specforge-go/src/lib.rs b/crates/specforge-go/src/lib.rs index 273a7a6..768f90f 100644 --- a/crates/specforge-go/src/lib.rs +++ b/crates/specforge-go/src/lib.rs @@ -385,7 +385,7 @@ fn emit_enum(e: &EnumModel) -> String { out.push_str(&go_doc(d, "")); if d.to_lowercase().contains("deprecated") { if let Some(alt) = go_schema_deprecation_alternative(d) { - out.push_str(&format!("// Deprecated: Use {alt} instead.\n")); + out.push_str(&format!("// Deprecated: Use {} instead.\n", go_inline(&alt))); } else { out.push_str(&format!("// Deprecated: {name} is deprecated.\n")); } @@ -411,7 +411,7 @@ fn emit_object(o: &ObjectModel, doc: &Document) -> String { out.push_str(&go_doc(d, "")); if d.to_lowercase().contains("deprecated") { if let Some(alt) = go_schema_deprecation_alternative(d) { - out.push_str(&format!("// Deprecated: Use {alt} instead.\n")); + out.push_str(&format!("// Deprecated: Use {} instead.\n", go_inline(&alt))); } else { out.push_str(&format!("// Deprecated: {name} is deprecated.\n")); } @@ -2661,7 +2661,7 @@ fn emit_method(op: &Operation) -> String { if !error_responses.is_empty() { for r in &error_responses { if let Some(desc) = &r.description { - body.push_str(&format!("// Returns {} for {}.\n", desc, r.status)); + body.push_str(&format!("// Returns {} for {}.\n", go_inline(desc), r.status)); } else { body.push_str(&format!("// Returns an error for {}.\n", r.status)); } @@ -2670,7 +2670,7 @@ fn emit_method(op: &Operation) -> String { // Deprecation notice. if is_operation_deprecated(op) { if let Some(alt) = go_deprecation_alternative(op) { - body.push_str(&format!("// Deprecated: Use {alt} instead.\n")); + body.push_str(&format!("// Deprecated: Use {} instead.\n", go_inline(&alt))); } else { body.push_str(&format!("// Deprecated: {name} is deprecated.\n")); } @@ -3219,6 +3219,13 @@ fn go_doc_label(text: &str, label: &str) -> String { out } +/// Sanitize a spec-derived string for safe inlining into a single Go `//` +/// comment line: escape `*/` (closes block comments) and collapse embedded +/// newlines to spaces so the comment stays on one line. +fn go_inline(text: &str) -> String { + text.replace("*/", "*/").replace(['\n', '\r'], " ") +} + fn escape_go_string(s: &str) -> String { s.replace('\\', "\\\\").replace('"', "\\\"") }