@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 +2 -2
- package/scripts/build-edges.js +50 -27
- package/scripts/generate-data.mjs +59 -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 +98 -136
- package/src/__tests__/composables/use-concept-edges-v2.test.ts +123 -88
- package/src/__tests__/partitive-relation-styling.test.ts +12 -54
- package/src/__tests__/partitive-relation.test.ts +72 -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 +137 -0
- package/src/adapters/model-bridge.ts +48 -19
- package/src/adapters/non-verbal/glossarist-augment.d.ts +85 -54
- package/src/adapters/types.ts +3 -8
- package/src/components/PartitiveRelationDiagram.vue +90 -76
- package/src/components/PartitiveRelationList.vue +27 -34
- package/src/components/RelationSphere.vue +58 -73
- package/src/composables/use-concept-edges.ts +26 -21
- package/src/utils/partitive-multiplicity.ts +99 -0
- package/src/utils/partitive-relation-styling.ts +29 -38
- package/src/views/ConceptView.vue +8 -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.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.
|
|
40
|
+
"glossarist": "^0.4.26",
|
|
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
|
@@ -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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1017
|
-
|
|
1018
|
-
|
|
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
|
+
}
|