Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

This book is a worked example: building an ontology from nothing to a validated artifact, one step at a time, following Noy & McGuinness’s Ontology Development 101 (2001 — “N&M”) adapted to LinkML. There is one chapter per N&M step. The schema grows incrementally, and each chapter embeds a frozen listing of the schema as it stood at that point, so editing the schema later can’t silently change what an earlier chapter shows.

The ontology is grounded in BFO 2020 (ISO/IEC 21838-2:2020) and the Common Core Ontologies (CCO). Grounding is by URI: each class is referenced through subclass_of and the prefixes, not pulled in with LinkML imports: (which is reserved for other LinkML schemas).

Jargon: BFO and CCO

BFO (the Basic Formal Ontology, ISO/IEC 21838-2:2020) is a small top-level ontology of the most general categories: objects, qualities, processes, and the like. CCO (the Common Core Ontologies) is a mid-level layer built on top of BFO. We ground the ontology in them by pointing its terms at BFO/CCO IRIs instead of inventing categories from scratch.

The domain

The domain is food and wine, N&M’s own running example: wines, foods, and the pairings between them, expressed in LinkML and grounded in BFO/CCO rather than the OWL/RDFS of the 2001 paper. Where N&M’s choices don’t sit cleanly on a BFO foundation, the chapter that makes the change says so.

The generated schema documentation (class diagrams, the interactive class graph, and RDF) is published next to this book under schema/.

Using this as a template

This repository is also the template the book describes. To build your own ontology with the same workflow, clone it (or use GitHub’s “Use this template”), then run the setup-ontology skill: it resets the showcase back to a blank scaffold, renames the placeholder to your schema’s name, and hands off to advance-step, which walks one N&M step per chapter. The README covers the toolchain and the setup sequence.

Domain and Scope

This chapter works Step 1 of Ontology Development 101 (Noy & McGuinness, 2001 — “N&M”) for the wine ontology. Step 1 is scoping, not modeling: no classes go in yet. The output is a set of decisions (what the ontology is about, what it is for, the questions it must answer, and who maintains it), together with the empty schema those decisions will fill.

Noy & McGuinness 2001 — §Step 1

We suggest starting the development of an ontology by defining its domain and scope. That is, answer several basic questions:

  • What is the domain that the ontology will cover?
  • For what we are going to use the ontology?
  • For what types of questions the information in the ontology should provide answers?
  • Who will use and maintain the ontology?

The answers to these questions may change during the ontology-design process, but at any given time they help limit the scope of the model.

The domain

In N&M’s words, “representation of food and wines is the domain of the ontology.” Concretely, that means wines (their grape, region, winery, color, body, and sugar), foods and dishes, and the pairing between them: which wine suits which dish. A Cabernet Sauvignon and a Riesling differ in ways that make one suit grilled meat and the other suit seafood. This ontology captures enough to make that distinction.

What it is for

N&M build the wine ontology for a stated use: “applications that suggest good combinations of wines and food.” That use sets the competency questions below, and every later modeling decision answers to it.

The deployment in mind here is a knowledge graph for graphRAG: retrieval-augmented generation that an AI workflow runs over a graph rather than over loose text. A workflow that recommends a wine for a dish, or explains why one suits another, gets there by traversing wines, their characteristics, and their pairings as graph structure. The ontology is the schema that graph is built on, so it has to carry exactly the entities and relationships those traversals need.

Noy & McGuinness 2001 — §Step 1, Competency questions

One of the ways to determine the scope of the ontology is to sketch a list of questions that a knowledge base based on the ontology should be able to answer, competency questions (Gruninger and Fox 1995). These questions will serve as the litmus test later: Does the ontology contain enough information to answer these types of questions? Do the answers require a particular level of detail or representation of a particular area? These competency questions are just a sketch and do not need to be exhaustive.

Competency questions

N&M sketch these competency questions for the wine and food ontology, the litmus test for whether the finished ontology carries enough information:

  1. Which wine characteristics should I consider when choosing a wine?
  2. Is Bordeaux a red or white wine?
  3. Does Cabernet Sauvignon go well with seafood?
  4. What is the best choice of wine for grilled meat?
  5. Which characteristics of a wine affect its appropriateness for a dish?
  6. Does a bouquet or body of a specific wine change with vintage year?
  7. What were good vintages for Napa Zinfandel?

They are a sketch, not a contract. N&M are explicit that the list “does not need to be exhaustive.” What they give us is a demand signal: wine characteristics (color, body, sugar, flavor), the wine-and-food pairing relation, vintages and how they affect a wine, and named entities like Bordeaux, Cabernet Sauvignon, and Napa Zinfandel all have to be representable; nothing else earns a place until a question reaches for it. Grounding those entities in BFO and CCO is Chapter 2’s job; promoting them into classes and a hierarchy is Chapter 4’s.

Who uses and maintains it

The maintainer is the developer who adopts this ontology as the foundation for that knowledge graph: the person building the graphRAG workflow, who extends the schema as their application’s questions grow. They maintain it the way any versioned schema is maintained: the version: field is the source of truth and each release is a matching git tag (see the README’s Versioning), so a workflow can pin to a known version and upgrade deliberately.

What it does not model

The competency questions bound the scope as much by what they leave out as by what they ask. So, deliberately out:

  • Restaurant and cellar inventory. The ontology models kinds of wine and food, not the bottles in stock or on a wine list.
  • Price, ratings, and reviews. Catalog data, not the ontology’s concern.
  • The chemistry and process of winemaking. We represent that a wine has a body or a sugar level, not the fermentation that produced it.

Naming an exclusion is a decision, not a gap; it is what keeps Chapter 4 from sprawling past the questions.

Noy & McGuinness 2001 — §3, three fundamental rules

  1. There is no one correct way to model a domain — there are always viable alternatives. The best solution almost always depends on the application that you have in mind and the extensions that you anticipate.
  2. Ontology development is necessarily an iterative process.
  3. Concepts in the ontology should be close to objects (physical or logical) and relationships in your domain of interest. These are most likely to be nouns (objects) or verbs (relationships) in sentences that describe your domain.

On iteration

These rules apply from here on. There is no single correct model of wine and food: whether color is a quality a wine bears or a class of wines, whether a vintage is an attribute of a wine or an entity in its own right, are open questions with viable answers, and the choice turns on the competency questions above. Development is iterative: later chapters revise earlier decisions, and this book records those reversals as they happen rather than tidying them away.

The starting point

Step 1 touches the schema only enough to record the decisions above:

Listing 1.1
id: https://w3id.org/wine-linkml/
name: wine
description: |-
  A worked-example ontology of food and wine, built following Ontology
  Development 101 (Noy & McGuinness, 2001) adapted to LinkML and grounded
  in BFO 2020 (ISO/IEC 21838-2:2020) and the Common Core Ontologies (CCO).
  Following N&M's own running example, it represents wines, foods, and
  the characteristics that make a wine appropriate for a dish. It is
  built for applications that suggest good wine-and-food combinations.
  See the book for the chapter-by-chapter build.
license: https://creativecommons.org/licenses/by/4.0/
version: 0.1.0-dev

prefixes:
  # The LinkML metamodel and this schema's own namespace
  linkml: https://w3id.org/linkml/
  wine: https://w3id.org/wine-linkml/

  # Foundational grounding: BFO and CCO
  obo: http://purl.obolibrary.org/obo/
  cco: https://www.commoncoreontologies.org/

default_prefix: wine
default_range: string

# Only linkml:types is imported. External OWL ontologies (BFO, CCO, ...)
# are reused by URI via class_uri/slot_uri plus the prefixes above —
# never via imports:, which is reserved for other LinkML schemas.
imports:
  - linkml:types

classes:
  # Step 4+ populate these.

slots:
  # Step 5+ populate these.

enums:
  # Step 4+ populate these.

The domain and scope live in the schema’s own description 1.1.1; the classes, slots, and enums sections 1.1.2 are present but empty. The build genuinely starts from nothing, and everything after this chapter adds to this one file, one N&M step at a time.

Reusing Existing Ontologies

This chapter works Step 2 of Ontology Development 101 (Noy & McGuinness, 2001 — “N&M”). Step 2 asks what to reuse before building anything new. It is where this book’s grounding in BFO and CCO stops being a claim and becomes a decision for each concept the competency questions named.

Noy & McGuinness 2001 — §Step 2

It is almost always worth considering what someone else has done and checking if we can refine and extend existing sources for our particular domain and task. Reusing existing ontologies may be a requirement if our system needs to interact with other applications that have already committed to particular ontologies or controlled vocabularies. […] There are libraries of reusable ontologies on the Web and in the literature. […]

For this guide however we will assume that no relevant ontologies already exist and start developing the ontology from scratch.

Taking the advice N&M skipped

N&M are emphatic that reuse is worth considering, and they point at the ontology libraries of their day (Ontolingua, DAML) and commercial vocabularies (UNSPSC, RosettaNet). Then, for the guide, they set all of it aside and start from scratch.

This book does the opposite, and the reason is the twenty-five years since. BFO is now an ISO standard (ISO/IEC 21838-2:2020), and CCO is a maintained mid-level layer on top of it. Grounding in them is the whole point of this build, so where N&M started from scratch, we take the advice they gave and reuse.

What we reuse, and what we invent

Reuse runs at two layers, and they get opposite decisions:

  • The foundation: reuse. Every concept grounds into BFO (the top-level categories) and CCO (mid-level classes like Organization and Geospatial Region), referenced by URI.
  • The domain: invent, but grounded. A well-known wine ontology exists (the W3C OWL Guide’s, itself a descendant of N&M’s example), and we could import it. We don’t. Importing a domain ontology inherits its modeling choices wholesale; building the wine classes ourselves, each one placed under the right BFO or CCO parent, is the skill this book is about.

A note on what is not available: we checked the OBO Relations Ontology and all 263 of CCO’s object properties for an off-the-shelf “pairs with” or “is appropriate for” relation. There isn’t one. The pairing at the heart of N&M’s example has to be modeled, not borrowed (more on that below).

Jargon: the BFO categories we ground to

A handful of BFO 2020 categories carry most of the groundings below. A material entity is a thing made of matter (a wine, a grape). An object aggregate is a material entity made of member objects (an organization is a group of people). A quality is a way a thing is that you can observe directly (a color, a weight). A disposition is a tendency to behave a certain way under the right conditions (fragility; or color, when CCO treats it as an optical property). A site is an immaterial place (a region). A temporal region is a stretch of time (a year). An information content entity is something that is about other things and can be recorded and copied (a recommendation, a label).

The reuse table

Every entity, quality, and relation the competency questions demand, and where it grounds:

ConceptGrounds toReuse / invent
Food, dishCCO Portion of Food (cco:ont00000307)reuse
WineCCO Portion of Processed Material (cco:ont00001084)reuse
Wine regionCCO Geospatial Region (cco:ont00000472)reuse
WineryCCO Organization (cco:ont00001180)reuse
GrapeBFO object (obo:BFO_0000030)invent
Color, body, sugar, flavorBFO quality (obo:BFO_0000019)invent
Vintage (the year)BFO one-dimensional temporal region (obo:BFO_0000038)invent
Wine-food pairinga CCO information content entity (a recommendation)invent

Three of these took a real decision, and they are where verifying the category, not just that the IRI resolves, matters.

Wine grounds to a processed material, which makes it an artifact. CCO offers no generic “portion of liquid,” and its food and material classes all sit under Material Artifact: “a Material Entity designed by some Agent to realize a certain Function.” Grounding wine under Portion of Processed Material therefore commits us to wine being a designed artifact. That is a real commitment, but a defensible one: a wine is a deliberately produced material, fermented and blended to be drunk, not a found portion of liquid.

Color is a quality here, not the disposition CCO makes it. CCO models Color as an optical property (a disposition to interact with electromagnetic radiation). That is a principled treatment of color in general, and we diverge from it deliberately. Wine color is not used as an optical fact; “red,” “white,” and “rosé” are categorical descriptors of the wine, and a wine’s color is not purely optical anyway: it reflects the grape, the skin contact, and the age. So we model wine color as a BFO quality the wine bears, and accept that this departs from CCO. Body, sugar, and flavor have no CCO class at all and ground straight to BFO quality.

