Skip to content

feat: engine, IR, and compat enhancements for dotnet + kotlin emitter support#19

Merged
gjtorikian merged 10 commits intomainfrom
dotnet
Apr 14, 2026
Merged

feat: engine, IR, and compat enhancements for dotnet + kotlin emitter support#19
gjtorikian merged 10 commits intomainfrom
dotnet

Conversation

@gjtorikian
Copy link
Copy Markdown
Collaborator

@gjtorikian gjtorikian commented Apr 14, 2026

Summary

  • Manifest-based stale-file pruningoagen generate now tracks emitted files in .oagen-manifest.json and deletes stale files on subsequent runs, with safety guards (header-marker gating, first-adoption skip, --no-prune opt-out)
  • IR improvements — synthesize enums/discriminators from oneOf patterns, singularize create-method names, add isArrayResponse flag to operation plan, add urlBuilder hint and SplitHint.optionalParams support
  • Engine enhancements — Kotlin deep-merge adapter, expose prior target manifest paths in emitter context, improved deep-merge insertion formatting
  • Compat — extract @deprecated JSDoc tag from class declarations for IDE strikethrough support
  • Build fix — treat test files as roots so they survive tree-shaking and get formatted

Test plan

  • New unit tests for manifest read/write/prune (test/engine/manifest.test.ts)
  • New tests for orchestrator pruning logic (test/engine/orchestrator-prune.test.ts)
  • New tests for isArrayResponse operation plan detection (test/engine/operation-plan.test.ts)
  • Updated operation-hints tests for singularized create methods
  • Verify oagen generate on a real spec to confirm stale-file pruning works end-to-end
  • Verify Kotlin deep-merge adapter with a Kotlin emitter target

🤖 Generated with Claude Code

gjtorikian and others added 10 commits April 13, 2026 11:52
Every run of \`oagen generate\` now writes \`.oagen-manifest.json\` to the
output and target directories listing every emitted path. On subsequent
runs, files recorded in the previous manifest but absent from the
current emission are deleted, preventing stale-file accumulation across
regens (alias/dedup targets that fall out of the IR).

Safety:
- Deletion is gated on the auto-generated header marker. Files missing
  the marker are preserved and reported (defends against hand-edits).
- First-adoption runs (no previous manifest) skip the prune phase and
  just write the baseline manifest, so existing SDKs aren't surprised.
- Opt out per run with \`--no-prune\`; the manifest is still refreshed so
  the next prune-enabled run has a current baseline.
- Empty parent directories are cleaned up after deletion.

Motivated by a 347-file cleanup in workos-python where dedup/alias
artifacts from prior regens had accumulated silently over time.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
OperationHint gains an optional `urlBuilder: boolean` flag for
redirect-style endpoints (e.g. /sso/authorize, /user_management/sessions/logout)
where the right thing to generate is a method that returns the
constructed URL rather than one that issues an HTTP call. Emitters that
ignore the flag retain previous behavior.

SplitHint.optionalParams was previously documented but not honored: the
resolver always built ResolvedWrapper.optionalParams as []. Wire it
through so split operations can mark exposed params as optional even
when no addressable variant model exists (e.g. inline oneOf branches).

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
POST /users would derive create_users — but a create call always creates
exactly one resource. Treat create as a single-resource verb in
deriveMethodName so POST /users -> create_user, POST /organizations ->
create_organization, etc. List operations remain plural.

Update the two test cases that asserted the old behavior.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Two complementary recognizers in schemaToTypeRef when a schema uses
oneOf (or anyOf) without an explicit `discriminator:` key:

1. oneOf-of-literals → enum. When every non-null variant is a plain
   string `const` (or single-element `enum`), collapse the union into
   a single EnumRef. Replaces the opaque `union<string, string, …>`
   representation with a proper enum whose members match the consts.
   Catches patterns like `provider: oneOf [{const: "AppleOAuth"}, ...]`.

2. oneOf-of-objects-with-shared-const → discriminated union. When
   every object variant pins the same property to a const value (and
   each variant has a resolvable model name via $ref/title), synthesize
   a `discriminator: { property, mapping }` on the union. Covers
   EventSchema-style shapes where the spec encodes a discriminator via
   `const: "..."` on each variant instead of the explicit
   `discriminator:` keyword.

Nothing regresses for spec authors who already use explicit
discriminators — those still short-circuit first.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Detect unpaginated array-of-model responses so emitters can return
Model[] and apply elementwise deserialization. Paginated operations
continue using the pagination wrapper instead.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Surface the deprecation message on ApiClass so emitters can propagate
it to service properties, giving IDE strikethrough at the access site.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Implement extractMembers and shouldSkipDeepMerge for the Kotlin merge
adapter. extractKotlinClassMembers parses property/function/companion
declarations and folds trailing getters/setters and preceding KDoc
into each member's text span.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Separate inserted members with blank lines and add a leading blank line
before the first insertion to visually separate from existing members.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Thread targetDir and priorTargetManifestPaths through EmitterContext so
emitters can distinguish oagen-written files from hand-maintained ones.
Reuse the already-read manifest in the orchestrator instead of reading
it twice.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
…ormatted

Test files (.spec.ts, .test.ts) are standalone entry points that belong
in the target repo. Marking them as roots ensures they're integrated and
processed by the post-generation formatter.

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@gjtorikian gjtorikian changed the title feat: engine, IR, and compat enhancements for dotnet emitter support feat: engine, IR, and compat enhancements for dotnet + kotlin emitter support Apr 14, 2026
@gjtorikian gjtorikian merged commit 9240fb2 into main Apr 14, 2026
8 checks passed
@gjtorikian gjtorikian deleted the dotnet branch April 14, 2026 21:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant