glossarist 0.4.12 → 0.4.13

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.12",
3
+ "version": "0.4.13",
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",
@@ -91,10 +91,10 @@
91
91
  "@rdfjs/data-model": "^2.0.0",
92
92
  "@rdfjs/dataset": "^2.0.0",
93
93
  "js-yaml": "^5.0.0",
94
- "jsonld": "^8.3.3",
94
+ "jsonld": "^9.0.0",
95
95
  "jszip": "^3.10.1",
96
96
  "n3": "^1.17.0",
97
- "rdf-validate-shacl": "^0.4.0"
97
+ "rdf-validate-shacl": "^0.6.5"
98
98
  },
99
99
  "devDependencies": {
100
100
  "@eslint/js": "^10.0.1",
@@ -5,7 +5,12 @@ import { BibliographyEntry } from './bibliography-entry.js';
5
5
  export class BibliographyData extends GlossaristModel {
6
6
  constructor(data = {}) {
7
7
  super();
8
- const entriesData = data.bibliography ?? data.entries ?? [];
8
+ let entriesData;
9
+ if (Array.isArray(data)) {
10
+ entriesData = data;
11
+ } else {
12
+ entriesData = data?.bibliography ?? data?.entries ?? [];
13
+ }
9
14
  this._rawEntries = Array.isArray(entriesData) ? entriesData : [];
10
15
  this._entries = null;
11
16
  }
@@ -58,6 +58,7 @@ export const REGISTER_STATUSES: readonly string[];
58
58
  export class Register extends GlossaristModel {
59
59
  readonly schemaVersion: string;
60
60
  readonly id: string | null;
61
+ readonly name: string | Record<string, string> | null;
61
62
  readonly ref: string | null;
62
63
  readonly refAliases: string[];
63
64
  readonly year: number | null;
@@ -79,6 +80,7 @@ export class Register extends GlossaristModel {
79
80
  readonly sections: Section[];
80
81
  sectionById(id: string): Section | null;
81
82
  sectionName(sectionId: string, lang: string): string | null;
83
+ displayName(lang: string): string | null;
82
84
  static fromJSON(data: Record<string, unknown>): Register;
83
85
  }
84
86
 
@@ -14,6 +14,7 @@ export class Register extends GlossaristModel {
14
14
  super();
15
15
  this.schemaVersion = data.schema_version ?? data.schemaVersion ?? '3';
16
16
  this.id = data.id ?? null;
17
+ this.name = data.name ?? null;
17
18
  this.ref = data.ref ?? null;
18
19
  this.refAliases = data.refAliases ?? data.ref_aliases ?? [];
19
20
  this.year = data.year ?? null;
@@ -42,7 +43,7 @@ export class Register extends GlossaristModel {
42
43
  _extractRaw(data) {
43
44
  const known = new Set([
44
45
  'schema_version', 'schemaVersion',
45
- 'id', 'ref', 'refAliases', 'ref_aliases',
46
+ 'id', 'name', 'ref', 'refAliases', 'ref_aliases',
46
47
  'year', 'urn', 'urnAliases', 'urn_aliases',
47
48
  'status', 'supersedes', 'owner',
48
49
  'sourceRepo', 'source_repo', 'tags',
@@ -138,6 +139,24 @@ export class Register extends GlossaristModel {
138
139
  return section ? section.name(lang) : null;
139
140
  }
140
141
 
142
+ // Localized display name. Returns the best-matching name string for
143
+ // the requested language, falling back to English, then to the first
144
+ // available language, then to null. Accepts three input shapes:
145
+ // - null/undefined → null
146
+ // - string → returned as-is for every language
147
+ // - { eng, fra, ... } → language-resolved
148
+ displayName(lang) {
149
+ if (this.name == null) return null;
150
+ if (typeof this.name === 'string') return this.name;
151
+ if (typeof this.name === 'object') {
152
+ return this.name[lang]
153
+ ?? this.name.eng
154
+ ?? Object.values(this.name)[0]
155
+ ?? null;
156
+ }
157
+ return null;
158
+ }
159
+
141
160
  // Returns the hex color for the requested UI mode, falling back to
142
161
  // the single hex when the spec is a string. Returns null when no
143
162
  // color is set or when the requested mode is missing.
@@ -148,6 +167,7 @@ export class Register extends GlossaristModel {
148
167
  toJSON() {
149
168
  const obj = { ...this._raw, schema_version: this.schemaVersion };
150
169
  if (this.id != null) obj.id = this.id;
170
+ if (this.name != null) obj.name = this.name;
151
171
  if (this.ref != null) obj.ref = this.ref;
152
172
  if (this.refAliases.length > 0) obj.refAliases = [...this.refAliases];
153
173
  if (this.year != null) obj.year = this.year;
@@ -7,12 +7,13 @@
7
7
  // prov:Organization, and the person is linked via prov:actedOnBehalfOf.
8
8
 
9
9
  import { namedNode, literal, quad } from './terms.js';
10
+ import { PREFIXES } from './predicates.js';
11
+ import { RDF_TYPE } from './curie.js';
10
12
 
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';
13
+ const PROV_NS = PREFIXES.prov;
14
+ const FOAF_NS = PREFIXES.foaf ?? 'http://xmlns.com/foaf/0.1/';
15
+ const DCTERMS_NS = PREFIXES.dcterms;
16
+ const RDFS_NS = PREFIXES.rdfs;
16
17
 
17
18
  const PROV = {
18
19
  Person: `${PROV_NS}Person`,
@@ -10,11 +10,14 @@
10
10
  // - dcterms:isPartOf (parent dataset)
11
11
 
12
12
  import { namedNode, literal, quad } from './terms.js';
13
+ import { PREFIXES } from './predicates.js';
14
+ import { RDF_TYPE } from './curie.js';
13
15
 
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';
16
+ // Namespace IRIs from canonical PREFIXES. foaf is absent from the
17
+ // JSON-LD context (therefore not in PREFIXES) — declare locally.
18
+ const DCTERMS_NS = PREFIXES.dcterms;
19
+ const FOAF_NS = PREFIXES.foaf ?? 'http://xmlns.com/foaf/0.1/';
20
+ const GLOSS_NS = PREFIXES.gloss;
18
21
 
19
22
  const DCTERMS = {
20
23
  BibliographicResource: `${DCTERMS_NS}BibliographicResource`,
@@ -0,0 +1,35 @@
1
+ // Deterministic blank node ID strategy.
2
+ //
3
+ // Single source of truth for bnode IDs across all emitters. Previously
4
+ // each emitter had its own local FNV-1a hash, producing different IDs
5
+ // for the same logical bnode. This module replaces those with one
6
+ // strategy: MD5 (via node:crypto) when available, FNV-1a fallback for
7
+ // browser bundles.
8
+ //
9
+ // Cross-process stability matches glossarist-ruby's deterministic-id
10
+ // when node:crypto is available. Within a single process, FNV-1a is
11
+ // also stable.
12
+
13
+ import { createHash } from 'node:crypto';
14
+
15
+ function fnv1a(seed) {
16
+ let h = 0x811c9dc5;
17
+ for (let i = 0; i < seed.length; i++) {
18
+ h ^= seed.charCodeAt(i);
19
+ h = Math.imul(h, 0x01000193);
20
+ }
21
+ return (h >>> 0).toString(16).padStart(8, '0');
22
+ }
23
+
24
+ export function deterministicBnodeId(...parts) {
25
+ const seed = parts.filter(p => p != null && p !== '').join('|');
26
+ if (!seed) return `b${fnv1a('empty')}`;
27
+ // MD5 matches glossarist-ruby's deterministic-id for cross-process
28
+ // stability. Truncated to 12 hex chars — enough collision resistance
29
+ // for bnode IDs within a single document.
30
+ try {
31
+ return createHash('md5').update(seed).digest('hex').slice(0, 12);
32
+ } catch {
33
+ return `b${fnv1a(seed)}`;
34
+ }
35
+ }
@@ -9,13 +9,14 @@
9
9
  // - prov:wasAssociatedWith for the CI bot agent
10
10
 
11
11
  import { namedNode, literal, quad } from './terms.js';
12
+ import { PREFIXES } from './predicates.js';
13
+ import { RDF_TYPE } from './curie.js';
12
14
 
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';
15
+ const PROV_NS = PREFIXES.prov;
16
+ const DCTERMS_NS = PREFIXES.dcterms;
17
+ const FOAF_NS = PREFIXES.foaf ?? 'http://xmlns.com/foaf/0.1/';
18
+ const GLOSS_NS = PREFIXES.gloss;
19
+ const XSD_NS = PREFIXES.xsd;
19
20
 
20
21
  const PROV = {
21
22
  Activity: `${PROV_NS}Activity`,
@@ -0,0 +1,45 @@
1
+ // CURIE helpers — resolve/compact IRIs against the canonical PREFIXES.
2
+ //
3
+ // Single source of truth for CURIE ↔ IRI conversion. Previously
4
+ // `resolveIri` was private to vocabulary-emitter and reimplemented
5
+ // (incorrectly) in other emitters. Callers now import from here.
6
+
7
+ import { PREFIXES } from './prefixes.js';
8
+
9
+ const ABSOLUTE_SCHEMES = new Set(['http', 'https', 'urn', 'file', 'mailto', 'ftp']);
10
+
11
+ export function isAbsoluteIri(s) {
12
+ if (typeof s !== 'string' || s.length === 0) return false;
13
+ if (s.startsWith('urn:')) return true;
14
+ const colon = s.indexOf(':');
15
+ if (colon < 1) return false;
16
+ return ABSOLUTE_SCHEMES.has(s.slice(0, colon));
17
+ }
18
+
19
+ export function resolveIri(iri, prefixes = PREFIXES) {
20
+ if (isAbsoluteIri(iri)) return iri;
21
+ const colon = iri.indexOf(':');
22
+ if (colon < 1) return iri;
23
+ const prefix = iri.slice(0, colon);
24
+ const local = iri.slice(colon + 1);
25
+ const base = prefixes[prefix];
26
+ return base ? `${base}${local}` : iri;
27
+ }
28
+
29
+ export function compactIri(iri, prefixes = PREFIXES) {
30
+ let best = null;
31
+ let bestLen = 0;
32
+ for (const [prefix, base] of Object.entries(prefixes)) {
33
+ if (iri.startsWith(base) && base.length > bestLen) {
34
+ best = `${prefix}:${iri.slice(base.length)}`;
35
+ bestLen = base.length;
36
+ }
37
+ }
38
+ return best ?? iri;
39
+ }
40
+
41
+ // Convenience constants — used by every emitter. Centralized here so
42
+ // the IRI string appears in exactly one place.
43
+ export const RDF_TYPE = `${PREFIXES.rdf}type`;
44
+ export const RDF_VALUE = `${PREFIXES.rdf}value`;
45
+ export const RDFS_LABEL = `${PREFIXES.rdfs}label`;
@@ -11,17 +11,17 @@
11
11
  // for cascading section membership).
12
12
 
13
13
  import { namedNode, blankNode, literal, quad } from './terms.js';
14
+ import { PREFIXES } from './predicates.js';
15
+ import { RDF_TYPE } from './curie.js';
16
+ import { deterministicBnodeId } from './bnode-id.js';
14
17
 
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#';
18
+ // Namespace IRIs from the canonical PREFIXES — single source of truth.
19
+ const DCAT_NS = PREFIXES.dcat ?? 'http://www.w3.org/ns/dcat#';
20
+ const DCTERMS_NS = PREFIXES.dcterms;
21
+ const SKOS_NS = PREFIXES.skos;
22
+ const PROV_NS = PREFIXES.prov;
23
+ const GLOSS_NS = PREFIXES.gloss;
24
+ const XSD_NS = PREFIXES.xsd;
25
25
 
26
26
  /**
27
27
  * @typedef {Object} DatasetDistribution
@@ -87,7 +87,6 @@ const GLOSS = {
87
87
  };
88
88
  const XSD_DATE = `${XSD_NS}date`;
89
89
  const XSD_INTEGER = `${XSD_NS}integer`;
90
- const RDF_TYPE = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type';
91
90
 
92
91
  /**
93
92
  * Emits dcat:Dataset + skos:ConceptScheme quads for one register's
@@ -141,7 +140,7 @@ function* distributionToQuads(ds, dist) {
141
140
  // outside their parent dataset. Using blankNode() (not namedNode with
142
141
  // a "_:..." string) so the n3 writer serializes them as _:bXXXX
143
142
  // rather than <_:bXXXX> which would be an invalid IRI.
144
- const distSubject = blankNode(`b${deterministicId('dist', dist.id, dist.downloadUrl)}`);
143
+ const distSubject = blankNode(`b${deterministicBnodeId('dist', dist.id, dist.downloadUrl)}`);
145
144
  yield quad(ds, namedNode(DCAT.distribution), distSubject);
146
145
  yield quad(distSubject, namedNode(RDF_TYPE), namedNode(DCAT.Distribution));
147
146
  yield quad(distSubject, namedNode(DCTERMS.title), literal(dist.title));
@@ -167,15 +166,5 @@ function* sectionToQuads(section) {
167
166
  }
168
167
  }
169
168
 
170
- // Local deterministic-id helper (avoid pulling crypto-bearing module
171
- // into this browser-safe file). Mirrors deterministicId's seed|join
172
- // pattern using a small hash. Repeated calls within one process are
173
- // stable; cross-process stability isn't required for blank node IDs.
174
- function deterministicId(...parts) {
175
- const seed = parts.filter(p => p != null).join('|');
176
- let h = 0;
177
- for (let i = 0; i < seed.length; i++) {
178
- h = ((h << 5) - h + seed.charCodeAt(i)) | 0;
179
- }
180
- return (h >>> 0).toString(16).padStart(8, '0');
181
- }
169
+ // Local deterministic-id helper is no longer needed — using the shared
170
+ // deterministicBnodeId from bnode-id.js for cross-emitter consistency.
@@ -12,13 +12,14 @@
12
12
  // Both kinds are also typed as skos:ConceptScheme for SKOS interop.
13
13
 
14
14
  import { namedNode, literal, quad } from './terms.js';
15
+ import { PREFIXES } from './predicates.js';
16
+ import { RDF_TYPE } from './curie.js';
15
17
 
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';
18
+ // Namespace IRIs from canonical PREFIXES.
19
+ const DCAT_NS = PREFIXES.dcat ?? 'http://www.w3.org/ns/dcat#';
20
+ const DCTERMS_NS = PREFIXES.dcterms;
21
+ const SKOS_NS = PREFIXES.skos;
22
+ const PROV_NS = PREFIXES.prov;
22
23
 
23
24
  const DCAT = {
24
25
  DatasetSeries: `${DCAT_NS}DatasetSeries`,
@@ -8,12 +8,13 @@
8
8
  // - dcat:downloadURL (canonical download)
9
9
 
10
10
  import { namedNode, literal, quad } from './terms.js';
11
+ import { PREFIXES } from './predicates.js';
12
+ import { RDF_TYPE } from './curie.js';
11
13
 
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';
14
+ const FOAF_NS = PREFIXES.foaf ?? 'http://xmlns.com/foaf/0.1/';
15
+ const DCTERMS_NS = PREFIXES.dcterms;
16
+ const DCAT_NS = PREFIXES.dcat ?? 'http://www.w3.org/ns/dcat#';
17
+ const XSD_NS = PREFIXES.xsd;
17
18
 
18
19
  const FOAF = {
19
20
  Image: `${FOAF_NS}Image`,
@@ -107,7 +107,46 @@ export interface VocabScheme {
107
107
  label: string;
108
108
  terms: readonly VocabTerm[];
109
109
  }
110
- export declare function resolveIri(iri: string): string;
110
+
111
+ // ── CURIE + bnode helpers ─────────────────────────────────────────────
112
+ export declare const RDF_TYPE: string;
113
+ export declare const RDF_VALUE: string;
114
+ export declare const RDFS_LABEL: string;
115
+ export declare function isAbsoluteIri(s: string): boolean;
116
+ export declare function resolveIri(iri: string, prefixes?: Record<string, string>): string;
117
+ export declare function compactIri(iri: string, prefixes?: Record<string, string>): string;
118
+ export declare function deterministicBnodeId(...parts: Array<string | number | null | undefined>): string;
119
+
120
+ // ── Sections builder (Quad[] → ClassInstance[] for UI) ────────────────
121
+ export interface PropValue {
122
+ predicate: string;
123
+ values: string[];
124
+ nested?: boolean;
125
+ }
126
+ export interface ClassInstance {
127
+ classId: string;
128
+ classLabel: string;
129
+ label: string;
130
+ props: PropValue[];
131
+ }
132
+ export interface SectionsBuilderOptions {
133
+ language?: string;
134
+ }
135
+ export declare function quadSectionsToClassInstances(
136
+ quads: readonly Quad[],
137
+ options?: SectionsBuilderOptions,
138
+ ): ClassInstance[];
139
+
140
+ // ── Provenance emitter (subject → prov:wasGeneratedBy + Activity) ─────
141
+ export interface ProvenanceInput {
142
+ subjectUri: string;
143
+ serializer: string;
144
+ serializerVersion: string;
145
+ generatedAt: string;
146
+ canonicalUri?: string;
147
+ }
148
+ export declare function provenanceToQuads(input: ProvenanceInput): Generator<Quad, void, unknown>;
149
+
111
150
  export declare function vocabularySchemeToQuads(scheme: VocabScheme): Generator<Quad, void, unknown>;
112
151
  export declare function vocabularyToQuads(schemes: readonly VocabScheme[]): Generator<Quad, void, unknown>;
113
152
 
package/src/rdf/index.js CHANGED
@@ -1,5 +1,14 @@
1
1
  // Public RDF layer. Mirrors lib/glossarist/rdf/ in glossarist-ruby.
2
2
  export { PRED, PREFIXES, SKOSXL, WELL_KNOWN } from './prefixes.js';
3
+ export {
4
+ resolveIri,
5
+ compactIri,
6
+ isAbsoluteIri,
7
+ RDF_TYPE,
8
+ RDF_VALUE,
9
+ RDFS_LABEL,
10
+ } from './curie.js';
11
+ export { deterministicBnodeId } from './bnode-id.js';
3
12
  export { deterministicId, deterministicBnode } from './deterministic-id.js';
4
13
  export { namedNode, blankNode, literal, defaultGraph, quad } from './terms.js';
5
14
  export {
@@ -25,7 +34,6 @@ export {
25
34
  export {
26
35
  vocabularySchemeToQuads,
27
36
  vocabularyToQuads,
28
- resolveIri,
29
37
  } from './vocabulary-emitter.js';
30
38
  export { datasetToQuads } from './dataset-emitter.js';
31
39
  export { groupToQuads } from './group-emitter.js';
@@ -43,3 +51,5 @@ export {
43
51
  } from './document-writer.js';
44
52
  export { writeTurtleSync } from './write-turtle-sync.js';
45
53
  export { validateShacl, loadShapes, clearShapesCache, quadsToDataset } from './shacl.js';
54
+ export { quadSectionsToClassInstances } from './sections-builder.js';
55
+ export { provenanceToQuads } from './provenance-emitter.js';
@@ -0,0 +1,87 @@
1
+ // Provenance emitter — emits prov:wasGeneratedBy + prov:Activity quads
2
+ // for any resource that needs build/view-time provenance.
3
+ //
4
+ // Replaces concept-browser's src/components/concept-rdf/provenance.ts
5
+ // (runtime provenance at view-time). Same output shape, but operates
6
+ // on standard RDF/JS Quad[] (no RdfGraph abstraction).
7
+
8
+ import { namedNode, blankNode, literal, quad } from './terms.js';
9
+ import { PREFIXES } from './predicates.js';
10
+ import { RDF_TYPE, RDFS_LABEL } from './curie.js';
11
+ import { deterministicBnodeId } from './bnode-id.js';
12
+
13
+ const PROV_NS = PREFIXES.prov;
14
+ const DCTERMS_NS = PREFIXES.dcterms;
15
+ const XSD_NS = PREFIXES.xsd;
16
+
17
+ const PROV = {
18
+ Activity: `${PROV_NS}Activity`,
19
+ Entity: `${PROV_NS}Entity`,
20
+ SoftwareAgent: `${PROV_NS}SoftwareAgent`,
21
+ Agent: `${PROV_NS}Agent`,
22
+ wasGeneratedBy: `${PROV_NS}wasGeneratedBy`,
23
+ wasAssociatedWith: `${PROV_NS}wasAssociatedWith`,
24
+ generatedAtTime: `${PROV_NS}generatedAtTime`,
25
+ startedAtTime: `${PROV_NS}startedAtTime`,
26
+ endedAtTime: `${PROV_NS}endedAtTime`,
27
+ };
28
+ const DCTERMS = {
29
+ isVersionOf: `${DCTERMS_NS}isVersionOf`,
30
+ title: `${DCTERMS_NS}title`,
31
+ };
32
+ const FOAF_NS = PREFIXES.foaf ?? 'http://xmlns.com/foaf/0.1/';
33
+ const XSD_DATETIME = `${XSD_NS}dateTime`;
34
+
35
+ /**
36
+ * @typedef {Object} ProvenanceInput
37
+ * @property {string} subjectUri — the resource being provenance-tagged
38
+ * @property {string} serializer — e.g. "concept-browser"
39
+ * @property {string} serializerVersion — e.g. "0.7.66"
40
+ * @property {string} generatedAt — ISO 8601 date-time
41
+ * @property {string} [canonicalUri] — for dcterms:isVersionOf (versions)
42
+ */
43
+
44
+ /**
45
+ * Emits provenance quads linking subjectUri to a prov:Activity
46
+ * performed by a prov:SoftwareAgent. The activity IRI is relative
47
+ * (under the document base): `activity/serializers/{serializer}/{version}`.
48
+ *
49
+ * Idempotent: same input always produces the same bnode IDs and the
50
+ * same set of quads.
51
+ *
52
+ * @param {ProvenanceInput} input
53
+ * @returns {Generator<import('@rdfjs/dataset').Quad, void, unknown>}
54
+ */
55
+ export function* provenanceToQuads(input) {
56
+ if (!input?.subjectUri) throw new Error('provenanceToQuads: subjectUri is required');
57
+ if (!input?.serializer) throw new Error('provenanceToQuads: serializer is required');
58
+ if (!input?.serializerVersion) throw new Error('provenanceToQuads: serializerVersion is required');
59
+ if (!input?.generatedAt) throw new Error('provenanceToQuads: generatedAt is required');
60
+
61
+ const subject = namedNode(input.subjectUri);
62
+ const activityIri = namedNode(`activity/serializers/${input.serializer}/${input.serializerVersion}`);
63
+
64
+ // Subject is a prov:Entity, generated by the activity, at the timestamp
65
+ yield quad(subject, namedNode(RDF_TYPE), namedNode(PROV.Entity));
66
+ yield quad(subject, namedNode(PROV.wasGeneratedBy), activityIri);
67
+ yield quad(subject, namedNode(PROV.generatedAtTime), literal(input.generatedAt, namedNode(XSD_DATETIME)));
68
+
69
+ if (input.canonicalUri && input.canonicalUri !== input.subjectUri) {
70
+ yield quad(subject, namedNode(DCTERMS.isVersionOf), namedNode(input.canonicalUri));
71
+ }
72
+
73
+ // Activity is a prov:Activity, associated with the software agent
74
+ yield quad(activityIri, namedNode(RDF_TYPE), namedNode(PROV.Activity));
75
+ yield quad(activityIri, namedNode(RDFS_LABEL), literal(`${input.serializer} ${input.serializerVersion}`));
76
+
77
+ // Software agent as a deterministic blank node — same input always
78
+ // produces the same bnode ID so multiple subjects that share a
79
+ // serializer/version collapse to one agent.
80
+ const agentId = deterministicBnodeId('agent', input.serializer, input.serializerVersion);
81
+ const agent = blankNode(`b${agentId}`);
82
+ yield quad(activityIri, namedNode(PROV.wasAssociatedWith), agent);
83
+ yield quad(agent, namedNode(RDF_TYPE), namedNode(PROV.SoftwareAgent));
84
+ yield quad(agent, namedNode(RDF_TYPE), namedNode(PROV.Agent));
85
+ yield quad(agent, namedNode(RDFS_LABEL), literal(input.serializer));
86
+ yield quad(agent, namedNode(`${FOAF_NS}name`), literal(input.serializer));
87
+ }
@@ -0,0 +1,170 @@
1
+ // Sections builder — transforms a Quad[] into the ClassInstance[]
2
+ // shape consumed by Vue UI components (RdfInstanceSection,
3
+ // RdfInstanceHeader, RdfSourcePanel).
4
+ //
5
+ // The grouping logic is RDF-structure-driven, not Vue-specific:
6
+ // 1. Group quads by subject (NamedNode or BlankNode)
7
+ // 2. For each subject:
8
+ // - rdf:type values → classId (first type wins, multi-type joined)
9
+ // - Derive classLabel from classId local name (CURIE local part)
10
+ // - Derive label from skos:prefLabel/rdfs:label/URI segment
11
+ // - Collect remaining predicates → props
12
+ // 3. For bnode objects, recursively nest their triples as a
13
+ // nested PropValue
14
+ // 4. Return ClassInstance[] sorted by subject IRI (insertion order
15
+ // preserved when subjects appear in canonical order)
16
+ //
17
+ // This module consumes standard RDF/JS Quad[] — no custom RdfGraph
18
+ // abstraction. Any consumer (Vue UI, CLI inspector, debug tool) can
19
+ // use it.
20
+
21
+ import { RDF_TYPE, RDFS_LABEL, compactIri } from './curie.js';
22
+ import { PREFIXES } from './predicates.js';
23
+
24
+ const SKOS_PREF_LABEL = `${PREFIXES.skos}prefLabel`;
25
+
26
+ /**
27
+ * @typedef {Object} PropValue
28
+ * @property {string} predicate
29
+ * @property {string[]} values
30
+ * @property {boolean} [nested]
31
+ */
32
+ /**
33
+ * @typedef {Object} ClassInstance
34
+ * @property {string} classId
35
+ * @property {string} classLabel
36
+ * @property {string} label
37
+ * @property {PropValue[]} props
38
+ */
39
+ /**
40
+ * @typedef {Object} SectionsBuilderOptions
41
+ * @property {string} [language='eng'] — preferred language for skos:prefLabel
42
+ */
43
+
44
+ /**
45
+ * Build ClassInstance[] from a Quad[]. Bnodes referenced as objects
46
+ * are recursively nested into the parent prop.
47
+ *
48
+ * @param {import('@rdfjs/dataset').Quad[]} quads
49
+ * @param {SectionsBuilderOptions} [options]
50
+ * @returns {ClassInstance[]}
51
+ */
52
+ export function quadSectionsToClassInstances(quads, options = {}) {
53
+ const language = options.language ?? 'eng';
54
+ if (!quads || quads.length === 0) return [];
55
+
56
+ // Group quads by subject
57
+ const bySubject = new Map();
58
+ const bnodeQuads = new Map(); // separate bnode quads for nested lookup
59
+ const order = [];
60
+
61
+ for (const q of quads) {
62
+ const subj = q.subject;
63
+ const isBnode = subj.termType === 'BlankNode';
64
+ const key = subj.value;
65
+ const target = isBnode ? bnodeQuads : bySubject;
66
+ if (!target.has(key)) {
67
+ target.set(key, { subject: subj, quads: [] });
68
+ if (!isBnode) order.push(key);
69
+ }
70
+ target.get(key).quads.push(q);
71
+ }
72
+
73
+ const result = [];
74
+ for (const key of order) {
75
+ const entry = bySubject.get(key);
76
+ result.push(resourceToClassInstance(entry.subject, entry.quads, bnodeQuads, language));
77
+ }
78
+ return result;
79
+ }
80
+
81
+ function resourceToClassInstance(subject, quads, bnodeQuads, language) {
82
+ const types = [];
83
+ const otherQuads = [];
84
+ let prefLabel = null;
85
+ let rdfsLabel = null;
86
+
87
+ for (const q of quads) {
88
+ if (q.predicate.value === RDF_TYPE) {
89
+ types.push(q.object.value);
90
+ } else if (q.predicate.value === SKOS_PREF_LABEL) {
91
+ // Prefer prefLabel in the requested language; first one wins per lang
92
+ if (!prefLabel || (q.object.language === language && prefLabel.language !== language)) {
93
+ prefLabel = { value: q.object.value, language: q.object.language };
94
+ }
95
+ } else if (q.predicate.value === RDFS_LABEL && !rdfsLabel) {
96
+ rdfsLabel = q.object.value;
97
+ } else {
98
+ otherQuads.push(q);
99
+ }
100
+ }
101
+
102
+ const subjectStr = subject.value;
103
+ const classId = types.length > 0 ? types[0] : '';
104
+ const classLabel = deriveClassLabel(classId);
105
+ const label = prefLabel?.value ?? rdfsLabel ?? deriveSubjectLabel(subjectStr);
106
+
107
+ // Group predicates (preserve insertion order)
108
+ const props = [];
109
+ const seenPredKeys = new Set();
110
+
111
+ for (const q of otherQuads) {
112
+ const isBnodeObj = q.object.termType === 'BlankNode';
113
+ const formatted = formatTerm(q.object, bnodeQuads);
114
+ if (formatted === '') continue;
115
+ const dedupKey = `${q.predicate.value}#${isBnodeObj ? 'n' : 'f'}#${formatted}`;
116
+ if (seenPredKeys.has(dedupKey)) continue;
117
+ seenPredKeys.add(dedupKey);
118
+
119
+ const prop = { predicate: q.predicate.value, values: [formatted] };
120
+ if (isBnodeObj) prop.nested = true;
121
+ props.push(prop);
122
+ }
123
+
124
+ return {
125
+ classId,
126
+ classLabel,
127
+ label,
128
+ props,
129
+ };
130
+ }
131
+
132
+ function formatTerm(term, bnodeQuads) {
133
+ switch (term.termType) {
134
+ case 'NamedNode':
135
+ return term.value;
136
+ case 'Literal':
137
+ return term.value;
138
+ case 'BlankNode': {
139
+ const entry = bnodeQuads.get(term.value);
140
+ if (!entry) return '';
141
+ const parts = entry.quads.map(q => {
142
+ const val = formatTerm(q.object, bnodeQuads);
143
+ return val ? `${compactIri(q.predicate.value)}: ${val}` : '';
144
+ }).filter(p => p);
145
+ return parts.join('; ');
146
+ }
147
+ default:
148
+ return '';
149
+ }
150
+ }
151
+
152
+ function deriveClassLabel(classId) {
153
+ if (!classId) return '';
154
+ // CURIE case: "gloss:Concept" → "Concept"
155
+ if (!classId.includes('://') && classId.includes(':')) {
156
+ return classId.slice(classId.indexOf(':') + 1);
157
+ }
158
+ // Absolute IRI: take last path segment, strip fragment/query
159
+ const noFrag = classId.split('#').pop();
160
+ const last = noFrag.split('/').pop();
161
+ return last || classId;
162
+ }
163
+
164
+ function deriveSubjectLabel(subject) {
165
+ if (!subject) return '';
166
+ // Last path segment or fragment
167
+ const noFrag = subject.split('#').pop();
168
+ const last = noFrag.split('/').pop();
169
+ return last || subject;
170
+ }
package/src/rdf/shacl.js CHANGED
@@ -76,9 +76,14 @@ export async function loadShapes({ shapesPath = SHAPES_PATH } = {}) {
76
76
  // Options:
77
77
  // - shapes: a pre-loaded Dataset to use instead of the default shapes
78
78
  // - shapesPath: alternative path to load shapes from (cached per path)
79
+ //
80
+ // Note: rdf-validate-shacl 0.6+ requires a factory with clownface. The
81
+ // library's default env (from defaultEnv.js) provides one — we no longer
82
+ // pass a custom factory. Callers still control Dataset creation via
83
+ // quadsToDataset below.
79
84
  export async function validateShacl(dataDataset, { shapes, shapesPath } = {}) {
80
85
  const shapesDataset = shapes ?? await loadShapes({ shapesPath });
81
- const validator = new ShaclValidatorCtor(shapesDataset, { factory: FACTORY });
86
+ const validator = new ShaclValidatorCtor(shapesDataset);
82
87
  return validator.validate(dataDataset);
83
88
  }
84
89
 
@@ -9,11 +9,12 @@
9
9
  // - prov:wasAssociatedWith (optional agent)
10
10
 
11
11
  import { namedNode, literal, quad } from './terms.js';
12
+ import { PREFIXES } from './predicates.js';
13
+ import { RDF_TYPE } from './curie.js';
12
14
 
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';
15
+ const PROV_NS = PREFIXES.prov;
16
+ const DCTERMS_NS = PREFIXES.dcterms;
17
+ const XSD_NS = PREFIXES.xsd;
17
18
 
18
19
  const PROV = {
19
20
  Entity: `${PROV_NS}Entity`,
@@ -14,9 +14,8 @@
14
14
 
15
15
  import { namedNode, literal, quad } from './terms.js';
16
16
  import { PREFIXES } from './predicates.js';
17
+ import { resolveIri, RDF_TYPE, RDFS_LABEL } from './curie.js';
17
18
 
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
19
  const SKOS_NS = PREFIXES.skos;
21
20
 
22
21
  /**
@@ -31,22 +30,6 @@ const SKOS_NS = PREFIXES.skos;
31
30
  * @property {readonly VocabTerm[]} terms
32
31
  */
33
32
 
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
33
  /**
51
34
  * Emits SKOS ConceptScheme + Concept quads for one scheme.
52
35
  *
@@ -58,14 +41,14 @@ export function* vocabularySchemeToQuads(scheme) {
58
41
  const schemeSubject = namedNode(schemeIri);
59
42
 
60
43
  yield quad(schemeSubject, namedNode(RDF_TYPE), namedNode(`${SKOS_NS}ConceptScheme`));
61
- yield quad(schemeSubject, namedNode(RDF_LABEL), literal(scheme.label, 'en'));
44
+ yield quad(schemeSubject, namedNode(RDFS_LABEL), literal(scheme.label, 'en'));
62
45
 
63
46
  for (const term of scheme.terms ?? []) {
64
47
  const termIri = resolveIri(term.iri);
65
48
  const termSubject = namedNode(termIri);
66
49
 
67
50
  yield quad(termSubject, namedNode(RDF_TYPE), namedNode(`${SKOS_NS}Concept`));
68
- yield quad(termSubject, namedNode(RDF_LABEL), literal(term.label, 'en'));
51
+ yield quad(termSubject, namedNode(RDFS_LABEL), literal(term.label, 'en'));
69
52
  yield quad(termSubject, namedNode(`${SKOS_NS}inScheme`), schemeSubject);
70
53
  yield quad(schemeSubject, namedNode(`${SKOS_NS}hasTopConcept`), termSubject);
71
54
  }
@@ -82,6 +65,3 @@ export function* vocabularyToQuads(schemes) {
82
65
  yield* vocabularySchemeToQuads(scheme);
83
66
  }
84
67
  }
85
-
86
- // Re-exported for callers that want to resolve CURIEs themselves.
87
- export { resolveIri };