From 832e634fa48ba7d2c63f62ae4e5f4887c7f2def7 Mon Sep 17 00:00:00 2001 From: Ronald Tse Date: Sat, 18 Jul 2026 15:14:39 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20entity=20scoping=20=E2=80=94=20register?= =?UTF-8?q?s,=20Body,=20three-tier=20resolution,=20AgendaItem=20URN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ContactCollection/VenueCollection pages renamed to ContactRegister/VenueRegister (model rename) - new: body.md, body-register.md, entity-resolution.md (three-tier inline/local_ref/ref + Edoxen::EntityResolver) - contact.md/venue.md: ref/local_ref/urn fields, Meeting.contacts[] - agenda.md: AgendaItem.urn + Edoxen::UrnFor; decision_ref fix - meeting-collection.md: committee/committee_group are Body - schema.md: register root docs, three-tier URN pattern, lychee fix - cli.md: validate-meetings/normalize-meetings coverage - public/schemas re-synced from edoxen-model@main - sidebar nav updated --- CONTEXT.md | 17 +- docs/agenda.md | 22 ++- docs/body-register.md | 72 +++++++ docs/body.md | 85 ++++++++ docs/cli.md | 14 +- ...tact-collection.md => contact-register.md} | 25 +-- docs/contact.md | 27 ++- docs/entity-ref.md | 1 + docs/entity-resolution.md | 115 +++++++++++ docs/meeting-collection.md | 9 +- docs/officer.md | 4 +- docs/schema.md | 55 ++++-- ...{venue-collection.md => venue-register.md} | 19 +- docs/venue.md | 15 ++ public/schemas/decision-collection.yaml | 160 ++++++++++++++- public/schemas/meeting.yaml | 187 +++++++++++++++++- src/data/site.ts | 7 +- 17 files changed, 761 insertions(+), 73 deletions(-) create mode 100644 docs/body-register.md create mode 100644 docs/body.md rename docs/{contact-collection.md => contact-register.md} (66%) create mode 100644 docs/entity-resolution.md rename docs/{venue-collection.md => venue-register.md} (58%) diff --git a/CONTEXT.md b/CONTEXT.md index 8b6fe16..76ee7fb 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -17,7 +17,7 @@ documents them, it does not own them. Canonical definitions live in ### Top-level containers -The model has four top-level container roots, used depending on what +The model has five top-level container roots, used depending on what you want to capture: - **MeetingCollection** — meeting-grain. Holds meetings with agendas, @@ -25,9 +25,11 @@ you want to capture: adopted. - **DecisionCollection** — decision-grain. Holds the decisions themselves with their admin fields and per-language renderings. -- **ContactCollection** (1.0) — registry of Contacts indexed by scoped +- **ContactRegister** (1.0) — registry of Contacts indexed by scoped URN. Referenced from Meetings, Components, HostRefs via `ref:`. -- **VenueCollection** (1.0) — registry of Venues indexed by scoped URN. +- **VenueRegister** (1.0) — registry of Venues indexed by scoped URN. +- **BodyRegister** (1.0) — registry of Bodies (committees, working + groups); members matched by `code` or `ref`. ### Decision-grain entities @@ -121,9 +123,12 @@ URN format: `urn:edoxen:{entity}:{scope}:{local-id}`. - `scope`: the dataset/registry name (e.g. `isotc154`, `oiml`). - `local-id`: local identifier within that scope. -Any entity-typed field accepts either **inline data** (full object) or -**a URN reference** (`{ ref: urn:edoxen:contact:... }`). Both patterns -are valid; the discriminator is presence of `ref`. +Any entity-typed field accepts **inline data** (full object), a +**document-scoped reference** (`{ local_ref: ... }` resolved against the +containing document's scoped collection), or **a URN reference** +(`{ ref: urn:edoxen:contact:... }` resolved against the matching +top-level Register). The discriminator is presence of `ref` / +`local_ref`. ## Site-level presentation terms diff --git a/docs/agenda.md b/docs/agenda.md index 6f0b2b2..22de2dc 100644 --- a/docs/agenda.md +++ b/docs/agenda.md @@ -48,13 +48,16 @@ agenda: | Field | Type | Description | |---|---|---| +| `urn` | `String` | First-class URN, hierarchical under the parent meeting URN (`{meetingUrn}:agenda:{label}`). Optional in source data — computable via `Edoxen::UrnFor.agenda_item` (see below). | | `label` | `String` | The printed number (`"4.2"`, `"10.a"`). May be empty for headers. | | `kind` | `AgendaItemKind` | One of `numbered`, `unnumbered`, `header`, `opening`, `closing`. | | `title` | `String` | Short title as printed. | | `description` | `String` | Optional body / context. | | `references` | `Reference[0..*]` | Supporting documents (background papers, prior resolutions). | | `outcome` | `AgendaItemOutcome` | What happened with this item. | -| `resolution_ref` | `String` | If the item produced a resolution, its identifier. | +| `decision_ref` | `String` | URN of the decision this item produced, if any. | +| `topics` | `Topic[0..*]` | The subject(s) of discussion at this item. | +| `components` | `String[0..*]` | MeetingComponent identifiers covering this item. | ### AgendaItemKind @@ -83,6 +86,23 @@ Captures what the meeting did with this item. The enum is - `adopted` — meeting adopted the proposal as presented - `withdrawn` — proponent withdrew the item +### AgendaItem URNs + +An item's `urn` is derived from the parent meeting URN plus the item +label — `{meetingUrn}:agenda:{label}`. It is optional in source data; +the gem computes it on demand and can backfill a whole agenda: + +```ruby +Edoxen::UrnFor.agenda_item(meeting_urn: "urn:oiml:ciml:meeting:ciml-60", label: "6.2") +# => "urn:oiml:ciml:meeting:ciml-60:agenda:6.2" + +Edoxen::UrnFor.parse("urn:oiml:ciml:meeting:ciml-60:agenda:6.2") +# => { meeting_urn: "urn:oiml:ciml:meeting:ciml-60", label: "6.2" } + +Edoxen::UrnFor.assign_to_agenda!(meeting.agenda, meeting.urn) +# sets urn on every item that doesn't already carry one +``` + ## References `Reference` is a small lightweight citation attached to agenda items. diff --git a/docs/body-register.md b/docs/body-register.md new file mode 100644 index 0000000..207ee0f --- /dev/null +++ b/docs/body-register.md @@ -0,0 +1,72 @@ +--- +title: Body Register +--- + +# BodyRegister + +The authoritative register of [Bodies](/docs/body) — committees, +subcommittees, and working groups. Parallel to +[ContactRegister](/docs/contact-register) and +[VenueRegister](/docs/venue-register). + +Other documents (Meeting, MeetingCollection) reference bodies via +`ref: urn:edoxen:body:{scope}:{local-id}` and resolve against the +matching BodyRegister. + +```yaml +scope: oiml +title: + - spelling: eng + value: OIML Bodies +bodies: + - code: CIML + kind: committee + name: + - spelling: eng + value: International Committee of Legal Metrology + - spelling: fra + value: Comité international de métrologie légale + - code: TC18 + kind: technical_committee + name: + - spelling: eng + value: 'TC 18 Instruments of measurement' + parent_ref: + urn: urn:edoxen:body:oiml:ciml +``` + +## Fields + +| Field | Type | Description | +|---|---|---| +| `scope` | `String` | Registry scope (matches the scope segment in referencing URNs). | +| `title` | `LocalizedString[0..*]` | Display title (localized). | +| `bodies` | `Body[0..*]` | Registry members. | +| `extensions` | `MeetingExtension[0..*]` | Profile-specific extensions. | + +## Lookup: matched by `code` or `ref` + +Body has **no `urn` attribute** — a difference from Contact and Venue, +whose register members carry `urn`. `BodyRegister#find_by_urn` therefore +matches the requested value against a member's `code` **or** `ref`: + +```ruby +register = Edoxen::BodyRegister.from_yaml(File.read("bodies.yaml")) + +register.find_by_urn("CIML") +# => # (matched on code) + +register.find_by_urn("urn:edoxen:body:oiml:ciml") +# => matches a member whose ref is that URN +``` + +In practice, members are keyed by their short `code`; a member whose +canonical identity is a full URN carries it in `ref`. + +## See also + +- [Body](/docs/body) — member shape +- [Entity resolution](/docs/entity-resolution) — the three-tier pattern +- [Contact Register](/docs/contact-register) — parallel registry for Contacts +- [Venue Register](/docs/venue-register) — parallel registry for Venues +- [Localization](/docs/localization) — spelling codes diff --git a/docs/body.md b/docs/body.md new file mode 100644 index 0000000..685a456 --- /dev/null +++ b/docs/body.md @@ -0,0 +1,85 @@ +--- +title: Body +--- + +# Body + +A **Body** is a committee, subcommittee, working group, or other +organised body that owns meetings and decisions. It carries a short +`code` (e.g. `ISO/TC 154`, `CIML`) and a localised full name. + +Body replaces the bare `String` previously used for +`Meeting.committee` and `Meeting.committee_group` — those fields are +now typed `Body`, so a committee can carry its own localised name, +kind, and parent body instead of a label. + +```yaml +# Inline — the Body carries full data +committee: + code: CIML + kind: committee + name: + - spelling: eng + value: International Committee of Legal Metrology + - spelling: fra + value: Comité international de métrologie légale +``` + +## Reference vs inline + +Like [Contact](/docs/contact) and [Venue](/docs/venue), a Body field +follows the three-tier [entity resolution](/docs/entity-resolution) +pattern: + +```yaml +# 1. Inline — full data, neither ref nor local_ref set (above) + +# 2. Document-scoped — local_ref matches the code of a Body in the +# same document's bodies[] collection (e.g. Meeting#bodies) +committee: { local_ref: CIML } +bodies: + - code: CIML + name: + - spelling: eng + value: International Committee of Legal Metrology + +# 3. Global register — ref resolves against a BodyRegister +committee: { ref: urn:edoxen:body:oiml:ciml } +``` + +The `reference?` predicate is true when `ref` **or** `local_ref` is +set; when either is set, other fields are ignored. + +Unlike Contact and Venue, **Body has no `urn` attribute** — a Body is +identified by its `code` (or by a full URN carried in `ref`). See +[Body Register](/docs/body-register) for how lookups match. + +## Fields + +| Field | Type | Description | +|---|---|---| +| `ref` | `String` | URN reference into a [BodyRegister](/docs/body-register) (alternative to inline data). | +| `local_ref` | `String` | Document-scoped reference — matches the `code` of an entry in the document's own `bodies[]`. | +| `code` | `String` | Short code of the body (`CIML`, `ISO/TC 154`). | +| `name` | `LocalizedString[0..*]` | Localized full name. | +| `kind` | `String` | What kind of body: `committee`, `subcommittee`, `working_group`, etc. Free-form. | +| `parent_ref` | `EntityRef` | The parent body (e.g. a working group's parent committee). | +| `extensions` | `MeetingExtension[0..*]` | Profile-specific attributes. | + +## Where Body is used + +| Entity | Field | Context | +|---|---|---| +| `Meeting` | `committee` | Owning committee. | +| `Meeting` | `committee_group` | Sub-committee / working group. | +| `Meeting` | `bodies[]` | Document-scoped bodies for `local_ref` resolution. | +| `BodyRegister` | `bodies[]` | Registry members. | +| `Body` | `parent_ref` | Parent body (as an EntityRef). | + +## See also + +- [Body Register](/docs/body-register) — scoped registry of Bodies +- [Entity resolution](/docs/entity-resolution) — the three-tier pattern +- [Contact](/docs/contact) / [Venue](/docs/venue) — the parallel resolvable entities +- [EntityRef](/docs/entity-ref) — the `parent_ref` type +- [Meeting Collection](/docs/meeting-collection) — where `committee` and `bodies[]` attach diff --git a/docs/cli.md b/docs/cli.md index 8c33e17..9a54b07 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -8,11 +8,19 @@ data files. ```sh edoxen help # Commands: -# edoxen help [COMMAND] # Describe available commands or one specific -# edoxen normalize YAML_FILE_PATTERN # Normalize YAML files using Edoxen schema -# edoxen validate YAML_FILE_PATTERN # Validate YAML files against Edoxen schema +# edoxen help [COMMAND] # Describe available commands or one specific +# edoxen validate YAML_FILE_PATTERN # Validate YAML files against Edoxen schema +# edoxen normalize YAML_FILE_PATTERN # Normalize YAML files using Edoxen schema +# edoxen validate-meetings YAML_FILE_PATTERN # Validate Meeting YAML files against the meeting schema +# edoxen normalize-meetings YAML_FILE_PATTERN # Normalize Meeting YAML files ``` +`validate` / `normalize` cover decision-side files +(`schema/edoxen.yaml` — DecisionCollection plus the register documents: +ContactRegister, VenueRegister, BodyRegister); +`validate-meetings` / `normalize-meetings` cover meeting-side files +(`schema/meeting.yaml` — MeetingCollection, Meeting, MeetingSeries). + ## validate ```sh diff --git a/docs/contact-collection.md b/docs/contact-register.md similarity index 66% rename from docs/contact-collection.md rename to docs/contact-register.md index a59dfb7..efcc66e 100644 --- a/docs/contact-collection.md +++ b/docs/contact-register.md @@ -1,16 +1,18 @@ --- -title: Contact Collection +title: Contact Register --- -# ContactCollection +# ContactRegister A registry of [Contacts](/docs/contact) indexed by scoped URN. Members -carry `urn: urn:edoxen:contact:{scope}:{local-id}`; the collection's +carry `urn: urn:edoxen:contact:{scope}:{local-id}`; the register's `scope` MUST match the scope segment in member URNs. Other documents (Meeting, MeetingComponent, HostRef, etc.) reference contacts via `ref: urn:edoxen:contact:{scope}:{local-id}` and resolve -against the matching ContactCollection. +against the matching ContactRegister. See +[Entity resolution](/docs/entity-resolution) for the full three-tier +pattern (inline / document-scoped / register). ```yaml scope: isotc154 @@ -46,26 +48,27 @@ contacts: ## Storage patterns -A ContactCollection can be serialized as: +A ContactRegister can be stored as: 1. **Single YAML file** — typical for small registries (10–1000 contacts). 2. **YAML Stream** — one Contact per document, separated by `---`, for large registries that need partial updates. Glossarist-style. -```ruby -Edoxen::ContactCollection.load_stream("contacts.stream.yaml") -``` +Stream loading is a service-layer concern (file-system I/O); the model +owns only the (de)serialisation of one register. ## Ruby helpers ```ruby -collection = Edoxen::ContactCollection.from_yaml(File.read("contacts.yaml")) -collection.find_by_urn("urn:edoxen:contact:isotc154:jianfang-zhang") +register = Edoxen::ContactRegister.from_yaml(File.read("contacts.yaml")) +register.find_by_urn("urn:edoxen:contact:isotc154:jianfang-zhang") # => # ``` ## See also - [Contact](/docs/contact) — member shape -- [VenueCollection](/docs/venue-collection) — parallel registry for Venues +- [Entity resolution](/docs/entity-resolution) — how `ref` / `local_ref` / inline resolve +- [Venue Register](/docs/venue-register) — parallel registry for Venues +- [Body Register](/docs/body-register) — parallel registry for Bodies - [Localization](/docs/localization) — spelling codes diff --git a/docs/contact.md b/docs/contact.md index a59107d..3d01e12 100644 --- a/docs/contact.md +++ b/docs/contact.md @@ -48,22 +48,31 @@ contact: ## Reference vs inline -A Contact field may be either **inline** (full data, as above) or a -**URN reference** to a Contact stored in a -[ContactCollection](/docs/contact-collection): +A Contact field follows the three-tier +[entity resolution](/docs/entity-resolution) pattern — **inline** +(full data, as above), a **document-scoped reference** (`local_ref`) +matching the `urn` of a Contact in the same document's `contacts[]` +collection (e.g. `Meeting#contacts[]`), or a **register reference** +(`ref`) to a Contact stored in a +[ContactRegister](/docs/contact-register): ```yaml -chair: - ref: urn:edoxen:contact:isotc154:anaya-muller +# Document-scoped — resolves within this file +chair: { local_ref: urn:edoxen:contact:isotc154:anaya-muller } + +# Global register — resolves against a ContactRegister document +chair: { ref: urn:edoxen:contact:isotc154:anaya-muller } ``` -The `ref` field discriminates. When set, other fields are ignored. +The `ref` / `local_ref` fields discriminate: `reference?` is true when +either is set, and when set, other fields are ignored. ## Fields | Field | Type | Description | |---|---|---| -| `ref` | `String` | URN reference (alternative to inline data). | +| `ref` | `String` | URN reference into a ContactRegister (alternative to inline data). | +| `local_ref` | `String` | Document-scoped reference — matches the `urn` of an entry in the document's own `contacts[]`. | | `urn` | `String` | This contact's registry URN (`urn:edoxen:contact:{scope}:{id}`). | | `name` | `LocalizedName[0..*]` | Localized structured name (see below). | | `kind` | `String` | What kind of contact: `person`, `organisation`, `department`, `role`, etc. Free-form. | @@ -125,6 +134,7 @@ for the full format. | Entity | Field | Context | |---|---|---| | `Meeting` | `contact` | General contact for the meeting. | +| `Meeting` | `contacts[]` | Document-scoped contacts for `local_ref` resolution. | | `Officer` | `person` (type: Contact/Person) | Chair, secretary, etc. | | `Attendance` | `person` (type: Contact/Person) | Who attended. | | `VoteRecord` | `person` (type: Contact/Person) | Who voted. | @@ -133,7 +143,8 @@ for the full format. ## See also -- [ContactCollection](/docs/contact-collection) — scoped URN registry of Contacts +- [ContactRegister](/docs/contact-register) — scoped URN registry of Contacts +- [Entity resolution](/docs/entity-resolution) — the inline / `local_ref` / `ref` pattern - [Localization](/docs/localization) — how per-field localization works - [Officer](/docs/officer) — Officer.person is a Contact - [MeetingExtension](/docs/extension) — profile-specific extensions diff --git a/docs/entity-ref.md b/docs/entity-ref.md index 32a0357..d7ce1a4 100644 --- a/docs/entity-ref.md +++ b/docs/entity-ref.md @@ -75,5 +75,6 @@ fields (non-breaking). The String fields will be removed in 1.0. ## See also - [Structured Identifier](/docs/structured-identifier) — the `{prefix, number}` type used in `identifier` +- [Entity resolution](/docs/entity-resolution) — the Contact/Venue/Body inline / `local_ref` / `ref` pattern (a different mechanism from EntityRef's own `local_ref`) - [Motion](/docs/motion) — pilot entity for EntityRef - [Architecture](/docs/architecture) — cross-grain pointer overview diff --git a/docs/entity-resolution.md b/docs/entity-resolution.md new file mode 100644 index 0000000..afc2e97 --- /dev/null +++ b/docs/entity-resolution.md @@ -0,0 +1,115 @@ +--- +title: Entity resolution +--- + +# Entity resolution + +[Contact](/docs/contact), [Venue](/docs/venue), and +[Body](/docs/body) share a **three-tier resolution pattern**. Wherever +one of these entities appears (a meeting's chair, a component's venue, +a meeting's committee), the field holds either the data itself or a +reference to where the data lives: + +| Tier | Discriminator | Resolves against | +|---|---|---| +| **1. Inline** | neither `ref` nor `local_ref` set | the entity itself — it IS the data | +| **2. Document-scoped** | `local_ref` set | the containing document's scoped collection (`Meeting#contacts[]`, `Meeting#venues[]`, `Meeting#bodies[]`) | +| **3. Global register** | `ref` set | the top-level register document ([ContactRegister](/docs/contact-register), [VenueRegister](/docs/venue-register), [BodyRegister](/docs/body-register)) | + +The `reference?` predicate on each entity is true when `ref` **or** +`local_ref` is set. When either is set, the other fields are ignored. + +```yaml +# Tier 1 — inline +contact: + kind: person + name: + - spelling: eng + value: { formatted: Dr. Anaya Müller } + +# Tier 2 — document-scoped: local_ref matches the urn of an entry in +# this same Meeting's contacts[] collection +contact: { local_ref: urn:edoxen:contact:isotc154:anaya-muller } +contacts: + - urn: urn:edoxen:contact:isotc154:anaya-muller + kind: person + name: + - spelling: eng + value: { formatted: Dr. Anaya Müller } + +# Tier 3 — global register: ref resolves against a ContactRegister +# document (a separate file, shared across meetings) +contact: { ref: urn:edoxen:contact:isotc154:anaya-muller } +``` + +Tier 2 keeps repeated people/places/bodies DRY *within* one file; +tier 3 shares them *across* files via a published register. + +## Matching keys + +- **Contact / Venue** — scoped-collection members are matched on their + `urn`; register members are matched on `urn` (`find_by_urn`). +- **Body** — has no `urn` attribute. Scoped members are matched on + `code`; `BodyRegister#find_by_urn` matches a member's `code` **or** + `ref`. See [Body Register](/docs/body-register#lookup-matched-by-code-or-ref). + +> **Note:** `local_ref` here is an entity-resolution pointer on +> Contact/Venue/Body. The [EntityRef](/docs/entity-ref) type also has a +> `local_ref` field — a within-file pointer between structural entities +> (e.g. to an agenda item). Same name, different mechanism. + +## `Edoxen::EntityResolver` + +The gem ships a service that walks the tier hierarchy for you. It is +**pure** — no mutation — and returns the resolved entity or `nil` when +a reference matches nothing. + +```ruby +resolver = Edoxen::EntityResolver.new( + scoped: { Edoxen::Contact => meeting.contacts }, + registers: { Edoxen::Contact => contact_register }, +) + +# One entity — inline entities pass through unchanged +resolver.resolve(meeting.contact) +# => # (or nil if the reference doesn't resolve) + +# A mixed list of inline and referenced entities +resolver.resolve_all(meeting.attendance.map(&:person)) +``` + +Resolution order inside `resolve`: + +1. `reference?` false → return the entity as-is (tier 1). +2. `local_ref` set → look up in the scoped collection registered for + the entity's class, matching member `urn` against `local_ref` + (tier 2). +3. `ref` set → look up in the global register registered for the + entity's class via `find_by_urn` (tier 3). +4. No match → `nil`. + +Both the `scoped:` and `registers:` hashes are keyed by entity class +(`Edoxen::Contact`, `Edoxen::Venue`, `Edoxen::Body`; a subclass such as +`Edoxen::Person` falls back to its superclass's entry), so one resolver +instance can serve all three entity types: + +```ruby +resolver = Edoxen::EntityResolver.new( + scoped: { + Edoxen::Contact => meeting.contacts, + Edoxen::Venue => meeting.venues, + Edoxen::Body => meeting.bodies, + }, + registers: { + Edoxen::Contact => contact_register, + Edoxen::Venue => venue_register, + Edoxen::Body => body_register, + }, +) +``` + +## See also + +- [Contact](/docs/contact) / [Venue](/docs/venue) / [Body](/docs/body) — the resolvable entities +- [Contact Register](/docs/contact-register) / [Venue Register](/docs/venue-register) / [Body Register](/docs/body-register) — the tier-3 registers +- [EntityRef](/docs/entity-ref) — typed cross-references between structural entities (Motion → Decision, etc.) diff --git a/docs/meeting-collection.md b/docs/meeting-collection.md index e91765a..52b87d2 100644 --- a/docs/meeting-collection.md +++ b/docs/meeting-collection.md @@ -21,7 +21,7 @@ meetings: status: completed year: 2025 date_range: { start: '2025-10-13', end: '2025-10-17' } - committee: CIML + committee: { code: CIML, name: [{ spelling: eng, value: International Committee of Legal Metrology }] } venues: - { name: 'Hotel Berlin', address: 'Berlin, Germany' } city: BER @@ -95,8 +95,8 @@ adopted. | `city` | `String` | IATA three-letter code (`BER`, `PAR`, `TYO`). | | `country_code` | `String` | ISO 3166-1 alpha-2 (`DE`, `FR`, `JP`). | | `virtual` | `Boolean` | True if held online. | -| `committee` | `String` | Owning committee (e.g. "ISO/TC 154"). | -| `committee_group` | `String` | Sub-committee / working group. | +| `committee` | `Body` | Owning committee — inline, `{ local_ref: ... }`, or `{ ref: ... }` (see [Body](/docs/body)). | +| `committee_group` | `Body` | Sub-committee / working group (same type). | ### People @@ -106,6 +106,9 @@ adopted. | `secretary` | `Person` | Recording secretary. | | `host` | `String` | Host organization. | | `hosts` | `HostRef[0..*]` | Multi-party hosting (hosting committee + secretariat, etc.). | +| `contact` | `Contact` | General contact for the meeting (inline or reference). | +| `contacts` | `Contact[0..*]` | Document-scoped [Contacts](/docs/contact) — `local_ref` targets resolve here (see [Entity resolution](/docs/entity-resolution)). | +| `bodies` | `Body[0..*]` | Document-scoped [Bodies](/docs/body) — `local_ref` targets for `committee` / `committee_group` resolve here. | ### Links diff --git a/docs/officer.md b/docs/officer.md index 7457e48..08818a7 100644 --- a/docs/officer.md +++ b/docs/officer.md @@ -19,7 +19,7 @@ shortcuts. One list, role discriminates. | Field | Type | Description | |---|---|---| | `role` | `OfficerRole` | `chair`, `vice_chair`, `deputy_chair`, `secretary`, `treasurer`, `parliamentarian`, `presiding_officer`, `sergeant_at_arms`, `other`. | -| `person` | `Contact` (or `Person`) | The officer. May be inline data or a `{ ref: urn:... }` URN reference. | +| `person` | `Contact` (or `Person`) | The officer. May be inline data, a document-scoped `{ local_ref: ... }`, or a `{ ref: urn:... }` register reference — see [Entity resolution](/docs/entity-resolution). | | `term_start` | `Date` | When their term began. | | `term_end` | `Date` | When their term ends. | | `extensions` | `MeetingExtension[0..*]` | Profile-specific extensions. | @@ -75,4 +75,4 @@ officers: - [Meeting Collection](/docs/meeting-collection) — officers attach to a Meeting - [MeetingComponent](/docs/meeting-component) — officers attach to a component - [Contact](/docs/contact) — Officer.person type -- [ContactCollection](/docs/contact-collection) — registry for URN references +- [ContactRegister](/docs/contact-register) — registry for URN references diff --git a/docs/schema.md b/docs/schema.md index 6e7f0f2..7304c85 100644 --- a/docs/schema.md +++ b/docs/schema.md @@ -8,9 +8,10 @@ Edoxen validates YAML data against JSON Schema (Draft 7) definitions. There are two schema files — one for **Decision** data (formal decisions adopted by meetings) and one for **Meeting** data (the meetings themselves, including embedded agendas, minutes, -attendance, and votes). Both schemas also accept **ContactCollection** -and **VenueCollection** as top-level documents (the URN registry -pattern). +attendance, and votes). The decision-side schema also accepts +**ContactRegister**, **VenueRegister**, and **BodyRegister** as +top-level documents (the URN registry pattern); the meeting-side +schema accepts `MeetingCollection`, `Meeting`, and `MeetingSeries`. ## How localization works in 1.0 @@ -43,7 +44,7 @@ metadata: dates: - { date: 2025-10-14, type: meeting } source_urls: - - { ref: 'https://oiml.org/…/en.pdf', format: pdf, spelling: eng } + - { ref: 'https://oiml.org/.../en.pdf', format: pdf, spelling: eng } decisions: - identifier: - { prefix: OIML, number: '2025/1' } @@ -136,10 +137,10 @@ decisions: Download: [meeting.yaml](/schemas/meeting.yaml) ([source](https://github.com/edoxen/edoxen-model/blob/main/schema/meeting.yaml)). -### Contact Collection +### Contact Register -Validates `ContactCollection` — a registry of Contacts indexed by -scoped URN. +Validates `ContactRegister` — a registry of Contacts indexed by +scoped URN. See [Contact Register](/docs/contact-register). ```yaml scope: oiml @@ -156,9 +157,10 @@ contacts: - { kind: orcid, value: "0000-0002-1234-5678" } ``` -### Venue Collection +### Venue Register -Validates `VenueCollection` — a registry of Venues indexed by scoped URN. +Validates `VenueRegister` — a registry of Venues indexed by scoped URN. +See [Venue Register](/docs/venue-register). ```yaml scope: oiml @@ -173,17 +175,40 @@ venues: address: 3 Rue Caulaincourt, 75018 Paris, France ``` +### Body Register + +Validates `BodyRegister` — a registry of Bodies (committees, working +groups). Members are matched by `code` or `ref` — Body has no `urn` +field. See [Body Register](/docs/body-register). + +```yaml +scope: oiml +title: + - { spelling: eng, value: "OIML Bodies" } +bodies: + - code: CIML + kind: committee + name: + - { spelling: eng, value: "International Committee of Legal Metrology" } +``` + ### URN reference pattern -Any entity-typed field accepts either inline data (full object) or -a URN reference (`{ ref: urn:edoxen:... }`): +Entity-typed fields (Contact, Venue, Body) follow the three-tier +[entity resolution](/docs/entity-resolution) pattern: inline data +(full object), a document-scoped reference (`{ local_ref: ... }`), or +a register URN reference (`{ ref: urn:edoxen:... }`): ```yaml # Inline contact: name: { formatted: "Roman Schwartz" } -# URN reference (resolves against a ContactCollection) +# Document-scoped (resolves against this document's contacts[]) +contact: + local_ref: urn:edoxen:contact:oiml:ciml-president + +# Register reference (resolves against a ContactRegister) contact: ref: urn:edoxen:contact:oiml:ciml-president ``` @@ -220,6 +245,8 @@ The schema files are standard JSON Schema (Draft 7). Use `ajv`, - [Architecture](/docs/architecture) — the information model overview - [Localization](/docs/localization) — the per-field localization pattern -- [Contact Collection](/docs/contact-collection) — the URN registry pattern -- [Venue Collection](/docs/venue-collection) — venue registries +- [Contact Register](/docs/contact-register) — the URN registry pattern +- [Venue Register](/docs/venue-register) — venue registries +- [Body Register](/docs/body-register) — body registries +- [Entity resolution](/docs/entity-resolution) — the three-tier reference pattern - [Migration guide](/docs/migration) — breaking changes guide diff --git a/docs/venue-collection.md b/docs/venue-register.md similarity index 58% rename from docs/venue-collection.md rename to docs/venue-register.md index ec251b2..90a285d 100644 --- a/docs/venue-collection.md +++ b/docs/venue-register.md @@ -1,11 +1,16 @@ --- -title: Venue Collection +title: Venue Register --- -# VenueCollection +# VenueRegister A registry of [Venues](/docs/venue) indexed by scoped URN. Mirrors -[ContactCollection](/docs/contact-collection). +[ContactRegister](/docs/contact-register). + +Members carry `urn: urn:edoxen:venue:{scope}:{local-id}`; the +register's `scope` MUST match the scope segment in member URNs. See +[Entity resolution](/docs/entity-resolution) for how `ref` / +`local_ref` / inline venues resolve. ```yaml scope: isotc154 @@ -39,12 +44,14 @@ venues: ## Ruby helpers ```ruby -collection = Edoxen::VenueCollection.from_yaml(File.read("venues.yaml")) -collection.find_by_urn("urn:edoxen:venue:isotc154:fairmont-house-hkma") +register = Edoxen::VenueRegister.from_yaml(File.read("venues.yaml")) +register.find_by_urn("urn:edoxen:venue:isotc154:fairmont-house-hkma") ``` ## See also - [Venue](/docs/venue) — member shape -- [ContactCollection](/docs/contact-collection) — parallel registry for Contacts +- [Entity resolution](/docs/entity-resolution) — how `ref` / `local_ref` / inline resolve +- [Contact Register](/docs/contact-register) — parallel registry for Contacts +- [Body Register](/docs/body-register) — parallel registry for Bodies - [Localization](/docs/localization) — spelling codes diff --git a/docs/venue.md b/docs/venue.md index 3de1225..9093831 100644 --- a/docs/venue.md +++ b/docs/venue.md @@ -12,6 +12,9 @@ Boolean` (insufficient — Zoom needs URL + passcode + dial-in numbers). | Field | Type | Description | |---|---|---| +| `ref` | string | URN reference into a [VenueRegister](/docs/venue-register) (alternative to inline data). | +| `local_ref` | string | Document-scoped reference — matches the `urn` of an entry in the document's own `venues[]`. | +| `urn` | string | This venue's registry URN (`urn:edoxen:venue:{scope}:{id}`). | | `kind` | enum | `physical` or `virtual` — required discriminator. | | `name` | string | Display name (e.g. "Acme Boardroom"). | | `label` | string | Short label (e.g. "Microsoft Teams — Directors only"). | @@ -53,6 +56,16 @@ Populated when `kind: virtual`. | `waiting_room` | boolean | Whether a waiting room is enabled. | | `registration_required` | boolean | Whether pre-registration is required. | +## Reference vs inline + +Like [Contact](/docs/contact), a Venue field follows the three-tier +[entity resolution](/docs/entity-resolution) pattern: inline data +(neither `ref` nor `local_ref` set), a document-scoped reference +(`local_ref` matching the `urn` of a venue in the same document's +`venues[]`), or a register reference (`ref`) resolved against a +[VenueRegister](/docs/venue-register). `reference?` is true when either +is set; when set, other fields are ignored. + ## Hybrid meetings A Meeting with both physical and virtual venues is hybrid. The @@ -87,6 +100,8 @@ venues: ## See also +- [Venue Register](/docs/venue-register) — scoped URN registry of Venues. +- [Entity resolution](/docs/entity-resolution) — the inline / `local_ref` / `ref` pattern. - [Meeting Collection](/docs/meeting-collection) — where venues attach. - [MeetingComponent](/docs/meeting-component) — components can pin a subset of the meeting's venues via `venue_refs[]`. diff --git a/public/schemas/decision-collection.yaml b/public/schemas/decision-collection.yaml index 8dfd109..2771e58 100644 --- a/public/schemas/decision-collection.yaml +++ b/public/schemas/decision-collection.yaml @@ -1,6 +1,6 @@ --- "$schema": http://json-schema.org/draft-07/schema# -"$id": https://github.com/edoxen/edoxen/schema/edoxen.yaml +"$id": https://github.com/edoxen/edoxen-model/schema/decision-collection.yaml title: Edoxen Decision Collection Schema description: | Schema for validating Edoxen decision-side YAML files. @@ -8,8 +8,8 @@ description: | Accepts three top-level document kinds via `oneOf`: * `DecisionCollection` — the formal outcomes adopted by a Meeting. - * `ContactCollection` — a scoped-URN registry of Contacts. - * `VenueCollection` — a scoped-URN registry of Venues. + * `ContactRegister` — a scoped-URN registry of Contacts. + * `VenueRegister` — a scoped-URN registry of Venues. Mirrors the canonical LutaML information model in https://github.com/edoxen/edoxen-model/tree/main/models . @@ -26,8 +26,9 @@ description: | type: object oneOf: - "$ref": "#/$defs/DecisionCollection" -- "$ref": "#/$defs/ContactCollection" -- "$ref": "#/$defs/VenueCollection" +- "$ref": "#/$defs/ContactRegister" +- "$ref": "#/$defs/VenueRegister" +- "$ref": "#/$defs/BodyRegister" "$defs": DecisionCollection: type: object @@ -545,6 +546,14 @@ oneOf: type: array items: type: string + statements: + type: array + items: + "$ref": "#/$defs/Statement" + declarations: + type: array + items: + "$ref": "#/$defs/Declaration" extensions: type: array items: @@ -561,7 +570,8 @@ oneOf: properties: ref: type: string - description: URN reference; if set, ignore other fields + local_ref: + type: string urn: type: string pattern: "^urn:edoxen:venue:[^:]+:[^:]+$" @@ -869,7 +879,8 @@ oneOf: properties: ref: type: string - description: URN reference; if set, ignore other fields + local_ref: + type: string urn: type: string pattern: "^urn:edoxen:contact:[^:]+:[^:]+$" @@ -915,6 +926,8 @@ oneOf: properties: ref: type: string + local_ref: + type: string urn: type: string pattern: "^urn:edoxen:contact:[^:]+:[^:]+$" @@ -1115,7 +1128,7 @@ oneOf: type: string contact: "$ref": "#/$defs/Contact" - ContactCollection: + ContactRegister: type: object description: | Registry of Contacts indexed by scoped URN. Members carry @@ -1137,9 +1150,9 @@ oneOf: type: array items: "$ref": "#/$defs/MeetingExtension" - VenueCollection: + VenueRegister: type: object - description: 'Registry of Venues indexed by scoped URN. Mirrors ContactCollection. + description: 'Registry of Venues indexed by scoped URN. Mirrors ContactRegister. ' additionalProperties: false @@ -1429,3 +1442,130 @@ oneOf: - abstain - absent - not_applicable + DateTimeRange: + type: object + description: | + Start + end pair with sub-day precision. Parallel to DateRange; + use when day granularity is insufficient (e.g. a meeting that + ran 09:00–11:30). + additionalProperties: false + properties: + start: + type: string + format: date-time + end: + type: string + format: date-time + StatementKind: + type: string + description: | + Discriminator for the three BS 0:2006 statement types. Adding a + new kind is a one-line enum extension; the Statement model + itself never needs to change (OCP). + enum: + - statement + - comment + - standpoint + DeclarationKind: + type: string + description: | + Discriminator for the two BS 0:2006 declaration types + (conflict of interest, IPR). + enum: + - conflict_of_interest + - ipr + Statement: + type: object + description: | + One remark made by one or more meeting members on a topic or a + minutes section. Per-field Localized description; party is a + list of Person references. The `kind` discriminator separates + the three BS 0:2006 statement types. + additionalProperties: false + properties: + kind: + "$ref": "#/$defs/StatementKind" + description: + type: array + items: + "$ref": "#/$defs/LocalizedString" + party: + type: array + items: + "$ref": "#/$defs/Person" + extensions: + type: array + items: + "$ref": "#/$defs/MeetingExtension" + Declaration: + type: object + description: | + A formal declaration (conflict of interest or IPR) made by one + or more meeting members. IPR-specific fields (`ipr_subject_ref`, + `ipr_target_ref`) are populated only when `kind == "ipr"`. + additionalProperties: false + properties: + kind: + "$ref": "#/$defs/DeclarationKind" + description: + type: array + items: + "$ref": "#/$defs/LocalizedString" + party: + type: array + items: + "$ref": "#/$defs/Person" + ipr_subject_ref: + "$ref": "#/$defs/EntityRef" + ipr_target_ref: + "$ref": "#/$defs/EntityRef" + extensions: + type: array + items: + "$ref": "#/$defs/MeetingExtension" + Body: + type: object + description: | + A committee, subcommittee, working group, or other organised body. + Three-tier entity resolution (ref / local_ref / inline). + additionalProperties: false + properties: + ref: + type: string + local_ref: + type: string + code: + type: string + name: + type: array + items: + "$ref": "#/$defs/LocalizedString" + kind: + type: string + parent_ref: + "$ref": "#/$defs/EntityRef" + extensions: + type: array + items: + "$ref": "#/$defs/MeetingExtension" + BodyRegister: + type: object + description: | + Authoritative register of Bodies (committees, subcommittees, + working groups). + additionalProperties: false + properties: + scope: + type: string + title: + type: array + items: + "$ref": "#/$defs/LocalizedString" + bodies: + type: array + items: + "$ref": "#/$defs/Body" + extensions: + type: array + items: + "$ref": "#/$defs/MeetingExtension" diff --git a/public/schemas/meeting.yaml b/public/schemas/meeting.yaml index 5139d01..497c119 100644 --- a/public/schemas/meeting.yaml +++ b/public/schemas/meeting.yaml @@ -1,6 +1,6 @@ --- "$schema": http://json-schema.org/draft-07/schema# -"$id": https://github.com/edoxen/edoxen/schema/meeting.yaml +"$id": https://github.com/edoxen/edoxen-model/schema/meeting.yaml title: Edoxen Meeting Schema description: | Schema for validating Edoxen Meeting/Agenda YAML files. @@ -215,7 +215,8 @@ oneOf: properties: ref: type: string - description: URN reference; if set, ignore other fields + local_ref: + type: string urn: type: string pattern: "^urn:edoxen:contact:[^:]+:[^:]+$" @@ -261,6 +262,8 @@ oneOf: properties: ref: type: string + local_ref: + type: string urn: type: string pattern: "^urn:edoxen:contact:[^:]+:[^:]+$" @@ -386,6 +389,16 @@ oneOf: type: array items: "$ref": "#/$defs/Reference" + statements: + type: array + items: + "$ref": "#/$defs/Statement" + topic_ref: + type: string + description: | + URN back-reference to the Topic this section records. + Optional; formalises the convention that + `MinutesSection#number` matches `AgendaItem#label`. Minutes: type: object description: | @@ -439,9 +452,19 @@ oneOf: "$ref": "#/$defs/LocalizedString" AgendaItem: type: object - description: 'One entry on an Agenda. 1.0: per-field Localized.' + description: | + One entry on an Agenda. 1.0: per-field Localized. + `urn` is the first-class URN of this item, derived from the + parent meeting URN and label (e.g. + `urn:oiml:ciml:meeting:ciml-60:agenda:6.2`). Optional in source + data; can be computed via `Edoxen::UrnFor.agenda_item`. additionalProperties: false properties: + urn: + type: string + description: | + First-class URN for this agenda item. Hierarchical under the + parent meeting URN: `urn:oiml:{body}:meeting:{slug}:agenda:{label}`. label: type: string kind: @@ -585,6 +608,14 @@ oneOf: type: array items: type: string + statements: + type: array + items: + "$ref": "#/$defs/Statement" + declarations: + type: array + items: + "$ref": "#/$defs/Declaration" extensions: type: array items: @@ -601,7 +632,8 @@ oneOf: properties: ref: type: string - description: URN reference; if set, ignore other fields + local_ref: + type: string urn: type: string pattern: "^urn:edoxen:venue:[^:]+:[^:]+$" @@ -960,8 +992,10 @@ oneOf: type: array items: "$ref": "#/$defs/LocalizedString" - date_range: + scheduled_date_range: "$ref": "#/$defs/DateRange" + occurred_date_range: + "$ref": "#/$defs/DateTimeRange" recurrence: "$ref": "#/$defs/Recurrence" venues: @@ -983,9 +1017,9 @@ oneOf: type: string pattern: "^[A-Z]{2}$" committee: - type: string + "$ref": "#/$defs/Body" committee_group: - type: string + "$ref": "#/$defs/Body" officers: type: array items: @@ -1008,6 +1042,14 @@ oneOf: "$ref": "#/$defs/LocalizedString" contact: "$ref": "#/$defs/Contact" + contacts: + type: array + items: + "$ref": "#/$defs/Contact" + bodies: + type: array + items: + "$ref": "#/$defs/Body" agenda: "$ref": "#/$defs/Agenda" components: @@ -1026,6 +1068,10 @@ oneOf: type: array items: "$ref": "#/$defs/Minutes" + declarations: + type: array + items: + "$ref": "#/$defs/Declaration" decisions: type: array items: @@ -1308,3 +1354,130 @@ oneOf: - weekly - monthly - yearly + DateTimeRange: + type: object + description: | + Start + end pair with sub-day precision. Parallel to DateRange; + use when day granularity is insufficient (e.g. a meeting that + ran 09:00–11:30). + additionalProperties: false + properties: + start: + type: string + format: date-time + end: + type: string + format: date-time + StatementKind: + type: string + description: | + Discriminator for the three BS 0:2006 statement types. Adding a + new kind is a one-line enum extension; the Statement model + itself never needs to change (OCP). + enum: + - statement + - comment + - standpoint + DeclarationKind: + type: string + description: | + Discriminator for the two BS 0:2006 declaration types + (conflict of interest, IPR). + enum: + - conflict_of_interest + - ipr + Statement: + type: object + description: | + One remark made by one or more meeting members on a topic or a + minutes section. Per-field Localized description; party is a + list of Person references. The `kind` discriminator separates + the three BS 0:2006 statement types. + additionalProperties: false + properties: + kind: + "$ref": "#/$defs/StatementKind" + description: + type: array + items: + "$ref": "#/$defs/LocalizedString" + party: + type: array + items: + "$ref": "#/$defs/Person" + extensions: + type: array + items: + "$ref": "#/$defs/MeetingExtension" + Declaration: + type: object + description: | + A formal declaration (conflict of interest or IPR) made by one + or more meeting members. IPR-specific fields (`ipr_subject_ref`, + `ipr_target_ref`) are populated only when `kind == "ipr"`. + additionalProperties: false + properties: + kind: + "$ref": "#/$defs/DeclarationKind" + description: + type: array + items: + "$ref": "#/$defs/LocalizedString" + party: + type: array + items: + "$ref": "#/$defs/Person" + ipr_subject_ref: + "$ref": "#/$defs/EntityRef" + ipr_target_ref: + "$ref": "#/$defs/EntityRef" + extensions: + type: array + items: + "$ref": "#/$defs/MeetingExtension" + Body: + type: object + description: | + A committee, subcommittee, working group, or other organised body. + Three-tier entity resolution (ref / local_ref / inline). + additionalProperties: false + properties: + ref: + type: string + local_ref: + type: string + code: + type: string + name: + type: array + items: + "$ref": "#/$defs/LocalizedString" + kind: + type: string + parent_ref: + "$ref": "#/$defs/EntityRef" + extensions: + type: array + items: + "$ref": "#/$defs/MeetingExtension" + BodyRegister: + type: object + description: | + Authoritative register of Bodies (committees, subcommittees, + working groups). + additionalProperties: false + properties: + scope: + type: string + title: + type: array + items: + "$ref": "#/$defs/LocalizedString" + bodies: + type: array + items: + "$ref": "#/$defs/Body" + extensions: + type: array + items: + "$ref": "#/$defs/MeetingExtension" diff --git a/src/data/site.ts b/src/data/site.ts index 4283659..e468cbd 100644 --- a/src/data/site.ts +++ b/src/data/site.ts @@ -52,8 +52,10 @@ export const sidebar: NavItem[] = [ { text: 'Meeting Collection', link: '/docs/meeting-collection' }, { text: 'Meeting Series', link: '/docs/meeting-series' }, { text: 'Meeting Component', link: '/docs/meeting-component' }, + { text: 'Body', link: '/docs/body' }, + { text: 'Body Register', link: '/docs/body-register' }, { text: 'Venue', link: '/docs/venue' }, - { text: 'Venue Collection', link: '/docs/venue-collection' }, + { text: 'Venue Register', link: '/docs/venue-register' }, { text: 'Agenda', link: '/docs/agenda' }, { text: 'Minutes', link: '/docs/minutes' }, { text: 'BS 0:2006 Minutes', link: '/docs/bs0-minutes' }, @@ -67,7 +69,7 @@ export const sidebar: NavItem[] = [ collapsed: false, items: [ { text: 'Contact', link: '/docs/contact' }, - { text: 'Contact Collection', link: '/docs/contact-collection' }, + { text: 'Contact Register', link: '/docs/contact-register' }, { text: 'Officer', link: '/docs/officer' }, ], }, @@ -114,6 +116,7 @@ export const sidebar: NavItem[] = [ items: [ { text: 'Structured Identifier', link: '/docs/structured-identifier' }, { text: 'EntityRef', link: '/docs/entity-ref' }, + { text: 'Entity Resolution', link: '/docs/entity-resolution' }, { text: 'Body Vocabulary', link: '/docs/body-vocabulary' }, { text: 'Source URL', link: '/docs/source-url' }, ],