@glossarist/concept-browser 0.7.85 → 0.7.86

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.86",
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.24",
41
41
  "js-yaml": "^4.1.0",
42
42
  "jszip": "^3.10.1",
43
43
  "pinia": "^2.3.1",
@@ -4,6 +4,13 @@ function buildConceptUri(uriBase, registerId, conceptId) {
4
4
 
5
5
  const VALID_COMPLETENESS = new Set(['complete', 'partial']);
6
6
  const VALID_CERTAINTY = new Set(['confirmed', 'possible']);
7
+ const VALID_MULTIPLICITY = new Set([
8
+ 'compulsory',
9
+ 'optional',
10
+ 'compulsory_multiple',
11
+ 'optional_multiple',
12
+ 'compulsory_at_least_one',
13
+ ]);
7
14
 
8
15
  function validateCompleteness(value) {
9
16
  if (value == null) return 'complete';
@@ -15,14 +22,40 @@ function validateCompleteness(value) {
15
22
  return value;
16
23
  }
17
24
 
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
- );
25
+ /**
26
+ * Resolve a partitive member's multiplicity from JSON-LD.
27
+ *
28
+ * Reads gl:multiplicity first (v3 wire shape, ISO 704:2022). Falls back
29
+ * to migrating from gl:certainty (v2 wire shape) for one release cycle
30
+ * until upstream emits multiplicity natively.
31
+ *
32
+ * confirmed → compulsory
33
+ * possible → optional
34
+ */
35
+ function resolveMultiplicity(member) {
36
+ const m = member['gl:multiplicity'] ?? member.multiplicity;
37
+ if (m != null) {
38
+ if (!VALID_MULTIPLICITY.has(m)) {
39
+ throw new Error(
40
+ `Invalid partitive member multiplicity: "${m}". Allowed: ${[...VALID_MULTIPLICITY].join(', ')}`,
41
+ );
42
+ }
43
+ return m;
24
44
  }
25
- return value;
45
+ const certainty = member['gl:certainty'] ?? member.certainty;
46
+ if (certainty != null) {
47
+ if (!VALID_CERTAINTY.has(certainty)) {
48
+ throw new Error(
49
+ `Invalid partitive member certainty: "${certainty}". Allowed: confirmed, possible`,
50
+ );
51
+ }
52
+ return certainty === 'possible' ? 'optional' : 'compulsory';
53
+ }
54
+ return 'compulsory';
55
+ }
56
+
57
+ function resolveIsDelimiting(member) {
58
+ return member['gl:isDelimiting'] === true || member.isDelimiting === true;
26
59
  }
27
60
 
28
61
  function normalizeLocalizedString(content) {
@@ -183,21 +216,19 @@ function extractPartitiveRelations(concept, registerId, uriBase, urnMap) {
183
216
  if (!uri || uri === sourceUri) return null;
184
217
  return {
185
218
  uri,
186
- certainty: validateCertainty(p['gl:certainty']),
219
+ multiplicity: resolveMultiplicity(p),
220
+ isDelimiting: resolveIsDelimiting(p),
187
221
  };
188
222
  })
189
223
  .filter(p => p !== null);
190
224
 
191
225
  if (partitives.length === 0) continue;
192
226
 
193
- const plurality = resolvePlurality(rel['gl:hasPlurality']);
194
-
195
227
  relations.push({
196
228
  source: sourceUri,
197
229
  comprehensive,
198
230
  partitives,
199
231
  completeness: validateCompleteness(rel['gl:completeness']),
200
- plurality,
201
232
  criterion: normalizeLocalizedString(rel['gl:criterion']),
202
233
  register: registerId,
203
234
  });
@@ -205,21 +236,6 @@ function extractPartitiveRelations(concept, registerId, uriBase, urnMap) {
205
236
  return relations;
206
237
  }
207
238
 
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
239
  function resolveConceptUri(ref, uriBase, urnMap) {
224
240
  const source = ref['gl:source'] || ref['source'];
225
241
  const id = ref['gl:id'] || ref['id'];
@@ -14,6 +14,23 @@ 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
+ // Legacy v2 → v3 migration helper: derive a gl:certainty value from the
19
+ // new ISO 704:2022 multiplicity so consumers that still read certainty
20
+ // keep working during the one-release-cycle deprecation window.
21
+ const CERTAINTY_FROM_MULTIPLICITY = {
22
+ compulsory: 'confirmed',
23
+ optional: 'possible',
24
+ compulsory_multiple: 'confirmed',
25
+ optional_multiple: 'possible',
26
+ compulsory_at_least_one: 'confirmed',
27
+ };
28
+ function multiplicityToCertainty(multiplicity) {
29
+ return multiplicity in CERTAINTY_FROM_MULTIPLICITY
30
+ ? CERTAINTY_FROM_MULTIPLICITY[multiplicity]
31
+ : null;
32
+ }
33
+
17
34
  function buildConceptUri(uriBase, registerId, conceptId) {
18
35
  return `${uriBase}/${registerId}/concept/${conceptId}`;
19
36
  }
@@ -693,7 +710,18 @@ function yamlToJsonLd(conceptYaml, register, refMaps) {
693
710
  } else {
694
711
  m['gl:ref'] = refToJsonLd(member, 'gl:ConceptRef');
695
712
  }
696
- if (member.certainty) m['gl:certainty'] = member.certainty;
713
+ // ISO 704:2022 multiplicity + is_delimiting (v3 wire shape).
714
+ // Emit both new and legacy fields for one release cycle so
715
+ // consumers that still read gl:certainty keep working.
716
+ if (member.multiplicity) m['gl:multiplicity'] = member.multiplicity;
717
+ if (member.is_delimiting === true || member.isDelimiting === true) {
718
+ m['gl:isDelimiting'] = true;
719
+ }
720
+ // Legacy gl:certainty — derived from multiplicity when not set
721
+ // explicitly. Remove after consumers migrate to multiplicity.
722
+ const legacyCertainty = member.certainty
723
+ ?? multiplicityToCertainty(member.multiplicity);
724
+ if (legacyCertainty) m['gl:certainty'] = legacyCertainty;
697
725
  return m;
698
726
  });
