@glossarist/concept-browser 0.7.88 → 0.7.90

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Non-verbal entity reference normalization.
3
+ *
4
+ * Normalizes JSON-LD structural entity refs (`gl:figureRef` /
5
+ * `gl:tableRef` / `gl:formulaRef`) into the shape
6
+ * `NonVerbalReference.fromJSON` expects.
7
+ *
8
+ * Accepts three wire forms:
9
+ * - bare string ID
10
+ * - `{ "@id": "../kind/foo" }`
11
+ * - `{ "@id": "../kind/foo", "gl:display": "Figure 3" }`
12
+ *
13
+ * Emits the canonical `{ ref, display? }` shape. The path's last
14
+ * segment is the entity id; the field name (`figureRef` vs `tableRef`
15
+ * vs `formulaRef`) is the kind discriminator upstream.
16
+ */
17
+ import { GL } from '../wire-keys';
18
+
19
+ export function normalizeEntityRefs(raw: unknown): unknown[] {
20
+ if (!Array.isArray(raw)) return [];
21
+ return raw.map(normalizeOneEntityRef).filter((v): v is Record<string, string> => v !== null);
22
+ }
23
+
24
+ function normalizeOneEntityRef(entry: unknown): Record<string, string> | null {
25
+ if (typeof entry === 'string') {
26
+ const trimmed = entry.trim();
27
+ return trimmed ? { ref: trimmed } : null;
28
+ }
29
+ if (!entry || typeof entry !== 'object') return null;
30
+ const obj = entry as Record<string, unknown>;
31
+ const atId = typeof obj['@id'] === 'string' ? (obj['@id'] as string) : null;
32
+ const explicitRef = typeof obj.ref === 'string' ? obj.ref
33
+ : typeof obj.entityId === 'string' ? obj.entityId
34
+ : typeof obj.entity_id === 'string' ? obj.entity_id
35
+ : null;
36
+ const entityId = (atId ? lastPathSegment(atId) : null) ?? explicitRef;
37
+ if (!entityId) return null;
38
+ const out: Record<string, string> = { ref: entityId };
39
+ const displayRaw = obj[GL.DISPLAY] ?? obj['gloss:display'] ?? obj.display;
40
+ if (typeof displayRaw === 'string') out.display = displayRaw;
41
+ return out;
42
+ }
43
+
44
+ function lastPathSegment(p: string): string | null {
45
+ const cleaned = p.replace(/[?#].*$/, '').replace(/\/+$/, '');
46
+ const segments = cleaned.split('/');
47
+ const last = segments[segments.length - 1];
48
+ return last ? decodeURIComponent(last) : null;
49
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Model bridge: converts between wire-format JSON and glossarist-js
3
+ * model instances.
4
+ *
5
+ * Supports two input formats:
6
+ * 1. JSON-LD (gl:-prefixed) — format produced by generate-data.mjs
7
+ * 2. Glossarist native — snake_case format from glossarist-js Concept.toJSON()
8
+ *
9
+ * Public API is re-exported here. Internal organization:
10
+ * - jsonld-types.ts — wire-format interface declarations
11
+ * - bridges.ts — WeakMap bridges for fields not yet in glossarist-js
12
+ * - mappers.ts — JSON-LD → snake_case mappers
13
+ * - partitive.ts — PartitiveRelation mapping + MECE migration
14
+ * - entity-refs.ts — non-verbal entity reference normalization
15
+ * - concept.ts — top-level Concept orchestration + URI construction
16
+ */
17
+ export {
18
+ conceptFromJson,
19
+ conceptToSummary,
20
+ conceptUri,
21
+ } from './concept';
22
+
23
+ export {
24
+ getAnnotations,
25
+ getDesignationTarget,
26
+ getRefText,
27
+ getRelatedSourceId,
28
+ getRelatedCitation,
29
+ } from './bridges';
30
+
31
+ export type {
32
+ JsonLdConcept,
33
+ JsonLdLocalizedConcept,
34
+ JsonLdPartitiveMember,
35
+ JsonLdPartitiveRelation,
36
+ JsonLdDesignation,
37
+ JsonLdSource,
38
+ JsonLdOrigin,
39
+ JsonLdRelated,
40
+ JsonLdRef,
41
+ JsonLdLocality,
42
+ JsonLdDate,
43
+ JsonLdContent,
44
+ JsonLdPronunciation,
45
+ JsonLdGrammarInfo,
46
+ } from './jsonld-types';
@@ -0,0 +1,169 @@
1
+ /**
2
+ * JSON-LD wire-format types for the model-bridge.
3
+ *
4
+ * Extracted from model-bridge.ts to keep the mapper/bridge logic
5
+ * scannable. The 'gl:' prefix is the Glossarist JSON-LD namespace —
6
+ * see data/concept-model/ontologies/glossarist.context.jsonld.
7
+ *
8
+ * Wire format is dictated by glossarist-js's JSON-LD context, which
9
+ * is stable. Schema changes are rare and reviewable.
10
+ */
11
+
12
+ export interface JsonLdContent {
13
+ 'gl:content'?: string;
14
+ }
15
+
16
+ export interface JsonLdDate {
17
+ 'gl:date'?: string;
18
+ 'gl:dateType'?: string;
19
+ }
20
+
21
+ export interface JsonLdPronunciation {
22
+ 'gl:content'?: string;
23
+ 'gl:language'?: string;
24
+ 'gl:script'?: string;
25
+ 'gl:system'?: string;
26
+ 'gl:country'?: string;
27
+ }
28
+
29
+ export interface JsonLdGrammarInfo {
30
+ 'gl:gender'?: string;
31
+ 'gl:number'?: string;
32
+ 'gl:partOfSpeech'?: string;
33
+ 'gl:noun'?: boolean;
34
+ 'gl:verb'?: boolean;
35
+ 'gl:adj'?: boolean;
36
+ 'gl:adverb'?: boolean;
37
+ 'gl:preposition'?: boolean;
38
+ 'gl:participle'?: boolean;
39
+ }
40
+
41
+ export interface JsonLdRef {
42
+ 'gl:source'?: string;
43
+ 'gl:id'?: string;
44
+ 'gl:version'?: string;
45
+ 'gl:text'?: string;
46
+ source?: string;
47
+ id?: string;
48
+ version?: string;
49
+ }
50
+
51
+ export interface JsonLdLocality {
52
+ 'gl:localityType'?: string;
53
+ 'gl:referenceFrom'?: string;
54
+ 'gl:referenceTo'?: string;
55
+ type?: string;
56
+ reference_from?: string;
57
+ reference_to?: string;
58
+ }
59
+
60
+ export interface JsonLdOrigin {
61
+ 'gl:ref'?: string | JsonLdRef;
62
+ 'gl:locality'?: JsonLdLocality;
63
+ 'gl:link'?: string;
64
+ 'gl:id'?: string;
65
+ 'gl:version'?: string;
66
+ 'gl:source'?: string;
67
+ }
68
+
69
+ export interface JsonLdSource {
70
+ 'gl:id'?: string;
71
+ 'gl:sourceType'?: string;
72
+ 'gl:sourceStatus'?: string;
73
+ 'gl:modification'?: string;
74
+ 'gl:origin'?: JsonLdOrigin;
75
+ 'gl:sourcedFrom'?: JsonLdOrigin[];
76
+ 'gl:sourced_from'?: JsonLdOrigin[];
77
+ }
78
+
79
+ export interface JsonLdRelated {
80
+ 'gl:relationshipType'?: string;
81
+ 'gl:ref'?: JsonLdRef;
82
+ '@id'?: string;
83
+ 'gl:term'?: string;
84
+ 'gl:target'?: string;
85
+ 'gl:sourceId'?: string;
86
+ 'gl:citation'?: JsonLdOrigin;
87
+ }
88
+
89
+ export interface JsonLdDesignation {
90
+ '@type'?: string;
91
+ 'gl:term'?: string;
92
+ 'gl:normativeStatus'?: string;
93
+ 'gl:absent'?: unknown;
94
+ 'gl:fieldOfApplication'?: string;
95
+ 'gl:usageInfo'?: string;
96
+ 'gl:geographicalArea'?: string;
97
+ 'gl:language'?: string;
98
+ 'gl:script'?: string;
99
+ 'gl:system'?: string;
100
+ 'gl:international'?: boolean;
101
+ 'gl:termType'?: string;
102
+ 'gl:pronunciation'?: JsonLdPronunciation[];
103
+ 'gl:source'?: JsonLdSource[];
104
+ 'gl:related'?: JsonLdRelated[];
105
+ 'gl:prefix'?: string;
106
+ 'gl:gender'?: string;
107
+ 'gl:grammarInfo'?: JsonLdGrammarInfo[];
108
+ }
109
+
110
+ export interface JsonLdLocalizedConcept {
111
+ 'gl:languageCode'?: string;
112
+ 'gl:entryStatus'?: string;
113
+ 'gl:classification'?: string;
114
+ 'gl:reviewType'?: string;
115
+ 'gl:domain'?: string;
116
+ 'gl:release'?: string;
117
+ 'gl:lineageSourceSimilarity'?: number;
118
+ 'gl:script'?: string;
119
+ 'gl:system'?: string;
120
+ 'gl:designation'?: JsonLdDesignation[];
121
+ 'gl:definition'?: JsonLdContent[];
122
+ 'gl:notes'?: JsonLdContent[];
123
+ 'gl:annotations'?: JsonLdContent[];
124
+ 'gl:examples'?: JsonLdContent[];
125
+ 'gl:source'?: JsonLdSource[];
126
+ 'gl:dates'?: JsonLdDate[];
127
+ 'gl:references'?: JsonLdRelated[];
128
+ 'gl:reviewDate'?: string;
129
+ 'gl:reviewDecisionDate'?: string;
130
+ 'gl:reviewDecisionEvent'?: string;
131
+ 'gl:reviewStatus'?: string;
132
+ 'gl:reviewDecision'?: string;
133
+ 'gl:reviewDecisionNotes'?: string;
134
+ }
135
+
136
+ export interface JsonLdPartitiveMember {
137
+ 'gl:ref'?: JsonLdRef;
138
+ /** ISO 704:2022 MECE: presence (required | optional) */
139
+ 'gl:presence'?: string;
140
+ /** ISO 704:2022 MECE: count (exactly_one | at_least_one | multiple) */
141
+ 'gl:count'?: string;
142
+ /** ISO 704:2022: delimiting part (3× stroke width in diagrams) */
143
+ 'gl:isDelimiting'?: boolean;
144
+ /** Legacy v3 field — one-string multiplicity, split into presence+count */
145
+ 'gl:multiplicity'?: string;
146
+ /** Legacy v2 field — migrated to presence+count */
147
+ 'gl:certainty'?: string;
148
+ }
149
+
150
+ export interface JsonLdPartitiveRelation {
151
+ 'gl:comprehensive'?: JsonLdRef;
152
+ 'gl:hasPartitive'?: JsonLdPartitiveMember[];
153
+ 'gl:completeness'?: string;
154
+ 'gl:criterion'?: Record<string, string> | string;
155
+ }
156
+
157
+ export interface JsonLdConcept {
158
+ '@type'?: string;
159
+ '@id'?: string;
160
+ 'gl:identifier'?: string | number;
161
+ 'gl:term'?: string;
162
+ 'gl:localizedConcept'?: Record<string, JsonLdLocalizedConcept>;
163
+ 'gl:related'?: JsonLdRelated[];
164
+ 'gl:partitiveRelations'?: JsonLdPartitiveRelation[];
165
+ 'gl:tags'?: string[];
166
+ 'gl:figureRef'?: unknown[];
167
+ 'gl:tableRef'?: unknown[];
168
+ 'gl:formulaRef'?: unknown[];
169
+ }
@@ -0,0 +1,277 @@
1
+ /**
2
+ * JSON-LD → Glossarist native mapping functions.
3
+ *
4
+ * Each mapper reads the wire-format JSON-LD (gl:-prefixed keys, see
5
+ * src/adapters/wire-keys.ts for the SSOT) and emits the snake_case
6
+ * dict that glossarist-js's Model.fromJSON constructors expect.
7
+ *
8
+ * Mappers are pure functions — no side effects, no global state.
9
+ * Bridge machinery (WeakMaps) lives in bridges.ts.
10
+ */
11
+ import { GL } from '../wire-keys';
12
+ import { DESIGNATION_REL_TYPES } from './bridges';
13
+ import type {
14
+ JsonLdDesignation,
15
+ JsonLdRef,
16
+ JsonLdLocality,
17
+ JsonLdOrigin,
18
+ JsonLdSource,
19
+ JsonLdRelated,
20
+ JsonLdLocalizedConcept,
21
+ JsonLdConcept,
22
+ } from './jsonld-types';
23
+
24
+ export function isJsonLd(doc: Record<string, unknown>): boolean {
25
+ return '@type' in doc && GL.LOCALIZED_CONCEPT in doc;
26
+ }
27
+
28
+ export function mapDesignationFromJsonLd(d: JsonLdDesignation): Record<string, unknown> {
29
+ const result: Record<string, unknown> = {};
30
+ const rawType = d['@type'] || '';
31
+
32
+ if (rawType.includes('Abbreviation')) result.type = 'abbreviation';
33
+ else if (rawType.includes('LetterSymbol')) result.type = 'letter_symbol';
34
+ else if (rawType.includes('GraphicalSymbol')) result.type = 'graphical_symbol';
35
+ else if (rawType.includes('Symbol')) result.type = 'symbol';
36
+ else result.type = 'expression';
37
+
38
+ result.designation = d[GL.TERM] ?? '';
39
+ result.normative_status = d[GL.NORMATIVE_STATUS] ?? null;
40
+
41
+ if (d[GL.ABSENT] != null) result.absent = d[GL.ABSENT];
42
+ if (d[GL.FIELD_OF_APPLICATION]) result.field_of_application = d[GL.FIELD_OF_APPLICATION];
43
+ if (d[GL.USAGE_INFO]) result.usage_info = d[GL.USAGE_INFO];
44
+ if (d[GL.GEOGRAPHICAL_AREA]) result.geographical_area = d[GL.GEOGRAPHICAL_AREA];
45
+ if (d[GL.LANGUAGE]) result.language = d[GL.LANGUAGE];
46
+ if (d[GL.SCRIPT]) result.script = d[GL.SCRIPT];
47
+ if (d[GL.SYSTEM]) result.system = d[GL.SYSTEM];
48
+ if (d[GL.INTERNATIONAL] != null) result.international = d[GL.INTERNATIONAL];
49
+ if (d[GL.TERM_TYPE]) result.term_type = d[GL.TERM_TYPE];
50
+
51
+ const pronunciation = d[GL.PRONUNCIATION];
52
+ if (pronunciation?.length) {
53
+ result.pronunciation = pronunciation.map(p => ({
54
+ content: p[GL.CONTENT] ?? null,
55
+ language: p[GL.LANGUAGE] ?? null,
56
+ script: p[GL.SCRIPT] ?? null,
57
+ system: p[GL.SYSTEM] ?? null,
58
+ country: p[GL.COUNTRY] ?? null,
59
+ }));
60
+ }
61
+
62
+ const sources = d[GL.SOURCE];
63
+ if (sources?.length) {
64
+ result.sources = sources.map(mapSourceFromJsonLd);
65
+ }
66
+
67
+ const related = d[GL.RELATED];
68
+ if (related?.length) {
69
+ result.related = related.map(r => {
70
+ const relType = r[GL.RELATIONSHIP_TYPE] ?? 'references';
71
+ if (DESIGNATION_REL_TYPES.has(relType) && r[GL.TARGET]) {
72
+ return { type: relType, target: r[GL.TARGET] };
73
+ }
74
+ return mapRelatedFromJsonLd(r);
75
+ });
76
+ }
77
+
78
+ if (d[GL.PREFIX] != null) result.prefix = d[GL.PREFIX];
79
+ if (d[GL.GENDER]) {
80
+ result.grammar_info = [{ gender: d[GL.GENDER] }];
81
+ }
82
+ const grammarInfo = d[GL.GRAMMAR_INFO];
83
+ if (grammarInfo?.length) {
84
+ result.grammar_info = grammarInfo.map(gi => ({
85
+ gender: gi[GL.GENDER] ?? null,
86
+ number: gi[GL.NUMBER] ?? null,
87
+ part_of_speech: gi[GL.PART_OF_SPEECH] ?? null,
88
+ noun: gi[GL.NOUN] ?? false,
89
+ verb: gi[GL.VERB] ?? false,
90
+ adj: gi[GL.ADJ] ?? false,
91
+ adverb: gi[GL.ADVERB] ?? false,
92
+ preposition: gi[GL.PREPOSITION] ?? false,
93
+ participle: gi[GL.PARTICIPLE] ?? false,
94
+ }));
95
+ }
96
+
97
+ return result;
98
+ }
99
+
100
+ export function mapRefFromJsonLd(rawRef: JsonLdRef | string | undefined): Record<string, unknown> | null {
101
+ if (!rawRef) return null;
102
+ if (typeof rawRef === 'string') return { source: rawRef };
103
+ const refObj: Record<string, unknown> = {};
104
+ // gl:-prefixed keys take precedence over unprefixed keys
105
+ refObj.source = rawRef[GL.SOURCE] ?? rawRef.source;
106
+ refObj.id = rawRef[GL.LOCAL_ID] ?? rawRef.id;
107
+ refObj.version = rawRef[GL.VERSION] ?? rawRef.version;
108
+ if (rawRef[GL.TEXT]) refObj.text = rawRef[GL.TEXT];
109
+ return (refObj.source ?? refObj.id ?? refObj.version ?? refObj.text) != null
110
+ ? refObj : null;
111
+ }
112
+
113
+ /**
114
+ * Map JSON-LD locality to glossarist's snake_case format.
115
+ * Always uses snake_case (reference_from/reference_to) for consistency
116
+ * with the glossarist model.
117
+ */
118
+ export function mapLocalityFromJsonLd(rawLoc: JsonLdLocality | undefined): Record<string, unknown> | null {
119
+ if (!rawLoc) return null;
120
+ const locObj: Record<string, unknown> = {};
121
+ locObj.type = rawLoc[GL.LOCALITY_TYPE] ?? rawLoc.type;
122
+ locObj.reference_from = rawLoc[GL.REFERENCE_FROM] ?? rawLoc.reference_from;
123
+ locObj.reference_to = rawLoc[GL.REFERENCE_TO] ?? rawLoc.reference_to;
124
+ return (locObj.type ?? locObj.reference_from ?? locObj.reference_to) != null
125
+ ? locObj : null;
126
+ }
127
+
128
+ export function mapDetailedDefinitionFromJsonLd(d: any): Record<string, unknown> {
129
+ const result: Record<string, unknown> = { content: d[GL.CONTENT] ?? '' };
130
+ if (d[GL.EXAMPLES]?.length) {
131
+ result.examples = d[GL.EXAMPLES].map(mapDetailedDefinitionFromJsonLd);
132
+ }
133
+ return result;
134
+ }
135
+
136
+ export function mapOriginFromJsonLd(o: JsonLdOrigin): Record<string, unknown> {
137
+ const origin: Record<string, unknown> = {};
138
+ const ref = mapRefFromJsonLd(o[GL.REF]);
139
+ if (ref) origin.ref = ref;
140
+ const loc = mapLocalityFromJsonLd(o[GL.LOCALITY]);
141
+ if (loc) origin.locality = loc;
142
+ if (o[GL.LINK]) origin.link = o[GL.LINK];
143
+ if (o[GL.LOCAL_ID]) origin.id = o[GL.LOCAL_ID];
144
+ if (o[GL.VERSION]) origin.version = o[GL.VERSION];
145
+ if (o[GL.SOURCE]) origin.source = o[GL.SOURCE];
146
+ return origin;
147
+ }
148
+
149
+ export function mapSourceFromJsonLd(s: JsonLdSource): Record<string, unknown> {
150
+ const result: Record<string, unknown> = {};
151
+ // JsonLdSource uses GL.LOCAL_ID ('gl:id') for the source's local id,
152
+ // distinct from JSON-LD's '@id' (the concept IRI).
153
+ if (s[GL.LOCAL_ID]) result.id = s[GL.LOCAL_ID];
154
+ if (s[GL.SOURCE_TYPE]) result.type = s[GL.SOURCE_TYPE];
155
+ if (s[GL.SOURCE_STATUS]) result.status = s[GL.SOURCE_STATUS];
156
+ if (s[GL.MODIFICATION]) result.modification = s[GL.MODIFICATION];
157
+
158
+ const origin = s[GL.ORIGIN];
159
+ if (origin) {
160
+ result.origin = mapOriginFromJsonLd(origin);
161
+ }
162
+
163
+ const sf = s[GL.SOURCED_FROM] ?? s[GL.SOURCED_FROM_ALT];
164
+ if (sf?.length) {
165
+ result.sourced_from = sf.map(item => mapOriginFromJsonLd(item));
166
+ }
167
+
168
+ return result;
169
+ }
170
+
171
+ export function mapRelatedFromJsonLd(r: JsonLdRelated): Record<string, unknown> {
172
+ const result: Record<string, unknown> = { type: 'references' };
173
+
174
+ if (r[GL.RELATIONSHIP_TYPE]) {
175
+ result.type = r[GL.RELATIONSHIP_TYPE];
176
+ }
177
+
178
+ if (r[GL.REF]) {
179
+ const ref = mapRefFromJsonLd(r[GL.REF]);
180
+ if (ref) result.ref = ref;
181
+ }
182
+
183
+ if (!result.ref && r['@id']) {
184
+ const uri = r['@id'];
185
+ const idMatch = uri.match(/\/concept\/([^/]+)$/);
186
+ result.ref = idMatch
187
+ ? { source: uri.split('/').slice(-3, -2)[0] || '', id: idMatch[1] }
188
+ : { source: uri, id: null };
189
+ }
190
+ if (r[GL.TERM]) result.content = r[GL.TERM];
191
+
192
+ // Bridged fields — stored in raw dict, extracted by attachBridges()
193
+ if (r[GL.SOURCE_ID]) result.sourceId = r[GL.SOURCE_ID];
194
+ const rawCitation = r[GL.CITATION];
195
+ if (rawCitation) {
196
+ const c = rawCitation;
197
+ const citation: Record<string, unknown> = {};
198
+ if (c[GL.REF]) {
199
+ const cr = mapRefFromJsonLd(c[GL.REF]);
200
+ if (cr) citation.ref = cr;
201
+ }
202
+ const loc = mapLocalityFromJsonLd(c[GL.LOCALITY]);
203
+ if (loc) citation.locality = loc;
204
+ if (c[GL.LINK]) citation.link = c[GL.LINK];
205
+ if (Object.keys(citation).length > 0) result.citation = citation;
206
+ }
207
+
208
+ return result;
209
+ }
210
+
211
+ export function mapLocalizedFromJsonLd(lc: JsonLdLocalizedConcept): Record<string, unknown> {
212
+ const data: Record<string, unknown> = {};
213
+
214
+ if (lc[GL.LANGUAGE_CODE]) data.language_code = lc[GL.LANGUAGE_CODE];
215
+ if (lc[GL.ENTRY_STATUS]) data.entry_status = lc[GL.ENTRY_STATUS];
216
+ if (lc[GL.CLASSIFICATION]) data.classification = lc[GL.CLASSIFICATION];
217
+ if (lc[GL.REVIEW_TYPE]) data.review_type = lc[GL.REVIEW_TYPE];
218
+ if (lc[GL.DOMAIN]) data.domain = lc[GL.DOMAIN];
219
+ if (lc[GL.RELEASE]) data.release = lc[GL.RELEASE];
220
+ if (lc[GL.LINEAGE_SOURCE_SIMILARITY] != null) data.lineage_source_similarity = lc[GL.LINEAGE_SOURCE_SIMILARITY];
221
+ if (lc[GL.SCRIPT]) data.script = lc[GL.SCRIPT];
222
+ if (lc[GL.SYSTEM]) data.system = lc[GL.SYSTEM];
223
+
224
+ const designation = lc[GL.DESIGNATION];
225
+ if (designation?.length) {
226
+ data.terms = designation.map(mapDesignationFromJsonLd);
227
+ }
228
+
229
+ const definition = lc[GL.DEFINITION];
230
+ if (definition?.length) {
231
+ data.definition = definition.map(mapDetailedDefinitionFromJsonLd);
232
+ }
233
+
234
+ const notes = lc[GL.NOTES];
235
+ if (notes?.length) {
236
+ data.notes = notes.map(mapDetailedDefinitionFromJsonLd);
237
+ }
238
+
239
+ const annotations = lc[GL.ANNOTATIONS];
240
+ if (annotations?.length) {
241
+ data.annotations = annotations.map(mapDetailedDefinitionFromJsonLd);
242
+ }
243
+
244
+ const examples = lc[GL.EXAMPLES];
245
+ if (examples?.length) {
246
+ data.examples = examples.map(mapDetailedDefinitionFromJsonLd);
247
+ }
248
+
249
+ const lcSources = lc[GL.SOURCE];
250
+ if (lcSources?.length) {
251
+ data.sources = lcSources.map(mapSourceFromJsonLd);
252
+ }
253
+
254
+ const dates = lc[GL.DATES];
255
+ if (dates?.length) {
256
+ data.dates = dates.map(d => ({
257
+ date: d[GL.DATE] ?? null,
258
+ type: d[GL.DATE_TYPE] ?? null,
259
+ }));
260
+ }
261
+
262
+ const lcReferences = lc[GL.REFERENCES];
263
+ if (lcReferences?.length) {
264
+ data.related = lcReferences.map(mapRelatedFromJsonLd);
265
+ }
266
+
267
+ if (lc[GL.REVIEW_DATE]) data.review_date = lc[GL.REVIEW_DATE];
268
+ if (lc[GL.REVIEW_DECISION_DATE]) data.review_decision_date = lc[GL.REVIEW_DECISION_DATE];
269
+ if (lc[GL.REVIEW_DECISION_EVENT]) data.review_decision_event = lc[GL.REVIEW_DECISION_EVENT];
270
+ if (lc[GL.REVIEW_STATUS]) data.review_status = lc[GL.REVIEW_STATUS];
271
+ if (lc[GL.REVIEW_DECISION]) data.review_decision = lc[GL.REVIEW_DECISION];
272
+ if (lc[GL.REVIEW_DECISION_NOTES]) data.review_decision_notes = lc[GL.REVIEW_DECISION_NOTES];
273
+
274
+ return data;
275
+ }
276
+
277
+ export type { JsonLdConcept };
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Partitive relation mapping: JSON-LD → glossarist-js PartitiveRelation.
3
+ *
4
+ * Handles ISO 704:2022 MECE axes (presence × count) plus legacy
5
+ * migration from the 5-value `multiplicity` enum and the 2-value
6
+ * `certainty` field. See BRIDGES.md for the migration contract.
7
+ */
8
+ import { GL } from '../wire-keys';
9
+ import { isPartitivePresence, isPartitiveCount } from '../../utils/partitive-multiplicity';
10
+ import { mapRefFromJsonLd } from './mappers';
11
+ import type { JsonLdPartitiveRelation } from './jsonld-types';
12
+
13
+ export function mapPartitiveRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<string, unknown> | null {
14
+ const comprehensive = r[GL.COMPREHENSIVE] ? mapRefFromJsonLd(r[GL.COMPREHENSIVE]) : null;
15
+ if (!comprehensive) return null;
16
+
17
+ const partitives = (r[GL.HAS_PARTITIVE] ?? [])
18
+ .map((m): Record<string, unknown> | null => {
19
+ const ref = m[GL.REF] ? mapRefFromJsonLd(m[GL.REF]) : null;
20
+ if (!ref) return null;
21
+ const out: Record<string, unknown> = { ref };
22
+ // ISO 704:2022 MECE: prefer presence × count from JSON-LD. Fall back
23
+ // to migrating the legacy one-string `multiplicity` or v2 `certainty`
24
+ // so data in transit from older glossarist versions still loads.
25
+ const presence = m[GL.PRESENCE];
26
+ const count = m[GL.COUNT];
27
+ if (isPartitivePresence(presence) && isPartitiveCount(count)) {
28
+ out.presence = presence;
29
+ out.count = count;
30
+ } else {
31
+ const raw = m[GL.MULTIPLICITY] ?? splitLegacyCertainty(m[GL.CERTAINTY]);
32
+ if (raw) {
33
+ const parts = splitMultiplicity(raw);
34
+ out.presence = parts.presence;
35
+ out.count = parts.count;
36
+ }
37
+ }
38
+ if (m[GL.IS_DELIMITING] === true) out.is_delimiting = true;
39
+ return out;
40
+ })
41
+ .filter((m): m is Record<string, unknown> => m !== null);
42
+
43
+ // ISO 704 requires ≥2 partitives. Skip malformed relations rather
44
+ // than letting the constructor throw and break the whole concept.
45
+ if (partitives.length < 2) return null;
46
+
47
+ const out: Record<string, unknown> = {
48
+ comprehensive,
49
+ partitives,
50
+ };
51
+
52
+ if (r[GL.COMPLETENESS]) out.completeness = r[GL.COMPLETENESS];
53
+
54
+ if (r[GL.CRITERION]) {
55
+ out.criterion = typeof r[GL.CRITERION] === 'string'
56
+ ? { default: r[GL.CRITERION] }
57
+ : r[GL.CRITERION];
58
+ }
59
+
60
+ return out;
61
+ }
62
+
63
+ /** Migrate legacy v2 certainty → old v3 multiplicity string. */
64
+ function splitLegacyCertainty(certainty: string | undefined): string | null {
65
+ if (!certainty) return null;
66
+ return certainty === 'possible' ? 'optional' : 'compulsory';
67
+ }
68
+
69
+ /** Split the legacy one-string multiplicity into MECE presence × count. */
70
+ function splitMultiplicity(m: string): { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' } {
71
+ const LEGACY_MAP: Record<string, { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' }> = {
72
+ compulsory: { presence: 'required', count: 'exactly_one' },
73
+ optional: { presence: 'optional', count: 'exactly_one' },
74
+ compulsory_multiple: { presence: 'required', count: 'multiple' },
75
+ optional_multiple: { presence: 'optional', count: 'multiple' },
76
+ compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
77
+ };
78
+ return LEGACY_MAP[m] ?? { presence: 'required', count: 'exactly_one' };
79
+ }