refactor(downgrader): rewrite converters on one table-driven engine - #26
Conversation
Both converters are now field tables that convert or drop each field, plus one rule that inlines any local $ref the conversion would leave dangling. A 3.1 to 3.0 schema never rejects a value its source accepts: `not` over a loosened operand is removed and `oneOf` with a loosened branch becomes `anyOf`. References into every removed or renumbered part are inlined, and 3.2 to 3.1 output validates as 3.1.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
There was a problem hiding this comment.
ℹ️ Two documentation-precision suggestions — no code issues found. The engine rewrite and both converters check out.
Reviewed changes
Read the entire diff end-to-end, ran the suite, typecheck, and lint, and probed the two load-bearing invariants (loosening propagation and reference inlining) with specialists.
- One table-driven engine —
shared.tsreplacesconvertRecord/deepClone/mapRecord/mapArray/convertInlinedwithdefineFields+convertObject(value, ctx, fields, finish), plus a fixpointdowngrade()driver that re-runs whiledanglesInfinds references whose targets the conversion removed or shifted. - Loosening tracking —
LOOSE/isLoose/hasLoose/isLooseSchemaremove anotwhose operand was loosened and rewrite a loosenedoneOftoanyOf, so dropping 2020-12 keywords no longer tightens (v3.1-to-v3.0.ts:187-315). - Expanded reference inlining — references into
$defs,components.mediaTypes,queryoperations, and shifted parameter lists are now inlined, not only those intowebhooks/components.pathItems. - 3.2 → 3.1 schema conversion —
xml.nodeType→attribute/wrapped,discriminator.defaultMappingremoved, and parameter/headerexample/examplesbesidecontentremoved;downgradeSchemaV32ToV31is no longer a plain clone. - Documented behavior changes — Links into removed parts are removed rather than rewritten to an
operationId; inlined Reference Objectsummary/descriptiondropped; reference loops left as written. - Tests — 483 pass with a new
expectNoNewDanglingRefscorpus assertion. I verifiedpnpm test,pnpm type:check, andpnpm lintall pass, and that no stale references to the removed exports remain.
Both specialist probes came back clean for in-scope behavior: the for(;;) loops in mergeRef and downgrade are structurally bounded (cyclic refs are gated out by aliasEnd → isInlinable, and dangling grows monotonically), and reference-free schemas propagate looseness exhaustively.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏
…k path items in components Rewrites the README contract, References, and Known limitations sections so every claim matches the converters, and moves Known limitations under its own heading. A Path Item $ref into a removed operation inside a components.callbacks entry is now inlined instead of left dangling.
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Incremental review of 0142bc5 since the prior pullfrog review at 516cab6. Read the range-diff and the full PR context; ran pnpm test (484 pass), pnpm type:check, and pnpm lint (all clean).
- Callback Path Item recognition —
isPathItemPointernow recognizes#/components/callbacks/{cb}/{expr}(the 4-token case) as a Path Item, so a Path Item$refinto a callback of a removedquery/additionalOperationsoperation is inlined instead of left dangling. The relaxedtokens.length > 3plus the explicitfirst === 'components'branch only adds that one case; thex-exclusion still holds. - Test coverage — the new
v3.2-to-v3.1.test.tscase exercises the 4-token branch, recursion through aqueryoperation, and thex--prefixed callback key staying untouched. It fails against the previous code, so it is real coverage. - README rewrite — tightened wording and reorganized the contract, References, and Known limitations sections. Both prior doc threads are addressed: the "adds no dangling references" guarantee now defers to References (which lists looping
$refchains), and thenot/oneOflimitation now says "reaches a loosened schema through a$refkept in the output". - New documentation claims checked — the added "except as
additionalProperties" exception matchesSCHEMA_FIELDS.additionalPropertieskeeping booleans as-is, and the content-map$refremoval wording matchesconvertContentEntry'sinline/skipAliasesbehavior.
DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Rewrites
@openapi-spec/downgraderon one small table-driven engine. 3.1 → 3.0 no longer makes a schema stricter when it drops keywords undernotoroneOf, and both converters inline or remove more$refs that used to dangle. Speed stays within 7% of main.Fixes
notwhose operand lost a keyword and turns aoneOfwith such a branch intoanyOf. For example,not: { prefixItems: … }used to becomenot: {}, which rejects everything.$refs into$defsand other dropped or moved schema parts.$refs into callbacks ofqueryandadditionalOperationsoperations.mappingentries that point into removed parts, such as aqueryoperation or$defs.content, which failed 3.1 validation. It also convertsxml.nodeTypeand removesdiscriminator.defaultMapping, including indowngradeSchemaV32ToV31, which used to only clone.Behavior changes
operationRefpoints intowebhooksorcomponents.pathItemseven when that operation is inlined intopaths. Main rewrote it to anoperationId.summaryanddescriptionoverride the target's.$refloop through a removed part is left as written, so it dangles. On main, 3.2 → 3.1 deleted it and 3.1 → 3.0 rewrote it into a self-reference.oneOfintoanyOfand removes a recursivenotin dereferenced input with object cycles, and nests a malformedallOfinstead of keeping or dropping it.{}one level earlier or later.Performance
On a 500-path document, 3.2 → 3.1 is slightly faster, and 3.1 → 3.0 is up to 7% slower because it now tracks loosened schemas.
Testing
operationFieldstest is removed with its helper, 6 pointer tests are merged into 4, and about 25 expectations change for the behavior above.