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.