diff --git a/crates/ogar-class-view/src/lib.rs b/crates/ogar-class-view/src/lib.rs index 20061af7..327fd8ea 100644 --- a/crates/ogar-class-view/src/lib.rs +++ b/crates/ogar-class-view/src/lib.rs @@ -158,6 +158,8 @@ use ogar_vocab::{ unit_of_measure, visit, vital_sign, + weather_cell, + weather_static_cell, }; /// All promoted canonical concepts: `(canonical_concept_name, Class)`. @@ -207,6 +209,12 @@ fn all_canonical_classes() -> Vec<(&'static str, Class)> { ("pricelist", pricelist()), ("pricelist_rule", pricelist_rule()), ("unit_of_measure", unit_of_measure()), + // ── 0x04XX — Weather / Atmosphere ── + // These canonical views intentionally carry no W1 payload fields: + // field/level/unit slots are selected by WeatherNext's ClassView + // manifest, not promoted into the shared OGAR schema. + ("weather_cell", weather_cell()), + ("weather_static_cell", weather_static_cell()), // ── 0x08XX — OCR (container kinds; content stays in content stores) ── ("unicharset", unicharset()), ("recoder", recoder()), diff --git a/crates/ogar-vocab/src/capability_registry.rs b/crates/ogar-vocab/src/capability_registry.rs index 3dff13fa..44bf1a0b 100644 --- a/crates/ogar-vocab/src/capability_registry.rs +++ b/crates/ogar-vocab/src/capability_registry.rs @@ -372,7 +372,7 @@ fn resolve_concept_row(id: u16) -> Option<(&'static str, u16)> { mod the_canon_carries_no_palette_rows { #[test] fn no_0x17xx_row_reached_the_globally_mirrored_codebook() { - assert_eq!(crate::class_ids::ALL.len(), 91); + assert_eq!(crate::class_ids::ALL.len(), 93); for (_, id) in crate::class_ids::ALL { assert_ne!(*id >> 8, 0x17, "a 0x17XX row reached the codebook"); } diff --git a/crates/ogar-vocab/src/lib.rs b/crates/ogar-vocab/src/lib.rs index cdc8f54f..053386cd 100644 --- a/crates/ogar-vocab/src/lib.rs +++ b/crates/ogar-vocab/src/lib.rs @@ -1104,7 +1104,7 @@ impl Class { /// 0x01XX project-mgmt (OP ↔ Redmine fork lineage) /// 0x02XX commerce / ERP (OSB ↔ Odoo cross-curator) /// 0x03XX Ontology (OBO biomedical reference: MONDO/HPO/Uberon/PATO/RO — zero rows here; concepts in ogar-obo) -/// 0x04XX unassigned +/// 0x04XX Weather / Atmosphere (forecast + atmospheric reference cells) /// 0x05XX unassigned /// 0x06XX unassigned /// 0x07XX reserved: OSINT @@ -1222,6 +1222,12 @@ const CODEBOOK: &[(&str, u16)] = &[ // consumers (odoo-rs, openproject-nexgen-rs, …) never pull them into their // concept space. Public reference, firewall-separated from `0x09` Health // PHI — same reference≠PHI split as Anatomy (0x0A). Do NOT mint rows here. + // ── 0x04XX — Weather / Atmosphere domain ── + // Canonical atmospheric/grid concepts consumed by WeatherNext and the + // lance-graph weather SoA bake. These are shared meanings; renderer / + // ClassView selection remains in the low u16 of the full classid. + ("weather_cell", 0x0401), + ("weather_static_cell", 0x0402), // ── 0x07XX — OSINT domain: ZERO vocabulary rows BY DESIGN (operator // ruling 2026-07-02, corrects PR #145's two hallucinated concept mints // `osint_system@0x0700` / `osint_person@0x0701`). Within the OSINT domain @@ -1417,6 +1423,9 @@ pub enum ConceptDomain { /// never pulls into ERP / project consumers. Public reference, NOT PHI — /// same reference≠PHI split as [`Anatomy`](Self::Anatomy). Ontology, + /// `0x04XX` — Weather / Atmosphere. Shared atmospheric and forecast-grid + /// concepts; public environmental reference data, not an OSM extension. + Weather, /// `0x07XX` — OSINT (open-source intelligence). Osint, /// `0x08XX` — OCR (optical character recognition / document @@ -1490,7 +1499,7 @@ pub enum ConceptDomain { /// transcribing a GPL/AGPL implementation, so this public codebook stays /// unencumbered while GPL consumers link it freely. Blocks, - /// Any high-byte slot not yet assigned a domain (`0x04XX`–`0x06XX`, + /// Any high-byte slot not yet assigned a domain (`0x05XX`–`0x06XX`, /// `0x10XX`–`0x16XX`, `0x18XX`+). Unassigned, } @@ -1504,6 +1513,7 @@ pub fn canonical_concept_domain(id: u16) -> ConceptDomain { 0x01 => ConceptDomain::ProjectMgmt, 0x02 => ConceptDomain::Commerce, 0x03 => ConceptDomain::Ontology, + 0x04 => ConceptDomain::Weather, 0x07 => ConceptDomain::Osint, 0x08 => ConceptDomain::Ocr, 0x09 => ConceptDomain::Health, @@ -1763,6 +1773,15 @@ pub mod class_ids { /// `uom.uom` (`qudt:Unit`). pub const UNIT_OF_MEASURE: u16 = 0x020B; + // ── 0x04XX — Weather / Atmosphere domain ── + + /// `weather_cell` (`0x0401`) — one dynamic atmospheric/forecast grid + /// cell. Time is a dataset/version axis, not a new concept id. + pub const WEATHER_CELL: u16 = 0x0401; + /// `weather_static_cell` (`0x0402`) — static support cell for terrain / + /// geography-derived weather context, distinct from dynamic fields. + pub const WEATHER_STATIC_CELL: u16 = 0x0402; + // ── 0x08XX — OCR domain (document extraction; the Tesseract-rs arc) ── // Class-level container KINDS only: the concept slots name the container // types the Core resolves — never their content. The 112 unichars of a @@ -2054,6 +2073,9 @@ pub mod class_ids { ("pricelist", PRICELIST), ("pricelist_rule", PRICELIST_RULE), ("unit_of_measure", UNIT_OF_MEASURE), + // 0x04XX — Weather / Atmosphere + ("weather_cell", WEATHER_CELL), + ("weather_static_cell", WEATHER_STATIC_CELL), // 0x07XX — OSINT: ZERO vocabulary rows BY DESIGN (operator ruling // 2026-07-02; see the CODEBOOK 0x07XX section note). No entries // follow — OGAR vocabulary carries no OSINT concept names. @@ -2182,7 +2204,7 @@ pub mod class_ids { // lance-graph mirror is rebuilt against it. assert_eq!( ALL.len(), - 91, + 93, "class_ids::ALL count changed — update this pin AND the \ lance-graph mirror COUNT_FUSE (crates/lance-graph-ogar/src/lib.rs) \ in the same PR", @@ -3005,6 +3027,8 @@ pub fn all_promoted_classes() -> Vec { // vocabulary carries no OSINT concept names, see the CODEBOOK // 0x07XX section note. // 0x08XX — OCR arm (9 container kinds), in class_ids::ALL order. + weather_cell(), + weather_static_cell(), unicharset(), recoder(), charset(), @@ -4653,6 +4677,30 @@ pub fn joint() -> Class { c } +// ───────────────────────────────────────────────────────────────────── +// 0x04XX — Weather / Atmosphere canonical builders. +// Field/level/unit slot semantics live in the consumer ClassView manifest; +// the canonical class carries identity, never the weather payload layout. +// ───────────────────────────────────────────────────────────────────── + +/// `weather_cell` (`0x0401`) — one dynamic atmospheric / forecast-grid cell. +#[must_use] +pub fn weather_cell() -> Class { + let mut c = Class::new("WeatherCell"); + c.language = Language::Unknown; + c.canonical_concept = Some("weather_cell".to_string()); + c +} + +/// `weather_static_cell` (`0x0402`) — static geography-derived support cell. +#[must_use] +pub fn weather_static_cell() -> Class { + let mut c = Class::new("WeatherStaticCell"); + c.language = Language::Unknown; + c.canonical_concept = Some("weather_static_cell".to_string()); + c +} + // ───────────────────────────────────────────────────────────────────── // 0x0FXX — Geo domain builders (OpenStreetMap geodata reference ontology). // The canonical reference shape of the OSM element model harvested from @@ -5570,8 +5618,10 @@ mod tests { // lives in ogar-obo; plug-and-play, never pulls into ERP consumers). assert_eq!(canonical_concept_domain(0x0300), ConceptDomain::Ontology); assert_eq!(canonical_concept_domain(0x03AB), ConceptDomain::Ontology); - // Unassigned blocks (4-6). - assert_eq!(canonical_concept_domain(0x0400), ConceptDomain::Unassigned); + // Weather / Atmosphere block (0x04), then still-unassigned 0x05-0x06. + assert_eq!(canonical_concept_domain(0x0400), ConceptDomain::Weather); + assert_eq!(canonical_concept_domain(0x0401), ConceptDomain::Weather); + assert_eq!(canonical_concept_domain(0x0500), ConceptDomain::Unassigned); assert_eq!(canonical_concept_domain(0x0600), ConceptDomain::Unassigned); // HR block (0x0D). assert_eq!(canonical_concept_domain(0x0D00), ConceptDomain::HR); @@ -6680,3 +6730,31 @@ mod tests { } } } + +#[cfg(test)] +mod weather_classid_mint_tests { + use super::*; + + #[test] + fn weather_domain_and_codebook_are_append_only_and_not_geo() { + assert_eq!( + canonical_concept_domain(class_ids::WEATHER_CELL), + ConceptDomain::Weather + ); + assert_eq!( + canonical_concept_domain(class_ids::WEATHER_STATIC_CELL), + ConceptDomain::Weather + ); + assert_ne!( + canonical_concept_domain(class_ids::WEATHER_CELL), + ConceptDomain::Geo + ); + assert_eq!(canonical_concept_id("weather_cell"), Some(0x0401)); + assert_eq!(canonical_concept_id("weather_static_cell"), Some(0x0402)); + assert_eq!(weather_cell().canonical_id(), Some(class_ids::WEATHER_CELL)); + assert_eq!( + weather_static_cell().canonical_id(), + Some(class_ids::WEATHER_STATIC_CELL) + ); + } +} diff --git a/crates/ogar-vocab/src/ports.rs b/crates/ogar-vocab/src/ports.rs index f0217340..6947a721 100644 --- a/crates/ogar-vocab/src/ports.rs +++ b/crates/ogar-vocab/src/ports.rs @@ -617,9 +617,46 @@ pub const OSM_ALIASES: &[(&str, u16)] = &[ ("User", class_ids::OSM_USER), ]; +// ── WeatherNext / weathernext-rs port ─────────────────────────────── + +/// WeatherNext's `PortSpec` over the shared Weather / Atmosphere (`0x04XX`) +/// canonical concepts. `0x0009` is the reserved WeatherNext ClassView skin; +/// the canonical meaning remains in the high u16. +pub struct WeatherNextPort; + +impl PortSpec for WeatherNextPort { + const NAMESPACE: &'static str = "WeatherNext"; + const BRIDGE_ID: &'static str = "weathernext"; + const APP_PREFIX: u16 = 0x0009; + fn aliases() -> &'static [(&'static str, u16)] { + WEATHERNEXT_ALIASES + } +} + +/// WeatherNext public names mapped onto shared canonical weather concepts. +pub const WEATHERNEXT_ALIASES: &[(&str, u16)] = &[ + ("WeatherCell", class_ids::WEATHER_CELL), + ("WeatherStaticCell", class_ids::WEATHER_STATIC_CELL), +]; + #[cfg(test)] mod tests { use super::*; + use crate::app::{app_of, concept_of, render_classid}; + + #[test] + fn weathernext_classview_composes_canon_high_custom_low() { + assert_eq!(WeatherNextPort::APP_PREFIX, 0x0009); + assert_eq!(WeatherNextPort::classview(), 0x0009); + assert_eq!(WeatherNextPort::class_id("WeatherCell"), Some(0x0401)); + assert_eq!(WeatherNextPort::class_id("WeatherStaticCell"), Some(0x0402)); + let dynamic = render_classid(WeatherNextPort::APP_PREFIX, class_ids::WEATHER_CELL); + let statics = render_classid(WeatherNextPort::APP_PREFIX, class_ids::WEATHER_STATIC_CELL); + assert_eq!(dynamic, 0x0401_0009); + assert_eq!(statics, 0x0402_0009); + assert_eq!(concept_of(dynamic), 0x0401); + assert_eq!(app_of(dynamic), 0x0009); + } #[test] fn openproject_namespace_and_bridge_id_match_canonical_strings() { diff --git a/docs/APP-CLASS-CODEBOOK-LAYOUT.md b/docs/APP-CLASS-CODEBOOK-LAYOUT.md index 9b2fc700..23453a68 100644 --- a/docs/APP-CLASS-CODEBOOK-LAYOUT.md +++ b/docs/APP-CLASS-CODEBOOK-LAYOUT.md @@ -137,7 +137,7 @@ materialised until the app mints its first private class). | `lo u16` | App / namespace | Core domain(s) it consumes | Private codebook today? | |---|---|---|---| -| `0x0000` | **Shared canonical core** | all (`0x01/02/07/08/09` + `0x0A` anatomy + `0x0B` auth + `0x0C` automation) | n/a (this *is* core) | +| `0x0000` | **Shared canonical core** | all (`0x01/02/04/07/08/09` + `0x0A` anatomy + `0x0B` auth + `0x0C` automation) | n/a (this *is* core) | | `0x0001` | OpenProject (openproject-nexgen-rs) | `0x01` project-mgmt | **no** — maps onto core | | `0x0002` | Odoo | `0x02` commerce | **no** — maps onto core (converge `od-ontology`) | | `0x0003` | WoA / woa-rs | `0x02` commerce (work orders) | **no** — maps onto core | @@ -146,6 +146,7 @@ materialised until the app mints its first private class). | `0x0006` | q2 (Gotham / aiwar / neo4j) | `0x07` osint (+ TBD) | **TBD** — port not yet authored | | `0x0007` | Redmine | `0x01` project-mgmt | **no** — same concepts as OpenProject, own templates | | `0x0008` | OpenStreetMap (openstreetmap-website-rs) | `0x0F` geo | **no** — maps entirely onto core | +| `0x0009` | WeatherNext / weathernext-rs | `0x04` weather / atmosphere | **no** — maps onto core | | `0x00A0` | (reserved) future app block | — | — | > **OpenProject (`0x0001`) and Redmine (`0x0007`) are the showcase:**