The pairing is a recommendation, not a relation on the wine. It is tempting to make “pairs with” a disposition the wine carries, but a wine does not bear a real tendency to suit seafood the way it bears a color. A pairing is a claim: a sommelier’s recommendation about a wine and a dish. We ground it as an information content entity that is about the pair. For the knowledge graph this ontology is built to feed, that earns its keep: a recommendation node can carry the rationale, the source, and a confidence that a bare edge between wine and dish cannot. It is also the clearest companion-ontology candidate in the build: pairing and recommendation knowledge may end up as its own layer rather than living in the wine ontology proper. We revisit that when the classes are built.

Two smaller calls, both deferred deliberately. A grape variety like Cabernet Sauvignon is modeled as a subclass of Grape with a made_from_grape relation, leaving for later the question of whether a cultivar is better treated as a recorded designation. And vintage is the harvest year (a temporal region the wine is tied to), leaving the “2018 vintage as a batch of wine” sense out until a question needs it.

Grounding by URI, not by import

The mechanism is the same throughout, and it is the pattern the rest of the book leans on: a concept is grounded by giving its class a class_uri (or a slot a slot_uri) that points at the BFO or CCO term, with the namespace declared once in the schema’s prefix manifest. External OWL ontologies are never pulled in with LinkML imports:, which is reserved for other LinkML schemas (only linkml:types is imported). The manifest the groundings rely on is already in place from Chapter 1:

Listing 2.1
# wine-yaml-v1.yaml
# @@ 15,31 @@
prefixes:
  # The LinkML metamodel and this schema's own namespace
  linkml: https://w3id.org/linkml/
  wine: https://w3id.org/wine-linkml/

  # Foundational grounding: BFO and CCO
  obo: http://purl.obolibrary.org/obo/
  cco: https://www.commoncoreontologies.org/

default_prefix: wine
default_range: string

# Only linkml:types is imported. External OWL ontologies (BFO, CCO, ...)
# are reused by URI via class_uri/slot_uri plus the prefixes above —
# never via imports:, which is reserved for other LinkML schemas.
imports:
  - linkml:types

Reuse only works if the targets are real, so each IRI in the table was checked against the current BFO 2020 and CCO ontology files: that it exists, and that its category is the one we want. That second check is the one that caught CCO’s Color: the IRI resolves, but it resolves to a disposition, which is why wine color grounds elsewhere.

Step 2 adds no new bytes to the schema. The prefixes are already declared, and each decision in the table becomes a class_uri when the class itself is built in Chapter 4. The reuse table is this chapter’s deliverable; Step 4 cashes it in.

Important Terms

This chapter works Step 3 of Ontology Development 101 (Noy & McGuinness, 2001 — “N&M”) for the wine ontology. Step 3 is a brainstorm with a discipline: write down every term the ontology should talk about, and put off every question about what kind of thing each term is.

Noy & McGuinness 2001 — §Step 3

It is useful to write down a list of all terms we would like either to make statements about or to explain to a user. What are the terms we would like to talk about? What properties do those terms have? What would we like to say about those terms? For example, important wine-related terms will include wine, grape, winery, location, a wine’s color, body, flavor and sugar content; different types of food, such as fish and red meat; subtypes of wine such as white wine, and so on. Initially, it is important to get a comprehensive list of terms without worrying about overlap between concepts they represent, relations among the terms, or any properties that the concepts may have, or whether the concepts are classes or slots.

Where the terms come from

N&M ask for a comprehensive list, and comprehensiveness invites invention: terms that sound like wine vocabulary but that nothing actually asks for. The discipline that keeps the list honest is sourcing. Every term below traces to one of three places: N&M’s own Step 3 example list (quoted above), a competency question from Chapter 1, or the domain work already done in Chapters 1 and 2. Nothing is on the list merely because it sounds like it belongs in a wine ontology.

The term list

SourceTerms
N&M’s Step 3 listwine, grape, winery, location, color, body, flavor, sugar content, food, fish, red meat, white wine
CQ 1 (characteristics)wine characteristic
CQs 2–3 (color, named wines)red wine, rosé, Bordeaux, Cabernet Sauvignon
CQs 3–5 (pairing)seafood, grilled meat, dish, pairing (“goes well with”), appropriateness, recommendation
CQs 6–7 (vintage)bouquet, vintage, vintage year, good vintage, Napa Zinfandel
Chapters 1–2 domain workwine region, grape variety, sweetness, producer, organization

The overlaps stay

The list is deliberately unsorted, and several entries name the same thing or nearly so: location and wine region; flavor and bouquet; sugar content and sweetness; winery, producer, and organization; pairing and recommendation. One term is slipperier still: Bordeaux names a region, and, by metonymy, the wine made there; CQ 2 (“Is Bordeaux a red or white wine?”) uses it as a wine. And white wine sits between a subtype of wine and a value of the color characteristic.

Resolving any of this now would be doing Step 4’s work early. N&M are explicit that the list should be collected “without worrying about overlap between concepts they represent,” so the overlaps stand, on the record, as input to the sorting that comes next.

What happens to the list

Every term on the list has a destination in the steps ahead. The entity-like terms (wine, grape, winery, region, food, dish, vintage) are promoted into classes and a hierarchy in Chapter 4, which also resolves the overlaps above. The characteristic-like terms (color, body, sugar content or sweetness, flavor or bouquet) become slots in Chapter 5, which settles their names; so do the relational ones (pairing’s “goes well with,” a wine’s link to its grape, winery, and region), since a LinkML slot covers a relationship as readily as an attribute. The value-like terms (red, white, and rosé, if Chapter 4 reads them as values of color rather than subtypes of wine) become enumeration values when the color slot gets its range. Chapter 6 then tightens the slots per class with slot_usage facets, and the named individuals (Bordeaux the wine, Cabernet Sauvignon, Napa Zinfandel) wait until Chapter 7, where they become instances and the competency questions get their answers. The groundings each class and slot will use were already decided in Chapter 2’s reuse table.

Like Step 2, this step adds no bytes to the schema: a term list is prose, not model. The schema still stands at the Chapter 1 snapshot, and Chapter 4 starts spending the list.

Classes and Hierarchy

This chapter works Step 4 of Ontology Development 101 (Noy & McGuinness, 2001 — “N&M”) for the wine ontology: the terms from Chapter 3 with independent existence become classes, each grounded in the BFO/CCO category Chapter 2 chose for it. This is the chapter where the schema first grows structure, and where two of Chapter 2’s calls get revised in the light of actually building it.

Noy & McGuinness 2001 — §Step 4

There are several possible approaches in developing a class hierarchy (Uschold and Gruninger 1996): A top-down development process starts with the definition of the most general concepts in the domain and subsequent specialization of the concepts. A bottom-up development process starts with the definition of the most specific classes, the leaves of the hierarchy, with subsequent grouping of these classes into more general concepts. A combination development process […] defines the more salient concepts first and then generalizes and specializes them appropriately.

N&M say none of the three approaches is inherently better. Ours is top-down in effect: the most general concepts (the BFO/CCO categories) already exist, and the domain classes specialize them directly.

From terms to classes

N&M’s selection rule: “we select the terms that describe objects having independent existence rather than terms that describe these objects. These terms will be classes in the ontology.” Applying it to the Chapter 3 list yields seven classes: Wine, Food, Grape, WineRegion, Winery, VintageYear, and PairingRecommendation. The characteristic terms (color, body, sugar content, flavor) describe wines rather than exist independently, so they wait for Chapter 5, as do the relational terms; red, white, and rosé wait with them as values of the color characteristic.

That last clause resolves the overlaps Chapter 3 left standing, mostly by picking one name per concept: wine region over location (WineRegion), winery over producer and organization (Winery, grounded in CCO Organization), and recommendation over pairing as the thing that gets a class (PairingRecommendation). Dish folds into Food. White wine goes the value route rather than the subtype route, for reasons the next section unpacks. And Bordeaux keeps both of its readings: the region becomes an instance of WineRegion and the wine an instance of Wine, two individuals sharing a name.

What an instance of Wine is

The choice that shapes everything downstream: an instance of Wine is a named kind of wine (Bordeaux, Napa Zinfandel, Cabernet Sauvignon), not a bottle. Chapter 1 scoped bottles out, and N&M’s own instance-or-class test settles the rest.

Noy & McGuinness 2001 — §4.6, An instance or a class?

Deciding whether a particular concept is a class in an ontology or an individual instance depends on what the potential applications of the ontology are. […] if we are only going to talk about pairing wine with food we will not be interested in the specific physical bottles of wine. Therefore, such terms as Sterling Vineyards Merlot are probably going to be the most specific terms we use. Therefore, Sterling Vineyards Merlot would be an instance in the knowledge base.

Our application is exactly the one N&M describe, so wine kinds are the instances. This is also where we part with N&M’s own hierarchy, which makes Red wine, White wine, and Rosé wine classes between Wine and the leaves. Color-as-subclass encodes one characteristic into the tree; once Chapter 5 gives Wine a color slot, the same fact would live in two places, and every additional characteristic would invite another layer of subclasses. So color stays a characteristic (a slot with red, white, and rosé as its values), the tree stays flat, and a knowledge graph built on the schema answers “Is Bordeaux a red or white wine?” by reading an attribute off a node instead of testing class membership.

The same reading revises a Chapter 2 aside: grape varieties were sketched there as subclasses of Grape, but they are kinds exactly as wines are, so an instance of Grape is a variety (Cabernet Sauvignon the grape, Zinfandel). Whether a cultivar is better treated as a recorded designation remains open, deferred to Chapter 7 where the instances actually appear.

Grounding, corrected

Chapter 2 said each class would carry a class_uri pointing at its BFO/CCO term. Building the classes shows that is the wrong relation: class_uri asserts identity, and identity is too strong. Our Wine is not CCO’s Portion of Processed Material; every wine is a portion of processed material, but not every portion of processed material is a wine. The relation we mean is subsumption, and LinkML has a slot for exactly that: subclass_of, which asserts rdfs:subClassOf to the external URI (the pattern established by Biolink and used across BFO-grounded LinkML schemas). The reuse table’s decisions stand unchanged; only the mechanism moves from class_uri to subclass_of.

Each domain class grounds directly, with no abstract wrapper classes in between: a wrapper that exists only to hold a grounding earns nothing, and with seven classes each grounding somewhere different there is nothing for an intermediate layer to share.

The classes

The schema’s first structural growth, as a difference against the Chapter 1 snapshot:

Listing 4.1
--- wine-yaml-v1
+++ wine-yaml-v2
@@ -30,12 +30,57 @@
 imports:
   - linkml:types
 
 classes:
-  # Step 4+ populate these.
 
+  Wine:
+    description: >-
+      A kind of wine, distinguished by its grape, provenance, and
+      characteristics. Instances are named wine kinds such as Bordeaux
+      or Napa Zinfandel, not individual bottles.
+    subclass_of: cco:ont00001084
+
+  Food:
+    description: >-
+      A kind of food or dish that a wine can be recommended for, such
+      as a seafood dish or grilled meat.
+    subclass_of: cco:ont00000307
+
+  Grape:
+    description: >-
+      A grape variety used to make wine, such as Cabernet Sauvignon or
+      Zinfandel. Instances are varieties, mirroring Wine's
+      kinds-as-instances reading.
+    subclass_of: obo:BFO_0000030
+
+  WineRegion:
+    description: >-
+      A geographic wine-growing region, such as Napa or Bordeaux.
+    subclass_of: cco:ont00000472
+
+  Winery:
+    description: >-
+      An organization that produces wine.
+    subclass_of: cco:ont00001180
+
+  VintageYear:
+    description: >-
+      The harvest year a wine is tied to. The "2018 vintage as a batch
+      of wine" sense stays out of scope (Chapter 2).
+    subclass_of: obo:BFO_0000038
+
+  PairingRecommendation:
+    description: >-
+      A recommendation that a wine suits a food, such as Cabernet
+      Sauvignon with a seafood dish. Grounded as information about the
+      pair rather than as a property of the wine (Chapter 2).
+    subclass_of: cco:ont00000958
+
 slots:
   # Step 5+ populate these.
 
 enums:
-  # Step 4+ populate these.
+  # Step 5 populates these (the color values, once the color slot exists).

The class section header 4.1.1 marks the selection rule at work. On Wine, the kinds-as-instances decision 4.1.2 and the corrected grounding mechanism 4.1.3 are recorded where they bind. PairingRecommendation carries the aboutness reading 4.1.4 from Chapter 2: a recommendation about a wine and a food, able to carry rationale, source, and confidence. Whether that class and its eventual slots belong in a companion ontology rather than here is still an open question; Chapter 7’s validation is the right place to judge it, once the class has a worked shape.

