Skip to content

fix(downgrader): keep schema identifiers unique when inlining 3.2 to 3.1 - #29

Open
dinwwwh wants to merge 1 commit into
mainfrom
claude/inlined-schemas-duplicate-id-7b3056
Open

dinwwwh wants to merge 1 commit into
mainfrom
claude/inlined-schemas-duplicate-id-7b3056

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Sep 29, 2026

Copy link
Copy Markdown
Member

Previously, 3.2 → 3.1 could write the same $id, $anchor, or $dynamicAnchor into its output more than once. That happened when a schema was inlined in several places, or inlined while its original was still in the output. The official validator then rejected the 3.1 document ($id ... defined more than once), and Ajv refused to compile it (resolves to more than one schema). Now only the first copy of a schema keeps its identifiers, so the output validates as 3.1 and, chained, as 3.0.

Fixes

  • Several refs to the same components.mediaTypes entry, or to a schema inside it, no longer repeat its identifiers.
  • A ref to a moved itemSchema, or to a parameter list that lost a querystring entry, no longer duplicates the identifiers the original keeps.
  • Later copies drop the identifiers instead of turning into $ref: <$id>, so the 3.2 → 3.1 → 3.0 chain adds no reference that 3.0 cannot resolve.
  • A schema shared in a dereferenced input is still one shared object in the output, and keeps its identifiers.
  • Documents without repeated identified schemas convert exactly as before.

Performance

  • The new check costs one Set lookup and three in checks per schema.
  • A target holding identifiers is converted at most twice: the first copy keeps them, and the stripped second copy is shared by every later place. Other targets are still converted once and shared.

Testing

  • 491 tests pass with 100% coverage of packages/downgrader/src; lint and type-check pass.
  • The four new regression tests fail on main. They cover repeated media types, $ref with siblings, a moved itemSchema, a shifted parameter list, $dynamicAnchor, and the inline cache rule.
  • An end-to-end document with a repeated $id and $anchor passes the official validator as 3.1 and, chained, as 3.0.

Known limits

  • A copy that loses its $id resolves relative $refs inside it, such as #/$defs/x, against the enclosing base. This is documented in the README. Such documents were already invalid before this change.

Inlining shared one converted target across every reference site, and
re-converted targets whose original survives (a moved itemSchema, a
shifted parameter list), so $id, $anchor, and $dynamicAnchor appeared
several times and the official validator and Ajv rejected the 3.1 output.

Only the first copy of a schema now keeps its identifiers; later copies
drop them, so the chained 3.0 output also stays valid.
@codecov

codecov Bot commented Sep 29, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ No new issues found.

Reviewed changes — reviewed the full 3.2 → 3.1 identifier-uniqueness fix at head becce35, including the shared-engine cache change, the converter-side stripping, the README updates, and all new tests.

  • Context.identified set added — source-schema identity tracked per pass (downgrade) and per speculative probe (convertsToDrop).
  • inline cache gate — records ctx.identified.size before converting an inline target and caches the result only when the size is unchanged, so an identifier-bearing target is converted a second time (stripped) and shared by later sites.
  • finishSchema strips repeats — a repeated source schema loses $id / $anchor / $dynamicAnchor; the first-converted copy keeps them.
  • README — documents the stripping and the relative-$ref consequence for a copy that loses its $id.
  • Tests — new shared.test.ts inline-cache regression, two v3.2-to-v3.1.test.ts cases (repeated mediaTypes / $ref siblings, moved itemSchema + shifted parameter list), a new chained 3.1 → 3.0 e2e.test.ts document, and an $id/$anchor enrichment of the existing shared-schema test.

I verified vitest run (491 passed), eslint ., and pnpm type:check are clean at this commit. To confirm the new tests are real coverage rather than theatre, I reverted the finishSchema block and the inline cache gate: exactly the four new tests fail and the rest pass. I also probed a suspected over-strip edge (a schema first converted inside a parameter that is then dropped, and also present in components.schemas) — the surviving component copy kept its $anchor, since ctx.seen memoization reuses the converted schema even when the parameter is discarded, so no over-strip occurs.

One non-blocking observation for context: the identifier attaches to whichever copy converts first (document order), so with paths serialized before components the $id survives on the inlined copy rather than the component. Uniqueness still holds and the README's "first copy" wording covers it, so no change requested.

Pullfrog  | View workflow run | Using DeepSeek Flash (default — pick a model for stronger reviews) | 𝕏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant