This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
bundle install— install dependenciesbundle exec rspec— run all testsbundle exec rspec spec/unit/citation_spec.rb— run a single test filebundle exec rspec spec/unit/citation_spec.rb:12— run a single test by line numberbundle exec rake— runsbundle exec rspec(default rake task)bundle exec rubocop— lint
Glossarist is a Ruby gem implementing the Glossarist concept model (ISO 10241-1). It provides classes for managing terminology glossaries with multi-language support, serialization to/from YAML, and bibliography integration via Relaton.
All model classes use Lutaml::Model::Serializable for serialization.
ManagedConceptCollection(managed_concept_collection.rb) — legacy enumerable collection of ManagedConcepts. File loading delegates to GlossaryStore via#load_from_files; the class is retained as an in-memory accumulator forSTS::Importerand the legacyConceptSetpath. New callers should use GlossaryStore directly.ManagedConcept(managed_concept.rb) — a managed concept withManagedConceptData(groups, localized_concepts map, sources), related concepts, dates, and status. Delegates localization viaadd_l10n/localization(lang).Concept(concept.rb) — base concept withConceptData(definition, terms/designations, notes, examples, sources, dates, language_code). Parent ofLocalizedConcept.LocalizedConcept(localized_concept.rb) — extendsConceptwithclassification,entry_status,review_type.ConceptData(concept_data.rb) — the data payload insideConcept: definition, terms, examples, notes, sources, non_verb_rep (collection ofNonVerbRepresource references), language_code (ISO 639), script (ISO 15924), system (ISO 24229 conversion system code). UsesDetailedDefinitioncollections for definition/examples/notes.ManagedConceptData(managed_concept_data.rb) — the data payload insideManagedConcept: id, localized_concepts hash (lang_code => uuid), domains (ConceptReference collection — terminological domain references), tags (string collection — organizational tags for grouping/filtering, not rendered as domains), sources.
NonVerbRep (non_verb_rep.rb) models non-verbal representations as URI references to external resources (images, tables, formulas), not embedded content. Attributes: type (image/table/formula), ref (URI — relative path, URN, or URL), text (alt text), sources (ConceptSource collection).
ConceptReference(concept_reference.rb) — a typed reference to another concept. Local refs useconcept_idalone; external refs usesource(URN prefix) +concept_idor a directurnfield. Haslocal?/external?predicates. Noto_gcr_hashorfrom_urn— model-driven architecture uses lutaml-model for serialization; URN construction/parsing belongs inUrnResolver.RelatedConcept(related_concept.rb) — a concept with a typed relationship. Relationship types cover ISO 10241-1 (deprecates/supersedes/superseded_by/compare/contrast/see), ISO 25964/SKOS (broader/narrower/broader_generic/narrower_generic/broader_partitive/narrower_partitive/broader_instantial/narrower_instantial/equivalent/close_match/broad_match/narrow_match/related_match), ISO 12620/TBX (homograph/false_friend/related_concept/related_concept_broader/related_concept_narrower/sequentially_related_concept/spatially_related_concept/temporally_related_concept), and designation-level (abbreviated_form_for/short_form_for). Types are defined inconfig.ymlunderrelated_concept.type.
In V3, related lives only on V3::ManagedConcept#related.
V3::ManagedConceptData does NOT declare or serialize related.
V2 placed related inside data; the SchemaMigration::V2ToV3#step_v2_to_v3
migration moves any data-level entries up to the concept level. The
V3 output never carries related at the data level.
This consolidation closes the legacy trap where writing to
data.related bypassed ManagedConcept.detect_schema_version (which
keys off concept.related).
PartitiveRelation (v3/partitive_relation.rb) — an ISO 704 / ISO 1087-1 / ISO 12620 partitive relation. One comprehensive concept (superordinate concept partitive) is connected to two or more partitive concepts (subordinate concepts partitive) which fitted together constitute the comprehensive.
Each PartitiveRelation carries:
comprehensive— ConceptRef to the superordinate concept partitive (the whole)partitives—PartitiveMember [2..*](ISO 704 "two or more"). Each member wraps a ConceptRef plus ISO 704:2022 notation metadata:multiplicity— one ofcompulsory(1 solid line, default),optional(1 dashed line),compulsory_multiple(2 solid lines),optional_multiple(2 dashed lines),compulsory_at_least_one(1 solid + 1 dashed line)is_delimiting— boolean (default false). A delimiting part behaves like a delimiting characteristic: it distinguishes the comprehensive from coordinate concepts. Rendered as a bold 3x-width line in the diagram. Orthogonal to multiplicity.
completeness—complete(default; the encoded partitives constitute the whole) orpartial(others exist beyond those encoded). ISO 704 rake backline notation.criterion— optional localized string hash ({ "eng" => "physical structure" }). ISO 12620 coordinate-concept coherence: partitives within one relation share the comprehensive AND the criterion.
Wired into V3::ManagedConcept#partitive_relations. NOT on ManagedConceptData (relationships live at the concept level for MECE consistency with related).
Enum values are SSOT-loaded from config.yml via GlossaryDefinition::COMPLETENESS_VALUES and MULTIPLICITY_VALUES. The values: option on each attribute documents the enum; the model's validate! method enforces structural invariants after construction (empty comprehensive, <2 partitives, self-loops, invalid enum values).
Semantic checks (≥2 partitives cardinality, duplicate comprehensive+criterion detection, criterion-absent warning, non-default multiplicity warning, ExternalConcept shape) live in Validation::Rules::PartitiveRelationRule (auto-registered by Validation::Rules, code GLS-221).
RDF emission: Rdf::GlossPartitiveRelation view class with companion GlossPartitiveMember. Wired into Transforms::ConceptToGlossTransform and emitted as gloss:hasPartitiveRelation link from GlossConcept.
Binary broader_partitive/narrower_partitive/has_part/is_part_of RelatedConcept edges and PartitiveRelation coexist — no automatic consolidation. Use a binary edge for pairwise relationships; use a PartitiveRelation when membership is jointly significant (≥2 partitives + completeness/multiplicity/delimiting/criterion metadata).
status: external is a value of ConceptStatus (not a separate class). A concept with status: external is referenced from this dataset but defined elsewhere — ISO 704 "parenthetic term" semantic role. Required fields (definition, sources) are relaxed; at least one designation is still required. Resolves to another dataset's concept via provided_by when a defining dataset is loaded.
Two new values in RelatedConceptType: provides (real concept → external concept) and provided_by (external concept → real concept). Inverse pair, established at the collection level.
ReferenceExtractor(reference_extractor.rb) — extractsConceptReferenceandAssetReferenceobjects from{{...}}mentions andimage::...[]references in concept text fields.ReferenceResolver(reference_resolver.rb) — resolves references via adapter chain (local → package → remote). Supports route overrides for URI remapping.UrnResolver(urn_resolver.rb) — converts URNs to canonical HTTP URLs (IEC Electropedia, ISO OBP). Extensible viaregister_scheme. All URN construction/parsing logic lives here, not in the model.
Concepts use deterministic UUID v5 (SHA-1) derived from their serialized YAML content and the OID namespace (Utilities::UUID). This means a concept's UUID is stable across sessions as long as its data doesn't change.
Designation::Base (designation/base.rb) uses a self-referencing factory pattern (of_yaml) that dispatches to subclasses based on type field: Expression, Symbol, Abbreviation, GraphicalSymbol, LetterSymbol. The bi-directional mapping is in SERIALIZED_TYPES (designation.rb).
Designation inheritance hierarchy (MECE):
- Base —
designation,normative_status(preferred/admitted/deprecated/superseded),geographical_area,type,language/ISO 639,script/ISO 15924,system/ISO 24229,international,absent,pronunciation(collection ofPronunciationobjects),sources(collection ofConceptSource— per-designation sources per ISO 10241-1 §6.8),term_type(ISO 12620 term type classification — 24 values fromconfig.yml),related(collection ofRelatedConcept— designation-level intra-entry relationships:abbreviated_form_for,short_form_for) - Expression < Base —
prefix,usage_info,field_of_application(IEC "specific use"),grammar_info - Abbreviation < Expression — abbreviation type booleans (
acronym,initialism,truncation) fromconfig.yml - Symbol < Base — (no additional attributes)
- LetterSymbol < Symbol —
text - GraphicalSymbol < Symbol —
text,image
ConceptData#domain stores URI references (relative like section-103-01, URN like urn:iec:std:iec:60050-103-01, or URL like https://...) to subject area concepts.
GlossaryStore(glossary_store.rb) — the dataset abstraction. Backed byLutaml::Store::PackageStore, handles loading/saving from directories and ZIPs, concept CRUD, metadata, bibliography, images, dataset-level non-verbal entities (#figures,#tables,#formulaslazy-loaded fromfigures//tables//formulas/subdirectories), and stats. Format detection is model-driven viaConceptDocument.for_version. All callers that need to load concepts from a dataset should use GlossaryStore.ConceptCollector(concept_collector.rb) — legacy scanner with hand-rolled format detection. No production callers; retained for ABI compatibility. Use GlossaryStore.ConceptManager(concept_manager.rb) — legacy file I/O used byManagedConceptCollection#load_from_files. No direct production callers; retained for the legacy collection path. Use GlossaryStore.ManagedConceptCollection(managed_concept_collection.rb) — legacy collection. File loading delegates to GlossaryStore; retained as an in-memory accumulator forSTS::Importer.ConceptSetnow uses GlossaryStore directly (previously went through this collection).
- Supports both camelCase and snake_case keys in YAML (e.g.,
localizedConcepts/localized_concepts) using%i[key1 key2]mapping syntax. - Also supports V1 format (
concept-*.yamlfiles at root level).
A dataset collection file — bibliography.yaml, images.yaml, and any future
equivalent — is the V3 glossarist dataset syntax: a YAML mapping with a
single wrapper key whose value is an array of typed items. No keyed maps
(indexing items by an out-of-band reference string), and no stray top-level
arrays — the array is always grouped under one named key. Each item carries its
own id field. (A keyed bibliography was tried and rejected as wrong — a
bibliography is an ordered collection, not a map, and keying forced the entry to
degenerate into citation_key + an untyped data hash. A bare top-level array
was also rejected — the user does not want stray arrays at the document root.)
Canonical models: BibliographyData + typed BibliographyEntry;
V3::ImageFile + V3::ImageEntry. bibliography.yaml wraps its entries under a
single bibliography: key. Because the root is a mapping, one key_value map
(map "bibliography", to: :entries) drives both the file (to_yaml/from_yaml)
and the in-memory store (to_hash/from_hash) — no overrides, no nested
Collection, no YAML.safe_load. BibliographyData#shortname is the
PackageStore record key only — never serialized. Documentation lives in
README.adoc (== Bibliography); this note is internal guidance, not user docs.
The remaining map nil keyed patterns live only in the V1 legacy adapters
(v1/register.rb, v1/concept.rb), which are intentional passthroughs for old
IEV-format datasets — do not "fix" them; that would break reading V1 data.
Config(config.rb) — singleton that holds registered classes for:localized_conceptand:managed_concept. Allows swapping implementations viaregister_class.GlossaryDefinition(glossary_definition.rb) — loads enum values (concept statuses, source types, etc.) fromconfig.yml.- A
glossarist.yamlfile in the working directory can register extension attributes.
Custom collection classes in collections/ extend Lutaml::Model::Collection:
LocalizationCollection— keyed bylanguage_code, used forManagedConceptData#localizations.DetailedDefinitionCollection,ConceptSourceCollection— typed collections for ConceptData fields.BibliographyCollection— extendsRelaton::Dbfor bibliography caching with cache version checking.AssetCollection,Collection(base enumerable),DesignationCollection.
Three classes handle glossary concept registry (GCR) ZIP packages:
GcrPackage(gcr_package.rb) — creates/loads/validates ZIP archives containingmetadata.yaml,register.yaml, andconcepts/*.yamlentries.GcrMetadata(gcr_metadata.rb) — package metadata (shortname, version, languages, statistics). Built from concepts viaGcrMetadata.from_concepts.GcrStatistics(gcr_statistics.rb) — computes stats (total concepts, languages, concepts by status) from concept data.SchemaMigration(schema_migration.rb) — migrates concepts from v0 (legacy IEV format) to v1, normalizing definitions, dates, entry statuses, and extracting inline references.ValidationResult(validation_result.rb) — simple errors/warnings container for package validation.
The exe/glossarist executable uses Thor. Commands:
generate_latex— converts concepts to LaTeX glossary entriespackage— creates.gcrZIP archives with optional compiled formats (--compiled-formats tbx,jsonld,turtle,jsonl)export— exports concepts in json/tbx/jsonld/turtle/jsonl formatsvalidate— validates datasets and.gcrfilesupgrade— migrates datasets to current schema version
ConceptToTbxTransform(transforms/concept_to_tbx_transform.rb) — converts ManagedConcept to TBX-XML using the tbx gem (ISO 30042:2019). ProducesTbx::ConceptEntryper concept orTbx::Documentfor full export.ConceptToSkosTransform(transforms/concept_to_skos_transform.rb) — converts ManagedConcept to SKOS RDF usingGlossarist::Rdf::SkosConcept. Hastransform(single) andtransform_document(batch, returnsSkosVocabulary). Produces JSON-LD and Turtle via the unifiedrdfDSL.ConceptToGlossTransform(transforms/concept_to_gloss_transform.rb) — converts ManagedConcept and dataset-level non-verbal entities to ontology-faithful RDF usingGlossarist::Rdf::Gloss*view classes.transform_document(concepts, figures:, tables:, formulas:)andto_turtle/to_jsonldaccept figures/tables/formulas kwargs (backward compatible — empty defaults). Uses aDESIGNATION_BUILDERSregistry keyed by exact Designation subclass (OCP) instead of a case/when switch.- SKOS/RDF models (
lib/glossarist/rdf/) —GlossConcept,GlossLocalizedConcept,GlossDesignationand subclasses,GlossFigure/GlossTable/GlossFormula/GlossFigureImage(dataset-level non-verbal entities per concept-model v3.1.0 K1/K2 shapes),SkosConcept,SkosVocabulary,LocalizedLiteral, namespace classes.EmitsExtraTriplesis autoloaded fromlutaml_ext.rbvia the immediate parent namespace file (rdf.rb);LutamlTurtleTransformExtis prepended intoLutaml::Turtle::Transformso the framework asks each instance for extra triples. - TBX, Turtle, JSON-LD, JSONL export all write a single document file; JSON writes per-concept files.
lutaml-model(~> 0.8.15) — serialization framework (YAML/XML/JSON-LD/Turtle)lutaml-store(~> 0.3) — package store backing GlossaryStorerelaton(~> 3.0.0.pre.alpha) — bibliography database integration (relaton-3 monogem prerelease line); resolves from released gems, no git pinssts(~> 0.6) — ISO/NISO STS model forSts::Importertbx(~> 0.1) — ISO 30042:2019 TBX model classesrdf-turtle(> 3.3),> 0.4) — Turtle emission + SHACL validation against vendored concept-model shapesshacl(thor— CLI commands
SchemaMigration is a thin facade over three single-concern modules:
SchemaMigration::V0ToV1— hash-to-hash transform (IEV legacy YAML → V1 shape). Single#migrateentry point.SchemaMigration::V2ToV3— model-to-model transform (V2::ManagedConcept→V3::ManagedConcept). Class methodsmigrate_concept/concept_version.SchemaMigration::CliPipeline— file I/O + dispatch + output writing forglossarist upgrade. Single#runentry point; output can be a directory of YAML files or a.gcrZIP package.
The facade preserves the historical class-method API (SchemaMigration.new, .migrate_concept, .concept_version, .upgrade_directory) so existing callers don't break. Adding V3→V4 later means adding a new module under SchemaMigration::, not editing these.
V2 and V3 namespace classes (e.g. V2::ManagedConceptData, V3::ManagedConceptData) exist because each schema version has its own key_value mapping and its own type references (V2::LocalizedConcept vs V3::LocalizedConcept). These are version-specific serialization adapters at a real seam — two adapters justifies the seam.
Do not attempt to "parameterize" or "collapse" these into the base class:
- OCP: Adding a new schema version = adding a new subclass, not modifying the base. This is the pattern working as intended.
- lutaml-model DSL is class-level:
attributeandkey_valuemappings are declarative DSL invoked at class definition time. They cannot be meaningfully "parameterized" at the instance level without metaprogramming (Class.new), which is worse than the current clear, declarative pattern. - Structural similarity ≠ accidental duplication: The fact that V2 and V3
ManagedConceptDatalook similar is because V3 evolved from V2. Theirlocalizations_from_yamlcallbacks differ in which class they instantiate, theirkey_valuemappings differ in which fields are mapped, and V2 has customrelatedhandling that V3 doesn't. These differences will diverge further as the schemas evolve. - Three similar lines is better than a wrong abstraction: The global instruction applies directly here.
See TODO.improve/ for detailed plans.
- Phase out
ManagedConceptCollectionentirely —ConceptSetnow loads via GlossaryStore directly (PR #212). The only remaining user isSTS::Importer, which uses the collection as an in-memory accumulator after loading via GlossaryStore/GcrPackage. Migrating STS::Importer to a plain Array would close the legacy chapter.
- Collapse V2/V3 ManagedConceptData — Version subclasses are a real seam, not duplication. See "Schema Version Subclasses Are NOT Duplication" above.
- Extract validation reporter from CLI::ValidateCommand — 159 lines of terminal formatting in a CLI command is normal. Creating 4 reporter classes to replace a 3-way case statement (where 2 branches are 1 line each) is premature abstraction. CLI commands are leaf nodes, not extension points.
- Refactor ConceptToGlossTransform — The transform is already deep (3 public methods, 343 lines of implementation). Moving mapping to domain models would violate model-driven by leaking RDF knowledge into the domain layer. Using lutaml-model views can't handle the type dispatching and URI construction the transform does. The module has locality (all mapping in one place) and leverage (one interface, N callers).