One check comes up nearly empty at this step, and honestly so: the class graph. With no slots, nothing connects the seven classes to each other yet, so the island test the build relies on (disconnected nodes are bugs to explain or remove) only starts to bite in Chapter 5, when the slots wire wines to their grapes, regions, wineries, and vintages. The classes are in place; Chapter 5 connects them.

Slots

This chapter works Step 5 of Ontology Development 101 (Noy & McGuinness, 2001 — “N&M”) for the wine ontology. The classes exist; now they get their properties, and the class graph gets its edges. N&M’s rule for what belongs here: “most of the remaining terms are likely to be properties of these classes.”

Noy & McGuinness 2001 — §Step 5

The classes alone will not provide enough information to answer the competency questions from Step 1. Once we have defined some of the classes, we must describe the internal structure of concepts. […] In general, there are several types of object properties that can become slots in an ontology: “intrinsic” properties such as the flavor of a wine; “extrinsic” properties such as a wine’s name, and area it comes from; parts, if the object is structured […]; relationships to other individuals; these are the relationships between individual members of the class and other items (e.g., the maker of a wine, representing a relationship between a wine and a winery, and the grape it is made from).

The policies

Four decisions apply across every slot, settled once:

  1. Names come from N&M where the paper supplies them: color, body, flavor, sugar, maker. That quietly settles two naming contests Chapter 3 left open: the paper says sugar (not sugar content or sweetness) and flavor (CQ 6’s bouquet is the same characteristic). made_from_grape keeps the name Chapter 2 already used in prose.
  2. The pairing stays reified. There is no direct goes_well_with edge from Wine to Food; the relation lives as the PairingRecommendation’s wine and food slots, per Chapters 2 and 4. A graph derives wine-to-food adjacency by traversing through the recommendation node, which is the point: the node is where the rationale rides.
  3. No inverses. A knowledge graph traverses edges in both directions; storing made_by alongside maker would be a second copy of the same fact.
  4. Cardinality stays lenient. Nothing is required yet, and only made_from_grape is multivalued (a blend has several varieties). Tightening is Chapter 6’s job, where the competency questions say what must be present.

N&M’s attachment rule (a slot belongs at the most general class that can carry it) applies trivially here: the hierarchy is flat, so each slot attaches to the one class it describes, and only name is shared by all seven.

The characteristics

Wine gets the four intrinsic slots straight from N&M’s list: color, body, flavor, and sugar. Chapter 4’s decision lands in the schema here: color ranges over an enumeration whose values are red, white, and rosé, so “Is Bordeaux a red or white wine?” reads a value off the wine kind. The other three stay strings for now, and that is N&M’s own sequencing rather than laziness: the paper enumerates their allowed values (light, medium, and full body; delicate to strong flavor; sweet, dry, and off-dry) in its Step 6, so this book does too: Chapter 6 turns those strings into enumerations.

One competency question looks like it should strain this design and does not. CQ 6 asks whether a wine’s bouquet or body changes with vintage year, and a characteristic stored on a single wine kind cannot vary. But instances are kinds, and kinds can be as specific as the application needs: the 2017 Napa Zinfandel and the 2018 Napa Zinfandel are two Wine instances with their own body values, N&M’s Sterling Vineyards Merlot logic taken one step further. Chapter 7 demonstrates it.

The provenance wiring

The relationship slots are N&M’s fourth kind of property, and they carry the competency questions about where a wine comes from: made_from_grape (to Grape, multivalued for blends), maker (to Winery), region (to WineRegion), and vintage (to VintageYear).

region is stored rather than derived, and the distinction matters: a wine’s appellation is a fact about the wine kind, not about its producer’s address. A winery in one region can make wine from grapes grown in another, so traversing maker and then asking where the winery sits would answer a different question than the one CQ 7 asks about Napa Zinfandel.

The omission is deliberate too: N&M give Winery a location slot, and this schema does not. No competency question asks where a winery is, and every slot here traces to a demand, the same discipline Chapter 3 applied to terms. The slot is a one-line addition the day a question reaches for it.

The recommendation’s shape

PairingRecommendation gets the slots Chapter 2 promised when it argued the pairing should be an information content entity: wine and food (what the recommendation is about), and rationale, source, and confidence (why, who says so, and how strongly). A bare wine-to-food edge could not carry that payload; a graphRAG workflow retrieves it to explain a pairing, not just assert one.

The graph closes

The schema growth, against the Chapter 4 snapshot:

Listing 5.1
--- wine-yaml-v2
+++ wine-yaml-v3
@@ -36,51 +36,171 @@
   Wine:
     description: >-
       A kind of wine, distinguished by its grape, provenance, and
       characteristics. Instances are named wine kinds such as Bordeaux
       or Napa Zinfandel, not individual bottles.
     subclass_of: cco:ont00001084
+    slots:
+      - name
+      - color
+      - body
+      - flavor
+      - sugar
+      - made_from_grape
+      - maker
+      - region
+      - vintage
 
   Food:
     description: >-
       A kind of food or dish that a wine can be recommended for, such
       as a seafood dish or grilled meat.
     subclass_of: cco:ont00000307
+    slots:
+      - name
 
   Grape:
     description: >-
       A grape variety used to make wine, such as Cabernet Sauvignon or
       Zinfandel. Instances are varieties, mirroring Wine's
       kinds-as-instances reading.
     subclass_of: obo:BFO_0000030
+    slots:
+      - name
 
   WineRegion:
     description: >-
       A geographic wine-growing region, such as Napa or Bordeaux.
     subclass_of: cco:ont00000472
+    slots:
+      - name
 
   Winery:
     description: >-
       An organization that produces wine.
     subclass_of: cco:ont00001180
+    slots:
+      - name
 
   VintageYear:
     description: >-
       The harvest year a wine is tied to. The "2018 vintage as a batch
       of wine" sense stays out of scope (Chapter 2).
     subclass_of: obo:BFO_0000038
+    slots:
+      - name
 
   PairingRecommendation:
     description: >-
       A recommendation that a wine suits a food, such as Cabernet
       Sauvignon with a seafood dish. Grounded as information about the
       pair rather than as a property of the wine (Chapter 2).
     subclass_of: cco:ont00000958
+    slots:
+      - name
+      - wine
+      - food
+      - rationale
+      - source
+      - confidence
 
 slots:
-  # Step 5+ populate these.
 
+  name:
+    description: >-
+      The name of a kind, place, producer, or recommendation. Every
+      class carries it; instances are named kinds (Chapter 4).
+    range: string
+
+  color:
+    description: >-
+      The color of a wine kind: a characteristic Chapter 2 read as a
+      BFO quality, carried here as a value on the wine.
+    range: WineColorEnum
+
+  body:
+    description: >-
+      The body of a wine kind. A string until Chapter 6 enumerates the
+      values, following N&M's own sequencing.
+    range: string
+
+  flavor:
+    description: >-
+      The flavor of a wine kind (CQ 6 says "bouquet" for the same
+      characteristic). A string until Chapter 6 enumerates the values.
+    range: string
+
+  sugar:
+    description: >-
+      The sugar level of a wine kind, N&M's name for the term the list
+      also carried as sweetness. A string until Chapter 6 enumerates
+      the values.
+    range: string
+
+  made_from_grape:
+    description: >-
+      The grape variety or varieties a wine kind is made from.
+    range: Grape
+    multivalued: true
+
+  maker:
+    description: >-
+      The winery that makes a wine kind, N&M's name for the
+      wine-winery relationship.
+    range: Winery
+
+  region:
+    description: >-
+      The wine region a wine kind comes from (its appellation), stored
+      rather than derived: a winery's address does not determine where
+      its wine's grapes grow.
+    range: WineRegion
+
+  vintage:
+    description: >-
+      The harvest year a wine kind is tied to. Vintage-specific kinds
+      (the 2018 Napa Zinfandel) are themselves valid Wine instances.
+    range: VintageYear
+
+  wine:
+    description: >-
+      The wine kind a pairing recommendation is about.
+    range: Wine
+
+  food:
+    description: >-
+      The food kind a pairing recommendation is about.
+    range: Food
+
+  rationale:
+    description: >-
+      Why the pairing works, in prose.
+    range: string
+
+  source:
+    description: >-
+      Where the recommendation comes from (a sommelier, a text, a
+      dataset).
+    range: string
+
+  confidence:
+    description: >-
+      How strongly the source backs the pairing, from 0 to 1.
+    range: float
+
 enums:
-  # Step 5 populates these (the color values, once the color slot exists).
+
+  WineColorEnum:
+    description: >-
+      The color values of a wine kind: values of the color
+      characteristic rather than subclasses of Wine (Chapter 4).
+    permissible_values:
+      red:
+      white:
+      rosé:

The slot section header 5.1.1 marks N&M’s remaining-terms rule; the color enumeration 5.1.2 lands the Chapter 4 decision; made_from_grape 5.1.3 carries the one multivalued call; and the recommendation’s wine slot 5.1.4 records the reification policy where it binds.

And the check Chapter 4 could only promise now passes: with the slots in place, the class graph has edges, and every one of the seven classes is connected: Wine reaches Grape, Winery, WineRegion, and VintageYear directly, and Food joins through PairingRecommendation. No islands: nothing in the schema exists that the worked example does not wire to everything else. Chapter 6 tightens what these slots allow; Chapter 7 fills them with the instances the competency questions name.

Slot Usage and Facets

This chapter works Step 6 of Ontology Development 101 (Noy & McGuinness, 2001 — “N&M”) for the wine ontology. The slots exist; now they get their constraints: which values are allowed, which slots must be filled, and what bounds a value must respect. Chapter 5 left everything deliberately loose, so every tightening here has to state its justification.

Noy & McGuinness 2001 — §Step 6

Slots can have different facets describing the value type, allowed values, the number of the values (cardinality), and other features of the values the slot can take.

The value sets

The three characteristic slots Chapter 5 left as strings become enumerations, and their values deserve honest attribution. flavor gets N&M’s set exactly: the paper says “the flavor slot can take on one of the three possible values: strong, moderate, and delicate.” For body and sugar the paper enumerates no complete set; its examples use light and full bodies (the Figure 5 instance has “a light body”; full is their default-value example) and dry and sweet sugars (the same instance is dry; the Port class inherits SWEET from Dessert wine). We complete each scale with the conventional middle value (medium body, off-dry sugar) and say so, rather than passing the full sets off as the paper’s.

Each permissible value also carries its own definition in the schema. An enum whose values are bare tokens makes the reader guess what off-dry means; the definition is one line, and it belongs to the artifact. The slot descriptions get the same treatment: “the color of a wine kind” the slot named color on the class Wine already says twice, so the description says what the characteristic is (the hue the wine presents, imparted chiefly by skin contact during maceration) instead of restating the name or the values.

The color enumeration itself needed no new values here: Chapter 5 already gave it those, because Chapter 4’s subtype-versus-value decision demanded them.

The cardinality sweep

Noy & McGuinness 2001 — §Step 6, cardinality

Sometimes it may be useful to set the maximum cardinality to 0. This setting would indicate that the slot cannot have any values for a particular subclass.

Chapter 5’s lenient stance holds unless something demands otherwise. Four slots earned required:

  • made_from_grape: the paper’s own call. It gives the grape slot a minimum cardinality of one, because every wine is made from at least one variety.
  • wine and food on PairingRecommendation: a recommendation about nothing is not a recommendation, and every pairing question (CQs 3–5) traverses both.
  • name: every competency question addresses its entities by name, and Chapter 7’s instances will be keyed by it.

Everything else stays optional: a wine kind with an unknown body is still a wine kind. The maximum-cardinality-zero facet N&M describe has no work to do in a flat hierarchy: it exists to switch a slot off for a particular subclass, and there are no subclasses to switch it off for. The same is true of default values (the paper’s example: full as a default body); a default is an application convenience, and no competency question asks for one.

confidence gets bounds instead of cardinality: minimum_value: 0, maximum_value: 1. Chapter 5’s prose said “from 0 to 1”; now the schema checks it.

What slot_usage is for, and why it sits idle here

LinkML’s slot_usage narrows an inherited slot for one class: a subclass tightens the range, the cardinality, or the values of a slot it shares with its siblings. That is the natural home of Step 6 work in a schema with deep hierarchies. This schema does not have one: each slot attaches to exactly one class, and the only shared slot (name) needs no narrowing anywhere. So the facets in this chapter land directly on the slot definitions, and slot_usage waits for the schema that needs it (worth knowing before assuming a Step 6 must produce one).