699
727
  }
@@ -977,7 +1005,14 @@ async function processDataset(dir, register, opts) {
977
1005
  partitiveRelations: {
978
1006
  count: 0,
979
1007
  byCompleteness: { complete: 0, partial: 0 },
980
- byMemberCertainty: { confirmed: 0, possible: 0 },
1008
+ byMemberMultiplicity: {
1009
+ compulsory: 0,
1010
+ optional: 0,
1011
+ compulsory_multiple: 0,
1012
+ optional_multiple: 0,
1013
+ compulsory_at_least_one: 0,
1014
+ },
1015
+ delimitingMembers: 0,
981
1016
  withCriterion: 0,
982
1017
  withoutCriterion: 0,
983
1018
  withPlurality: 0,
@@ -1013,9 +1048,14 @@ async function processDataset(dir, register, opts) {
1013
1048
  (s.partitiveRelations.byCompleteness[completeness] || 0) + 1;
1014
1049
  const partitives = rel.partitives || [];
1015
1050
  for (const member of partitives) {
1016
- const certainty = member.certainty || 'confirmed';
1017
- s.partitiveRelations.byMemberCertainty[certainty] =
1018
- (s.partitiveRelations.byMemberCertainty[certainty] || 0) + 1;
1051
+ // Prefer multiplicity; fall back to legacy certainty for one release.
1052
+ const multiplicity = member.multiplicity
1053
+ ?? (member.certainty === 'possible' ? 'optional' : 'compulsory');
1054
+ s.partitiveRelations.byMemberMultiplicity[multiplicity] =
1055
+ (s.partitiveRelations.byMemberMultiplicity[multiplicity] || 0) + 1;
1056
+ if (member.is_delimiting === true || member.isDelimiting === true) {
1057
+ s.partitiveRelations.delimitingMembers += 1;
1058
+ }
1019
1059
  }
1020
1060
  if (rel.criterion) {
1021
1061
  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
+ }