@ifc-lite/viewer 1.14.2 → 1.14.3

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.
Files changed (73) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/assets/{Arrow.dom-CSgnLhN4.js → Arrow.dom-BgkZDIQm.js} +1 -1
  3. package/dist/assets/basketViewActivator-h_M3YbMW.js +1 -0
  4. package/dist/assets/{browser-qSKWrKQW.js → browser-CRQ0bPh1.js} +1 -1
  5. package/dist/assets/ifc-lite_bg-DyBKoGgk.wasm +0 -0
  6. package/dist/assets/{index-4Y4XaV8N.js → index-Be6XjVeM.js} +72324 -61561
  7. package/dist/assets/index-C4VVJRL-.js +229 -0
  8. package/dist/assets/index-DdwD4c-E.css +1 -0
  9. package/dist/assets/{native-bridge-CSFDsEkg.js → native-bridge-DtcJqlOi.js} +1 -1
  10. package/dist/assets/{wasm-bridge-Zf90ysEm.js → wasm-bridge-BJJVu9P2.js} +1 -1
  11. package/dist/index.html +2 -2
  12. package/package.json +21 -20
  13. package/src/App.tsx +17 -1
  14. package/src/components/viewer/BasketPresentationDock.tsx +8 -4
  15. package/src/components/viewer/ChatPanel.tsx +1402 -0
  16. package/src/components/viewer/CodeEditor.tsx +70 -4
  17. package/src/components/viewer/MainToolbar.tsx +1 -1
  18. package/src/components/viewer/ScriptPanel.tsx +351 -184
  19. package/src/components/viewer/UpgradePage.tsx +69 -0
  20. package/src/components/viewer/Viewport.tsx +23 -0
  21. package/src/components/viewer/chat/ChatMessage.tsx +144 -0
  22. package/src/components/viewer/chat/ExecutableCodeBlock.tsx +416 -0
  23. package/src/components/viewer/chat/ModelSelector.tsx +102 -0
  24. package/src/components/viewer/chat/renderTextContent.test.ts +23 -0
  25. package/src/components/viewer/chat/renderTextContent.ts +19 -0
  26. package/src/hooks/useIfcCache.ts +1 -2
  27. package/src/hooks/useSandbox.ts +122 -6
  28. package/src/index.css +10 -0
  29. package/src/lib/attachments.ts +46 -0
  30. package/src/lib/llm/ClerkChatSync.tsx +74 -0
  31. package/src/lib/llm/clerk-auth.ts +62 -0
  32. package/src/lib/llm/code-extractor.ts +50 -0
  33. package/src/lib/llm/context-builder.test.ts +18 -0
  34. package/src/lib/llm/context-builder.ts +305 -0
  35. package/src/lib/llm/free-models.test.ts +118 -0
  36. package/src/lib/llm/message-capabilities.test.ts +131 -0
  37. package/src/lib/llm/message-capabilities.ts +94 -0
  38. package/src/lib/llm/models.ts +197 -0
  39. package/src/lib/llm/repair-loop.test.ts +91 -0
  40. package/src/lib/llm/repair-loop.ts +76 -0
  41. package/src/lib/llm/script-diagnostics.ts +445 -0
  42. package/src/lib/llm/script-edit-ops.test.ts +399 -0
  43. package/src/lib/llm/script-edit-ops.ts +954 -0
  44. package/src/lib/llm/script-preflight.test.ts +513 -0
  45. package/src/lib/llm/script-preflight.ts +990 -0
  46. package/src/lib/llm/script-preservation.test.ts +128 -0
  47. package/src/lib/llm/script-preservation.ts +152 -0
  48. package/src/lib/llm/stream-client.test.ts +97 -0
  49. package/src/lib/llm/stream-client.ts +410 -0
  50. package/src/lib/llm/system-prompt.test.ts +181 -0
  51. package/src/lib/llm/system-prompt.ts +665 -0
  52. package/src/lib/llm/types.ts +150 -0
  53. package/src/lib/scripts/templates/bim-globals.d.ts +226 -7
  54. package/src/lib/scripts/templates/create-building.ts +12 -12
  55. package/src/main.tsx +10 -1
  56. package/src/sdk/adapters/export-adapter.test.ts +24 -0
  57. package/src/sdk/adapters/export-adapter.ts +40 -16
  58. package/src/sdk/adapters/files-adapter.ts +39 -0
  59. package/src/sdk/adapters/model-compat.ts +1 -1
  60. package/src/sdk/adapters/mutate-adapter.ts +20 -6
  61. package/src/sdk/adapters/mutation-view.ts +112 -0
  62. package/src/sdk/adapters/query-adapter.ts +100 -4
  63. package/src/sdk/local-backend.ts +4 -0
  64. package/src/store/index.ts +15 -1
  65. package/src/store/slices/chatSlice.test.ts +325 -0
  66. package/src/store/slices/chatSlice.ts +468 -0
  67. package/src/store/slices/scriptSlice.test.ts +75 -0
  68. package/src/store/slices/scriptSlice.ts +256 -9
  69. package/src/vite-env.d.ts +10 -0
  70. package/vite.config.ts +21 -2
  71. package/dist/assets/ifc-lite_bg-BOvNXJA_.wasm +0 -0
  72. package/dist/assets/index-ByrFvN5A.css +0 -1
  73. package/dist/assets/index-CN7qDq7G.js +0 -216