Property characteristics get the same honest treatment. Nothing in the wine schema is symmetric, transitive, or reflexive (N&M’s inverse-slot example, produces on Winery, is exactly what Chapter 5’s no-inverses policy declined), so the OWL-DL rule (a transitive property must stay “simple,” free of cardinality and asymmetry claims) is satisfied without effort. It starts to matter the day a relation like part of enters the schema.

The constraints

The diff below also carries a cleanup. The slot and enum descriptions had accumulated references to the book: chapter numbers, the paper’s name for a term, what a later chapter would do. A schema outlives its book. The description fields ship with the artifact (into the generated docs, the RDF, and any downstream graph), so they now read as self-contained domain documentation, and the book-facing context lives where it belongs: in comments and callout markers, which never enter the artifact’s data.

The schema growth, against the Chapter 5 snapshot:

Listing 6.1
--- wine-yaml-v3
+++ wine-yaml-v4
@@ -2,16 +2,16 @@
 name: wine
 description: |-
   A worked-example ontology of food and wine, built following Ontology
   Development 101 (Noy & McGuinness, 2001) adapted to LinkML and grounded
   in BFO 2020 (ISO/IEC 21838-2:2020) and the Common Core Ontologies (CCO).
-  Following N&M's own running example, it represents wines, foods, and
-  the characteristics that make a wine appropriate for a dish. It is
+  Following the paper's own running example, it represents wines, foods,
+  and the characteristics that make a wine appropriate for a dish. It is
   built for applications that suggest good wine-and-food combinations.
-  See the book for the chapter-by-chapter build.
+  The accompanying book documents the build.
 license: https://creativecommons.org/licenses/by/4.0/
 version: 0.1.0-dev
 
 prefixes:
   # The LinkML metamodel and this schema's own namespace
   linkml: https://w3id.org/linkml/
@@ -22,14 +22,15 @@
   cco: https://www.commoncoreontologies.org/
 
 default_prefix: wine
 default_range: string
 
 # Only linkml:types is imported. External OWL ontologies (BFO, CCO, ...)
-# are reused by URI via class_uri/slot_uri plus the prefixes above —
-# never via imports:, which is reserved for other LinkML schemas.
+# are reused by URI (subclass_of to the external term, resolved through
+# the prefixes above), never via imports:, which is reserved for other
+# LinkML schemas.
 imports:
   - linkml:types
 
 classes:
 
@@ -83,23 +84,23 @@
     slots:
       - name
 
   VintageYear:
     description: >-
       The harvest year a wine is tied to. The "2018 vintage as a batch
-      of wine" sense stays out of scope (Chapter 2).
+      of wine" sense is deliberately out of scope.
     subclass_of: obo:BFO_0000038
     slots:
       - name
 
   PairingRecommendation:
     description: >-
       A recommendation that a wine suits a food, such as Cabernet
       Sauvignon with a seafood dish. Grounded as information about the
-      pair rather than as a property of the wine (Chapter 2).
+      pair rather than as a property of the wine.
     subclass_of: cco:ont00000958
     slots:
       - name
       - wine
       - food
       - rationale
@@ -108,53 +109,58 @@
 
 slots:
 
   name:
     description: >-
-      The name of a kind, place, producer, or recommendation. Every
-      class carries it; instances are named kinds (Chapter 4).
+      The name by which a kind, place, producer, or recommendation is
+      known. Every class carries it.
     range: string
+    # required: every competency question addresses its entities by name.
+    required: true
 
   color:
     description: >-
-      The color of a wine kind: a characteristic Chapter 2 read as a
-      BFO quality, carried here as a value on the wine.
+      The hue a wine kind presents, imparted chiefly by the extent of
+      grape-skin contact during maceration and shifted by variety and
+      age.
     range: WineColorEnum
 
   body:
     description: >-
-      The body of a wine kind. A string until Chapter 6 enumerates the
-      values, following N&M's own sequencing.
-    range: string
+      The perceived weight and fullness of a wine kind on the palate,
+      driven by alcohol, extract, and residual sugar.
+    range: BodyEnum
 
   flavor:
+    # "bouquet" names the same characteristic in one competency question.
     description: >-
-      The flavor of a wine kind (CQ 6 says "bouquet" for the same
-      characteristic). A string until Chapter 6 enumerates the values.
-    range: string
+      The intensity of the taste and aroma impression a wine kind
+      makes.
+    range: FlavorEnum
 
   sugar:
+    # "sugar" is the source paper's name for what is often called sweetness.
     description: >-
-      The sugar level of a wine kind, N&M's name for the term the list
-      also carried as sweetness. A string until Chapter 6 enumerates
-      the values.
-    range: string
+      The residual sugar a wine kind retains after fermentation,
+      perceived as its sweetness.
+    range: SugarEnum
 
   made_from_grape:
     description: >-
       The grape variety or varieties a wine kind is made from.
     range: Grape
     multivalued: true
+    required: true
 
   maker:
     description: >-
-      The winery that makes a wine kind, N&M's name for the
-      wine-winery relationship.
+      The winery that makes a wine kind.
     range: Winery
 
   region:
     description: >-
       The wine region a wine kind comes from (its appellation), stored
       rather than derived: a winery's address does not determine where
@@ -169,17 +175,20 @@
 
   wine:
     description: >-
       The wine kind a pairing recommendation is about.
     range: Wine
+    # required: a recommendation about nothing is not one.
+    required: true
 
   food:
     description: >-
       The food kind a pairing recommendation is about.
     range: Food
+    required: true
 
   rationale:
     description: >-
       Why the pairing works, in prose.
     range: string
 
@@ -190,17 +199,56 @@
     range: string
 
   confidence:
     description: >-
       How strongly the source backs the pairing, from 0 to 1.
     range: float
+    minimum_value: 0
+    maximum_value: 1
 
 enums:
 
   WineColorEnum:
     description: >-
-      The color values of a wine kind: values of the color
-      characteristic rather than subclasses of Wine (Chapter 4).
+      The hue classifications a wine kind can present.
     permissible_values:
       red:
+        description: Fermented with extended contact between juice and dark grape skins.
       white:
+        description: Fermented with minimal skin contact, typically from pale-skinned grapes.
       rosé:
+        description: Fermented with brief skin contact, sitting between white and red.
+
+  BodyEnum:
+    description: >-
+      The palate-weight classifications of a wine.
+    permissible_values:
+      light:
+        description: Low perceived weight on the palate.
+      medium:
+        description: Intermediate perceived weight on the palate.
+      full:
+        description: High perceived weight, typically with higher alcohol and extract.
+
+  FlavorEnum:
+    description: >-
+      The taste-and-aroma intensity classifications of a wine.
+    permissible_values:
+      delicate:
+        description: A subtle, low-intensity impression.
+      moderate:
+        description: An intermediate-intensity impression.
+      strong:
+        description: A pronounced, high-intensity impression.
+
+  SugarEnum:
+    description: >-
+      The residual-sugar classifications of a wine.
+    permissible_values:
+      dry:
+        description: Little or no perceptible residual sugar.
+      off-dry:
+        description: Slightly perceptible residual sugar.
+      sweet:
+        description: Clearly perceptible residual sugar.

The new enumerations carry their attribution 6.1.4 in the schema itself; made_from_grape records N&M’s minimum-cardinality call 6.1.2; and confidence turns Chapter 5’s prose promise into a checked constraint 6.1.3.

The schema is now structurally complete: classes, slots, values, and constraints. What it does not yet have is anything in it. Chapter 7 creates the instances the competency questions name, validates the lot, and asks the only question that finally matters: can the ontology answer what Chapter 1 said it would?

Instances and Validation

This chapter demonstrates Step 7 of Ontology Development 101 (Noy & McGuinness, 2001 — “N&M”). It is where the schema gains its first instance graph. The last step creates individuals (instances of the classes, the nodes of a graph that conforms to the schema), validates the graph against the schema, and tests whether it can answer the Chapter 1 competency questions.

Noy & McGuinness 2001 — §Step 7

The last step is creating individual instances of classes in the hierarchy. Defining an individual instance of a class requires (1) choosing a class, (2) creating an individual instance of that class, and (3) filling in the slot values.

A small example instance graph

A small instance graph can illustrate the shape of the schema. An individual node in the instance graph is an instance of a class, while an edge is a slot whose value is another individual. The following instance graph contains just four individuals and the edges between them, but it is enough to illustrate the concept:

Listing 7.1
# A minimal preview: just enough individuals to see what an instance
# graph is — a few nodes and the edges between them. This is NOT the
# worked example. The full catalog that answers all seven competency
# questions is a separate, larger data file (data/wine-instances.yaml,
# reproduced in Appendix A); this file exists only to introduce the idea.
#
title: A small preview
description: >-
  The fewest records that show what an instance graph is: one wine and
  the grape, winery, and region it points at.
# Four individuals, three edges: the Chateau Morgon Beaujolais wine is
# made from Gamay, made by Chateau Morgon, and comes from Beaujolais.
grapes:
  - id: gamay
    name: Gamay

regions:
  - id: beaujolais
    name: Beaujolais

wineries:
  - id: chateau-morgon
    name: Chateau Morgon

wines:
  - id: chateau-morgon-beaujolais
    name: Chateau Morgon Beaujolais
    color: red
    made_from_grape: [gamay]
    maker: chateau-morgon
    region: beaujolais

The Chateau Morgon Beaujolais wine is one node. Gamay, the Chateau Morgon winery, and the Beaujolais region are three more. The made_from_grape, maker, and region edges connect them. color: red is a value the wine carries, not an edge, because it points at an enumerated hue rather than at another record. That is the whole idea: the individuals are nodes, the object-valued slots are the edges between them, and the scalar slots are labels on a node.

The graph above is a small preview, not a fully worked example. It holds the fewest records that can illustrate an individual, a node, and an edge, and it lives in its own data file, data/wine-preview.yaml. The full catalog (large enough to answer all seven competency questions) is a separate, larger data file, built up over the rest of this chapter and reproduced in Appendix A.

What a data file needs that a schema did not

Three additions had to land before the first instance could be written, and one of them was a mistake the litmus test caught before it was even run.

The first is a container. A data file needs a single root record to hang everything else on, and the schema had none: the classes were all domain things, no vessel to hold a catalog of them. N&M never needed one because their tool (Protégé) was the container, its project file the implicit root. A plain data file has no such ambient vessel, so the schema grows a WineCatalog class marked tree_root, holding a collection for each kind of record. It is an honest adaptation of the method to a file-based workflow, not a modeling insight about wine.

The second is identifiers. Chapter 5 said instances would be keyed by their name, and building the data file refined that. A name is a human label (“Chateau Morgon Beaujolais”), but records have to reference each other (a wine names its maker, a recommendation names its wine), and a reference wants a short, stable, punctuation-free key, not a display string. So every class gains an id slot marked identifier, and name stays the label. It is the same move N&M’s own tooling makes under the hood; the data file just makes it explicit.

The third addition was forced by the test itself, before it ran. Laying the seven competency questions against the roster, CQ 7 (“What were good vintages for Napa Zinfandel?”) had no answer the schema could give. Worse, Chapter 1 had put “price, ratings, and reviews” explicitly out of scope. The two are in flat contradiction, and the contradiction was ours: the exclusion list was written against an earlier draft of the scope and never re-checked against N&M’s seven questions when Chapter 1 was rebuilt around them. This is exactly the failure the competency questions exist to catch, and it is worth leaving on the record rather than quietly patching Chapter 1, because catching it here is the method working as designed.

N&M’s second rule is the remedy: ontology development is iterative. The fix refines the exclusion instead of reversing it. A consumer’s catalog reviews and prices stay out; a sourced judgment of a vintage’s quality is a different thing, and the schema already has the shape for a recorded judgment. So a VintageAssessment joins PairingRecommendation as a second information content entity: a verdict about a vintage-specific wine, carrying its source and confidence exactly as the pairing does.

The growth against the Chapter 6 snapshot:

Listing 7.2
--- wine-yaml-v4
+++ wine-yaml-v5
@@ -1,17 +1,14 @@
 id: https://w3id.org/wine-linkml/
 name: wine
 description: |-
