@glossarist/concept-browser 0.7.96 → 0.7.97

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.96",
3
+ "version": "0.7.97",
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": {
@@ -0,0 +1,112 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { computed } 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 { conceptFromJson } from '../../adapters/model-bridge';
7
+ import type { Manifest, GraphEdge } from '../../adapters/types';
8
+ import { makeManifest, setupPinia } from '../test-helpers';
9
+ import { getFactory, resetFactory } from '../../adapters/factory';
10
+
11
+ /**
12
+ * End-to-end pipeline test using the actual live JSON-LD shape from
13
+ * the deployed oimlsmart/vocab site (vim-2012 concept 1.9).
14
+ *
15
+ * Reproduces the live bug: 1.9 has gl:genericRelations but the sphere
16
+ * renders nothing because conceptGenericRelations is empty.
17
+ */
18
+ describe('1.9 live pipeline — generic projection through model-bridge', () => {
19
+ const URN = 'urn:oiml:pub:v:2:2012';
20
+ const manifest: Manifest = makeManifest({
21
+ id: 'vim-2012',
22
+ datasetUri: URN,
23
+ uriBase: 'https://oimlsmart.github.io/vocab',
24
+ });
25
+ const noEdges: GraphEdge[] = [];
26
+ const noopRouter = { push() {} } as unknown as Router;
27
+
28
+ let store: ReturnType<typeof useVocabularyStore>;
29
+
30
+ beforeEach(() => {
31
+ setupPinia();
32
+ resetFactory();
33
+ store = useVocabularyStore();
34
+ store.manifests.set(manifest.id, manifest);
35
+
36
+ const factory = getFactory();
37
+ factory.resolveRelatedRef = vi.fn((ref: { source: string | null; id: string | null } | null) =>
38
+ ref?.source === URN && ref.id
39
+ ? { registerId: manifest.id, conceptId: ref.id }
40
+ : null,
41
+ );
42
+ });
43
+
44
+ it('projects gl:genericRelations → conceptGenericRelations through conceptFromJson', () => {
45
+ const liveJson = {
46
+ '@context': 'https://glossarist.org/ns/context.jsonld',
47
+ '@id': 'https://oimlsmart.github.io/vocab/vim-2012/concept/1.9',
48
+ '@type': 'gl:Concept',
49
+ 'gl:identifier': '1.9',
50
+ 'gl:status': 'valid',
51
+ 'gl:localizedConcept': {
52
+ eng: {
53
+ '@type': 'gl:LocalizedConcept',
54
+ 'gl:languageCode': 'eng',
55
+ 'gl:entryStatus': 'valid',
56
+ 'gl:designation': [{ '@type': 'gl:Expression', 'gl:normativeStatus': 'preferred', 'gl:term': 'measurement unit' }],
57
+ 'gl:definition': [{ '@type': 'gl:DetailedDefinition', 'gl:content': 'real scalar quantity' }],
58
+ },
59
+ },
60
+ 'gl:related': [
61
+ { '@type': 'gl:RelatedConcept', 'gl:relationshipType': 'broader_generic',
62
+ 'gl:ref': { '@type': 'gl:ConceptRef', 'gl:source': URN, 'gl:id': '1.12' } },
63
+ ],
64
+ 'gl:genericRelations': [
65
+ {
66
+ '@type': 'gl:GenericRelation',
67
+ 'gl:comprehensive': { '@type': 'gl:ConceptRef', 'gl:source': URN, 'gl:id': '1.9' },
68
+ 'gl:criterion': { eng: 'by magnitude relationship to a reference measurement unit' },
69
+ 'gl:hasGeneric': [
70
+ { '@type': 'gl:GenericMember',
71
+ 'gl:ref': { '@type': 'gl:ConceptRef', 'gl:source': URN, 'gl:id': '1.17' },
72
+ 'gl:presence': 'required', 'gl:count': 'at_least_one',
73
+ 'gl:delimitingCharacteristic': { eng: 'multiple of a unit' } },
74
+ { '@type': 'gl:GenericMember',
75
+ 'gl:ref': { '@type': 'gl:ConceptRef', 'gl:source': URN, 'gl:id': '1.18' },
76
+ 'gl:presence': 'required', 'gl:count': 'at_least_one',
77
+ 'gl:delimitingCharacteristic': { eng: 'submultiple of a unit' } },
78
+ { '@type': 'gl:GenericMember',
79
+ 'gl:ref': { '@type': 'gl:ConceptRef', 'gl:source': URN, 'gl:id': '1.12' },
80
+ 'gl:presence': 'required', 'gl:count': 'at_least_one',
81
+ 'gl:delimitingCharacteristic': { eng: 'coherent derived unit' } },
82
+ ],
83
+ 'gl:completeness': 'partial',
84
+ },
85
+ ],
86
+ };
87
+
88
+ const concept = conceptFromJson(liveJson as any);
89
+
90
+ /* Sanity: model-bridge produced a Concept with relations. */
91
+ expect(concept).toBeTruthy();
92
+
93
+ /* Project through useConceptEdges, exactly as ConceptView does. */
94
+ const { conceptGenericRelations } = useConceptEdges(
95
+ computed(() => concept),
96
+ computed(() => 'vim-2012'),
97
+ computed(() => manifest),
98
+ computed(() => noEdges),
99
+ noopRouter,
100
+ );
101
+
102
+ const rels = conceptGenericRelations.value;
103
+ /* This is the live-bug assertion: must NOT be empty. */
104
+ expect(rels).toHaveLength(1);
105
+ expect(rels[0].members).toHaveLength(3);
106
+ expect(rels[0].members.map(m => m.uri)).toEqual([
107
+ 'https://oimlsmart.github.io/vocab/vim-2012/concept/1.17',
108
+ 'https://oimlsmart.github.io/vocab/vim-2012/concept/1.18',
109
+ 'https://oimlsmart.github.io/vocab/vim-2012/concept/1.12',
110
+ ]);
111
+ });
112
+ });
@@ -141,6 +141,8 @@ export interface JsonLdPartitiveMember {
141
141
  'gl:count'?: string;
142
142
  /** ISO 704:2022: delimiting part (3× stroke width in diagrams) */
143
143
  'gl:isDelimiting'?: boolean;
144
+ /** ISO 704:2022 §5.5.4.2.1: GenericMember delimiting characteristic (required on generic members) */
145
+ 'gl:delimitingCharacteristic'?: Record<string, string> | string;
144
146
  /** Legacy v3 field — one-string multiplicity, split into presence+count */
145
147
  'gl:multiplicity'?: string;
146
148
  /** Legacy v2 field — migrated to presence+count */
@@ -516,6 +516,59 @@ function mapPartitiveRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<stri
516
516
  return out;
517
517
  }