@@ -0,0 +1,445 @@
1
+ /* This Source Code Form is subject to the terms of the Mozilla Public
2
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
3
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
+
5
+ export type ScriptDiagnosticSeverity = 'error' | 'warning';
6
+ export type ScriptDiagnosticSource = 'preflight' | 'runtime' | 'patch';
7
+ export type RepairScope = 'local' | 'block' | 'structural' | 'full_rewrite';
8
+
9
+ export type PreflightDiagnosticCode =
10
+ | 'unknown_namespace'
11
+ | 'unknown_method'
12
+ | 'create_contract'
13
+ | 'bare_identifier'
14
+ | 'wall_hosted_opening_pattern'
15
+ | 'metadata_query_pattern'
16
+ | 'world_placement_elevation'
17
+ | 'detached_snippet_scope';
18
+
19
+ export type RuntimeDiagnosticCode =
20
+ | 'generic_placement_contract'
21
+ | 'plate_contract_mismatch'
22
+ | 'world_placement_elevation'
23
+ | 'detached_snippet_scope'
24
+ | 'wall_hosted_opening_alignment';
25
+
26
+ export type PatchDiagnosticCode =
27
+ | 'patch_revision_conflict'
28
+ | 'patch_range_error'
29
+ | 'patch_semantic_error'
30
+ | 'unsafe_full_replacement'
31
+ | 'destructive_partial_rewrite';
32
+
33
+ export interface ScriptDiagnosticRange {
34
+ from: number;
35
+ to: number;
36
+ }
37
+
38
+ export interface ScriptDiagnosticEvidence {
39
+ source?: ScriptDiagnosticSource;
40
+ code?: string;
41
+ message?: string;
42
+ methodName?: string;
43
+ symbol?: string;
44
+ failureKind?: string;
45
+ range?: ScriptDiagnosticRange;
46
+ line?: number;
47
+ column?: number;
48
+ snippet?: string;
49
+ }
50
+
51
+ export interface ScriptDiagnosticBase<TSource extends ScriptDiagnosticSource, TCode extends string> {
52
+ source: TSource;
53
+ code: TCode;
54
+ severity: ScriptDiagnosticSeverity;
55
+ message: string;
56
+ rootCauseKey: string;
57
+ repairScope: RepairScope;
58
+ evidence?: ScriptDiagnosticEvidence[];
59
+ data?: Record<string, unknown>;
60
+ }
61
+
62
+ export type PreflightScriptDiagnostic = ScriptDiagnosticBase<'preflight', PreflightDiagnosticCode>;
63
+ export type RuntimeScriptDiagnostic = ScriptDiagnosticBase<'runtime', RuntimeDiagnosticCode>;
64
+ export type PatchScriptDiagnostic = ScriptDiagnosticBase<'patch', PatchDiagnosticCode>;
65
+
66
+ export type ScriptDiagnostic =
67
+ | PreflightScriptDiagnostic
68
+ | RuntimeScriptDiagnostic
69
+ | PatchScriptDiagnostic;
70
+
71
+ export interface RootCauseDiagnosticGroup {
72
+ rootCauseKey: string;
73
+ repairScope: RepairScope;
74
+ summary: string;
75
+ diagnostics: ScriptDiagnostic[];
76
+ evidence: ScriptDiagnosticEvidence[];
77
+ }
78
+
79
+ export function createPreflightDiagnostic(
80
+ code: PreflightDiagnosticCode,
81
+ message: string,
82
+ severity: ScriptDiagnosticSeverity = 'error',
83
+ data?: Record<string, unknown>,
84
+ ): PreflightScriptDiagnostic {
85
+ return createDiagnosticBase('preflight', code, message, severity, data);
86
+ }
87
+
88
+ export function createRuntimeDiagnostic(
89
+ code: RuntimeDiagnosticCode,
90
+ message: string,
91
+ severity: ScriptDiagnosticSeverity = 'error',
92
+ data?: Record<string, unknown>,
93
+ ): RuntimeScriptDiagnostic {
94
+ return createDiagnosticBase('runtime', code, message, severity, data);
95
+ }
96
+
97
+ export function createPatchDiagnostic(
98
+ code: PatchDiagnosticCode,
99
+ message: string,
100
+ severity: ScriptDiagnosticSeverity = 'error',
101
+ data?: Record<string, unknown>,
102
+ ): PatchScriptDiagnostic {
103
+ return createDiagnosticBase('patch', code, message, severity, data);
104
+ }
105
+
106
+ export function formatDiagnosticsForDisplay(diagnostics: ScriptDiagnostic[]): string[] {
107
+ return diagnostics.map((diagnostic) => diagnostic.message);
108
+ }
109
+
110
+ export function formatDiagnosticsForPrompt(diagnostics: ScriptDiagnostic[]): string {
111
+ if (diagnostics.length === 0) return '';
112
+ const groups = groupDiagnosticsByRootCause(diagnostics);
113
+ const rootCauseLines = groups.flatMap((group) => {
114
+ const lines = [`- [root-cause:${group.rootCauseKey}] ${group.summary} (scope=${group.repairScope})`];
115
+ for (const evidence of group.evidence.slice(0, 4)) {
116
+ lines.push(` supporting evidence: ${formatEvidence(evidence)}`);
117
+ }
118
+ return lines;
119
+ });
120
+ const rawLines = diagnostics.map((diagnostic) => {
121
+ const details = formatDiagnosticDetails(diagnostic.data);
122
+ const hint = typeof diagnostic.data?.fixHint === 'string' ? ` Hint: ${diagnostic.data.fixHint}` : '';
123
+ return `- [${diagnostic.source}:${diagnostic.code}] ${diagnostic.message} (rootCause=${diagnostic.rootCauseKey}, scope=${diagnostic.repairScope})${details}${hint}`;
124
+ });
125
+
126
+ return [
127
+ 'Root causes:',
128
+ ...rootCauseLines,
129
+ 'Raw diagnostics:',
130
+ ...rawLines,
131
+ ].join('\n');
132
+ }
133
+
134
+ export function groupDiagnosticsByRootCause(diagnostics: ScriptDiagnostic[]): RootCauseDiagnosticGroup[] {
135
+ const groups = new Map<string, RootCauseDiagnosticGroup>();
136
+
137
+ for (const diagnostic of diagnostics) {
138
+ const key = diagnostic.rootCauseKey;
139
+ const existing = groups.get(key);
140
+ const evidence = dedupeEvidence(collectDiagnosticEvidence(diagnostic));
141
+ if (!existing) {
142
+ groups.set(key, {
143
+ rootCauseKey: key,
144
+ repairScope: diagnostic.repairScope,
145
+ summary: summarizeRootCause(key, diagnostic),
146
+ diagnostics: [diagnostic],
147
+ evidence,
148
+ });
149
+ continue;
150
+ }
151
+
152
+ existing.diagnostics.push(diagnostic);
153
+ existing.repairScope = widenRepairScope(existing.repairScope, diagnostic.repairScope);
154
+ existing.evidence = dedupeEvidence([...existing.evidence, ...evidence]);
155
+ }
156
+
157
+ return [...groups.values()].sort((left, right) => compareRepairScope(right.repairScope, left.repairScope));
158
+ }
159
+
160
+ export function getPrimaryRootCause(diagnostics: ScriptDiagnostic[]): RootCauseDiagnosticGroup | null {
161
+ return groupDiagnosticsByRootCause(diagnostics)[0] ?? null;
162
+ }
163
+
164
+ function formatDiagnosticDetails(data?: Record<string, unknown>): string {
165
+ if (!data) return '';
166
+
167
+ const details: string[] = [];
168
+ const opId = asString(data.opId);
169
+ const methodName = asString(data.methodName);
170
+ const symbol = asString(data.symbol);
171
+ const failureKind = asString(data.failureKind);
172
+ const snippet = asString(data.snippet);
173
+ const range = formatRange(data.range);
174
+ const selection = formatRange(data.selection);
175
+ const currentEditorRevision = asNumber(data.currentEditorRevision);
176
+ const expectedBaseRevision = asNumber(data.expectedBaseRevision);
177
+ const line = asNumber(data.line);
178
+ const column = asNumber(data.column);
179
+
180
+ if (opId) details.push(`op=${opId}`);
181
+ if (methodName) details.push(`method=${methodName}`);
182
+ if (symbol) details.push(`symbol=${symbol}`);
183
+ if (failureKind) details.push(`failure=${failureKind}`);
184
+ if (line !== null) details.push(`line=${line}`);
185
+ if (column !== null) details.push(`column=${column}`);
186
+ if (range) details.push(`range=${range}`);
187
+ if (selection) details.push(`selection=${selection}`);
188
+ if (expectedBaseRevision !== null) details.push(`expectedRevision=${expectedBaseRevision}`);
189
+ if (currentEditorRevision !== null) details.push(`currentRevision=${currentEditorRevision}`);
190
+ if (snippet) details.push(`snippet=${JSON.stringify(truncateSnippet(snippet))}`);
191
+
192
+ return details.length > 0 ? ` (${details.join(', ')})` : '';
193
+ }
194
+
195
+ function formatRange(value: unknown): string | null {
196
+ if (!value || typeof value !== 'object') return null;
197
+ const from = asNumber((value as Record<string, unknown>).from);
198
+ const to = asNumber((value as Record<string, unknown>).to);
199
+ if (from === null || to === null) return null;
200
+ return `${from}..${to}`;
201
+ }
202
+
203
+ function formatEvidence(evidence: ScriptDiagnosticEvidence): string {
204
+ const parts: string[] = [];
205
+ if (evidence.code) parts.push(`${evidence.source ?? 'diagnostic'}:${evidence.code}`);
206
+ if (evidence.methodName) parts.push(`method=${evidence.methodName}`);
207
+ if (evidence.symbol) parts.push(`symbol=${evidence.symbol}`);
208
+ if (evidence.failureKind) parts.push(`failure=${evidence.failureKind}`);
209
+ const range = formatRange(evidence.range);
210
+ if (range) parts.push(`range=${range}`);
211
+ if (typeof evidence.line === 'number') parts.push(`line=${evidence.line}`);
212
+ if (typeof evidence.column === 'number') parts.push(`column=${evidence.column}`);
213
+ if (evidence.snippet) parts.push(`snippet=${JSON.stringify(truncateSnippet(evidence.snippet))}`);
214
+ if (evidence.message) parts.push(evidence.message);
215
+ return parts.join(', ');
216
+ }
217
+
218
+ function createDiagnosticBase<TSource extends ScriptDiagnosticSource, TCode extends string>(
219
+ source: TSource,
220
+ code: TCode,
221
+ message: string,
222
+ severity: ScriptDiagnosticSeverity,
223
+ data?: Record<string, unknown>,
224
+ ): ScriptDiagnosticBase<TSource, TCode> {
225
+ const rootCauseKey = asString(data?.rootCauseKey) ?? defaultRootCauseKey(source, code);
226
+ const repairScope = asRepairScope(data?.repairScope) ?? defaultRepairScope(rootCauseKey, code);
227
+ const evidence = dedupeEvidence(buildEvidenceFromData(source, code, message, data));
228
+
229
+ return {
230
+ source,
231
+ code,
232
+ severity,
233
+ message,
234
+ rootCauseKey,
235
+ repairScope,
236
+ evidence: evidence.length > 0 ? evidence : undefined,
237
+ data,
238
+ };
239
+ }
240
+
241
+ function buildEvidenceFromData(
242
+ source: ScriptDiagnosticSource,
243
+ code: string,
244
+ message: string,
245
+ data?: Record<string, unknown>,
246
+ ): ScriptDiagnosticEvidence[] {
247
+ const explicit = Array.isArray(data?.evidence)
248
+ ? (data.evidence as unknown[]).map(asEvidence).filter((value): value is ScriptDiagnosticEvidence => Boolean(value))
249
+ : [];
250
+ if (explicit.length > 0) return explicit;
251
+
252
+ const methodName = asString(data?.methodName);
253
+ const symbol = asString(data?.symbol);
254
+ const failureKind = asString(data?.failureKind);
255
+ const snippet = asString(data?.snippet);
256
+ const range = asRange(data?.range);
257
+ const line = asNumber(data?.line);
258
+ const column = asNumber(data?.column);
259
+ if (!methodName && !symbol && !failureKind && !snippet && !range && line === null && column === null) {
260
+ return [];
261
+ }
262
+
263
+ return [{
264
+ source,
265
+ code,
266
+ message,
267
+ methodName: methodName ?? undefined,
268
+ symbol: symbol ?? undefined,
269
+ failureKind: failureKind ?? undefined,
270
+ snippet: snippet ?? undefined,
271
+ range: range ?? undefined,
272
+ line: line ?? undefined,
273
+ column: column ?? undefined,
274
+ }];
275
+ }
276
+
277
+ function collectDiagnosticEvidence(diagnostic: ScriptDiagnostic): ScriptDiagnosticEvidence[] {
278
+ return diagnostic.evidence ?? [];
279
+ }
280
+
281
+ function summarizeRootCause(rootCauseKey: string, diagnostic: ScriptDiagnostic): string {
282
+ switch (rootCauseKey) {
283
+ case 'api_contract_mismatch':
284
+ return 'The failing code does not match the exact BIM API contract and should be repaired by correcting the payload shape or required keys.';
285
+ case 'placement_context_mismatch':
286
+ return 'The script is mixing placement or host-context assumptions, so related geometry calls should be repaired together instead of as isolated lines.';
287
+ case 'detached_fragment_rewrite':
288
+ return 'The proposed or current script fragment depends on missing surrounding declarations, so the repair should preserve and reconnect the broader script context.';
289
+ case 'creator_lifecycle_violation':
290
+ return 'The script uses a creator or project lifecycle in an invalid order, so the repair must fix the broader creation/finalization flow.';
291
+ case 'structural_script_corruption':
292
+ return 'The repair payload would corrupt, truncate, or replace too much of the script, so context-preserving structural repair is required.';
293
+ case 'stale_patch_target':
294
+ return 'The repair patch no longer matches the current script snapshot, so it must be regenerated against the latest revision and exact text.';
295
+ case 'malformed_repair_reply':
296
+ return 'The repair reply format is invalid or ambiguous, so the next repair turn should reissue a clean patch-only response.';
297
+ case 'unknown_api_reference':
298
+ return 'The script references an unknown BIM namespace or method and should be corrected to match the supported API surface.';
299
+ case 'metadata_access_pattern':
300
+ return 'The script is using a brittle metadata access pattern and should switch to dedicated IFC query helpers.';
301
+ default:
302
+ return diagnostic.message;
303
+ }
304
+ }
305
+
306
+ function defaultRootCauseKey(source: ScriptDiagnosticSource, code: string): string {
307
+ switch (code) {
308
+ case 'unknown_namespace':
309
+ case 'unknown_method':
310
+ return 'unknown_api_reference';
311
+ case 'create_contract':
312
+ case 'bare_identifier':
313
+ case 'generic_placement_contract':
314
+ case 'plate_contract_mismatch':
315
+ return 'api_contract_mismatch';
316
+ case 'wall_hosted_opening_pattern':
317
+ case 'world_placement_elevation':
318
+ case 'wall_hosted_opening_alignment':
319
+ return 'placement_context_mismatch';
320
+ case 'detached_snippet_scope':
321
+ return 'detached_fragment_rewrite';
322
+ case 'metadata_query_pattern':
323
+ return 'metadata_access_pattern';
324
+ case 'patch_revision_conflict':
325
+ case 'patch_range_error':
326
+ return 'stale_patch_target';
327
+ case 'patch_semantic_error':
328
+ return source === 'patch' ? 'malformed_repair_reply' : 'api_contract_mismatch';
329
+ case 'unsafe_full_replacement':
330
+ case 'destructive_partial_rewrite':
331
+ return 'structural_script_corruption';
332
+ default:
333
+ return code;
334
+ }
335
+ }
336
+
337
+ function defaultRepairScope(rootCauseKey: string, code: string): RepairScope {
338
+ switch (rootCauseKey) {
339
+ case 'stale_patch_target':
340
+ case 'malformed_repair_reply':
341
+ case 'structural_script_corruption':
342
+ case 'detached_fragment_rewrite':
343
+ case 'creator_lifecycle_violation':
344
+ return 'structural';
345
+ case 'placement_context_mismatch':
346
+ return 'block';
347
+ case 'api_contract_mismatch':
348
+ case 'unknown_api_reference':
349
+ case 'metadata_access_pattern':
350
+ return code === 'bare_identifier' ? 'block' : 'local';
351
+ default:
352
+ return 'local';
353
+ }
354
+ }
355
+
356
+ function widenRepairScope(left: RepairScope, right: RepairScope): RepairScope {
357
+ return compareRepairScope(left, right) >= 0 ? left : right;
358
+ }
359
+
360
+ function compareRepairScope(left: RepairScope, right: RepairScope): number {
361
+ return repairScopeRank(left) - repairScopeRank(right);
362
+ }
363
+
364
+ function repairScopeRank(scope: RepairScope): number {
365
+ switch (scope) {
366
+ case 'local':
367
+ return 0;
368
+ case 'block':
369
+ return 1;
370
+ case 'structural':
371
+ return 2;
372
+ case 'full_rewrite':
373
+ return 3;
374
+ }
375
+ }
376
+
377
+ function dedupeEvidence(evidence: ScriptDiagnosticEvidence[]): ScriptDiagnosticEvidence[] {
378
+ const seen = new Set<string>();
379
+ const result: ScriptDiagnosticEvidence[] = [];
380
+ for (const item of evidence) {
381
+ const key = [
382
+ item.source ?? '',
383
+ item.code ?? '',
384
+ item.methodName ?? '',
385
+ item.symbol ?? '',
386
+ item.failureKind ?? '',
387
+ item.range?.from ?? '',
388
+ item.range?.to ?? '',
389
+ item.snippet ?? '',
390
+ ].join('|');
391
+ if (seen.has(key)) continue;
392
+ seen.add(key);
393
+ result.push(item);
394
+ }
395
+ return result;
396
+ }
397
+
398
+ function asRange(value: unknown): ScriptDiagnosticRange | null {
399
+ if (!value || typeof value !== 'object') return null;
400
+ const from = asNumber((value as Record<string, unknown>).from);
401
+ const to = asNumber((value as Record<string, unknown>).to);
402
+ if (from === null || to === null) return null;
403
+ return { from, to };
404
+ }
405
+
406
+ function asEvidence(value: unknown): ScriptDiagnosticEvidence | null {
407
+ if (!value || typeof value !== 'object') return null;
408
+ const record = value as Record<string, unknown>;
409
+ return {
410
+ source: asDiagnosticSource(record.source) ?? undefined,
411
+ code: asString(record.code) ?? undefined,
412
+ message: asString(record.message) ?? undefined,
413
+ methodName: asString(record.methodName) ?? undefined,
414
+ symbol: asString(record.symbol) ?? undefined,
415
+ failureKind: asString(record.failureKind) ?? undefined,
416
+ range: asRange(record.range) ?? undefined,
417
+ line: asNumber(record.line) ?? undefined,
418
+ column: asNumber(record.column) ?? undefined,
419
+ snippet: asString(record.snippet) ?? undefined,
420
+ };
421
+ }
422
+
423
+ function asDiagnosticSource(value: unknown): ScriptDiagnosticSource | null {
424
+ return value === 'preflight' || value === 'runtime' || value === 'patch'
425
+ ? value
426
+ : null;
427
+ }
428
+
429
+ function asRepairScope(value: unknown): RepairScope | null {
430
+ return value === 'local' || value === 'block' || value === 'structural' || value === 'full_rewrite'
431
+ ? value
432
+ : null;
433
+ }
434
+
435
+ function asString(value: unknown): string | null {
436
+ return typeof value === 'string' && value.length > 0 ? value : null;
437
+ }
438
+
439
+ function asNumber(value: unknown): number | null {
440
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
441
+ }
442
+
443
+ function truncateSnippet(value: string): string {
444
+ return value.length > 80 ? `${value.slice(0, 77)}...` : value;
445
+ }