-  A worked-example ontology of food and wine, built following Ontology
-  Development 101 (Noy & McGuinness, 2001) adapted to LinkML and grounded
-  in BFO 2020 (ISO/IEC 21838-2:2020) and the Common Core Ontologies (CCO).
-  Following the paper's own running example, it represents wines, foods,
-  and the characteristics that make a wine appropriate for a dish. It is
-  built for applications that suggest good wine-and-food combinations.
-  The accompanying book documents the build.
+  An ontology of food and wine: the kinds of wine, the foods they
+  accompany, and the characteristics that make a wine appropriate for a
+  dish. It is built for applications that recommend good wine-and-food
+  pairings.
 license: https://creativecommons.org/licenses/by/4.0/
 version: 0.1.0-dev
 
 prefixes:
   # The LinkML metamodel and this schema's own namespace
   linkml: https://w3id.org/linkml/
@@ -40,12 +37,13 @@
       A kind of wine, distinguished by its grape, provenance, and
       characteristics. Instances are named wine kinds such as Bordeaux
       or Napa Zinfandel, not individual bottles.
     subclass_of: cco:ont00001084
     slots:
+      - id
       - name
       - color
       - body
       - flavor
       - sugar
       - made_from_grape
@@ -56,69 +54,139 @@
   Food:
     description: >-
       A kind of food or dish that a wine can be recommended for, such
       as a seafood dish or grilled meat.
     subclass_of: cco:ont00000307
     slots:
+      - id
       - name
 
   Grape:
     description: >-
       A grape variety used to make wine, such as Cabernet Sauvignon or
       Zinfandel. Instances are varieties, mirroring Wine's
       kinds-as-instances reading.
     subclass_of: obo:BFO_0000030
     slots:
+      - id
       - name
 
   WineRegion:
     description: >-
       A geographic wine-growing region, such as Napa or Bordeaux.
     subclass_of: cco:ont00000472
     slots:
+      - id
       - name
 
   Winery:
     description: >-
       An organization that produces wine.
     subclass_of: cco:ont00001180
     slots:
+      - id
       - name
 
   VintageYear:
     description: >-
       The harvest year a wine is tied to. The "2018 vintage as a batch
       of wine" sense is deliberately out of scope.
     subclass_of: obo:BFO_0000038
     slots:
+      - id
       - name
 
   PairingRecommendation:
     description: >-
       A recommendation that a wine suits a food, such as Cabernet
       Sauvignon with a seafood dish. Grounded as information about the
       pair rather than as a property of the wine.
     subclass_of: cco:ont00000958
     slots:
+      - id
       - name
       - wine
       - food
       - rationale
       - source
       - confidence
 
+  VintageAssessment:
+    description: >-
+      A sourced judgment of the quality of a vintage-specific wine
+      kind, such as a vintage chart's verdict on a year's harvest.
+    subclass_of: cco:ont00000958
+    slots:
+      - id
+      - name
+      - wine
+      - verdict
+      - source
+      - confidence
+
+  WineCatalog:
+    description: >-
+      The container holding a catalog of records: wine kinds, foods,
+      grapes, regions, wineries, vintage years, and the judgments
+      about them.
+    tree_root: true
+    attributes:
+      wines:
+        description: The wine kinds in this catalog.
+        range: Wine
+        multivalued: true
+      foods:
+        description: The food kinds in this catalog.
+        range: Food
+        multivalued: true
+      grapes:
+        description: The grape varieties in this catalog.
+        range: Grape
+        multivalued: true
+      regions:
+        description: The wine regions in this catalog.
+        range: WineRegion
+        multivalued: true
+      wineries:
+        description: The wineries in this catalog.
+        range: Winery
+        multivalued: true
+      vintage_years:
+        description: The vintage years in this catalog.
+        range: VintageYear
+        multivalued: true
+      pairing_recommendations:
+        description: The pairing recommendations in this catalog.
+        range: PairingRecommendation
+        multivalued: true
+      vintage_assessments:
+        description: The vintage assessments in this catalog.
+        range: VintageAssessment
+        multivalued: true
+
 slots:
 
+  id:
+    description: >-
+      The short, stable identifier a record is referenced by within a
+      catalog.
+    identifier: true
+    range: string
+
   name:
     description: >-
       The name by which a kind, place, producer, or recommendation is
       known. Every class carries it.
     range: string
-    # required: every competency question addresses its entities by name.
+    # required: every competency question addresses its entities by
+    # name.
     required: true
 
   color:
     description: >-
       The hue a wine kind presents, imparted chiefly by the extent of
@@ -130,20 +198,22 @@
     description: >-
       The perceived weight and fullness of a wine kind on the palate,
       driven by alcohol, extract, and residual sugar.
     range: BodyEnum
 
   flavor:
-    # "bouquet" names the same characteristic in one competency question.
+    # "bouquet" names the same characteristic in one competency
+    # question.
     description: >-
       The intensity of the taste and aroma impression a wine kind
       makes.
     range: FlavorEnum
 
   sugar:
-    # "sugar" is the source paper's name for what is often called sweetness.
+    # "sugar" is the source paper's name for what is often called
+    # sweetness.
     description: >-
       The residual sugar a wine kind retains after fermentation,
       perceived as its sweetness.
     range: SugarEnum
 
@@ -173,15 +243,15 @@
       (the 2018 Napa Zinfandel) are themselves valid Wine instances.
     range: VintageYear
 
   wine:
     description: >-
-      The wine kind a pairing recommendation is about.
+      The wine kind a recommendation or assessment is about.
     range: Wine
-    # required: a recommendation about nothing is not one.
+    # required: a judgment about nothing is not one.
     required: true
 
   food:
     description: >-
       The food kind a pairing recommendation is about.
     range: Food
@@ -191,48 +261,62 @@
     description: >-
       Why the pairing works, in prose.
     range: string
 
   source:
     description: >-
-      Where the recommendation comes from (a sommelier, a text, a
-      dataset).
+      Where a recommendation or assessment comes from (a sommelier, a
+      text, a dataset).
     range: string
 
   confidence:
     description: >-
-      How strongly the source backs the pairing, from 0 to 1.
+      How strongly the source backs the judgment, from 0 to 1.
     range: float
     minimum_value: 0
     maximum_value: 1
 
+  verdict:
+    description: >-
+      The quality judgment an assessment renders on its wine.
+    range: VerdictEnum
+    required: true
+
 enums:
 
   WineColorEnum:
     description: >-
       The hue classifications a wine kind can present.
     permissible_values:
       red:
-        description: Fermented with extended contact between juice and dark grape skins.
+        description: >-
+          Fermented with extended contact between juice and dark grape
+          skins.
       white:
-        description: Fermented with minimal skin contact, typically from pale-skinned grapes.
+        description: >-
+          Fermented with minimal skin contact, typically from
+          pale-skinned grapes.
       rosé:
-        description: Fermented with brief skin contact, sitting between white and red.
+        description: >-
+          Fermented with brief skin contact, sitting between white and
+          red.
 
   BodyEnum:
     description: >-
       The palate-weight classifications of a wine.
     permissible_values:
       light:
         description: Low perceived weight on the palate.
       medium:
         description: Intermediate perceived weight on the palate.
       full:
-        description: High perceived weight, typically with higher alcohol and extract.
+        description: >-
+          High perceived weight, typically with higher alcohol and
+          extract.
 
   FlavorEnum:
     description: >-
       The taste-and-aroma intensity classifications of a wine.
     permissible_values:
       delicate:
@@ -249,6 +333,19 @@
       dry:
         description: Little or no perceptible residual sugar.
       off-dry:
         description: Slightly perceptible residual sugar.
       sweet:
         description: Clearly perceptible residual sugar.
+
+  VerdictEnum:
+    description: >-
+      The quality classifications an assessment can render.
+    permissible_values:
+      exceptional:
+        description: Well above the typical quality of comparable years.
+      good:
+        description: Above typical quality.
+      average:
+        description: Typical quality.
+      poor:
+        description: Below typical quality.

VintageAssessment 7.2.1 mirrors the pairing recommendation, WineCatalog 7.2.2 is the file’s root, and the id slot 7.2.3 records the name-to-identifier refinement. The diff also shows the schema’s own description growing shorter; that is a different kind of finishing work, and the last section returns to it.

The worked example

N&M close their guide with a single instance: a Chateau Morgon Beaujolais, red, light-bodied, delicate, dry, made from the Gamay grape by the Chateau Morgon winery in the Beaujolais region. This book reproduces it faithfully, with one difference the demand-driven build made inevitable. N&M’s instance also carries a tannin level; this schema has no tannin slot, because no competency question ever asked about tannin. The instance drops the value rather than the schema growing a slot to hold it: the demand-driven rule (a slot exists because a question needs it) applied to the very last step.

Around that anchor, the catalog grows to the size the seven questions need: five grape varieties, three regions, three wineries (including N&M’s own Sterling Vineyards, whose Merlot was their example of a wine kind as an instance), the 2017 and 2018 Napa Zinfandel as two vintage-specific kinds differing in body, three foods, three pairing recommendations with their rationales, and two vintage assessments. The complete catalog is Appendix A. Those counts describe the catalog as this step built it; a later chapter (past the first tagged release) grows the worked example to exercise schema constructs and checks the seven questions never demanded, so the Appendix A you can browse is richer than the tally here.

This catalog is an instance graph conforming to the schema, not the only one. The schema is the reusable model; an instance graph is one dataset built on it, and the same schema would hold a different sommelier’s catalog, a small test set, or a production graph of thousands of wines, each an independent instance graph validated against the schema the same way. This one is deliberately small: large enough to answer the seven questions, and small enough to read whole. The published schema page carries both this catalog and the chapter’s opening preview behind a selector, so the four-record teaching graph and the full worked example sit a click apart, each rendered as its own graph of individuals and edges. This catalog is the one the page opens on (the exemplar).

Validation

The data file is a claim: that every record conforms to the schema built over the last four chapters. The claim is checkable, natively, with no tooling outside the one that generates the docs:

$ panschema validate --schema schema/wine.yaml --data data/wine-instances.yaml
data/wine-instances.yaml conforms to schema/wine.yaml

Here Chapter 6’s constraints start to bite. validate checks every facet the schema declares: that each wine has the required grape and each judgment its required wine, that confidence sits in [0, 1], that color is one of the three enumerated hues and verdict one of the four, that every maker and region reference resolves to a record that exists, and that no two records share an id. A mistyped verdict: goob fails with a named diagnostic, not a silently broken graph:

$ panschema validate --schema schema/wine.yaml --data data/invalid/bad-verdict.yaml
instance `va-2018`: slot `verdict` (class `VintageAssessment`) value `goob` is not a permissible value of enum `VerdictEnum`
Error: 1 validation error(s) in data/invalid/bad-verdict.yaml

A made_from_grape pointing at a grape that was never defined is caught the same way. These broken fixtures live in data/invalid/ and are asserted to fail in CI, so the rejection is a tested claim rather than a promise. The constraints were worth declaring precisely because this step enforces them.

The litmus test

Chapter 1 set out the competency questions as the litmus test for the finished ontology. Here they are, each answered against the catalog.

CQ 1: Which wine characteristics should I consider when choosing a wine? Answered by the schema itself, before any instance: the Wine class carries color, body, flavor, and sugar, plus its grape, region, maker, and vintage. The question is about the model, and the model answers it.

CQ 2: Is Bordeaux a red or white wine? The bordeaux-wine record has color: red. A lookup by name, one hop to a value.

CQ 3: Does Cabernet Sauvignon go well with seafood? No pairing recommendation links the Cabernet Sauvignon to a seafood dish, and none is inferable. The honest answer is “none on record”: the closed-world reading, where absence of a recommendation is not evidence against the pairing, only absence of a claim for it. The graph says what it knows and no more.

CQ 4: What is the best choice of wine for grilled meat? The pr-cabsauv-grilledmeat recommendation names the Cabernet Sauvignon, with confidence 0.9 and the rationale that its tannins bind the fats and proteins of grilled red meat. The Chapter 2 decision pays off here: a bare goes_well_with edge could have answered which wine; the reified recommendation answers which, why, on whose word, and how sure.

CQ 5: Which characteristics of a wine affect its appropriateness for a dish? No single record answers this; the answer is a pattern across the three recommendations’ rationales: tannin against protein, acidity against delicate seafood, a light body’s versatility with white meat. A query gathers the three rationales, but it can’t generalize across them; the next section takes that up.

CQ 6: Does a wine’s bouquet or body change with vintage year? The 2017 and 2018 Napa Zinfandel are two Wine records sharing grape, region, and maker but differing in vintage, and in body: medium against full. Chapter 5’s answer to the same worry, made concrete: vintage variation lives in vintage-specific kinds, not in a single kind whose values somehow change.

