Skip to content

Resolve relative references in serialized card documents against each resource's own id#5578

Open
habdelra wants to merge 1 commit into
mainfrom
cs-12287-linked-cards-relative-adoptsfrom-module-is-resolved-against
Open

Resolve relative references in serialized card documents against each resource's own id#5578
habdelra wants to merge 1 commit into
mainfrom
cs-12287-linked-cards-relative-adoptsfrom-module-is-resolved-against

Conversation

@habdelra

@habdelra habdelra commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Serialized card documents carry relative references — a card's adoptsFrom module and its relationship links — and those references were being resolved against different bases depending on how the document was assembled. The single-card path rebased included[] refs onto the primary document URL; the search / prerendered path (which the host actually loads cards through) left them relative to each resource's own id. A consumer can't tell which base a given ref used, so no single resolution rule was correct for both.

The concrete symptom: when the host hydrates a linked card out of a parent card's document (the linked resource rides in included[]), a linked instance that lives in a subdirectory — instances are stored under a <CardType>/ container, so their id is a directory deeper than the module that defines them — had its relative adoptsFrom.module resolved against the parent card's base. It landed one directory off, fetched a module URL that doesn't exist, threw could not find card, and presented an existing, cleanly-indexed card as Card Not Found.

The rule

Every reference in a resource — its adoptsFrom module and its relationship/instance links — is relative to that resource's own id, independent of the document that delivers it. One rule for the document's primary resource, for each included[] resource, and for every member of a collection response. A consumer inverts it the same way everywhere: resolve a resource's references against that resource's own id (data.id / included[].id), never against the enclosing document.

JSON:API gives no explicit guidance here — its relative-URL handling is scoped to links, and adoptsFrom lives in meta, which the spec treats as opaque; the closest binding authority it defers to, RFC 3986 §5.1, establishes one base per document with no per-embedded-resource base. So the convention is ours to define, and own-id is the one that makes sense and is the only base that works uniformly across single-card documents, included[], and collections (a collection has no single primary to rebase against).

Changes

  • relativizeResource relativizes every reference in a resource — module deps and instance/relationship URLs alike — against the resource's own id. For a document's primary resource the base is unchanged; only included[] / collection members shift.
  • cardClassFromResource and _updateFromSerialized resolve a resource's adoptsFrom and relationship links against its own id when saved, falling back to the caller's base only for an id-less contained field.
  • Crystal-clear comments at the serializer and the resolver state the single rule, since it's subtle and easy to re-break.

One deliberate artifact of applying the rule uniformly: a resource's own links.self is also relativized against its own id, so an included resource's self-link becomes self-referential (e.g. ./owner rather than the previous document-relative ../dir/owner). Consumers resolve it against included[].id, so it round-trips correctly.

Because the wire form of included[] references changes (own-id-relative rather than document-relative), served-output and save-payload expectations across the suite are updated to match the rule.

Test

