Model each product as a small graph. Nodes are the things facts can belong to: the product group, its parts (a hood, a jacket, a battery), each purchasable variant, each seller’s offer, and the documents your facts come from. Edges say how facts move between them: inherits, overrides, excludes, scoped to and derived from.
Three rules follow. A fact reaches a variant only through an inheritance edge that no exclusion blocks. A fact scoped to a part never answers a question about the whole product. An offer fact (price, stock, returns) never passes from one seller to another.
This is our proposed model for your own data. No platform reads it, and it doesn’t predict which sibling an agent will pick; that’s a separate experiment.
What a level table can’t express
If you haven’t mapped your facts to levels yet, start with the variants guide. Its matrix puts each fact at the group, variant or offer level, and it catches most leaks. This page is about the leaks a table can’t describe, because they’re about relationships, not levels:
| Leak | Why a level table misses it | Edge that fixes it |
|---|---|---|
| A part’s rating becomes the product’s | “Hood insulation rated to -30°C” sits at the group level, since every size has the hood. The table says “group” and stops | Scoped to: the fact attaches to the hood, which is a part of every variant |
| A group fact that’s true for nine sizes out of ten | Schema.org reads group properties as shared by every variant. There’s no “all except” | Excludes: an explicit edge from the group fact to the one variant it doesn’t cover |
| One seller’s terms show up on another’s offer | Price and returns are “offer level” in the table, but a catalog can cluster offers from several sellers under one product | No edge between offers. Each offer inherits only from its own seller’s policies |
| A generated summary drifts from its source | The table records where a fact lives, not where a copy came from | Derived from: the copy points back to the claim it summarises, so the two can be compared |
The graph
Nodes
| Node | What it is | ID to use |
|---|---|---|
| Group | The product family, never sold on its own | Your product ID; productGroupID in markup |
| Component | A part every variant has, or some variants have: hood, pants, battery, lid | Your own stable label, such as jacket/hood |
| Variant | The exact item that ships | SKU or variant ID; GTIN where assigned |
| Offer | One seller’s terms for one variant: price, stock, shipping, returns | Seller ID plus variant ID |
| Policy | A seller’s store-wide terms: returns, shipping zones | Seller ID plus policy version |
| Claim | One checkable statement, with value, unit and condition | A claim ID, as in our claim-record guide |
| Source | The document behind a claim: manual, test report, certificate | Document title and issue date |
Edges
| Edge | From → to | Meaning | Rule |
|---|---|---|---|
has_variant |
Group → variant | This variant belongs to this family | Every variant has exactly one group |
has_component |
Group or variant → component | This item includes this part | A component on the group is on every variant unless excluded |
asserts |
Claim → group, variant or component | The claim is true of this node | Attach to the narrowest node that’s true |
inherits |
Group → variant (implicit) | Group claims apply to each variant | Only for claims attached to the group itself |
excludes |
Claim → variant or offer | This claim does not apply here | Blocks inheritance. Always wins over inherits |
overrides |
Claim → claim | A narrower claim replaces a broader one for a node | Use for real differences, such as a final-sale offer |
offered_as |
Variant → offer | This seller sells this variant | One offer per seller per variant |
governed_by |
Offer → policy | The seller’s store-wide terms apply to this offer | Only the offer’s own seller’s policy |
derived_from |
Copy → claim | A generated or rewritten copy of a claim | A copy never adds scope, value or certainty the claim doesn’t have |
cites |
Claim → source | The document behind the claim | Unknown claims cite nothing and say why |
Resolution rules
To answer “is claim C true of variant V, as sold in offer O?”, walk the graph in this order. These rules are our proposal.
- Is C attached to V, or to a component V has? If it’s attached to a component, the answer is only ever about that component.
- Otherwise, is C attached to V’s group, with no
excludesedge to V? If yes, it applies. If an exclusion exists, it doesn’t. - Is C an offer or policy fact? Then look only at O and O’s own seller’s policy. Never another seller’s offer.
- Does a narrower claim override C for this node? The narrower one wins.
- None of the above? The answer is unknown, not “no”.
That gives three possible answers: applies, doesn’t apply, unknown. A system that collapses them into yes and no will either over-promise or quietly drop true facts.
Worked case: a two-piece snowsuit
On October 26, 2026 we searched Shopify’s agent catalog for “toddler snowsuit rated for minus 30”. Position 9 was a two-piece snowsuit, listed at USD 339.00. Store and material brand names are withheld. Here’s what its record said.
From the description:
- “The insulated jacket features expandable sleeves that grow 4 cm, polar fleece lining for extra warmth, and a hood with [insulation brand] insulation rated to -30°C.”
- “The snow pants include removable and adjustable suspenders, an elastic waist, and two zippered side pockets.”
- [Reinforcement fabric] “at knees, seat, and hems”.
From the record’s other fields:
- Sizes: 4, 5, 6, 7, 8, 9, 10, 12, 14 and 16. The returned variant was size 4.
- Shopify’s generated spec list: “Insulation: [insulation brand] rated to -30°C”, “Material: Fleece with [reinforcement fabric] reinforcement”, “Pockets: Two zippered side pockets”, “Available sizes: 5-6 years, 7-8 years, 6 US, 16 US”.
- A generated top feature: “[Insulation brand] insulation rated to -30°C keeps kids safe from the elements”.
- Ratings: product 4.9 from 11 reviews; the size 4 variant 4.8 from 4.
The graph for this record
| Node | Claims attached | Edge notes |
|---|---|---|
| Group: two-piece snowsuit | Has a jacket and snow pants | has_component to jacket and pants |
| Component: jacket | Sleeves grow 4 cm; polar fleece lining | has_component to hood |
| Component: jacket/hood | Insulation rated to -30°C | The rating is scoped to the hood only |
| Component: pants | Adjustable suspenders; elastic waist; two zippered side pockets | |
| Components: knees, seat, hems | [Reinforcement fabric] reinforcement | |
| Variants: sizes 4 to 16 | Size label only; no size range stated | has_variant from the group, ten times |
| Offer: size 4, this seller | USD 339.00; available | offered_as from size 4 |
| Rating: product | 4.9 from 11 | Population not stated |
| Rating: size 4 variant | 4.8 from 4 | Population not stated |
Where the generated copies broke the edges
Each generated line is a derived_from copy of a description claim. Three of them moved a claim to a wider node than the source supports:
| Generated copy | Source claim | What changed |
|---|---|---|
| “Insulation: … rated to -30°C” | The hood’s insulation is rated to -30°C | Scope widened from the hood to the whole suit |
| “… keeps kids safe from the elements” | No such claim | Certainty added. The source states a rating, not a safety outcome |
| “Pockets: Two zippered side pockets” | The snow pants have two zippered side pockets | Scope widened from the pants to the whole product |
| “Material: Fleece with … reinforcement” | Fleece is the jacket’s lining; reinforcement is at knees, seat and hems | Two part-level facts merged into one product-level material |
| “Available sizes: 5-6 years, 7-8 years, 6 US, 16 US” | Ten size options, 4 to 16 | Doesn’t match the variant nodes at all |
We can’t see what Shopify generated these from, so none of this proves an error in Shopify’s process. Shopify itself marks these fields as Inferred: generated or enriched by Shopify, and not merchant-authored source text. What the graph shows is where a reader of the generated lines would go wrong.
What the graph answers
| Question | Answer | Why |
|---|---|---|
| Is the hood’s insulation rated to -30°C? | Applies, to every size | Hood claim; every variant has the hood through the group |
| Is the snowsuit rated to -30°C? | Unknown | The only rating is scoped to the hood. Nothing is attached to the whole suit |
| Does the jacket have pockets? | Unknown | The pockets are attached to the pants |
| Does size 6 exist? | Yes | It’s a variant node. The generated “6 US” doesn’t create or confirm it |
| What does size 4 cost from this seller? | USD 339.00, on October 26 | Offer node, observed on that date |
| What’s size 4’s rating? | 4.8 from 4, or 4.9 from 11 for the product | Two nodes, two populations. Don’t merge them |
The second row is the one that matters to a parent shopping for -30°C weather. A shopper whose need is “the whole suit for -30°C” gets an honest unknown, not a yes borrowed from the hood. That’s why the catalog research insists on the word hood.
The merchant’s fix follows from the graph. If the whole suit has a temperature rating from a real source, attach it to the group, with its condition. If it doesn’t, the page should say what the hood rating covers, in those words, so no copy can widen it.
Exclusions and overrides, with hypothetical examples
The snowsuit needed scope edges. Other catalogues need the other two. These examples are hypothetical.
| Situation | Without the edge | With it |
|---|---|---|
| A leather boot family where one colour is synthetic | “Leather upper” on the group reaches every colour | excludes from “leather upper” to the synthetic colour, plus its own material claim |
| A store with 30-day returns and one final-sale colourway | The store policy reaches every offer | A final-sale claim on that offer overrides the store policy |
| A humidifier family where only the 6 L model covers 600 sq ft | The hero model’s coverage sits in the shared description | Coverage attached to each variant, nothing on the group |
| A seller reselling your product under the same catalog product | Your return policy appears to cover their offer | Their offer is governed_by their own policy; no edge to yours |
The markup vocabularies support part of this. Schema.org lets a return policy sit on an organization, a product or an offer. Google recommends store-wide return policy markup under your Organization. So an offer-level policy is the place for a final-sale exception. What neither vocabulary has is a way to say “every variant except this one” or “this part only”. Those exceptions have to live in your own data, and in visible text that says them plainly.
Offers when a catalog clusters sellers
Shopify says Global Catalog search results are clustered by a Universal Product ID and include offers from multiple merchants, with a separate step that returns pricing and seller checkout links for the chosen variant. OpenAI says that after a shopper picks a product, ChatGPT may show other merchants offering it, at the same or different prices, ranked partly on whether a seller is the maker or primary seller.
So the group node may be shared between sellers while the offer nodes are not. Product identity and specifications can reasonably be the same across sellers of one item. Price, stock, shipping and returns must stay with each seller’s own offer.
We haven’t seen how clustering merges facts. In our 162-search snapshot, every one of the 1,620 results carried exactly one variant from one seller, and we didn’t make the follow-up call that returns options and sellers. Treat the multi-seller part of the graph as documented, not observed.
Encode it in your own data
You don’t need graph software. A list of edges in a spreadsheet or JSON is enough:
[
{ "from": "snowsuit", "edge": "has_component", "to": "jacket" },
{ "from": "jacket", "edge": "has_component", "to": "jacket/hood" },
{ "from": "claim/hood-insulation-minus-30c", "edge": "asserts", "to": "jacket/hood" },
{ "from": "snowsuit", "edge": "has_variant", "to": "size-4" },
{ "from": "size-4", "edge": "offered_as", "to": "offer/[seller]/size-4" },
{ "from": "generated/insulation-line", "edge": "derived_from", "to": "claim/hood-insulation-minus-30c" }
]
Then publish each resolved answer where readers already look. Markup: ProductGroup with hasVariant or isVariantOf, group properties only for facts that really apply to every variant, and hasMerchantReturnPolicy on the offer when it differs. On Shopify: variant fields and metafields for variant facts, and Catalog Mapping if separate products should be grouped as one listing. Shopify’s help page says Catalog Mapping can do that grouping and source data from metafields; we haven’t verified that screen. In every case, put component scope in the visible sentence (“the hood’s insulation”), because no structured field carries it.
What this model is not
- Not a ranking lever. It keeps facts where they’re true. We haven’t measured whether that changes picks.
- Not a sibling-choice experiment. Which variant an agent picks, and whether a hero variant draws picks away from its siblings, needs its own controlled test.
- Not a rule to collapse variants. Nothing here suggests one product per brand or one variant per product. Keep every variant a shopper would choose differently.
- Not a platform format. No assistant consumes these edges. They decide what you publish.
How we checked
On October 27, 2026 we re-read Schema.org’s ProductGroup and MerchantReturnPolicy pages, Google’s Product and Product variant structured-data pages, Shopify’s Global Catalog documentation and agentic storefronts help page, and OpenAI’s shopping help page, and quoted or paraphrased only what they say now.
We reopened the saved Shopify agent-catalog response for “toddler snowsuit rated for minus 30”, taken on October 26, 2026 with no buyer context, top 10 results. For position 9 we read the description, option list, returned variant, price, both ratings and Shopify’s generated spec, feature and selling-point lists word for word; every quote in the tables comes from that record, with brand names replaced. A script confirmed that all 1,620 results in the 162-search snapshot carried one variant from one seller. We didn’t call get_product, query any assistant or inspect the garment.
Your next step
Pick one product that has parts or a size range. Write every claim on its page as one line, then next to each write the narrowest node it’s true of: the whole product, one part, one variant or one seller’s offer. Any claim you can only attach to a part, but your page states about the whole product, is a scope leak. Fix the sentence first.