CQ 7: What were good vintages for Napa Zinfandel? The question that forced VintageAssessment into the schema. Two assessments name the two vintages: 2018 is good, 2017 average, each with its source and confidence. The vintage the question asks after is answerable now, and answerable with attribution: not “2018 was good” but “the regional vintage chart rates 2018 good.”

Seven questions, seven answers the graph supports. The one that comes up short of a clean query answer, CQ 5, is short in an instructive way, and it is where the graph’s real purpose comes into focus.

The graph this was always about

The schema rendered as RDF is the model; the catalog is an instance graph built on it; together they are a knowledge graph. The individuals (the wine kinds, foods, grapes, regions, wineries, and vintages) are its nodes; the provenance slots are its edges, and the two judgment classes are individuals that sit between other nodes, carrying the rationale a plain edge could not. Every decision in this book was made to make an instance graph on this schema answer questions well. Chapter 1 named the use; Chapter 2 grounded the pairing as a recommendation so it could hold a rationale; Chapter 3 held the term list to what the questions reached for; Chapter 4 made wine kinds the nodes so a recommendation could point at them; Chapter 5 reified the pairing and skipped inverses because a graph traverses both ways on its own; and Chapter 6 constrained the values so a query could count on them. The “validated artifact” of this chapter and the deployable knowledge graph are the same bytes.

CQ 4 shows what that buys a workflow that answers questions over the graph. Asked for a wine to serve with grilled meat, such a workflow does not guess: it retrieves the subgraph around grilled meat (the recommendation node, the wine it names, the rationale, the source, the confidence) and hands that to a language model as grounding. The answer it produces is not a plausible-sounding invention but a reading of what the graph records, and it can cite the node it came from. CQ 5 is the same move one step further: retrieval gathers the recommendation rationales, and the model reads the pattern across them that no single node states. A bare wine-to-food edge holds none of that. Reifying the pairing put something there to retrieve.

Chapter 1 said this ontology would be maintained by the developer building such a workflow on top of it. This chapter hands them the foundation: a schema pinned to a version, one instance graph whose competency questions are on the record as answered, and the query patterns the litmus test just walked through. Their own instance graph (their catalog, their production data) is a different, larger graph over the same schema, validated the same way and queried by the same patterns. The exemplar proves the shape; their data fills it.

What the build got right, and what it deferred

The lesson N&M leave for last is that the worked example should have driven the build from the start, not arrived at the end to validate it. It did. Every class traces to a competency question or the domain N&M set; every slot to a question that needed it; the tannin level that N&M’s own instance carried never entered the schema because nothing asked for it, and CQ 7 forced a class into being the moment the roster met the questions. Nothing in the schema is here only because this chapter needed it: the mark, N&M warn, of a build that reasoned backward from its data.

Two questions the earlier chapters deferred to this one can now be answered from the worked example. The first: whether a grape variety is better modeled as a recorded designation than as an instance. In practice, instances of Grape carried the weight the competency questions put on them (a wine names its varieties, a variety is looked up by name), and nothing asked the variety to behave like a document that is authored, versioned, or copied. The simpler reading held; the designation view stays a note for an application that needs it, not a complication this one paid for.

The second: whether the pairing recommendation, and now the vintage assessment, belong in this ontology at all, or in a companion ontology of judgments layered over it. The worked example makes the case for keeping them: both are small, both reuse the same information-content-entity grounding, and both are exactly what the graph is queried for. The signal to split would be a recommendation layer that grew its own vocabulary (provenance chains, confidence models, competing sources reconciled), heavy enough to fight the wine modeling around it. That is the companion-ontology smell this book has watched for since Chapter 2, and the wine ontology has not reached it, so the judgments stay.

One last edit is not about wine at all. Through the build, the schema’s description announced how it was made: grounded in BFO and CCO, following a method, adapted to LinkML, documented by this book. A finished artifact answers to whoever loads it, not to how it was built, and that description ships into every generated doc and downstream graph. So it now states only what the ontology is about and what it is for. The grounding is still legible to anyone who reads the structure (the prefixes, the subclass_of links), and the method is still here, in this book; neither needs to ride along in the metadata.

The schema is complete, the graph is built, and the questions Chapter 1 asked are answered. The ontology is ready for the workflow it was always being built for.

Refinements

The seven N&M steps are done, and the schema carries a v0.1.0 tag. But an ontology is not finished when its first version ships — N&M’s second fundamental rule in ontology design is that development is iterative. Using the schema, with real instances and a release behind it, turned up a handful of refinements. A few tighten the model with a constraint it lacked; others exercise a value it declared but never used.

Noy & McGuinness 2001 — some fundamental rules in ontology design

Ontology development is necessarily an iterative process.

Justifying verdicts at the extremes

Chapter 6 reified the vintage assessment: rather than hang a quality value off a wine, a VintageAssessment records a judgment about it, carrying the source and confidence a bare attribute could not. Through v0.1.0 the reification was only half-used. Nothing stopped a record from claiming verdict: exceptional with no reason given — the strongest judgment the schema allows, backed by a source name and a number, but no account of why.

An extreme verdict is exactly the one that should show its work. This chapter adds a rationale slot to the assessment (the pairing recommendation already had one, so the assessment reuses it), and a rule: when the verdict is exceptional or poor, the rationale is required.

Listing 8.1
--- wine-yaml-v5
+++ wine-yaml-v6
@@ -4,13 +4,13 @@
 description: |-
   An ontology of food and wine: the kinds of wine, the foods they
   accompany, and the characteristics that make a wine appropriate for a
   dish. It is built for applications that recommend good wine-and-food
   pairings.
 license: https://creativecommons.org/licenses/by/4.0/
-version: 0.1.0-dev
+version: 0.2.0-dev
 
 prefixes:
   # The LinkML metamodel and this schema's own namespace
   linkml: https://w3id.org/linkml/
   wine: https://w3id.org/wine-linkml/
 
@@ -121,12 +121,27 @@
       - id
       - name
       - wine
       - verdict
       - source
       - confidence
+      - rationale
+    rules:
+      - preconditions:
+          slot_conditions:
+            verdict:
+              any_of:
+                - equals_string: exceptional
+                - equals_string: poor
+        postconditions:
+          slot_conditions:
+            rationale:
+              required: true
+        description: >-
+          An exceptional or poor verdict must carry a rationale.
 
   WineCatalog:
     description: >-
       The container holding a catalog of records: wine kinds, foods,
       grapes, regions, wineries, vintage years, and the judgments
@@ -173,12 +188,14 @@
   id:
     description: >-
       The short, stable identifier a record is referenced by within a
       catalog.
     identifier: true
     range: string
+    pattern: '^[a-z0-9]+(-[a-z0-9]+)*$'
 
   name:
     description: >-
       The name by which a kind, place, producer, or recommendation is
       known. Every class carries it.
     range: string
@@ -256,13 +273,13 @@
       The food kind a pairing recommendation is about.
     range: Food
     required: true
 
   rationale:
     description: >-
-      Why the pairing works, in prose.
+      The reasoning behind a recommendation or a verdict, in prose.
     range: string
 
   source:
     description: >-
       Where a recommendation or assessment comes from (a sommelier, a
       text, a dataset).

The rule 8.1.1 is a conditional constraint: a precondition on the verdict, a postcondition on the rationale. An average or good vintage needs no defense, and the rule stays quiet. An exceptional or poor one fires it, and a missing rationale becomes an error rather than an oversight. Here is an assessment that trips it:

Listing 8.2
# Deliberately invalid: an `exceptional` verdict with no `rationale`, which
# the VintageAssessment rule forbids. Kept out of the conforming data and
# asserted to fail in CI.
grapes:
  - id: zinfandel
    name: Zinfandel
wines:
  - id: napa-zinfandel-2018
    name: Napa Zinfandel 2018
    made_from_grape: [zinfandel]
vintage_assessments:
  - id: va-2018
    name: Napa Zinfandel 2018 assessment
    wine: napa-zinfandel-2018
    verdict: exceptional
    source: regional vintage chart
    confidence: 0.9

validate rejects it and names the rule that caught it:

$ panschema validate --schema schema/wine.yaml --data data/invalid/unjustified-verdict.yaml
instance `va-2018`: rule `#1` (class `VintageAssessment`) applies, but slot `rationale` is required but absent
Error: 1 validation error(s) in data/invalid/unjustified-verdict.yaml

Supply the reason and the record conforms. The rule cannot ask for a good rationale — a validator has no way to judge that — only that a claim deviating from the average carry one. That is a low bar, and the point: the reified judgment now enforces its own minimum standard of evidence.

Every value the catalog can hold

The Step-7 catalog was deliberately small, sized to the seven competency questions and no larger. One side effect was that several enumerated values sat declared but unused: the schema admits a rosé wine, an off-dry or sweet one, and an exceptional or poor verdict, but no record reached for them. They were present in the model and absent from every picture of it.

This chapter grows the worked example to exercise them: a dry Provence Rosé, an off-dry Mosel Riesling, a sweet Late Harvest Riesling, and — the records the new rule governs — an exceptional assessment of the late-harvest wine and a poor one of the rosé, each with its rationale. Appendix A holds the grown catalog, and the published instance graph now shows the full range of colors, sweetness levels, and verdicts the schema always allowed.

This departs from the demand-driven rule that has governed the build since Step 1: none of these records answers a competency question. It is worth being honest about the trade. They exist to exercise the schema’s own vocabulary and the tooling that renders it, not to serve the pairing application. A production ontology would add them only when a question needed them; a showcase adds them so the reader can see every construct in use.

Identifier hygiene

Every id in the catalog has followed one shape by convention — lowercase, words joined by hyphens (napa-zinfandel-2018, va-2017). The convention was followed but never stated. A pattern states it 8.1.2, and an id is minted into an IRI, so a stray space or a capital is not a style nit but a broken identifier. A record with a malformed id:

Listing 8.3
# Deliberately invalid: an `id` with an uppercase letter and an underscore,
# which the lowercase-kebab-case pattern forbids. Kept out of the conforming
# data and asserted to fail in CI.
grapes:
  - id: Zinfandel_Grape
    name: Zinfandel

is caught before it can become one:

$ panschema validate --schema schema/wine.yaml --data data/invalid/bad-id.yaml
instance `Zinfandel_Grape`: slot `id` (class `Grape`) value `Zinfandel_Grape` does not match pattern `^[a-z0-9]+(-[a-z0-9]+)*$`

It projects wherever the schema goes — a SHACL sh:pattern, a JSON Schema pattern, and the check above — for the cost of one line.

What was left out

Refinement is as much about what not to add. Three constructs were weighed and left out on the same test that kept the rule and the pattern — does it improve the model, or only exercise a feature — each waiting on a demand the curated catalog does not yet make:

  • meaning: on the enum values would ground a permissible value to an external IRI, the way a class grounds with subclass_of, carrying the grounding thesis to the leaves — but the leaves have no honest anchor, since no maintained vocabulary names “the color red as a wine characteristic” or a subjective “exceptional.” It would fit a value that does, once two graphs need the same IRI to line up.
  • unique_keys on (winery, name, vintage) restates a uniqueness the id already guarantees — until records arrive from data the author does not control, where a duplicate wine becomes a risk the id cannot catch.
  • ifabsent needs a slot with a sensible default, and none here has one.

The litmus, revisited

The seven competency questions still hold against the grown catalog, and the new records extend a few of the answers. CQ 2 (“is Bordeaux red or white?”) is unchanged — a lookup is a lookup. But a question over the color or sweetness range now sees more than reds, whites, and dry wines: it sees a rosé and a sweet one too. CQ 7 (“what were the good vintages?”) gains the exceptional late-harvest Riesling — and, because of the new rule, gains it with a reason attached, which is the shape a retrieval-augmented answer wants. The questions did not change; the catalog answers them more fully.

Saying the range once

Even though the schema declared default_range: string, four slots (id, name, rationale, source) repeated the range: string declaration. We’ve removed the four redundant entries, so a range: line now means that the slot differs from the default.

Listing 8.4
--- wine-yaml-v6
+++ wine-yaml-v7
@@ -6,9 +6,9 @@
   accompany, and the characteristics that make a wine appropriate for a
   dish. It is built for applications that recommend good wine-and-food
   pairings.
 license: https://creativecommons.org/licenses/by/4.0/
-version: 0.2.0-dev
+version: 0.2.0
 
 prefixes:
   # The LinkML metamodel and this schema's own namespace
   linkml: https://w3id.org/linkml/
