Skip to content

fix: resolve entity local refs before inlining (python-sdk d650f0b) - #11

Merged
chaz8081 merged 1 commit into
mainfrom
entity-refs
Aug 21, 2026
Merged

fix: resolve entity local refs before inlining (python-sdk d650f0b)#11
chaz8081 merged 1 commit into
mainfrom
entity-refs

Conversation

@chaz8081

Copy link
Copy Markdown
Owner

Ports python-sdk PR #79, which fixes python-sdk#72 — the dangling-$ref defect this SDK reported. Both issues we filed upstream are now closed and tracked here.

Spec is unchanged at a2d8bf0; only the preprocessor moved, 35af25cd650f0b. 3 of 145 preprocessed files differ, and unresolvable local refs go 24 → 0.

The port

flatten_entity_reference copied ucp.json#/$defs/entity into three documents without resolving the entity's own document-relative refs, so #/$defs/version dangled in each new host. Upstream resolves those refs once, at extraction, while the body still sits in ucp.json; every later copy is self-contained and flatten_entity_reference itself never changed. preprocess.ResolveLocalRefs is that port, wired into Preprocess at the same point.

Three behaviours follow python rather than Go taste, because goldens are byte-compared against its output — sibling keys override the resolved target, an unresolvable ref is left alone rather than erroring, and the post-substitution walk continues with the caller's seen set. Documented at the code, including that the last one would recurse without bound on a genuinely cyclic local ref, in python too.

ResolveRef's fallback — the reason our models were never affected by the upstream bug — is deleted, since no unresolvable local ref remains. TestResolveRefDoesNotRescueDanglingLocalRefs pins the stricter rule in its place.

Breaking change

The inlined schema no longer routes through ucp.json's $defs:

- Version UCPVersion `json:"version"`
+ Version string     `json:"version"`

Same validation strength — the pattern is checked in place, and null still fails — but a breaking change for callers naming the type. Not tagged.

What the wider coverage found

The oracle can now compile the 71 targets it previously could not, which roughly tripled what the harness compares: 693 payloads across 157 types → 1,024 across 228, skips 23 → 3.

The first run of the wider corpus found a real gap. Fields the emitter carries as json.RawMessage are invisible to Validate, so a payload invalid only inside one is accepted — shopping/types/error_response.json's ucp (carried raw to break an import cycle) and capability's extends (no single Go shape).

It is reported and counted every run, not absorbed. Attribution is proved rather than assumed: every leaf of the oracle's rejection must fall inside a raw field, or the payload stays a mismatch. Deleting the field and revalidating would not work — these properties are required, so removal trades the real complaint for a missing-property one and every disagreement would look explained. TestRawFieldExplainsRejection pins both directions, including that a root-level complaint is never attributable.

Closing that gap is a design decision, not part of this port.

Verification

gofmt clean, go build, go vet, root tests and the conformance module all pass. grep -c '^require' go.mod = 0. TestPreprocessMatchesGoldens = 145 files, 0 mismatches. README example re-extracted and run verbatim; output matched.

Ports python-sdk PR #79, which fixes python-sdk#72 — the dangling-$ref
defect this SDK reported. flatten_entity_reference deep-copied
ucp.json#/$defs/entity into capability.json, payment_handler.json and
service.json without resolving the entity's own document-relative refs,
leaving 24 refs to "#/$defs/version" pointing at nothing.

Upstream resolves the entity's local refs once, at extraction, while the
body still sits in ucp.json; every copy made afterwards is self-contained
and flatten_entity_reference is untouched. preprocess.ResolveLocalRefs is
that port, called from Preprocess at the same point. Three behaviours are
faithful to python rather than to Go taste, because goldens are
byte-compared against its output: sibling keys override the resolved
target, an unresolvable ref is left alone rather than erroring, and the
walk continues with the caller's seen set. All three are documented at
the code.

ResolveRef's fallback, which resolved these refs against ucp.json and is
why our emitted models were never affected, is deleted: the corpus now
contains no unresolvable local ref. TestResolveRefDoesNotRescueDangling-
LocalRefs pins the stricter rule in its place.

Generated models change for the three affected packages: the inlined
schema no longer routes through ucp.json's $defs, so Version UCPVersion
becomes Version string with the pattern checked in place. Same validation
strength, but a breaking change for callers naming the type.

The oracle can now compile the 71 targets it previously could not, which
roughly tripled differential coverage — 693 payloads across 157 types to
1,024 across 228 — and the first wider run found a real gap. Fields the
emitter carries as json.RawMessage are invisible to Validate, so a
payload invalid only inside one is accepted. Reported and counted every
run rather than absorbed, with attribution proved from the oracle's own
error locations: every leaf must fall inside a raw field or the payload
stays a mismatch. Two properties are affected — error_response's ucp, to
break an import cycle, and capability's extends, which has no single Go
shape. Fixing that gap is a separate design decision.
@chaz8081
chaz8081 merged commit 3f42067 into main Aug 21, 2026
2 checks passed
@chaz8081
chaz8081 deleted the entity-refs branch August 21, 2026 01:13
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.

[Bug]: Inlined entity $refs are not rebased onto their origin document

1 participant