@glossarist/concept-browser 0.7.92 → 0.7.94
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__/__fixtures__/concepts.ts +30 -0
- package/src/__tests__/components/GenericRelationList.test.ts +4 -4
- package/src/__tests__/components/GenericRelationList.test.ts.bak +147 -0
- package/src/__tests__/composables/use-concept-edges-hyperedge.test.ts +115 -0
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +6 -6
- package/src/__tests__/composables/use-concept-edges-v2.test.ts.bak +5 -5
- package/src/__tests__/wire-keys.test.ts +1 -1
- package/src/__tests__/wire-keys.test.ts.bak +65 -0
- package/src/adapters/model-bridge/hyperedge.ts +9 -0
- package/src/adapters/non-verbal/glossarist-augment.d.ts +22 -9
- package/src/adapters/types.ts +4 -1
- package/src/adapters/wire-keys.ts +1 -0
- package/src/components/ConceptRdfView.vue +6 -4
- package/src/components/GenericRelationList.vue +11 -3
- package/src/composables/use-concept-edges.ts +37 -21
- package/src/views/ConceptView.vue +9 -47
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.94",
|
|
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.34",
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"jszip": "^3.10.1",
|
|
43
43
|
"pinia": "^2.3.1",
|
|
@@ -204,6 +204,35 @@ function withdrawnConcept(): Concept {
|
|
|
204
204
|
});
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
+
function withPartitiveHyperedge(): Concept {
|
|
208
|
+
const source = 'urn:glossarist:fixtures';
|
|
209
|
+
return Concept.fromJSON({
|
|
210
|
+
id: '1.3',
|
|
211
|
+
uri: `${BASE}/with-partitive-hyperedge`,
|
|
212
|
+
status: 'valid',
|
|
213
|
+
localizations: {
|
|
214
|
+
eng: {
|
|
215
|
+
language_code: 'eng',
|
|
216
|
+
entry_status: 'valid',
|
|
217
|
+
terms: [{ type: 'expression', designation: 'system of quantities', normative_status: 'preferred' }],
|
|
218
|
+
definition: [{ content: 'A concept that has a partitive decomposition.' }],
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
relations: [
|
|
222
|
+
{
|
|
223
|
+
type: 'partitive_relation',
|
|
224
|
+
comprehensive: { source, id: '1.3' },
|
|
225
|
+
members: [
|
|
226
|
+
{ ref: { source, id: '1.22' }, presence: 'required', count: 'at_least_one' },
|
|
227
|
+
{ ref: { source, id: '1.4' }, presence: 'required', count: 'multiple' },
|
|
228
|
+
{ ref: { source, id: '1.5' }, presence: 'required', count: 'at_least_one' },
|
|
229
|
+
],
|
|
230
|
+
completeness: 'complete',
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
207
236
|
export const CONCEPT_FIXTURES: readonly ConceptFixture[] = [
|
|
208
237
|
{ name: 'minimal', description: 'one localization, one designation', uri: `${BASE}/minimal`, concept: minimal() },
|
|
209
238
|
{ name: 'multilingual', description: 'three languages across Latn and CJK scripts', uri: `${BASE}/multilingual`, concept: multilingual() },
|
|
@@ -212,6 +241,7 @@ export const CONCEPT_FIXTURES: readonly ConceptFixture[] = [
|
|
|
212
241
|
{ name: 'with-non-verbal', description: 'figure, formula, and table non-verbal reps', uri: `${BASE}/with-non-verbal`, concept: withNonVerbal() },
|
|
213
242
|
{ name: 'with-dates', description: 'accepted/amended/retired lifecycle', uri: `${BASE}/with-dates`, concept: withDates() },
|
|
214
243
|
{ name: 'withdrawn', description: 'withdrawn status with retirement date (J3)', uri: `${BASE}/withdrawn`, concept: withdrawnConcept() },
|
|
244
|
+
{ name: 'with-partitive-hyperedge', description: 'partitive decomposition in concept.relations', uri: `${BASE}/with-partitive-hyperedge`, concept: withPartitiveHyperedge() },
|
|
215
245
|
];
|
|
216
246
|
|
|
217
247
|
export function fixtureByName(name: string): ConceptFixture {
|
|
@@ -58,8 +58,8 @@ function makeRelation(overrides: Partial<GenericRelationWire> = {}): GenericRela
|
|
|
58
58
|
source: 'https://example.org/test/concept/5-1',
|
|
59
59
|
comprehensive: 'https://example.org/test/concept/5-1',
|
|
60
60
|
members: [
|
|
61
|
-
{ uri: 'https://example.org/test/concept/5-4', presence: 'required', count: 'exactly_one',
|
|
62
|
-
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one',
|
|
61
|
+
{ uri: 'https://example.org/test/concept/5-4', presence: 'required', count: 'exactly_one', delimitingCharacteristic: { eng: "test characteristic" } },
|
|
62
|
+
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one', delimitingCharacteristic: { eng: "test characteristic" } },
|
|
63
63
|
],
|
|
64
64
|
completeness: 'complete',
|
|
65
65
|
register: 'test',
|
|
@@ -120,8 +120,8 @@ describe('GenericRelationList — wire-type contract mount test', () => {
|
|
|
120
120
|
props: {
|
|
121
121
|
relations: [makeRelation({
|
|
122
122
|
members: [
|
|
123
|
-
{ uri: 'https://example.org/test/concept/5-4', presence: 'optional', count: 'multiple',
|
|
124
|
-
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one',
|
|
123
|
+
{ uri: 'https://example.org/test/concept/5-4', presence: 'optional', count: 'multiple', delimitingCharacteristic: { eng: "delimiting by light sensors" } },
|
|
124
|
+
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one', delimitingCharacteristic: { eng: "test characteristic" } },
|
|
125
125
|
],
|
|
126
126
|
})],
|
|
127
127
|
manifest,
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { computed, ref } from 'vue';
|
|
3
|
+
import type { Router } from 'vue-router';
|
|
4
|
+
import { useConceptEdges } from '../../composables/use-concept-edges';
|
|
5
|
+
import { useVocabularyStore } from '../../stores/vocabulary';
|
|
6
|
+
import type { Manifest, GraphEdge } from '../../adapters/types';
|
|
7
|
+
import { makeManifest, setupPinia } from '../test-helpers';
|
|
8
|
+
import { fixtureByName } from '../__fixtures__/concepts';
|
|
9
|
+
import { getFactory, resetFactory } from '../../adapters/factory';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Regression test for the sphere-view hyperedge projection.
|
|
13
|
+
*
|
|
14
|
+
* Bug (2026-07-30): ConceptView.vue read `concept.partitiveRelations`
|
|
15
|
+
* — a field glossarist 0.4.34 REMOVED. The field was silently
|
|
16
|
+
* `undefined`, the sphere rendered no rake, and no test caught it.
|
|
17
|
+
*
|
|
18
|
+
* Fix: ConceptView consumes `useConceptEdges.conceptPartitiveRelations`,
|
|
19
|
+
* which reads the unified `concept.relations` array and filters by
|
|
20
|
+
* `instanceof PartitiveHyperedge`. This spec pins that contract —
|
|
21
|
+
* if glossarist ever renames or removes `.relations`, this fails.
|
|
22
|
+
*/
|
|
23
|
+
describe('useConceptEdges — hyperedge projection (regression)', () => {
|
|
24
|
+
const URN_SOURCE = 'urn:glossarist:fixtures';
|
|
25
|
+
const manifest: Manifest = makeManifest({
|
|
26
|
+
id: 'fixtures',
|
|
27
|
+
datasetUri: URN_SOURCE,
|
|
28
|
+
uriBase: 'https://glossarist.org',
|
|
29
|
+
});
|
|
30
|
+
const noEdges: GraphEdge[] = [];
|
|
31
|
+
const noopRouter = { push() {} } as unknown as Router;
|
|
32
|
+
|
|
33
|
+
let store: ReturnType<typeof useVocabularyStore>;
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
setupPinia();
|
|
37
|
+
resetFactory();
|
|
38
|
+
store = useVocabularyStore();
|
|
39
|
+
store.manifests.set(manifest.id, manifest);
|
|
40
|
+
|
|
41
|
+
const factory = getFactory();
|
|
42
|
+
factory.resolveRelatedRef = vi.fn((ref: { source: string | null; id: string | null } | null) =>
|
|
43
|
+
ref?.source === URN_SOURCE && ref.id
|
|
44
|
+
? { registerId: manifest.id, conceptId: ref.id }
|
|
45
|
+
: null,
|
|
46
|
+
);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('projects a PartitiveHyperedge from concept.relations', () => {
|
|
50
|
+
const { concept } = fixtureByName('with-partitive-hyperedge');
|
|
51
|
+
const { conceptPartitiveRelations } = useConceptEdges(
|
|
52
|
+
computed(() => concept),
|
|
53
|
+
computed(() => 'fixtures'),
|
|
54
|
+
computed(() => manifest),
|
|
55
|
+
computed(() => noEdges),
|
|
56
|
+
noopRouter,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const rels = conceptPartitiveRelations.value;
|
|
60
|
+
expect(rels).toHaveLength(1);
|
|
61
|
+
|
|
62
|
+
const [rel] = rels;
|
|
63
|
+
expect(rel.comprehensive).toBe('https://glossarist.org/fixtures/concept/1.3');
|
|
64
|
+
expect(rel.partitives).toHaveLength(3);
|
|
65
|
+
expect(rel.partitives.map(m => m.uri)).toEqual([
|
|
66
|
+
'https://glossarist.org/fixtures/concept/1.22',
|
|
67
|
+
'https://glossarist.org/fixtures/concept/1.4',
|
|
68
|
+
'https://glossarist.org/fixtures/concept/1.5',
|
|
69
|
+
]);
|
|
70
|
+
expect(rel.partitives[1].presence).toBe('required');
|
|
71
|
+
expect(rel.partitives[1].count).toBe('multiple');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('returns [] when concept is null (loading state)', () => {
|
|
75
|
+
const { conceptPartitiveRelations, conceptGenericRelations } = useConceptEdges(
|
|
76
|
+
computed(() => null),
|
|
77
|
+
computed(() => 'fixtures'),
|
|
78
|
+
computed(() => manifest),
|
|
79
|
+
computed(() => noEdges),
|
|
80
|
+
noopRouter,
|
|
81
|
+
);
|
|
82
|
+
expect(conceptPartitiveRelations.value).toEqual([]);
|
|
83
|
+
expect(conceptGenericRelations.value).toEqual([]);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('returns [] when manifest is null', () => {
|
|
87
|
+
const { concept } = fixtureByName('with-partitive-hyperedge');
|
|
88
|
+
const { conceptPartitiveRelations } = useConceptEdges(
|
|
89
|
+
computed(() => concept),
|
|
90
|
+
computed(() => 'fixtures'),
|
|
91
|
+
computed(() => null),
|
|
92
|
+
computed(() => noEdges),
|
|
93
|
+
noopRouter,
|
|
94
|
+
);
|
|
95
|
+
expect(conceptPartitiveRelations.value).toEqual([]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('would have caught the .partitiveRelations regression', () => {
|
|
99
|
+
// Simulate the pre-fix behavior: the old code read concept.partitiveRelations
|
|
100
|
+
// (undefined in glossarist 0.4.34+) and produced an empty array.
|
|
101
|
+
// This spec proves the projection now reads concept.relations instead.
|
|
102
|
+
const { concept } = fixtureByName('with-partitive-hyperedge');
|
|
103
|
+
expect((concept as any).partitiveRelations).toBeUndefined();
|
|
104
|
+
expect((concept as any).relations).toHaveLength(1);
|
|
105
|
+
|
|
106
|
+
const { conceptPartitiveRelations } = useConceptEdges(
|
|
107
|
+
computed(() => concept),
|
|
108
|
+
computed(() => 'fixtures'),
|
|
109
|
+
computed(() => manifest),
|
|
110
|
+
computed(() => noEdges),
|
|
111
|
+
noopRouter,
|
|
112
|
+
);
|
|
113
|
+
expect(conceptPartitiveRelations.value.length).toBe(1);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
@@ -39,7 +39,7 @@ function makePartitive(
|
|
|
39
39
|
if (fields.presence) data.presence = fields.presence;
|
|
40
40
|
if (fields.count) data.count = fields.count;
|
|
41
41
|
if (fields.isDelimiting) data.is_delimiting = fields.isDelimiting;
|
|
42
|
-
return PartitiveMemberModel
|
|
42
|
+
return new (PartitiveMemberModel as unknown as new (data: unknown) => PartitiveMember)(data) as PartitiveMember;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
function makeRelation(opts: {
|
|
@@ -48,7 +48,7 @@ function makeRelation(opts: {
|
|
|
48
48
|
completeness?: 'complete' | 'partial';
|
|
49
49
|
criterion?: Record<string, string>;
|
|
50
50
|
}): PartitiveHyperedge {
|
|
51
|
-
return PartitiveHyperedgeModel
|
|
51
|
+
return new (PartitiveHyperedgeModel as unknown as new (data: unknown) => PartitiveHyperedge)(opts) as PartitiveHyperedge;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () => {
|
|
@@ -84,14 +84,14 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
84
84
|
|
|
85
85
|
it('is_delimiting defaults to false', () => {
|
|
86
86
|
const m = makePartitive('test', '1.1');
|
|
87
|
-
expect(m.is_delimiting).toBe(false);
|
|
88
|
-
expect(m.is_delimiting).toBe(false);
|
|
87
|
+
expect((m as any).is_delimiting).toBe(false);
|
|
88
|
+
expect((m as any).is_delimiting).toBe(false);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it('is_delimiting is preserved when set to true', () => {
|
|
92
92
|
const m = makePartitive('test', '1.1', { isDelimiting: true });
|
|
93
|
-
expect(m.is_delimiting).toBe(true);
|
|
94
|
-
expect(m.is_delimiting).toBe(true);
|
|
93
|
+
expect((m as any).is_delimiting).toBe(true);
|
|
94
|
+
expect((m as any).is_delimiting).toBe(true);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
it('rejects the vacuous (presence=optional, count=at_least_one)', () => {
|
|
@@ -84,14 +84,14 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
84
84
|
|
|
85
85
|
it('is_delimiting defaults to false', () => {
|
|
86
86
|
const m = makePartitive('test', '1.1');
|
|
87
|
-
expect(m.is_delimiting).toBe(false);
|
|
88
|
-
expect(m.is_delimiting).toBe(false);
|
|
87
|
+
expect((m as any).is_delimiting).toBe(false);
|
|
88
|
+
expect((m as any).is_delimiting).toBe(false);
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it('is_delimiting is preserved when set to true', () => {
|
|
92
92
|
const m = makePartitive('test', '1.1', { isDelimiting: true });
|
|
93
|
-
expect(m.is_delimiting).toBe(true);
|
|
94
|
-
expect(m.is_delimiting).toBe(true);
|
|
93
|
+
expect((m as any).is_delimiting).toBe(true);
|
|
94
|
+
expect((m as any).is_delimiting).toBe(true);
|
|
95
95
|
});
|
|
96
96
|
|
|
97
97
|
it('rejects the vacuous (presence=optional, count=at_least_one)', () => {
|
|
@@ -107,7 +107,7 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
107
107
|
comprehensive: makeRef('test', '1.3'),
|
|
108
108
|
partitives: [makePartitive('test', '1.4'), makePartitive('test', '1.5')],
|
|
109
109
|
});
|
|
110
|
-
expect(r.members).toHaveLength(2);
|
|
110
|
+
expect((r as any).members).toHaveLength(2);
|
|
111
111
|
expect((r as any).completeness).toBe('complete');
|
|
112
112
|
expect((r as any).isComplete).toBe(true);
|
|
113
113
|
expect((r as any).isPartial).toBe(false);
|
|
@@ -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(91);
|
|
64
64
|
});
|
|
65
65
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wire-key SSOT contract.
|
|
3
|
+
*
|
|
4
|
+
* Audit-driven (AUDIT-2026-07-29 P1-3).
|
|
5
|
+
*
|
|
6
|
+
* The GL record is the single source of truth for the JSON-LD wire-format
|
|
7
|
+
* keys used across src/adapters/. This spec pins:
|
|
8
|
+
*
|
|
9
|
+
* - Every entry is a string starting with 'gl:' (or '@' for JSON-LD core)
|
|
10
|
+
* - No two entries share the same value (injectivity — one key, one name)
|
|
11
|
+
* - The set of values matches what's declared in jsonld-types.ts
|
|
12
|
+
* (catches drift if someone adds a key to one but not the other)
|
|
13
|
+
*/
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { GL } from '../adapters/wire-keys';
|
|
16
|
+
|
|
17
|
+
describe('GL wire-key SSOT', () => {
|
|
18
|
+
it('every value is a non-empty string', () => {
|
|
19
|
+
for (const value of Object.values(GL)) {
|
|
20
|
+
expect(typeof value).toBe('string');
|
|
21
|
+
expect(value.length).toBeGreaterThan(0);
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('every value starts with gl: or @ (JSON-LD namespace)', () => {
|
|
26
|
+
for (const value of Object.values(GL)) {
|
|
27
|
+
expect(value.startsWith('gl:') || value.startsWith('@')).toBe(true);
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('no two entries share the same value (injectivity)', () => {
|
|
32
|
+
const values = Object.values(GL);
|
|
33
|
+
expect(new Set(values).size).toBe(values.length);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('MECE keys are present (presence, count, isDelimiting, multiplicity, certainty)', () => {
|
|
37
|
+
expect(GL.PRESENCE).toBe('gl:presence');
|
|
38
|
+
expect(GL.COUNT).toBe('gl:count');
|
|
39
|
+
expect(GL.IS_DELIMITING).toBe('gl:isDelimiting');
|
|
40
|
+
expect(GL.MULTIPLICITY).toBe('gl:multiplicity');
|
|
41
|
+
expect(GL.CERTAINTY).toBe('gl:certainty');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('JSON-LD core keys are present', () => {
|
|
45
|
+
expect(GL.ID).toBe('@id');
|
|
46
|
+
expect(GL.TYPE).toBe('@type');
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('LOCAL_ID is distinct from ID (one is gl:id, the other @id)', () => {
|
|
50
|
+
// Both represent "id" but in different namespaces:
|
|
51
|
+
// GL.ID (@id) — JSON-LD IRI for the concept
|
|
52
|
+
// GL.LOCAL_ID (gl:id) — source-local id within ConceptSource/Origin/Ref
|
|
53
|
+
expect(GL.ID).toBe('@id');
|
|
54
|
+
expect(GL.LOCAL_ID).toBe('gl:id');
|
|
55
|
+
expect(GL.ID).not.toBe(GL.LOCAL_ID);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('count matches documented set (no drift)', () => {
|
|
59
|
+
// Update this number when adding a new key.
|
|
60
|
+
// The point of this test: when adding a key, you remember to update
|
|
61
|
+
// the count; when removing, you notice the test fails. Catches
|
|
62
|
+
// accidental drift in either direction.
|
|
63
|
+
expect(Object.keys(GL).length).toBe(90);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -53,6 +53,15 @@ export function mapHyperedgeFromJsonLd(
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
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
|
+
}
|
|
56
65
|
return out;
|
|
57
66
|
})
|
|
58
67
|
.filter((m): m is Record<string, unknown> => m !== null);
|
|
@@ -171,8 +171,6 @@ declare module 'glossarist' {
|
|
|
171
171
|
|
|
172
172
|
interface Concept {
|
|
173
173
|
readonly relations: AbstractHyperedge[];
|
|
174
|
-
readonly partitiveRelations: PartitiveHyperedge[];
|
|
175
|
-
readonly genericRelations: GenericHyperedge[];
|
|
176
174
|
}
|
|
177
175
|
|
|
178
176
|
// Re-exposed here so callers importing from the top-level `glossarist`
|
|
@@ -182,22 +180,37 @@ declare module 'glossarist' {
|
|
|
182
180
|
ref?: ConceptRef;
|
|
183
181
|
presence?: PartitivePresence;
|
|
184
182
|
count?: PartitiveCount;
|
|
185
|
-
is_delimiting?: boolean;
|
|
186
183
|
});
|
|
187
184
|
readonly ref: ConceptRef;
|
|
188
185
|
readonly presence: PartitivePresence;
|
|
189
186
|
readonly count: PartitiveCount;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
readonly isOptional: boolean;
|
|
193
|
-
get isDelimiting(): boolean;
|
|
187
|
+
required(): boolean;
|
|
188
|
+
optional(): boolean;
|
|
194
189
|
toJSON(): Record<string, unknown>;
|
|
195
190
|
static fromJSON(data: Record<string, unknown>): HyperedgeMember;
|
|
196
191
|
static identityOf(value: unknown): string;
|
|
197
192
|
}
|
|
198
193
|
|
|
199
|
-
export class PartitiveMember extends HyperedgeMember {
|
|
200
|
-
|
|
194
|
+
export class PartitiveMember extends HyperedgeMember {
|
|
195
|
+
constructor(data?: {
|
|
196
|
+
ref?: ConceptRef;
|
|
197
|
+
presence?: PartitivePresence;
|
|
198
|
+
count?: PartitiveCount;
|
|
199
|
+
is_delimiting?: boolean;
|
|
200
|
+
});
|
|
201
|
+
readonly is_delimiting: boolean;
|
|
202
|
+
get isDelimiting(): boolean;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export class GenericMember extends HyperedgeMember {
|
|
206
|
+
constructor(data?: {
|
|
207
|
+
ref?: ConceptRef;
|
|
208
|
+
presence?: PartitivePresence;
|
|
209
|
+
count?: PartitiveCount;
|
|
210
|
+
delimitingCharacteristic?: Record<string, string> | string;
|
|
211
|
+
});
|
|
212
|
+
readonly delimitingCharacteristic: Record<string, string>;
|
|
213
|
+
}
|
|
201
214
|
|
|
202
215
|
export class AbstractHyperedge extends GlossaristModel {
|
|
203
216
|
readonly comprehensive: ConceptRef;
|
package/src/adapters/types.ts
CHANGED
|
@@ -200,7 +200,10 @@ export interface GenericMemberWire {
|
|
|
200
200
|
uri: string;
|
|
201
201
|
presence: 'required' | 'optional';
|
|
202
202
|
count: 'exactly_one' | 'at_least_one' | 'multiple';
|
|
203
|
-
|
|
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>;
|
|
204
207
|
}
|
|
205
208
|
|
|
206
209
|
export interface GraphNode {
|
|
@@ -10,12 +10,14 @@ import { useRdfDocument } from './use-rdf-document';
|
|
|
10
10
|
const props = defineProps<{
|
|
11
11
|
concept: Concept;
|
|
12
12
|
registerId: string;
|
|
13
|
-
conceptUriValue: string;
|
|
13
|
+
conceptUriValue: string | null;
|
|
14
14
|
}>();
|
|
15
15
|
|
|
16
|
+
const uri = computed(() => props.conceptUriValue ?? '');
|
|
17
|
+
|
|
16
18
|
const { sections, turtle, jsonld, typeChain } = useRdfDocument(
|
|
17
19
|
() => props.concept,
|
|
18
|
-
() =>
|
|
20
|
+
() => uri.value,
|
|
19
21
|
);
|
|
20
22
|
|
|
21
23
|
const resourceCount = computed(() => sections.value.length);
|
|
@@ -23,11 +25,11 @@ const conceptId = computed(() => props.concept.id);
|
|
|
23
25
|
</script>
|
|
24
26
|
|
|
25
27
|
<template>
|
|
26
|
-
<ErrorBoundary title="RDF view failed" :retry-key="
|
|
28
|
+
<ErrorBoundary title="RDF view failed" :retry-key="uri">
|
|
27
29
|
<div class="space-y-6">
|
|
28
30
|
<div class="card p-5">
|
|
29
31
|
<div class="flex items-start justify-between gap-3">
|
|
30
|
-
<RdfInstanceHeader :uri="
|
|
32
|
+
<RdfInstanceHeader :uri="uri" :concept-id="conceptId" />
|
|
31
33
|
</div>
|
|
32
34
|
<div class="mt-4 pt-3 border-t border-ink-100/60">
|
|
33
35
|
<div class="flex items-center gap-1.5 flex-wrap text-xs text-ink-400">
|
|
@@ -42,12 +42,17 @@ function designationFor(uri: string): string {
|
|
|
42
42
|
return resolveDesignation(uri, store, factory, locale.value);
|
|
43
43
|
}
|
|
44
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
|
+
|
|
45
50
|
function memberLabel(member: GenericRelationWire['members'][number]): PartitiveMemberLabeled {
|
|
46
51
|
return {
|
|
47
52
|
uri: member.uri,
|
|
48
53
|
presence: member.presence,
|
|
49
54
|
count: member.count,
|
|
50
|
-
isDelimiting:
|
|
55
|
+
isDelimiting: false,
|
|
51
56
|
label: designationFor(member.uri),
|
|
52
57
|
};
|
|
53
58
|
}
|
|
@@ -78,12 +83,15 @@ function memberLabel(member: GenericRelationWire['members'][number]): PartitiveM
|
|
|
78
83
|
:completeness="rel.completeness"
|
|
79
84
|
/>
|
|
80
85
|
|
|
81
|
-
<ul class="member-presence-list" aria-label="Member
|
|
86
|
+
<ul class="member-presence-list" aria-label="Member delimiting characteristics">
|
|
82
87
|
<li v-for="(m, j) in rel.members" :key="j">
|
|
83
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>
|
|
84
93
|
<span v-if="m.presence && m.presence !== 'required'" class="member-presence">{{ presenceLabel(m.presence) }}</span>
|
|
85
94
|
<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
95
|
</li>
|
|
88
96
|
</ul>
|
|
89
97
|
</div>
|
|
@@ -39,9 +39,9 @@ export interface EdgeDisplay {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export function useConceptEdges(
|
|
42
|
-
concept: ComputedRef<Concept>,
|
|
42
|
+
concept: ComputedRef<Concept | null | undefined>,
|
|
43
43
|
registerId: ComputedRef<string>,
|
|
44
|
-
manifest: ComputedRef<Manifest>,
|
|
44
|
+
manifest: ComputedRef<Manifest | null | undefined>,
|
|
45
45
|
edges: ComputedRef<GraphEdge[]>,
|
|
46
46
|
router: Router,
|
|
47
47
|
) {
|
|
@@ -50,24 +50,30 @@ export function useConceptEdges(
|
|
|
50
50
|
const { getColor } = useDsStyle();
|
|
51
51
|
const { locale } = useI18n();
|
|
52
52
|
|
|
53
|
-
const conceptUriValue = computed(() =>
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
const conceptUriValue = computed(() => {
|
|
54
|
+
if (!concept.value || !manifest.value) return null;
|
|
55
|
+
return conceptUri(concept.value, registerId.value, manifest.value.uriBase);
|
|
56
|
+
});
|
|
56
57
|
|
|
57
|
-
const outgoingEdges = computed(() =>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
const outgoingEdges = computed(() => {
|
|
59
|
+
const uri = conceptUriValue.value;
|
|
60
|
+
if (!uri) return [];
|
|
61
|
+
return store.graph.getUniqueEdges(uri, 'outgoing', 'target')
|
|
62
|
+
.filter(e => e.type !== 'domain' && e.type !== 'section');
|
|
63
|
+
});
|
|
61
64
|
|
|
62
|
-
const incomingEdges = computed(() =>
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
const incomingEdges = computed(() => {
|
|
66
|
+
const uri = conceptUriValue.value;
|
|
67
|
+
if (!uri) return [];
|
|
68
|
+
return store.graph.getUniqueEdges(uri, 'incoming', 'source')
|
|
69
|
+
.filter(e => e.type !== 'domain' && e.type !== 'section');
|
|
70
|
+
});
|
|
66
71
|
|
|
67
72
|
const edgeDisplayCache = computed(() => {
|
|
68
73
|
const cache = new Map<string, EdgeDisplay>();
|
|
74
|
+
const focusUri = conceptUriValue.value;
|
|
69
75
|
for (const e of edges.value) {
|
|
70
|
-
const uri = e.source ===
|
|
76
|
+
const uri = e.source === focusUri ? e.target : e.source;
|
|
71
77
|
if (cache.has(uri)) continue;
|
|
72
78
|
const resolution = factory.resolve(uri, registerId.value);
|
|
73
79
|
const isLocal = resolution.type === 'internal' && resolution.registerId === registerId.value;
|
|
@@ -131,6 +137,7 @@ export function useConceptEdges(
|
|
|
131
137
|
|
|
132
138
|
// Concept-level related concepts (managed concept cross-references)
|
|
133
139
|
const conceptRelated = computed(() => {
|
|
140
|
+
if (!concept.value) return [];
|
|
134
141
|
const direct = concept.value.relatedConcepts?.filter(rc => !INVERSE_RELATIONSHIPS[rc.type]) ?? [];
|
|
135
142
|
const derived = incomingEdges.value
|
|
136
143
|
.filter(e => INVERSE_RELATIONSHIPS[e.type])
|
|
@@ -169,20 +176,27 @@ export function useConceptEdges(
|
|
|
169
176
|
// discrimination is via instanceof.
|
|
170
177
|
function projectHyperedge(rel: any): PartitiveRelationWire | GenericRelationWire | null {
|
|
171
178
|
const source = conceptUriValue.value;
|
|
179
|
+
if (!source) return null;
|
|
172
180
|
const comprehensive = resolveConceptRefUri(rel.comprehensive);
|
|
173
181
|
if (!comprehensive) return null;
|
|
174
|
-
const
|
|
175
|
-
|
|
182
|
+
const isGeneric = rel instanceof GenericHyperedge;
|
|
183
|
+
const members: any[] = (rel.members ?? rel.partitives ?? [])
|
|
184
|
+
.map((m: any): any | null => {
|
|
176
185
|
const uri = resolveConceptRefUri(m.ref);
|
|
177
186
|
if (!uri || uri === source) return null;
|
|
178
|
-
|
|
187
|
+
const base: any = {
|
|
179
188
|
uri,
|
|
180
189
|
presence: readPresence(m),
|
|
181
190
|
count: readCount(m),
|
|
182
|
-
isDelimiting: readIsDelimiting(m),
|
|
183
191
|
};
|
|
192
|
+
if (isGeneric) {
|
|
193
|
+
base.delimitingCharacteristic = m.delimitingCharacteristic;
|
|
194
|
+
} else {
|
|
195
|
+
base.isDelimiting = readIsDelimiting(m);
|
|
196
|
+
}
|
|
197
|
+
return base;
|
|
184
198
|
})
|
|
185
|
-
.filter((m:
|
|
199
|
+
.filter((m: any | null): m is any => m !== null);
|
|
186
200
|
if (members.length === 0) return null;
|
|
187
201
|
return {
|
|
188
202
|
source,
|
|
@@ -196,14 +210,16 @@ export function useConceptEdges(
|
|
|
196
210
|
}
|
|
197
211
|
|
|
198
212
|
const conceptPartitiveRelations = computed<PartitiveRelationWire[]>(() => {
|
|
199
|
-
|
|
213
|
+
if (!concept.value) return [];
|
|
214
|
+
return ((concept.value.relations as any[]) ?? [])
|
|
200
215
|
.filter(r => r instanceof PartitiveHyperedge)
|
|
201
216
|
.map(projectHyperedge)
|
|
202
217
|
.filter((r): r is PartitiveRelationWire => r !== null);
|
|
203
218
|
});
|
|
204
219
|
|
|
205
220
|
const conceptGenericRelations = computed<GenericRelationWire[]>(() => {
|
|
206
|
-
|
|
221
|
+
if (!concept.value) return [];
|
|
222
|
+
return ((concept.value.relations as any[]) ?? [])
|
|
207
223
|
.filter(r => r instanceof GenericHyperedge)
|
|
208
224
|
.map(projectHyperedge)
|
|
209
225
|
.filter((r): r is GenericRelationWire => r !== null);
|
|
@@ -2,14 +2,10 @@
|
|
|
2
2
|
import { computed, watch, ref, onMounted, onUnmounted } from 'vue';
|
|
3
3
|
import { useRouter } from 'vue-router';
|
|
4
4
|
import { useVocabularyStore } from '../stores/vocabulary';
|
|
5
|
-
import { conceptUri } from '../adapters/model-bridge';
|
|
6
|
-
import { UriRouter } from '../adapters/UriRouter';
|
|
7
|
-
import type { PartitiveRelationWire, PartitiveMemberWire } from '../adapters/types';
|
|
8
|
-
import type { PartitiveMember } from 'glossarist/models';
|
|
9
|
-
import type { PartitivePresence, PartitiveCount } from '../utils/partitive-multiplicity';
|
|
10
5
|
import ConceptDetail from '../components/ConceptDetail.vue';
|
|
11
6
|
import RelationSphere from '../components/RelationSphere.vue';
|
|
12
7
|
import ShortcutsModal from '../components/ShortcutsModal.vue';
|
|
8
|
+
import { useConceptEdges } from '../composables/use-concept-edges';
|
|
13
9
|
import { useI18n } from '../i18n';
|
|
14
10
|
|
|
15
11
|
const { t } = useI18n();
|
|
@@ -63,47 +59,13 @@ const edges = computed(() => store.conceptEdges);
|
|
|
63
59
|
const activeRegisterId = computed(() => sphereFocusPayload.value?.registerId ?? props.registerId);
|
|
64
60
|
const adjacent = ref({ prev: null as string | null, next: null as string | null });
|
|
65
61
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const c = concept.value;
|
|
74
|
-
const m = manifest.value;
|
|
75
|
-
if (!c || !m) return [];
|
|
76
|
-
const sourceUri = conceptUri(c, activeRegisterId.value, m.uriBase);
|
|
77
|
-
const out: PartitiveRelationWire[] = [];
|
|
78
|
-
for (const rel of c.partitiveRelations ?? []) {
|
|
79
|
-
const compRef = rel.comprehensive;
|
|
80
|
-
const compTarget = store.manifests.get(activeRegisterId.value)
|
|
81
|
-
? UriRouter.buildConceptUri(m.uriBase, activeRegisterId.value, compRef.id ?? '')
|
|
82
|
-
: null;
|
|
83
|
-
if (!compRef.id || !compTarget) continue;
|
|
84
|
-
const partitives: PartitiveMemberWire[] = rel.partitives
|
|
85
|
-
.map((member): PartitiveMemberWire | null => {
|
|
86
|
-
const ref = member.ref;
|
|
87
|
-
if (!ref?.id) return null;
|
|
88
|
-
const uri = UriRouter.buildConceptUri(m.uriBase, activeRegisterId.value, ref.id);
|
|
89
|
-
if (uri === sourceUri) return null;
|
|
90
|
-
const presence: PartitivePresence = member.presence;
|
|
91
|
-
const count: PartitiveCount = member.count;
|
|
92
|
-
const isDelimiting = member.isDelimiting;
|
|
93
|
-
return { uri, presence, count, isDelimiting };
|
|
94
|
-
})
|
|
95
|
-
.filter((x): x is PartitiveMemberWire => x !== null);
|
|
96
|
-
if (partitives.length === 0) continue;
|
|
97
|
-
out.push({
|
|
98
|
-
source: sourceUri,
|
|
99
|
-
comprehensive: compTarget,
|
|
100
|
-
partitives,
|
|
101
|
-
completeness: rel.completeness,
|
|
102
|
-
register: activeRegisterId.value,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
return out;
|
|
106
|
-
});
|
|
62
|
+
const { conceptPartitiveRelations } = useConceptEdges(
|
|
63
|
+
concept,
|
|
64
|
+
activeRegisterId,
|
|
65
|
+
manifest,
|
|
66
|
+
edges,
|
|
67
|
+
router,
|
|
68
|
+
);
|
|
107
69
|
const viewMode = ref<'detail' | 'sphere'>(
|
|
108
70
|
router.currentRoute.value.query.view === 'sphere' ? 'sphere' : 'detail'
|
|
109
71
|
);
|
|
@@ -326,7 +288,7 @@ onUnmounted(() => window.removeEventListener('keydown', onKeydown));
|
|
|
326
288
|
:manifest="manifest"
|
|
327
289
|
:register-id="activeRegisterId"
|
|
328
290
|
:edges="edges"
|
|
329
|
-
:partitive-relations="
|
|
291
|
+
:partitive-relations="conceptPartitiveRelations"
|
|
330
292
|
@navigate="onSphereNavigate"
|
|
331
293
|
/>
|
|
332
294
|
<!-- Detail mode -->
|