@glossarist/concept-browser 0.7.50 → 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 (170) 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/figure-bridge.ts +22 -23
  86. package/src/adapters/non-verbal/formula-bridge.ts +11 -9
  87. package/src/adapters/non-verbal/glossarist-augment.d.ts +133 -0
  88. package/src/adapters/non-verbal/index.ts +12 -9
  89. package/src/adapters/non-verbal/kind.ts +2 -1
  90. package/src/adapters/non-verbal/table-bridge.ts +12 -10
  91. package/src/adapters/non-verbal/types.ts +36 -54
  92. package/src/adapters/non-verbal-resolver.ts +6 -3
  93. package/src/components/AppSidebar.vue +189 -93
  94. package/src/components/ConceptDetail.vue +8 -0
  95. package/src/components/ConceptEditionRail.vue +222 -0
  96. package/src/components/ConceptRdfView.vue +37 -377
  97. package/src/components/DatasetSeriesCard.vue +270 -0
  98. package/src/components/ErrorBoundary.vue +95 -0
  99. package/src/components/FormatDownloads.vue +17 -13
  100. package/src/components/HomeSeriesSection.vue +277 -0
  101. package/src/components/NonVerbalRepDisplay.vue +2 -2
  102. package/src/components/RelationSphere.vue +1672 -0
  103. package/src/components/SidebarSeriesSection.vue +239 -0
  104. package/src/components/concept-rdf/RdfInstanceHeader.vue +47 -0
  105. package/src/components/concept-rdf/RdfInstanceSection.vue +54 -0
  106. package/src/components/concept-rdf/RdfPrefixLegend.vue +27 -0
  107. package/src/components/concept-rdf/RdfSourcePanel.vue +72 -0
  108. package/src/components/concept-rdf/agents-emitter.ts +82 -0
  109. package/src/components/concept-rdf/bibliography-emitter.ts +83 -0
  110. package/src/components/concept-rdf/build-activity-emitter.ts +89 -0
  111. package/src/components/concept-rdf/concept-emitter.ts +443 -0
  112. package/src/components/concept-rdf/dataset-emitter.ts +95 -0
  113. package/src/components/concept-rdf/group-emitter.ts +69 -0
  114. package/src/components/concept-rdf/image-variant-emitter.ts +46 -0
  115. package/src/components/concept-rdf/jsonld-writer.ts +82 -0
  116. package/src/components/concept-rdf/predicates.ts +261 -0
  117. package/src/components/concept-rdf/provenance.ts +80 -0
  118. package/src/components/concept-rdf/rdf-graph.ts +211 -0
  119. package/src/components/concept-rdf/rdf-prefixes.ts +23 -0
  120. package/src/components/concept-rdf/sections-builder.ts +62 -0
  121. package/src/components/concept-rdf/table-formula-emitter.ts +101 -0
  122. package/src/components/concept-rdf/turtle-writer.ts +116 -0
  123. package/src/components/concept-rdf/use-rdf-document.ts +72 -0
  124. package/src/components/concept-rdf/version-emitter.ts +65 -0
  125. package/src/components/concept-rdf/vocabulary-emitter.ts +62 -0
  126. package/src/components/figure/FigureDisplay.vue +16 -15
  127. package/src/components/figure/FigureImages.vue +38 -16
  128. package/src/components/figure/figure-image-pick.ts +1 -1
  129. package/src/components/figure/figure-layout.ts +1 -1
  130. package/src/components/formula/FormulaDisplay.vue +11 -9
  131. package/src/components/formula/FormulaExpression.vue +4 -4
  132. package/src/components/non-verbal/NonVerbalCaption.vue +5 -5
  133. package/src/components/non-verbal/NonVerbalSources.vue +3 -11
  134. package/src/components/table/TableDisplay.vue +6 -4
  135. package/src/components/table/TableMarkup.vue +1 -1
  136. package/src/composables/use-color-theme.ts +82 -0
  137. package/src/composables/use-format-registry.ts +42 -0
  138. package/src/composables/use-non-verbal-entity.ts +2 -1
  139. package/src/composables/useDatasetSeries.ts +258 -0
  140. package/src/composables/useSphereProjection.ts +125 -0
  141. package/src/config/group-types.ts +92 -0
  142. package/src/config/types.ts +81 -2
  143. package/src/config/use-site-config.ts +2 -1
  144. package/src/errors.ts +136 -0
  145. package/src/i18n/locales/eng.yml +24 -0
  146. package/src/i18n/locales/fra.yml +24 -0
  147. package/src/stores/vocabulary.ts +3 -1
  148. package/src/style.css +17 -2
  149. package/src/types/agents-version-turtle.d.ts +27 -0
  150. package/src/types/bibliography-turtle.d.ts +12 -0
  151. package/src/types/build-activity-turtle.d.ts +16 -0
  152. package/src/types/build-cache.d.ts +20 -0
  153. package/src/types/dataset-turtle.d.ts +32 -0
  154. package/src/types/normalize-yaml.d.ts +16 -0
  155. package/src/types/turtle-escape.d.ts +6 -0
  156. package/src/types/vocab-turtle.d.ts +13 -0
  157. package/src/utils/asciidoc-lite.ts +11 -6
  158. package/src/utils/bcp47.ts +141 -0
  159. package/src/utils/color-theme-integration.ts +11 -0
  160. package/src/utils/color-theme.ts +129 -0
  161. package/src/utils/dataset-style.ts +31 -6
  162. package/src/utils/locale.ts +6 -14
  163. package/src/utils/markdown-lite.ts +6 -1
  164. package/src/utils/relation-sphere-styling.ts +63 -0
  165. package/src/utils/relationship-categories.ts +30 -0
  166. package/src/utils/url-safety.ts +30 -0
  167. package/src/views/ConceptView.vue +183 -9
  168. package/src/views/DatasetView.vue +6 -0
  169. package/src/views/HomeView.vue +5 -0
  170. package/vite.config.ts +7 -0