518
518
 
519
+ /**
520
+ * Map a GenericRelation JSON-LD entry to the dict shape that
521
+ * glossarist-js's GenericHyperedge constructor expects.
522
+ *
523
+ * Mirrors mapPartitiveRelationFromJsonLd but reads `gl:hasGeneric` and
524
+ * carries each member's `gl:delimitingCharacteristic` (ISO 704:2022
525
+ * §5.5.4.2.1 — required on every GenericMember).
526
+ */
527
+ function mapGenericRelationFromJsonLd(r: JsonLdPartitiveRelation): Record<string, unknown> | null {
528
+ const comprehensive = r[GL.COMPREHENSIVE] ? mapRefFromJsonLd(r[GL.COMPREHENSIVE]) : null;
529
+ if (!comprehensive) return null;
530
+
531
+ const members = (r[GL.HAS_GENERIC] ?? [])
532
+ .map((m): Record<string, unknown> | null => {
533
+ const ref = m[GL.REF] ? mapRefFromJsonLd(m[GL.REF]) : null;
534
+ if (!ref) return null;
535
+ const out: Record<string, unknown> = { ref };
536
+ const presence = m[GL.PRESENCE];
537
+ const count = m[GL.COUNT];
538
+ if (isPartitivePresence(presence) && isPartitiveCount(count)) {
539
+ out.presence = presence;
540
+ out.count = count;
541
+ } else {
542
+ const raw = m[GL.MULTIPLICITY] ?? splitLegacyCertainty(m[GL.CERTAINTY]);
543
+ if (raw) {
544
+ const parts = splitMultiplicity(raw);
545
+ out.presence = parts.presence;
546
+ out.count = parts.count;
547
+ }
548
+ }
549
+ const dc = m[GL.DELIMITING_CHARACTERISTIC];
550
+ if (dc) {
551
+ out.delimitingCharacteristic = typeof dc === 'string' ? { default: dc } : dc;
552
+ }
553
+ return out;
554
+ })
555
+ .filter((m): m is Record<string, unknown> => m !== null);
556
+
557
+ if (members.length < 2) return null;
558
+
559
+ const out: Record<string, unknown> = { comprehensive, members };
560
+
561
+ if (r[GL.COMPLETENESS]) out.completeness = r[GL.COMPLETENESS];
562
+
563
+ if (r[GL.CRITERION]) {
564
+ out.criterion = typeof r[GL.CRITERION] === 'string'
565
+ ? { default: r[GL.CRITERION] }
566
+ : r[GL.CRITERION];
567
+ }
568
+
569
+ return out;
570
+ }
571
+
519
572
  /** Migrate legacy v2 certainty → old v3 multiplicity string. */
