@glossarist/concept-browser 0.7.51 → 0.7.52

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.
Files changed (152) hide show
  1. package/cli/index.mjs +32 -0
  2. package/env.d.ts +15 -0
  3. package/package.json +12 -2
  4. package/scripts/__tests__/doctor.test.mjs +147 -0
  5. package/scripts/doctor.mjs +327 -0
  6. package/scripts/generate-data.mjs +136 -0
  7. package/scripts/generate-ontology-data.mjs +3 -3
  8. package/scripts/generate-ontology-schema.mjs +3 -3
  9. package/scripts/lib/agents-turtle.mjs +64 -0
  10. package/scripts/lib/bibliography-turtle.mjs +54 -0
  11. package/scripts/lib/build-activity-turtle.mjs +92 -0
  12. package/scripts/lib/build-cache.mjs +70 -0
  13. package/scripts/lib/dataset-turtle.mjs +79 -0
  14. package/scripts/lib/turtle-escape.mjs +0 -0
  15. package/scripts/lib/version-turtle.mjs +56 -0
  16. package/scripts/lib/vocab-turtle.mjs +64 -0
  17. package/scripts/normalize-yaml.mjs +99 -0
  18. package/scripts/sync-concept-model.mjs +86 -0
  19. package/scripts/validate-shacl.mjs +194 -0
  20. package/src/__fixtures__/concept-shape.ttl +20 -0
  21. package/src/__fixtures__/shacl/bad/concept.ttl +7 -0
  22. package/src/__fixtures__/shacl/empty/.gitkeep +0 -0
  23. package/src/__fixtures__/shacl/good/concept.ttl +8 -0
  24. package/src/__tests__/__fixtures__/concepts.ts +221 -0
  25. package/src/__tests__/adapters/concept-identity.test.ts +76 -0
  26. package/src/__tests__/components/error-boundary.test.ts +109 -0
  27. package/src/__tests__/composables/use-dataset-series.test.ts +262 -0
  28. package/src/__tests__/concept-rdf/agents-emitter.test.ts +110 -0
  29. package/src/__tests__/concept-rdf/bibliography-emitter.test.ts +159 -0
  30. package/src/__tests__/concept-rdf/build-activity-emitter.test.ts +119 -0
  31. package/src/__tests__/concept-rdf/coerce-date.test.ts +97 -0
  32. package/src/__tests__/concept-rdf/concept-emitter.test.ts +258 -0
  33. package/src/__tests__/concept-rdf/dataset-emitter.test.ts +224 -0
  34. package/src/__tests__/concept-rdf/differential.test.ts +96 -0
  35. package/src/__tests__/concept-rdf/group-emitter.test.ts +109 -0
  36. package/src/__tests__/concept-rdf/image-variant-emitter.test.ts +135 -0
  37. package/src/__tests__/concept-rdf/jsonld-writer.test.ts +109 -0
  38. package/src/__tests__/concept-rdf/nonverbal-rep.test.ts +177 -0
  39. package/src/__tests__/concept-rdf/property-based.test.ts +179 -0
  40. package/src/__tests__/concept-rdf/provenance.test.ts +110 -0
  41. package/src/__tests__/concept-rdf/quad-isomorphism.test.ts +43 -0
  42. package/src/__tests__/concept-rdf/quad-isomorphism.ts +47 -0
  43. package/src/__tests__/concept-rdf/rdf-components.test.ts +145 -0
  44. package/src/__tests__/concept-rdf/rdf-graph.test.ts +115 -0
  45. package/src/__tests__/concept-rdf/round-trip.test.ts +243 -0
  46. package/src/__tests__/concept-rdf/scoped-examples.test.ts +126 -0
  47. package/src/__tests__/concept-rdf/sections-builder.test.ts +94 -0
  48. package/src/__tests__/concept-rdf/shacl-conformance.test.ts +110 -0
  49. package/src/__tests__/concept-rdf/shape-consistency.test.ts +138 -0
  50. package/src/__tests__/concept-rdf/snapshot-generation.test.ts +75 -0
  51. package/src/__tests__/concept-rdf/table-formula-emitter.test.ts +142 -0
  52. package/src/__tests__/concept-rdf/turtle-writer.test.ts +114 -0
  53. package/src/__tests__/concept-rdf/use-rdf-document.test.ts +246 -0
  54. package/src/__tests__/concept-rdf/version-emitter.test.ts +120 -0
  55. package/src/__tests__/concept-rdf/vocabulary-ssot.test.ts +100 -0
  56. package/src/__tests__/concept-rdf-view.test.ts +136 -0
  57. package/src/__tests__/dataset-style.test.ts +12 -7
  58. package/src/__tests__/errors/errors.test.ts +142 -0
  59. package/src/__tests__/format-downloads.test.ts +47 -65
  60. package/src/__tests__/markdown-lite.test.ts +19 -0
  61. package/src/__tests__/perf/bundle-layout.test.ts +50 -0
  62. package/src/__tests__/perf/serialization-perf.test.ts +121 -0
  63. package/src/__tests__/scripts/agents-turtle.test.ts +61 -0
  64. package/src/__tests__/scripts/bibliography-turtle.test.ts +59 -0
  65. package/src/__tests__/scripts/build-activity-turtle.test.ts +75 -0
  66. package/src/__tests__/scripts/build-cache.test.ts +78 -0
  67. package/src/__tests__/scripts/build-integration.test.ts +134 -0
  68. package/src/__tests__/scripts/dataset-turtle.test.ts +94 -0
  69. package/src/__tests__/scripts/normalize-yaml.test.ts +98 -0
  70. package/src/__tests__/scripts/stryker-config.test.ts +33 -0
  71. package/src/__tests__/scripts/turtle-escape.test.ts +63 -0
  72. package/src/__tests__/scripts/version-turtle.test.ts +72 -0
  73. package/src/__tests__/scripts/vocab-turtle.test.ts +63 -0
  74. package/src/__tests__/use-format-registry.test.ts +125 -0
  75. package/src/__tests__/utils/bcp47.test.ts +166 -0
  76. package/src/__tests__/utils/color-theme.test.ts +143 -0
  77. package/src/__tests__/utils/url-safety.test.ts +65 -0
  78. package/src/__tests__/validate-shacl.test.ts +100 -0
  79. package/src/adapters/DatasetAdapter.ts +11 -5
  80. package/src/adapters/GraphDataSource.ts +2 -1
  81. package/src/adapters/UriRouter.ts +2 -1
  82. package/src/adapters/concept-identity.ts +69 -0
  83. package/src/adapters/factory.ts +3 -2
  84. package/src/adapters/model-bridge.ts +2 -1
  85. package/src/adapters/non-verbal/glossarist-augment.d.ts +7 -0
  86. package/src/adapters/non-verbal-resolver.ts +2 -1
  87. package/src/components/AppSidebar.vue +189 -93
  88. package/src/components/ConceptDetail.vue +8 -0
  89. package/src/components/ConceptEditionRail.vue +222 -0
  90. package/src/components/ConceptRdfView.vue +37 -377
  91. package/src/components/DatasetSeriesCard.vue +270 -0
  92. package/src/components/ErrorBoundary.vue +95 -0
  93. package/src/components/FormatDownloads.vue +17 -13
  94. package/src/components/HomeSeriesSection.vue +277 -0
  95. package/src/components/RelationSphere.vue +1672 -0
  96. package/src/components/SidebarSeriesSection.vue +239 -0
  97. package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
  98. package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
  99. package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
  100. package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
  101. package/src/components/concept-rdf/agents-emitter.ts +82 -0
  102. package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
  103. package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
  104. package/src/components/concept-rdf/concept-emitter.ts +443 -0
  105. package/src/components/concept-rdf/dataset-emitter.ts +95 -0
  106. package/src/components/concept-rdf/group-emitter.ts +69 -0
  107. package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
  108. package/src/components/concept-rdf/jsonld-writer.ts +82 -0
  109. package/src/components/concept-rdf/predicates.ts +261 -0
  110. package/src/components/concept-rdf/provenance.ts +80 -0
  111. package/src/components/concept-rdf/rdf-graph.ts +211 -0
  112. package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
  113. package/src/components/concept-rdf/sections-builder.ts +62 -0
  114. package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
  115. package/src/components/concept-rdf/turtle-writer.ts +116 -0
  116. package/src/components/concept-rdf/use-rdf-document.ts +72 -0
  117. package/src/components/concept-rdf/version-emitter.ts +65 -0
  118. package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
  119. package/src/composables/use-color-theme.ts +82 -0
  120. package/src/composables/use-format-registry.ts +42 -0
  121. package/src/composables/useDatasetSeries.ts +258 -0
  122. package/src/composables/useSphereProjection.ts +125 -0
  123. package/src/config/group-types.ts +92 -0
  124. package/src/config/types.ts +81 -2
  125. package/src/config/use-site-config.ts +2 -1
  126. package/src/errors.ts +136 -0
  127. package/src/i18n/locales/eng.yml +24 -0
  128. package/src/i18n/locales/fra.yml +24 -0
  129. package/src/stores/vocabulary.ts +3 -1
  130. package/src/style.css +17 -2
  131. package/src/types/agents-version-turtle.d.ts +27 -0
  132. package/src/types/bibliography-turtle.d.ts +12 -0
  133. package/src/types/build-activity-turtle.d.ts +16 -0
  134. package/src/types/build-cache.d.ts +20 -0
  135. package/src/types/dataset-turtle.d.ts +32 -0
  136. package/src/types/normalize-yaml.d.ts +16 -0
  137. package/src/types/turtle-escape.d.ts +6 -0
  138. package/src/types/vocab-turtle.d.ts +13 -0
  139. package/src/utils/asciidoc-lite.ts +11 -6
  140. package/src/utils/bcp47.ts +141 -0
  141. package/src/utils/color-theme-integration.ts +11 -0
  142. package/src/utils/color-theme.ts +129 -0
  143. package/src/utils/dataset-style.ts +31 -6
  144. package/src/utils/locale.ts +6 -14
  145. package/src/utils/markdown-lite.ts +6 -1
  146. package/src/utils/relation-sphere-styling.ts +63 -0
  147. package/src/utils/relationship-categories.ts +30 -0
  148. package/src/utils/url-safety.ts +30 -0
  149. package/src/views/ConceptView.vue +183 -9
  150. package/src/views/DatasetView.vue +6 -0
  151. package/src/views/HomeView.vue +5 -0
  152. package/vite.config.ts +7 -0
