shape-ArticleCompleteness
type: NodeShape
value: 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.
message: An Article should carry a headline, a publication date and an image.
property: shape-ArticleCompleteness-datePublished
property: shape-ArticleCompleteness-headline
property: shape-ArticleCompleteness-image
severity: Warning
target class: Article

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.
type: NodeShape
message: Every breadcrumb entry needs exactly one position and one name.
property: shape-BreadcrumbPositions-position
severity: Violation
target class: ListItem

shape-EntityUriSpace
type: NodeShape
value: 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.
message: Nodes this export describes must be identified under the entity's own base.
pattern: ^https://personaopps\.com/
severity: Warning
target subjects of: type

shape-IriIdentity
type: NodeShape
value: 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.
message: Every typed node must carry an IRI @id, never a blank node.
node kind: IRI
severity: Violation
target subjects of: type

shape-OfferCatalogBackReference
type: NodeShape
value: 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.
message: An OfferCatalog must be referenced by something's hasOfferCatalog.
property: shape-OfferCatalogBackReference-inv-hasOfferCatalog
severity: Violation
target class: OfferCatalog

shape-ProductIdentity
type: NodeShape
value: 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.
message: A Product should carry an offer and a stable identifier.
property: shape-ProductIdentity-offers
property: shape-ProductIdentity-sku
severity: Warning
target class: Product

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.
type: NodeShape
message: An Article may carry only one headline and one publication date.
property: shape-SingularHeadline-datePublished
property: shape-SingularHeadline-headline
severity: Violation
target class: Article

shape-SingularName
type: NodeShape
value: 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.
message: A node may carry only one name.
property: shape-SingularName-name
severity: Violation
target subjects of: name

shape-ArticleCompleteness-datePublished
type: PropertyShape
message: Article has no publication date.
min count: 1
path: datePublished
severity: Warning

shape-ArticleCompleteness-headline
type: PropertyShape
message: Article has no headline.
min count: 1
path: headline
severity: Warning

shape-ArticleCompleteness-image
type: PropertyShape
message: Article has no image.
min count: 1
path: image
severity: Warning

shape-BreadcrumbPositions-position
type: PropertyShape
datatype: integer
max count: 1
message: ListItem has no single integer position.
min count: 1
path: position
severity: Violation

shape-OfferCatalogBackReference-inv-hasOfferCatalog
type: PropertyShape
message: OfferCatalog is orphaned — nothing points at it.
min count: 1
path:
  inverse path: hasOfferCatalog
severity: Violation

shape-ProductIdentity-offers
type: PropertyShape
message: Product has no Offer.
min count: 1
path: offers
severity: Warning

shape-ProductIdentity-sku
type: PropertyShape
message: Product has no sku (gtin/mpn/productID are equally acceptable).
min count: 1
path: sku
severity: Warning

shape-SingularHeadline-datePublished
type: PropertyShape
max count: 1
message: Two publication dates on one Article.
path: datePublished
severity: Violation

shape-SingularHeadline-headline
type: PropertyShape
max count: 1
message: Two headlines on one Article.
path: headline
severity: Violation

shape-SingularName-name
type: PropertyShape
max count: 1
message: Two names on one node: an @id collision merged two things.
path: name
severity: Violation