Architecture

Edoxen is an information model for any meeting — the umbrella term for the published record of a meeting (agenda, attendance, motions, votings, decisions, minutes). The model is generic by design: its core captures concepts shared across standards bodies, parliaments, technical communities, academic conferences, and corporate boards. Domain-specific concepts (Bill, Witness, Petition, Quorum Bell) live in profile extensions via the MeetingExtension slot on every core entity (ISO 8601-2 §15).

The model has two parallel top-level containers — one for meetings, one for decisions — both sharing the same multilingual plumbing. This page walks the model first, then the pipeline, then the multilingual plumbing.

The five concerns

A meeting has five concerns, each modelled as a distinct first-class entity rather than a column on a flat table:

  1. Meetings — the event itself: identifier, venues (polymorphic), officers (role-discriminated), agenda, components (flat sub-events), attendance, minutes, declarations (BS 0:2006 §7.6).
  2. Motions + Votings — the procedural record: who moved what, how the question was put, how members voted, what the chair declared.
  3. Decisions — the formal outcomes (resolution, order, ruling, determination, recommendation, statement, finding, opinion).
  4. Topics — the subjects of discussion: documents, assets, references, standing statements + declarations, with cross-meeting threading via resumption_of.
  5. Series — the recurring parent (annual plenaries, monthly board meetings, IETF meeting series) with a Recurrence rule.

The model also carries BS 0:2006 §7.6 statements and declarations as first-class core entities — attached to meetings, topics, and minutes sections. See BS 0:2006 Minutes for the full treatment.

Every core entity also carries an extensions: MeetingExtension[0..*] slot for adopter-specific profiles.

Which file root?

Your first decision: which grain do you need?

The two views capture the same events from opposite directions: a Meeting points to its Resolutions via resolution_refs[]; a Resolution points back to its Meeting via meeting: MeetingIdentifier. You can use one, the other, or both — they’re not mutually exclusive.

Layer 1 — The information model

Both collections share the same shape in miniature: an outer container with metadata plus an array of children, where each child carries admin fields once and localizations[] for per-language content. The full model has more — Meeting has agendas, schedules, relations; Resolution has considerations, actions, approvals — but the shape is the same in both.

Resolution tree

This is the resolution-grain view — one file, many resolutions, each in one or more languages:

ResolutionCollection metadata : ResolutionMetadata resolutions : Resolution[1..*] ResolutionMetadata title, dates, venue, city, country_code, source_urls[1..*] language-agnostic Resolution identifier : StructuredIdentifier[1..*] type, doi, urn dates : ResolutionDate[] meeting : MeetingIdentifier categories : String[0..*] localizations: Localization[1..*] admin fields declared once SourceUrl ref : string format : pdf | html language_code : ISO 639-3 one per source PDF Localization language_code, script, title, subject, message Consideration type, message Action type (enum), message Approval type, degree, message

Meeting tree

This is the meeting-grain view — one file, many meetings, each with agenda items and the resolutions they adopted:

MeetingCollection metadata : MeetingCollectionMetadata meetings : Meeting[0..*] Meeting identifier : StructuredIdentifier[1..*] type, status, year, date_range venue, city, country_code, chair relations : MeetingRelation[] resolution_refs[] : string agenda : Agenda attendance[] : Attendance vote_records[] : VoteRecord minutes[] : Minutes localizations : MeetingLocalization[] Agenda status, source_doc, items : AgendaItem[0..*] AgendaItem label · kind (enum) · title · outcome · resolution_ref

Read the trees top-down:

  • ResolutionCollection is the file root for the resolution-grain view. It carries ResolutionMetadata (the publication’s title, dates, venue) and a list of Resolution children.
  • Resolution is one decision. Admin fields (identifier, type, doi, urn, dates, meeting, categories) live here and are declared once, never per-language. Holds a localizations[] array of monolingual renderings.
  • MeetingCollection is the file root for the meeting-grain view — one or many Meeting children, each with their own agendas, schedules, chairs, and adopted resolutions.
  • Meeting is one sitting: when, where, who chaired, what was on the agenda, who was in the room, how they voted, and what was said. Admin fields declared once; MeetingLocalization[] carries per-language content.
  • Agenda is the ordered list of items the meeting worked through. Each AgendaItem is enum-typed (numbered, header, opening, closing) and may carry a resolution_ref to the resolution the meeting adopted on that item.
  • Attendance + VoteRecord capture who was in the room and how they voted. See Attendance & Votes.
  • Minutes is the narrative record of the meeting — the running text of what was said, sliced by agenda item. Each MinutesSection carries a number field that joins to AgendaItem.label. See Minutes.
  • Across the grain: a Resolution.meeting field carries a MeetingIdentifier back to the meeting that adopted it. A Meeting.resolution_refs[] carries the strings the other way. And VoteRecord.resolution_ref joins votes to Resolution.identifier.

