@glossarist/concept-browser 0.7.84 → 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 +2 -2
- package/scripts/build-edges.js +42 -26
- package/scripts/generate-data.mjs +45 -5
- package/scripts/migrate-v1-to-v3.d.mts +6 -0
- package/scripts/migrate-v1-to-v3.mjs +176 -0
- package/src/__tests__/components/PartitiveRelationDiagram.test.ts +92 -136
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +115 -87
- package/src/__tests__/partitive-relation-styling.test.ts +12 -54
- package/src/__tests__/partitive-relation.test.ts +42 -90
- package/src/__tests__/scripts/migrate-v1-to-v3.d.mts +8 -0
- package/src/__tests__/scripts/migrate-v1-to-v3.test.ts +115 -0
- package/src/__tests__/utils/partitive-multiplicity.test.ts +113 -0
- package/src/adapters/model-bridge.ts +15 -19
- package/src/adapters/non-verbal/glossarist-augment.d.ts +34 -18
- package/src/adapters/types.ts +2 -8
- package/src/components/PartitiveRelationDiagram.vue +88 -76
- package/src/components/PartitiveRelationList.vue +26 -34
- package/src/components/RelationSphere.vue +173 -106
- package/src/composables/use-concept-edges.ts +21 -21
- package/src/utils/partitive-multiplicity.ts +106 -0
- package/src/utils/partitive-relation-styling.ts +29 -38
- package/src/views/ConceptView.vue +7 -13
- package/src/__tests__/model-bridge-partitive.test.ts +0 -146
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glossarist/concept-browser",
|
|
3
|
-
"version": "0.7.
|
|
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.
|
|
40
|
+
"glossarist": "^0.4.24",
|
|
41
41
|
"js-yaml": "^4.1.0",
|
|
42
42
|
"jszip": "^3.10.1",
|
|
43
43
|
"pinia": "^2.3.1",
|
package/scripts/build-edges.js
CHANGED
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1017
|
-
|
|
1018
|
-
(
|
|
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
|
+
}
|