Skip to content

fix: retain the source root when selecting a $defs entry so self-root refs resolve - #47

Open
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix-select-def-self-root
Open

fix: retain the source root when selecting a $defs entry so self-root refs resolve#47
vishkaty wants to merge 1 commit into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix-select-def-self-root

Conversation

@vishkaty

@vishkaty vishkaty commented Aug 5, 2026

Copy link
Copy Markdown

Fixes #45.

What

validate --def <name> (and resolve --def) stack-overflowed when the selected $defs entry reaches a reference to the source file root. select_def built a wrapper that kept $schema, a $ref: "#/$defs/<name>" and the file's $defs, but dropped the file's $id, so a $ref: "#" inside the selected def resolved to the wrapper root — whose only content is the $ref back into the def — and recursed unboundedly. On the shipped payment_instrument.json, --def selected_payment_instrument aborts.

Fix

When (and only when) the selected def's reachable subgraph references the source root, the wrapper embeds the whole source file as an $id'd resource and roots the selection via <root_id>#/$defs/<name>, so #, escaping fragment pointers, and absolute self-$id refs all resolve to the real source root (the 2020-12 embedded-resource rule). Simply carrying $id onto the old wrapper does not work — # would still bind to the wrapper root and the cycle survives; the embedded resource is what gives # its intended target. When the file has no $id, a stable content-hashed URN is synthesized, guarded against colliding with any $id the source already declares.

Root-reference detection is scoped to the selected def's reachable subgraph (a visited-set walk following #/$defs/X edges from <name>), so a def with no root dependency of its own keeps the existing plain wrapper byte-for-byte, even when an unrelated sibling def in the same file carries a self-root ref. Transitive dependencies are still detected (a selected def that reaches the root through a def it references embeds correctly).

Scope (stated honestly)

This fixes the $ref: "#" self-root case that #45 reports. It does not fix two pre-existing, separate --def/bundler behaviors (present on main before this change, unchanged by it): a directly self-recursive #/$defs/<name> ref still overflows (the general bundler-recursion class, which overlaps #46), and a root-level $anchor ref reports fragment not found. Those are out of scope here.

Verification

  • The #45 crash is gone on the real shipped schema: a complete selected payment instrument validates Valid; an incomplete one returns a clean "id" is a required property rejection; neither crashes. (Note: the exact payload in the issue omits the schema-required id, so post-fix that command correctly exits 1 with a validation error rather than 0 — the crash it demonstrated is fixed regardless of payload validity, since it occurred during schema construction.)
  • New tests fail on the pre-fix code and pass after (self-root accept/reject, no-$id synthesis, the sibling-scope case, and a transitive-embed case), each kill-tested. Full suite 326 pass.
  • resolve --def output for 8 unaffected defs is byte-identical to main. Derived resolve --op / validate --schema paths are untouched.
  • cargo fmt --check, cargo clippy --all-targets -- -D warnings, cargo build --release, and the pinned pre-commit are all clean.

Happy to adjust the approach if you prefer one of the alternatives.

… refs resolve

Observed: `validate --def selected_payment_instrument` against the shipped
shopping/types/payment_instrument.json crashes the process with a stack
overflow. The def is `allOf: [{"$ref": "#"}, ...]`, and select_def's
wrapper ($schema + $ref + a copy of $defs) drops the source file's $id
and root, so "#" re-binds to the wrapper root — whose only content is the
$ref back into the def, an unbounded resolution cycle.

Expected: selecting a def that references its file root validates normally,
with "#" meaning the source file's root, as 2020-12 resolves it for
embedded resources.

When the selected def's reachable subgraph (the def itself plus every
sibling def it reaches through #/$defs/... pointers) references the source
root — self-root "#", fragment pointers escaping $defs, or absolute refs
to the file's own $id — the wrapper now embeds the whole source file as an
$id'd $defs resource and roots the selection through that $id,
synthesizing a stable content-hashed URN when the file has no $id (guarded
against colliding with any $id the source already declares). Detection is
scoped to the selected def's reachable subgraph, so a def with no root
dependency of its own keeps the existing wrapper byte-for-byte — including
when an unrelated sibling def carries a self-root ref — and emitted
`resolve --def` output for such defs is unchanged.

Fixes Universal-Commerce-Protocol#45
@damaz91 damaz91 added the status:needs-triage Signal that the PR is ready for human triage label Aug 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

status:needs-triage Signal that the PR is ready for human triage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

validate --def crashes with a stack overflow when the selected $defs entry contains $ref: "#" (shipped payment_instrument.json)

2 participants