@glossarist/concept-browser 0.7.91 → 0.7.92
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__/composables/use-concept-edges-v2.test.ts +19 -20
- 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/adapters/model-bridge/concept.ts +12 -2
- package/src/adapters/model-bridge/hyperedge.ts +94 -0
- package/src/adapters/model-bridge/jsonld-types.ts +3 -0
- package/src/adapters/non-verbal/glossarist-augment.d.ts +60 -44
- package/src/adapters/types.ts +24 -0
- package/src/adapters/wire-keys.ts +3 -0
- package/src/components/ConceptDetail.vue +10 -0
- package/src/components/GenericRelationList.vue +168 -0
- package/src/components/PartitiveRelationList.vue +2 -19
- package/src/composables/use-concept-edges.ts +49 -51
- package/src/utils/resolve-designation.ts +50 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.92",
|
|
4
4
|
"description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"astro": "^7.0.7",
|
|
38
38
|
"d3": "^7.9.0",
|
|
39
39
|
"favicons": "^7.2.0",
|
|
40
|
-
"glossarist": "^0.4.
|
|
40
|
+
"glossarist": "^0.4.33",
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"jszip": "^3.10.1",
|
|
43
43
|
"pinia": "^2.3.1",
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GenericRelationList mount test — pins the wire-type contract so
|
|
3
|
+
* the 5 type bugs found in audit (entry.titles, member.ref,
|
|
4
|
+
* is_delimiting casing, comprehensive.source, missing props)
|
|
5
|
+
* can't regress.
|
|
6
|
+
*/
|
|
7
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
8
|
+
import { mount } from '@vue/test-utils';
|
|
9
|
+
import GenericRelationList from '../../components/GenericRelationList.vue';
|
|
10
|
+
import type { GenericRelationWire } from '../../adapters/types';
|
|
11
|
+
|
|
12
|
+
// Stub the composables the component depends on
|
|
13
|
+
vi.mock('../../stores/vocabulary', () => ({
|
|
14
|
+
useVocabularyStore: () => ({
|
|
15
|
+
graph: { getNode: () => null },
|
|
16
|
+
datasets: new Map(),
|
|
17
|
+
}),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock('../../adapters/factory', () => ({
|
|
21
|
+
getFactory: () => ({
|
|
22
|
+
resolve: (uri: string) => ({ type: 'unresolved' as const, uri }),
|
|
23
|
+
}),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
vi.mock('../../i18n', () => ({
|
|
27
|
+
useI18n: () => ({ t: (key: string) => key }),
|
|
28
|
+
locale: { value: 'eng' },
|
|
29
|
+
}));
|
|
30
|
+
|
|
31
|
+
vi.mock('vue-router', () => ({
|
|
32
|
+
useRouter: () => ({ push: vi.fn() }),
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
const manifest = {
|
|
36
|
+
id: 'test',
|
|
37
|
+
datasetUri: 'https://example.org/test',
|
|
38
|
+
title: 'Test',
|
|
39
|
+
description: '',
|
|
40
|
+
owner: '',
|
|
41
|
+
baseUrl: '',
|
|
42
|
+
languages: ['eng'],
|
|
43
|
+
conceptCount: 0,
|
|
44
|
+
conceptUrlTemplate: '',
|
|
45
|
+
indexUrl: '',
|
|
46
|
+
contextUrl: '',
|
|
47
|
+
uriBase: 'https://example.org',
|
|
48
|
+
status: 'valid',
|
|
49
|
+
schemaVersion: '3',
|
|
50
|
+
tags: [],
|
|
51
|
+
lastUpdated: '',
|
|
52
|
+
sourceRepo: '',
|
|
53
|
+
chunkSize: 100,
|
|
54
|
+
} as any;
|
|
55
|
+
|
|
56
|
+
function makeRelation(overrides: Partial<GenericRelationWire> = {}): GenericRelationWire {
|
|
57
|
+
return {
|
|
58
|
+
source: 'https://example.org/test/concept/5-1',
|
|
59
|
+
comprehensive: 'https://example.org/test/concept/5-1',
|
|
60
|
+
members: [
|
|
61
|
+
{ uri: 'https://example.org/test/concept/5-4', presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
62
|
+
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
63
|
+
],
|
|
64
|
+
completeness: 'complete',
|
|
65
|
+
register: 'test',
|
|
66
|
+
...overrides,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('GenericRelationList — wire-type contract mount test', () => {
|
|
71
|
+
it('mounts without errors for a well-formed relation', () => {
|
|
72
|
+
const w = mount(GenericRelationList, {
|
|
73
|
+
props: {
|
|
74
|
+
relations: [makeRelation()],
|
|
75
|
+
manifest,
|
|
76
|
+
registerId: 'test',
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
expect(w.html()).toContain('generic-relation-card');
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('renders completeness badge', () => {
|
|
83
|
+
const w = mount(GenericRelationList, {
|
|
84
|
+
props: {
|
|
85
|
+
relations: [makeRelation({ completeness: 'partial' })],
|
|
86
|
+
manifest,
|
|
87
|
+
registerId: 'test',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
expect(w.html()).toContain('completeness-partial');
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('renders multiple relations', () => {
|
|
94
|
+
const w = mount(GenericRelationList, {
|
|
95
|
+
props: {
|
|
96
|
+
relations: [
|
|
97
|
+
makeRelation({ comprehensive: 'https://example.org/test/concept/5-1' }),
|
|
98
|
+
makeRelation({ comprehensive: 'https://example.org/test/concept/5-2' }),
|
|
99
|
+
],
|
|
100
|
+
manifest,
|
|
101
|
+
registerId: 'test',
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(w.findAll('.generic-relation-card')).toHaveLength(2);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('renders criterion when present', () => {
|
|
108
|
+
const w = mount(GenericRelationList, {
|
|
109
|
+
props: {
|
|
110
|
+
relations: [makeRelation({ criterion: { eng: 'by realization medium' } })],
|
|
111
|
+
manifest,
|
|
112
|
+
registerId: 'test',
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
expect(w.html()).toContain('by realization medium');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('renders member presence/count badges for non-default values', () => {
|
|
119
|
+
const w = mount(GenericRelationList, {
|
|
120
|
+
props: {
|
|
121
|
+
relations: [makeRelation({
|
|
122
|
+
members: [
|
|
123
|
+
{ uri: 'https://example.org/test/concept/5-4', presence: 'optional', count: 'multiple', isDelimiting: true },
|
|
124
|
+
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one', isDelimiting: false },
|
|
125
|
+
],
|
|
126
|
+
})],
|
|
127
|
+
manifest,
|
|
128
|
+
registerId: 'test',
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
const html = w.html();
|
|
132
|
+
expect(html).toContain('Optional');
|
|
133
|
+
expect(html).toContain('Multiple');
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('renders empty section when relations array is empty', () => {
|
|
137
|
+
const w = mount(GenericRelationList, {
|
|
138
|
+
props: {
|
|
139
|
+
relations: [],
|
|
140
|
+
manifest,
|
|
141
|
+
registerId: 'test',
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
expect(w.find('section.generic-relations').exists()).toBe(true);
|
|
145
|
+
expect(w.findAll('.generic-relation-card')).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
// glossarist
|
|
3
|
-
//
|
|
4
|
-
// (extended by our local augmentation) so the types are correct.
|
|
2
|
+
// glossarist 0.4.33 renamed PartitiveRelation → PartitiveHyperedge.
|
|
3
|
+
// Import from glossarist/models (extended by our local augmentation).
|
|
5
4
|
import type {
|
|
6
|
-
|
|
5
|
+
PartitiveHyperedge,
|
|
7
6
|
PartitiveMember,
|
|
8
7
|
ConceptRef,
|
|
9
8
|
} from 'glossarist/models';
|
|
10
9
|
import {
|
|
11
|
-
|
|
10
|
+
PartitiveHyperedge as PartitiveHyperedgeModel,
|
|
12
11
|
PartitiveMember as PartitiveMemberModel,
|
|
13
12
|
ConceptRef as ConceptRefModel,
|
|
14
13
|
} from 'glossarist/models';
|
|
@@ -48,8 +47,8 @@ function makeRelation(opts: {
|
|
|
48
47
|
partitives: PartitiveMember[];
|
|
49
48
|
completeness?: 'complete' | 'partial';
|
|
50
49
|
criterion?: Record<string, string>;
|
|
51
|
-
}):
|
|
52
|
-
return
|
|
50
|
+
}): PartitiveHyperedge {
|
|
51
|
+
return PartitiveHyperedgeModel.fromJSON(opts as unknown as Record<string, unknown>) as PartitiveHyperedge;
|
|
53
52
|
}
|
|
54
53
|
|
|
55
54
|
describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () => {
|
|
@@ -57,8 +56,8 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
57
56
|
it('presence defaults to required, count defaults to exactly_one', () => {
|
|
58
57
|
const m = makePartitive('test', '1.1');
|
|
59
58
|
expect(m.presence).toBe('required');
|
|
60
|
-
expect(m.
|
|
61
|
-
expect(m.
|
|
59
|
+
expect(m.presence === "required").toBe(true);
|
|
60
|
+
expect(m.presence === "optional").toBe(false);
|
|
62
61
|
});
|
|
63
62
|
|
|
64
63
|
it('count defaults to exactly_one', () => {
|
|
@@ -69,8 +68,8 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
69
68
|
it('presence=optional is preserved', () => {
|
|
70
69
|
const m = makePartitive('test', '1.1', { presence: 'optional' });
|
|
71
70
|
expect(m.presence).toBe('optional');
|
|
72
|
-
expect(m.
|
|
73
|
-
expect(m.
|
|
71
|
+
expect(m.presence === "optional").toBe(true);
|
|
72
|
+
expect(m.presence === "required").toBe(false);
|
|
74
73
|
});
|
|
75
74
|
|
|
76
75
|
it('count=multiple is preserved', () => {
|
|
@@ -86,13 +85,13 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
86
85
|
it('is_delimiting defaults to false', () => {
|
|
87
86
|
const m = makePartitive('test', '1.1');
|
|
88
87
|
expect(m.is_delimiting).toBe(false);
|
|
89
|
-
expect(m.
|
|
88
|
+
expect(m.is_delimiting).toBe(false);
|
|
90
89
|
});
|
|
91
90
|
|
|
92
91
|
it('is_delimiting is preserved when set to true', () => {
|
|
93
92
|
const m = makePartitive('test', '1.1', { isDelimiting: true });
|
|
94
93
|
expect(m.is_delimiting).toBe(true);
|
|
95
|
-
expect(m.
|
|
94
|
+
expect(m.is_delimiting).toBe(true);
|
|
96
95
|
});
|
|
97
96
|
|
|
98
97
|
it('rejects the vacuous (presence=optional, count=at_least_one)', () => {
|
|
@@ -108,10 +107,10 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
108
107
|
comprehensive: makeRef('test', '1.3'),
|
|
109
108
|
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
110
109
|
});
|
|
111
|
-
expect(r.
|
|
112
|
-
expect(r.completeness).toBe('complete');
|
|
113
|
-
expect(r.isComplete).toBe(true);
|
|
114
|
-
expect(r.isPartial).toBe(false);
|
|
110
|
+
expect((r as any).members).toHaveLength(2);
|
|
111
|
+
expect((r as any).completeness).toBe('complete');
|
|
112
|
+
expect((r as any).isComplete).toBe(true);
|
|
113
|
+
expect((r as any).isPartial).toBe(false);
|
|
115
114
|
});
|
|
116
115
|
|
|
117
116
|
it('completeness=partial is preserved', () => {
|
|
@@ -120,8 +119,8 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
120
119
|
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
121
120
|
completeness: 'partial',
|
|
122
121
|
});
|
|
123
|
-
expect(r.completeness).toBe('partial');
|
|
124
|
-
expect(r.isPartial).toBe(true);
|
|
122
|
+
expect((r as any).completeness).toBe('partial');
|
|
123
|
+
expect((r as any).isPartial).toBe(true);
|
|
125
124
|
});
|
|
126
125
|
|
|
127
126
|
it('isCoordinate true when ≥2 partitives', () => {
|
|
@@ -129,7 +128,7 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
129
128
|
comprehensive: makeRef('test', '1.3'),
|
|
130
129
|
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
131
130
|
});
|
|
132
|
-
expect(r.isCoordinate).toBe(true);
|
|
131
|
+
expect((r as any).isCoordinate).toBe(true);
|
|
133
132
|
});
|
|
134
133
|
|
|
135
134
|
it('rejects < 2 partitives (ISO 704 requirement)', () => {
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
// glossarist 0.4.33 renamed PartitiveRelation → PartitiveHyperedge.
|
|
3
|
+
// Import from glossarist/models (extended by our local augmentation).
|
|
4
|
+
import type {
|
|
5
|
+
PartitiveHyperedge,
|
|
6
|
+
PartitiveMember,
|
|
7
|
+
ConceptRef,
|
|
8
|
+
} from 'glossarist/models';
|
|
9
|
+
import {
|
|
10
|
+
PartitiveHyperedge as PartitiveHyperedgeModel,
|
|
11
|
+
PartitiveMember as PartitiveMemberModel,
|
|
12
|
+
ConceptRef as ConceptRefModel,
|
|
13
|
+
} from 'glossarist/models';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Verifies glossarist-js 0.4.26's MECE-native PartitiveRelation model
|
|
17
|
+
* (ISO 704:2022 presence × count + is_delimiting).
|
|
18
|
+
*
|
|
19
|
+
* The concept-browser consumes these fields directly (no migration
|
|
20
|
+
* bridge). These specs pin the runtime contract so a regression in
|
|
21
|
+
* upstream is caught here.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
function makeRef(source: string, id: string): ConceptRef {
|
|
25
|
+
return ConceptRefModel.fromJSON({ source, id }) as ConceptRef;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function makePartitive(
|
|
29
|
+
source: string,
|
|
30
|
+
id: string,
|
|
31
|
+
fields: { presence?: 'required' | 'optional'; count?: 'exactly_one' | 'at_least_one' | 'multiple'; isDelimiting?: boolean } = {},
|
|
32
|
+
): PartitiveMember {
|
|
33
|
+
const data: {
|
|
34
|
+
ref: ConceptRef;
|
|
35
|
+
presence?: 'required' | 'optional';
|
|
36
|
+
count?: 'exactly_one' | 'at_least_one' | 'multiple';
|
|
37
|
+
is_delimiting?: boolean;
|
|
38
|
+
} = { ref: makeRef(source, id) };
|
|
39
|
+
if (fields.presence) data.presence = fields.presence;
|
|
40
|
+
if (fields.count) data.count = fields.count;
|
|
41
|
+
if (fields.isDelimiting) data.is_delimiting = fields.isDelimiting;
|
|
42
|
+
return PartitiveMemberModel.fromJSON(data as unknown as Record<string, unknown>) as PartitiveMember;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function makeRelation(opts: {
|
|
46
|
+
comprehensive: ConceptRef;
|
|
47
|
+
partitives: PartitiveMember[];
|
|
48
|
+
completeness?: 'complete' | 'partial';
|
|
49
|
+
criterion?: Record<string, string>;
|
|
50
|
+
}): PartitiveHyperedge {
|
|
51
|
+
return PartitiveHyperedgeModel.fromJSON(opts as unknown as Record<string, unknown>) as PartitiveHyperedge;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () => {
|
|
55
|
+
describe('PartitiveMember (ISO 704:2022 presence × count + delimiting)', () => {
|
|
56
|
+
it('presence defaults to required, count defaults to exactly_one', () => {
|
|
57
|
+
const m = makePartitive('test', '1.1');
|
|
58
|
+
expect(m.presence).toBe('required');
|
|
59
|
+
expect(m.presence === "required").toBe(true);
|
|
60
|
+
expect(m.presence === "optional").toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('count defaults to exactly_one', () => {
|
|
64
|
+
const m = makePartitive('test', '1.1');
|
|
65
|
+
expect(m.count).toBe('exactly_one');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('presence=optional is preserved', () => {
|
|
69
|
+
const m = makePartitive('test', '1.1', { presence: 'optional' });
|
|
70
|
+
expect(m.presence).toBe('optional');
|
|
71
|
+
expect(m.presence === "optional").toBe(true);
|
|
72
|
+
expect(m.presence === "required").toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('count=multiple is preserved', () => {
|
|
76
|
+
const m = makePartitive('test', '1.1', { count: 'multiple' });
|
|
77
|
+
expect(m.count).toBe('multiple');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('count=at_least_one is preserved', () => {
|
|
81
|
+
const m = makePartitive('test', '1.1', { count: 'at_least_one' });
|
|
82
|
+
expect(m.count).toBe('at_least_one');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('is_delimiting defaults to false', () => {
|
|
86
|
+
const m = makePartitive('test', '1.1');
|
|
87
|
+
expect(m.is_delimiting).toBe(false);
|
|
88
|
+
expect(m.is_delimiting).toBe(false);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('is_delimiting is preserved when set to true', () => {
|
|
92
|
+
const m = makePartitive('test', '1.1', { isDelimiting: true });
|
|
93
|
+
expect(m.is_delimiting).toBe(true);
|
|
94
|
+
expect(m.is_delimiting).toBe(true);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('rejects the vacuous (presence=optional, count=at_least_one)', () => {
|
|
98
|
+
expect(() =>
|
|
99
|
+
makePartitive('test', '1.1', { presence: 'optional', count: 'at_least_one' }),
|
|
100
|
+
).toThrow(/collapses to optional \+ multiple/);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('PartitiveRelation', () => {
|
|
105
|
+
it('accepts 2 members with default axes', () => {
|
|
106
|
+
const r = makeRelation({
|
|
107
|
+
comprehensive: makeRef('test', '1.3'),
|
|
108
|
+
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
109
|
+
});
|
|
110
|
+
expect(r.members).toHaveLength(2);
|
|
111
|
+
expect((r as any).completeness).toBe('complete');
|
|
112
|
+
expect((r as any).isComplete).toBe(true);
|
|
113
|
+
expect((r as any).isPartial).toBe(false);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('completeness=partial is preserved', () => {
|
|
117
|
+
const r = makeRelation({
|
|
118
|
+
comprehensive: makeRef('test', '1.3'),
|
|
119
|
+
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
120
|
+
completeness: 'partial',
|
|
121
|
+
});
|
|
122
|
+
expect((r as any).completeness).toBe('partial');
|
|
123
|
+
expect((r as any).isPartial).toBe(true);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('isCoordinate true when ≥2 partitives', () => {
|
|
127
|
+
const r = makeRelation({
|
|
128
|
+
comprehensive: makeRef('test', '1.3'),
|
|
129
|
+
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
130
|
+
});
|
|
131
|
+
expect((r as any).isCoordinate).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('rejects < 2 partitives (ISO 704 requirement)', () => {
|
|
135
|
+
expect(() => makeRelation({
|
|
136
|
+
comprehensive: makeRef('test', '1.3'),
|
|
137
|
+
partitives: [makePartitive('test', '1.4')],
|
|
138
|
+
})).toThrow(/≥2 partitives/);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('round-trips through toJSON with MECE fields', () => {
|
|
142
|
+
const r = makeRelation({
|
|
143
|
+
comprehensive: makeRef('test', '1.3'),
|
|
144
|
+
partitives: [
|
|
145
|
+
makePartitive('test', '1.4', { presence: 'required', count: 'multiple' }),
|
|
146
|
+
makePartitive('test', '1.5', { presence: 'optional', isDelimiting: true }),
|
|
147
|
+
],
|
|
148
|
+
completeness: 'partial',
|
|
149
|
+
criterion: { eng: 'physical structure' },
|
|
150
|
+
});
|
|
151
|
+
const json = r.toJSON() as {
|
|
152
|
+
comprehensive: unknown;
|
|
153
|
+
partitives: Array<{ presence?: string; count?: string; is_delimiting?: boolean; ref: unknown }>;
|
|
154
|
+
completeness: string;
|
|
155
|
+
criterion?: Record<string, string>;
|
|
156
|
+
};
|
|
157
|
+
expect(json.comprehensive).toBeDefined();
|
|
158
|
+
expect(json.partitives).toHaveLength(2);
|
|
159
|
+
expect(json.partitives[0].count).toBe('multiple');
|
|
160
|
+
expect(json.partitives[1].presence).toBe('optional');
|
|
161
|
+
expect(json.partitives[1].is_delimiting).toBe(true);
|
|
162
|
+
expect(json.completeness).toBe('partial');
|
|
163
|
+
expect(json.criterion).toEqual({ eng: 'physical structure' });
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
});
|
|
@@ -23,9 +23,9 @@ interface PartitiveMemberLike {
|
|
|
23
23
|
|
|
24
24
|
function loadPartitives(doc: Record<string, unknown>): PartitiveMemberLike[] {
|
|
25
25
|
const concept = conceptFromJson(doc as any);
|
|
26
|
-
const relations = (concept as any).
|
|
26
|
+
const relations = (concept as any).relations ?? [];
|
|
27
27
|
return relations.flatMap((r: any) =>
|
|
28
|
-
(r.partitives ?? []).map((m: any) => ({
|
|
28
|
+
((r.members ?? r.partitives) ?? []).map((m: any) => ({
|
|
29
29
|
presence: m.presence,
|
|
30
30
|
count: m.count,
|
|
31
31
|
is_delimiting: m.is_delimiting,
|
|
@@ -60,6 +60,6 @@ describe('GL wire-key SSOT', () => {
|
|
|
60
60
|
// The point of this test: when adding a key, you remember to update
|
|
61
61
|
// the count; when removing, you notice the test fails. Catches
|
|
62
62
|
// accidental drift in either direction.
|
|
63
|
-
expect(Object.keys(GL).length).toBe(
|
|
63
|
+
expect(Object.keys(GL).length).toBe(90);
|
|
64
64
|
});
|
|
65
65
|
});
|
|
@@ -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,94 @@
|
|
|
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
|
+
return out;
|
|
57
|
+
})
|
|
58
|
+
.filter((m): m is Record<string, unknown> => m !== null);
|
|
59
|
+
|
|
60
|
+
if (partitives.length < 2) return null;
|
|
61
|
+
|
|
62
|
+
const out: Record<string, unknown> = {
|
|
63
|
+
comprehensive,
|
|
64
|
+
members: partitives,
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (r[GL.COMPLETENESS]) out.completeness = r[GL.COMPLETENESS];
|
|
68
|
+
|
|
69
|
+
if (r[GL.CRITERION]) {
|
|
70
|
+
out.criterion = typeof r[GL.CRITERION] === 'string'
|
|
71
|
+
? { default: r[GL.CRITERION] }
|
|
72
|
+
: r[GL.CRITERION];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Migrate legacy v2 certainty → old v3 multiplicity string. */
|
|
79
|
+
function splitLegacyCertainty(certainty: string | undefined): string | null {
|
|
80
|
+
if (!certainty) return null;
|
|
81
|
+
return certainty === 'possible' ? 'optional' : 'compulsory';
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Split the legacy one-string multiplicity into MECE presence × count. */
|
|
85
|
+
function splitMultiplicity(m: string): { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' } {
|
|
86
|
+
const LEGACY_MAP: Record<string, { presence: 'required' | 'optional'; count: 'exactly_one' | 'at_least_one' | 'multiple' }> = {
|
|
87
|
+
compulsory: { presence: 'required', count: 'exactly_one' },
|
|
88
|
+
optional: { presence: 'optional', count: 'exactly_one' },
|
|
89
|
+
compulsory_multiple: { presence: 'required', count: 'multiple' },
|
|
90
|
+
optional_multiple: { presence: 'optional', count: 'multiple' },
|
|
91
|
+
compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
|
|
92
|
+
};
|
|
93
|
+
return LEGACY_MAP[m] ?? { presence: 'required', count: 'exactly_one' };
|
|
94
|
+
}
|
|
@@ -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,12 +170,14 @@ 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;
|
|
@@ -189,36 +191,43 @@ declare module 'glossarist' {
|
|
|
189
191
|
readonly isRequired: boolean;
|
|
190
192
|
readonly isOptional: boolean;
|
|
191
193
|
get isDelimiting(): boolean;
|
|
192
|
-
toJSON():
|
|
193
|
-
static fromJSON(data: Record<string, unknown>):
|
|
194
|
+
toJSON(): Record<string, unknown>;
|
|
195
|
+
static fromJSON(data: Record<string, unknown>): HyperedgeMember;
|
|
194
196
|
static identityOf(value: unknown): string;
|
|
195
197
|
}
|
|
196
198
|
|
|
197
|
-
export class
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
completeness?: Completeness;
|
|
202
|
-
criterion?: Record<string, string> | string;
|
|
203
|
-
});
|
|
199
|
+
export class PartitiveMember extends HyperedgeMember {}
|
|
200
|
+
export class GenericMember extends HyperedgeMember {}
|
|
201
|
+
|
|
202
|
+
export class AbstractHyperedge extends GlossaristModel {
|
|
204
203
|
readonly comprehensive: ConceptRef;
|
|
205
|
-
readonly
|
|
204
|
+
readonly members: HyperedgeMember[];
|
|
206
205
|
readonly completeness: Completeness;
|
|
207
206
|
readonly criterion: Record<string, string> | null;
|
|
207
|
+
readonly sources: ConceptSource[];
|
|
208
|
+
readonly notes: Record<string, string> | null;
|
|
209
|
+
readonly status: string | null;
|
|
208
210
|
readonly isComplete: boolean;
|
|
209
211
|
readonly isPartial: boolean;
|
|
210
212
|
readonly isCoordinate: boolean;
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
static fromJSON(data: Record<string, unknown>): PartitiveRelation;
|
|
213
|
+
toJSON(): Record<string, unknown>;
|
|
214
|
+
static fromJSON(data: Record<string, unknown>): AbstractHyperedge;
|
|
215
|
+
static identityOf(value: unknown): string;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export class PartitiveHyperedge extends AbstractHyperedge {
|
|
219
|
+
get partitives(): HyperedgeMember[];
|
|
220
|
+
static fromJSON(data: Record<string, unknown>): PartitiveHyperedge;
|
|
220
221
|
static identityOf(value: unknown): string;
|
|
221
222
|
}
|
|
223
|
+
|
|
224
|
+
export class GenericHyperedge extends AbstractHyperedge {
|
|
225
|
+
static fromJSON(data: Record<string, unknown>): GenericHyperedge;
|
|
226
|
+
static identityOf(value: unknown): string;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Legacy aliases (deprecated — use Hyperedge-based names)
|
|
230
|
+
export class PartitiveRelation extends PartitiveHyperedge {}
|
|
222
231
|
}
|
|
223
232
|
|
|
224
233
|
// Re-declare the same exports under the 'glossarist/models' subpath so
|
|
@@ -261,9 +270,9 @@ declare module 'glossarist/models' {
|
|
|
261
270
|
// (TypeSharedPlurality removed — superseded by presence/count on
|
|
262
271
|
// PartitiveMember per MECE refactor. Was dead code.)
|
|
263
272
|
|
|
264
|
-
// ──
|
|
273
|
+
// ── Hyperedge classes (glossarist 0.4.33 — unified n-ary model) ─
|
|
265
274
|
|
|
266
|
-
export class
|
|
275
|
+
export class HyperedgeMember extends GlossaristModel {
|
|
267
276
|
constructor(data?: {
|
|
268
277
|
ref?: ConceptRef;
|
|
269
278
|
presence?: PartitivePresence;
|
|
@@ -274,37 +283,44 @@ declare module 'glossarist/models' {
|
|
|
274
283
|
readonly presence: PartitivePresence;
|
|
275
284
|
readonly count: PartitiveCount;
|
|
276
285
|
readonly is_delimiting: boolean;
|
|
277
|
-
|
|
278
|
-
|
|
286
|
+
required(): boolean;
|
|
287
|
+
optional(): boolean;
|
|
279
288
|
get isDelimiting(): boolean;
|
|
280
|
-
toJSON():
|
|
281
|
-
static fromJSON(data: Record<string, unknown>):
|
|
289
|
+
toJSON(): Record<string, unknown>;
|
|
290
|
+
static fromJSON(data: Record<string, unknown>): HyperedgeMember;
|
|
282
291
|
static identityOf(value: unknown): string;
|
|
283
292
|
}
|
|
284
293
|
|
|
285
|
-
export class
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
completeness?: Completeness;
|
|
290
|
-
criterion?: Record<string, string> | string;
|
|
291
|
-
});
|
|
294
|
+
export class PartitiveMember extends HyperedgeMember {}
|
|
295
|
+
export class GenericMember extends HyperedgeMember {}
|
|
296
|
+
|
|
297
|
+
export class AbstractHyperedge extends GlossaristModel {
|
|
292
298
|
readonly comprehensive: ConceptRef;
|
|
293
|
-
readonly
|
|
299
|
+
readonly members: HyperedgeMember[];
|
|
294
300
|
readonly completeness: Completeness;
|
|
295
301
|
readonly criterion: Record<string, string> | null;
|
|
302
|
+
readonly sources: ConceptSource[];
|
|
303
|
+
readonly notes: Record<string, string> | null;
|
|
304
|
+
readonly status: string | null;
|
|
296
305
|
readonly isComplete: boolean;
|
|
297
306
|
readonly isPartial: boolean;
|
|
298
307
|
readonly isCoordinate: boolean;
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
static
|
|
308
|
+
toJSON(): Record<string, unknown>;
|
|
309
|
+
static fromJSON(data: Record<string, unknown>): AbstractHyperedge;
|
|
310
|
+
static identityOf(value: unknown): string;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export class PartitiveHyperedge extends AbstractHyperedge {
|
|
314
|
+
get partitives(): HyperedgeMember[];
|
|
315
|
+
static fromJSON(data: Record<string, unknown>): PartitiveHyperedge;
|
|
316
|
+
static identityOf(value: unknown): string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export class GenericHyperedge extends AbstractHyperedge {
|
|
320
|
+
static fromJSON(data: Record<string, unknown>): GenericHyperedge;
|
|
308
321
|
static identityOf(value: unknown): string;
|
|
309
322
|
}
|
|
323
|
+
|
|
324
|
+
// Legacy aliases (deprecated)
|
|
325
|
+
export class PartitiveRelation extends PartitiveHyperedge {}
|
|
310
326
|
}
|
package/src/adapters/types.ts
CHANGED
|
@@ -179,6 +179,30 @@ 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
|
+
isDelimiting: boolean;
|
|
204
|
+
}
|
|
205
|
+
|
|
182
206
|
export interface GraphNode {
|
|
183
207
|
uri: string;
|
|
184
208
|
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
|
|
@@ -139,6 +140,8 @@ export const GL = {
|
|
|
139
140
|
// PartitiveRelation
|
|
140
141
|
COMPREHENSIVE: 'gl:comprehensive',
|
|
141
142
|
HAS_PARTITIVE: 'gl:hasPartitive',
|
|
143
|
+
HAS_GENERIC: 'gl:hasGeneric',
|
|
144
|
+
HAS_MEMBER: 'gl:members',
|
|
142
145
|
COMPLETENESS: 'gl:completeness',
|
|
143
146
|
CRITERION: 'gl:criterion',
|
|
144
147
|
|
|
@@ -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,168 @@
|
|
|
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
|
+
function memberLabel(member: GenericRelationWire['members'][number]): PartitiveMemberLabeled {
|
|
46
|
+
return {
|
|
47
|
+
uri: member.uri,
|
|
48
|
+
presence: member.presence,
|
|
49
|
+
count: member.count,
|
|
50
|
+
isDelimiting: member.isDelimiting,
|
|
51
|
+
label: designationFor(member.uri),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
|
|
56
|
+
<template>
|
|
57
|
+
<section class="generic-relations" aria-label="Generic relations">
|
|
58
|
+
<div
|
|
59
|
+
v-for="(rel, i) in relations"
|
|
60
|
+
:key="`generic-${i}-${rel.comprehensive}`"
|
|
61
|
+
class="generic-relation-card"
|
|
62
|
+
>
|
|
63
|
+
<header class="relation-header">
|
|
64
|
+
<span class="badge badge-completeness" :class="`completeness-${rel.completeness}`">
|
|
65
|
+
{{ completenessLabel(rel.completeness) }}
|
|
66
|
+
</span>
|
|
67
|
+
<span class="badge badge-type">{{ t('relations.generic') }}</span>
|
|
68
|
+
<h4 class="comprehensive">{{ designationFor(rel.comprehensive) }}</h4>
|
|
69
|
+
</header>
|
|
70
|
+
|
|
71
|
+
<p v-if="rel.criterion" class="criterion">
|
|
72
|
+
<em>{{ rel.criterion[locale] || rel.criterion.eng || Object.values(rel.criterion)[0] }}</em>
|
|
73
|
+
</p>
|
|
74
|
+
|
|
75
|
+
<PartitiveRelationDiagram
|
|
76
|
+
:partitives="rel.members.map(memberLabel)"
|
|
77
|
+
:comprehensive-label="designationFor(rel.comprehensive)"
|
|
78
|
+
:completeness="rel.completeness"
|
|
79
|
+
/>
|
|
80
|
+
|
|
81
|
+
<ul class="member-presence-list" aria-label="Member presence and count">
|
|
82
|
+
<li v-for="(m, j) in rel.members" :key="j">
|
|
83
|
+
<span class="member-label">{{ designationFor(m.uri) }}</span>
|
|
84
|
+
<span v-if="m.presence && m.presence !== 'required'" class="member-presence">{{ presenceLabel(m.presence) }}</span>
|
|
85
|
+
<span v-if="m.count && m.count !== 'exactly_one'" class="member-count">{{ countLabel(m.count) }}</span>
|
|
86
|
+
<span v-if="m.isDelimiting" class="member-delimiting">{{ t('relations.delimiting') }}</span>
|
|
87
|
+
</li>
|
|
88
|
+
</ul>
|
|
89
|
+
</div>
|
|
90
|
+
</section>
|
|
91
|
+
</template>
|
|
92
|
+
|
|
93
|
+
<style scoped>
|
|
94
|
+
.generic-relation-card {
|
|
95
|
+
border: 1px solid var(--color-border, #ccc);
|
|
96
|
+
border-radius: 6px;
|
|
97
|
+
padding: 1rem;
|
|
98
|
+
margin-bottom: 1rem;
|
|
99
|
+
background: var(--color-surface, #fff);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.relation-header {
|
|
103
|
+
display: flex;
|
|
104
|
+
align-items: center;
|
|
105
|
+
gap: 0.5rem;
|
|
106
|
+
flex-wrap: wrap;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.badge {
|
|
110
|
+
font-size: 0.75rem;
|
|
111
|
+
font-weight: 600;
|
|
112
|
+
padding: 0.15rem 0.5rem;
|
|
113
|
+
border-radius: 4px;
|
|
114
|
+
text-transform: uppercase;
|
|
115
|
+
letter-spacing: 0.05em;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.badge-completeness.completeness-complete {
|
|
119
|
+
background: var(--color-success-soft, #d4edda);
|
|
120
|
+
color: var(--color-success-fg, #155724);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.badge-completeness.completeness-partial {
|
|
124
|
+
background: var(--color-warn-soft, #fff3cd);
|
|
125
|
+
color: var(--color-warn-fg, #856404);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
.badge-type {
|
|
129
|
+
background: var(--color-accent-soft, #d1ecf1);
|
|
130
|
+
color: var(--color-accent-fg, #0c5460);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.comprehensive {
|
|
134
|
+
margin: 0;
|
|
135
|
+
font-size: 1.1rem;
|
|
136
|
+
flex: 1 1 auto;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.criterion {
|
|
140
|
+
color: var(--color-text-muted, #6c757d);
|
|
141
|
+
margin: 0.5rem 0;
|
|
142
|
+
font-size: 0.95rem;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.member-presence-list {
|
|
146
|
+
list-style: none;
|
|
147
|
+
padding: 0;
|
|
148
|
+
margin: 0.5rem 0 0;
|
|
149
|
+
font-size: 0.85rem;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.member-presence-list li {
|
|
153
|
+
display: flex;
|
|
154
|
+
gap: 0.5rem;
|
|
155
|
+
align-items: center;
|
|
156
|
+
padding: 0.15rem 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.member-presence-list .member-presence,
|
|
160
|
+
.member-presence-list .member-count,
|
|
161
|
+
.member-presence-list .member-delimiting {
|
|
162
|
+
color: var(--color-text-muted, #6c757d);
|
|
163
|
+
font-size: 0.75rem;
|
|
164
|
+
border: 1px solid var(--color-border-light, #eee);
|
|
165
|
+
padding: 0.05rem 0.4rem;
|
|
166
|
+
border-radius: 3px;
|
|
167
|
+
}
|
|
168
|
+
</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) {
|
|
@@ -4,6 +4,8 @@ import type { Router } from 'vue-router';
|
|
|
4
4
|
// Import the MECE-augmented types from glossarist/models (extended by
|
|
5
5
|
// src/adapters/non-verbal/glossarist-augment.d.ts).
|
|
6
6
|
import type { Concept, RelatedConcept, ConceptRef } from 'glossarist';
|
|
7
|
+
import { PartitiveHyperedge } from 'glossarist';
|
|
8
|
+
import { GenericHyperedge } from 'glossarist/models';
|
|
7
9
|
import type {
|
|
8
10
|
PartitiveRelation as GlsPartitiveRelation,
|
|
9
11
|
PartitiveMember as GlsPartitiveMember,
|
|
@@ -13,9 +15,11 @@ import type {
|
|
|
13
15
|
GraphEdge,
|
|
14
16
|
PartitiveRelationWire,
|
|
15
17
|
PartitiveMemberWire,
|
|
18
|
+
GenericRelationWire,
|
|
16
19
|
} from '../adapters/types';
|
|
17
20
|
import { getFactory } from '../adapters/factory';
|
|
18
21
|
import { conceptUri } from '../adapters/model-bridge';
|
|
22
|
+
import { resolveDesignation } from '../utils/resolve-designation';
|
|
19
23
|
import { UriRouter } from '../adapters/UriRouter';
|
|
20
24
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
21
25
|
import { useDsStyle } from '../utils/dataset-style';
|
|
@@ -111,25 +115,7 @@ export function useConceptEdges(
|
|
|
111
115
|
* IDs like '1.3'.
|
|
112
116
|
*/
|
|
113
117
|
function designationFor(uri: string): string {
|
|
114
|
-
|
|
115
|
-
if (node) {
|
|
116
|
-
const des = node.designations[locale.value]
|
|
117
|
-
|| node.designations.eng
|
|
118
|
-
|| Object.values(node.designations)[0];
|
|
119
|
-
if (des) return des;
|
|
120
|
-
}
|
|
121
|
-
const resolution = factory.resolve(uri);
|
|
122
|
-
if (resolution.type === 'internal') {
|
|
123
|
-
const adapter = store.datasets.get(resolution.registerId);
|
|
124
|
-
const entry = adapter?.getIndexEntry(resolution.conceptId);
|
|
125
|
-
if (entry) {
|
|
126
|
-
const des = entry.designations[locale.value]
|
|
127
|
-
|| entry.designations.eng
|
|
128
|
-
|| Object.values(entry.designations)[0];
|
|
129
|
-
if (des) return des;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
return resolution.type === 'internal' ? resolution.conceptId : uri;
|
|
118
|
+
return resolveDesignation(uri, store, factory, locale.value);
|
|
133
119
|
}
|
|
134
120
|
|
|
135
121
|
function edgeBadgeColor(type: string, direction: 'out' | 'in'): string {
|
|
@@ -174,42 +160,53 @@ export function useConceptEdges(
|
|
|
174
160
|
});
|
|
175
161
|
});
|
|
176
162
|
|
|
177
|
-
// Concept-level
|
|
178
|
-
// Reads the glossarist-js
|
|
179
|
-
// and projects
|
|
180
|
-
//
|
|
181
|
-
//
|
|
182
|
-
|
|
163
|
+
// Concept-level n-ary hyperedge relations (ISO 704 one-to-many
|
|
164
|
+
// decompositions). Reads the glossarist-js unified `concept.relations`
|
|
165
|
+
// array and projects PartitiveHyperedge → PartitiveRelationWire,
|
|
166
|
+
// GenericHyperedge → GenericRelationWire for display.
|
|
167
|
+
//
|
|
168
|
+
// glossarist 0.4.33+ exposes a unified `.relations` array; type
|
|
169
|
+
// discrimination is via instanceof.
|
|
170
|
+
function projectHyperedge(rel: any): PartitiveRelationWire | GenericRelationWire | null {
|
|
183
171
|
const source = conceptUriValue.value;
|
|
184
|
-
const
|
|
185
|
-
return
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
.map((m: GlsPartitiveMember): PartitiveMemberWire | null => {
|
|
191
|
-
const uri = resolveConceptRefUri(m.ref);
|
|
192
|
-
if (!uri || uri === source) return null;
|
|
193
|
-
return {
|
|
194
|
-
uri,
|
|
195
|
-
presence: readPresence(m),
|
|
196
|
-
count: readCount(m),
|
|
197
|
-
isDelimiting: readIsDelimiting(m),
|
|
198
|
-
};
|
|
199
|
-
})
|
|
200
|
-
.filter((m: PartitiveMemberWire | null): m is PartitiveMemberWire => m !== null);
|
|
201
|
-
if (partitives.length === 0) return null;
|
|
202
|
-
|
|
172
|
+
const comprehensive = resolveConceptRefUri(rel.comprehensive);
|
|
173
|
+
if (!comprehensive) return null;
|
|
174
|
+
const members: PartitiveMemberWire[] = (rel.members ?? rel.partitives ?? [])
|
|
175
|
+
.map((m: any): PartitiveMemberWire | null => {
|
|
176
|
+
const uri = resolveConceptRefUri(m.ref);
|
|
177
|
+
if (!uri || uri === source) return null;
|
|
203
178
|
return {
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
criterion: rel.criterion ?? undefined,
|
|
209
|
-
register: registerId.value,
|
|
179
|
+
uri,
|
|
180
|
+
presence: readPresence(m),
|
|
181
|
+
count: readCount(m),
|
|
182
|
+
isDelimiting: readIsDelimiting(m),
|
|
210
183
|
};
|
|
211
184
|
})
|
|
212
|
-
.filter((
|
|
185
|
+
.filter((m: PartitiveMemberWire | null): m is PartitiveMemberWire => m !== null);
|
|
186
|
+
if (members.length === 0) return null;
|
|
187
|
+
return {
|
|
188
|
+
source,
|
|
189
|
+
comprehensive,
|
|
190
|
+
members,
|
|
191
|
+
partitives: members,
|
|
192
|
+
completeness: rel.completeness,
|
|
193
|
+
criterion: rel.criterion ?? undefined,
|
|
194
|
+
register: registerId.value,
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const conceptPartitiveRelations = computed<PartitiveRelationWire[]>(() => {
|
|
199
|
+
return (concept.value.relations as any[] ?? [])
|
|
200
|
+
.filter(r => r instanceof PartitiveHyperedge)
|
|
201
|
+
.map(projectHyperedge)
|
|
202
|
+
.filter((r): r is PartitiveRelationWire => r !== null);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
const conceptGenericRelations = computed<GenericRelationWire[]>(() => {
|
|
206
|
+
return (concept.value.relations as any[] ?? [])
|
|
207
|
+
.filter(r => r instanceof GenericHyperedge)
|
|
208
|
+
.map(projectHyperedge)
|
|
209
|
+
.filter((r): r is GenericRelationWire => r !== null);
|
|
213
210
|
});
|
|
214
211
|
|
|
215
212
|
function resolveConceptRefUri(ref: ConceptRef | null): string | null {
|
|
@@ -307,6 +304,7 @@ export function useConceptEdges(
|
|
|
307
304
|
inverseEdgeType,
|
|
308
305
|
conceptRelated,
|
|
309
306
|
conceptPartitiveRelations,
|
|
307
|
+
conceptGenericRelations,
|
|
310
308
|
resolveRelatedRef,
|
|
311
309
|
getResolvedRef,
|
|
312
310
|
relatedLabel,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a concept URI to its display designation (term string).
|
|
3
|
+
*
|
|
4
|
+
* Single SSOT for the designation fallback chain used by:
|
|
5
|
+
* - PartitiveRelationList.vue
|
|
6
|
+
* - GenericRelationList.vue
|
|
7
|
+
* - use-concept-edges.ts (designationFor method)
|
|
8
|
+
*
|
|
9
|
+
* Previously each site had its own copy with subtle divergences (one
|
|
10
|
+
* used `entry.titles`, another used `entry.designations` — the former
|
|
11
|
+
* is a bug since ConceptSummary has no `titles` property).
|
|
12
|
+
*
|
|
13
|
+
* Fallback chain:
|
|
14
|
+
* 1. BFS graph node (already loaded as a neighbor) → locale → eng → any
|
|
15
|
+
* 2. Dataset adapter index entry → designations[locale] → eng → any
|
|
16
|
+
* 3. Concept ID (for internal URIs) or raw URI (for external)
|
|
17
|
+
*/
|
|
18
|
+
import type { useVocabularyStore } from '../stores/vocabulary';
|
|
19
|
+
import type { AdapterFactory } from '../adapters/factory';
|
|
20
|
+
|
|
21
|
+
type VocabularyStore = ReturnType<typeof useVocabularyStore>;
|
|
22
|
+
type Factory = AdapterFactory;
|
|
23
|
+
|
|
24
|
+
export function resolveDesignation(
|
|
25
|
+
uri: string,
|
|
26
|
+
store: VocabularyStore,
|
|
27
|
+
factory: Factory,
|
|
28
|
+
currentLocale: string,
|
|
29
|
+
): string {
|
|
30
|
+
const node = store.graph.getNode(uri);
|
|
31
|
+
if (node) {
|
|
32
|
+
const des = node.designations[currentLocale]
|
|
33
|
+
|| node.designations.eng
|
|
34
|
+
|| Object.values(node.designations)[0];
|
|
35
|
+
if (des) return des;
|
|
36
|
+
}
|
|
37
|
+
const resolution = factory.resolve(uri);
|
|
38
|
+
if (resolution.type === 'internal') {
|
|
39
|
+
const adapter = store.datasets.get(resolution.registerId);
|
|
40
|
+
const entry = adapter?.getIndexEntry(resolution.conceptId);
|
|
41
|
+
if (entry) {
|
|
42
|
+
const des = entry.designations[currentLocale]
|
|
43
|
+
|| entry.designations.eng
|
|
44
|
+
|| Object.values(entry.designations)[0];
|
|
45
|
+
if (des) return des;
|
|
46
|
+
}
|
|
47
|
+
return resolution.conceptId;
|
|
48
|
+
}
|
|
49
|
+
return uri;
|
|
50
|
+
}
|