Skip to content

v1.66.0

Latest

Choose a tag to compare

@erraggy erraggy released this 29 Aug 04:11
Immutable release. Only release title and notes can be modified.
76456a8

🔗 One Shape, and Every Name Still Names It

v1.65.0 let you spare the names an author published, by declining to consolidate them. The duplicate shapes stayed. This release consolidates the shape and keeps every name, because a Schema Object may itself be a $ref.

oastools join --semantic-dedup --dedup-mode pointer pets.json store.json orders.json

Three services, each declaring its own name for one shape:

--dedup-mode remove (default) --dedup-mode pointer
definitions orders.Marker pets.Label
orders.Marker$ref pets.Label
store.Tag$ref pets.Label
GET /pets returns orders.Marker pets.Label
GET /store/inventory returns orders.Marker store.Tag

One shape stored either way. Under pointer no reference is rewritten, so a reference to any name in the group still resolves to the shape it always did, wherever it sits: an operation, a webhook, a callback, or another schema.

🧩 Why this is a strategy and not a caller workaround

The consumer-visible difference is in generated code. The same three documents through oastools generate -types:

remove pointer
types generated 1 3
type OrdersMarker struct {...} type PetsLabel struct {...}
type OrdersMarker = PetsLabel
type StoreTag = PetsLabel

Those are Go type aliases, so all three names are distinct identifiers and mutually assignable. Code that names PetsLabel keeps compiling under pointer and breaks under the default.

🏷️ Whose vocabulary wins

Ranking by sort order alone made the winning vocabulary incidental: above, an Orders name became the shape for a Pets response. pointer keeps every name, so the survivor is only the name the shape is stored under, and it is ranked to read well:

  1. A name a collision rename invented still loses to one a document declared.
  2. Otherwise, the name the earliest source document declared wins.
  3. Otherwise, sort order.

The default keeps its existing ranking, so the collision collapse and the semantic pass still cannot disagree about which name a document keeps (#498).

📋 Sizing it, and the report

--dedup-report now says what became of each folded name, not just where it came from:

Consolidations (1):
  pets.Label (declared)
    <- orders.Marker (declared) -> kept as reference
    <- store.Tag (declared) -> kept as reference

In Go, JoinResult.Consolidations[i].Folded[j].Pointer carries the same thing: the difference between a name a consumer can still refer to and one that is gone.

result, err := joiner.JoinWithOptions(
    joiner.WithFilePaths("pets.json", "store.json", "orders.json"),
    joiner.WithSemanticDeduplication(true),
    joiner.WithDeduplicationMode(joiner.DeduplicationModePointer),
)

Available on every surface: the library option above and JoinerConfig.DeduplicationMode, the CLI --dedup-mode, and dedup_mode on the MCP join tool.

✅ What does not change

  • Names one schema tree references are still held apart. A Shipment requiring both an OriginAddress and a DestinationAddress keeps both names under every mode (#501).
  • --dedup-scope composes. generated-only still declines to fold a declared name at all.
  • The equivalence comparison is untouched.

⚠️ One cost

OAS 2.0 ignores a $ref's siblings, so a pointer carries no description of its own. Where two documents described the same shape differently, the survivor's description is the one that remains. A real reduction, though a smaller one than losing the name.

🔒 User Impact

  • No breaking changes. DeduplicationModeRemove is the default and the previous behavior, byte for byte.
  • Public API additions only: DeduplicationMode, DeduplicationModeRemove, DeduplicationModePointer, ValidDeduplicationModes(), IsValidDeduplicationMode(), WithDeduplicationMode(), JoinerConfig.DeduplicationMode, FoldedName.Pointer.
  • Fix: the MCP join tool now validates dedup_mode and dedup_scope before resolving specs, so a mistyped value no longer fetches and parses every input (including URLs) before failing. dedup_scope had this gap since it was added.
  • No dependency changes.

📊 Quality Metrics

  • ✅ 11292 tests passing
  • ✅ Zero vulnerabilities (govulncheck clean)
  • ✅ Benchmarks flat: joiner allocs/op unchanged on the default path (32/32, 33/33, 120/120)
  • ✅ Both output documents validate, OAS 2.0 and OAS 3.x; fix --prune-schemas keeps the pointers

What's Changed

Other Changes

  • feat(joiner): consolidate equivalent schemas by reference instead of by deletion by @erraggy in #554
  • chore: prepare v1.66.0 release by @erraggy in #555

Issues Fixed

Full Changelog: v1.65.0...v1.66.0