@glossarist/concept-browser 0.7.85 → 0.7.87

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glossarist/concept-browser",
3
- "version": "0.7.85",
3
+ "version": "0.7.87",
4
4
  "description": "Vue SPA for browsing Glossarist terminology datasets with cross-reference resolution, graph visualization, and multi-language support",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,7 +37,7 @@
37
37
  "astro": "^7.0.7",
38
38
  "d3": "^7.9.0",
39
39
  "favicons": "^7.2.0",
40
- "glossarist": "^0.4.20",
40
+ "glossarist": "^0.4.26",
41
41
  "js-yaml": "^4.1.0",
42
42
  "jszip": "^3.10.1",
43
43
  "pinia": "^2.3.1",
@@ -3,7 +3,6 @@ function buildConceptUri(uriBase, registerId, conceptId) {
3
3
  }
4
4
 
5
5
  const VALID_COMPLETENESS = new Set(['complete', 'partial']);
6
- const VALID_CERTAINTY = new Set(['confirmed', 'possible']);
7
6
 
8
7
  function validateCompleteness(value) {
9
8
  if (value == null) return 'complete';
@@ -15,14 +14,54 @@ function validateCompleteness(value) {
15
14
  return value;
16
15
  }
17
16
 
18
- function validateCertainty(value) {
19
- if (value == null) return 'confirmed';
20
- if (!VALID_CERTAINTY.has(value)) {
21
- throw new Error(
22
- `Invalid partitive member certainty: "${value}". Allowed: confirmed, possible`,
23
- );
17
+ /**
18
+ * Resolve a partitive member's multiplicity from JSON-LD.
19
+ *
20
+ * Reads gl:multiplicity first (v3 wire shape, ISO 704:2022). Falls back
21
+ * to migrating from gl:certainty (v2 wire shape) for one release cycle
22
+ * until upstream emits multiplicity natively.
23
+ *
24
+ * confirmed → compulsory
25
+ * possible → optional
26
+ */
27
+ const VALID_PRESENCE = new Set(['required', 'optional']);
28
+ const VALID_COUNT = new Set(['exactly_one', 'at_least_one', 'multiple']);
29
+ const LEGACY_MULTIPLICITY_MAP = {
30
+ compulsory: { presence: 'required', count: 'exactly_one' },
31
+ optional: { presence: 'optional', count: 'exactly_one' },
32
+ compulsory_multiple: { presence: 'required', count: 'multiple' },
33
+ optional_multiple: { presence: 'optional', count: 'multiple' },
34
+ compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
35
+ };
36
+
37
+ function resolvePresence(member) {
38
+ const p = member['gl:presence'] ?? member.presence;
39
+ if (p != null) {
40
+ if (!VALID_PRESENCE.has(p)) throw new Error(`Invalid presence: "${p}"`);
41
+ return p;
24
42
  }
25
- return value;
43
+ return migrateMultiplicity(member).presence;
44
+ }
45
+
46
+ function resolveCount(member) {
47
+ const c = member['gl:count'] ?? member.count;
48
+ if (c != null) {
49
+ if (!VALID_COUNT.has(c)) throw new Error(`Invalid count: "${c}"`);
50
+ return c;
51
+ }
52
+ return migrateMultiplicity(member).count;
53
+ }
54
+
55
+ function migrateMultiplicity(member) {
56
+ const m = member['gl:multiplicity'] ?? member.multiplicity;
57
+ if (m && LEGACY_MULTIPLICITY_MAP[m]) return LEGACY_MULTIPLICITY_MAP[m];
58
+ const certainty = member['gl:certainty'] ?? member.certainty;
59
+ if (certainty === 'possible') return { presence: 'optional', count: 'exactly_one' };
60
+ return { presence: 'required', count: 'exactly_one' };
61
+ }
62
+
63
+ function resolveIsDelimiting(member) {
64
+ return member['gl:isDelimiting'] === true || member.isDelimiting === true;
26
65
  }
27
66
 
28
67
  function normalizeLocalizedString(content) {
@@ -183,21 +222,20 @@ function extractPartitiveRelations(concept, registerId, uriBase, urnMap) {
183
222
  if (!uri || uri === sourceUri) return null;
184
223
  return {
185
224
  uri,
186
- certainty: validateCertainty(p['gl:certainty']),
225
+ presence: resolvePresence(p),
226
+ count: resolveCount(p),
227
+ isDelimiting: resolveIsDelimiting(p),
187
228
  };
188
229
  })
189
230
  .filter(p => p !== null);
190
231
 
191
232
  if (partitives.length === 0) continue;
192
233
 
193
- const plurality = resolvePlurality(rel['gl:hasPlurality']);
194
-
195
234
  relations.push({
196
235
  source: sourceUri,
197
236
  comprehensive,
198
237
  partitives,
199
238
  completeness: validateCompleteness(rel['gl:completeness']),
200
- plurality,
201
239
  criterion: normalizeLocalizedString(rel['gl:criterion']),
202
240
  register: registerId,
203
241
  });
@@ -205,21 +243,6 @@ function extractPartitiveRelations(concept, registerId, uriBase, urnMap) {
205
243
  return relations;
206
244
  }
207
245
 
208
- function resolvePlurality(plurality) {
209
- if (!plurality) return null;
210
- const isShared = plurality['gl:isShared'] ?? plurality.is_shared;
211
- if (typeof isShared !== 'boolean') return null;
212
- const isUncertain = plurality['gl:isUncertain'] ?? plurality.is_uncertain ?? false;
213
- const sharedTypeRef = plurality['gl:sharedType'] ?? plurality.shared_type;
214
- let sharedType = null;
215
- if (sharedTypeRef) {
216
- const source = sharedTypeRef['gl:source'] ?? sharedTypeRef.source;
217
- const id = sharedTypeRef['gl:id'] ?? sharedTypeRef.id;
218
- if (source && id) sharedType = `${source}:${id}`;
219
- }
220
- return { isShared, isUncertain, sharedType };
221
- }
222
-
223
246
  function resolveConceptUri(ref, uriBase, urnMap) {
224
247
  const source = ref['gl:source'] || ref['source'];
225
248
  const id = ref['gl:id'] || ref['id'];
@@ -14,6 +14,30 @@ import { buildVersionHistoryTurtle } from './lib/version-turtle.mjs';
14
14
  import { buildBibliographyTurtle } from './lib/bibliography-turtle.mjs';
15
15
  import { ttlLit } from './lib/turtle-escape.mjs';
16
16
  import { firstNonEmpty } from './lib/first-non-empty.mjs';
17
+
18
+ // MECE partitive multiplicity: 2 independent axes (ISO 704:2022).
19
+ // presence × count
20
+ // Legacy 5-value `multiplicity` and 2-value `certainty` map into the
21
+ // 2-axis model — keep emitting them for one release cycle so consumers
22
+ // that still read the legacy fields keep working.
23
+ const LEGACY_MULTIPLICITY_MAP = {
24
+ compulsory: { presence: 'required', count: 'exactly_one' },
25
+ optional: { presence: 'optional', count: 'exactly_one' },
26
+ compulsory_multiple: { presence: 'required', count: 'multiple' },
27
+ optional_multiple: { presence: 'optional', count: 'multiple' },
28
+ compulsory_at_least_one: { presence: 'required', count: 'at_least_one' },
29
+ };
30
+ const LEGACY_CERTAINTY_FROM_AXIS = {
31
+ required: 'confirmed',
32
+ optional: 'possible',
33
+ };
34
+
35
+ function splitMultiplicity(multiplicity) {
36
+ return multiplicity in LEGACY_MULTIPLICITY_MAP
37
+ ? LEGACY_MULTIPLICITY_MAP[multiplicity]
38
+ : { presence: 'required', count: 'exactly_one' };
39
+ }
40
+
17
41
  function buildConceptUri(uriBase, registerId, conceptId) {
18
42
  return `${uriBase}/${registerId}/concept/${conceptId}`;
19
43
  }
@@ -693,7 +717,28 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
693
717
  } else {
694
718
  m['gl:ref'] = refToJsonLd(member, 'gl:ConceptRef');
695
719
  }
696
- if (member.certainty) m['gl:certainty'] = member.certainty;
720
+ // ISO 704:2022 MECE axes: presence × count.
721
+ // Prefer explicit YAML values; fall back to migrating from the
722
+ // legacy 5-value `multiplicity` enum for one release cycle.
723
+ const presence = member.presence
724
+ ?? splitMultiplicity(member.multiplicity ?? '').presence;
725
+ const count = member.count
726
+ ?? splitMultiplicity(member.multiplicity ?? '').count;
727
+
728
+ m['gl:presence'] = presence;
729
+ m['gl:count'] = count;
730
+
731
+ if (member.is_delimiting === true || member.isDelimiting === true) {
732
+ m['gl:isDelimiting'] = true;
733
+ }
734
+
735
+ // Legacy gl:multiplicity + gl:certainty — emitted for one
736
+ // release cycle. Remove after downstream consumers migrate to
737
+ // presence + count.
738
+ if (member.multiplicity) m['gl:multiplicity'] = member.multiplicity;
739
+ const legacyCertainty = member.certainty
740
+ ?? LEGACY_CERTAINTY_FROM_AXIS[presence];
741
+ if (legacyCertainty) m['gl:certainty'] = legacyCertainty;
697
742
  return m;
698
743
  });
699
744
  }
