Add a skill for declaring and sizing query-backed relationships - #123
Conversation
A query-backed `linksTo` / `linksToMany` reads like an ordinary
relationship and holds a bounded page of its query. That is the one thing
about it an author has to know, and nothing said it: counting
`field.length` over a match set larger than the page reports a
confidently wrong number, with no error and no warning in the card.
The new skill leads with that trap and its fix — read `totalMatchCount`,
which comes from the search's own count and no page bounds, so a
count-shaped rollup holds no rows at all. Then declaring a larger page
when the rows are genuinely needed, why asking for the maximum is not
free, `eager: false`, the singular-`linksTo` arity, and the rule of thumb
for when a field is the wrong tool and a search component is the right
one: a field is for a relationship the card reasons over, a component for
a list it renders.
Three existing places described this surface and are now consistent with
it:
`bxl-authoring` §7 told authors to aggregate over query-backed inverses
"for display and reporting" and warned only about staleness, so
`COUNT([Claims[]])` over 600 claims silently reported 500. It now carries
the bounded page alongside the staleness bullets, and its checklist item
names the same thing.
`glossary.md` and `boxel/references/relationship-loading-state.md` both
documented the relationship status as `{ isLoading, membership }`. Both
now carry the full shape, including that `isLoaded` means settled — which
a truncated set also is — so `isPartial` is what licenses a reduction.
Also registers the skill the way this repo requires: a `Skill/` card
whose `instructionsSource` points at the one SKILL.md rather than
duplicating the body, plus catalog lines in `index.md` and the glossary.
Staging Workspace Sync SuccessfulSuccessfully synced changes to staging workspace Sync Details |
There was a problem hiding this comment.
Pull request overview
Adds a new Boxel authoring skill that documents the semantics and sizing tradeoffs of query-backed linksTo/linksToMany relationships (bounded pages, counting via totalMatchCount, and partial/truncation detection), and updates existing docs to reflect the expanded relationship-membership status shape.
Changes:
- Introduces
skills/query-backed-relationships/SKILL.mdcovering bounded pages,totalMatchCount/isPartial, page sizing,eager: false, and singularlinksToquery semantics. - Updates glossary and BXL authoring guidance to explicitly warn about truncation and point to the new skill.
- Expands
getRelationshipMembershipState()documentation to include{ isLoaded, totalMatchCount, isPartial }.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| skills/query-backed-relationships/SKILL.md | New skill documenting query-backed relationship behavior, pitfalls, and sizing guidance. |
| skills/glossary.md | Adds glossary entries for totalMatchCount/isPartial, page ceilings, and registers the new skill. |
| skills/bxl-authoring/SKILL.md | Updates inverse-aggregation guidance to include bounded-page truncation and references the new skill. |
| skills/boxel/references/relationship-loading-state.md | Updates the documented getRelationshipMembershipState() return shape and guidance. |
| Skill/query-backed-relationships.json | Adds in-app SkillPlusMarkdown card pointing instructionsSource to the new SKILL.md. |
| index.md | Adds the new skill to the top-level skill catalog list. |
Suppressed comments (1)
skills/query-backed-relationships/SKILL.md:81
membershiphere reads like it contains card instances, butgetRelationshipMembershipState()’smembershipis a per-slotRelationshipState[](perrelationship-loading-state.md). Clarifying this avoids confusion between the relationship field value (this.everyActivity) vs the membership state object.
| `membership` | the rows the field is holding (`undefined` until resolved) |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Two things review caught, both mine.
The count example did `totalMatchCount ?? 0` while the same file warns,
twelve lines later, that an absent count means unknown and must not be
collapsed to zero. It now returns the value as authored — an empty field
says "no answer" where a nought claims one — and the "are there any?"
line says plainly that `?? 0` reads a genuine zero and an unknown alike,
so a caller who needs them apart branches on `undefined` first.
`skills/boxel/references/relationship-loading-state.md` gained the full
status shape but its `Skill/` twin still documented `{ isLoading,
membership }`. Nothing syncs those two trees, which is the invariant the
README calls out and which this change had already claimed to honor. The
twin now carries `isLoaded`, `totalMatchCount` and `isPartial`, and its
card summary says so rather than describing the spinner alone.
`bxl-authoring` needed no such treatment: its card points
`instructionsSource` at the shared `skills/` file, so one file already
serves both harnesses.
|
[Claude Code 🤖] Flagging the merge gate, since approval is the point where it's easiest to miss: the API this documents isn't on Merging syncs to the staging realm, so landing it before then hands authors a skill telling them to call something that isn't there. Safe to merge once those land — checking Generated by Claude Code |
Four files describing relationship status were edited on both sides. The substantive one: main documents `isLoaded` as "the one to gate on before reading a rollup over the field and trusting the number", which is the claim this branch exists to correct — a truncated set is settled too, so `isLoaded` says membership is final, not that it is complete, and `isPartial` is what licenses a reduction. The merged bullet keeps main's descriptive wording and drops the guarantee, with `isPartial` documented directly beneath it. Two of main's corrections are taken as-is. The glossary's per-slot pointer now names `getRelationshipMembershipState`, which is the function that exists — `getRelationship` is not exported. And the observe-only section's example becomes main's rewrite, where a count gated on `isLoaded` never resolves the field it counts; the shared reference file already carries that version, so the `Skill/` twin follows it rather than diverging. The card summary covers both sides: `isLoading`, `isLoaded`, the `totalMatchCount` / `isPartial` pair, and main's `eager` option. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CG5z1n3z6iy7Sr8Fv5XjBf
The status table described `membership` as "the rows the field is holding", which reads as the linked cards. It is a `RelationshipState` per slot, so an author following that description would map over it expecting instances and find none of the fields they asked for. The row now names the type, and a line beneath it points at the field getter for the cards and at the defensive-traversal reference for the slot states. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CG5z1n3z6iy7Sr8Fv5XjBf
A query-backed
linksTo/linksToManyreads like an ordinary relationship and holds a bounded page of its query. That is the one thing about it an author has to know, and nothing here said it.The trap it leads with
The fix isn't a bigger page — it's not holding rows at all.
totalMatchCountcomes from the search's ownCOUNT(*), which no page bounds:Then: declaring a larger page when the rows are genuinely needed, why asking for the maximum isn't free (the page is a cost paid on every resolution of every instance, and each row's own query fields resolve in the next layer of the same pass),
eager: false, the singular-linksToarity, and a rule of thumb — a query-backed field is for a relationship the card reasons over; a search component is for a list it renders.Three existing places described this surface and were wrong or incomplete
This is the part worth reviewing closely — the new file is additive, these are edits.
skills/bxl-authoring/SKILL.md§7 told authors to aggregate over query-backed inverses "for display and reporting" and warned only about staleness. Nothing about truncation — soCOUNT([Claims[]])over 600 claims silently reported 500. The bounded page now sits alongside the staleness bullets, withtotalMatchCount/isPartialas the check before reducing, and checklist item 5 names it too.skills/glossary.mdandskills/boxel/references/relationship-loading-state.mdboth documented the status as{ isLoading, membership }. Both now carry the full shape, including the trap thatisLoadedmeans settled — which a truncated set also is — soisPartialis what licenses a reduction. The glossary also gains entries fortotalMatchCount/isPartialand the page ceiling.Encouragingly,
boxel-patterns/patterns/show-count-tiles-from-queryalready got this right (results.meta.page.total, "not the entries"), so the correct instinct existed for the search-component surface and was missing only for query fields. No change needed there.Repo conventions followed
boxel.kind: skillfrontmatter, nested (the silent trap inboxel-skill-authoring).Skill/source-code-editing.jsondoes it —Skill/query-backed-relationships.jsonpointsinstructionsSourceat../skills/query-backed-relationships/SKILL.mdrather than duplicating the body, so one file serves both harnesses.index.mdandskills/glossary.md(both the term entries and the skill registry).../bxl-authoring/SKILL.md), matching the existing files; no absolute realm URLs.Generated by Claude Code