A new deserialization test hydrates a two-level link graph — a root card whose linksToMany targets live in a subdirectory, each with a further linksTo into a deeper subdirectory, every linked resource carrying an own-id-relative adoptsFrom. It fails on the old resolution base (http://test-realm/test/section not found) and passes with the fix.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 3f4de21602

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/base/card-serialization.ts
@habdelra
habdelra marked this pull request as draft July 22, 2026 19:32
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Preview deployments

Host Test Results

    1 files      1 suites   2h 52m 50s ⏱️
3 596 tests 3 581 ✅ 15 💤 0 ❌
3 615 runs  3 600 ✅ 15 💤 0 ❌

Results for commit 0ac14cc.

Realm Server Test Results

    1 files  ±0      1 suites  ±0   11m 43s ⏱️ -11s
1 933 tests +9  1 933 ✅ +9  0 💤 ±0  0 ❌ ±0 
2 012 runs  +9  2 012 ✅ +9  0 💤 ±0  0 ❌ ±0 

Results for commit 0ac14cc. ± Comparison against earlier commit c46008f.

@habdelra
habdelra force-pushed the cs-12287-linked-cards-relative-adoptsfrom-module-is-resolved-against branch 3 times, most recently from fdc7854 to c46008f Compare July 22, 2026 23:26
@habdelra
habdelra marked this pull request as ready for review July 22, 2026 23:28
@habdelra
habdelra requested a review from Copilot July 22, 2026 23:29
@habdelra habdelra changed the title Resolve a linked card's relative adoptsFrom against its own id Resolve relative references in serialized card documents against each resource's own id Jul 22, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR standardizes how relative references inside card resources are interpreted and produced: all resource-local references (including meta.adoptsFrom.module and relationship/instance URLs) are treated as relative to the resource’s own id, regardless of whether the resource is the primary data or arrives via included[].

Changes:

  • Update relativizeResource to relativize all URLs/module refs against each resource’s own id base (rather than the enclosing document’s base).
  • Update host-side deserialization (cardClassFromResource, _updateFromSerialized) to resolve adoptsFrom and relationship links against the resource/instance id when available.
  • Refresh expectations across realm-server + host integration tests and add a new nested-link deserialization test for the reported failure mode.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/runtime-common/realm-index-query-engine.ts Change relativization base from document primary URL to per-resource URL; expanded contract comments.
packages/base/card-serialization.ts Ensure adoptsFrom.module resolution prefers resource.id over caller context when present.
packages/base/card-api.gts Resolve adoptsFrom and relationship links against the instance’s own id when available; fall back to threaded context otherwise.
packages/realm-server/tests/card-endpoints-test.ts Update served-document expectations for relationship links.self and adoptsFrom.module under the new base rule.
packages/host/tests/integration/realm-test.gts Update integration expectations and clarify included-card adoptsFrom.module semantics.
packages/host/tests/integration/realm-indexing-test.gts Update indexing-related expectations for links.self relative forms.
packages/host/tests/integration/components/serialization-test.gts Add a regression test covering nested linked-card hydration with own-id-relative adoptsFrom.
packages/host/tests/integration/components/card-copy-test.gts Update copied-card included adoptsFrom.module expectation to own-id-relative form.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread packages/runtime-common/realm-index-query-engine.ts Outdated

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c46008f0dc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/runtime-common/realm-index-query-engine.ts
Every reference in a card resource — its adoptsFrom module and its
relationship/instance links — is serialized relative to that resource's own
id, independent of the document that delivers it. The one rule holds for a
document's primary resource, for each included[] resource, and for every
member of a collection response, so a resource carries the same relative refs
no matter how it is delivered; a consumer inverts it the same way everywhere,
resolving a resource's references against its own id (data.id / included[].id).

Previously included resources were relativized against the primary document
URL, and the host threaded that base down into nested link deserialization, so
a linked card in a subdirectory resolved its module one directory off and an
existing, cleanly-indexed card surfaced as Card Not Found.

- relativizeResource relativizes every reference against the resource's own id.
- cardClassFromResource and _updateFromSerialized resolve a resource's
  adoptsFrom and relationship links against its own id when it is saved,
  falling back to the caller's base only for an id-less contained field.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@habdelra
habdelra force-pushed the cs-12287-linked-cards-relative-adoptsfrom-module-is-resolved-against branch from c46008f to 0ac14cc Compare July 22, 2026 23:50
@habdelra
habdelra requested a review from a team July 23, 2026 00:21

@richardhjtan richardhjtan left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[Claude Code 🤖] Reviewed through the lens of the JSON:API relative-reference contract between the realm server's serializer and the host's deserializer — the part of this codebase where a wrong base silently produces a relative string that still parses, so the failure surfaces later as a runtime "not found" rather than a type error.

Bottom line: no blocking issues. The change is internally consistent with the rest of the system and the new regression test exercises the exact failure mode described.

What lands right

  • relativizeResource (packages/runtime-common/realm-index-query-engine.ts) now relativizes every reference in a resource — instance/relationship URLs and module deps alike — against that resource's own id rather than the document's primary URL. Its only call site is cardDocument() in the same file, which realm.ts uses uniformly for the GET, POST-create, and PATCH-save response bodies, so read and write responses can't drift apart.
  • Collection responses (/_search, built in searchEntries()) never went through relativizeResource — they serve pristine_doc close to as-is — and the indexer resolves a card's own adoptsFrom.module relative to its own id at index time. So the claim that collections "already carry the own-id-relative form natively" checks out: this PR brings the single-card included[] path in line with a convention the rest of the system already followed, rather than introducing a new one.
  • On the client, cardClassFromResource (packages/base/card-serialization.ts) and the relationship/override resolution in _updateFromSerialized (packages/base/card-api.gts) flip the same priority: resource id before the threaded relativeTo. That fallback matters only for id-less contained fields, which correctly keep inheriting the parent's base since they have no id of their own.
  • The new test in packages/host/tests/integration/components/serialization-test.gts hand-builds a two-level linksToManylinksTo chain with own-id-relative adoptsFrom in included[]. Under the old priority (relativeTo ?? resource.id) the linked resources would resolve their module against the root card's id instead of their own — reproducing the reported one-directory-off "Card Not Found" — and pass only under the new order. It pins the actual regression, not just document shape.

On the two points already raised and closed in review threads

  • The mounted-realm double-rebase conflict was real against the intermediate commit and was resolved by updating the affected test's expectation rather than the resolver. Tracing the fixture's directory layout against relativizeResource's math confirms the new expected value is the correct relative form, so the reconciliation holds.
  • The descendant-link round-trip gap (a resource whose id is a path-prefix of a link target loses a segment on resolve, because the URL-form base branch resolves with standard new URL(ref, base) semantics, which drop a non-slash-terminated base's last segment) reproduces when traced through relativeReference/resolveRRIReference, and is correctly assessed as unreachable today since card ids are flat (<CardType>/<name>) — no card id is ever a prefix of another. It predates this PR (the primary resource already relativized its own links against its own id before this change) but this PR does widen which resources run through that math, from just the primary to every included[] resource. Worth a look if relativeReference is ever asked to round-trip a hierarchical id space; not something this PR needs to address.

No further recommendations — the diff is narrow, the added docstrings state the rule once and point at where it's enforced, and the four surfaces checked (single-card serve, /_search, client deserialization, index-time storage) agree with each other.

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.

4 participants