Localization, the multilingual plumbing:

  • Localization (under Resolution) is one monolingual rendering of a Resolution. ISO 639-3 + ISO 15924.
  • MeetingLocalization (under Meeting) is one monolingual rendering of a Meeting. Carries the localized title, general area, and practical info.
  • Action, Consideration, Approval are the leaf content nodes inside a Resolution’s Localization. They never live directly on the resolution — anything translatable lives below a Localization.

The hard rule: anything translatable lives below a Localization or MeetingLocalization. Anything administrative lives above it. That separation is what makes the multilingual model work — and what makes EN↔FR drift visible on git diff.

The full LutaML/UML definitions live in edoxen/edoxen-model. The Ruby classes are generated from those definitions.

Layer 2 — The round-trip pipeline

A YAML file flows through three framework stages. None of them is hand-rolled:

Stage What it catches
1. Parse Malformed YAML — tabs-as-indent, duplicated keys, encoding issues.
2. Decode Type errors — language_code: 42 instead of a string, missing required attributes, unknown attribute names (lutaml-model raises UnknownAttribute).
3. Validate Wire-format drift — regex mismatches (^[a-z]{3}$ for language codes, ^[A-Z][a-z]{3}$ for scripts), enum violations on Action.type, additionalProperties: false catches typos at the object level.

The reverse direction (object → YAML) goes through the same lutaml-model framework. Round-trip parity is therefore a property of the framework, not of bespoke to_yaml code.

Layer 3 — Multilingual plumbing

EN and FR (or any ISO 639-3 pair) live as siblings inside the same file, under a single Resolution:

resolutions/ciml-39-decisions.yaml one file · both languages · git diff sees both blocks together Resolution · OIML/2016/1 identifier, doi, urn, dates declared once · language-agnostic LOCALIZATION · eng / Latn title: "Approval of the agenda" actions: [{ type: approves, message: "Approves…" }] LOCALIZATION · fra / Latn title: "Approbation de l'ordre" actions: [{ type: approves, message: "Approuve…" }] Shared admin identifier · doi · urn · dates

Three properties fall out of this shape:

  1. One file per meeting, not per language. A git diff always shows the EN line and the FR-line that changed together. Drift is visible in review, not invisible across files.
  2. The action verb is canonical. type: approves lives once per Action, regardless of language. The French block reuses it; only the message differs.
  3. Adding a third language is one new entry. No schema change, no new file convention, no migration. Drop a language_code: deu block next to the existing two.

For the deep-dive — including the translator workflow and the validation pipeline that catches drift before it ships — see Localization sync.

How the gem fits

The Ruby gem is the runtime that drives all three layers:

edoxen (gem)
├── lib/edoxen/
│   ├── models/         # lutaml-model classes (Resolution, Action, …)
│   ├── schema_validator.rb
│   └── cli/            # Thor commands: validate, normalize
├── schema/
│   └── edoxen.yaml     # JSON Schema (Draft 7) — the wire-format lock
└── exe/edoxen          # CLI entrypoint
  • lib/edoxen/models/ — Ruby classes generated from the LutaML definitions in edoxen-model. Each class declares its attributes and mappings; serialization is handled by lutaml-model.
  • schema/edoxen.yaml — the JSON Schema. Distributed inside the gem so edoxen validate works offline. (Currently scoped to ResolutionCollection; the MeetingCollection schema is in progress.)
  • exe/edoxen — the CLI. Wraps validate and normalize as Thor commands; designed for CI pipelines.

The schema and the Ruby classes are both derived from the same LutaML/UML source in edoxen/edoxen-model. The docs you are reading are the introduction; the model files are the ground truth.

Where to next