@glossarist/concept-browser 0.7.91 → 0.7.93
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/__tests__/components/GenericRelationList.test.ts +147 -0
- package/src/__tests__/components/GenericRelationList.test.ts.bak +147 -0
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +22 -23
- package/src/__tests__/composables/use-concept-edges-v2.test.ts.bak +166 -0
- package/src/__tests__/model-bridge-partitive-migration.test.ts +2 -2
- package/src/__tests__/wire-keys.test.ts +1 -1
- package/src/__tests__/wire-keys.test.ts.bak +65 -0
- package/src/adapters/model-bridge/concept.ts +12 -2
- package/src/adapters/model-bridge/hyperedge.ts +103 -0
- package/src/adapters/model-bridge/jsonld-types.ts +3 -0
- package/src/adapters/non-verbal/glossarist-augment.d.ts +77 -46
- package/src/adapters/types.ts +27 -0
- package/src/adapters/wire-keys.ts +4 -0
- package/src/components/ConceptDetail.vue +10 -0
- package/src/components/GenericRelationList.vue +176 -0
- package/src/components/PartitiveRelationList.vue +2 -19
- package/src/composables/use-concept-edges.ts +56 -52
- package/src/utils/resolve-designation.ts +50 -0
- package/src/views/ConceptView.vue +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.93",
|
|
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",
|
|
@@ -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', delimitingCharacteristic: { eng: "test characteristic" } },
|
|
62
|
+
{ uri: 'https://example.org/test/concept/5-5', presence: 'required', count: 'exactly_one', delimitingCharacteristic: { eng: "test characteristic" } },
|
|
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', 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
|
+
],
|
|
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,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';
|
|
@@ -40,7 +39,7 @@ function makePartitive(
|
|
|
40
39
|
if (fields.presence) data.presence = fields.presence;
|
|
41
40
|
if (fields.count) data.count = fields.count;
|
|
42
41
|
if (fields.isDelimiting) data.is_delimiting = fields.isDelimiting;
|
|
43
|
-
return PartitiveMemberModel
|
|
42
|
+
return new PartitiveMemberModel(data as any) as PartitiveMember;
|
|
44
43
|
}
|
|
45
44
|
|
|
46
45
|
function makeRelation(opts: {
|
|
@@ -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 new PartitiveHyperedgeModel(opts as any) 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', () => {
|
|
@@ -85,14 +84,14 @@ describe('useConceptEdges — MECE PartitiveRelation (glossarist 0.4.26)', () =>
|
|
|
85
84
|
|
|
86
85
|
it('is_delimiting defaults to false', () => {
|
|
87
86
|
const m = makePartitive('test', '1.1');
|
|
88
|
-
expect(m.is_delimiting).toBe(false);
|
|
89
|
-
expect(m.
|
|
87
|
+
expect((m as any).is_delimiting).toBe(false);
|
|
88
|
+
expect((m as any).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
|
-
expect(m.is_delimiting).toBe(true);
|
|
95
|
-
expect(m.
|
|
93
|
+
expect((m as any).is_delimiting).toBe(true);
|
|
94
|
+
expect((m as any).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 as any).is_delimiting).toBe(false);
|
|
88
|
+
expect((m as any).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 as any).is_delimiting).toBe(true);
|
|
94
|
+
expect((m as any).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 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);
|
|
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(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
|
+
});
|