@@ -189,17 +189,15 @@
     description: >-
       The short, stable identifier a record is referenced by within a
       catalog.
     identifier: true
-    range: string
     pattern: '^[a-z0-9]+(-[a-z0-9]+)*$'
 
   name:
     description: >-
       The name by which a kind, place, producer, or recommendation is
       known. Every class carries it.
-    range: string
     # required: every competency question addresses its entities by
     # name.
     required: true
 
@@ -276,15 +274,13 @@
 
   rationale:
     description: >-
       The reasoning behind a recommendation or a verdict, in prose.
-    range: string
 
   source:
     description: >-
       Where a recommendation or assessment comes from (a sommelier, a
       text, a dataset).
-    range: string
 
   confidence:
     description: >-
       How strongly the source backs the judgment, from 0 to 1.

(The version line is the v0.2.0 release bump catching up. The v6 listing was frozen just before the tag.)

This waited on the toolchain. Until panschema#89, only the JSON Schema writer honored default_range. The RDF, the SHACL shapes, and validate ignored it, so the terse style would have shipped datatype properties with no range at all. The fix resolves the default into the slot definitions when the schema loads, and the regenerated artifacts are byte-identical to before the deletion.

Where it stands

The schema is tagged v0.2.0. None of these refinements is a new N&M step; each came from using the ontology, not from the method. That is the second rule — development is iterative — exercised through use.

Questions as Data

Chapter 1 sketched seven competency questions (CQs) and called them the litmus test. Chapter 7 ran that test: each question answered in prose against the catalog. Chapter 8 grew the catalog and walked the answers again. Both walks were a person reading prose against a file, and that is the weakness: a prose answer has no tooling behind it. Edit the catalog and nothing tells you an answer went stale.

Noy & McGuinness 2001 — §Step 1, Competency questions

These questions will serve as the litmus test later: Does the ontology contain enough information to answer these types of questions?

This chapter turns the CQs into benchmark data. The vocabulary for the benchmark is cqa, a schema for competency question-and-answer (CQ&A) records: one record pairs a question with the full specification of its correct answer (the ground_truth in prose, the answer_kind the question demands, and the records in the target graph a correct answer must reach and cite). cqa is itself a LinkML ontology, built through the same seven steps as this book, and the two builds are entangled: cqa has no domain graph of its own (a benchmark is meaningless without a graph to benchmark, and a contract’s instances are always a consumer’s), so wine’s benchmark is the worked example that pulled cqa’s initial set of classes and slots into existence. The demand-driven rule that has governed this build since Step 1 ran across the cqa/wine repository boundary.

Six questions, encoded

The benchmark is one file, data/wine-benchmark.yaml. It conforms to cqa, not to wine: the records below are instances of cqa’s CompetencyQuestionAnswer, and every wine record they mention is reached by reference rather than copied in: each expected_anchors value 9.1.3 is a bare id, expanded against the target namespace (target_schema) 9.1.1 the file declares exactly once.

Listing 9.1
# The worked example: wine's competency questions as a cqa benchmark.
#
# Every anchor names a record in wine's published worked-example graph.
# No wine record is copied here — this file states what a correct answer
# must reach, and the graph it reaches into lives in another repository
# at the version named below. Anchors are written short: the target
# schema's namespace is declared once, and each bare id expands against
# it.
#
# CQ-wine 1 is absent on purpose. It asks which characteristics to consider
# when choosing a wine, which wine's schema answers before any instance
# exists, so there is no record to anchor.
id: wine-benchmark
name: Wine competency questions
description: >-
  The seven competency questions of the wine ontology, less the one its
  schema answers, each paired with the specification of its correct answer
  over wine's worked-example graph.

target_schema: https://w3id.org/wine-linkml/
target_schema_version: 0.2.0
target_dataset: worked-example
target_dataset_version: 0.2.0

competency_question_answers:
  - id: cq-02
    name: Bordeaux colour
    question: Is Bordeaux a red or white wine?
    ground_truth: >-
      Red. The bordeaux-wine record carries color: red — a lookup by name,
      one hop to a value.
    answer_kind:
      - lookup
    expected_anchors:
      - bordeaux-wine

  - id: cq-03
    name: Cabernet Sauvignon with seafood
    question: Does Cabernet Sauvignon go well with seafood?
    ground_truth: >-
      None on record. No pairing recommendation links the Cabernet
      Sauvignon to a seafood dish, and none is inferable. Under a
      closed-world reading the absence of a recommendation is not evidence
      against the pairing, only the absence of a claim for it.
    answer_kind:
      - closed-world-negative
    expected_anchors:
      - cabernet-sauvignon
      - seafood-dish
    unconnected_anchors:
      - cabernet-sauvignon
      - seafood-dish
    connecting_class: https://w3id.org/wine-linkml/PairingRecommendation

  - id: cq-04
    name: Wine for grilled meat
    question: What is the best choice of wine for grilled meat?
    ground_truth: >-
      The Cabernet Sauvignon, on the pr-cabsauv-grilledmeat recommendation,
      with confidence 0.9 and the rationale that its tannins bind the fats
      and proteins of grilled red meat.
    answer_kind:
      - attribution
    expected_anchors:
      - grilled-meat
      - cabernet-sauvignon
      - pr-cabsauv-grilledmeat
    expected_citations:
      - pr-cabsauv-grilledmeat

  - id: cq-05
    name: Characteristics affecting a pairing
    question: >-
      Which characteristics of a wine affect its appropriateness for a
      dish?
    ground_truth: >-
      A pattern across the three recommendations' rationales, which no
      single record states: tannin against protein, acidity against
      delicate seafood, and a light body's versatility with white meat.
    answer_kind:
      - synthesis
    expected_anchors:
      - pr-cabsauv-grilledmeat
      - pr-chardonnay-seafood
      - pr-beaujolais-chicken
      - cabernet-sauvignon
      - napa-chardonnay
      - chateau-morgon-beaujolais
    expected_citations:
      - pr-cabsauv-grilledmeat
      - pr-chardonnay-seafood
      - pr-beaujolais-chicken

  - id: cq-06
    name: Vintage variation
    question: Does a bouquet or body of a specific wine change with vintage year?
    ground_truth: >-
      Yes. The 2017 and 2018 Napa Zinfandel share grape, region and maker
      and differ in vintage and in body: medium against full.
    answer_kind:
      - comparison
    expected_anchors:
      - napa-zinfandel-2017
      - napa-zinfandel-2018
      - vy-2017
      - vy-2018

  - id: cq-07
    name: Good vintages for Napa Zinfandel
    question: What were good vintages for Napa Zinfandel?
    ground_truth: >-
      2018. The regional vintage chart rates 2018 good and 2017 average,
      each with its source and confidence. The answer carries attribution:
      not "2018 was good" but "the regional vintage chart rates 2018 good."
    answer_kind:
      - attribution
      - comparison
    expected_anchors:
      - napa-zinfandel-2018
      - napa-zinfandel-2017
      - va-napa-zin-2018
      - va-napa-zin-2017
    expected_citations:
      - va-napa-zin-2018
      - va-napa-zin-2017

Six records, not seven. CQ 1 (“which characteristics should I consider?”) has no row because Chapter 7 answered it with the schema itself: the Wine class carries color, body, flavor, and sugar before any instance exists. There is no record to anchor and no honest way to invent one, so a retrieval benchmark cannot express the question. The file says so in a comment; the omission is a finding about what this kind of benchmark covers, not a slip.

The claims are about one graph at one version, and the target_schema and target_dataset fields 9.1.1 pin both. answer_kind 9.1.2 names the check a correct answer must survive (a lookup is one hop to a value, a synthesis is a pattern no single record states), and CQ 7 carries two kinds at once 9.1.7, because “2018 was good” is worse than “the regional vintage chart rates 2018 good,” and dropping either the comparison or the attribution makes the answer weaker.

CQ 3’s answer is an absence, and unconnected_anchors states which absence 9.1.4: it is not enough to anchor the wine and the dish, because a consumer of the answer cannot tell “correctly found no pairing” from “retrieved nothing” unless the claimed gap is named. connecting_class narrows the claim 9.1.5 to exactly what Chapter 7’s prose said: no PairingRecommendation joins them. And expected_citations 9.1.6 only ever names judgment-side records, because the recommendation carries the rationale, source, and confidence an attributed answer rests on; the reference-side entities (wines, foods, vintages) anchor but never cite.

Two schemas, two graphs, one manifest

The benchmark enters panschema.toml as a second schema with its own dataset, beside wine’s. The manifest now composes a system rather than listing outputs: wine’s schema with its catalog, cqa’s with the benchmark, and references crossing from one graph into the other. Verification covers the composition (each graph against its own schema, plus what must hold at the crossing):

Listing 9.2
# panschema consumer manifest: the schemas and datasets this repo
# composes, and what must hold across them. Bare `panschema verify`
# checks everything declared here without writing outputs; `panschema
# generate` emits the machine-readable formats to site/artifacts/
# (deployed with the docs site). HTML documentation comes from
# `panschema publish` + panschema-publish.toml, not here.
#
# `path = "."` references THIS repo's own package (the panschema-publish.toml
# at the root) — the local-package field, distinct from `source`, which is for
# remote `github:` dependencies. Format keys are deny_unknown_fields, so a typo
# is a hard parse error, not a skip: note `json_schema` (underscore) but
# `instance-graph-json` (hyphens). Postgres is deliberately omitted: wine's
# multivalued attributes make that writer skip most classes.
[schemas.wine]
path = "."

[generate.wine]
shacl = "site/artifacts/wine-shapes.ttl"
json_schema = "site/artifacts/wine.schema.json"
ttl = "site/artifacts/wine.ttl"
instance-graph-json = "site/artifacts/wine-instances.graph.json"
instances = ["data/wine-instances.yaml"]

[schemas.cqa]
source = "github:padamson/cqa-schema"
version = "0.1.0"

[generate.cqa]
ttl = "site/artifacts/wine-benchmark.ttl"
instances = ["data/wine-benchmark.yaml"]

# What a benchmark record means (which slots carry absence claims, what
# short anchors expand against) is declared in cqa's schema, on the
# slots themselves. This block only says which graphs to hold the claims
# against.
[check.cqa]
resolve_against = ["wine"]
require_namespace_coverage = true

The contract is a released dependency 9.2.1: source says where it is published and version pins the release. panschema fetch resolves the pin once, downloads the tagged release into a local cache, and writes panschema.lock, which records the checksum the pin resolved to and is committed beside the manifest. From then on every run of verify and generate reads the contract the lockfile names, and CI runs fetch --check first: a fetch that drifts from the lockfile fails before the checks start. Moving to a later release is one edit to version and one re-run of fetch, and verification against the new contract says whether the benchmark still conforms to it. The entry also publishes the benchmark as a self-contained knowledge graph 9.2.2 next to wine.ttl.

The [check.cqa] block declares what must hold at the crossing, and wine cannot delegate it. An anchor is an external reference, and panschema deliberately exempts external references from its dangling check: a benchmark’s whole shape is pointing into a graph it does not contain. Whether those references actually resolve is a question about the graph pair, answerable only where both graphs exist, which is this repository and nowhere else. resolve_against 9.2.3 checks every anchor against the IRIs wine’s datasets mint, and require_namespace_coverage 9.2.4 closes the remaining hole: mistype the target base by one character and all twenty-eight references expand into a namespace no entry covers, which is rejected rather than passed as “external, unchecked.”

Nothing in the block configures the absence check. That is on purpose: a consumer does not get to decide what a benchmark record means. cqa’s schema declares, on the slot itself, that unconnected_anchors carries an absence claim and that connecting_class narrows it, the way identifier: true is declared where the slot is defined rather than in every file that uses it. The verifier reads the meaning from the contract, so every consumer runs the same check; there is no per-repo binding to get wrong. The absence claim is then discharged the same way the dangling check tests a positive reference: “this record exists over there” and “no record over there joins these” are both statements about the referenced graph.

The manifest keys themselves know nothing about benchmarks: they would hold any pair of schemas whose graphs reference each other (two catalogs sharing a vocabulary, an inventory pointing into a registry). Cross-graph verification is the general capability; the benchmark is its first customer, the same way this book’s earlier chapters were the first customer of default_range resolution and the rule syntax. One command runs all of it from the manifest, writing nothing:

