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).
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.
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.
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:
- Which wine characteristics should I consider when choosing a wine?
- Is Bordeaux a red or white wine?
- Does Cabernet Sauvignon go well with seafood?
- What is the best choice of wine for grilled meat?
- Which characteristics of a wine affect its appropriateness for a dish?
- Does a bouquet or body of a specific wine change with vintage year?
- 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.
- 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.
- Ontology development is necessarily an iterative process.
- 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:
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.
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).
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:
| Concept | Grounds to | Reuse / invent |
|---|---|---|
| Food, dish | CCO Portion of Food (cco:ont00000307) | reuse |
| Wine | CCO Portion of Processed Material (cco:ont00001084) | reuse |
| Wine region | CCO Geospatial Region (cco:ont00000472) | reuse |
| Winery | CCO Organization (cco:ont00001180) | reuse |
| Grape | BFO object (obo:BFO_0000030) | invent |
| Color, body, sugar, flavor | BFO quality (obo:BFO_0000019) | invent |
| Vintage (the year) | BFO one-dimensional temporal region (obo:BFO_0000038) | invent |
| Wine-food pairing | a 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:
# 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.
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
| Source | Terms |
|---|---|
| N&M’s Step 3 list | wine, 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 work | wine 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.
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.
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:
--- 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.”
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:
- 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_grapekeeps the name Chapter 2 already used in prose. - The pairing stays reified. There is no direct
goes_well_withedge fromWinetoFood; the relation lives as thePairingRecommendation’swineandfoodslots, 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. - No inverses. A knowledge graph traverses edges in both
directions; storing
made_byalongsidemakerwould be a second copy of the same fact. - Cardinality stays lenient. Nothing is
requiredyet, and onlymade_from_grapeismultivalued(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:
--- 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.
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
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.wineandfoodonPairingRecommendation: 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:
--- 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.
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:
# 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:
--- 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.
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.
--- 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:
# 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:
# 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 withsubclass_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_keyson (winery, name, vintage) restates a uniqueness theidalready guarantees — until records arrive from data the author does not control, where a duplicate wine becomes a risk theidcannot catch.ifabsentneeds 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.
--- 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.
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.
# 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):
# 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).
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.
# 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.