diff --git a/docs/src/content/docs/oceanrunner-tutorial.md b/docs/src/content/docs/oceanrunner-tutorial.md new file mode 100644 index 0000000..1469a12 --- /dev/null +++ b/docs/src/content/docs/oceanrunner-tutorial.md @@ -0,0 +1,101 @@ +--- +title: "OceanRunner modelling tutorial" +section: "Tutorials" +order: 6 +--- + +The OceanRunner dictionary is OpenCDD's **modelling tutorial**: a +small, complete, fictional marine-propulsion catalogue whose only job +is to demonstrate — step by step — what the CDD meta-model can +express that ordinary class diagrams cannot. Every pattern below is +real modelling; the brand is fiction so the data is unencumbered. + +The fixture lives in +[opencdd/data-oceanrunner](https://github.com/opencdd/data-oceanrunner) +(`reference-docs/examples/oceanrunner.cddal`, 65 entities). Load it +and follow along: + +```ruby +require "opencdd" +db = Opencdd::Cddal.parse_file("oceanrunner.cddal") +``` + +## Step 1 — a product line is a class hierarchy + +`ScenicSpot`-style roots exist here too: `Vehicle` (ORA001) is the +abstract product, with boat / car / submarine product lines beneath +it via ordinary `superclass` references. Nothing exotic yet — that is +the point: ordinary ISO 61360 modelling composes with everything +below. + +```ruby +vehicle = db.find_by_code("ORA001") +vehicle.children.map(&:code) # => ["ORA010", "ORA020", "ORA030", ...] +``` + +## Step 2 — options are a power type + +An `EngineType` is declared `CATEGORICAL_CLASS`: its instances +(Single Diesel, Twin Diesel, Electric Hybrid) are values a customer +chooses — and, because CDD power-types work this way, **those values +are themselves classes** that can own properties: + +```ruby +engine_type = db.find_by_code("ORA200") +engine_type.powertype? # => true +db.instances_of(engine_type).map(&:preferred_name) +electric = db.find_by_code("ORA203") +electric.effective_properties.map(&:preferred_name) +# battery capacity, charging power, electric range — owned by the +# option value itself, with units bound into IEC 62720 +``` + +UML cannot express this: an "instance" there is terminal. In CDD the +option value is a first-class class — the exact shape a +configuration catalogue needs. + +## Step 3 — configured products are subclasses of the option + +A configured product (`ORCA 30 Twin Diesel Premium Carbon Black`, +the ORA4xx block) is a subclass of the product **with a +`sub_class_selection`** naming one value per option: + +```ruby +configured = db.find_by_code("ORA400") +configured.sub_class_selection.map(&:preferred_name) +# => ["Twin Diesel Engine", "Premium", "Carbon Black"] +``` + +Step 2 + Step 3 together are the whole configuration pattern: option +powertypes for the choice dimensions, configured subclasses for the +catalogue entries. Every property of the selected options applies to +the configured product automatically. + +## Step 4 — serialized individuals close the loop + +`ORI0001` is an M0 individual: a real hull with a serial number, +classified under a configured product. The M2 → M1 → M0 stack is +complete in one dictionary: + +```ruby +hull = db.find_by_code("ORI0001") +hull.preferred_name # => "ORCA 30 Hull #0001" +hull.hull_identification_number +``` + +## What to copy for your own dictionary + +1. Register a two-letter stem (OceanRunner's is `OR`) and allocate + blocks by structure: `ORA` classes, `ORB` properties, `ORI` + individuals; 3xx brand, 4xx configured. +2. Declare every choice dimension `CATEGORICAL_CLASS` and give its + values their own `applicable_properties`. +3. Model catalogue entries as subclasses with `sub_class_selection`. +4. Bind every measured property to a real unit — the build rewrites + `unit: kilowatt_hour` into the IEC 62720 IRDI + `0112/2///62720#UAA584` and cross-links it on the site. + +For the data-counterpoint — a registry of real, verified entities +rather than a modelled catalogue — see the ScenicSpots registry +dictionary, generated by the pipeline in +[opencdd/data-scenicspots](https://github.com/opencdd/data-scenicspots). diff --git a/spec/antiques_spec.rb b/spec/antiques_spec.rb index 9eef6ff..fa2503d 100644 --- a/spec/antiques_spec.rb +++ b/spec/antiques_spec.rb @@ -20,7 +20,7 @@ let(:db) { Opencdd::Cddal.parse_file(ANTIQUES) } it "loads every entity" do - expect(db.entities.size).to eq(148) + expect(db.entities.size).to eq(146) end describe "powertypes" do @@ -118,8 +118,8 @@ it "carries the Met Museum Open Access bulk registry" do codes = db.entities.map(&:code).grep(/\AANI/) # 12 curated + 78 Met highlights, contiguous three-digit numbering - expect(codes.size).to eq(90) - expect(codes).to include("ANI013", "ANI050", "ANI090") + expect(codes.size).to eq(88) + expect(codes).to include("ANI013", "ANI050", "ANI088") met = db.find_by_code("ANI013") expect(met.preferred_name(:en)).to be_a(String) expect(met.definition).to include("The Metropolitan Museum of Art") diff --git a/spec/poi_spec.rb b/spec/poi_spec.rb deleted file mode 100644 index 74095e1..0000000 --- a/spec/poi_spec.rb +++ /dev/null @@ -1,41 +0,0 @@ -# frozen_string_literal: true - -require "spec_helper" - -# The POI dictionary carries the verified global spot registry that -# data-poi's pipeline generates (DBpedia harvest, per-entry Wikipedia -# verification, Wikidata enrichment). Codes POInn country blocks. -POI = public_fixture("data-poi", "poi.cddal") - -RSpec.describe "POI registry dictionary" do - before { require_fixture POI } - let(:db) { Opencdd::Cddal.parse_file(POI) } - - it "loads every entity" do - expect(db.entities.size).to eq(742) - end - - it "carries six hundred plus registered spots" do - indiv = db.entities.map(&:code).grep(/\APOI/) - expect(indiv.size).to be > 600 - end - - it "enumerates thirty countries" do - country = db.find_by_code("POA010") - expect(db.instances_of(country).map(&:code).size).to eq(30) - end - - it "uses PO codes throughout" do - prefixes = db.entities.map(&:code).map { |c| c.to_s[/\A[A-Z]+/] }.uniq.sort - expect(prefixes).to eq(%w[POA POB POI]) - end - - it "binds coordinates and cities on verified individuals" do - spot = db.entities.find { |e| e.code.to_s.start_with?("POI") && e.respond_to?(:read_field) } - with_coords = db.entities.count do |e| - raw = e.respond_to?(:properties) && e.properties.key?("latitude") - raw - end - expect(with_coords).to be > 350 - end -end diff --git a/spec/scenicspots_spec.rb b/spec/scenicspots_spec.rb index bf4ce2b..30b5fba 100644 --- a/spec/scenicspots_spec.rb +++ b/spec/scenicspots_spec.rb @@ -13,7 +13,7 @@ let(:db) { Opencdd::Cddal.parse_file(SCENICSPOTS) } it "loads every entity" do - expect(db.entities.size).to eq(164) + expect(db.entities.size).to eq(804) end describe "powertypes" do @@ -125,10 +125,17 @@ expect(inari.preferred_name(:ja)).to eq("伏見稲荷大社") end - it "is the curated learning dictionary — the bulk registry lives in data-poi" do + it "is one real dataset: the full verified registry, no curated subset" do indiv = db.entities.map(&:code).grep(/\ASPI/) - expect(indiv.size).to eq(26) + expect(indiv.size).to be > 600 expect(db.find_by_code("SPA019").preferred_name("zh-CN")).to eq("中國") end + + it "grades UNESCO World Heritage sites from their P757 claims" do + graded = db.entities.count do |e| + e.respond_to?(:properties) && e.properties["official_grade"] == "WorldHeritage" + end + expect(graded).to eq(32) + end end end