Warning, not Violation, and deliberately: Google's Article rich result wants all three, but a missing image is the operator's copy to fix, not a bug in this tool. Measured against the fixture matrix before choosing the severity — 0 of 3 Articles there carry an image, so gating on it would fail CI on correct output.
An Article should carry a headline, a publication date and an image.
ListItem is the most numerous class in a typical export (41 of 637 triples in the fixture) and a position that is missing or doubled silently reorders a breadcrumb trail in search results.
Every breadcrumb entry needs exactly one position and one name.
The one shape whose content is entity-specific, which is why the file is emitted per entity rather than once for the engine. A subject outside the entity's base means the export is asserting facts under someone else's namespace. The trailing slash is part of the rule, not decoration: `identifiers.py` mints every id as `{base}/#kind-slug`, and when this shape first ran, five call sites in layer2 and transforms were hand-rolling `{base}#…` instead. Both forms sit inside the namespace but they are distinct IRIs, so a node minted through both paths would never have merged. Those sites now go through the helpers; this pins it.
Nodes this export describes must be identified under the entity's own base.
BHE-DES-RDF-001 §8.5. A blank node gets a random label on every run, so an otherwise-identical export produces byte-different Turtle and the manifest's sha256 stops meaning anything. Phase 2 minted @ids for all 26 shapes that used to be blank; this is the shape that keeps them minted.
Every typed node must carry an IRI @id, never a blank node.
The defect §3.4 opens with. The old _deduplicate_graph kept whichever same-@id node had more keys, so layer2's deliberate partial {@id, @type, hasOfferCatalog} Organization node lost to the richer Layer-1 one and no ProductsServices page ever shipped the back-reference. Phase 0.5 fixed it; this shape is what notices if it regresses.
An OfferCatalog must be referenced by something's hasOfferCatalog.
Phase 1 wired the product model to the generator for the first time; before it, no product entered through the UI reached the JSON-LD at all. Warning rather than Violation because most small-business catalogues have no GTIN and many services have no price — the fixture's own Product has neither.
A Product should carry an offer and a stable identifier.
The same collision detector aimed where it bit hardest. Academic article titles are the routine case of titles over 60 characters, so Articles are where truncated slugs collided.
An Article may carry only one headline and one publication date.
The observable symptom of the §3.4 @id-collision defect. Two truncated slugs colliding made dedup merge two unrelated things into one node, and a merged node carries both names. Phase 0.6 disambiguates the slugs; a second name here means it stopped working. Cheapest collision detector available — `name` is on almost every node.
ListItem has no single integer position.
OfferCatalog is orphaned — nothing points at it.
Product has no sku (gtin/mpn/productID are equally acceptable).
Two names on one node: an @id collision merged two things.