$ panschema verify --strict
Using manifest: panschema.toml
note: 28 cross-graph reference(s) leave this dataset and are not checked here:
  `cq-02` references `https://w3id.org/wine-linkml/bordeaux-wine` via `expected_anchors`
  `cq-03` references `https://w3id.org/wine-linkml/cabernet-sauvignon` via `expected_anchors`
  ... (26 more lines, one per outbound reference)
note: schema `cqa`: 28 of 28 cross-graph reference(s) into `wine` namespace(s) resolve
note: schema `cqa`: 1 of 1 stated absence claim(s) hold against `wine`
note: schema `cqa`: 2 of 2 version pin(s) agree with `wine`

The output looks like it contradicts itself (“not checked here,” then “28 of 28 resolve”). It comes from two passes with different scopes. The first verifies the benchmark as one dataset against its schema, and a dataset is a single instance file: within it, records reference each other by bare id, and those references are checked for dangling targets. An anchor is the other kind of reference: before anything reads it, the bare id expands against the declared target namespace into an IRI pointing at a record in a different dataset (napa-zinfandel-2017 is a record in data/wine-instances.yaml, minted into that same IRI under wine’s schema). The expansion is also why the console output above shows full IRIs for values the file writes in one word. A file-scoped pass cannot resolve those references, and in the general case never could: a benchmark normally points at a graph that lives in another repository altogether. So it enumerates them instead (the benchmark’s entire claim on the catalog, one line per reference; the same record IRI can appear on several) and hands off. The three notes after it are the [check.cqa] pass, which exists because this repository is the special case where the target graph is on hand: it loads wine’s datasets, mints their IRIs, and discharges the list. Every reference resolves, the stated absence holds, and the two versions the benchmark pins are the ones wine’s package declares.

Each of the four gates (resolution, the absence, namespace coverage, the version pins) was made to fail once during authoring and caught each time. The failure modes themselves are the panschema and cqa toolchain’s to test, and its suite pins them; this chapter shows the passing run, which is the state every push must reproduce.

What keeps it true

“Checking the benchmark” can mean two different things, and only one of them happens in this repository. verify --strict checks the answer key itself: every record the key points at exists, and the one claim it makes about the graph’s shape holds. Nobody here answers the questions; that is what the benchmark’s other fields are shaped for, and it is out of scope for this book. The closed-world negative is the boundary case: its correct answer is the key’s claim about the graph, so verifying the claim is as close to answering the question as verification gets. For the other kinds, the ground truth’s content stays a prose claim (nothing checks that bordeaux-wine is in fact red).

Verification and validation

Systems engineering keeps these words apart: verification asks whether the thing was built right (the artifact matches its specification), validation asks whether the right thing was built (it serves the need) — solving the equations right versus solving the right equations (Roache 1997). This book uses the terms strictly, and the tool’s verb agrees: everything panschema verify does is verification. Chapter 7 said “validation,” following the data-engineering convention (SHACL produces “validation reports,” JSON Schema tools are “validators,” LinkML ships linkml-validate) — a parallel, older sense of the word that is fine in tools whose scope holds no validation in the strict sense. This stack holds both activities, so it needs both words: the verb was renamed once the distinction started doing real work, and the neighboring formats keep their own names. Validation in the strict sense is the litmus test itself: the competency questions are the ontology’s requirements, and answering them from the graph is the evidence the right ontology was built. Chapters 7 and 8 ran that test by hand; nothing in this chapter runs it by machine.

CI runs the manifest-wide verify on every push, so the litmus test that was a section of prose is now a gate: an anchor that stops resolving, or an absence that stops holding, fails the build before anything publishes.

The failure that gate cannot see is the quiet one. Removing a record breaks an anchor loudly; changing one breaks nothing visible. Correct the 2018 vintage assessment’s verdict from good to average and CQ 7’s anchors still resolve, its citations are still anchors, verification still passes — and the benchmark’s ground truth is simply wrong. The pinned target versions exist for exactly this failure, and they are checked, not just declared: cqa says on each version slot which sibling it records, and the verifier compares the pin with the version wine’s package actually declares. A pin that falls behind fails the strict run. That sets a standing rule for this repository: any catalog edit that could change an answer bumps the package version, corrections included, and the bump keeps the build red until the benchmark has been re-read against the new graph and re-pinned. A consumer pinning the old version and a consumer reading main may then disagree about what the benchmark asserts, but each knows which claim it holds.

The version, decided

Implementing the benchmark changed nothing in schema/wine.yaml. The demand ran the other way: wine’s questions pulled cqa’s model into existence, and wine’s own model already held every record the answers anchor. So v0.2.0 stands.

A bump was considered anyway, for the package (it now ships a third dataset), and rejected. The schema’s version: field is ontology metadata: it becomes owl:versionInfo in the published artifact, so stamping a new version on an unchanged model would announce a change that did not happen. And the falsification rule above governs edits that change answers; adding the benchmark changes none (the benchmark targets 0.2.0, and every claim in it was verified against that graph). The benchmark rides the bleeding edge until the next release whose model actually changes, which will carry it into a version.

Two pages

The published site now carries two schema-docs pages. Wine’s own page is unchanged. The new page renders the contract’s schema with wine’s benchmark as its instance graph (the records lead, cqa’s reference section follows) under schema/cqa/. Today it exists only at the bleeding edge (schema/cqa/main/), because no released version of the package contains the benchmark; the first release that does will give it the same version dropdown and current/ alias as the main page. The book’s toolbar button becomes a menu with an entry for each.

The pages are meant to be read side by side. Every anchor on the benchmark page is a record on the catalog page, and checking that correspondence by eye is how the benchmark was authored; the [check.cqa] gates re-check it on every push. The pair also shows the division of labor at a glance: one page holds what the catalog says, the other holds what a correct answer about it must reach.

Where it stands

The schema stands at v0.2.0, untouched. Six of the seven competency questions are now records whose answer key a machine keeps sound on every push; the seventh is answered by the schema and stays prose, which is the right home for it. The contract freezes this same file as the worked example in its own book: one benchmark serving two ontologies, each the demand for the other.

Appendix A: The Worked Example

This is the full wine catalog Chapter 7 builds: the instance graph that answers all seven competency questions. It is a single LinkML data file, data/wine-instances.yaml, and it conforms to the schema — panschema validate confirms it, and the publish step revalidates it before the site goes out.

Chapter 7 opens with a four-record preview to introduce what an individual and an edge are; this is the larger graph that preview stood in for. Both are published on the schema page behind an in-page selector, so the two can be read together: the preview to learn the shape, this catalog to watch it carry the questions. The schema is one model; these are two instance graphs conforming to it, and a deployment’s own catalog would be a third.

Listing A.1 — The complete wine catalog
# The worked example: a wine catalog rich enough to answer all seven of
# the competency questions. Chateau Morgon Beaujolais is Noy &
# McGuinness's own Step 7 instance, reproduced faithfully.
title: The worked example
description: >-
  The full catalog: enough wines, grapes, regions, wineries, foods, and
  recorded judgments to answer all seven competency questions.
grapes:
  - id: gamay
    name: Gamay
  - id: cabernet-sauvignon-grape
    name: Cabernet Sauvignon
  - id: zinfandel
    name: Zinfandel
  - id: merlot
    name: Merlot
  - id: chardonnay
    name: Chardonnay
  - id: grenache
    name: Grenache
  - id: riesling
    name: Riesling

regions:
  - id: beaujolais
    name: Beaujolais
  - id: bordeaux-region
    name: Bordeaux
  - id: napa
    name: Napa Valley
  - id: provence
    name: Provence
  - id: mosel
    name: Mosel

wineries:
  - id: chateau-morgon
    name: Chateau Morgon
  - id: sterling-vineyards
    name: Sterling Vineyards
  - id: napa-cellars
    name: Napa Cellars

vintage_years:
  - id: vy-2017
    name: "2017"
  - id: vy-2018
    name: "2018"

wines:
  - id: chateau-morgon-beaujolais
    name: Chateau Morgon Beaujolais
    color: red
    body: light
    flavor: delicate
    sugar: dry
    made_from_grape: [gamay]
    maker: chateau-morgon
    region: beaujolais
  - id: bordeaux-wine
    name: Bordeaux
    color: red
    body: full
    flavor: strong
    sugar: dry
    made_from_grape: [cabernet-sauvignon-grape, merlot]
    region: bordeaux-region
  - id: cabernet-sauvignon
    name: Cabernet Sauvignon
    color: red
    body: full
    flavor: strong
    sugar: dry
    made_from_grape: [cabernet-sauvignon-grape]
  - id: sterling-merlot
    name: Sterling Vineyards Merlot
    color: red
    body: medium
    flavor: moderate
    sugar: dry
    made_from_grape: [merlot]
    maker: sterling-vineyards
    region: napa
  - id: napa-chardonnay
    name: Napa Chardonnay
    color: white
    body: full
    flavor: moderate
    sugar: dry
    made_from_grape: [chardonnay]
    maker: napa-cellars
    region: napa
  - id: napa-zinfandel-2017
    name: Napa Zinfandel 2017
    color: red
    body: medium
    flavor: moderate
    sugar: dry
    made_from_grape: [zinfandel]
    maker: napa-cellars
    region: napa
    vintage: vy-2017
  - id: napa-zinfandel-2018
    name: Napa Zinfandel 2018
    color: red
    body: full
    flavor: strong
    sugar: dry
    made_from_grape: [zinfandel]
    maker: napa-cellars
    region: napa
    vintage: vy-2018
  - id: provence-rose
    name: Provence Rosé
    color: rosé
    body: light
    flavor: delicate
    sugar: dry
    made_from_grape: [grenache]
    region: provence
  - id: mosel-riesling
    name: Mosel Riesling
    color: white
    body: light
    flavor: moderate
    sugar: off-dry
    made_from_grape: [riesling]
    region: mosel
  - id: late-harvest-riesling
    name: Late Harvest Riesling
    color: white
    body: full
    flavor: strong
    sugar: sweet
    made_from_grape: [riesling]
    region: mosel

foods:
  - id: seafood-dish
    name: Seafood dish
  - id: grilled-meat
    name: Grilled meat
  - id: roast-chicken
    name: Roast chicken

pairing_recommendations:
  - id: pr-cabsauv-grilledmeat
    name: Cabernet Sauvignon with grilled meat
    wine: cabernet-sauvignon
    food: grilled-meat
    rationale: >-
      The tannins in a full-bodied Cabernet Sauvignon bind the fats and
      proteins of grilled red meat, softening the wine and cleansing the
      palate between bites.
    source: House sommelier
    confidence: 0.9
  - id: pr-chardonnay-seafood
    name: Napa Chardonnay with a seafood dish
    wine: napa-chardonnay
    food: seafood-dish
    rationale: >-
      A crisp white with bright acidity lifts delicate seafood without
      overwhelming it; the wine's weight matches the dish's.
    source: House sommelier
    confidence: 0.85
  - id: pr-beaujolais-chicken
    name: Beaujolais with roast chicken
    wine: chateau-morgon-beaujolais
    food: roast-chicken
    rationale: >-
      A light-bodied, low-tannin red is versatile enough for white meat;
      its delicate flavor complements roast chicken rather than masking
      it.
    source: Pairing guide
    confidence: 0.7

vintage_assessments:
  - id: va-napa-zin-2018
    name: Napa Zinfandel 2018 assessment
    wine: napa-zinfandel-2018
    verdict: good
    source: Regional vintage chart
    confidence: 0.8
  - id: va-napa-zin-2017
    name: Napa Zinfandel 2017 assessment
    wine: napa-zinfandel-2017
    verdict: average
    source: Regional vintage chart
    confidence: 0.75
  - id: va-late-harvest-riesling
    name: Late Harvest Riesling assessment
    wine: late-harvest-riesling
    verdict: exceptional
    source: Regional vintage chart
    confidence: 0.9
    rationale: >-
      A long, dry autumn let botrytis concentrate the sugars fully, giving
      a depth and balance that lift the vintage well above its peers.
  - id: va-provence-rose
    name: Provence Rosé assessment
    wine: provence-rose
    verdict: poor
    source: Regional vintage chart
    confidence: 0.7
    rationale: >-
      A cool, wet growing season left the fruit underripe and the wine
      thin and short, below the appellation's usual standard.

List of Listings

Every frozen snapshot the book embeds, in the order it first appears, grouped by chapter.

Domain and Scope

Reusing Existing Ontologies

Classes and Hierarchy

Slots

Slot Usage and Facets

Instances and Validation

Refinements

Questions as Data

Appendix A: The Worked Example