glossarist 0.4.7 → 0.4.9

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",
3
- "version": "0.4.7",
3
+ "version": "0.4.9",
4
4
  "description": "JavaScript SDK for Glossarist GCR packages — read, write, validate, and manage terminology concepts",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -42,6 +42,11 @@
42
42
  "import": "./src/rdf/index.js",
43
43
  "default": "./src/rdf/index.js"
44
44
  },
45
+ "./rdf/prefixes": {
46
+ "types": "./src/rdf/predicates.d.ts",
47
+ "import": "./src/rdf/predicates.js",
48
+ "default": "./src/rdf/predicates.js"
49
+ },
45
50
  "./transforms": {
46
51
  "types": "./src/transforms/index.d.ts",
47
52
  "import": "./src/transforms/index.js",
@@ -91,6 +91,31 @@ export class Register extends GlossaristModel {
91
91
  return [sectionId, ...this.sectionAncestorIds(sectionId)];
92
92
  }
93
93
 
94
+ // Walks the section tree DOWNWARD from `sectionId`, returning all
95
+ // descendant section IDs in pre-order (immediate children first,
96
+ // then their children). The returned array does NOT include
97
+ // sectionId itself. Returns [] when sectionId is unknown or is a
98
+ // leaf section. Useful for filtering concepts by parent section
99
+ // (include descendants) — the inverse direction of sectionClosure.
100
+ sectionDescendantIds(sectionId) {
101
+ if (!sectionId) return [];
102
+ const section = this.sectionById(sectionId);
103
+ if (!section) return [];
104
+ const out = [];
105
+ for (const child of section.children ?? []) {
106
+ out.push(child.id);
107
+ out.push(...this.sectionDescendantIds(child.id));
108
+ }
109
+ return out;
110
+ }
111
+
112
+ // Returns the descendant closure of `sectionId`: the section plus
113
+ // all of its descendants. Useful for "show me everything in section
114
+ // X or below" queries.
115
+ sectionDescendantClosure(sectionId) {
116
+ return [sectionId, ...this.sectionDescendantIds(sectionId)];
117
+ }
118
+
94
119
  // Returns all section IDs the concept belongs to: the concept's own
95
120
  // sections plus every ancestor of each. Concept-side section
96
121
  // membership comes from `concept.sections` if set, otherwise from
@@ -1,9 +1,24 @@
1
1
  // Categorization SSOT for the 52 glossarist relationship types.
2
2
  //
3
3
  // Each type in RELATIONSHIP_TYPES belongs to exactly one category. The
4
- // categories are semantic buckets used for filtering and visualization
5
- // in concept-browser. Bringing the categorization into glossarist-js
6
- // means all consumers see the same buckets.
4
+ // categories mirror the concept-model taxonomy
5
+ // (concept-browser:src/data/taxonomies.json) so JS consumers and the
6
+ // Vue UI see the same buckets. Adding a new type means adding a
7
+ // taxonomy entry — no code changes here.
8
+ //
9
+ // The 8 categories:
10
+ // lifecycle — supersession, deprecation, replacement
11
+ // hierarchical — SKOS + ISO 25964 broader/narrower (generic/partitive/instantial)
12
+ // associative — generic see-also references
13
+ // comparative — compare / contrast
14
+ // spatiotemporal — sequential, spatial, temporal
15
+ // lexical — homograph, false_friend (cross-language)
16
+ // mapping — SKOS mapping properties (cross-vocabulary)
17
+ // definitional — ISO 19135 concept-to-concept relations: definition, part,
18
+ // instance, inheritance, versioning
19
+ //
20
+ // Designation-level relationships (abbreviated_form_for, short_form_for)
21
+ // are a separate concern — see DESIGNATION_RELATIONSHIP_TYPES.
7
22
 
8
23
  import { RELATIONSHIP_TYPES } from './related-concept.js';
9
24
 
@@ -75,22 +90,21 @@ export const RELATION_CATEGORIES = Object.freeze({
75
90
  ]),
76
91
  }),
77
92
 
