@glossarist/concept-browser 0.7.135 → 0.7.137
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/data/concept-model/SOURCE.json +2 -3
- package/data/concept-model/glossarist.context.jsonld +15 -0
- package/data/concept-model/glossarist.ttl +86 -1
- package/data/concept-model/shapes/glossarist.shacl.ttl +169 -1
- package/package.json +2 -2
- package/scripts/__tests__/doctor.test.ts +45 -5
- package/scripts/doctor.js +64 -34
- package/scripts/doctor.ts +83 -35
- package/scripts/generate-data.js +44 -308
- package/scripts/generate-data.ts +53 -27
- package/scripts/lib/concept-uri.ts +11 -0
- package/src/__tests__/scripts/concept-csv.test.ts +52 -88
- package/src/data/ontology-schema.json +374 -8
- package/scripts/lib/concept-formats.ts +0 -326
- package/src/__tests__/concept-formats.test.ts +0 -111
- package/src/utils/concept-formats.ts +0 -123
package/scripts/generate-data.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import yaml from 'js-yaml';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
naturalSort,
|
|
6
|
+
Register,
|
|
7
|
+
parseMention,
|
|
8
|
+
parseMentions,
|
|
9
|
+
InvalidMentionError,
|
|
10
|
+
parseConceptYaml,
|
|
11
|
+
conceptToTurtle,
|
|
12
|
+
conceptsToTbx,
|
|
13
|
+
conceptsToCsv,
|
|
14
|
+
emitOutputSet,
|
|
15
|
+
type Concept,
|
|
16
|
+
} from 'glossarist';
|
|
5
17
|
import { loadSiteConfig } from './load-site-config';
|
|
6
18
|
import { getGroups } from './lib/concept-groups';
|
|
7
19
|
import { consumeDatasetEntities } from './lib/build/non-verbal-consumer';
|
|
@@ -23,7 +35,7 @@ import type {
|
|
|
23
35
|
YamlNewsFrontmatter,
|
|
24
36
|
YamlContentPage,
|
|
25
37
|
} from './lib/yaml-types';
|
|
26
|
-
import { buildConceptUri } from './lib/concept-uri';
|
|
38
|
+
import { buildConceptUri, buildConceptUriPrefix } from './lib/concept-uri';
|
|
27
39
|
import {
|
|
28
40
|
loadConceptFile,
|
|
29
41
|
readYaml,
|
|
@@ -41,15 +53,6 @@ import {
|
|
|
41
53
|
refsToJsonLd,
|
|
42
54
|
citationToJsonLd,
|
|
43
55
|
} from './lib/jsonld-emitter';
|
|
44
|
-
import {
|
|
45
|
-
escapeTurtle,
|
|
46
|
-
escapeXml,
|
|
47
|
-
conceptJsonToTurtle,
|
|
48
|
-
conceptJsonToSkosJsonLd,
|
|
49
|
-
conceptJsonToTbx,
|
|
50
|
-
conceptsToCsv,
|
|
51
|
-
conceptsToSkosJsonLdGraph,
|
|
52
|
-
} from './lib/concept-formats';
|
|
53
56
|
|
|
54
57
|
// MECE partitive multiplicity: 2 independent axes (ISO 704:2022).
|
|
55
58
|
// presence × count
|
|
@@ -691,7 +694,7 @@ async function processDataset(dir, register, opts) {
|
|
|
691
694
|
for (const f of fs.readdirSync(conceptsDir)) fs.unlinkSync(path.join(conceptsDir, f));
|
|
692
695
|
}
|
|
693
696
|
const concepts = [];
|
|
694
|
-
const
|
|
697
|
+
const conceptInstances: Concept[] = [];
|
|
695
698
|
const langTermCounts = {};
|
|
696
699
|
const langDefCounts = {};
|
|
697
700
|
const availableFormats = ['ttl', 'jsonld', 'yaml', 'tbx', 'csv'];
|
|
@@ -805,17 +808,34 @@ async function processDataset(dir, register, opts) {
|
|
|
805
808
|
const termid = String(conceptYaml.termid);
|
|
806
809
|
const jsonld = yamlToJsonLd(conceptYaml, register, dsRefMaps);
|
|
807
810
|
writeJson(path.join(conceptsDir, `${termid}.json`), jsonld);
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
811
|
+
// Canonical interchange exports — routed through glossarist's
|
|
812
|
+
// output API (one model walk; predicates generated from the
|
|
813
|
+
// concept-model context, wire shape from schemas/v3). The hand-rolled
|
|
814
|
+
// gl:-shaped emitters in scripts/lib/concept-formats.ts are retired.
|
|
815
|
+
// Note: TBX now includes <term> entries — the retired emitter
|
|
816
|
+
// computed termEntries but never wrote them into the document.
|
|
817
|
+
let conceptInstance: Concept | null = null;
|
|
818
|
+
try {
|
|
819
|
+
conceptInstance = parseConceptYaml(fs.readFileSync(path.join(dir, file), 'utf8'), termid);
|
|
820
|
+
} catch (e) {
|
|
821
|
+
console.warn(` ${termid}: canonical parse failed (${e.message}); skipping glossarist exports`);
|
|
822
|
+
}
|
|
823
|
+
if (conceptInstance) {
|
|
824
|
+
conceptInstances.push(conceptInstance);
|
|
825
|
+
fs.writeFileSync(
|
|
826
|
+
path.join(conceptsDir, `${termid}.ttl`),
|
|
827
|
+
conceptToTurtle(conceptInstance, { uriBase: refMaps.uriBase, registerId: register }),
|
|
828
|
+
);
|
|
829
|
+
const conceptJsonLd = await emitOutputSet([conceptInstance], {
|
|
830
|
+
registerId: register,
|
|
831
|
+
uriBase: refMaps.uriBase,
|
|
832
|
+
formats: ['jsonld'],
|
|
833
|
+
});
|
|
834
|
+
fs.writeFileSync(path.join(conceptsDir, `${termid}.jsonld`), conceptJsonLd.jsonld!);
|
|
835
|
+
const tbxContent = conceptsToTbx([conceptInstance], { registerId: register });
|
|
836
|
+
if (tbxContent) {
|
|
837
|
+
fs.writeFileSync(path.join(conceptsDir, `${termid}.tbx`), tbxContent);
|
|
838
|
+
}
|
|
819
839
|
}
|
|
820
840
|
|
|
821
841
|
fs.copyFileSync(path.join(dir, file), path.join(conceptsDir, `${termid}.yaml`));
|
|
@@ -835,9 +855,15 @@ async function processDataset(dir, register, opts) {
|
|
|
835
855
|
}
|
|
836
856
|
}
|
|
837
857
|
|
|
838
|
-
// Aggregate distributions: single-file exports of the whole register
|
|
839
|
-
|
|
840
|
-
|
|
858
|
+
// Aggregate distributions: single-file exports of the whole register,
|
|
859
|
+
// emitted by glossarist's output-set API from the canonical model walk.
|
|
860
|
+
const aggregate = await emitOutputSet(conceptInstances, {
|
|
861
|
+
registerId: register,
|
|
862
|
+
uriBase: refMaps.uriBase,
|
|
863
|
+
languageOrder: opts.languageOrder || opts.languages,
|
|
864
|
+
});
|
|
865
|
+
fs.writeFileSync(path.join(DATA, register, `${register}.csv`), aggregate.csv!);
|
|
866
|
+
fs.writeFileSync(path.join(DATA, register, `${register}.jsonld`), aggregate.jsonld!);
|
|
841
867
|
|
|
842
868
|
const CHUNK_SIZE = 500;
|
|
843
869
|
const chunks = [];
|
|
@@ -885,7 +911,7 @@ async function processDataset(dir, register, opts) {
|
|
|
885
911
|
fs.writeFileSync(
|
|
886
912
|
path.join(DATA, register, 'graph-nodes.json'),
|
|
887
913
|
JSON.stringify({
|
|
888
|
-
uriPrefix:
|
|
914
|
+
uriPrefix: buildConceptUriPrefix(refMaps.uriBase, register),
|
|
889
915
|
registerId: register,
|
|
890
916
|
nodes: graphNodeEntries,
|
|
891
917
|
}),
|
|
@@ -15,3 +15,14 @@
|
|
|
15
15
|
export function buildConceptUri(uriBase: string, registerId: string, conceptId: string | number): string {
|
|
16
16
|
return `${uriBase}/dataset/${registerId}/concept/${conceptId}`;
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
/** Prefix of every concept URI of a register (for keying node maps and
|
|
20
|
+
* matching edge endpoints against a register's URI space). */
|
|
21
|
+
export function buildConceptUriPrefix(uriBase: string, registerId: string): string {
|
|
22
|
+
return `${uriBase}/dataset/${registerId}/concept/`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Prefix of any URI belonging to a register (for the cross-ref index). */
|
|
26
|
+
export function buildDatasetUriPrefix(uriBase: string, registerId: string): string {
|
|
27
|
+
return `${uriBase}/dataset/${registerId}/`;
|
|
28
|
+
}
|
|
@@ -1,8 +1,14 @@
|
|
|
1
|
+
// CSV aggregate export — now routed through glossarist's canonical
|
|
2
|
+
// output API (glossarist@0.4.58, glossarist-js#132). These specs pin the
|
|
3
|
+
// production contract that shipped in concept-browser#214: column order,
|
|
4
|
+
// preferred/alt split, RFC 4180 quoting, BOM + CRLF, languageOrder,
|
|
5
|
+
// and no-drop rows for concepts without localizations.
|
|
6
|
+
|
|
1
7
|
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
8
|
+
import { Concept, CSV_COLUMNS, conceptsToCsv } from 'glossarist';
|
|
3
9
|
|
|
4
10
|
function parseCsv(csv: string): string[][] {
|
|
5
|
-
const body = csv.replace(
|
|
11
|
+
const body = csv.replace(/^/, '');
|
|
6
12
|
const rows: string[][] = [];
|
|
7
13
|
let row: string[] = [];
|
|
8
14
|
let field = '';
|
|
@@ -29,114 +35,72 @@ function parseCsv(csv: string): string[][] {
|
|
|
29
35
|
return rows;
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
const baseConcept = {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}],
|
|
41
|
-
'gl:localizedConcept': {
|
|
38
|
+
const baseConcept = () => new Concept({
|
|
39
|
+
id: '1-1-010',
|
|
40
|
+
termid: '1-1-010',
|
|
41
|
+
status: 'valid',
|
|
42
|
+
uri: 'https://example.com/vocab/concept/1-1-010',
|
|
43
|
+
domains: [{ conceptId: 'section-1' }],
|
|
44
|
+
sources: [{ type: 'authoritative', origin: { ref: { source: 'IALA Dictionary' }, link: 'https://example.com/source' } }],
|
|
45
|
+
localizations: {
|
|
42
46
|
eng: {
|
|
43
|
-
|
|
44
|
-
{
|
|
45
|
-
{
|
|
47
|
+
terms: [
|
|
48
|
+
{ designation: 'Buoyage and beaconage', normative_status: 'preferred' },
|
|
49
|
+
{ designation: 'Sea-marking (G.B.)', normative_status: 'admitted' },
|
|
46
50
|
],
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
definition: [{ content: 'Ensemble of beacons, buoys, seamarks and small lights.' }],
|
|
52
|
+
notes: [{ content: 'Note one.' }, { content: 'Note two.' }],
|
|
49
53
|
},
|
|
50
54
|
fra: {
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
terms: [{ designation: 'Balisage', normative_status: 'preferred' }],
|
|
56
|
+
definition: [{ content: 'Ensemble de balises.' }],
|
|
53
57
|
},
|
|
54
58
|
},
|
|
55
|
-
};
|
|
59
|
+
});
|
|
56
60
|
|
|
57
|
-
describe('conceptsToCsv', () => {
|
|
61
|
+
describe('conceptsToCsv (glossarist output API)', () => {
|
|
58
62
|
it('emits UTF-8 BOM, CRLF line endings, and the model-driven header', () => {
|
|
59
|
-
const csv = conceptsToCsv([baseConcept]);
|
|
60
|
-
expect(csv.startsWith('
|
|
63
|
+
const csv = conceptsToCsv([baseConcept()]);
|
|
64
|
+
expect(csv.startsWith('')).toBe(true);
|
|
61
65
|
expect(csv.endsWith('\r\n')).toBe(true);
|
|
62
|
-
expect(csv).toContain('\r\n');
|
|
63
66
|
const rows = parseCsv(csv);
|
|
64
|
-
expect(rows[0]).
|
|
67
|
+
expect(rows[0]!.join(',')).toBe(CSV_COLUMNS.join(','));
|
|
65
68
|
});
|
|
66
69
|
|
|
67
|
-
it('
|
|
68
|
-
const rows = parseCsv(conceptsToCsv([baseConcept]));
|
|
70
|
+
it('one row per concept × language; preferred designation first', () => {
|
|
71
|
+
const rows = parseCsv(conceptsToCsv([baseConcept()]));
|
|
69
72
|
expect(rows).toHaveLength(3);
|
|
70
|
-
|
|
71
|
-
expect(
|
|
72
|
-
expect(
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
it('honors languageOrder from the deployment config', () => {
|
|
76
|
-
const rows = parseCsv(conceptsToCsv([baseConcept], { languageOrder: ['fra', 'eng'] }));
|
|
77
|
-
expect(rows[1][CSV_COLUMNS.indexOf('language')]).toBe('fra');
|
|
78
|
-
expect(rows[2][CSV_COLUMNS.indexOf('language')]).toBe('eng');
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
it('keeps languages missing from languageOrder at the end', () => {
|
|
82
|
-
const rows = parseCsv(conceptsToCsv([baseConcept], { languageOrder: ['deu', 'eng'] }));
|
|
83
|
-
expect(rows.slice(1).map(r => r[CSV_COLUMNS.indexOf('language')])).toEqual(['eng', 'fra']);
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('separates the preferred term from alt terms', () => {
|
|
87
|
-
const rows = parseCsv(conceptsToCsv([baseConcept]));
|
|
88
|
-
const eng = rows[1];
|
|
89
|
-
expect(eng[CSV_COLUMNS.indexOf('term')]).toBe('Buoyage and beaconage');
|
|
90
|
-
expect(eng[CSV_COLUMNS.indexOf('alt_terms')]).toBe('Sea-marking (G.B.)');
|
|
73
|
+
const eng = rows[1]!;
|
|
74
|
+
expect(eng[0]).toBe('1-1-010');
|
|
75
|
+
expect(eng[4]).toBe('eng');
|
|
76
|
+
expect(eng[5]).toBe('Buoyage and beaconage');
|
|
77
|
+
expect(eng[6]).toBe('Sea-marking (G.B.)');
|
|
91
78
|
});
|
|
92
79
|
|
|
93
|
-
it('
|
|
94
|
-
const rows = parseCsv(conceptsToCsv([baseConcept]));
|
|
95
|
-
expect(rows[1][
|
|
80
|
+
it('definition/notes columns carry the full content', () => {
|
|
81
|
+
const rows = parseCsv(conceptsToCsv([baseConcept()]));
|
|
82
|
+
expect(rows[1]![7]).toBe('Ensemble of beacons, buoys, seamarks and small lights.');
|
|
83
|
+
expect(rows[1]![8]).toBe('Note one.\nNote two.');
|
|
84
|
+
expect(rows[2]![7]).toBe('Ensemble de balises.');
|
|
96
85
|
});
|
|
97
86
|
|
|
98
|
-
it('
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
expect(csv).toContain('"Says ""hello"", loudly"');
|
|
103
|
-
expect(parseCsv(csv)[1][CSV_COLUMNS.indexOf('definition')]).toBe('Says "hello", loudly');
|
|
87
|
+
it('sources and source_links from origin ref/link', () => {
|
|
88
|
+
const rows = parseCsv(conceptsToCsv([baseConcept()]));
|
|
89
|
+
expect(rows[1]![10]).toBe('IALA Dictionary');
|
|
90
|
+
expect(rows[1]![11]).toBe('https://example.com/source');
|
|
104
91
|
});
|
|
105
92
|
|
|
106
|
-
it('
|
|
107
|
-
const rows = parseCsv(conceptsToCsv([baseConcept]));
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
expect(row[CSV_COLUMNS.indexOf('source_links')]).toBe('https://example.com/source');
|
|
111
|
-
}
|
|
93
|
+
it('languageOrder reorders rows; unlisted languages append after', () => {
|
|
94
|
+
const rows = parseCsv(conceptsToCsv([baseConcept()], { languageOrder: ['fra', 'deu'] }));
|
|
95
|
+
expect(rows[1]![4]).toBe('fra');
|
|
96
|
+
expect(rows[2]![4]).toBe('eng');
|
|
112
97
|
});
|
|
113
98
|
|
|
114
|
-
it('still
|
|
115
|
-
const bare = {
|
|
116
|
-
'@id': 'https://example.com/vocab/concept/9-9-999',
|
|
117
|
-
'gl:identifier': '9-9-999',
|
|
118
|
-
'gl:status': 'retired',
|
|
119
|
-
};
|
|
99
|
+
it('concept without localizations still gets a row (no silent drops)', () => {
|
|
100
|
+
const bare = new Concept({ id: '222-02-02', termid: '222-02-02' });
|
|
120
101
|
const rows = parseCsv(conceptsToCsv([bare]));
|
|
121
102
|
expect(rows).toHaveLength(2);
|
|
122
|
-
expect(rows[1][
|
|
123
|
-
expect(rows[1][
|
|
124
|
-
expect(rows[1][CSV_COLUMNS.indexOf('term')]).toBe('');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('carries section domains and status columns', () => {
|
|
128
|
-
const rows = parseCsv(conceptsToCsv([baseConcept]));
|
|
129
|
-
expect(rows[1][CSV_COLUMNS.indexOf('section')]).toBe('section-1');
|
|
130
|
-
expect(rows[1][CSV_COLUMNS.indexOf('status')]).toBe('valid');
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe('conceptsToSkosJsonLdGraph', () => {
|
|
135
|
-
it('wraps concepts in a shared @context and @graph', () => {
|
|
136
|
-
const parsed = JSON.parse(conceptsToSkosJsonLdGraph([baseConcept]));
|
|
137
|
-
expect(parsed['@context']['skos']).toBe('http://www.w3.org/2004/02/skos/core#');
|
|
138
|
-
expect(parsed['@graph']).toHaveLength(1);
|
|
139
|
-
expect(parsed['@graph'][0]['@id']).toBe(baseConcept['@id']);
|
|
140
|
-
expect(parsed['@graph'][0]['skos:prefLabel']['eng']).toBe('Buoyage and beaconage');
|
|
103
|
+
expect(rows[1]![0]).toBe('222-02-02');
|
|
104
|
+
expect(rows[1]![4]).toBe('');
|
|
141
105
|
});
|
|
142
106
|
});
|