glost 0.2.0 → 0.4.0
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/CHANGELOG.md +156 -0
- package/README.md +4 -4
- package/dist/cli/migrate.d.ts +8 -0
- package/dist/cli/migrate.d.ts.map +1 -0
- package/dist/cli/migrate.js +229 -0
- package/dist/cli/migrate.js.map +1 -0
- package/dist/errors.d.ts +168 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +300 -0
- package/dist/errors.js.map +1 -0
- package/dist/guards.d.ts +1 -1
- package/dist/guards.d.ts.map +1 -1
- package/dist/index.d.ts +8 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -6
- package/dist/index.js.map +1 -1
- package/dist/nodes.d.ts +1 -1
- package/dist/nodes.d.ts.map +1 -1
- package/dist/nodes.js +0 -1
- package/dist/nodes.js.map +1 -1
- package/dist/types.d.ts +25 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils.d.ts +2 -2
- package/dist/utils.d.ts.map +1 -1
- package/package.json +12 -2
- package/src/cli/migrate.ts +294 -0
- package/src/errors.ts +394 -0
- package/src/guards.ts +1 -1
- package/src/index.ts +9 -8
- package/src/nodes.ts +1 -2
- package/src/types.ts +25 -9
- package/src/utils.ts +2 -2
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* GLOST Migration CLI
|
|
4
|
+
*
|
|
5
|
+
* Command-line interface for migrating GLOST documents
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
// Type declarations for glost-utils (dynamically imported)
|
|
13
|
+
type MigrationResult = {
|
|
14
|
+
nodesUpdated: number;
|
|
15
|
+
changes: Array<{ path: string; oldCode: string; newCode: string }>;
|
|
16
|
+
hasChanges: boolean;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type TranscriptionMigrationResult = {
|
|
20
|
+
transcriptionsUpdated: number;
|
|
21
|
+
nodesProcessed: number;
|
|
22
|
+
hasChanges: boolean;
|
|
23
|
+
changes: Array<{ path: string; systems: string[] }>;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type GlostUtils = {
|
|
27
|
+
migrateAllLanguageCodes: (doc: any, options?: any) => MigrationResult;
|
|
28
|
+
migrateTranscriptionSchema: (doc: any, options?: any) => TranscriptionMigrationResult;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Import migration functions from glost-utils
|
|
32
|
+
async function loadUtils(): Promise<GlostUtils> {
|
|
33
|
+
try {
|
|
34
|
+
// @ts-expect-error - glost-utils is an optional peer dependency
|
|
35
|
+
const utils = await import('glost-utils') as any;
|
|
36
|
+
return {
|
|
37
|
+
migrateAllLanguageCodes: utils.migrateAllLanguageCodes,
|
|
38
|
+
migrateTranscriptionSchema: utils.migrateTranscriptionSchema,
|
|
39
|
+
};
|
|
40
|
+
} catch (error) {
|
|
41
|
+
console.error('Error: glost-utils is required for migration.');
|
|
42
|
+
console.error('Install it with: npm install glost-utils');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface CompleteMigrationResult {
|
|
48
|
+
languageCodes: MigrationResult;
|
|
49
|
+
transcriptionSchema: TranscriptionMigrationResult;
|
|
50
|
+
totalChanges: number;
|
|
51
|
+
success: boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function migrateToV04(doc: any, options: { dryRun?: boolean; addDefaultRegions?: boolean } = {}): Promise<CompleteMigrationResult> {
|
|
55
|
+
const { dryRun = false } = options;
|
|
56
|
+
const utils = await loadUtils();
|
|
57
|
+
|
|
58
|
+
const languageCodes = utils.migrateAllLanguageCodes(doc, {
|
|
59
|
+
addDefaultRegions: options.addDefaultRegions ?? true,
|
|
60
|
+
dryRun,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const transcriptionSchema = utils.migrateTranscriptionSchema(doc, {
|
|
64
|
+
dryRun,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const totalChanges = languageCodes.changes.length + transcriptionSchema.transcriptionsUpdated;
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
languageCodes,
|
|
71
|
+
transcriptionSchema,
|
|
72
|
+
totalChanges,
|
|
73
|
+
success: true,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const args = process.argv.slice(2);
|
|
78
|
+
|
|
79
|
+
function showHelp() {
|
|
80
|
+
console.log(`
|
|
81
|
+
GLOST Migration Tool
|
|
82
|
+
|
|
83
|
+
Usage:
|
|
84
|
+
glost migrate <command> <path>
|
|
85
|
+
|
|
86
|
+
Commands:
|
|
87
|
+
v0.3-to-v0.4 <path> Migrate documents from v0.3.x to v0.4.0
|
|
88
|
+
analyze <path> Analyze what would be migrated (dry run)
|
|
89
|
+
help Show this help message
|
|
90
|
+
|
|
91
|
+
Options:
|
|
92
|
+
--no-regions Don't add default regions to language codes
|
|
93
|
+
--dry-run Show what would change without modifying files
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
glost migrate v0.3-to-v0.4 ./docs
|
|
97
|
+
glost migrate analyze ./docs/story.glost.json
|
|
98
|
+
glost migrate v0.3-to-v0.4 ./docs --dry-run
|
|
99
|
+
`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isGlostFile(filePath: string): boolean {
|
|
103
|
+
return filePath.endsWith('.glost.json') || filePath.endsWith('.json');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function findGlostFiles(dir: string): string[] {
|
|
107
|
+
const files: string[] = [];
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
111
|
+
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
const fullPath = path.join(dir, entry.name);
|
|
114
|
+
|
|
115
|
+
if (entry.isDirectory()) {
|
|
116
|
+
if (entry.name !== 'node_modules' && entry.name !== '.git') {
|
|
117
|
+
files.push(...findGlostFiles(fullPath));
|
|
118
|
+
}
|
|
119
|
+
} else if (entry.isFile() && isGlostFile(entry.name)) {
|
|
120
|
+
files.push(fullPath);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
console.error(`Error reading directory ${dir}:`, error);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return files;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function migrateFile(filePath: string, options: { dryRun?: boolean; addDefaultRegions?: boolean }) {
|
|
131
|
+
try {
|
|
132
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
133
|
+
const doc = JSON.parse(content);
|
|
134
|
+
|
|
135
|
+
const result = await migrateToV04(doc, options);
|
|
136
|
+
|
|
137
|
+
if (result.totalChanges === 0) {
|
|
138
|
+
console.log(`✓ ${filePath} - No changes needed`);
|
|
139
|
+
return { changed: false, file: filePath };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
console.log(`${options.dryRun ? '○' : '✓'} ${filePath} - ${result.totalChanges} changes`);
|
|
143
|
+
|
|
144
|
+
if (result.languageCodes.changes.length > 0) {
|
|
145
|
+
console.log(` Language codes: ${result.languageCodes.nodesUpdated} nodes updated`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (result.transcriptionSchema.transcriptionsUpdated > 0) {
|
|
149
|
+
console.log(` Transcriptions: ${result.transcriptionSchema.transcriptionsUpdated} updated`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (!options.dryRun) {
|
|
153
|
+
fs.writeFileSync(filePath, JSON.stringify(doc, null, 2));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return { changed: true, file: filePath, changes: result.totalChanges };
|
|
157
|
+
} catch (error) {
|
|
158
|
+
console.error(`✗ ${filePath} - Error:`, (error as Error).message);
|
|
159
|
+
return { changed: false, file: filePath, error };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function main() {
|
|
164
|
+
if (args.length === 0 || args[0] === 'help') {
|
|
165
|
+
showHelp();
|
|
166
|
+
process.exit(0);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const command = args[0];
|
|
170
|
+
const targetPath = args[1];
|
|
171
|
+
|
|
172
|
+
if (!targetPath) {
|
|
173
|
+
console.error('Error: No path provided');
|
|
174
|
+
showHelp();
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const options = {
|
|
179
|
+
dryRun: args.includes('--dry-run'),
|
|
180
|
+
addDefaultRegions: !args.includes('--no-regions'),
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const fullPath = path.resolve(targetPath);
|
|
184
|
+
|
|
185
|
+
if (!fs.existsSync(fullPath)) {
|
|
186
|
+
console.error(`Error: Path does not exist: ${fullPath}`);
|
|
187
|
+
process.exit(1);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const stats = fs.statSync(fullPath);
|
|
191
|
+
let files: string[];
|
|
192
|
+
|
|
193
|
+
if (stats.isDirectory()) {
|
|
194
|
+
files = findGlostFiles(fullPath);
|
|
195
|
+
|
|
196
|
+
if (files.length === 0) {
|
|
197
|
+
console.log('No GLOST files found in directory');
|
|
198
|
+
process.exit(0);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
console.log(`Found ${files.length} GLOST file(s)\n`);
|
|
202
|
+
} else if (stats.isFile()) {
|
|
203
|
+
if (!isGlostFile(fullPath)) {
|
|
204
|
+
console.error('Error: File does not appear to be a GLOST document');
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
files = [fullPath];
|
|
208
|
+
} else {
|
|
209
|
+
console.error('Error: Path is neither a file nor a directory');
|
|
210
|
+
process.exit(1);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
switch (command) {
|
|
214
|
+
case 'v0.3-to-v0.4':
|
|
215
|
+
case 'v0-to-v04':
|
|
216
|
+
case 'migrate': {
|
|
217
|
+
if (options.dryRun) {
|
|
218
|
+
console.log('DRY RUN - No files will be modified\n');
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let totalChanged = 0;
|
|
222
|
+
let totalChanges = 0;
|
|
223
|
+
|
|
224
|
+
for (const file of files) {
|
|
225
|
+
const result = await migrateFile(file, options);
|
|
226
|
+
if (result.changed) {
|
|
227
|
+
totalChanged++;
|
|
228
|
+
totalChanges += result.changes || 0;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log(`\nSummary:`);
|
|
233
|
+
console.log(` Files processed: ${files.length}`);
|
|
234
|
+
console.log(` Files ${options.dryRun ? 'needing changes' : 'changed'}: ${totalChanged}`);
|
|
235
|
+
console.log(` Total changes: ${totalChanges}`);
|
|
236
|
+
|
|
237
|
+
if (options.dryRun && totalChanged > 0) {
|
|
238
|
+
console.log(`\nRun without --dry-run to apply changes`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
case 'analyze': {
|
|
245
|
+
for (const file of files) {
|
|
246
|
+
try {
|
|
247
|
+
const content = fs.readFileSync(file, 'utf-8');
|
|
248
|
+
const doc = JSON.parse(content);
|
|
249
|
+
const result = await migrateToV04(doc, { dryRun: true });
|
|
250
|
+
|
|
251
|
+
console.log(`\nFile: ${file}`);
|
|
252
|
+
console.log(`Total changes needed: ${result.totalChanges}`);
|
|
253
|
+
|
|
254
|
+
if (result.languageCodes.changes.length > 0) {
|
|
255
|
+
console.log(`\nLanguage code changes (${result.languageCodes.changes.length}):`);
|
|
256
|
+
result.languageCodes.changes.slice(0, 5).forEach(change => {
|
|
257
|
+
console.log(` ${change.path}: ${change.oldCode} → ${change.newCode}`);
|
|
258
|
+
});
|
|
259
|
+
if (result.languageCodes.changes.length > 5) {
|
|
260
|
+
console.log(` ... and ${result.languageCodes.changes.length - 5} more`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (result.transcriptionSchema.changes.length > 0) {
|
|
265
|
+
console.log(`\nTranscription schema changes (${result.transcriptionSchema.changes.length}):`);
|
|
266
|
+
result.transcriptionSchema.changes.slice(0, 5).forEach(change => {
|
|
267
|
+
console.log(` ${change.path}: systems ${change.systems.join(', ')}`);
|
|
268
|
+
});
|
|
269
|
+
if (result.transcriptionSchema.changes.length > 5) {
|
|
270
|
+
console.log(` ... and ${result.transcriptionSchema.changes.length - 5} more`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (result.totalChanges === 0) {
|
|
275
|
+
console.log('No changes needed');
|
|
276
|
+
}
|
|
277
|
+
} catch (error) {
|
|
278
|
+
console.error(`Error analyzing ${file}:`, (error as Error).message);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
break;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
default:
|
|
285
|
+
console.error(`Unknown command: ${command}`);
|
|
286
|
+
showHelp();
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
main().catch(error => {
|
|
292
|
+
console.error('Fatal error:', error);
|
|
293
|
+
process.exit(1);
|
|
294
|
+
});
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GLOST Error Classes
|
|
3
|
+
*
|
|
4
|
+
* Comprehensive error handling with context, suggestions, and helpful messages
|
|
5
|
+
*
|
|
6
|
+
* @packageDocumentation
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { GLOSTNode } from './types.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Context information for GLOST errors
|
|
13
|
+
*/
|
|
14
|
+
export interface GLOSTErrorContext {
|
|
15
|
+
/** The node where the error occurred */
|
|
16
|
+
node?: GLOSTNode;
|
|
17
|
+
/** Path to the node in the document tree */
|
|
18
|
+
path?: string[];
|
|
19
|
+
/** Source file path (if known) */
|
|
20
|
+
file?: string;
|
|
21
|
+
/** Suggestion for fixing the error */
|
|
22
|
+
suggestion?: string;
|
|
23
|
+
/** URL to relevant documentation */
|
|
24
|
+
docsUrl?: string;
|
|
25
|
+
/** Additional context data */
|
|
26
|
+
[key: string]: any;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Base class for all GLOST errors
|
|
31
|
+
*/
|
|
32
|
+
export class GLOSTError extends Error {
|
|
33
|
+
public readonly context: GLOSTErrorContext;
|
|
34
|
+
|
|
35
|
+
constructor(message: string, context: GLOSTErrorContext = {}) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = 'GLOSTError';
|
|
38
|
+
this.context = context;
|
|
39
|
+
|
|
40
|
+
// Maintain proper stack trace
|
|
41
|
+
if (Error.captureStackTrace) {
|
|
42
|
+
Error.captureStackTrace(this, this.constructor);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Format error message with context
|
|
48
|
+
*/
|
|
49
|
+
toString(): string {
|
|
50
|
+
const parts: string[] = [];
|
|
51
|
+
|
|
52
|
+
// Header
|
|
53
|
+
parts.push(`${this.name}: ${this.message}`);
|
|
54
|
+
parts.push('');
|
|
55
|
+
|
|
56
|
+
// Location information
|
|
57
|
+
if (this.context.path) {
|
|
58
|
+
parts.push(` Location: ${this.context.path.join('.')}`);
|
|
59
|
+
}
|
|
60
|
+
if (this.context.file) {
|
|
61
|
+
parts.push(` File: ${this.context.file}`);
|
|
62
|
+
}
|
|
63
|
+
if (this.context.node) {
|
|
64
|
+
parts.push(` Node type: ${(this.context.node as any).type || 'unknown'}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Suggestion
|
|
68
|
+
if (this.context.suggestion) {
|
|
69
|
+
parts.push('');
|
|
70
|
+
parts.push(` Suggestion: ${this.context.suggestion}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Documentation link
|
|
74
|
+
if (this.context.docsUrl) {
|
|
75
|
+
parts.push('');
|
|
76
|
+
parts.push(` Documentation: ${this.context.docsUrl}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Stack trace
|
|
80
|
+
if (this.stack) {
|
|
81
|
+
parts.push('');
|
|
82
|
+
parts.push(' Stack trace:');
|
|
83
|
+
const stackLines = this.stack.split('\n').slice(1); // Skip first line (message)
|
|
84
|
+
parts.push(...stackLines.map(line => ` ${line}`));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return parts.join('\n');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get a concise error summary
|
|
92
|
+
*/
|
|
93
|
+
toSummary(): string {
|
|
94
|
+
let summary = `${this.name}: ${this.message}`;
|
|
95
|
+
if (this.context.path) {
|
|
96
|
+
summary += ` (at ${this.context.path.join('.')})`;
|
|
97
|
+
}
|
|
98
|
+
return summary;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Validation error for schema violations
|
|
104
|
+
*/
|
|
105
|
+
export class GLOSTValidationError extends GLOSTError {
|
|
106
|
+
constructor(message: string, context: GLOSTErrorContext = {}) {
|
|
107
|
+
super(message, context);
|
|
108
|
+
this.name = 'GLOSTValidationError';
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
toString(): string {
|
|
112
|
+
const parts: string[] = [];
|
|
113
|
+
|
|
114
|
+
parts.push(`${this.name}: ${this.message}`);
|
|
115
|
+
parts.push('');
|
|
116
|
+
|
|
117
|
+
// Location
|
|
118
|
+
if (this.context.path) {
|
|
119
|
+
const location = this.context.path.length > 0
|
|
120
|
+
? this.context.path.join('.')
|
|
121
|
+
: 'root';
|
|
122
|
+
parts.push(` Location: ${location}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (this.context.node) {
|
|
126
|
+
const node = this.context.node as any;
|
|
127
|
+
parts.push(` Node type: ${node.type || 'unknown'}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this.context.file) {
|
|
131
|
+
parts.push(` File: ${this.context.file}`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Expected vs Received
|
|
135
|
+
if (this.context.expected) {
|
|
136
|
+
parts.push('');
|
|
137
|
+
parts.push(` Expected: ${JSON.stringify(this.context.expected, null, 2)}`);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (this.context.received) {
|
|
141
|
+
parts.push(` Received: ${JSON.stringify(this.context.received, null, 2)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Problem explanation
|
|
145
|
+
if (this.context.problem) {
|
|
146
|
+
parts.push('');
|
|
147
|
+
parts.push(` Problem: ${this.context.problem}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Suggestion
|
|
151
|
+
if (this.context.suggestion) {
|
|
152
|
+
parts.push('');
|
|
153
|
+
parts.push(` Suggestion: ${this.context.suggestion}`);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Documentation
|
|
157
|
+
if (this.context.docsUrl) {
|
|
158
|
+
parts.push('');
|
|
159
|
+
parts.push(` Documentation: ${this.context.docsUrl}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return parts.join('\n');
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Error for missing required fields
|
|
168
|
+
*/
|
|
169
|
+
export class GLOSTMissingFieldError extends GLOSTValidationError {
|
|
170
|
+
constructor(
|
|
171
|
+
fieldName: string,
|
|
172
|
+
nodeType: string,
|
|
173
|
+
context: GLOSTErrorContext = {}
|
|
174
|
+
) {
|
|
175
|
+
const message = `Missing required field '${fieldName}' on ${nodeType}`;
|
|
176
|
+
super(message, {
|
|
177
|
+
...context,
|
|
178
|
+
problem: `${nodeType} must have a '${fieldName}' field.`,
|
|
179
|
+
docsUrl: context.docsUrl || `https://glost.dev/docs/node-types#${nodeType.toLowerCase()}`,
|
|
180
|
+
});
|
|
181
|
+
this.name = 'GLOSTMissingFieldError';
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* Error for invalid field types
|
|
187
|
+
*/
|
|
188
|
+
export class GLOSTInvalidTypeError extends GLOSTValidationError {
|
|
189
|
+
constructor(
|
|
190
|
+
fieldName: string,
|
|
191
|
+
expectedType: string,
|
|
192
|
+
receivedType: string,
|
|
193
|
+
context: GLOSTErrorContext = {}
|
|
194
|
+
) {
|
|
195
|
+
const message = `Invalid type for field '${fieldName}': expected ${expectedType}, got ${receivedType}`;
|
|
196
|
+
super(message, {
|
|
197
|
+
...context,
|
|
198
|
+
problem: `Field '${fieldName}' must be of type ${expectedType}.`,
|
|
199
|
+
suggestion: `Convert the value to ${expectedType} or check your data source.`,
|
|
200
|
+
});
|
|
201
|
+
this.name = 'GLOSTInvalidTypeError';
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Error for invalid language codes
|
|
207
|
+
*/
|
|
208
|
+
export class GLOSTInvalidLanguageCodeError extends GLOSTValidationError {
|
|
209
|
+
constructor(
|
|
210
|
+
code: string,
|
|
211
|
+
context: GLOSTErrorContext = {}
|
|
212
|
+
) {
|
|
213
|
+
const message = `Invalid language code: "${code}"`;
|
|
214
|
+
super(message, {
|
|
215
|
+
...context,
|
|
216
|
+
problem: `Language codes must follow BCP-47 format (e.g., "en-US", "th-TH").`,
|
|
217
|
+
suggestion: `Use normalizeLanguageCode() from glost-common to convert "${code}" to a valid format.`,
|
|
218
|
+
docsUrl: context.docsUrl || 'https://glost.dev/docs/languages#bcp-47',
|
|
219
|
+
});
|
|
220
|
+
this.name = 'GLOSTInvalidLanguageCodeError';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Error for extension processing
|
|
226
|
+
*/
|
|
227
|
+
export class GLOSTExtensionError extends GLOSTError {
|
|
228
|
+
constructor(
|
|
229
|
+
extensionName: string,
|
|
230
|
+
message: string,
|
|
231
|
+
context: GLOSTErrorContext = {}
|
|
232
|
+
) {
|
|
233
|
+
super(`[${extensionName}] ${message}`, context);
|
|
234
|
+
this.name = 'GLOSTExtensionError';
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Error for provider issues
|
|
240
|
+
*/
|
|
241
|
+
export class GLOSTProviderError extends GLOSTError {
|
|
242
|
+
constructor(
|
|
243
|
+
providerName: string,
|
|
244
|
+
message: string,
|
|
245
|
+
context: GLOSTErrorContext = {}
|
|
246
|
+
) {
|
|
247
|
+
super(`[${providerName} Provider] ${message}`, context);
|
|
248
|
+
this.name = 'GLOSTProviderError';
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Error for document parsing
|
|
254
|
+
*/
|
|
255
|
+
export class GLOSTParseError extends GLOSTError {
|
|
256
|
+
constructor(message: string, context: GLOSTErrorContext = {}) {
|
|
257
|
+
super(message, context);
|
|
258
|
+
this.name = 'GLOSTParseError';
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Error for serialization issues
|
|
264
|
+
*/
|
|
265
|
+
export class GLOSTSerializationError extends GLOSTError {
|
|
266
|
+
constructor(message: string, context: GLOSTErrorContext = {}) {
|
|
267
|
+
super(message, context);
|
|
268
|
+
this.name = 'GLOSTSerializationError';
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Create a validation error with helpful context
|
|
274
|
+
*
|
|
275
|
+
* @param message - Error message
|
|
276
|
+
* @param options - Error options
|
|
277
|
+
* @returns GLOSTValidationError instance
|
|
278
|
+
*
|
|
279
|
+
* @example
|
|
280
|
+
* ```typescript
|
|
281
|
+
* throw createValidationError('Invalid word node', {
|
|
282
|
+
* node: wordNode,
|
|
283
|
+
* path: ['document', 'children', '0'],
|
|
284
|
+
* suggestion: 'Add a "text" field to the word node',
|
|
285
|
+
* docsUrl: 'https://glost.dev/docs/node-types#word'
|
|
286
|
+
* });
|
|
287
|
+
* ```
|
|
288
|
+
*/
|
|
289
|
+
export function createValidationError(
|
|
290
|
+
message: string,
|
|
291
|
+
options: {
|
|
292
|
+
node?: GLOSTNode;
|
|
293
|
+
path?: string[];
|
|
294
|
+
file?: string;
|
|
295
|
+
suggestion?: string;
|
|
296
|
+
docsUrl?: string;
|
|
297
|
+
expected?: any;
|
|
298
|
+
received?: any;
|
|
299
|
+
problem?: string;
|
|
300
|
+
} = {}
|
|
301
|
+
): GLOSTValidationError {
|
|
302
|
+
return new GLOSTValidationError(message, options);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Format a path array as a readable string
|
|
307
|
+
*
|
|
308
|
+
* @param path - Path array
|
|
309
|
+
* @returns Formatted path string
|
|
310
|
+
*
|
|
311
|
+
* @example
|
|
312
|
+
* ```typescript
|
|
313
|
+
* formatPath(['document', 'children', '0', 'text'])
|
|
314
|
+
* // Returns: "document.children[0].text"
|
|
315
|
+
* ```
|
|
316
|
+
*/
|
|
317
|
+
export function formatPath(path: Array<string | number>): string {
|
|
318
|
+
if (path.length === 0) return 'root';
|
|
319
|
+
|
|
320
|
+
return path.reduce<string>((acc, segment, index) => {
|
|
321
|
+
if (index === 0) return String(segment);
|
|
322
|
+
|
|
323
|
+
if (typeof segment === 'number' || !isNaN(Number(segment))) {
|
|
324
|
+
return `${acc}[${segment}]`;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return `${acc}.${segment}`;
|
|
328
|
+
}, '');
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Assert that a condition is true, throw validation error if not
|
|
333
|
+
*
|
|
334
|
+
* @param condition - Condition to check
|
|
335
|
+
* @param message - Error message if condition is false
|
|
336
|
+
* @param context - Error context
|
|
337
|
+
*
|
|
338
|
+
* @example
|
|
339
|
+
* ```typescript
|
|
340
|
+
* glostAssert(
|
|
341
|
+
* node.type === 'word',
|
|
342
|
+
* 'Node must be a word node',
|
|
343
|
+
* { node, path: ['document', 'children', '0'] }
|
|
344
|
+
* );
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
export function glostAssert(
|
|
348
|
+
condition: any,
|
|
349
|
+
message: string,
|
|
350
|
+
context?: GLOSTErrorContext
|
|
351
|
+
): asserts condition {
|
|
352
|
+
if (!condition) {
|
|
353
|
+
throw new GLOSTValidationError(message, context);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Wrap an error with additional GLOST context
|
|
359
|
+
*
|
|
360
|
+
* @param error - Original error
|
|
361
|
+
* @param context - Additional context
|
|
362
|
+
* @returns GLOST error
|
|
363
|
+
*
|
|
364
|
+
* @example
|
|
365
|
+
* ```typescript
|
|
366
|
+
* try {
|
|
367
|
+
* await processNode(node);
|
|
368
|
+
* } catch (error) {
|
|
369
|
+
* throw wrapError(error, {
|
|
370
|
+
* node,
|
|
371
|
+
* path: ['document', 'children', '0'],
|
|
372
|
+
* suggestion: 'Check that the node has all required fields'
|
|
373
|
+
* });
|
|
374
|
+
* }
|
|
375
|
+
* ```
|
|
376
|
+
*/
|
|
377
|
+
export function wrapError(
|
|
378
|
+
error: Error,
|
|
379
|
+
context: GLOSTErrorContext
|
|
380
|
+
): GLOSTError {
|
|
381
|
+
if (error instanceof GLOSTError) {
|
|
382
|
+
// Merge contexts - create new error with merged context
|
|
383
|
+
return new GLOSTError(error.message, {
|
|
384
|
+
...error.context,
|
|
385
|
+
...context,
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Create new GLOST error
|
|
390
|
+
return new GLOSTError(error.message, {
|
|
391
|
+
...context,
|
|
392
|
+
originalError: error,
|
|
393
|
+
});
|
|
394
|
+
}
|