Skip to content

fix: stop crawl external refs and the local base retriever from doubling a path segment - #72

Open
vishkaty wants to merge 2 commits into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/local-base-doubled-schemas-segment
Open

fix: stop crawl external refs and the local base retriever from doubling a path segment#72
vishkaty wants to merge 2 commits into
Universal-Commerce-Protocol:mainfrom
vishkaty:fix/local-base-doubled-schemas-segment

Conversation

@vishkaty

Copy link
Copy Markdown

Root cause

Introduced by #66, commit 9f91c3d. Bisected the range between 9b5c320 (good) and b52518f (bad) commit by commit; 9f91c3d is the first bad one, and the two docs only commits after it do not change the result. #66 is otherwise a sound rework that closed #43, #45 and #46; this is one layout shape it did not cover.

crawl_external_refs in src/loader.rs resolves the relative refs of a fetched document against its declared $id (fetched_base), which is correct on its own, but upstream jsonschema::dereference resolves the relative refs of a fetched resource against the URI it was retrieved under, not its declared $id. When a relative ref is written correctly for the referrer disk location, but the referrer $id nests it one level deeper than a sibling directory (discovery beside schemas, both under https://ucp.dev/schemas/...), the URL join off the retrieval position doubles the shared segment. Only the correct, undoubled key from our own crawl had ever been registered, so upstream dereference fails with not present in a registry the moment it independently rederives the doubled key for a further hop.

A related site: the UcpRetriever explicit schema local base plus schema remote base mapping (step 1) hits the same doubled join and returns a hard file not found error instead of falling through to the learned anchor relocation in step 3, which already recovers from this mismatch for the no remote base case.

Fix

Both changes are additive.

  • crawl_external_refs: when the declared $id of a fetched document differs from the URI it was retrieved under, crawl the refs of that document a second time using the retrieval URI as base, in addition to the existing pass using its declared $id, so resources reachable through either base end up registered. The second pass is guarded on that difference, so it is a no op when the two agree.
  • UcpRetriever::retrieve: only return the explicit mapping candidate when the mapped path exists on disk, falling through to the remaining steps otherwise, exactly as an unmapped URI already does. One behavior delta to name: a mapped URI whose local file is missing used to fail hard at the mapping step and now takes the same fallthrough as every unmapped URI, which with the remote feature can end in a network fetch.

Tests

Added to tests/conformance_test.rs, matching the bundle and oracle_is_valid idiom already used in the file (accept and reject verdicts checked through the jsonschema crate, never our own emitted schema text):

  • sibling_directory_nested_under_id_resolves_without_doubling reproduces the shape through bundle_refs, the path exercised by validate --schema --schema-local-base with no remote base.
  • sibling_directory_nested_under_id_resolves_without_doubling_with_url_mapping covers the same shape through bundle_refs_with_url_mapping (schema local base plus schema remote base, also used internally by compose), which had no direct test before this change; its only prior coverage came through CLI fixtures whose declared $id and disk layout agree.

Each fails before the fix with the failure shape of its own site: the first with the same not present in a registry error the real repro produces, the second with a doubled path file not found error at the explicit mapping step. Both pass after. Reverting src/loader.rs alone with the tests kept turns both red again with those same shapes, confirming they test the fix rather than passing on their own.

Verification

Fixes #71.

…ts one level deeper than disk

Adds two cases matching the shape reported against a real ucp v2026-04-08
source checkout: discovery/profile_schema.json declares an $id that nests
discovery under schemas in URL terms, while the two sit as siblings on
disk. A relative ref written correctly for disk (../shared/mid.json in
the fixture, ../schemas/ucp.json in the real tree) doubles the shared
segment when resolved by URL join against the declared $id instead.

Both cases fail on current main, each with the failure shape of its own
site:
- sibling_directory_nested_under_id_resolves_without_doubling exercises
  bundle_refs, the path used by validate --schema --schema-local-base
  with no remote base set, and fails with the Resource ... is not
  present in a registry shape the real repro produces.
- sibling_directory_nested_under_id_resolves_without_doubling_with_url_mapping
  exercises bundle_refs_with_url_mapping (--schema-local-base plus
  --schema-remote-base, also used internally by compose) and fails with
  a doubled path file not found error at the explicit mapping step. That
  entry point had no direct test before this; its only prior coverage
  came through CLI fixtures whose declared $id and disk layout agree.
…ing a path segment

Introduced by Universal-Commerce-Protocol#66 (9f91c3d): crawl_external_refs resolves the relative
refs of a fetched document against its declared $id, which is correct in
isolation, but upstream jsonschema::dereference resolves those same refs
against the URI the document was retrieved under, not its declared $id.
When a relative ref is written correctly for the referrer disk location
but the referrer $id nests it one level deeper than a sibling directory,
the URL join off the retrieval position doubles the shared segment, and
only the correct, undoubled key from our own crawl had ever been
registered, so upstream dereference fails the moment it independently
rederives the doubled key for a further hop. Universal-Commerce-Protocol#66 is otherwise a sound
rework that closed Universal-Commerce-Protocol#43, Universal-Commerce-Protocol#45 and Universal-Commerce-Protocol#46; the four Draft 2020 12 conformance
cases it added still pass with this fix.

A related site: the UcpRetriever explicit --schema-local-base plus
--schema-remote-base mapping hits the same doubled join and returns a
hard file not found error instead of falling through to the learned
anchor relocation that already recovers from this mismatch for the no
remote base case.

Fix, both changes additive:
- crawl_external_refs also crawls the refs of a fetched document under
  the URI it was retrieved by, in addition to its declared $id, when the
  two differ, so resources reachable through either base end up
  registered. Guarded on that difference, so it is a no op when the two
  agree.
- UcpRetriever::retrieve only takes the explicit mapping candidate when
  the mapped path exists, falling through to the remaining steps
  otherwise, exactly as an unmapped URI already does. A mapped URI whose
  local file is missing therefore now reaches the same fallthrough,
  including the remote fetch step when the remote feature is built in,
  instead of failing hard at the mapping step.

Verified against the exact reported command on a real ucp v2026-04-08
source checkout (exit 2 before, exit 0 after). cargo test --all-targets:
355 passed, 0 failed, 0 ignored. The Universal-Commerce-Protocol#45 and Universal-Commerce-Protocol#46 regression fixtures and
the four Draft 2020 12 conformance cases Universal-Commerce-Protocol#66 itself added still pass.
The released v2026-08-25 tree is unaffected: lint over its source
directory and def selection on common/types/payment_instrument.json
behave identically on main and on this branch in both the accept and the
reject direction. Reverting src/loader.rs alone with the tests kept
turns both new tests red, each with the failure shape of its own site,
confirming they test the fix rather than passing on their own.
cargo fmt --check and cargo clippy --all-targets -- -D warnings both
clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

4 participants