78
- conceptInstance: Object.freeze({
79
- label: 'Concept / Instance',
80
- description: 'ISO 19135 concept-to-concept relations: definition, part, instance, inheritance.',
93
+ // ISO 19135 concept-to-concept relations. Replaces the previous
94
+ // `conceptInstance` and `versioning` split — concept-browser's
95
+ // taxonomy treats versioning as part of the definitional cluster
96
+ // (a version IS a definitional variant of the same concept identity).
97
+ // Renaming aligns glossarist-js with the canonical taxonomy at
98
+ // concept-browser:src/data/taxonomies.json.
99
+ definitional: Object.freeze({
100
+ label: 'Definitional',
101
+ description: 'ISO 19135 concept-to-concept relations: definition, part, instance, inheritance, versioning.',
81
102
  types: Object.freeze([
82
103
  'has_concept', 'is_concept_of',
83
104
  'instance_of', 'has_instance',
84
105
  'has_definition', 'definition_of',
85
106
  'has_part', 'is_part_of',
86
107
  'inherits', 'inherited_by',
87
- ]),
88
- }),
89
-
90
- versioning: Object.freeze({
91
- label: 'Versioning',
92
- description: 'Version lineage within the same concept identity.',
93
- types: Object.freeze([
94
108
  'has_version', 'version_of',
95
109
  'current_version', 'current_version_of',
96
110
  ]),
@@ -106,6 +120,14 @@ const _CATEGORY_BY_TYPE = (() => {
106
120
  return map;
107
121
  })();
108
122
 
123
+ // Backward-compat aliases for callers that adopted the original
124
+ // glossarist-js naming (v0.4.6). Resolved at lookup time so any of
125
+ // the three names returns the definitional category.
126
+ const _CATEGORY_ALIASES = {
127
+ conceptInstance: 'definitional',
128
+ versioning: 'definitional',
129
+ };
130
+
109
131
  /**
110
132
  * Returns the category key for the given relationship type, or null
111
133
  * when the type is not categorized (e.g. an unrecognized custom type).
@@ -116,10 +138,13 @@ export function categoryOf(type) {
116
138
 
117
139
  /**
118
140
  * Returns the category definition ({ label, description, types }) for
119
- * the given category key, or null when the key is unknown.
141
+ * the given category key. Accepts both canonical names (definitional)
142
+ * and aliases from earlier glossarist-js versions (conceptInstance,
143
+ * versioning) for backward compatibility.
120
144
  */
121
145
  export function categoryDefinition(categoryKey) {
122
- return RELATION_CATEGORIES[categoryKey] ?? null;
146
+ const canonical = _CATEGORY_ALIASES[categoryKey] ?? categoryKey;
147
+ return RELATION_CATEGORIES[canonical] ?? null;
123
148
  }
124
149
 
125
150
  /**
@@ -18,8 +18,7 @@ export const RELATION_COLOR_DEFAULTS = Object.freeze({
18
18
  spatiotemporal: { light: '#0D9488', dark: '#5EEAD4' },
19
19
  lexical: { light: '#D97706', dark: '#FBBF24' },
20
20
  mapping: { light: '#2563EB', dark: '#93BBFD' },
21
- conceptInstance:{ light: '#65A30D', dark: '#A3E635' },
22
- versioning: { light: '#9333EA', dark: '#C4B5FD' },
21
+ definitional: { light: '#65A30D', dark: '#A3E635' },
23
22
  }),
24
23
 
25
24
  // Per-type overrides. Use the same { light, dark } shape. Keys are
@@ -0,0 +1,130 @@
1
+ // Agents emitter — emits foaf:Person / prov:Organization from
2
+ // contributor declarations. Mirrors concept-browser's agents-emitter.ts.
3
+ //
4
+ // Each contributor becomes a foaf:Person + prov:Person + prov:Agent
5
+ // with foaf:name, optional foaf:mbox (mailto link), rdfs:seeAlso (url).
6
+ // Their organization (when provided) becomes a foaf:Organization +
7
+ // prov:Organization, and the person is linked via prov:actedOnBehalfOf.
8
+
9
+ import { namedNode, literal, quad } from './terms.js';
10
+
11
+ const PROV_NS = 'http://www.w3.org/ns/prov#';
12
+ const FOAF_NS = 'http://xmlns.com/foaf/0.1/';
13
+ const DCTERMS_NS = 'http://purl.org/dc/terms/';
14
+ const RDFS_NS = 'http://www.w3.org/2000/01/rdf-schema#';
15
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
16
+
17
+ const PROV = {
18
+ Person: `${PROV_NS}Person`,
19
+ Organization: `${PROV_NS}Organization`,
20
+ Agent: `${PROV_NS}Agent`,
21
+ actedOnBehalfOf: `${PROV_NS}actedOnBehalfOf`,
22
+ };
23
+ const FOAF = {
24
+ Person: `${FOAF_NS}Person`,
25
+ Organization: `${FOAF_NS}Organization`,
26
+ name: `${FOAF_NS}name`,
27
+ mbox: `${FOAF_NS}mbox`,
28
+ };
29
+ const DCTERMS = {
30
+ description: `${DCTERMS_NS}description`,
31
+ };
32
+ const RDFS = {
33
+ seeAlso: `${RDFS_NS}seeAlso`,
34
+ };
35
+
36
+ const DEFAULT_AGENT_BASE = 'https://glossarist.org/agent';
37
+ const DEFAULT_ORG_BASE = 'https://glossarist.org/org';
38
+
39
+ /**
40
+ * @typedef {Object} Contributor
41
+ * @property {string} name
42
+ * @property {string} [role]
43
+ * @property {string} [organization]
44
+ * @property {string} [url]
45
+ * @property {string} [email]
46
+ */
47
+ /**
48
+ * @typedef {Contributor & { slug: string, agentIri: string }} AgentInput
49
+ */
50
+
51
+ /**
52
+ * Slugify a name for IRIs. Lowercase, ASCII, hyphenated.
53
+ */
54
+ export function slugify(input) {
55
+ return String(input)
56
+ .toLowerCase()
57
+ .normalize('NFKD')
58
+ .replace(/[̀-ͯ]/g, '')
59
+ .replace(/[^a-z0-9]+/g, '-')
60
+ .replace(/^-+|-+$/g, '');
61
+ }
62
+
63
+ /**
64
+ * Builds AgentInput records from raw contributor declarations.
65
+ * Useful for callers that want to inspect the IRIs before emission.
66
+ */
67
+ export function agentsFromContributors(contributors, agentBase = DEFAULT_AGENT_BASE) {
68
+ return (contributors ?? []).map(c => {
69
+ const slug = slugify(c.name);
70
+ return {
71
+ slug,
72
+ name: c.name,
73
+ role: c.role,
74
+ organization: c.organization,
75
+ url: c.url,
76
+ email: c.email,
77
+ agentIri: `${agentBase}/${slug}`,
78
+ };
79
+ });
80
+ }
81
+
82
+ /**
83
+ * Emits foaf:Person + prov:Organization quads for a list of agents.
84
+ *
85
+ * Returns a generator of quads AND the set of unique person and
86
+ * organization IRIs emitted. Organizations are deduplicated — the
87
+ * same org across multiple contributors is emitted once.
88
+ *
89
+ * @param {readonly AgentInput[]} agents
90
+ * @param {Object} [options]
91
+ * @param {string} [options.orgBase] — default https://glossarist.org/org
92
+ * @returns {Generator<Quad, void, unknown>}
93
+ */
94
+ export function* agentsToQuads(agents, options = {}) {
95
+ const orgBase = options.orgBase ?? DEFAULT_ORG_BASE;
96
+ const emittedOrgs = new Set();
97
+
98
+ for (const a of agents ?? []) {
99
+ const person = namedNode(a.agentIri);
100
+ yield quad(person, namedNode(RDF_TYPE), namedNode(FOAF.Person));
101
+ yield quad(person, namedNode(RDF_TYPE), namedNode(PROV.Person));
102
+ yield quad(person, namedNode(RDF_TYPE), namedNode(PROV.Agent));
103
+ yield quad(person, namedNode(FOAF.name), literal(a.name));
104
+
105
+ if (a.email) {
106
+ yield quad(person, namedNode(FOAF.mbox), namedNode(`mailto:${a.email}`));
107
+ }
108
+ if (a.url) {
109
+ yield quad(person, namedNode(RDFS.seeAlso), namedNode(a.url));
110
+ }
111
+ if (a.role) {
112
+ yield quad(person, namedNode(DCTERMS.description), literal(a.role));
113
+ }
114
+
115
+ if (a.organization) {
116
+ const orgSlug = slugify(a.organization);
117
+ const orgIri = `${orgBase}/${orgSlug}`;
118
+ yield quad(person, namedNode(PROV.actedOnBehalfOf), namedNode(orgIri));
119
+
120
+ if (!emittedOrgs.has(orgIri)) {
121
+ emittedOrgs.add(orgIri);
122
+ const org = namedNode(orgIri);
123
+ yield quad(org, namedNode(RDF_TYPE), namedNode(FOAF.Organization));
124
+ yield quad(org, namedNode(RDF_TYPE), namedNode(PROV.Organization));
125
+ yield quad(org, namedNode(RDF_TYPE), namedNode(PROV.Agent));
126
+ yield quad(org, namedNode(FOAF.name), literal(a.organization));
127
+ }
128
+ }
129
+ }
130
+ }
@@ -0,0 +1,120 @@
1
+ // Bibliography emitter — emits dcterms:BibliographicResource per
2
+ // bibliography entry. Mirrors concept-browser's bibliography-emitter.ts.
3
+ //
4
+ // Each entry becomes a typed resource with:
5
+ // - dcterms:identifier (the entry id)
6
+ // - dcterms:bibliographicCitation (the reference string)
7
+ // - dcterms:title (optional)
8
+ // - foaf:page (optional link)
9
+ // - dcterms:type (optional, value URIs from gloss:bibtype/*)
10
+ // - dcterms:isPartOf (parent dataset)
11
+
12
+ import { namedNode, literal, quad } from './terms.js';
13
+
14
+ const DCTERMS_NS = 'http://purl.org/dc/terms/';
15
+ const FOAF_NS = 'http://xmlns.com/foaf/0.1/';
16
+ const GLOSS_NS = 'https://www.glossarist.org/ontologies/';
17
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
18
+
19
+ const DCTERMS = {
20
+ BibliographicResource: `${DCTERMS_NS}BibliographicResource`,
21
+ identifier: `${DCTERMS_NS}identifier`,
22
+ bibliographicCitation: `${DCTERMS_NS}bibliographicCitation`,
23
+ title: `${DCTERMS_NS}title`,
24
+ type: `${DCTERMS_NS}type`,
25
+ isPartOf: `${DCTERMS_NS}isPartOf`,
26
+ };
27
+ const FOAF = {
28
+ page: `${FOAF_NS}page`,
29
+ };
30
+
31
+ const DEFAULT_BASE_URI = 'https://glossarist.org';
32
+
33
+ /**
34
+ * @typedef {Object} BibliographyEntry
35
+ * @property {string} id
36
+ * @property {string} reference
37
+ * @property {string} [title]
38
+ * @property {string} [link]
39
+ * @property {string} [type]
40
+ */
41
+ /**
42
+ * @typedef {Object} BibliographyInput
43
+ * @property {string} registerId
44
+ * @property {readonly BibliographyEntry[]} entries
45
+ * @property {string} [baseUri]
46
+ */
47
+
48
+ /**
49
+ * Returns the canonical IRI for a bibliography entry.
50
+ *
51
+ * @param {string} registerId
52
+ * @param {string} entryId
53
+ * @param {string} [baseUri]
54
+ */
55
+ export function bibliographyEntryIri(registerId, entryId, baseUri = DEFAULT_BASE_URI) {
56
+ return `${baseUri}/${registerId}/bib/${entryId}`;
57
+ }
58
+
59
+ /**
60
+ * Emits dcterms:BibliographicResource quads for each entry. Entries
61
+ * without an id or reference are skipped (matches concept-browser).
62
+ *
63
+ * @param {BibliographyInput} input
64
+ * @returns {Generator<Quad, void, unknown>}
65
+ */
66
+ export function* bibliographyToQuads(input) {
67
+ const baseUri = input.baseUri ?? DEFAULT_BASE_URI;
68
+ const parentIri = `${baseUri}/${input.registerId}/`;
69
+
70
+ for (const entry of input.entries ?? []) {
71
+ if (!entry.id || !entry.reference) continue;
72
+ const entryIri = bibliographyEntryIri(input.registerId, entry.id, baseUri);
73
+ const e = namedNode(entryIri);
74
+
75
+ yield quad(e, namedNode(RDF_TYPE), namedNode(DCTERMS.BibliographicResource));
76
+ yield quad(e, namedNode(DCTERMS.identifier), literal(entry.id));
77
+ yield quad(e, namedNode(DCTERMS.bibliographicCitation), literal(entry.reference));
78
+ if (entry.title) {
79
+ yield quad(e, namedNode(DCTERMS.title), literal(entry.title));
80
+ }
81
+ if (entry.link) {
82
+ yield quad(e, namedNode(FOAF.page), namedNode(entry.link));
83
+ }
84
+ if (entry.type) {
85
+ yield quad(e, namedNode(DCTERMS.type), namedNode(`${GLOSS_NS}bibtype/${entry.type}`));
86
+ }
87
+ yield quad(e, namedNode(DCTERMS.isPartOf), namedNode(parentIri));
88
+ }
89
+ }
90
+
91
+ /**
92
+ * Normalizes raw bibliography data from a v3 YAML bibliography file.
93
+ * Accepts both shapes:
94
+ * - { bibliography: [...] }
95
+ * - { <id>: { ... }, <id>: { ... } }
96
+ *
97
+ * Returns a flat list of BibliographyEntry objects with explicit ids.
98
+ */
99
+ export function normalizeBibliographyData(raw) {
100
+ if (!raw || typeof raw !== 'object') return [];
101
+ if (Array.isArray(raw.bibliography)) {
102
+ return raw.bibliography.map(e => entryFromV3(e));
103
+ }
104
+ const entries = [];
105
+ for (const [id, value] of Object.entries(raw)) {
106
+ if (!value || typeof value !== 'object') continue;
107
+ entries.push(entryFromV3(value, id));
108
+ }
109
+ return entries;
110
+ }
111
+
112
+ function entryFromV3(e, fallbackId) {
113
+ return {
114
+ id: e.id ?? fallbackId ?? '',
115
+ reference: e.reference ?? '',
116
+ title: e.title,
117
+ link: e.link,
118
+ type: e.type,
119
+ };
120
+ }
@@ -0,0 +1,127 @@
1
+ // Build activity emitter — emits prov:Activity per build run.
2
+ // Mirrors concept-browser's build-activity-emitter.ts.
3
+ //
4
+ // Models a single CI build as a prov:Activity with:
5
+ // - prov:StartingPoint and prov:EndingPoint time bounds
6
+ // - prov:used links to the git commit, the build tool (prov:SoftwareAgent),
7
+ // and each dataset register
8
+ // - gloss:conceptCount for the build output
9
+ // - prov:wasAssociatedWith for the CI bot agent
10
+
11
+ import { namedNode, literal, quad } from './terms.js';
12
+
13
+ const PROV_NS = 'http://www.w3.org/ns/prov#';
14
+ const DCTERMS_NS = 'http://purl.org/dc/terms/';
15
+ const FOAF_NS = 'http://xmlns.com/foaf/0.1/';
16
+ const GLOSS_NS = 'https://www.glossarist.org/ontologies/';
17
+ const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
18
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
19
+
20
+ const PROV = {
21
+ Activity: `${PROV_NS}Activity`,
22
+ StartingPoint: `${PROV_NS}StartingPoint`,
23
+ EndingPoint: `${PROV_NS}EndingPoint`,
24
+ Entity: `${PROV_NS}Entity`,
25
+ SoftwareAgent: `${PROV_NS}SoftwareAgent`,
26
+ Agent: `${PROV_NS}Agent`,
27
+ Person: `${PROV_NS}Person`,
28
+ generatedAtTime: `${PROV_NS}generatedAtTime`,
29
+ atTime: `${PROV_NS}atTime`,
30
+ used: `${PROV_NS}used`,
31
+ wasAssociatedWith: `${PROV_NS}wasAssociatedWith`,
32
+ version: `${PROV_NS}version`,
33
+ };
34
+ const DCTERMS = {
35
+ identifier: `${DCTERMS_NS}identifier`,
36
+ description: `${DCTERMS_NS}description`,
37
+ };
38
+ const FOAF = {
39
+ Person: `${FOAF_NS}Person`,
40
+ };
41
+ const GLOSS = {
42
+ conceptCount: `${GLOSS_NS}conceptCount`,
43
+ };
44
+ const XSD_DATE_TIME = `${XSD_NS}dateTime`;
45
+ const XSD_INTEGER = `${XSD_NS}integer`;
46
+
47
+ /**
48
+ * @typedef {Object} BuildActivityInput
49
+ * @property {string} runId
50
+ * @property {string} startedAt — ISO date-time
51
+ * @property {string} endedAt — ISO date-time
52
+ * @property {string} [gitSha]
53
+ * @property {string} [gitBranch]
54
+ * @property {string} toolId
55
+ * @property {string} toolVersion
56
+ * @property {readonly string[]} datasetRegisters
57
+ * @property {number} conceptCount
58
+ * @property {string} [associatedAgentIri]
59
+ * @property {string} [baseUri] — default https://glossarist.org
60
+ */
61
+
62
+ /**
63
+ * Returns the canonical relative IRI for a build activity.
64
+ */
65
+ export function buildActivityIri(input) {
66
+ return `activity/build/${input.runId}`;
67
+ }
68
+
69
+ /**
70
+ * Emits prov:Activity quads for one build run, including the
71
+ * starting/ending time bounds, the build tool as prov:SoftwareAgent,
72
+ * the git commit (when provided) as a prov:Entity, and each dataset
73
+ * register as a prov:used target.
74
+ *
75
+ * @param {BuildActivityInput} input
76
+ * @returns {Generator<Quad, void, unknown>}
77
+ */
78
+ export function* buildActivityToQuads(input) {
79
+ const baseUri = input.baseUri ?? 'https://glossarist.org';
80
+ const activityIri = `${baseUri}/${buildActivityIri(input)}`;
81
+ const activity = namedNode(activityIri);
82
+
83
+ yield quad(activity, namedNode(RDF_TYPE), namedNode(PROV.Activity));
84
+ yield quad(activity, namedNode(PROV.generatedAtTime), literal(input.endedAt, namedNode(XSD_DATE_TIME)));
85
+
86
+ // StartingPoint and EndingPoint are sub-types of prov:TimeBound.
87
+ // Modeling them as separate resources lets consumers query exact
88
+ // start vs end without parsing date ranges.
89
+ const start = namedNode(`${activityIri}/start`);
90
+ yield quad(start, namedNode(RDF_TYPE), namedNode(PROV.StartingPoint));
91
+ yield quad(start, namedNode(PROV.atTime), literal(input.startedAt, namedNode(XSD_DATE_TIME)));
92
+
93
+ const end = namedNode(`${activityIri}/end`);
94
+ yield quad(end, namedNode(RDF_TYPE), namedNode(PROV.EndingPoint));
95
+ yield quad(end, namedNode(PROV.atTime), literal(input.endedAt, namedNode(XSD_DATE_TIME)));
96
+
97
+ if (input.gitSha) {
98
+ const commitIri = `${baseUri}/commit/${input.gitSha}`;
99
+ yield quad(activity, namedNode(PROV.used), namedNode(commitIri));
100
+ const commit = namedNode(commitIri);
101
+ yield quad(commit, namedNode(RDF_TYPE), namedNode(PROV.Entity));
102
+ if (input.gitBranch) {
103
+ yield quad(commit, namedNode(DCTERMS.description), literal(`branch: ${input.gitBranch}`));
104
+ }
105
+ }
106
+
107
+ const toolIri = `${baseUri}/tool/${input.toolId}/${input.toolVersion}`;
108
+ yield quad(activity, namedNode(PROV.used), namedNode(toolIri));
109
+ const tool = namedNode(toolIri);
110
+ yield quad(tool, namedNode(RDF_TYPE), namedNode(PROV.Entity));
111
+ yield quad(tool, namedNode(RDF_TYPE), namedNode(PROV.SoftwareAgent));
112
+ yield quad(tool, namedNode(DCTERMS.identifier), literal(input.toolVersion));
113
+ yield quad(tool, namedNode(PROV.version), literal(input.toolVersion));
114
+
115
+ for (const register of input.datasetRegisters ?? []) {
116
+ yield quad(activity, namedNode(PROV.used), namedNode(`${baseUri}/${register}/`));
117
+ }
118
+
119
+ yield quad(activity, namedNode(GLOSS.conceptCount), literal(String(input.conceptCount), namedNode(XSD_INTEGER)));
120
+
121
+ if (input.associatedAgentIri) {
122
+ yield quad(activity, namedNode(PROV.wasAssociatedWith), namedNode(input.associatedAgentIri));
123
+ const agent = namedNode(input.associatedAgentIri);
124
+ yield quad(agent, namedNode(RDF_TYPE), namedNode(PROV.Agent));
125
+ yield quad(agent, namedNode(RDF_TYPE), namedNode(FOAF.Person));
126
+ }
127
+ }
@@ -0,0 +1,179 @@
1
+ // Dataset emitter — emits dcat:Dataset + skos:ConceptScheme per
2
+ // register. Mirrors concept-browser's dataset-emitter.ts.
3
+ //
4
+ // The dataset describes the vocabulary as a whole: title, description,
5
+ // modified date, languages, distributions (Turtle/JSON-LD/YAML), top
6
+ // concepts, sections, source repo, publisher, contact.
7
+ //
8
+ // Each section becomes a skos:Collection linked via skos:member to
9
+ // its concept URIs and via gloss:hasParentSection / gloss:hasChildSection
10
+ // to its relatives (the ontology declares these as owl:TransitiveProperty
11
+ // for cascading section membership).
12
+
13
+ import { namedNode, literal, quad } from './terms.js';
14
+
15
+ // Prefix namespaces used by this emitter. Hard-coded (not via PREFIXES)
16
+ // because dcat and prov are absent from the JSON-LD context that
17
+ // PREFIXES is generated from — they appear in instance data only.
18
+ // See concept-model/prefixes.ttl for the canonical bindings.
19
+ const DCAT_NS = 'http://www.w3.org/ns/dcat#';
20
+ const DCTERMS_NS = 'http://purl.org/dc/terms/';
21
+ const SKOS_NS = 'http://www.w3.org/2004/02/skos/core#';
22
+ const PROV_NS = 'http://www.w3.org/ns/prov#';
23
+ const GLOSS_NS = 'https://www.glossarist.org/ontologies/';
24
+ const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
25
+
26
+ /**
27
+ * @typedef {Object} DatasetDistribution
28
+ * @property {string} id
29
+ * @property {string} title
30
+ * @property {string} mediaType
31
+ * @property {string} downloadUrl
32
+ * @property {number} [byteSize]
33
+ */
34
+ /**
35
+ * @typedef {Object} DatasetSection
36
+ * @property {string} collectionIri
37
+ * @property {string} title
38
+ * @property {readonly string[]} memberUris
39
+ * @property {string} [parentCollectionIri]
40
+ * @property {readonly string[]} [childCollectionIris]
41
+ */
42
+ /**
43
+ * @typedef {Object} DatasetEmitterInput
44
+ * @property {string} datasetIri
45
+ * @property {string} registerId
46
+ * @property {string} title
47
+ * @property {string} [description]
48
+ * @property {string} modified — ISO date (YYYY-MM-DD)
49
+ * @property {readonly string[]} languages
50
+ * @property {readonly DatasetDistribution[]} distributions
51
+ * @property {readonly string[]} topConceptUris
52
+ * @property {readonly DatasetSection[]} sections
53
+ * @property {string} [sourceRepoUrl]
54
+ * @property {string} [publisherIri]
55
+ * @property {string} [contactIri]
56
+ */
57
+
58
+ const DCAT = {
59
+ Dataset: `${DCAT_NS}Dataset`,
60
+ ConceptScheme: `${SKOS_NS}ConceptScheme`,
61
+ Distribution: `${DCAT_NS}Distribution`,
62
+ Collection: `${SKOS_NS}Collection`,
63
+ mediaType: `${DCAT_NS}mediaType`,
64
+ downloadURL: `${DCAT_NS}downloadURL`,
65
+ byteSize: `${DCAT_NS}byteSize`,
66
+ distribution: `${DCAT_NS}distribution`,
67
+ contactPoint: `${DCAT_NS}contactPoint`,
68
+ };
69
+ const DCTERMS = {
70
+ title: `${DCTERMS_NS}title`,
71
+ description: `${DCTERMS_NS}description`,
72
+ modified: `${DCTERMS_NS}modified`,
73
+ identifier: `${DCTERMS_NS}identifier`,
74
+ language: `${DCTERMS_NS}language`,
75
+ publisher: `${DCTERMS_NS}publisher`,
76
+ };
77
+ const SKOS = {
78
+ hasTopConcept: `${SKOS_NS}hasTopConcept`,
79
+ member: `${SKOS_NS}member`,
80
+ };
81
+ const PROV = {
82
+ wasDerivedFrom: `${PROV_NS}wasDerivedFrom`,
83
+ };
84
+ const GLOSS = {
85
+ hasParentSection: `${GLOSS_NS}hasParentSection`,
86
+ hasChildSection: `${GLOSS_NS}hasChildSection`,
87
+ };
88
+ const XSD_DATE = `${XSD_NS}date`;
89
+ const XSD_INTEGER = `${XSD_NS}integer`;
90
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
91
+
92
+ /**
93
+ * Emits dcat:Dataset + skos:ConceptScheme quads for one register's
94
+ * metadata, plus its distributions, sections, and top concepts.
95
+ *
96
+ * @param {DatasetEmitterInput} input
97
+ * @returns {Generator<Quad, void, unknown>}
98
+ */
99
+ export function* datasetToQuads(input) {
100
+ const ds = namedNode(input.datasetIri);
101
+
102
+ yield quad(ds, namedNode(RDF_TYPE), namedNode(DCAT.Dataset));
103
+ yield quad(ds, namedNode(RDF_TYPE), namedNode(DCAT.ConceptScheme));
104
+ yield quad(ds, namedNode(DCTERMS.title), literal(input.title));
105
+ if (input.description) {
106
+ yield quad(ds, namedNode(DCTERMS.description), literal(input.description));
107
+ }
108
+ yield quad(ds, namedNode(DCTERMS.modified), literal(input.modified, namedNode(XSD_DATE)));
109
+ yield quad(ds, namedNode(DCTERMS.identifier), literal(input.registerId));
110
+
111
+ for (const lang of input.languages ?? []) {
112
+ const langIri = `http://id.loc.gov/vocabulary/iso639-1/${lang}`;
113
+ yield quad(ds, namedNode(DCTERMS.language), namedNode(langIri));
114
+ }
115
+
116
+ for (const dist of input.distributions ?? []) {
117
+ yield* distributionToQuads(ds, dist);
118
+ }
119
+
120
+ for (const conceptUri of input.topConceptUris ?? []) {
121
+ yield quad(ds, namedNode(SKOS.hasTopConcept), namedNode(conceptUri));
122
+ }
123
+
124
+ if (input.sourceRepoUrl) {
125
+ yield quad(ds, namedNode(PROV.wasDerivedFrom), namedNode(input.sourceRepoUrl));
126
+ }
127
+ if (input.publisherIri) {
128
+ yield quad(ds, namedNode(DCTERMS.publisher), namedNode(input.publisherIri));
129
+ }
130
+ if (input.contactIri) {
131
+ yield quad(ds, namedNode(DCAT.contactPoint), namedNode(input.contactIri));
132
+ }
133
+
134
+ for (const section of input.sections ?? []) {
135
+ yield* sectionToQuads(section);
136
+ }
137
+ }
138
+
139
+ function* distributionToQuads(ds, dist) {
140
+ // Distributions are blank nodes — they don't have stable identity
141
+ // outside their parent dataset.
142
+ const distSubject = namedNode(`_:b${deterministicId('dist', dist.id, dist.downloadUrl)}`);
143
+ yield quad(ds, namedNode(DCAT.distribution), distSubject);
144
+ yield quad(distSubject, namedNode(RDF_TYPE), namedNode(DCAT.Distribution));
145
+ yield quad(distSubject, namedNode(DCTERMS.title), literal(dist.title));
146
+ yield quad(distSubject, namedNode(DCAT.mediaType), literal(dist.mediaType));
147
+ yield quad(distSubject, namedNode(DCAT.downloadURL), namedNode(dist.downloadUrl));
148
+ if (dist.byteSize != null) {
149
+ yield quad(distSubject, namedNode(DCAT.byteSize), literal(String(dist.byteSize), namedNode(XSD_INTEGER)));
150
+ }
151
+ }
152
+
153
+ function* sectionToQuads(section) {
154
+ const coll = namedNode(section.collectionIri);
155
+ yield quad(coll, namedNode(RDF_TYPE), namedNode(DCAT.Collection));
156
+ yield quad(coll, namedNode(DCTERMS.title), literal(section.title));
157
+ for (const member of section.memberUris ?? []) {
158
+ yield quad(coll, namedNode(SKOS.member), namedNode(member));
159
+ }
160
+ if (section.parentCollectionIri) {
161
+ yield quad(coll, namedNode(GLOSS.hasParentSection), namedNode(section.parentCollectionIri));
162
+ }
163
+ for (const child of section.childCollectionIris ?? []) {
164
+ yield quad(coll, namedNode(GLOSS.hasChildSection), namedNode(child));
165
+ }
166
+ }
167
+
168
+ // Local deterministic-id helper (avoid pulling crypto-bearing module
169
+ // into this browser-safe file). Mirrors deterministicId's seed|join
170
+ // pattern using a small hash. Repeated calls within one process are
171
+ // stable; cross-process stability isn't required for blank node IDs.
172
+ function deterministicId(...parts) {
173
+ const seed = parts.filter(p => p != null).join('|');
174
+ let h = 0;
175
+ for (let i = 0; i < seed.length; i++) {
176
+ h = ((h << 5) - h + seed.charCodeAt(i)) | 0;
177
+ }
178
+ return (h >>> 0).toString(16).padStart(8, '0');
179
+ }
@@ -0,0 +1,132 @@
1
+ // Group emitter — emits dcat:DatasetSeries (lineage) or dcat:Catalog
2
+ // (topic/family/collection) for dataset groups. Mirrors concept-browser's
3
+ // group-emitter.ts.
4
+ //
5
+ // A lineage group (editions of the same vocabulary) is rendered as
6
+ // dcat:DatasetSeries with hasVersion links to each member edition.
7
+ // The current edition is linked via hasCurrentVersion.
8
+ //
9
+ // Other group kinds (topic, family, collection) are rendered as
10
+ // dcat:Catalog with dcat:dataset links to each member.
11
+ //
12
+ // Both kinds are also typed as skos:ConceptScheme for SKOS interop.
13
+
14
+ import { namedNode, literal, quad } from './terms.js';
15
+
16
+ // Hard-coded namespaces — see dataset-emitter.js for rationale.
17
+ const DCAT_NS = 'http://www.w3.org/ns/dcat#';
18
+ const DCTERMS_NS = 'http://purl.org/dc/terms/';
19
+ const SKOS_NS = 'http://www.w3.org/2004/02/skos/core#';
20
+ const PROV_NS = 'http://www.w3.org/ns/prov#';
21
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
22
+
23
+ const DCAT = {
24
+ DatasetSeries: `${DCAT_NS}DatasetSeries`,
25
+ Catalog: `${DCAT_NS}Catalog`,
26
+ ConceptScheme: `${SKOS_NS}ConceptScheme`,
27
+ hasVersion: `${DCAT_NS}hasVersion`,
28
+ hasCurrentVersion: `${DCAT_NS}hasCurrentVersion`,
29
+ dataset: `${DCAT_NS}dataset`,
30
+ theme: `${DCAT_NS}theme`,
31
+ keyword: `${DCAT_NS}keyword`,
32
+ contactPoint: `${DCAT_NS}contactPoint`,
33
+ };
34
+ const DCTERMS = {
35
+ title: `${DCTERMS_NS}title`,
36
+ description: `${DCTERMS_NS}description`,
37
+ identifier: `${DCTERMS_NS}identifier`,
38
+ subject: `${DCTERMS_NS}subject`,
39
+ publisher: `${DCTERMS_NS}publisher`,
40
+ };
41
+ const PROV = {
42
+ wasDerivedFrom: `${PROV_NS}wasDerivedFrom`,
43
+ };
44
+
45
+ /**
46
+ * @typedef {'lineage' | 'topic' | 'family' | 'collection' | 'default'} DatasetGroupKind
47
+ */
48
+ /**
49
+ * @typedef {Object} GroupEmitInput
50
+ * @property {string} groupId
51
+ * @property {string} groupIri
52
+ * @property {DatasetGroupKind} kind
53
+ * @property {string} title
54
+ * @property {string} [description]
55
+ * @property {readonly string[]} memberIris
56
+ * @property {string} [currentMemberIri]
57
+ * @property {string} [subject]
58
+ * @property {readonly string[]} [themes]
59
+ * @property {readonly string[]} [keywords]
60
+ * @property {string} [publisher]
61
+ * @property {string} [contact]
62
+ * @property {string} [sourceRepo]
63
+ */
64
+
65
+ // Returns the RDF class IRIs for a group kind. Empty for 'default'
66
+ // (no semantics — group is purely a presentation concern).
67
+ function rdfClassesForKind(kind) {
68
+ switch (kind) {
69
+ case 'lineage': return [DCAT.DatasetSeries, DCAT.ConceptScheme];
70
+ case 'topic':
71
+ case 'family':
72
+ case 'collection': return [DCAT.Catalog, DCAT.ConceptScheme];
73
+ default: return [];
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Emits dcat:DatasetSeries or dcat:Catalog quads for one group.
79
+ *
80
+ * Returns an empty generator (no quads) for 'default' kind — those
81
+ * groups have no RDF semantics.
82
+ *
83
+ * @param {GroupEmitInput} input
84
+ * @returns {Generator<Quad, void, unknown>}
85
+ */
86
+ export function* groupToQuads(input) {
87
+ const classes = rdfClassesForKind(input.kind);
88
+ if (classes.length === 0) return;
89
+
90
+ const g = namedNode(input.groupIri);
91
+
92
+ for (const cls of classes) {
93
+ yield quad(g, namedNode(RDF_TYPE), namedNode(cls));
94
+ }
95
+
96
+ yield quad(g, namedNode(DCTERMS.title), literal(input.title));
97
+ yield quad(g, namedNode(DCTERMS.identifier), literal(input.groupId));
98
+ if (input.description) {
99
+ yield quad(g, namedNode(DCTERMS.description), literal(input.description));
100
+ }
101
+ if (input.subject) {
102
+ yield quad(g, namedNode(DCTERMS.subject), literal(input.subject));
103
+ }
104
+ for (const theme of input.themes ?? []) {
105
+ yield quad(g, namedNode(DCAT.theme), namedNode(theme));
106
+ }
107
+ for (const kw of input.keywords ?? []) {
108
+ yield quad(g, namedNode(DCAT.keyword), literal(kw));
109
+ }
110
+ if (input.publisher) {
111
+ yield quad(g, namedNode(DCTERMS.publisher), namedNode(input.publisher));
112
+ }
113
+ if (input.contact) {
114
+ yield quad(g, namedNode(DCAT.contactPoint), namedNode(input.contact));
115
+ }
116
+ if (input.sourceRepo) {
117
+ yield quad(g, namedNode(PROV.wasDerivedFrom), namedNode(input.sourceRepo));
118
+ }
119
+
120
+ if (input.kind === 'lineage') {
121
+ for (const memberIri of input.memberIris ?? []) {
122
+ yield quad(g, namedNode(DCAT.hasVersion), namedNode(memberIri));
123
+ }
124
+ if (input.currentMemberIri) {
125
+ yield quad(g, namedNode(DCAT.hasCurrentVersion), namedNode(input.currentMemberIri));
126
+ }
127
+ } else {
128
+ for (const memberIri of input.memberIris ?? []) {
129
+ yield quad(g, namedNode(DCAT.dataset), namedNode(memberIri));
130
+ }
131
+ }
132
+ }
@@ -0,0 +1,77 @@
1
+ // Image variant emitter — emits foaf:Image per format/language variant
2
+ // of a dataset figure. Mirrors concept-browser's image-variant-emitter.ts.
3
+ //
4
+ // Each variant becomes a foaf:Image with:
5
+ // - dcterms:format (MIME type)
6
+ // - dcterms:language (when localized)
7
+ // - dcat:byteSize (when known)
8
+ // - dcat:downloadURL (canonical download)
9
+
10
+ import { namedNode, literal, quad } from './terms.js';
11
+
12
+ const FOAF_NS = 'http://xmlns.com/foaf/0.1/';
13
+ const DCTERMS_NS = 'http://purl.org/dc/terms/';
14
+ const DCAT_NS = 'http://www.w3.org/ns/dcat#';
15
+ const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
16
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
17
+
18
+ const FOAF = {
19
+ Image: `${FOAF_NS}Image`,
20
+ };
21
+ const DCTERMS = {
22
+ format: `${DCTERMS_NS}format`,
23
+ language: `${DCTERMS_NS}language`,
24
+ };
25
+ const DCAT = {
26
+ byteSize: `${DCAT_NS}byteSize`,
27
+ downloadURL: `${DCAT_NS}downloadURL`,
28
+ };
29
+ const XSD_INTEGER = `${XSD_NS}integer`;
30
+
31
+ const DEFAULT_BASE_URI = 'https://glossarist.org';
32
+
33
+ /**
34
+ * @typedef {'svg' | 'png' | 'webp' | 'jpg' | 'gif'} ImageFormat
35
+ */
36
+ /**
37
+ * @typedef {Object} ImageVariantInput
38
+ * @property {string} registerId
39
+ * @property {string} figureId
40
+ * @property {string} [lang]
41
+ * @property {ImageFormat} format
42
+ * @property {number} [byteSize]
43
+ * @property {string} downloadUrl
44
+ */
45
+
46
+ const MIME_BY_FORMAT = Object.freeze({
47
+ svg: 'image/svg+xml',
48
+ png: 'image/png',
49
+ webp: 'image/webp',
50
+ jpg: 'image/jpeg',
51
+ gif: 'image/gif',
52
+ });
53
+
54
+ /**
55
+ * Returns the canonical IRI for an image variant. Includes the lang
56
+ * segment when the variant is localized, otherwise omits it.
57
+ */
58
+ export function imageVariantIri(input, baseUri = DEFAULT_BASE_URI) {
59
+ const langSuffix = input.lang ? `${input.lang}.` : '';
60
+ return `${baseUri}/${input.registerId}/image/${input.figureId}/${langSuffix}${input.format}`;
61
+ }
62
+
63
+ /**
64
+ * Emits foaf:Image quads for one image variant.
65
+ */
66
+ export function* imageVariantToQuads(input, baseUri = DEFAULT_BASE_URI) {
67
+ const img = namedNode(imageVariantIri(input, baseUri));
68
+ yield quad(img, namedNode(RDF_TYPE), namedNode(FOAF.Image));
69
+ yield quad(img, namedNode(DCTERMS.format), literal(MIME_BY_FORMAT[input.format] ?? 'application/octet-stream'));
70
+ if (input.lang) {
71
+ yield quad(img, namedNode(DCTERMS.language), literal(input.lang));
72
+ }
73
+ if (input.byteSize != null) {
74
+ yield quad(img, namedNode(DCAT.byteSize), literal(String(input.byteSize), namedNode(XSD_INTEGER)));
75
+ }
76
+ yield quad(img, namedNode(DCAT.downloadURL), namedNode(input.downloadUrl));
77
+ }
@@ -97,6 +97,158 @@ export type NonVerbalEntity = Figure | Table | Formula;
97
97
  export declare function nonVerbalEntityUri(entity: NonVerbalEntity, options: NonVerbalEntityEmitOptions): string;
98
98
  export declare function nonVerbalEntityToQuads(entity: NonVerbalEntity, options: NonVerbalEntityEmitOptions): Generator<Quad, void, unknown>;
99
99
 
100
+ // Vocabulary emitter (SKOS ConceptSchemes for enumeration IRIs)
101
+ export interface VocabTerm {
102
+ iri: string;
103
+ label: string;
104
+ }
105
+ export interface VocabScheme {
106
+ schemeIri: string;
107
+ label: string;
108
+ terms: readonly VocabTerm[];
109
+ }
110
+ export declare function resolveIri(iri: string): string;
111
+ export declare function vocabularySchemeToQuads(scheme: VocabScheme): Generator<Quad, void, unknown>;
112
+ export declare function vocabularyToQuads(schemes: readonly VocabScheme[]): Generator<Quad, void, unknown>;
113
+
114
+ // Dataset emitter (dcat:Dataset + skos:ConceptScheme per register)
115
+ export interface DatasetDistribution {
116
+ id: string;
117
+ title: string;
118
+ mediaType: string;
119
+ downloadUrl: string;
120
+ byteSize?: number;
121
+ }
122
+ export interface DatasetSection {
123
+ collectionIri: string;
124
+ title: string;
125
+ memberUris: readonly string[];
126
+ parentCollectionIri?: string;
127
+ childCollectionIris?: readonly string[];
128
+ }
129
+ export interface DatasetEmitterInput {
130
+ datasetIri: string;
131
+ registerId: string;
132
+ title: string;
133
+ description?: string;
134
+ modified: string;
135
+ languages: readonly string[];
136
+ distributions: readonly DatasetDistribution[];
137
+ topConceptUris: readonly string[];
138
+ sections: readonly DatasetSection[];
139
+ sourceRepoUrl?: string;
140
+ publisherIri?: string;
141
+ contactIri?: string;
142
+ }
143
+ export declare function datasetToQuads(input: DatasetEmitterInput): Generator<Quad, void, unknown>;
144
+
145
+ // Group emitter (dcat:DatasetSeries or dcat:Catalog)
146
+ export type DatasetGroupKind = 'lineage' | 'topic' | 'family' | 'collection' | 'default';
147
+ export interface GroupEmitInput {
148
+ groupId: string;
149
+ groupIri: string;
150
+ kind: DatasetGroupKind;
151
+ title: string;
152
+ description?: string;
153
+ memberIris: readonly string[];
154
+ currentMemberIri?: string;
155
+ subject?: string;
156
+ themes?: readonly string[];
157
+ keywords?: readonly string[];
158
+ publisher?: string;
159
+ contact?: string;
160
+ sourceRepo?: string;
161
+ }
162
+ export declare function groupToQuads(input: GroupEmitInput): Generator<Quad, void, unknown>;
163
+
164
+ // Bibliography emitter (dcterms:BibliographicResource)
165
+ export interface BibliographyEntry {
166
+ id: string;
167
+ reference: string;
168
+ title?: string;
169
+ link?: string;
170
+ type?: string;
171
+ }
172
+ export interface BibliographyInput {
173
+ registerId: string;
174
+ entries: readonly BibliographyEntry[];
175
+ baseUri?: string;
176
+ }
177
+ export declare function bibliographyEntryIri(registerId: string, entryId: string, baseUri?: string): string;
178
+ export declare function bibliographyToQuads(input: BibliographyInput): Generator<Quad, void, unknown>;
179
+ export declare function normalizeBibliographyData(raw: unknown): BibliographyEntry[];
180
+
181
+ // Build activity emitter (prov:Activity per build run)
182
+ export interface BuildActivityInput {
183
+ runId: string;
184
+ startedAt: string;
185
+ endedAt: string;
186
+ gitSha?: string;
187
+ gitBranch?: string;
188
+ toolId: string;
189
+ toolVersion: string;
190
+ datasetRegisters: readonly string[];
191
+ conceptCount: number;
192
+ associatedAgentIri?: string;
193
+ baseUri?: string;
194
+ }
195
+ export declare function buildActivityIri(input: Pick<BuildActivityInput, 'runId'>): string;
196
+ export declare function buildActivityToQuads(input: BuildActivityInput): Generator<Quad, void, unknown>;
197
+
198
+ // Agents emitter (foaf:Person / prov:Organization)
199
+ export interface Contributor {
200
+ name: string;
201
+ role?: string;
202
+ organization?: string;
203
+ url?: string;
204
+ email?: string;
205
+ }
206
+ export interface AgentInput extends Contributor {
207
+ slug: string;
208
+ agentIri: string;
209
+ }
210
+ export declare function slugify(input: string): string;
211
+ export declare function agentsFromContributors(contributors: readonly Contributor[], agentBase?: string): AgentInput[];
212
+ export declare function agentsToQuads(agents: readonly AgentInput[], options?: { orgBase?: string }): Generator<Quad, void, unknown>;
213
+
214
+ // Version emitter (prov:Entity version chain)
215
+ export interface DatasetVersionInput {
216
+ registerId: string;
217
+ version: string;
218
+ versionIri: string;
219
+ datasetIri: string;
220
+ generatedAt: string;
221
+ previousVersionIri?: string;
222
+ changeSummary?: string;
223
+ associatedAgentIri?: string;
224
+ }
225
+ export interface VersionHistoryEntry {
226
+ version: string;
227
+ generatedAt: string;
228
+ changeSummary?: string;
229
+ }
230
+ export interface VersionEmitAllInput {
231
+ registerId: string;
232
+ datasetIri: string;
233
+ versions: readonly VersionHistoryEntry[];
234
+ associatedAgentIri?: string;
235
+ }
236
+ export declare function versionToQuads(input: DatasetVersionInput): Generator<Quad, void, unknown>;
237
+ export declare function versionHistoryToQuads(input: VersionEmitAllInput): Generator<Quad, void, unknown>;
238
+
239
+ // Image variant emitter (foaf:Image per format/language variant)
240
+ export type ImageFormat = 'svg' | 'png' | 'webp' | 'jpg' | 'gif';
241
+ export interface ImageVariantInput {
242
+ registerId: string;
243
+ figureId: string;
244
+ lang?: string;
245
+ format: ImageFormat;
246
+ byteSize?: number;
247
+ downloadUrl: string;
248
+ }
249
+ export declare function imageVariantIri(input: ImageVariantInput, baseUri?: string): string;
250
+ export declare function imageVariantToQuads(input: ImageVariantInput, baseUri?: string): Generator<Quad, void, unknown>;
251
+
100
252
  export declare function collectQuads(quadsIterable: Iterable<Quad>): Quad[];
101
253
 
102
254
  export interface WriteTurtleOptions {
package/src/rdf/index.js CHANGED
@@ -22,6 +22,18 @@ export {
22
22
  nonVerbalEntityToQuads,
23
23
  nonVerbalEntityUri,
24
24
  } from './gloss-non-verbal-rep.js';
25
+ export {
26
+ vocabularySchemeToQuads,
27
+ vocabularyToQuads,
28
+ resolveIri,
29
+ } from './vocabulary-emitter.js';
30
+ export { datasetToQuads } from './dataset-emitter.js';
31
+ export { groupToQuads } from './group-emitter.js';
32
+ export { bibliographyToQuads, bibliographyEntryIri, normalizeBibliographyData } from './bibliography-emitter.js';
33
+ export { buildActivityIri, buildActivityToQuads } from './build-activity-emitter.js';
34
+ export { agentsFromContributors, agentsToQuads, slugify } from './agents-emitter.js';
35
+ export { versionToQuads, versionHistoryToQuads } from './version-emitter.js';
36
+ export { imageVariantIri, imageVariantToQuads } from './image-variant-emitter.js';
25
37
  export {
26
38
  collectQuads,
27
39
  writeTurtle,
@@ -0,0 +1,96 @@
1
+ // Version emitter — emits prov:Entity version chain per dataset.
2
+ // Mirrors concept-browser's version-emitter.ts.
3
+ //
4
+ // Each version of a dataset becomes a prov:Entity typed resource with:
5
+ // - dcterms:isVersionOf → the dataset
6
+ // - prov:wasRevisionOf → previous version (when provided)
7
+ // - prov:generatedAtTime (xsd:dateTime)
8
+ // - dcterms:description (optional change summary)
9
+ // - prov:wasAssociatedWith (optional agent)
10
+
11
+ import { namedNode, literal, quad } from './terms.js';
12
+
13
+ const PROV_NS = 'http://www.w3.org/ns/prov#';
14
+ const DCTERMS_NS = 'http://purl.org/dc/terms/';
15
+ const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
16
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
17
+
18
+ const PROV = {
19
+ Entity: `${PROV_NS}Entity`,
20
+ wasRevisionOf: `${PROV_NS}wasRevisionOf`,
21
+ generatedAtTime: `${PROV_NS}generatedAtTime`,
22
+ wasAssociatedWith: `${PROV_NS}wasAssociatedWith`,
23
+ };
24
+ const DCTERMS = {
25
+ isVersionOf: `${DCTERMS_NS}isVersionOf`,
26
+ description: `${DCTERMS_NS}description`,
27
+ };
28
+ const XSD_DATE_TIME = `${XSD_NS}dateTime`;
29
+
30
+ /**
31
+ * @typedef {Object} DatasetVersionInput
32
+ * @property {string} registerId
33
+ * @property {string} version
34
+ * @property {string} versionIri
35
+ * @property {string} datasetIri
36
+ * @property {string} generatedAt — ISO date-time
37
+ * @property {string} [previousVersionIri]
38
+ * @property {string} [changeSummary]
39
+ * @property {string} [associatedAgentIri]
40
+ */
41
+ /**
42
+ * @typedef {Object} VersionHistoryEntry
43
+ * @property {string} version
44
+ * @property {string} generatedAt
45
+ * @property {string} [changeSummary]
46
+ */
47
+ /**
48
+ * @typedef {Object} VersionEmitAllInput
49
+ * @property {string} registerId
50
+ * @property {string} datasetIri
51
+ * @property {readonly VersionHistoryEntry[]} versions
52
+ * @property {string} [associatedAgentIri]
53
+ */
54
+
55
+ /**
56
+ * Emits a single prov:Entity version with its provenance chain.
57
+ */
58
+ export function* versionToQuads(input) {
59
+ const v = namedNode(input.versionIri);
60
+ yield quad(v, namedNode(RDF_TYPE), namedNode(PROV.Entity));
61
+ yield quad(v, namedNode(DCTERMS.isVersionOf), namedNode(input.datasetIri));
62
+ if (input.previousVersionIri) {
63
+ yield quad(v, namedNode(PROV.wasRevisionOf), namedNode(input.previousVersionIri));
64
+ }
65
+ yield quad(v, namedNode(PROV.generatedAtTime), literal(input.generatedAt, namedNode(XSD_DATE_TIME)));
66
+ if (input.changeSummary) {
67
+ yield quad(v, namedNode(DCTERMS.description), literal(input.changeSummary));
68
+ }
69
+ if (input.associatedAgentIri) {
70
+ yield quad(v, namedNode(PROV.wasAssociatedWith), namedNode(input.associatedAgentIri));
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Emits the full version history chain. Versions are linked in
76
+ * sequence via prov:wasRevisionOf (newest → previous → ... → oldest).
77
+ *
78
+ * The version IRI is constructed as `${datasetIri}versions/${version}`.
79
+ */
80
+ export function* versionHistoryToQuads(input) {
81
+ let previousIri;
82
+ for (const v of input.versions ?? []) {
83
+ const versionIri = `${input.datasetIri}versions/${v.version}`;
84
+ yield* versionToQuads({
85
+ registerId: input.registerId,
86
+ version: v.version,
87
+ versionIri,
88
+ datasetIri: input.datasetIri,
89
+ generatedAt: v.generatedAt,
90
+ previousVersionIri: previousIri,
91
+ changeSummary: v.changeSummary,
92
+ associatedAgentIri: input.associatedAgentIri,
93
+ });
94
+ previousIri = versionIri;
95
+ }
96
+ }
@@ -0,0 +1,87 @@
1
+ // Vocabulary graph emitter — emits SKOS ConceptSchemes for the
2
+ // enumeration IRIs that glossarist emits in instance data
3
+ // (gloss:status/*, gloss:entstatus/*, gloss:norm/*, gloss:srcstatus/*,
4
+ // gloss:srctype/*, gloss:datetype/*, gloss:rel/*, etc.).
5
+ //
6
+ // Each enumeration value becomes a skos:Concept in its corresponding
7
+ // skos:ConceptScheme. The scheme uses skos:hasTopConcept to link to
8
+ // its members; members use skos:inScheme to link back.
9
+ //
10
+ // Accepts the schemes data as a plain object — no fs deps, browser-safe.
11
+ // Callers can load the JSON file themselves (Node) or construct the
12
+ // data inline. CURIEs in the input are resolved to absolute IRIs via
13
+ // PREFIXES.
14
+
15
+ import { namedNode, literal, quad } from './terms.js';
16
+ import { PREFIXES } from './predicates.js';
17
+
18
+ const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
19
+ const RDF_LABEL = 'http://www.w3.org/2000/01/rdf-schema#label';
20
+ const SKOS_NS = PREFIXES.skos;
21
+
22
+ /**
23
+ * @typedef {Object} VocabTerm
24
+ * @property {string} iri — absolute IRI or CURIE (resolved via PREFIXES)
25
+ * @property {string} label — human-readable label (typically English)
26
+ */
27
+ /**
28
+ * @typedef {Object} VocabScheme
29
+ * @property {string} schemeIri — absolute IRI or CURIE
30
+ * @property {string} label
31
+ * @property {readonly VocabTerm[]} terms
32
+ */
33
+
34
+ /**
35
+ * Resolves a CURIE (e.g. "gloss:status/valid") to an absolute IRI
36
+ * using PREFIXES. Pass-through if already absolute.
37
+ */
38
+ function resolveIri(iri) {
39
+ if (iri.startsWith('http://') || iri.startsWith('https://') || iri.startsWith('urn:')) {
40
+ return iri;
41
+ }
42
+ const colonIdx = iri.indexOf(':');
43
+ if (colonIdx < 0) return iri;
44
+ const prefix = iri.slice(0, colonIdx);
45
+ const local = iri.slice(colonIdx + 1);
46
+ const base = PREFIXES[prefix];
47
+ return base ? `${base}${local}` : iri;
48
+ }
49
+
50
+ /**
51
+ * Emits SKOS ConceptScheme + Concept quads for one scheme.
52
+ *
53
+ * @param {VocabScheme} scheme
54
+ * @returns {Generator<Quad, void, unknown>}
55
+ */
56
+ export function* vocabularySchemeToQuads(scheme) {
57
+ const schemeIri = resolveIri(scheme.schemeIri);
58
+ const schemeSubject = namedNode(schemeIri);
59
+
60
+ yield quad(schemeSubject, namedNode(RDF_TYPE), namedNode(`${SKOS_NS}ConceptScheme`));
61
+ yield quad(schemeSubject, namedNode(RDF_LABEL), literal(scheme.label, 'en'));
62
+
63
+ for (const term of scheme.terms ?? []) {
64
+ const termIri = resolveIri(term.iri);
65
+ const termSubject = namedNode(termIri);
66
+
67
+ yield quad(termSubject, namedNode(RDF_TYPE), namedNode(`${SKOS_NS}Concept`));
68
+ yield quad(termSubject, namedNode(RDF_LABEL), literal(term.label, 'en'));
69
+ yield quad(termSubject, namedNode(`${SKOS_NS}inScheme`), schemeSubject);
70
+ yield quad(schemeSubject, namedNode(`${SKOS_NS}hasTopConcept`), termSubject);
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Emits SKOS ConceptScheme + Concept quads for many schemes.
76
+ *
77
+ * @param {readonly VocabScheme[]} schemes
78
+ * @returns {Generator<Quad, void, unknown>}
79
+ */
80
+ export function* vocabularyToQuads(schemes) {
81
+ for (const scheme of schemes ?? []) {
82
+ yield* vocabularySchemeToQuads(scheme);
83
+ }
84
+ }
85
+
86
+ // Re-exported for callers that want to resolve CURIEs themselves.
87
+ export { resolveIri };