v1.65.0
🏷️ Deduplication Learns Whose Name It Is Removing
Semantic deduplication consolidates equivalent schemas, and until this release it consolidated two populations of names as one. Some names the joiner invented itself, resolving a collision between documents. Others an author wrote and published. Only the second kind is visible to anyone consuming the joined document, and for an aggregator generating client SDKs from it, removing one is a breaking change.
There was no way to ask for the first without the second. Turning the pass off entirely left the invented aliases in the published document.
oastools join --schema-strategy deduplicate-or-rename --equivalence-mode deep \
--semantic-dedup [--dedup-scope generated-only] pets.json store.json| definition | who wrote it | all (default) |
generated-only |
|---|---|---|---|
Pet.Category |
declared | kept | kept |
Pet.Category_store |
a collision rename | kept | kept |
store.Inventory |
declared | kept | kept |
store.Stock |
declared | removed | kept |
store.Stock and store.Inventory are the same shape and no schema tree holds them apart, so the default scope merges them and the name the author published is gone.
What the scope does not change
generated-only is not "consolidate less". A group mixing the two populations still consolidates: the invented names fold into the declared name that outranks them, which is the point of running the pass at all.
| equivalent group | all |
generated-only |
|---|---|---|
Common, Api_Common |
Common |
Common |
Zebra, Api_Common |
Zebra |
Zebra |
Inventory, Stock |
Inventory |
Inventory, Stock |
The equivalence comparison, the rules deciding which name survives, and the guarantee that names one schema tree references stay apart are all identical under both scopes. Only a second declared name in a group is treated differently.
📋 Sizing it before you choose
Diffing the joined document against its inputs cannot tell an invented alias from a declared name some other pass removed. Both are simply absent. --dedup-report says which was which:
Consolidations (1):
store.Inventory (declared)
<- store.Stock (declared)
In Go, JoinResult.Consolidations carries the same thing per consolidation: the surviving name, what folded into it, and whether a rename produced each. Off by default, since a large join consolidates enough names that recording them all is worth asking for.
config.SemanticDeduplication = true
config.DeduplicationScope = joiner.DeduplicationScopeGeneratedOnly
config.DeduplicationReport = true🔧 A rename could take a name a document declared
This one changes output, and it is the reason the scope above can be trusted.
Choosing a name for a renamed schema checked the names merged so far. A document merged later could declare exactly that name, and then two things went wrong at once. The invented name kept the spelling and the author's own declaration was pushed aside to a suffix. Worse, the surviving name was recorded as invented even though a document had declared it, so semantic deduplication ranked a published name as an alias and folded it away.
a.json and b.json collide on Common, so b's copy is renamed Common_b
c.json declares Common_b itself
before: Common, Common_b (b's schema), Common_b_c (c's own schema, displaced)
after: Common, Common_b (c's schema), Common_b_2 (b's renamed schema)
Neither symptom announced itself. The document validated and every reference resolved. A rename now avoids every name any source document declares, including documents not merged yet, so the set recording which names the joiner invented is finally accurate. --dedup-scope generated-only reads that set, which is why this fix had to land first.
If you join documents whose rename template can produce a name another document declares, this release changes your output. The declared name wins and the renamed schema takes the next free suffix.
🔒 User Impact
- No breaking API changes. Everything added is new and off by default:
DeduplicationScope,DeduplicationReport,WithDeduplicationScope,WithDeduplicationReport,JoinResult.Consolidations,Consolidation,FoldedName, and the--dedup-scope/--dedup-reportflags. The MCP join tool gainsdedup_scope - One behavior change, described above: a join whose rename target collides with a name a later document declares now names schemas differently than v1.64.1
- No dependency changes
- Existing calls that never set a scope behave exactly as they did
📊 Quality Metrics
- ✅ 11,270 tests passing
- ✅ Zero vulnerabilities (
govulncheckclean) - ✅ Benchmarks show no regression: the new work costs 2 allocations per join, and nothing at all when a join renames nothing
- ✅ Doc examples compiled rather than reviewed by eye
What's Changed
Other Changes
- fix(joiner): reserve every declared schema name before a rename picks one by @erraggy in #548
- docs: stop citing a status-code constant that does not exist by @erraggy in #549
- feat(joiner): scope semantic deduplication by name provenance by @erraggy in #550
- chore: prepare v1.65.0 release by @erraggy in #552
Issues Fixed
Full Changelog: v1.64.1...v1.65.0