520
573
  function splitLegacyCertainty(certainty: string | undefined): string | null {
521
574
  if (!certainty) return null;
@@ -549,6 +602,9 @@ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
549
602
  const partitiveRelations = (doc[GL.PARTITIVE_RELATIONS] ?? [])
550
603
  .map(mapPartitiveRelationFromJsonLd)
551
604
  .filter((r): r is Record<string, unknown> => r !== null);
605
+ const genericRelations = (doc[GL.GENERIC_RELATIONS] ?? [])
606
+ .map(mapGenericRelationFromJsonLd)
607
+ .filter((r): r is Record<string, unknown> => r !== null);
552
608
  const tagsArr = doc[GL.TAGS];
553
609
  const tags = Array.isArray(tagsArr) ? [...tagsArr] : [];
554
610
 
@@ -559,6 +615,7 @@ function conceptFromJsonLd(doc: JsonLdConcept): Concept {
559
615
  localizations,
560
616
  related,
561
617
  partitive_relations: partitiveRelations,
618
+ generic_relations: genericRelations,
562
619
  tags,
563
620
  figures: normalizeEntityRefs(doc[GL.FIGURE_REF]),
564
621
  tables: normalizeEntityRefs(doc[GL.TABLE_REF]),
@@ -1078,7 +1078,7 @@ const legendItems = computed(() => {
1078
1078
  if (partitiveCount > 0) {
1079
1079
  typeMap.set('__partitive__', {
1080
1080
  type: '__partitive__',
1081
- label: 'partitive relation',
1081
+ label: 'Partitive Relation',
1082
1082
  color: uiStore.isDark ? '#2dd4bf' : '#0d9488',
1083
1083
  count: partitiveCount,
1084
1084
  });
@@ -1088,7 +1088,7 @@ const legendItems = computed(() => {
1088
1088
  if (genericCount > 0) {
1089
1089
  typeMap.set('__generic__', {
1090
1090
  type: '__generic__',
1091
- label: 'generic relation',
1091
+ label: 'General Relation',
1092
1092
  color: uiStore.isDark ? '#fbbf24' : '#b45309',
1093
1093
  count: genericCount,
1094
1094
  });