@@ -0,0 +1,221 @@
1
+ import { Concept } from 'glossarist';
2
+
3
+ export interface ConceptFixture {
4
+ readonly name: string;
5
+ readonly description: string;
6
+ readonly uri: string;
7
+ readonly concept: Concept;
8
+ }
9
+
10
+ const BASE = 'https://glossarist.org/fixtures/concept';
11
+
12
+ function minimal(): Concept {
13
+ return Concept.fromJSON({
14
+ id: '1.1',
15
+ uri: `${BASE}/minimal`,
16
+ status: 'valid',
17
+ localizations: {
18
+ eng: {
19
+ language_code: 'eng',
20
+ entry_status: 'valid',
21
+ terms: [{ type: 'expression', designation: 'minimal concept', normative_status: 'preferred' }],
22
+ definition: [{ content: 'A minimal concept used to verify the baseline emission path.' }],
23
+ },
24
+ },
25
+ });
26
+ }
27
+
28
+ function multilingual(): Concept {
29
+ return Concept.fromJSON({
30
+ id: '2.1',
31
+ uri: `${BASE}/multilingual`,
32
+ status: 'valid',
33
+ localizations: {
34
+ eng: {
35
+ language_code: 'eng',
36
+ entry_status: 'valid',
37
+ terms: [{ type: 'expression', designation: 'coordinate system', normative_status: 'preferred' }],
38
+ definition: [{ content: 'A system for specifying positions in space.' }],
39
+ },
40
+ fra: {
41
+ language_code: 'fra',
42
+ entry_status: 'valid',
43
+ terms: [{ type: 'expression', designation: 'système de coordonnées', normative_status: 'preferred' }],
44
+ definition: [{ content: 'Un système pour spécifier des positions dans l’espace.' }],
45
+ },
46
+ jpn: {
47
+ language_code: 'jpn',
48
+ entry_status: 'valid',
49
+ terms: [{ type: 'expression', designation: '座標系', normative_status: 'preferred' }],
50
+ definition: [{ content: '空間における位置を指定するための体系。' }],
51
+ },
52
+ },
53
+ });
54
+ }
55
+
56
+ function fullRelationships(): Concept {
57
+ return Concept.fromJSON({
58
+ id: '3.1',
59
+ uri: `${BASE}/full-relationships`,
60
+ status: 'valid',
61
+ related: [
62
+ { type: 'supersedes', content: 'Replaces older term.', ref: { source: 'IEC', id: '60050-3.1.1' } },
63
+ { type: 'superseded_by', content: 'Superseded by newer.', ref: { source: 'ISO', id: '9999-1.2.3' } },
64
+ { type: 'derived', content: 'Derived from source.', ref: { source: 'VIM', id: '1.1' } },
65
+ { type: 'compare', content: 'Compare with similar.', ref: { source: 'IEV', id: '102-01-01' } },
66
+ { type: 'contrast', content: 'Contrast with this.', ref: { source: 'IEV', id: '102-02-02' } },
67
+ ],
68
+ localizations: {
69
+ eng: {
70
+ language_code: 'eng',
71
+ entry_status: 'valid',
72
+ terms: [{ type: 'expression', designation: 'related concept hub', normative_status: 'preferred' }],
73
+ definition: [{ content: 'A concept that demonstrates every relationship type.' }],
74
+ },
75
+ },
76
+ });
77
+ }
78
+
79
+ function withSources(): Concept {
80
+ return Concept.fromJSON({
81
+ id: '4.1',
82
+ uri: `${BASE}/with-sources`,
83
+ status: 'valid',
84
+ sources: [
85
+ {
86
+ status: 'identical',
87
+ type: 'authoritative',
88
+ modification: 'revised 2024',
89
+ origin: {
90
+ ref: { source: 'ISO 704', id: '3.1', version: '2020' },
91
+ locality: { type: 'clause', referenceFrom: '3.1', referenceTo: '3.5' },
92
+ link: 'https://example.org/iso-704',
93
+ original: 'Original wording here.',
94
+ },
95
+ },
96
+ {
97
+ status: 'modified',
98
+ type: 'lineage',
99
+ origin: {
100
+ ref: { source: 'IEC 60050', id: '102-01', version: '2008' },
101
+ locality: { type: 'clause', referenceFrom: '102-01-01' },
102
+ },
103
+ },
104
+ {
105
+ status: 'restyled',
106
+ type: 'lineage',
107
+ origin: {
108
+ ref: { source: 'VIM', id: '1.2', version: '2012' },
109
+ },
110
+ },
111
+ ],
112
+ localizations: {
113
+ eng: {
114
+ language_code: 'eng',
115
+ entry_status: 'valid',
116
+ terms: [{ type: 'expression', designation: 'cited term', normative_status: 'preferred' }],
117
+ definition: [{ content: 'A concept that carries structured citations.' }],
118
+ sources: [
119
+ {
120
+ status: 'identical',
121
+ type: 'authoritative',
122
+ origin: {
123
+ ref: { source: 'ISO 1087', id: '2.1', version: '2019' },
124
+ locality: { type: 'clause', referenceFrom: '2.1' },
125
+ },
126
+ },
127
+ ],
128
+ },
129
+ },
130
+ });
131
+ }
132
+
133
+ function withNonVerbal(): Concept {
134
+ return Concept.fromJSON({
135
+ id: '5.1',
136
+ uri: `${BASE}/with-non-verbal`,
137
+ status: 'valid',
138
+ localizations: {
139
+ eng: {
140
+ language_code: 'eng',
141
+ entry_status: 'valid',
142
+ terms: [{ type: 'expression', designation: 'angle of repose', normative_status: 'preferred' }],
143
+ definition: [{ content: 'The steepest angle relative to the horizontal at which a material can be piled without sliding.' }],
144
+ non_verbal_rep: [
145
+ {
146
+ type: 'figure',
147
+ caption: 'Angle of repose diagram',
148
+ description: 'Schematic diagram showing the angle.',
149
+ images: [{ src: 'https://glossarist.org/figs/angle.svg' }],
150
+ },
151
+ {
152
+ type: 'formula',
153
+ caption: 'tan(θ) = μ',
154
+ },
155
+ {
156
+ type: 'table',
157
+ caption: 'Measured angles',
158
+ description: 'Empirical values across materials.',
159
+ },
160
+ ],
161
+ },
162
+ },
163
+ });
164
+ }
165
+
166
+ function withDates(): Concept {
167
+ return Concept.fromJSON({
168
+ id: '6.1',
169
+ uri: `${BASE}/with-dates`,
170
+ status: 'valid',
171
+ dates: [
172
+ { type: 'accepted', date: '2020-01-15' },
173
+ { type: 'amended', date: '2023-06-30' },
174
+ { type: 'retired', date: '2025-12-31' },
175
+ ],
176
+ localizations: {
177
+ eng: {
178
+ language_code: 'eng',
179
+ entry_status: 'valid',
180
+ terms: [{ type: 'expression', designation: 'lifecycle concept', normative_status: 'preferred' }],
181
+ definition: [{ content: 'A concept that exercises the accepted/amended/retired date types.' }],
182
+ },
183
+ },
184
+ });
185
+ }
186
+
187
+ function withdrawnConcept(): Concept {
188
+ return Concept.fromJSON({
189
+ id: '7.1',
190
+ uri: `${BASE}/withdrawn`,
191
+ status: 'withdrawn',
192
+ dates: [
193
+ { type: 'accepted', date: '2018-03-01' },
194
+ { type: 'retired', date: '2024-09-15' },
195
+ ],
196
+ localizations: {
197
+ eng: {
198
+ language_code: 'eng',
199
+ entry_status: 'withdrawn',
200
+ terms: [{ type: 'expression', designation: 'withdrawn concept', normative_status: 'preferred' }],
201
+ definition: [{ content: 'A withdrawn concept with a retirement date.' }],
202
+ },
203
+ },
204
+ });
205
+ }
206
+
207
+ export const CONCEPT_FIXTURES: readonly ConceptFixture[] = [
208
+ { name: 'minimal', description: 'one localization, one designation', uri: `${BASE}/minimal`, concept: minimal() },
209
+ { name: 'multilingual', description: 'three languages across Latn and CJK scripts', uri: `${BASE}/multilingual`, concept: multilingual() },
210
+ { name: 'full-relationships', description: 'every supported related-concept type', uri: `${BASE}/full-relationships`, concept: fullRelationships() },
211
+ { name: 'with-sources', description: 'structured citations, locality, original wording', uri: `${BASE}/with-sources`, concept: withSources() },
212
+ { name: 'with-non-verbal', description: 'figure, formula, and table non-verbal reps', uri: `${BASE}/with-non-verbal`, concept: withNonVerbal() },
213
+ { name: 'with-dates', description: 'accepted/amended/retired lifecycle', uri: `${BASE}/with-dates`, concept: withDates() },
214
+ { name: 'withdrawn', description: 'withdrawn status with retirement date (J3)', uri: `${BASE}/withdrawn`, concept: withdrawnConcept() },
215
+ ];
216
+
217
+ export function fixtureByName(name: string): ConceptFixture {
218
+ const f = CONCEPT_FIXTURES.find(f => f.name === name);
219
+ if (!f) throw new Error(`Unknown concept fixture: ${name}`);
220
+ return f;
221
+ }
@@ -0,0 +1,76 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { ConceptIdentity } from '../../adapters/concept-identity';
3
+ import { InvalidConceptIdentityError, InvalidConceptUriError } from '../../errors';
4
+
5
+ describe('ConceptIdentity', () => {
6
+ it('derives uri, slug, and path from parts', () => {
7
+ const id = new ConceptIdentity('3.1.1', 'iso-10303-2', 'https://glossarist.org');
8
+ expect(id.uri).toBe('https://glossarist.org/iso-10303-2/concept/3.1.1');
9
+ expect(id.slug).toBe('3.1.1');
10
+ expect(id.path).toBe('iso-10303-2/concepts/3.1.1');
11
+ });
12
+
13
+ it('rejects empty parts with a typed SerializationError', () => {
14
+ expect(() => new ConceptIdentity('', 'r', 'b')).toThrow(InvalidConceptIdentityError);
15
+ expect(() => new ConceptIdentity('x', '', 'b')).toThrow(InvalidConceptIdentityError);
16
+ expect(() => new ConceptIdentity('x', 'r', '')).toThrow(InvalidConceptIdentityError);
17
+ });
18
+
19
+ it('equals another identity with the same URI', () => {
20
+ const a = new ConceptIdentity('1', 'r', 'https://glossarist.org');
21
+ const b = new ConceptIdentity('1', 'r', 'https://glossarist.org');
22
+ expect(a.equals(b)).toBe(true);
23
+ });
24
+
25
+ it('does not equal an identity with a different local id', () => {
26
+ const a = new ConceptIdentity('1', 'r', 'https://glossarist.org');
27
+ const b = new ConceptIdentity('2', 'r', 'https://glossarist.org');
28
+ expect(a.equals(b)).toBe(false);
29
+ });
30
+
31
+ it('round-trips through fromUri', () => {
32
+ const original = new ConceptIdentity('3.1.1', 'iso-10303-2', 'https://glossarist.org');
33
+ const roundTrip = ConceptIdentity.fromUri(original.uri);
34
+ expect(roundTrip.equals(original)).toBe(true);
35
+ });
36
+
37
+ it('fromUri rejects non-concept URIs with a typed error', () => {
38
+ expect(() => ConceptIdentity.fromUri('https://example.org/foo/bar')).toThrow(InvalidConceptUriError);
39
+ });
40
+
41
+ it('isConceptUri recognizes canonical URIs', () => {
42
+ expect(ConceptIdentity.isConceptUri('https://glossarist.org/iso-10303-2/concept/3.1.1')).toBe(true);
43
+ expect(ConceptIdentity.isConceptUri('https://example.org')).toBe(false);
44
+ });
45
+
46
+ it('localizationUri follows the canonical sub-resource pattern', () => {
47
+ const id = new ConceptIdentity('3.1.1', 'iso-10303-2', 'https://glossarist.org');
48
+ expect(id.localizationUri('eng')).toBe('https://glossarist.org/iso-10303-2/concept/3.1.1/eng');
49
+ });
50
+
51
+ it('designationUri composes localization + desig slugs', () => {
52
+ const id = new ConceptIdentity('3.1.1', 'iso-10303-2', 'https://glossarist.org');
53
+ expect(id.designationUri('eng', 'atomic_data_unit')).toBe(
54
+ 'https://glossarist.org/iso-10303-2/concept/3.1.1/eng/desig/atomic_data_unit',
55
+ );
56
+ });
57
+
58
+ it('domainUri follows the register-scoped pattern', () => {
59
+ const id = new ConceptIdentity('3.1.1', 'iso-10303-2', 'https://glossarist.org');
60
+ expect(id.domainUri('geometry')).toBe('https://glossarist.org/iso-10303-2/domain/geometry');
61
+ });
62
+
63
+ it('toString returns the URI for logging', () => {
64
+ const id = new ConceptIdentity('3.1.1', 'iso-10303-2', 'https://glossarist.org');
65
+ expect(`${id}`).toBe('https://glossarist.org/iso-10303-2/concept/3.1.1');
66
+ });
67
+
68
+ it('toJSON serializes the parts', () => {
69
+ const id = new ConceptIdentity('3.1.1', 'iso-10303-2', 'https://glossarist.org');
70
+ expect(id.toJSON()).toEqual({
71
+ localId: '3.1.1',
72
+ registerId: 'iso-10303-2',
73
+ uriBase: 'https://glossarist.org',
74
+ });
75
+ });
76
+ });
@@ -0,0 +1,109 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { mount } from '@vue/test-utils';
3
+ import { h, defineComponent, ref, computed } from 'vue';
4
+ import ErrorBoundary from '../../components/ErrorBoundary.vue';
5
+ import { ConceptNotFoundError, GlossaristError } from '../../errors';
6
+
7
+ const Bomb = defineComponent({
8
+ name: 'Bomb',
9
+ props: { error: { type: Object as () => Error | undefined, default: undefined } },
10
+ setup(props) {
11
+ return () => {
12
+ if (props.error) throw props.error;
13
+ return h('p', 'all good');
14
+ };
15
+ },
16
+ });
17
+
18
+ function mountBoundary(initialError?: Error) {
19
+ const holder = ref<Error | undefined>(initialError);
20
+ const wrapper = mount(ErrorBoundary, {
21
+ props: {},
22
+ slots: { default: () => h(Bomb, { error: holder.value }) },
23
+ });
24
+ return { wrapper, holder };
25
+ }
26
+
27
+ describe('ErrorBoundary', () => {
28
+ it('renders the slot when no error is thrown', () => {
29
+ const wrapper = mount(ErrorBoundary, {
30
+ props: {},
31
+ slots: { default: () => h('p', 'all good') },
32
+ });
33
+ expect(wrapper.text()).toContain('all good');
34
+ expect(wrapper.find('[role="alert"]').exists()).toBe(false);
35
+ });
36
+
37
+ it('catches a child error and renders the fallback', async () => {
38
+ const { wrapper, holder } = mountBoundary();
39
+ holder.value = new Error('boom');
40
+ await wrapper.vm.$nextTick();
41
+ expect(wrapper.text()).toContain('boom');
42
+ expect(wrapper.find('[role="alert"]').exists()).toBe(true);
43
+ });
44
+
45
+ it('uses the custom title when provided', async () => {
46
+ const wrapper = mount(ErrorBoundary, {
47
+ props: { title: 'Concept failed' },
48
+ slots: { default: () => h('p', 'ok') },
49
+ });
50
+ expect(wrapper.find('h3').exists()).toBe(false);
51
+
52
+ const holder = ref<Error | undefined>();
53
+ const w2 = mount(ErrorBoundary, {
54
+ props: { title: 'Concept failed' },
55
+ slots: { default: () => h(Bomb, { error: holder.value }) },
56
+ });
57
+ holder.value = new Error('x');
58
+ await w2.vm.$nextTick();
59
+ expect(w2.find('h3').text()).toBe('Concept failed');
60
+ });
61
+
62
+ it('emits an error event with the captured value', async () => {
63
+ const { wrapper, holder } = mountBoundary();
64
+ holder.value = new Error('boom');
65
+ await wrapper.vm.$nextTick();
66
+ const evt = wrapper.emitted('error');
67
+ expect(evt).toBeDefined();
68
+ expect(evt?.[0]?.[0]).toBeInstanceOf(Error);
69
+ });
70
+
71
+ it('shows details block for GlossaristError instances', async () => {
72
+ const holder = ref<Error | undefined>();
73
+ const wrapper = mount(ErrorBoundary, {
74
+ props: {},
75
+ slots: { default: () => h(Bomb, { error: holder.value }) },
76
+ });
77
+ holder.value = ConceptNotFoundError.make('iso1', '3.1.1');
78
+ await wrapper.vm.$nextTick();
79
+ expect(wrapper.find('details').exists()).toBe(true);
80
+ expect(wrapper.text()).toContain('ConceptNotFoundError');
81
+ });
82
+
83
+ it('hides details for plain Errors (no context to show)', async () => {
84
+ const holder = ref<Error | undefined>();
85
+ const wrapper = mount(ErrorBoundary, {
86
+ props: {},
87
+ slots: { default: () => h(Bomb, { error: holder.value }) },
88
+ });
89
+ holder.value = new Error('plain');
90
+ await wrapper.vm.$nextTick();
91
+ expect(wrapper.find('details').exists()).toBe(false);
92
+ });
93
+
94
+ it('exposes a retryKey data attribute when provided', async () => {
95
+ const holder = ref<Error | undefined>();
96
+ const wrapper = mount(ErrorBoundary, {
97
+ props: { retryKey: 'concept-3.1.1' },
98
+ slots: { default: () => h(Bomb, { error: holder.value }) },
99
+ });
100
+ holder.value = new Error('boom');
101
+ await wrapper.vm.$nextTick();
102
+ expect(wrapper.find('[data-retry-key="concept-3.1.1"]').exists()).toBe(true);
103
+ });
104
+
105
+ it('GlossaristError is recognized as GlossaristError (sanity)', () => {
106
+ const err = ConceptNotFoundError.make('r', 'c');
107
+ expect(err).toBeInstanceOf(GlossaristError);
108
+ });
109
+ });
@@ -0,0 +1,262 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { extractYear, deriveSeriesKey, groupManifestsIntoSeries } from '../../composables/useDatasetSeries';
3
+ import type { Manifest } from '../../adapters/types';
4
+
5
+ function makeManifest(overrides: Partial<Manifest> = {}): Manifest {
6
+ return {
7
+ id: 'test',
8
+ title: 'Test',
9
+ ref: 'TEST',
10
+ conceptCount: 0,
11
+ languages: ['eng'],
12
+ sections: [],
13
+ status: 'valid',
14
+ schemaVersion: '1.0',
15
+ lastUpdated: '2026-01-01',
16
+ ...overrides,
17
+ } as unknown as Manifest;
18
+ }
19
+
20
+ describe('extractYear — ISO standard references', () => {
21
+ it('extracts the year from `:YYYY` ISO references', () => {
22
+ expect(extractYear('ISO 10241-1:2011')).toBe(2011);
23
+ expect(extractYear('ISO/IEC 11179-1:2015')).toBe(2015);
24
+ expect(extractYear('ISO 19115:2003')).toBe(2003);
25
+ expect(extractYear('IEC 60050-102:2017')).toBe(2017);
26
+ });
27
+
28
+ it('extracts the year from `:YYYYa` ISO references (revision letter)', () => {
29
+ expect(extractYear('ISO 123:2020a')).toBe(2020);
30
+ expect(extractYear('ISO 123:2018A')).toBe(2018);
31
+ });
32
+
33
+ it('does NOT match a 4-digit standard number without a colon', () => {
34
+ expect(extractYear('ISO 10241')).toBeUndefined();
35
+ expect(extractYear('ISO 19115')).toBeUndefined();
36
+ expect(extractYear('IEC 60050')).toBeUndefined();
37
+ });
38
+ });
39
+
40
+ describe('extractYear — naming-convention suffix', () => {
41
+ it('extracts the year from `<name>-YYYY` suffix', () => {
42
+ expect(extractYear('viml-2022')).toBe(2022);
43
+ expect(extractYear('viml-1968')).toBe(1968);
44
+ });
45
+
46
+ it('extracts the year from `<name>_YYYY` suffix', () => {
47
+ expect(extractYear('viml_2022')).toBe(2022);
48
+ });
49
+
50
+ it('extracts the year from `<name> YYYY` suffix', () => {
51
+ expect(extractYear('VIML 1968')).toBe(1968);
52
+ });
53
+
54
+ it('extracts the year with revision letter', () => {
55
+ expect(extractYear('viml-2022a')).toBe(2022);
56
+ expect(extractYear('viml-2022A')).toBe(2022);
57
+ });
58
+ });
59
+
60
+ describe('extractYear — bare year', () => {
61
+ it('extracts a bare 4-digit year as the entire string', () => {
62
+ expect(extractYear('2022')).toBe(2022);
63
+ expect(extractYear(' 1968 ')).toBe(1968);
64
+ });
65
+ });
66
+
67
+ describe('extractYear — rejection cases', () => {
68
+ it('returns undefined for strings with no year', () => {
69
+ expect(extractYear('VIM')).toBeUndefined();
70
+ expect(extractYear('ISO')).toBeUndefined();
71
+ expect(extractYear('')).toBeUndefined();
72
+ });
73
+
74
+ it('returns undefined for 4-digit runs that are out of range', () => {
75
+ expect(extractYear('file-1800')).toBeUndefined();
76
+ expect(extractYear('file-2200')).toBeUndefined();
77
+ });
78
+
79
+ it('handles ambiguous strings by preferring ISO `:YYYY` form', () => {
80
+ expect(extractYear('ISO 704:2020 and ISO 10241:2011')).toBe(2011);
81
+ });
82
+ });
83
+
84
+ describe('deriveSeriesKey', () => {
85
+ it('strips trailing -YYYY', () => {
86
+ expect(deriveSeriesKey('viml-2022')).toBe('viml');
87
+ expect(deriveSeriesKey('viml-1968')).toBe('viml');
88
+ });
89
+
90
+ it('strips trailing _YYYY', () => {
91
+ expect(deriveSeriesKey('viml_2022')).toBe('viml');
92
+ });
93
+
94
+ it('strips trailing :YYYY', () => {
95
+ expect(deriveSeriesKey('iso-704:2020')).toBe('iso-704');
96
+ });
97
+
98
+ it('preserves keys without year suffix', () => {
99
+ expect(deriveSeriesKey('iso-704')).toBe('iso-704');
100
+ expect(deriveSeriesKey('viml')).toBe('viml');
101
+ });
102
+
103
+ it('handles year+letter suffix', () => {
104
+ expect(deriveSeriesKey('viml-2022a')).toBe('viml');
105
+ });
106
+ });
107
+
108
+ describe('groupManifestsIntoSeries', () => {
109
+ describe('strategy 1 — config-driven lineage series', () => {
110
+ it('uses config when at least one lineage group is configured', () => {
111
+ const manifests = [
112
+ makeManifest({ id: 'viml-2022', status: 'valid' }),
113
+ makeManifest({ id: 'viml-2013', status: 'valid' }),
114
+ ];
115
+ const configured = [{
116
+ id: 'viml',
117
+ label: 'VIML',
118
+ datasets: ['viml-2022', 'viml-2013'],
119
+ kind: 'lineage' as const,
120
+ }];
121
+ const series = groupManifestsIntoSeries(manifests, undefined, configured);
122
+ expect(series.length).toBe(1);
123
+ expect(series[0].key).toBe('viml');
124
+ expect(series[0].members.length).toBe(2);
125
+ expect(series[0].configured).toBe(true);
126
+ });
127
+
128
+ it('includes stub members for datasets whose manifest is not loaded', () => {
129
+ const manifests = [makeManifest({ id: 'viml-2022', status: 'valid' })];
130
+ const configured = [{
131
+ id: 'viml',
132
+ datasets: ['viml-2022', 'viml-2013', 'viml-1968'],
133
+ kind: 'lineage' as const,
134
+ }];
135
+ const series = groupManifestsIntoSeries(manifests, undefined, configured);
136
+ expect(series[0].members.length).toBe(3);
137
+ });
138
+
139
+ it('honors explicit current from config', () => {
140
+ const manifests = [
141
+ makeManifest({ id: 'viml-2022', status: 'valid' }),
142
+ makeManifest({ id: 'viml-2013', status: 'valid' }),
143
+ ];
144
+ const configured = [{
145
+ id: 'viml',
146
+ datasets: ['viml-2013', 'viml-2022'],
147
+ kind: 'lineage' as const,
148
+ current: 'viml-2013',
149
+ }];
150
+ const series = groupManifestsIntoSeries(manifests, undefined, configured);
151
+ expect(series[0].current?.id).toBe('viml-2013');
152
+ });
153
+
154
+ it('falls back to newest valid member when current is unset', () => {
155
+ const manifests = [
156
+ makeManifest({ id: 'viml-2022', status: 'valid' }),
157
+ makeManifest({ id: 'viml-2013', status: 'valid' }),
158
+ ];
159
+ const configured = [{
160
+ id: 'viml',
161
+ datasets: ['viml-2013', 'viml-2022'],
162
+ kind: 'lineage' as const,
163
+ }];
164
+ const series = groupManifestsIntoSeries(manifests, undefined, configured);
165
+ expect(series[0].current?.id).toBe('viml-2022');
166
+ });
167
+
168
+ it('sorts members by year ascending', () => {
169
+ const manifests = [
170
+ makeManifest({ id: 'viml-2022', status: 'valid' }),
171
+ makeManifest({ id: 'viml-1968', status: 'withdrawn' }),
172
+ makeManifest({ id: 'viml-2013', status: 'valid' }),
173
+ ];
174
+ const configured = [{
175
+ id: 'viml',
176
+ datasets: ['viml-2022', 'viml-1968', 'viml-2013'],
177
+ kind: 'lineage' as const,
178
+ }];
179
+ const series = groupManifestsIntoSeries(manifests, undefined, configured);
180
+ const years = series[0].members.map(m => m.year);
181
+ expect(years).toEqual([1968, 2013, 2022]);
182
+ });
183
+ });
184
+
185
+ describe('strategy 2 — auto-derive by naming convention', () => {
186
+ it('groups by stripped key when no config is provided', () => {
187
+ const manifests = [
188
+ makeManifest({ id: 'viml-2022', status: 'valid' }),
189
+ makeManifest({ id: 'viml-2013', status: 'valid' }),
190
+ makeManifest({ id: 'iso-geodetic', status: 'valid' }),
191
+ ];
192
+ const series = groupManifestsIntoSeries(manifests, undefined, []);
193
+ expect(series.length).toBe(2);
194
+ const viml = series.find(s => s.key === 'viml');
195
+ expect(viml?.members.length).toBe(2);
196
+ expect(viml?.configured).toBe(false);
197
+ });
198
+
199
+ it('sorts groups alphabetically', () => {
200
+ const manifests = [
201
+ makeManifest({ id: 'viml-2022' }),
202
+ makeManifest({ id: 'iso-geodetic' }),
203
+ ];
204
+ const series = groupManifestsIntoSeries(manifests, undefined, []);
205
+ expect(series.map(s => s.key)).toEqual(['iso-geodetic', 'viml']);
206
+ });
207
+
208
+ it('picks newest valid as current in auto-derived mode', () => {
209
+ const manifests = [
210
+ makeManifest({ id: 'viml-2013', status: 'valid' }),
211
+ makeManifest({ id: 'viml-2022', status: 'valid' }),
212
+ ];
213
+ const series = groupManifestsIntoSeries(manifests, undefined, []);
214
+ expect(series[0].current?.id).toBe('viml-2022');
215
+ });
216
+ });
217
+
218
+ describe('strategy selection', () => {
219
+ it('picks strategy 1 when at least one configured lineage group exists', () => {
220
+ const manifests = [
221
+ makeManifest({ id: 'viml-2022' }),
222
+ makeManifest({ id: 'viml-2013' }),
223
+ makeManifest({ id: 'iso-geodetic' }),
224
+ ];
225
+ const configured = [{
226
+ id: 'viml',
227
+ datasets: ['viml-2022', 'viml-2013'],
228
+ kind: 'lineage' as const,
229
+ }];
230
+ const series = groupManifestsIntoSeries(manifests, undefined, configured);
231
+ // Strategy 1 → only `viml` group, no auto-derive
232
+ expect(series.length).toBe(1);
233
+ expect(series[0].key).toBe('viml');
234
+ });
235
+
236
+ it('falls through to strategy 2 when no configured lineage groups exist', () => {
237
+ const manifests = [
238
+ makeManifest({ id: 'viml-2022' }),
239
+ makeManifest({ id: 'viml-2013' }),
240
+ ];
241
+ const configured = [{
242
+ id: 'topic-group',
243
+ datasets: ['viml-2022', 'viml-2013'],
244
+ kind: 'topic' as const,
245
+ }];
246
+ const series = groupManifestsIntoSeries(manifests, undefined, configured);
247
+ expect(series.some(s => !s.configured)).toBe(true);
248
+ });
249
+ });
250
+
251
+ describe('edge cases', () => {
252
+ it('empty manifests array returns empty series array', () => {
253
+ expect(groupManifestsIntoSeries([], undefined, [])).toEqual([]);
254
+ });
255
+
256
+ it('empty config falls through to strategy 2', () => {
257
+ const manifests = [makeManifest({ id: 'viml-2022' }), makeManifest({ id: 'viml-2013' })];
258
+ const series = groupManifestsIntoSeries(manifests, undefined, []);
259
+ expect(series[0].key).toBe('viml');
260
+ });
261
+ });
262
+ });