@@ -977,7 +1022,9 @@ async function processDataset(dir, register, opts) {
977
1022
  partitiveRelations: {
978
1023
  count: 0,
979
1024
  byCompleteness: { complete: 0, partial: 0 },
980
- byMemberCertainty: { confirmed: 0, possible: 0 },
1025
+ byPresence: { required: 0, optional: 0 },
1026
+ byCount: { exactly_one: 0, at_least_one: 0, multiple: 0 },
1027
+ delimitingMembers: 0,
981
1028
  withCriterion: 0,
982
1029
  withoutCriterion: 0,
983
1030
  withPlurality: 0,
@@ -1013,9 +1060,16 @@ async function processDataset(dir, register, opts) {
1013
1060
  (s.partitiveRelations.byCompleteness[completeness] || 0) + 1;
1014
1061
  const partitives = rel.partitives || [];
1015
1062
  for (const member of partitives) {
1016
- const certainty = member.certainty || 'confirmed';
1017
- s.partitiveRelations.byMemberCertainty[certainty] =
1018
- (s.partitiveRelations.byMemberCertainty[certainty] || 0) + 1;
1063
+ const axes = splitMultiplicity(member.multiplicity ?? '');
1064
+ const presence = member.presence ?? axes.presence;
1065
+ const count = member.count ?? axes.count;
1066
+ s.partitiveRelations.byPresence[presence] =
1067
+ (s.partitiveRelations.byPresence[presence] || 0) + 1;
1068
+ s.partitiveRelations.byCount[count] =
1069
+ (s.partitiveRelations.byCount[count] || 0) + 1;
1070
+ if (member.is_delimiting === true || member.isDelimiting === true) {
1071
+ s.partitiveRelations.delimitingMembers += 1;
1072
+ }
1019
1073
  }
1020
1074
  if (rel.criterion) {
1021
1075
  s.partitiveRelations.withCriterion += 1;
@@ -0,0 +1,6 @@
1
+ // Type declarations for the .mjs script — vitest needs a typed surface.
2
+ // The actual implementations are imported by value from the script.
3
+ export function isV1PartitiveRelation(rel: unknown): boolean;
4
+ export function migrateRelation(rel: unknown): Record<string, unknown> | null;
5
+ export function migrateConceptData(data: Record<string, unknown> | null | undefined): Record<string, unknown> | null;
6
+ export function isAlreadyMigrated(content: unknown): boolean;
@@ -0,0 +1,176 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * migrate-v1-to-v3.mjs — convert partitive relations from v1 (v2 certainty)
4
+ * to v3 (ISO 704:2022 multiplicity + is_delimiting) in vocab YAML sources.
5
+ *
6
+ * Input shape (v1):
7
+ * partitive_relations:
8
+ * - comprehensive: { source, id }
9
+ * parts:
10
+ * - { source, id }
11
+ * enumeration: complete|partial
12
+ * markers: [...]
13
+ * content: ...
14
+ *
15
+ * Output shape (v3, glossarist 0.4.20 native):
16
+ * partitive_relations:
17
+ * - comprehensive: { source, id }
18
+ * partitives:
19
+ * - ref: { source, id }
20
+ * certainty: confirmed|possible
21
+ * # Migration notes: when glossarist publishes multiplicity
22
+ * # natively, regenerate-data.mjs will emit gl:multiplicity
23
+ * # alongside gl:certainty; no further YAML change needed.
24
+ *
25
+ * Mapping:
26
+ * v1 `parts` (each) → v3 `partitives[i].ref`
27
+ * v1 `enumeration` → v3 `completeness` (rename only)
28
+ * v1 `markers` → DROPPED (v3 reads multiplicity from per-member certainty)
29
+ * v1 `content` → DROPPED (not part of ISO 704 v3; was v1-only)
30
+ *
31
+ * The CLI is idempotent: files that already use `partitives:` are
32
+ * skipped (left to the user to convert by hand if needed).
33
+ *
34
+ * Usage:
35
+ * node scripts/migrate-v1-to-v3.mjs path/to/dataset.yaml
36
+ * node scripts/migrate-v1-to-v3.mjs --in datasets/vim --out datasets/vim-v3
37
+ * node scripts/migrate-v1-to-v3.mjs --dry-run path/to/file.yaml
38
+ *
39
+ * --in <dir|file> input path (file or directory of .yaml)
40
+ * --out <dir|file> output path (mirror of --in if not given)
41
+ * --dry-run print what would change; do not write
42
+ */
43
+
44
+ import fs from 'fs';
45
+ import path from 'path';
46
+
47
+ // ── Pure conversion logic (separated for testability) ────────────────
48
+
49
+ /** Detect a partitive-relation in v1 shape. */
50
+ export function isV1PartitiveRelation(rel) {
51
+ return rel && Array.isArray(rel.parts);
52
+ }
53
+
54
+ /** Convert a single v1 relation to v3 shape. */
55
+ export function migrateRelation(rel) {
56
+ if (!isV1PartitiveRelation(rel)) return null;
57
+ return {
58
+ comprehensive: rel.comprehensive,
59
+ completeness: rel.enumeration === 'open'
60
+ ? 'partial'
61
+ : (rel.enumeration === 'closed' || rel.enumeration == null)
62
+ ? 'complete'
63
+ : rel.enumeration,
64
+ partitives: (rel.parts ?? []).map(p => ({
65
+ ref: p,
66
+ certainty: 'confirmed',
67
+ })),
68
+ ...(rel.content ? { content: rel.content } : {}),
69
+ };
70
+ }
71
+
72
+ /** Migrate a full concept's data block. Returns null if nothing to migrate. */
73
+ export function migrateConceptData(data) {
74
+ if (!data?.partitive_relations?.length) return null;
75
+ const migrated = data.partitive_relations
76
+ .map(migrateRelation)
77
+ .filter(r => r !== null);
78
+ if (migrated.length === 0) return null;
79
+ return { ...data, partitive_relations: migrated };
80
+ }
81
+
82
+ /** Skip file if already migrated (uses v3 'partitives' field). */
83
+ export function isAlreadyMigrated(content) {
84
+ return Boolean(content?.data?.partitive_relations?.some?.(rel => Array.isArray(rel.partitives)));
85
+ }
86
+
87
+ // ── File / directory walking ──────────────────────────────────────────
88
+
89
+ function walkFiles(input) {
90
+ const stat = fs.statSync(input);
91
+ if (stat.isFile()) return [input];
92
+ const out = [];
93
+ for (const entry of fs.readdirSync(input, { withFileTypes: true })) {
94
+ const full = path.join(input, entry.name);
95
+ if (entry.isDirectory()) out.push(...walkFiles(full));
96
+ else if (entry.isFile() && /\.(ya?ml)$/i.test(entry.name)) out.push(full);
97
+ }
98
+ return out;
99
+ }
100
+
101
+ /** Very small YAML parser sufficient for our shape — we only touch
102
+ * data.partitive_relations[]. We intentionally use js-yaml when available
103
+ * (via dynamic import) for full round-trip; otherwise a minimal regex
104
+ * fallback handles the structure. */
105
+ async function loadYamlModule() {
106
+ try {
107
+ return await import('js-yaml');
108
+ } catch {
109
+ return null;
110
+ }
111
+ }
112
+
113
+ function outputPath(input, file, outRoot) {
114
+ const stat = fs.statSync(input);
115
+ if (stat.isFile()) return outRoot;
116
+ const rel = path.relative(input, file);
117
+ return path.join(outRoot, rel);
118
+ }
119
+
120
+ async function migrateFile(input, file, options, yaml) {
121
+ const raw = fs.readFileSync(file, 'utf8');
122
+ const parsed = yaml ? yaml.load(raw) : JSON.parse(raw);
123
+ if (!parsed?.data) return { changed: false };
124
+ if (isAlreadyMigrated(parsed)) return { changed: false, skipped: true };
125
+ const migrated = migrateConceptData(parsed.data);
126
+ if (!migrated) return { changed: false };
127
+ parsed.data = { ...parsed.data, ...migrated };
128
+ const out = outputPath(input, file, options.out);
129
+ fs.mkdirSync(path.dirname(out), { recursive: true });
130
+ const outText = yaml ? yaml.dump(parsed) : JSON.stringify(parsed, null, 2);
131
+ if (!options.dryRun) fs.writeFileSync(out, outText);
132
+ return { changed: true, out, in: file };
133
+ }
134
+
135
+ // ── CLI ─────────────────────────────────────────────────────────────
136
+
137
+ async function main() {
138
+ const args = process.argv.slice(2);
139
+ const opts = { in: null, out: null, dryRun: false };
140
+ for (let i = 0; i < args.length; i++) {
141
+ const a = args[i];
142
+ if (a === '--in') opts.in = args[++i];
143
+ else if (a === '--out') opts.out = args[++i];
144
+ else if (a === '--dry-run') opts.dryRun = true;
145
+ else if (!opts.in) opts.in = a;
146
+ else if (!opts.out) opts.out = a;
147
+ }
148
+ if (!opts.in) {
149
+ console.error('Usage: node scripts/migrate-v1-to-v3.mjs --in <dir|file> [--out <dir|file>] [--dry-run]');
150
+ process.exit(1);
151
+ }
152
+ if (!opts.out) opts.out = opts.dryRun ? opts.in : `${opts.in}.v3`;
153
+ const yaml = await loadYamlModule();
154
+ if (!yaml) {
155
+ console.error('js-yaml is required; install via npm install js-yaml');
156
+ process.exit(1);
157
+ }
158
+ const files = walkFiles(opts.in);
159
+ let changed = 0, skipped = 0, total = 0;
160
+ for (const f of files) {
161
+ total++;
162
+ const res = await migrateFile(opts.in, f, opts, yaml);
163
+ if (res.changed) {
164
+ changed++;
165
+ console.log(`✓ ${res.in} → ${res.out}`);
166
+ } else if (res.skipped) {
167
+ skipped++;
168
+ }
169
+ }
170
+ console.log(`\n${changed} migrated, ${skipped} skipped (already v3), ${total - changed - skipped} unchanged.`);
171
+ if (opts.dryRun) console.log('(dry-run — no files written)');
172
+ }
173
+
174
+ if (import.meta.url === `file://${process.argv[1]}`) {
175
+ main();
176
+ }