@@ -0,0 +1,110 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Parser, Store } from 'n3';
3
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
4
+ import {
5
+ agentsFromContributors,
6
+ emitAgentsGraph,
7
+ } from '../../components/concept-rdf/agents-emitter';
8
+
9
+ const FOAF = 'http://xmlns.com/foaf/0.1/';
10
+ const PROV = 'http://www.w3.org/ns/prov#';
11
+ const DCTERMS = 'http://purl.org/dc/terms/';
12
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
13
+
14
+ function parse(turtle: string): Store {
15
+ const parser = new Parser({ format: 'Turtle' });
16
+ const store = new Store();
17
+ store.addQuads(parser.parse(turtle));
18
+ return store;
19
+ }
20
+
21
+ describe('agentsFromContributors', () => {
22
+ it('turns each contributor into an AgentInput with a slugified IRI', () => {
23
+ const agents = agentsFromContributors([
24
+ { name: 'Ada Lovelace', role: 'Editor', organization: 'Royal Society', email: 'ada@example.org' },
25
+ { name: 'Grace Hopper', organization: 'USN' },
26
+ ]);
27
+ expect(agents.length).toBe(2);
28
+ expect(agents[0].agentIri).toBe('https://glossarist.org/agent/ada-lovelace');
29
+ expect(agents[1].agentIri).toBe('https://glossarist.org/agent/grace-hopper');
30
+ expect(agents[0].role).toBe('Editor');
31
+ });
32
+
33
+ it('deduplicates organizations by slug', () => {
34
+ const result = emitAgentsGraph(agentsFromContributors([
35
+ { name: 'Alice', organization: 'ISO' },
36
+ { name: 'Bob', organization: 'ISO' },
37
+ { name: 'Carol', organization: 'IEC' },
38
+ ]));
39
+ expect(result.organizationIris.length).toBe(2);
40
+ });
41
+ });
42
+
43
+ describe('emitAgentsGraph — J4 agent records', () => {
44
+ it('types each person as foaf:Person, prov:Person, prov:Agent', () => {
45
+ const graph = emitAgentsGraph(agentsFromContributors([
46
+ { name: 'Ada Lovelace', email: 'ada@example.org' },
47
+ ]));
48
+ const store = parse(writeTurtle(graph.graph));
49
+ const types = store.getObjects('https://glossarist.org/agent/ada-lovelace', RDF_TYPE, null).map(q => q.value);
50
+ expect(types).toContain(`${FOAF}Person`);
51
+ expect(types).toContain(`${PROV}Person`);
52
+ expect(types).toContain(`${PROV}Agent`);
53
+ });
54
+
55
+ it('records name, email (as mailto IRI), role, and url', () => {
56
+ const graph = emitAgentsGraph(agentsFromContributors([
57
+ { name: 'Ada Lovelace', role: 'Editor', email: 'ada@example.org', url: 'https://example.org/ada' },
58
+ ]));
59
+ const store = parse(writeTurtle(graph.graph));
60
+ const names = store.getObjects('https://glossarist.org/agent/ada-lovelace', `${FOAF}name`, null).map(q => q.value);
61
+ expect(names).toContain('Ada Lovelace');
62
+
63
+ const mboxes = store.getObjects('https://glossarist.org/agent/ada-lovelace', `${FOAF}mbox`, null).map(q => q.value);
64
+ expect(mboxes).toContain('mailto:ada@example.org');
65
+
66
+ const roles = store.getObjects('https://glossarist.org/agent/ada-lovelace', `${DCTERMS}description`, null).map(q => q.value);
67
+ expect(roles).toContain('Editor');
68
+
69
+ const seeAlso = store.getObjects('https://glossarist.org/agent/ada-lovelace', 'http://www.w3.org/2000/01/rdf-schema#seeAlso', null).map(q => q.value);
70
+ expect(seeAlso).toContain('https://example.org/ada');
71
+ });
72
+
73
+ it('creates a single prov:Organization per unique organization', () => {
74
+ const result = emitAgentsGraph(agentsFromContributors([
75
+ { name: 'Alice', organization: 'ISO' },
76
+ { name: 'Bob', organization: 'ISO' },
77
+ ]));
78
+ expect(result.organizationIris).toEqual(['https://glossarist.org/org/iso']);
79
+
80
+ const store = parse(writeTurtle(result.graph));
81
+ const types = store.getObjects('https://glossarist.org/org/iso', RDF_TYPE, null).map(q => q.value);
82
+ expect(types).toContain(`${FOAF}Organization`);
83
+ expect(types).toContain(`${PROV}Organization`);
84
+ expect(types).toContain(`${PROV}Agent`);
85
+
86
+ const names = store.getObjects('https://glossarist.org/org/iso', `${FOAF}name`, null).map(q => q.value);
87
+ expect(names).toContain('ISO');
88
+ });
89
+
90
+ it('links every person to their organization via prov:actedOnBehalfOf', () => {
91
+ const result = emitAgentsGraph(agentsFromContributors([
92
+ { name: 'Alice', organization: 'ISO' },
93
+ { name: 'Bob', organization: 'IEC' },
94
+ ]));
95
+ const store = parse(writeTurtle(result.graph));
96
+ const aliceOrg = store.getObjects('https://glossarist.org/agent/alice', `${PROV}actedOnBehalfOf`, null).map(q => q.value);
97
+ expect(aliceOrg).toContain('https://glossarist.org/org/iso');
98
+ const bobOrg = store.getObjects('https://glossarist.org/agent/bob', `${PROV}actedOnBehalfOf`, null).map(q => q.value);
99
+ expect(bobOrg).toContain('https://glossarist.org/org/iec');
100
+ });
101
+
102
+ it('omits mbox, seeAlso, and actedOnBehalfOf when not provided', () => {
103
+ const graph = emitAgentsGraph(agentsFromContributors([{ name: 'Solo' }]));
104
+ const store = parse(writeTurtle(graph.graph));
105
+ const mbox = store.getObjects('https://glossarist.org/agent/solo', `${FOAF}mbox`, null);
106
+ expect(mbox).toHaveLength(0);
107
+ const acted = store.getObjects('https://glossarist.org/agent/solo', `${PROV}actedOnBehalfOf`, null);
108
+ expect(acted).toHaveLength(0);
109
+ });
110
+ });
@@ -0,0 +1,159 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Parser, Store } from 'n3';
3
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
4
+ import {
5
+ emitBibliographyGraph,
6
+ bibliographyEntryIri,
7
+ normalizeBibliographyData,
8
+ } from '../../components/concept-rdf/bibliography-emitter';
9
+
10
+ const FOAF = 'http://xmlns.com/foaf/0.1/';
11
+ const DCTERMS = 'http://purl.org/dc/terms/';
12
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
13
+
14
+ function parse(turtle: string): Store {
15
+ const parser = new Parser({ format: 'Turtle' });
16
+ const store = new Store();
17
+ store.addQuads(parser.parse(turtle));
18
+ return store;
19
+ }
20
+
21
+ describe('emitBibliographyGraph — K5 bibliography graph', () => {
22
+ it('types each entry as dcterms:BibliographicResource', () => {
23
+ const graph = emitBibliographyGraph({
24
+ registerId: 'iso-geodetic',
25
+ entries: [{ id: 'iso-704', reference: 'ISO 704' }],
26
+ });
27
+ const store = parse(writeTurtle(graph));
28
+ const iri = bibliographyEntryIri('iso-geodetic', 'iso-704');
29
+ const types = store.getObjects(iri, RDF_TYPE, null).map(q => q.value);
30
+ expect(types).toContain(`${DCTERMS}BibliographicResource`);
31
+ });
32
+
33
+ it('emits identifier, bibliographicCitation, and title as literals', () => {
34
+ const graph = emitBibliographyGraph({
35
+ registerId: 'iso-geodetic',
36
+ entries: [
37
+ { id: 'iso-704', reference: 'ISO 704', title: 'Terminology work — Principles and methods' },
38
+ ],
39
+ });
40
+ const store = parse(writeTurtle(graph));
41
+ const iri = bibliographyEntryIri('iso-geodetic', 'iso-704');
42
+ const ids = store.getObjects(iri, `${DCTERMS}identifier`, null).map(q => q.value);
43
+ expect(ids).toContain('iso-704');
44
+ const cites = store.getObjects(iri, `${DCTERMS}bibliographicCitation`, null).map(q => q.value);
45
+ expect(cites).toContain('ISO 704');
46
+ const titles = store.getObjects(iri, `${DCTERMS}title`, null).map(q => q.value);
47
+ expect(titles).toContain('Terminology work — Principles and methods');
48
+ });
49
+
50
+ it('emits foaf:page when link is provided', () => {
51
+ const graph = emitBibliographyGraph({
52
+ registerId: 'iso-geodetic',
53
+ entries: [{ id: 'ref', reference: 'X', link: 'https://example.org/x' }],
54
+ });
55
+ const store = parse(writeTurtle(graph));
56
+ const iri = bibliographyEntryIri('iso-geodetic', 'ref');
57
+ const pages = store.getObjects(iri, `${FOAF}page`, null).map(q => q.value);
58
+ expect(pages).toContain('https://example.org/x');
59
+ });
60
+
61
+ it('omits foaf:page when link is absent', () => {
62
+ const graph = emitBibliographyGraph({
63
+ registerId: 'iso-geodetic',
64
+ entries: [{ id: 'no-link', reference: 'Y' }],
65
+ });
66
+ const store = parse(writeTurtle(graph));
67
+ const iri = bibliographyEntryIri('iso-geodetic', 'no-link');
68
+ const pages = store.getObjects(iri, `${FOAF}page`, null);
69
+ expect(pages).toHaveLength(0);
70
+ });
71
+
72
+ it('emits one resource per entry', () => {
73
+ const graph = emitBibliographyGraph({
74
+ registerId: 'iso-geodetic',
75
+ entries: [
76
+ { id: 'a', reference: 'A' },
77
+ { id: 'b', reference: 'B' },
78
+ { id: 'c', reference: 'C' },
79
+ ],
80
+ });
81
+ const iriA = bibliographyEntryIri('iso-geodetic', 'a');
82
+ const iriB = bibliographyEntryIri('iso-geodetic', 'b');
83
+ const iriC = bibliographyEntryIri('iso-geodetic', 'c');
84
+ const store = parse(writeTurtle(graph));
85
+ expect(store.getObjects(iriA, `${DCTERMS}bibliographicCitation`, null).map(q => q.value)).toContain('A');
86
+ expect(store.getObjects(iriB, `${DCTERMS}bibliographicCitation`, null).map(q => q.value)).toContain('B');
87
+ expect(store.getObjects(iriC, `${DCTERMS}bibliographicCitation`, null).map(q => q.value)).toContain('C');
88
+ });
89
+
90
+ it('links each entry to the dataset via dcterms:isPartOf', () => {
91
+ const graph = emitBibliographyGraph({
92
+ registerId: 'iso-geodetic',
93
+ entries: [{ id: 'a', reference: 'A' }],
94
+ });
95
+ const store = parse(writeTurtle(graph));
96
+ const iri = bibliographyEntryIri('iso-geodetic', 'a');
97
+ const parts = store.getObjects(iri, `${DCTERMS}isPartOf`, null).map(q => q.value);
98
+ expect(parts).toContain('https://glossarist.org/iso-geodetic/');
99
+ });
100
+ });
101
+
102
+ describe('B3/C — V3 bibliography shape (wrapper key)', () => {
103
+ it('normalizeBibliographyData unwraps { bibliography: [...] } V3 shape', () => {
104
+ const v3 = {
105
+ bibliography: [
106
+ { id: 'iso704', reference: 'ISO 704', title: 'Terminology work', type: 'normative' },
107
+ { id: 'iso10241', reference: 'ISO 10241-1' },
108
+ ],
109
+ };
110
+ const entries = normalizeBibliographyData(v3);
111
+ expect(entries.length).toBe(2);
112
+ expect(entries[0].id).toBe('iso704');
113
+ expect(entries[0].type).toBe('normative');
114
+ expect(entries[1].id).toBe('iso10241');
115
+ });
116
+
117
+ it('normalizeBibliographyData accepts the legacy flat-map shape', () => {
118
+ const legacy = {
119
+ iso704: { reference: 'ISO 704', title: 'Terminology work' },
120
+ iso10241: { reference: 'ISO 10241-1' },
121
+ };
122
+ const entries = normalizeBibliographyData(legacy);
123
+ expect(entries.length).toBe(2);
124
+ expect(entries.find(e => e.id === 'iso704')?.title).toBe('Terminology work');
125
+ });
126
+
127
+ it('emitBibliographyGraph handles both V3 and legacy shapes identically', () => {
128
+ const v3 = { bibliography: [{ id: 'iso704', reference: 'ISO 704', title: 'T' }] };
129
+ const legacy = { iso704: { reference: 'ISO 704', title: 'T' } };
130
+
131
+ const graphV3 = emitBibliographyGraph({
132
+ registerId: 'r',
133
+ entries: normalizeBibliographyData(v3),
134
+ });
135
+ const graphLegacy = emitBibliographyGraph({
136
+ registerId: 'r',
137
+ entries: normalizeBibliographyData(legacy),
138
+ });
139
+
140
+ const storeV3 = parse(writeTurtle(graphV3));
141
+ const storeLegacy = parse(writeTurtle(graphLegacy));
142
+
143
+ const iri = bibliographyEntryIri('r', 'iso704');
144
+ const v3Titles = storeV3.getObjects(iri, `${DCTERMS}title`, null).map(q => q.value);
145
+ const legacyTitles = storeLegacy.getObjects(iri, `${DCTERMS}title`, null).map(q => q.value);
146
+ expect(v3Titles).toEqual(legacyTitles);
147
+ });
148
+
149
+ it('emits dcterms:type when entry.type is provided', () => {
150
+ const graph = emitBibliographyGraph({
151
+ registerId: 'r',
152
+ entries: [{ id: 'a', reference: 'X', type: 'normative' }],
153
+ });
154
+ const store = parse(writeTurtle(graph));
155
+ const iri = bibliographyEntryIri('r', 'a');
156
+ const types = store.getObjects(iri, `${DCTERMS}type`, null).map(q => q.value);
157
+ expect(types.some(t => t.includes('bibtype/normative'))).toBe(true);
158
+ });
159
+ });
@@ -0,0 +1,119 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Parser, Store } from 'n3';
3
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
4
+ import { emitBuildActivityGraph, activityIri } from '../../components/concept-rdf/build-activity-emitter';
5
+ import type { BuildActivityInput } from '../../components/concept-rdf/build-activity-emitter';
6
+
7
+ function makeInput(overrides: Partial<BuildActivityInput> = {}): BuildActivityInput {
8
+ return {
9
+ runId: '2026-06-28T12-00-00Z',
10
+ startedAt: '2026-06-28T12:00:00Z',
11
+ endedAt: '2026-06-28T12:05:00Z',
12
+ gitSha: 'abc1234',
13
+ gitBranch: 'main',
14
+ toolId: 'concept-browser',
15
+ toolVersion: '0.7.51',
16
+ datasetRegisters: ['iso-geodetic', 'iso-10303-2-terms'],
17
+ conceptCount: 1234,
18
+ associatedAgentIri: 'https://glossarist.org/agent/ci-bot',
19
+ ...overrides,
20
+ };
21
+ }
22
+
23
+ function parse(turtle: string): Store {
24
+ const parser = new Parser({ format: 'Turtle' });
25
+ const store = new Store();
26
+ store.addQuads(parser.parse(turtle));
27
+ return store;
28
+ }
29
+
30
+ const expand = (v: string): string => {
31
+ if (v.startsWith('prov:')) return `http://www.w3.org/ns/prov#${v.slice(5)}`;
32
+ if (v.startsWith('dcterms:')) return `http://purl.org/dc/terms/${v.slice(8)}`;
33
+ if (v.startsWith('rdf:')) return `http://www.w3.org/1999/02/22-rdf-syntax-ns#${v.slice(4)}`;
34
+ if (v.startsWith('xsd:')) return `http://www.w3.org/2001/XMLSchema#${v.slice(4)}`;
35
+ if (v.startsWith('foaf:')) return `http://xmlns.com/foaf/0.1/${v.slice(5)}`;
36
+ return v;
37
+ };
38
+
39
+ describe('emitBuildActivityGraph — J7 build activity records', () => {
40
+ it('types the activity as prov:Activity', () => {
41
+ const input = makeInput();
42
+ const graph = emitBuildActivityGraph(input);
43
+ const store = parse(writeTurtle(graph));
44
+ const iri = activityIri(input);
45
+ const types = store.getObjects(iri, expand('rdf:type'), null).map(q => q.value);
46
+ expect(types).toContain(expand('prov:Activity'));
47
+ });
48
+
49
+ it('records the start and end times', () => {
50
+ const input = makeInput();
51
+ const graph = emitBuildActivityGraph(input);
52
+ const store = parse(writeTurtle(graph));
53
+ const iri = activityIri(input);
54
+ const generated = store.getObjects(iri, expand('prov:generatedAtTime'), null).map(q => q.value);
55
+ expect(generated).toContain('2026-06-28T12:05:00Z');
56
+ });
57
+
58
+ it('types the tool as prov:SoftwareAgent with version', () => {
59
+ const input = makeInput();
60
+ const graph = emitBuildActivityGraph(input);
61
+ const store = parse(writeTurtle(graph));
62
+ const toolIri = `https://glossarist.org/tool/${input.toolId}/${input.toolVersion}`;
63
+ const types = store.getObjects(toolIri, expand('rdf:type'), null).map(q => q.value);
64
+ expect(types).toContain(expand('prov:SoftwareAgent'));
65
+ const version = store.getObjects(toolIri, expand('prov:version'), null).map(q => q.value);
66
+ expect(version).toContain(input.toolVersion);
67
+ });
68
+
69
+ it('records the git commit and the tool as prov:used entities', () => {
70
+ const input = makeInput();
71
+ const graph = emitBuildActivityGraph(input);
72
+ const store = parse(writeTurtle(graph));
73
+ const iri = activityIri(input);
74
+ const used = store.getObjects(iri, expand('prov:used'), null).map(q => q.value);
75
+ expect(used).toContain(`https://glossarist.org/commit/${input.gitSha}`);
76
+ expect(used).toContain(`https://glossarist.org/tool/${input.toolId}/${input.toolVersion}`);
77
+ });
78
+
79
+ it('references every dataset register via prov:used', () => {
80
+ const input = makeInput();
81
+ const graph = emitBuildActivityGraph(input);
82
+ const store = parse(writeTurtle(graph));
83
+ const iri = activityIri(input);
84
+ const used = store.getObjects(iri, expand('prov:used'), null).map(q => q.value);
85
+ for (const register of input.datasetRegisters) {
86
+ expect(used).toContain(`https://glossarist.org/${register}/`);
87
+ }
88
+ });
89
+
90
+ it('records the concept count as an xsd:integer literal', () => {
91
+ const input = makeInput();
92
+ const graph = emitBuildActivityGraph(input);
93
+ const store = parse(writeTurtle(graph));
94
+ const iri = activityIri(input);
95
+ const counts = store.getObjects(iri, 'https://www.glossarist.org/ontologies/conceptCount', null).map(q => q.value);
96
+ expect(counts).toContain('1234');
97
+ });
98
+
99
+ it('associates the CI agent when provided', () => {
100
+ const input = makeInput();
101
+ const graph = emitBuildActivityGraph(input);
102
+ const store = parse(writeTurtle(graph));
103
+ const iri = activityIri(input);
104
+ const agents = store.getObjects(iri, expand('prov:wasAssociatedWith'), null).map(q => q.value);
105
+ expect(agents).toContain(input.associatedAgentIri);
106
+
107
+ const agentTypes = store.getObjects(input.associatedAgentIri!, expand('rdf:type'), null).map(q => q.value);
108
+ expect(agentTypes).toContain(expand('prov:Agent'));
109
+ });
110
+
111
+ it('omits the commit entity when gitSha is not provided', () => {
112
+ const input = makeInput({ gitSha: undefined, gitBranch: undefined });
113
+ const graph = emitBuildActivityGraph(input);
114
+ const store = parse(writeTurtle(graph));
115
+ const iri = activityIri(input);
116
+ const used = store.getObjects(iri, expand('prov:used'), null).map(q => q.value);
117
+ expect(used.some(u => u.includes('/commit/'))).toBe(false);
118
+ });
119
+ });
@@ -0,0 +1,97 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Parser, Store } from 'n3';
3
+ import { Concept } from 'glossarist';
4
+ import { writeTurtle } from '../../components/concept-rdf/turtle-writer';
5
+ import { emitConceptGraph } from '../../components/concept-rdf/concept-emitter';
6
+
7
+ const G = 'https://www.glossarist.org/ontologies/';
8
+ const XSD = 'http://www.w3.org/2001/XMLSchema#';
9
+
10
+ function parse(turtle: string): Store {
11
+ const parser = new Parser({ format: 'Turtle' });
12
+ const store = new Store();
13
+ store.addQuads(parser.parse(turtle));
14
+ return store;
15
+ }
16
+
17
+ const BASE = 'https://glossarist.org/fixtures/dates';
18
+
19
+ describe('B4/C — coerceToDateTime handles year-only and month-only dates', () => {
20
+ it('coerces yyyy-mm-dd to xsd:dateTime unchanged (with T00:00:00Z)', () => {
21
+ const concept = Concept.fromJSON({
22
+ id: 'd.1', uri: `${BASE}/d.1`, status: 'valid',
23
+ dates: [{ type: 'accepted', date: '2020-06-15' }],
24
+ localizations: {
25
+ eng: {
26
+ language_code: 'eng', entry_status: 'valid',
27
+ terms: [{ type: 'expression', designation: 'd1', normative_status: 'preferred' }],
28
+ definition: [{ content: 'def' }],
29
+ },
30
+ },
31
+ });
32
+ const { graph } = emitConceptGraph(concept, concept.uri ?? '');
33
+ const store = parse(writeTurtle(graph));
34
+ const dateBlanks = store.getObjects(concept.uri ?? '', `${G}hasDate`, null);
35
+ const values = store.getObjects(dateBlanks[0], `${G}dateValue`, null);
36
+ expect((values[0] as any).value).toBe('2020-06-15T00:00:00Z');
37
+ expect((values[0] as any).datatype.value).toBe(`${XSD}dateTime`);
38
+ });
39
+
40
+ it('coerces year-only "2020" to "2020-01-01T00:00:00Z"', () => {
41
+ const concept = Concept.fromJSON({
42
+ id: 'd.2', uri: `${BASE}/d.2`, status: 'valid',
43
+ dates: [{ type: 'accepted', date: '2020' }],
44
+ localizations: {
45
+ eng: {
46
+ language_code: 'eng', entry_status: 'valid',
47
+ terms: [{ type: 'expression', designation: 'd2', normative_status: 'preferred' }],
48
+ definition: [{ content: 'def' }],
49
+ },
50
+ },
51
+ });
52
+ const { graph } = emitConceptGraph(concept, concept.uri ?? '');
53
+ const store = parse(writeTurtle(graph));
54
+ const dateBlanks = store.getObjects(concept.uri ?? '', `${G}hasDate`, null);
55
+ const values = store.getObjects(dateBlanks[0], `${G}dateValue`, null);
56
+ expect((values[0] as any).value).toBe('2020-01-01T00:00:00Z');
57
+ expect((values[0] as any).datatype.value).toBe(`${XSD}dateTime`);
58
+ });
59
+
60
+ it('coerces month-only "2020-06" to "2020-06-01T00:00:00Z"', () => {
61
+ const concept = Concept.fromJSON({
62
+ id: 'd.3', uri: `${BASE}/d.3`, status: 'valid',
63
+ dates: [{ type: 'accepted', date: '2020-06' }],
64
+ localizations: {
65
+ eng: {
66
+ language_code: 'eng', entry_status: 'valid',
67
+ terms: [{ type: 'expression', designation: 'd3', normative_status: 'preferred' }],
68
+ definition: [{ content: 'def' }],
69
+ },
70
+ },
71
+ });
72
+ const { graph } = emitConceptGraph(concept, concept.uri ?? '');
73
+ const store = parse(writeTurtle(graph));
74
+ const dateBlanks = store.getObjects(concept.uri ?? '', `${G}hasDate`, null);
75
+ const values = store.getObjects(dateBlanks[0], `${G}dateValue`, null);
76
+ expect((values[0] as any).value).toBe('2020-06-01T00:00:00Z');
77
+ });
78
+
79
+ it('passes through ISO 8601 datetimes unchanged', () => {
80
+ const concept = Concept.fromJSON({
81
+ id: 'd.4', uri: `${BASE}/d.4`, status: 'valid',
82
+ dates: [{ type: 'accepted', date: '2020-06-15T13:45:30Z' }],
83
+ localizations: {
84
+ eng: {
85
+ language_code: 'eng', entry_status: 'valid',
86
+ terms: [{ type: 'expression', designation: 'd4', normative_status: 'preferred' }],
87
+ definition: [{ content: 'def' }],
88
+ },
89
+ },
90
+ });
91
+ const { graph } = emitConceptGraph(concept, concept.uri ?? '');
92
+ const store = parse(writeTurtle(graph));
93
+ const dateBlanks = store.getObjects(concept.uri ?? '', `${G}hasDate`, null);
94
+ const values = store.getObjects(dateBlanks[0], `${G}dateValue`, null);
95
+ expect((values[0] as any).value).toBe('2020-06-15T13:45:30Z');
96
+ });
97
+ });