Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Refinements

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

Noy & McGuinness 2001 — some fundamental rules in ontology design

Ontology development is necessarily an iterative process.

Justifying verdicts at the extremes

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

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

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

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

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

validate rejects it and names the rule that caught it:

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

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

Every value the catalog can hold

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

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

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

Identifier hygiene

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

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

is caught before it can become one:

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

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

What was left out

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

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

The litmus, revisited

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

Saying the range once

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

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

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

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

Where it stands

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