@glossarist/concept-browser 0.7.91 → 0.7.93
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.
- package/package.json +2 -2
- package/src/__tests__/components/GenericRelationList.test.ts +147 -0
- package/src/__tests__/components/GenericRelationList.test.ts.bak +147 -0
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +22 -23
- package/src/__tests__/composables/use-concept-edges-v2.test.ts.bak +166 -0
- package/src/__tests__/model-bridge-partitive-migration.test.ts +2 -2
- package/src/__tests__/wire-keys.test.ts +1 -1
- package/src/__tests__/wire-keys.test.ts.bak +65 -0
- package/src/adapters/model-bridge/concept.ts +12 -2
- package/src/adapters/model-bridge/hyperedge.ts +103 -0
- package/src/adapters/model-bridge/jsonld-types.ts +3 -0
- package/src/adapters/non-verbal/glossarist-augment.d.ts +77 -46
- package/src/adapters/types.ts +27 -0
- package/src/adapters/wire-keys.ts +4 -0
- package/src/components/ConceptDetail.vue +10 -0
- package/src/components/GenericRelationList.vue +176 -0
- package/src/components/PartitiveRelationList.vue +2 -19
- package/src/composables/use-concept-edges.ts +56 -52
- package/src/utils/resolve-designation.ts +50 -0
- package/src/views/ConceptView.vue +1 -1
|
@@ -10,7 +10,7 @@ import type { ConceptSummary } from '../types';
|
|
|
10
10
|
import { ConceptIdentity } from '../concept-identity';
|
|
11
11
|
import { GL } from '../wire-keys';
|
|
12
12
|
import { isJsonLd, mapLocalizedFromJsonLd, mapRelatedFromJsonLd } from './mappers';
|
|
13
|
-
import {
|
|
13
|
+
import { mapHyperedgeFromJsonLd } from './hyperedge';
|
|
14
14
|
import { normalizeEntityRefs } from './entity-refs';
|
|
15
15
|
import { attachBridges, attachSourcedFromToSources } from './bridges';
|
|
16
16
|
import type { JsonLdConcept } from './jsonld-types';
|
|
@@ -27,9 +27,18 @@ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
const related = (doc[GL.RELATED] ?? []).map(mapRelatedFromJsonLd);
|
|
30
|
+
|
|
31
|
+
// Unified hyperedge mapping: both partitive and generic relations
|
|
32
|
+
// share the same JSON-LD wire shape. glossarist-js dispatches by
|
|
33
|
+
// wire key (`partitive_relations` / `generic_relations`).
|
|
30
34
|
const partitiveRelations = (doc[GL.PARTITIVE_RELATIONS] ?? [])
|
|
31
|
-
.map(
|
|
35
|
+
.map((r: any) => mapHyperedgeFromJsonLd(r, 'gl:hasPartitive'))
|
|
36
|
+
.filter((r): r is Record<string, unknown> => r !== null);
|
|
37
|
+
|
|
38
|
+
const genericRelations = (doc[GL.GENERIC_RELATIONS] ?? [])
|
|
39
|
+
.map((r: any) => mapHyperedgeFromJsonLd(r, 'gl:hasGeneric'))
|
|
32
40
|
.filter((r): r is Record<string, unknown> => r !== null);
|
|
41
|
+
|
|
33
42
|
const tagsArr = doc[GL.TAGS];
|
|
34
43
|
const tags = Array.isArray(tagsArr) ? [...tagsArr] : [];
|
|
35
44
|
|
|
@@ -40,6 +49,7 @@ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
|
|
|
40
49
|
localizations,
|
|
41
50
|
related,
|
|
42
51
|
partitive_relations: partitiveRelations,
|
|
52
|
+
generic_relations: genericRelations,
|
|
43
53
|
tags,
|
|
44
54
|
figures: normalizeEntityRefs(doc[GL.FIGURE_REF]),
|
|
45
55
|
tables: normalizeEntityRefs(doc[GL.TABLE_REF]),
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hyperedge (n-ary relation) mapping: JSON-LD → glossarist-js model.
|
|
3
|
+
*
|
|
4
|
+
* Handles ISO 704:2022 n-ary relations — both PartitiveHyperedge and
|
|
5
|
+
* GenericHyperedge share the same JSON-LD wire shape. The mapper is
|
|
6
|
+
* identical for both; the glossarist-js Concept constructor dispatches
|
|
7
|
+
* by wire key (`partitive_relations` vs `generic_relations`).
|
|
8
|
+
*
|
|
9
|
+
* MECE migration: reads presence × count directly (ISO 704:2022 v3).
|
|
10
|
+
* Falls back to migrating the legacy 5-value `multiplicity` or v2
|
|
11
|
+
* `certainty` field.
|
|
12
|
+
*/
|
|
13
|
+
import { GL } from '../wire-keys';
|
|
14
|
+
import { isPartitivePresence, isPartitiveCount } from '../../utils/partitive-multiplicity';
|
|
15
|
+
import { mapRefFromJsonLd } from './mappers';
|
|
16
|
+
import type { JsonLdPartitiveRelation } from './jsonld-types';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Map a JSON-LD relation entry to the raw dict shape that
|
|
20
|
+
* glossarist-js's AbstractHyperedge subclasses accept.
|
|
21
|
+
*
|
|
22
|
+
* Works for both PartitiveHyperedge and GenericHyperedge — the only
|
|
23
|
+
* difference at the wire level is which wire key the parent dict
|
|
24
|
+
* uses (`partitive_relations` vs `generic_relations`).
|
|
25
|
+
*
|
|
26
|
+
* Reads `gl:hasPartitive`, `gl:hasGeneric`, or `gl:members` for
|
|
27
|
+
* the member array (in order of preference).
|
|
28
|
+
*/
|
|
29
|
+
export function mapHyperedgeFromJsonLd(
|
|
30
|
+
r: JsonLdPartitiveRelation,
|
|
31
|
+
memberKey: 'gl:hasPartitive' | 'gl:hasGeneric' | 'gl:members' = 'gl:hasPartitive',
|
|
32
|
+
): Record<string, unknown> | null {
|
|
33
|
+
const comprehensive = r[GL.COMPREHENSIVE] ? mapRefFromJsonLd(r[GL.COMPREHENSIVE]) : null;
|
|
34
|
+
if (!comprehensive) return null;
|
|
35
|
+
|
|
36
|
+
const rawMembers = r[GL.HAS_PARTITIVE] ?? r[GL.HAS_GENERIC] ?? r[GL.HAS_MEMBER] ?? r[memberKey] ?? [];
|
|
37
|
+
const partitives = (rawMembers as any[])
|
|
38
|
+
.map((m): Record<string, unknown> | null => {
|
|
39
|
+
const ref = m[GL.REF] ? mapRefFromJsonLd(m[GL.REF]) : null;
|
|
40
|
+
if (!ref) return null;
|
|
41
|
+
const out: Record<string, unknown> = { ref };
|
|
42
|
+
const presence = m[GL.PRESENCE];
|
|
43
|
+
const count = m[GL.COUNT];
|
|
44
|
+
if (isPartitivePresence(presence) && isPartitiveCount(count)) {
|
|
45
|
+
out.presence = presence;
|
|
46
|
+
out.count = count;
|
|
47
|
+
} else {
|
|
48
|
+
const raw = m[GL.MULTIPLICITY] ?? splitLegacyCertainty(m[GL.CERTAINTY]);
|
|
49
|
+
if (raw) {
|
|
50
|
+
const parts = splitMultiplicity(raw);
|
|
51
|
+
out.presence = parts.presence;
|
|
52
|
+
out.count = parts.count;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (m[GL.IS_DELIMITING] === true) out.is_delimiting = true;
|
|
56
|
+
// ISO 704:2022 §5.5.4.2.1 — GenericMember carries a required
|
|
57
|
+
// delimitingCharacteristic (the intension difference between
|
|
58
|
+
// this specific concept and its coordinate concepts).
|
|
59
|
+
const dc = m[GL.DELIMITING_CHARACTERISTIC];
|
|
60
|
+
if (dc) {
|
|
61
|
+
out.delimitingCharacteristic = typeof dc === 'string'
|
|
62
|
+
? { default: dc }
|
|
63
|
+
: dc;
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
})
|
|
67
|
+
.filter((m): m is Record<string, unknown> => m !== null);
|
|
68
|
+
|
|
69
|
+
if (partitives.length < 2) return null;
|
|
70
|
+
|
|
71
|
+
const out: Record<string, unknown> = {
|
|
72
|
+
comprehensive,
|
|
73
|
+
members: partitives,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
if (r[GL.COMPLETENESS]) out.completeness = r[GL.COMPLETENESS];
|
|
77
|
+
|
|
78
|
+
if (r[GL.CRITERION]) {
|
|
79
|
+
out.criterion = typeof r[GL.CRITERION] === 'string'
|
|
80
|
+
? { default: r[GL.CRITERION] }
|
|
81
|
+
: r[GL.CRITERION];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return out;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Migrate legacy v2 certainty → old v3 multiplicity string. */
|
|
88
|
+
function splitLegacyCertainty(certainty: string | undefined): string | null {
|
|
89
|
+
if (!certainty) return null;
|
|
90
|
+
return certainty === 'possible' ? 'optional' : 'compulsory';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Split the legacy one-string multiplicity into MECE presence × count. */
|
|
94
|
+
function splitMultiplicity(m: string): { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' } {
|
|
95
|
+
const LEGACY_MAP: Record<string, { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' }> = {
|
|
96
|
+
compulsory: { presence: 'required', count: 'exactly_one' },
|
|
97
|
+
optional: { presence: 'optional', count: 'exactly_one' },
|
|
98
|
+
compulsory_multiple: { presence: 'required', count: 'multiple' },
|
|
99
|
+
optional_multiple: { presence: 'optional', count: 'multiple' },
|
|
100
|
+
compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
|
|
101
|
+
};
|
|
102
|
+
return LEGACY_MAP[m] ?? { presence: 'required', count: 'exactly_one' };
|
|
103
|
+
}
|
|
@@ -150,6 +150,8 @@ export interface JsonLdPartitiveMember {
|
|
|
150
150
|
export interface JsonLdPartitiveRelation {
|
|
151
151
|
'gl:comprehensive'?: JsonLdRef;
|
|
152
152
|
'gl:hasPartitive'?: JsonLdPartitiveMember[];
|
|
153
|
+
'gl:hasGeneric'?: JsonLdPartitiveMember[];
|
|
154
|
+
'gl:members'?: JsonLdPartitiveMember[];
|
|
153
155
|
'gl:completeness'?: string;
|
|
154
156
|
'gl:criterion'?: Record<string, string> | string;
|
|
155
157
|
}
|
|
@@ -162,6 +164,7 @@ export interface JsonLdConcept {
|
|
|
162
164
|
'gl:localizedConcept'?: Record<string, JsonLdLocalizedConcept>;
|
|
163
165
|
'gl:related'?: JsonLdRelated[];
|
|
164
166
|
'gl:partitiveRelations'?: JsonLdPartitiveRelation[];
|
|
167
|
+
'gl:genericRelations'?: JsonLdPartitiveRelation[];
|
|
165
168
|
'gl:tags'?: string[];
|
|
166
169
|
'gl:figureRef'?: unknown[];
|
|
167
170
|
'gl:tableRef'?: unknown[];
|
|
@@ -170,55 +170,79 @@ declare module 'glossarist' {
|
|
|
170
170
|
function pairFromMultiplicity(name: Multiplicity): { presence: PartitivePresence; count: PartitiveCount };
|
|
171
171
|
|
|
172
172
|
interface Concept {
|
|
173
|
-
readonly
|
|
173
|
+
readonly relations: AbstractHyperedge[];
|
|
174
|
+
readonly partitiveRelations: PartitiveHyperedge[];
|
|
175
|
+
readonly genericRelations: GenericHyperedge[];
|
|
174
176
|
}
|
|
175
177
|
|
|
176
178
|
// Re-exposed here so callers importing from the top-level `glossarist`
|
|
177
179
|
// entry get the MECE-augmented shape, not the stale d.ts type.
|
|
178
|
-
export class
|
|
180
|
+
export class HyperedgeMember extends GlossaristModel {
|
|
179
181
|
constructor(data?: {
|
|
180
182
|
ref?: ConceptRef;
|
|
181
183
|
presence?: PartitivePresence;
|
|
182
184
|
count?: PartitiveCount;
|
|
183
|
-
is_delimiting?: boolean;
|
|
184
185
|
});
|
|
185
186
|
readonly ref: ConceptRef;
|
|
186
187
|
readonly presence: PartitivePresence;
|
|
187
188
|
readonly count: PartitiveCount;
|
|
189
|
+
required(): boolean;
|
|
190
|
+
optional(): boolean;
|
|
191
|
+
toJSON(): Record<string, unknown>;
|
|
192
|
+
static fromJSON(data: Record<string, unknown>): HyperedgeMember;
|
|
193
|
+
static identityOf(value: unknown): string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export class PartitiveMember extends HyperedgeMember {
|
|
197
|
+
constructor(data?: {
|
|
198
|
+
ref?: ConceptRef;
|
|
199
|
+
presence?: PartitivePresence;
|
|
200
|
+
count?: PartitiveCount;
|
|
201
|
+
is_delimiting?: boolean;
|
|
202
|
+
});
|
|
188
203
|
readonly is_delimiting: boolean;
|
|
189
|
-
readonly isRequired: boolean;
|
|
190
|
-
readonly isOptional: boolean;
|
|
191
204
|
get isDelimiting(): boolean;
|
|
192
|
-
toJSON(): { ref: ReturnType<ConceptRef['toJSON']>; presence?: PartitivePresence; count?: PartitiveCount; is_delimiting?: boolean };
|
|
193
|
-
static fromJSON(data: Record<string, unknown>): PartitiveMember;
|
|
194
|
-
static identityOf(value: unknown): string;
|
|
195
205
|
}
|
|
196
206
|
|
|
197
|
-
export class
|
|
207
|
+
export class GenericMember extends HyperedgeMember {
|
|
198
208
|
constructor(data?: {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
209
|
+
ref?: ConceptRef;
|
|
210
|
+
presence?: PartitivePresence;
|
|
211
|
+
count?: PartitiveCount;
|
|
212
|
+
delimitingCharacteristic?: Record<string, string> | string;
|
|
203
213
|
});
|
|
214
|
+
readonly delimitingCharacteristic: Record<string, string>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export class AbstractHyperedge extends GlossaristModel {
|
|
204
218
|
readonly comprehensive: ConceptRef;
|
|
205
|
-
readonly
|
|
219
|
+
readonly members: HyperedgeMember[];
|
|
206
220
|
readonly completeness: Completeness;
|
|
207
221
|
readonly criterion: Record<string, string> | null;
|
|
222
|
+
readonly sources: ConceptSource[];
|
|
223
|
+
readonly notes: Record<string, string> | null;
|
|
224
|
+
readonly status: string | null;
|
|
208
225
|
readonly isComplete: boolean;
|
|
209
226
|
readonly isPartial: boolean;
|
|
210
227
|
readonly isCoordinate: boolean;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
static
|
|
228
|
+
toJSON(): Record<string, unknown>;
|
|
229
|
+
static fromJSON(data: Record<string, unknown>): AbstractHyperedge;
|
|
230
|
+
static identityOf(value: unknown): string;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export class PartitiveHyperedge extends AbstractHyperedge {
|
|
234
|
+
get partitives(): HyperedgeMember[];
|
|
235
|
+
static fromJSON(data: Record<string, unknown>): PartitiveHyperedge;
|
|
236
|
+
static identityOf(value: unknown): string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export class GenericHyperedge extends AbstractHyperedge {
|
|
240
|
+
static fromJSON(data: Record<string, unknown>): GenericHyperedge;
|
|
220
241
|
static identityOf(value: unknown): string;
|
|
221
242
|
}
|
|
243
|
+
|
|
244
|
+
// Legacy aliases (deprecated — use Hyperedge-based names)
|
|
245
|
+
export class PartitiveRelation extends PartitiveHyperedge {}
|
|
222
246
|
}
|
|
223
247
|
|
|
224
248
|
// Re-declare the same exports under the 'glossarist/models' subpath so
|
|
@@ -261,9 +285,9 @@ declare module 'glossarist/models' {
|
|
|
261
285
|
// (TypeSharedPlurality removed — superseded by presence/count on
|
|
262
286
|
// PartitiveMember per MECE refactor. Was dead code.)
|
|
263
287
|
|
|
264
|
-
// ──
|
|
288
|
+
// ── Hyperedge classes (glossarist 0.4.33 — unified n-ary model) ─
|
|
265
289
|
|
|
266
|
-
export class
|
|
290
|
+
export class HyperedgeMember extends GlossaristModel {
|
|
267
291
|
constructor(data?: {
|
|
268
292
|
ref?: ConceptRef;
|
|
269
293
|
presence?: PartitivePresence;
|
|
@@ -274,37 +298,44 @@ declare module 'glossarist/models' {
|
|
|
274
298
|
readonly presence: PartitivePresence;
|
|
275
299
|
readonly count: PartitiveCount;
|
|
276
300
|
readonly is_delimiting: boolean;
|
|
277
|
-
|
|
278
|
-
|
|
301
|
+
required(): boolean;
|
|
302
|
+
optional(): boolean;
|
|
279
303
|
get isDelimiting(): boolean;
|
|
280
|
-
toJSON():
|
|
281
|
-
static fromJSON(data: Record<string, unknown>):
|
|
304
|
+
toJSON(): Record<string, unknown>;
|
|
305
|
+
static fromJSON(data: Record<string, unknown>): HyperedgeMember;
|
|
282
306
|
static identityOf(value: unknown): string;
|
|
283
307
|
}
|
|
284
308
|
|
|
285
|
-
export class
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
completeness?: Completeness;
|
|
290
|
-
criterion?: Record<string, string> | string;
|
|
291
|
-
});
|
|
309
|
+
export class PartitiveMember extends HyperedgeMember {}
|
|
310
|
+
export class GenericMember extends HyperedgeMember {}
|
|
311
|
+
|
|
312
|
+
export class AbstractHyperedge extends GlossaristModel {
|
|
292
313
|
readonly comprehensive: ConceptRef;
|
|
293
|
-
readonly
|
|
314
|
+
readonly members: HyperedgeMember[];
|
|
294
315
|
readonly completeness: Completeness;
|
|
295
316
|
readonly criterion: Record<string, string> | null;
|
|
317
|
+
readonly sources: ConceptSource[];
|
|
318
|
+
readonly notes: Record<string, string> | null;
|
|
319
|
+
readonly status: string | null;
|
|
296
320
|
readonly isComplete: boolean;
|
|
297
321
|
readonly isPartial: boolean;
|
|
298
322
|
readonly isCoordinate: boolean;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
static fromJSON(data: Record<string, unknown>): PartitiveRelation;
|
|
323
|
+
toJSON(): Record<string, unknown>;
|
|
324
|
+
static fromJSON(data: Record<string, unknown>): AbstractHyperedge;
|
|
325
|
+
static identityOf(value: unknown): string;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export class PartitiveHyperedge extends AbstractHyperedge {
|
|
329
|
+
get partitives(): HyperedgeMember[];
|
|
330
|
+
static fromJSON(data: Record<string, unknown>): PartitiveHyperedge;
|
|
308
331
|
static identityOf(value: unknown): string;
|
|
309
332
|
}
|
|
333
|
+
|
|
334
|
+
export class GenericHyperedge extends AbstractHyperedge {
|
|
335
|
+
static fromJSON(data: Record<string, unknown>): GenericHyperedge;
|
|
336
|
+
static identityOf(value: unknown): string;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Legacy aliases (deprecated)
|
|
340
|
+
export class PartitiveRelation extends PartitiveHyperedge {}
|
|
310
341
|
}
|
package/src/adapters/types.ts
CHANGED
|
@@ -179,6 +179,33 @@ export interface PartitiveMemberWire {
|
|
|
179
179
|
isDelimiting: boolean;
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/**
|
|
183
|
+
* GenericRelationWire — ISO 704 / 1087-1 / 12620 generic (genus/
|
|
184
|
+
* species) decomposition, mirror of PartitiveRelationWire.
|
|
185
|
+
*
|
|
186
|
+
* The OIML V 2-200:2010 dataset has ~8 generic hyperedges with
|
|
187
|
+
* multiple criterion groups per comprehensive; this shape supports
|
|
188
|
+
* that natively. See docs/design/generic-relation.md (concept-model).
|
|
189
|
+
*/
|
|
190
|
+
export interface GenericRelationWire {
|
|
191
|
+
source: string;
|
|
192
|
+
comprehensive: string;
|
|
193
|
+
members: GenericMemberWire[];
|
|
194
|
+
completeness: 'complete' | 'partial';
|
|
195
|
+
criterion?: Record<string, string>;
|
|
196
|
+
register: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface GenericMemberWire {
|
|
200
|
+
uri: string;
|
|
201
|
+
presence: 'required' | 'optional';
|
|
202
|
+
count: 'exactly_one' | 'at_least_one' | 'multiple';
|
|
203
|
+
/** ISO 704:2022 §5.5.4.2.1 — the delimiting characteristic that
|
|
204
|
+
* distinguishes this specific concept from its coordinate concepts.
|
|
205
|
+
* Required on every GenericMember. LocalizedString shape. */
|
|
206
|
+
delimitingCharacteristic: Record<string, string>;
|
|
207
|
+
}
|
|
208
|
+
|
|
182
209
|
export interface GraphNode {
|
|
183
210
|
uri: string;
|
|
184
211
|
register: string;
|
|
@@ -37,6 +37,7 @@ export const GL = {
|
|
|
37
37
|
LOCALIZED_CONCEPT: 'gl:localizedConcept',
|
|
38
38
|
RELATED: 'gl:related',
|
|
39
39
|
PARTITIVE_RELATIONS: 'gl:partitiveRelations',
|
|
40
|
+
GENERIC_RELATIONS: 'gl:genericRelations',
|
|
40
41
|
TAGS: 'gl:tags',
|
|
41
42
|
|
|
42
43
|
// LocalizedConcept
|
|
@@ -133,12 +134,15 @@ export const GL = {
|
|
|
133
134
|
PRESENCE: 'gl:presence',
|
|
134
135
|
COUNT: 'gl:count',
|
|
135
136
|
IS_DELIMITING: 'gl:isDelimiting',
|
|
137
|
+
DELIMITING_CHARACTERISTIC: 'gl:delimitingCharacteristic',
|
|
136
138
|
MULTIPLICITY: 'gl:multiplicity',
|
|
137
139
|
CERTAINTY: 'gl:certainty',
|
|
138
140
|
|
|
139
141
|
// PartitiveRelation
|
|
140
142
|
COMPREHENSIVE: 'gl:comprehensive',
|
|
141
143
|
HAS_PARTITIVE: 'gl:hasPartitive',
|
|
144
|
+
HAS_GENERIC: 'gl:hasGeneric',
|
|
145
|
+
HAS_MEMBER: 'gl:members',
|
|
142
146
|
COMPLETENESS: 'gl:completeness',
|
|
143
147
|
CRITERION: 'gl:criterion',
|
|
144
148
|
|
|
@@ -28,6 +28,7 @@ import ConceptEditionRail from './ConceptEditionRail.vue';
|
|
|
28
28
|
import NonVerbalRepDisplay from './NonVerbalRepDisplay.vue';
|
|
29
29
|
import NonVerbalList from './non-verbal/NonVerbalList.vue';
|
|
30
30
|
import PartitiveRelationList from './PartitiveRelationList.vue';
|
|
31
|
+
import GenericRelationList from './GenericRelationList.vue';
|
|
31
32
|
import ConceptDiffModal from './ConceptDiffModal.vue';
|
|
32
33
|
import CitationDisplay from './CitationDisplay.vue';
|
|
33
34
|
import DesignationList from './DesignationList.vue';
|
|
@@ -75,6 +76,7 @@ const {
|
|
|
75
76
|
inverseEdgeType,
|
|
76
77
|
conceptRelated,
|
|
77
78
|
conceptPartitiveRelations,
|
|
79
|
+
conceptGenericRelations,
|
|
78
80
|
getResolvedRef,
|
|
79
81
|
relatedLabel,
|
|
80
82
|
navigateEdge,
|
|
@@ -577,6 +579,14 @@ const nonVerbalReps = computed(() => {
|
|
|
577
579
|
:register-id="registerId"
|
|
578
580
|
/>
|
|
579
581
|
|
|
582
|
+
<!-- Generic relations — genus/species decompositions (ISO 704) -->
|
|
583
|
+
<GenericRelationList
|
|
584
|
+
v-if="conceptGenericRelations.length"
|
|
585
|
+
:relations="conceptGenericRelations"
|
|
586
|
+
:manifest="manifest"
|
|
587
|
+
:register-id="registerId"
|
|
588
|
+
/>
|
|
589
|
+
|
|
580
590
|
<!-- Edition series — supersession chain across vocabulary editions -->
|
|
581
591
|
<ConceptEditionRail
|
|
582
592
|
:concept-uri="conceptUri(props.concept, props.registerId, props.manifest.uriBase)"
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* GenericRelationList — renders one-to-many generic (genus/species)
|
|
4
|
+
* decompositions as ISO 704 rake diagrams. Mirror of
|
|
5
|
+
* PartitiveRelationList with type label swapped.
|
|
6
|
+
*
|
|
7
|
+
* Each relation card shows:
|
|
8
|
+
* - completeness badge (Complete / Partial)
|
|
9
|
+
* - the comprehensive (genus) designation as the card title
|
|
10
|
+
* - optional criterion as italic text
|
|
11
|
+
* - the rake diagram itself (PartitiveRelationDiagram, reused —
|
|
12
|
+
* the line notation is identical for generic rakes)
|
|
13
|
+
*
|
|
14
|
+
* OIML V 2-200:2010 has ~8 generic hyperedges; 5.1 measurement
|
|
15
|
+
* standard alone has 6 criterion groups. This component is the
|
|
16
|
+
* primary rendering surface for those datasets.
|
|
17
|
+
*/
|
|
18
|
+
import type { GenericRelationWire, Manifest } from '../adapters/types';
|
|
19
|
+
import { useRouter } from 'vue-router';
|
|
20
|
+
import { useVocabularyStore } from '../stores/vocabulary';
|
|
21
|
+
import { getFactory } from '../adapters/factory';
|
|
22
|
+
import { useI18n, locale } from '../i18n';
|
|
23
|
+
import { completenessLabel } from '../utils/partitive-relation-styling';
|
|
24
|
+
import { presenceLabel, countLabel } from '../utils/partitive-multiplicity';
|
|
25
|
+
import { resolveDesignation } from '../utils/resolve-designation';
|
|
26
|
+
import PartitiveRelationDiagram, {
|
|
27
|
+
type PartitiveMemberLabeled,
|
|
28
|
+
} from './PartitiveRelationDiagram.vue';
|
|
29
|
+
|
|
30
|
+
const props = defineProps<{
|
|
31
|
+
relations: GenericRelationWire[];
|
|
32
|
+
manifest: Manifest;
|
|
33
|
+
registerId: string;
|
|
34
|
+
}>();
|
|
35
|
+
|
|
36
|
+
const router = useRouter();
|
|
37
|
+
const store = useVocabularyStore();
|
|
38
|
+
const factory = getFactory();
|
|
39
|
+
const { t } = useI18n();
|
|
40
|
+
|
|
41
|
+
function designationFor(uri: string): string {
|
|
42
|
+
return resolveDesignation(uri, store, factory, locale.value);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Resolve a LocalizedString delimitingCharacteristic to the current locale. */
|
|
46
|
+
function characteristicText(dc: Record<string, string>): string {
|
|
47
|
+
return dc[locale.value] ?? dc.default ?? dc.eng ?? Object.values(dc)[0] ?? '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function memberLabel(member: GenericRelationWire['members'][number]): PartitiveMemberLabeled {
|
|
51
|
+
return {
|
|
52
|
+
uri: member.uri,
|
|
53
|
+
presence: member.presence,
|
|
54
|
+
count: member.count,
|
|
55
|
+
isDelimiting: false,
|
|
56
|
+
label: designationFor(member.uri),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
</script>
|
|
60
|
+
|
|
61
|
+
<template>
|
|
62
|
+
<section class="generic-relations" aria-label="Generic relations">
|
|
63
|
+
<div
|
|
64
|
+
v-for="(rel, i) in relations"
|
|
65
|
+
:key="`generic-${i}-${rel.comprehensive}`"
|
|
66
|
+
class="generic-relation-card"
|
|
67
|
+
>
|
|
68
|
+
<header class="relation-header">
|
|
69
|
+
<span class="badge badge-completeness" :class="`completeness-${rel.completeness}`">
|
|
70
|
+
{{ completenessLabel(rel.completeness) }}
|
|
71
|
+
</span>
|
|
72
|
+
<span class="badge badge-type">{{ t('relations.generic') }}</span>
|
|
73
|
+
<h4 class="comprehensive">{{ designationFor(rel.comprehensive) }}</h4>
|
|
74
|
+
</header>
|
|
75
|
+
|
|
76
|
+
<p v-if="rel.criterion" class="criterion">
|
|
77
|
+
<em>{{ rel.criterion[locale] || rel.criterion.eng || Object.values(rel.criterion)[0] }}</em>
|
|
78
|
+
</p>
|
|
79
|
+
|
|
80
|
+
<PartitiveRelationDiagram
|
|
81
|
+
:partitives="rel.members.map(memberLabel)"
|
|
82
|
+
:comprehensive-label="designationFor(rel.comprehensive)"
|
|
83
|
+
:completeness="rel.completeness"
|
|
84
|
+
/>
|
|
85
|
+
|
|
86
|
+
<ul class="member-presence-list" aria-label="Member delimiting characteristics">
|
|
87
|
+
<li v-for="(m, j) in rel.members" :key="j">
|
|
88
|
+
<span class="member-label">{{ designationFor(m.uri) }}</span>
|
|
89
|
+
<span
|
|
90
|
+
v-if="m.delimitingCharacteristic"
|
|
91
|
+
class="member-delimiting-characteristic"
|
|
92
|
+
>{{ characteristicText(m.delimitingCharacteristic) }}</span>
|
|
93
|
+
<span v-if="m.presence && m.presence !== 'required'" class="member-presence">{{ presenceLabel(m.presence) }}</span>
|
|
94
|
+
<span v-if="m.count && m.count !== 'exactly_one'" class="member-count">{{ countLabel(m.count) }}</span>
|
|
95
|
+
</li>
|
|
96
|
+
</ul>
|
|
97
|
+
</div>
|
|
98
|
+
</section>
|
|
99
|
+
</template>
|
|
100
|
+
|
|
101
|
+
<style scoped>
|
|
102
|
+
.generic-relation-card {
|
|
103
|
+
border: 1px solid var(--color-border, #ccc);
|
|
104
|
+
border-radius: 6px;
|
|
105
|
+
padding: 1rem;
|
|
106
|
+
margin-bottom: 1rem;
|
|
107
|
+
background: var(--color-surface, #fff);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
.relation-header {
|
|
111
|
+
display: flex;
|
|
112
|
+
align-items: center;
|
|
113
|
+
gap: 0.5rem;
|
|
114
|
+
flex-wrap: wrap;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.badge {
|
|
118
|
+
font-size: 0.75rem;
|
|
119
|
+
font-weight: 600;
|
|
120
|
+
padding: 0.15rem 0.5rem;
|
|
121
|
+
border-radius: 4px;
|
|
122
|
+
text-transform: uppercase;
|
|
123
|
+
letter-spacing: 0.05em;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.badge-completeness.completeness-complete {
|
|
127
|
+
background: var(--color-success-soft, #d4edda);
|
|
128
|
+
color: var(--color-success-fg, #155724);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.badge-completeness.completeness-partial {
|
|
132
|
+
background: var(--color-warn-soft, #fff3cd);
|
|
133
|
+
color: var(--color-warn-fg, #856404);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.badge-type {
|
|
137
|
+
background: var(--color-accent-soft, #d1ecf1);
|
|
138
|
+
color: var(--color-accent-fg, #0c5460);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.comprehensive {
|
|
142
|
+
margin: 0;
|
|
143
|
+
font-size: 1.1rem;
|
|
144
|
+
flex: 1 1 auto;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.criterion {
|
|
148
|
+
color: var(--color-text-muted, #6c757d);
|
|
149
|
+
margin: 0.5rem 0;
|
|
150
|
+
font-size: 0.95rem;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
.member-presence-list {
|
|
154
|
+
list-style: none;
|
|
155
|
+
padding: 0;
|
|
156
|
+
margin: 0.5rem 0 0;
|
|
157
|
+
font-size: 0.85rem;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.member-presence-list li {
|
|
161
|
+
display: flex;
|
|
162
|
+
gap: 0.5rem;
|
|
163
|
+
align-items: center;
|
|
164
|
+
padding: 0.15rem 0;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
.member-presence-list .member-presence,
|
|
168
|
+
.member-presence-list .member-count,
|
|
169
|
+
.member-presence-list .member-delimiting {
|
|
170
|
+
color: var(--color-text-muted, #6c757d);
|
|
171
|
+
font-size: 0.75rem;
|
|
172
|
+
border: 1px solid var(--color-border-light, #eee);
|
|
173
|
+
padding: 0.05rem 0.4rem;
|
|
174
|
+
border-radius: 3px;
|
|
175
|
+
}
|
|
176
|
+
</style>
|
|
@@ -20,6 +20,7 @@ import { getFactory } from '../adapters/factory';
|
|
|
20
20
|
import { useI18n, locale } from '../i18n';
|
|
21
21
|
import { completenessLabel } from '../utils/partitive-relation-styling';
|
|
22
22
|
import { presenceLabel, countLabel } from '../utils/partitive-multiplicity';
|
|
23
|
+
import { resolveDesignation } from '../utils/resolve-designation';
|
|
23
24
|
import PartitiveRelationDiagram, {
|
|
24
25
|
type PartitiveMemberLabeled,
|
|
25
26
|
} from './PartitiveRelationDiagram.vue';
|
|
@@ -36,25 +37,7 @@ const factory = getFactory();
|
|
|
36
37
|
const { t } = useI18n();
|
|
37
38
|
|
|
38
39
|
function designationFor(uri: string): string {
|
|
39
|
-
|
|
40
|
-
if (node) {
|
|
41
|
-
const des = node.designations[locale.value]
|
|
42
|
-
|| node.designations.eng
|
|
43
|
-
|| Object.values(node.designations)[0];
|
|
44
|
-
if (des) return des;
|
|
45
|
-
}
|
|
46
|
-
const resolution = factory.resolve(uri);
|
|
47
|
-
if (resolution.type === 'internal') {
|
|
48
|
-
const adapter = store.datasets.get(resolution.registerId);
|
|
49
|
-
const entry = adapter?.getIndexEntry(resolution.conceptId);
|
|
50
|
-
if (entry) {
|
|
51
|
-
const des = entry.designations[locale.value]
|
|
52
|
-
|| entry.designations.eng
|
|
53
|
-
|| Object.values(entry.designations)[0];
|
|
54
|
-
if (des) return des;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
return resolution.type === 'internal' ? resolution.conceptId : uri;
|
|
40
|
+
return resolveDesignation(uri, store, factory, locale.value);
|
|
58
41
|
}
|
|
59
42
|
|
|
60
43
|
function navigate(uri: string) {
|