@valbuild/cli 0.97.0 → 0.97.2

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.
@@ -126,8 +126,6 @@ const textEncoder = new TextEncoder();
126
126
 
127
127
  // Types for handler system
128
128
 
129
- // Cache types for avoiding redundant service.get() calls
130
-
131
129
  // Handler functions
132
130
  async function handleFileMetadata(ctx) {
133
131
  var _fileSource$source;
@@ -160,40 +158,6 @@ async function handleFileMetadata(ctx) {
160
158
  shouldApplyPatch: true
161
159
  };
162
160
  }
163
- async function handleKeyOfCheck(ctx) {
164
- if (!ctx.validationError.value || typeof ctx.validationError.value !== "object" || !("key" in ctx.validationError.value) || !("sourcePath" in ctx.validationError.value)) {
165
- return {
166
- success: false,
167
- errorMessage: `Unexpected error in ${ctx.sourcePath}: ${ctx.validationError.message} (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)`
168
- };
169
- }
170
- const {
171
- key,
172
- sourcePath
173
- } = ctx.validationError.value;
174
- if (typeof key !== "string") {
175
- return {
176
- success: false,
177
- errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'key' to be a string - this is likely a bug in Val)`
178
- };
179
- }
180
- if (typeof sourcePath !== "string") {
181
- return {
182
- success: false,
183
- errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)`
184
- };
185
- }
186
- const res = await checkKeyIsValid(key, sourcePath, ctx.service, ctx.keyOfCache);
187
- if (res.error) {
188
- return {
189
- success: false,
190
- errorMessage: res.message
191
- };
192
- }
193
- return {
194
- success: true
195
- };
196
- }
197
161
  async function handleRemoteFileUpload(ctx) {
198
162
  var _resolvedRemoteFileAt;
199
163
  if (!ctx.fix) {
@@ -402,155 +366,6 @@ async function handleRemoteFileCheck() {
402
366
  shouldApplyPatch: true
403
367
  };
404
368
  }
405
-
406
- // Helper function
407
- async function checkKeyIsValid(key, sourcePath, service, cache) {
408
- const [moduleFilePath, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
409
- const cacheKey = `${moduleFilePath}::${modulePath}`;
410
- let keyOfModuleSource;
411
- let keyOfModuleSchema;
412
- const cached = cache.get(cacheKey);
413
- if (cached) {
414
- keyOfModuleSource = cached.source;
415
- keyOfModuleSchema = cached.schema;
416
- } else {
417
- const keyOfModule = await service.get(moduleFilePath, modulePath, {
418
- source: true,
419
- schema: true,
420
- validate: false
421
- });
422
- keyOfModuleSource = keyOfModule.source;
423
- keyOfModuleSchema = keyOfModule.schema;
424
- cache.set(cacheKey, {
425
- source: keyOfModuleSource,
426
- schema: keyOfModuleSchema
427
- });
428
- }
429
- if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
430
- return {
431
- error: true,
432
- message: `Expected key at ${sourcePath} to be of type 'record'`
433
- };
434
- }
435
- if (keyOfModuleSource && typeof keyOfModuleSource === "object" && key in keyOfModuleSource) {
436
- return {
437
- error: false
438
- };
439
- }
440
- if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
441
- return {
442
- error: true,
443
- message: `Expected ${sourcePath} to be a truthy object`
444
- };
445
- }
446
- const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
447
- return {
448
- error: true,
449
- message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}`
450
- };
451
- }
452
-
453
- /**
454
- * Check if a route is valid by scanning all router modules
455
- * and validating against include/exclude patterns
456
- */
457
- async function checkRouteIsValid(route, include, exclude, service, valFiles, cache) {
458
- // 1. Scan all val files to find modules with routers (use cache if available)
459
- if (!cache.loaded) {
460
- for (const file of valFiles) {
461
- var _valModule$schema;
462
- const moduleFilePath = `/${file}`;
463
- const valModule = await service.get(moduleFilePath, "", {
464
- source: true,
465
- schema: true,
466
- validate: false
467
- });
468
-
469
- // Check if this module has a router defined
470
- if (((_valModule$schema = valModule.schema) === null || _valModule$schema === void 0 ? void 0 : _valModule$schema.type) === "record" && valModule.schema.router) {
471
- if (valModule.source && typeof valModule.source === "object") {
472
- cache.modules[moduleFilePath] = valModule.source;
473
- }
474
- }
475
- }
476
- cache.loaded = true;
477
- }
478
- const routerModules = cache.modules;
479
-
480
- // 2. Check if route exists in any router module
481
- let foundInModule = null;
482
- for (const [moduleFilePath, source] of Object.entries(routerModules)) {
483
- if (route in source) {
484
- foundInModule = moduleFilePath;
485
- break;
486
- }
487
- }
488
- if (!foundInModule) {
489
- // Route not found in any router module
490
- let allRoutes = Object.values(routerModules).flatMap(source => Object.keys(source));
491
- if (allRoutes.length === 0) {
492
- return {
493
- error: true,
494
- message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`
495
- };
496
- }
497
-
498
- // Filter routes by include/exclude patterns for suggestions
499
- allRoutes = internal.filterRoutesByPatterns(allRoutes, include, exclude);
500
- const alternatives = findSimilar(route, allRoutes);
501
- return {
502
- error: true,
503
- message: `Route '${route}' does not exist in any router module. ${alternatives.length > 0 ? `Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}` : "No similar routes found."}`
504
- };
505
- }
506
-
507
- // 3. Validate against include/exclude patterns
508
- const patternValidation = internal.validateRoutePatterns(route, include, exclude);
509
- if (!patternValidation.valid) {
510
- return {
511
- error: true,
512
- message: patternValidation.message
513
- };
514
- }
515
- return {
516
- error: false
517
- };
518
- }
519
-
520
- /**
521
- * Handler for router:check-route validation fix
522
- */
523
- async function handleRouteCheck(ctx) {
524
- const {
525
- sourcePath,
526
- validationError,
527
- service,
528
- valFiles,
529
- routerModulesCache
530
- } = ctx;
531
-
532
- // Extract route and patterns from validation error value
533
- const value = validationError.value;
534
- if (!value || typeof value.route !== "string") {
535
- return {
536
- success: false,
537
- errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`
538
- };
539
- }
540
- const route = value.route;
541
-
542
- // Check if the route is valid
543
- const result = await checkRouteIsValid(route, value.include, value.exclude, service, valFiles, routerModulesCache);
544
- if (result.error) {
545
- return {
546
- success: false,
547
- errorMessage: `${sourcePath}: ${result.message}`
548
- };
549
- }
550
- return {
551
- success: true
552
- };
553
- }
554
369
  async function handleUniqueFolderCheck(ctx) {
555
370
  const value = ctx.validationError.value;
556
371
  if (!value || typeof value.directory !== "string") {
@@ -649,14 +464,14 @@ async function handleCheckAllFiles(ctx) {
649
464
  };
650
465
  }
651
466
 
652
- // Fix handler registry
467
+ // Fix handler registry. `keyof:check-keys` and `router:check-route` are
468
+ // resolved upfront by the shared resolveSchemaSourceFixes — they never reach
469
+ // this registry, so they're excluded from the key set.
653
470
  const currentFixHandlers = {
654
471
  "image:check-metadata": handleFileMetadata,
655
472
  "image:add-metadata": handleFileMetadata,
656
473
  "file:check-metadata": handleFileMetadata,
657
474
  "file:add-metadata": handleFileMetadata,
658
- "keyof:check-keys": handleKeyOfCheck,
659
- "router:check-route": handleRouteCheck,
660
475
  "image:upload-remote": handleRemoteFileUpload,
661
476
  "file:upload-remote": handleRemoteFileUpload,
662
477
  "image:download-remote": handleRemoteFileDownload,
@@ -708,12 +523,27 @@ async function* runValidation({
708
523
  const service = await server.createService(projectRoot, {}, fs);
709
524
  let errors = 0;
710
525
 
711
- // Create caches that persist across all file validations
712
- const keyOfCache = new Map();
713
- const routerModulesCache = {
714
- loaded: false,
715
- modules: {}
526
+ // Build a single schema/source snapshot up front so the shared resolver
527
+ // can resolve keyof:check-keys / router:check-route references that span
528
+ // multiple val files.
529
+ const snapshot = {
530
+ schemas: {},
531
+ sources: {}
716
532
  };
533
+ for (const file of valFiles) {
534
+ const moduleFilePath = `/${file}`;
535
+ const valModule = await service.get(moduleFilePath, "", {
536
+ source: true,
537
+ schema: true,
538
+ validate: false
539
+ });
540
+ if (valModule.schema) {
541
+ snapshot.schemas[moduleFilePath] = valModule.schema;
542
+ }
543
+ if (valModule.source !== undefined) {
544
+ snapshot.sources[moduleFilePath] = valModule.source;
545
+ }
546
+ }
717
547
  async function* validateFile(file) {
718
548
  const moduleFilePath = `/${file}`; // TODO: check if this always works? (Windows?)
719
549
  const start = Date.now();
@@ -737,7 +567,12 @@ async function* runValidation({
737
567
  let fixedErrors = 0;
738
568
  if (valModule.errors) {
739
569
  if (valModule.errors.validation) {
740
- for (const [sourcePath, validationErrors] of Object.entries(valModule.errors.validation)) {
570
+ // Resolve schema/source fixes (keyof:check-keys, router:check-route)
571
+ // against the snapshot before per-error dispatch. Resolved errors
572
+ // are dropped; invalid references come back with rewritten messages
573
+ // and fixes cleared, so they fall through the "no fixes" branch.
574
+ const resolvedValidationErrors = internal.resolveSchemaSourceFixes(valModule.errors.validation, snapshot);
575
+ for (const [sourcePath, validationErrors] of Object.entries(resolvedValidationErrors)) {
741
576
  for (const v of validationErrors) {
742
577
  if (!v.fixes || v.fixes.length === 0) {
743
578
  // No fixes available - just report error
@@ -780,9 +615,7 @@ async function* runValidation({
780
615
  remoteFileBuckets,
781
616
  remoteFilesCounter,
782
617
  remote,
783
- project,
784
- keyOfCache,
785
- routerModulesCache
618
+ project
786
619
  });
787
620
 
788
621
  // Yield any events from handler
@@ -894,31 +727,6 @@ async function* runValidation({
894
727
  }
895
728
  }
896
729
 
897
- // GPT generated levenshtein distance algorithm:
898
- const levenshtein = (a, b) => {
899
- const [m, n] = [a.length, b.length];
900
- if (!m || !n) return Math.max(m, n);
901
- const dp = Array.from({
902
- length: m + 1
903
- }, (_, i) => i);
904
- for (let j = 1; j <= n; j++) {
905
- let prev = dp[0];
906
- dp[0] = j;
907
- for (let i = 1; i <= m; i++) {
908
- const temp = dp[i];
909
- dp[i] = a[i - 1] === b[j - 1] ? prev : Math.min(prev + 1, dp[i - 1] + 1, dp[i] + 1);
910
- prev = temp;
911
- }
912
- }
913
- return dp[m];
914
- };
915
- function findSimilar(key, targets) {
916
- return targets.map(target => ({
917
- target,
918
- distance: levenshtein(key, target)
919
- })).sort((a, b) => a.distance - b.distance);
920
- }
921
-
922
730
  async function validate({
923
731
  root,
924
732
  fix
@@ -126,8 +126,6 @@ const textEncoder = new TextEncoder();
126
126
 
127
127
  // Types for handler system
128
128
 
129
- // Cache types for avoiding redundant service.get() calls
130
-
131
129
  // Handler functions
132
130
  async function handleFileMetadata(ctx) {
133
131
  var _fileSource$source;
@@ -160,40 +158,6 @@ async function handleFileMetadata(ctx) {
160
158
  shouldApplyPatch: true
161
159
  };
162
160
  }
163
- async function handleKeyOfCheck(ctx) {
164
- if (!ctx.validationError.value || typeof ctx.validationError.value !== "object" || !("key" in ctx.validationError.value) || !("sourcePath" in ctx.validationError.value)) {
165
- return {
166
- success: false,
167
- errorMessage: `Unexpected error in ${ctx.sourcePath}: ${ctx.validationError.message} (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)`
168
- };
169
- }
170
- const {
171
- key,
172
- sourcePath
173
- } = ctx.validationError.value;
174
- if (typeof key !== "string") {
175
- return {
176
- success: false,
177
- errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'key' to be a string - this is likely a bug in Val)`
178
- };
179
- }
180
- if (typeof sourcePath !== "string") {
181
- return {
182
- success: false,
183
- errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)`
184
- };
185
- }
186
- const res = await checkKeyIsValid(key, sourcePath, ctx.service, ctx.keyOfCache);
187
- if (res.error) {
188
- return {
189
- success: false,
190
- errorMessage: res.message
191
- };
192
- }
193
- return {
194
- success: true
195
- };
196
- }
197
161
  async function handleRemoteFileUpload(ctx) {
198
162
  var _resolvedRemoteFileAt;
199
163
  if (!ctx.fix) {
@@ -402,155 +366,6 @@ async function handleRemoteFileCheck() {
402
366
  shouldApplyPatch: true
403
367
  };
404
368
  }
405
-
406
- // Helper function
407
- async function checkKeyIsValid(key, sourcePath, service, cache) {
408
- const [moduleFilePath, modulePath] = core.Internal.splitModuleFilePathAndModulePath(sourcePath);
409
- const cacheKey = `${moduleFilePath}::${modulePath}`;
410
- let keyOfModuleSource;
411
- let keyOfModuleSchema;
412
- const cached = cache.get(cacheKey);
413
- if (cached) {
414
- keyOfModuleSource = cached.source;
415
- keyOfModuleSchema = cached.schema;
416
- } else {
417
- const keyOfModule = await service.get(moduleFilePath, modulePath, {
418
- source: true,
419
- schema: true,
420
- validate: false
421
- });
422
- keyOfModuleSource = keyOfModule.source;
423
- keyOfModuleSchema = keyOfModule.schema;
424
- cache.set(cacheKey, {
425
- source: keyOfModuleSource,
426
- schema: keyOfModuleSchema
427
- });
428
- }
429
- if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
430
- return {
431
- error: true,
432
- message: `Expected key at ${sourcePath} to be of type 'record'`
433
- };
434
- }
435
- if (keyOfModuleSource && typeof keyOfModuleSource === "object" && key in keyOfModuleSource) {
436
- return {
437
- error: false
438
- };
439
- }
440
- if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
441
- return {
442
- error: true,
443
- message: `Expected ${sourcePath} to be a truthy object`
444
- };
445
- }
446
- const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
447
- return {
448
- error: true,
449
- message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}`
450
- };
451
- }
452
-
453
- /**
454
- * Check if a route is valid by scanning all router modules
455
- * and validating against include/exclude patterns
456
- */
457
- async function checkRouteIsValid(route, include, exclude, service, valFiles, cache) {
458
- // 1. Scan all val files to find modules with routers (use cache if available)
459
- if (!cache.loaded) {
460
- for (const file of valFiles) {
461
- var _valModule$schema;
462
- const moduleFilePath = `/${file}`;
463
- const valModule = await service.get(moduleFilePath, "", {
464
- source: true,
465
- schema: true,
466
- validate: false
467
- });
468
-
469
- // Check if this module has a router defined
470
- if (((_valModule$schema = valModule.schema) === null || _valModule$schema === void 0 ? void 0 : _valModule$schema.type) === "record" && valModule.schema.router) {
471
- if (valModule.source && typeof valModule.source === "object") {
472
- cache.modules[moduleFilePath] = valModule.source;
473
- }
474
- }
475
- }
476
- cache.loaded = true;
477
- }
478
- const routerModules = cache.modules;
479
-
480
- // 2. Check if route exists in any router module
481
- let foundInModule = null;
482
- for (const [moduleFilePath, source] of Object.entries(routerModules)) {
483
- if (route in source) {
484
- foundInModule = moduleFilePath;
485
- break;
486
- }
487
- }
488
- if (!foundInModule) {
489
- // Route not found in any router module
490
- let allRoutes = Object.values(routerModules).flatMap(source => Object.keys(source));
491
- if (allRoutes.length === 0) {
492
- return {
493
- error: true,
494
- message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`
495
- };
496
- }
497
-
498
- // Filter routes by include/exclude patterns for suggestions
499
- allRoutes = internal.filterRoutesByPatterns(allRoutes, include, exclude);
500
- const alternatives = findSimilar(route, allRoutes);
501
- return {
502
- error: true,
503
- message: `Route '${route}' does not exist in any router module. ${alternatives.length > 0 ? `Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}` : "No similar routes found."}`
504
- };
505
- }
506
-
507
- // 3. Validate against include/exclude patterns
508
- const patternValidation = internal.validateRoutePatterns(route, include, exclude);
509
- if (!patternValidation.valid) {
510
- return {
511
- error: true,
512
- message: patternValidation.message
513
- };
514
- }
515
- return {
516
- error: false
517
- };
518
- }
519
-
520
- /**
521
- * Handler for router:check-route validation fix
522
- */
523
- async function handleRouteCheck(ctx) {
524
- const {
525
- sourcePath,
526
- validationError,
527
- service,
528
- valFiles,
529
- routerModulesCache
530
- } = ctx;
531
-
532
- // Extract route and patterns from validation error value
533
- const value = validationError.value;
534
- if (!value || typeof value.route !== "string") {
535
- return {
536
- success: false,
537
- errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`
538
- };
539
- }
540
- const route = value.route;
541
-
542
- // Check if the route is valid
543
- const result = await checkRouteIsValid(route, value.include, value.exclude, service, valFiles, routerModulesCache);
544
- if (result.error) {
545
- return {
546
- success: false,
547
- errorMessage: `${sourcePath}: ${result.message}`
548
- };
549
- }
550
- return {
551
- success: true
552
- };
553
- }
554
369
  async function handleUniqueFolderCheck(ctx) {
555
370
  const value = ctx.validationError.value;
556
371
  if (!value || typeof value.directory !== "string") {
@@ -649,14 +464,14 @@ async function handleCheckAllFiles(ctx) {
649
464
  };
650
465
  }
651
466
 
652
- // Fix handler registry
467
+ // Fix handler registry. `keyof:check-keys` and `router:check-route` are
468
+ // resolved upfront by the shared resolveSchemaSourceFixes — they never reach
469
+ // this registry, so they're excluded from the key set.
653
470
  const currentFixHandlers = {
654
471
  "image:check-metadata": handleFileMetadata,
655
472
  "image:add-metadata": handleFileMetadata,
656
473
  "file:check-metadata": handleFileMetadata,
657
474
  "file:add-metadata": handleFileMetadata,
658
- "keyof:check-keys": handleKeyOfCheck,
659
- "router:check-route": handleRouteCheck,
660
475
  "image:upload-remote": handleRemoteFileUpload,
661
476
  "file:upload-remote": handleRemoteFileUpload,
662
477
  "image:download-remote": handleRemoteFileDownload,
@@ -708,12 +523,27 @@ async function* runValidation({
708
523
  const service = await server.createService(projectRoot, {}, fs);
709
524
  let errors = 0;
710
525
 
711
- // Create caches that persist across all file validations
712
- const keyOfCache = new Map();
713
- const routerModulesCache = {
714
- loaded: false,
715
- modules: {}
526
+ // Build a single schema/source snapshot up front so the shared resolver
527
+ // can resolve keyof:check-keys / router:check-route references that span
528
+ // multiple val files.
529
+ const snapshot = {
530
+ schemas: {},
531
+ sources: {}
716
532
  };
533
+ for (const file of valFiles) {
534
+ const moduleFilePath = `/${file}`;
535
+ const valModule = await service.get(moduleFilePath, "", {
536
+ source: true,
537
+ schema: true,
538
+ validate: false
539
+ });
540
+ if (valModule.schema) {
541
+ snapshot.schemas[moduleFilePath] = valModule.schema;
542
+ }
543
+ if (valModule.source !== undefined) {
544
+ snapshot.sources[moduleFilePath] = valModule.source;
545
+ }
546
+ }
717
547
  async function* validateFile(file) {
718
548
  const moduleFilePath = `/${file}`; // TODO: check if this always works? (Windows?)
719
549
  const start = Date.now();
@@ -737,7 +567,12 @@ async function* runValidation({
737
567
  let fixedErrors = 0;
738
568
  if (valModule.errors) {
739
569
  if (valModule.errors.validation) {
740
- for (const [sourcePath, validationErrors] of Object.entries(valModule.errors.validation)) {
570
+ // Resolve schema/source fixes (keyof:check-keys, router:check-route)
571
+ // against the snapshot before per-error dispatch. Resolved errors
572
+ // are dropped; invalid references come back with rewritten messages
573
+ // and fixes cleared, so they fall through the "no fixes" branch.
574
+ const resolvedValidationErrors = internal.resolveSchemaSourceFixes(valModule.errors.validation, snapshot);
575
+ for (const [sourcePath, validationErrors] of Object.entries(resolvedValidationErrors)) {
741
576
  for (const v of validationErrors) {
742
577
  if (!v.fixes || v.fixes.length === 0) {
743
578
  // No fixes available - just report error
@@ -780,9 +615,7 @@ async function* runValidation({
780
615
  remoteFileBuckets,
781
616
  remoteFilesCounter,
782
617
  remote,
783
- project,
784
- keyOfCache,
785
- routerModulesCache
618
+ project
786
619
  });
787
620
 
788
621
  // Yield any events from handler
@@ -894,31 +727,6 @@ async function* runValidation({
894
727
  }
895
728
  }
896
729
 
897
- // GPT generated levenshtein distance algorithm:
898
- const levenshtein = (a, b) => {
899
- const [m, n] = [a.length, b.length];
900
- if (!m || !n) return Math.max(m, n);
901
- const dp = Array.from({
902
- length: m + 1
903
- }, (_, i) => i);
904
- for (let j = 1; j <= n; j++) {
905
- let prev = dp[0];
906
- dp[0] = j;
907
- for (let i = 1; i <= m; i++) {
908
- const temp = dp[i];
909
- dp[i] = a[i - 1] === b[j - 1] ? prev : Math.min(prev + 1, dp[i - 1] + 1, dp[i] + 1);
910
- prev = temp;
911
- }
912
- }
913
- return dp[m];
914
- };
915
- function findSimilar(key, targets) {
916
- return targets.map(target => ({
917
- target,
918
- distance: levenshtein(key, target)
919
- })).sort((a, b) => a.distance - b.distance);
920
- }
921
-
922
730
  async function validate({
923
731
  root,
924
732
  fix
@@ -10,7 +10,7 @@ import vm from 'node:vm';
10
10
  import ts from 'typescript';
11
11
  import z from 'zod';
12
12
  import { createRequire } from 'node:module';
13
- import { filterRoutesByPatterns, validateRoutePatterns } from '@valbuild/shared/internal';
13
+ import { resolveSchemaSourceFixes } from '@valbuild/shared/internal';
14
14
  import nodeFs from 'fs';
15
15
 
16
16
  function error(message) {
@@ -94,8 +94,6 @@ const textEncoder = new TextEncoder();
94
94
 
95
95
  // Types for handler system
96
96
 
97
- // Cache types for avoiding redundant service.get() calls
98
-
99
97
  // Handler functions
100
98
  async function handleFileMetadata(ctx) {
101
99
  var _fileSource$source;
@@ -128,40 +126,6 @@ async function handleFileMetadata(ctx) {
128
126
  shouldApplyPatch: true
129
127
  };
130
128
  }
131
- async function handleKeyOfCheck(ctx) {
132
- if (!ctx.validationError.value || typeof ctx.validationError.value !== "object" || !("key" in ctx.validationError.value) || !("sourcePath" in ctx.validationError.value)) {
133
- return {
134
- success: false,
135
- errorMessage: `Unexpected error in ${ctx.sourcePath}: ${ctx.validationError.message} (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)`
136
- };
137
- }
138
- const {
139
- key,
140
- sourcePath
141
- } = ctx.validationError.value;
142
- if (typeof key !== "string") {
143
- return {
144
- success: false,
145
- errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'key' to be a string - this is likely a bug in Val)`
146
- };
147
- }
148
- if (typeof sourcePath !== "string") {
149
- return {
150
- success: false,
151
- errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)`
152
- };
153
- }
154
- const res = await checkKeyIsValid(key, sourcePath, ctx.service, ctx.keyOfCache);
155
- if (res.error) {
156
- return {
157
- success: false,
158
- errorMessage: res.message
159
- };
160
- }
161
- return {
162
- success: true
163
- };
164
- }
165
129
  async function handleRemoteFileUpload(ctx) {
166
130
  var _resolvedRemoteFileAt;
167
131
  if (!ctx.fix) {
@@ -370,155 +334,6 @@ async function handleRemoteFileCheck() {
370
334
  shouldApplyPatch: true
371
335
  };
372
336
  }
373
-
374
- // Helper function
375
- async function checkKeyIsValid(key, sourcePath, service, cache) {
376
- const [moduleFilePath, modulePath] = Internal.splitModuleFilePathAndModulePath(sourcePath);
377
- const cacheKey = `${moduleFilePath}::${modulePath}`;
378
- let keyOfModuleSource;
379
- let keyOfModuleSchema;
380
- const cached = cache.get(cacheKey);
381
- if (cached) {
382
- keyOfModuleSource = cached.source;
383
- keyOfModuleSchema = cached.schema;
384
- } else {
385
- const keyOfModule = await service.get(moduleFilePath, modulePath, {
386
- source: true,
387
- schema: true,
388
- validate: false
389
- });
390
- keyOfModuleSource = keyOfModule.source;
391
- keyOfModuleSchema = keyOfModule.schema;
392
- cache.set(cacheKey, {
393
- source: keyOfModuleSource,
394
- schema: keyOfModuleSchema
395
- });
396
- }
397
- if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
398
- return {
399
- error: true,
400
- message: `Expected key at ${sourcePath} to be of type 'record'`
401
- };
402
- }
403
- if (keyOfModuleSource && typeof keyOfModuleSource === "object" && key in keyOfModuleSource) {
404
- return {
405
- error: false
406
- };
407
- }
408
- if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
409
- return {
410
- error: true,
411
- message: `Expected ${sourcePath} to be a truthy object`
412
- };
413
- }
414
- const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
415
- return {
416
- error: true,
417
- message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}`
418
- };
419
- }
420
-
421
- /**
422
- * Check if a route is valid by scanning all router modules
423
- * and validating against include/exclude patterns
424
- */
425
- async function checkRouteIsValid(route, include, exclude, service, valFiles, cache) {
426
- // 1. Scan all val files to find modules with routers (use cache if available)
427
- if (!cache.loaded) {
428
- for (const file of valFiles) {
429
- var _valModule$schema;
430
- const moduleFilePath = `/${file}`;
431
- const valModule = await service.get(moduleFilePath, "", {
432
- source: true,
433
- schema: true,
434
- validate: false
435
- });
436
-
437
- // Check if this module has a router defined
438
- if (((_valModule$schema = valModule.schema) === null || _valModule$schema === void 0 ? void 0 : _valModule$schema.type) === "record" && valModule.schema.router) {
439
- if (valModule.source && typeof valModule.source === "object") {
440
- cache.modules[moduleFilePath] = valModule.source;
441
- }
442
- }
443
- }
444
- cache.loaded = true;
445
- }
446
- const routerModules = cache.modules;
447
-
448
- // 2. Check if route exists in any router module
449
- let foundInModule = null;
450
- for (const [moduleFilePath, source] of Object.entries(routerModules)) {
451
- if (route in source) {
452
- foundInModule = moduleFilePath;
453
- break;
454
- }
455
- }
456
- if (!foundInModule) {
457
- // Route not found in any router module
458
- let allRoutes = Object.values(routerModules).flatMap(source => Object.keys(source));
459
- if (allRoutes.length === 0) {
460
- return {
461
- error: true,
462
- message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`
463
- };
464
- }
465
-
466
- // Filter routes by include/exclude patterns for suggestions
467
- allRoutes = filterRoutesByPatterns(allRoutes, include, exclude);
468
- const alternatives = findSimilar(route, allRoutes);
469
- return {
470
- error: true,
471
- message: `Route '${route}' does not exist in any router module. ${alternatives.length > 0 ? `Closest match: '${alternatives[0].target}'. Other similar: ${alternatives.slice(1, 4).map(a => `'${a.target}'`).join(", ")}${alternatives.length > 4 ? ", ..." : ""}` : "No similar routes found."}`
472
- };
473
- }
474
-
475
- // 3. Validate against include/exclude patterns
476
- const patternValidation = validateRoutePatterns(route, include, exclude);
477
- if (!patternValidation.valid) {
478
- return {
479
- error: true,
480
- message: patternValidation.message
481
- };
482
- }
483
- return {
484
- error: false
485
- };
486
- }
487
-
488
- /**
489
- * Handler for router:check-route validation fix
490
- */
491
- async function handleRouteCheck(ctx) {
492
- const {
493
- sourcePath,
494
- validationError,
495
- service,
496
- valFiles,
497
- routerModulesCache
498
- } = ctx;
499
-
500
- // Extract route and patterns from validation error value
501
- const value = validationError.value;
502
- if (!value || typeof value.route !== "string") {
503
- return {
504
- success: false,
505
- errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`
506
- };
507
- }
508
- const route = value.route;
509
-
510
- // Check if the route is valid
511
- const result = await checkRouteIsValid(route, value.include, value.exclude, service, valFiles, routerModulesCache);
512
- if (result.error) {
513
- return {
514
- success: false,
515
- errorMessage: `${sourcePath}: ${result.message}`
516
- };
517
- }
518
- return {
519
- success: true
520
- };
521
- }
522
337
  async function handleUniqueFolderCheck(ctx) {
523
338
  const value = ctx.validationError.value;
524
339
  if (!value || typeof value.directory !== "string") {
@@ -617,14 +432,14 @@ async function handleCheckAllFiles(ctx) {
617
432
  };
618
433
  }
619
434
 
620
- // Fix handler registry
435
+ // Fix handler registry. `keyof:check-keys` and `router:check-route` are
436
+ // resolved upfront by the shared resolveSchemaSourceFixes — they never reach
437
+ // this registry, so they're excluded from the key set.
621
438
  const currentFixHandlers = {
622
439
  "image:check-metadata": handleFileMetadata,
623
440
  "image:add-metadata": handleFileMetadata,
624
441
  "file:check-metadata": handleFileMetadata,
625
442
  "file:add-metadata": handleFileMetadata,
626
- "keyof:check-keys": handleKeyOfCheck,
627
- "router:check-route": handleRouteCheck,
628
443
  "image:upload-remote": handleRemoteFileUpload,
629
444
  "file:upload-remote": handleRemoteFileUpload,
630
445
  "image:download-remote": handleRemoteFileDownload,
@@ -676,12 +491,27 @@ async function* runValidation({
676
491
  const service = await createService(projectRoot, {}, fs);
677
492
  let errors = 0;
678
493
 
679
- // Create caches that persist across all file validations
680
- const keyOfCache = new Map();
681
- const routerModulesCache = {
682
- loaded: false,
683
- modules: {}
494
+ // Build a single schema/source snapshot up front so the shared resolver
495
+ // can resolve keyof:check-keys / router:check-route references that span
496
+ // multiple val files.
497
+ const snapshot = {
498
+ schemas: {},
499
+ sources: {}
684
500
  };
501
+ for (const file of valFiles) {
502
+ const moduleFilePath = `/${file}`;
503
+ const valModule = await service.get(moduleFilePath, "", {
504
+ source: true,
505
+ schema: true,
506
+ validate: false
507
+ });
508
+ if (valModule.schema) {
509
+ snapshot.schemas[moduleFilePath] = valModule.schema;
510
+ }
511
+ if (valModule.source !== undefined) {
512
+ snapshot.sources[moduleFilePath] = valModule.source;
513
+ }
514
+ }
685
515
  async function* validateFile(file) {
686
516
  const moduleFilePath = `/${file}`; // TODO: check if this always works? (Windows?)
687
517
  const start = Date.now();
@@ -705,7 +535,12 @@ async function* runValidation({
705
535
  let fixedErrors = 0;
706
536
  if (valModule.errors) {
707
537
  if (valModule.errors.validation) {
708
- for (const [sourcePath, validationErrors] of Object.entries(valModule.errors.validation)) {
538
+ // Resolve schema/source fixes (keyof:check-keys, router:check-route)
539
+ // against the snapshot before per-error dispatch. Resolved errors
540
+ // are dropped; invalid references come back with rewritten messages
541
+ // and fixes cleared, so they fall through the "no fixes" branch.
542
+ const resolvedValidationErrors = resolveSchemaSourceFixes(valModule.errors.validation, snapshot);
543
+ for (const [sourcePath, validationErrors] of Object.entries(resolvedValidationErrors)) {
709
544
  for (const v of validationErrors) {
710
545
  if (!v.fixes || v.fixes.length === 0) {
711
546
  // No fixes available - just report error
@@ -748,9 +583,7 @@ async function* runValidation({
748
583
  remoteFileBuckets,
749
584
  remoteFilesCounter,
750
585
  remote,
751
- project,
752
- keyOfCache,
753
- routerModulesCache
586
+ project
754
587
  });
755
588
 
756
589
  // Yield any events from handler
@@ -862,31 +695,6 @@ async function* runValidation({
862
695
  }
863
696
  }
864
697
 
865
- // GPT generated levenshtein distance algorithm:
866
- const levenshtein = (a, b) => {
867
- const [m, n] = [a.length, b.length];
868
- if (!m || !n) return Math.max(m, n);
869
- const dp = Array.from({
870
- length: m + 1
871
- }, (_, i) => i);
872
- for (let j = 1; j <= n; j++) {
873
- let prev = dp[0];
874
- dp[0] = j;
875
- for (let i = 1; i <= m; i++) {
876
- const temp = dp[i];
877
- dp[i] = a[i - 1] === b[j - 1] ? prev : Math.min(prev + 1, dp[i - 1] + 1, dp[i] + 1);
878
- prev = temp;
879
- }
880
- }
881
- return dp[m];
882
- };
883
- function findSimilar(key, targets) {
884
- return targets.map(target => ({
885
- target,
886
- distance: levenshtein(key, target)
887
- })).sort((a, b) => a.distance - b.distance);
888
- }
889
-
890
698
  async function validate({
891
699
  root,
892
700
  fix
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@valbuild/cli",
3
3
  "private": false,
4
- "version": "0.97.0",
4
+ "version": "0.97.2",
5
5
  "description": "Val CLI tools",
6
6
  "repository": {
7
7
  "type": "git",
@@ -25,10 +25,10 @@
25
25
  "open": "^9.1.0",
26
26
  "picocolors": "^1.1.1",
27
27
  "zod": "^4.3.5",
28
- "@valbuild/core": "0.97.0",
28
+ "@valbuild/core": "0.97.1",
29
29
  "@valbuild/eslint-plugin": "0.93.0",
30
- "@valbuild/server": "0.97.0",
31
- "@valbuild/shared": "0.97.0"
30
+ "@valbuild/server": "0.97.2",
31
+ "@valbuild/shared": "0.97.1"
32
32
  },
33
33
  "peerDependencies": {
34
34
  "prettier": "*",
@@ -10,6 +10,7 @@ import {
10
10
  import {
11
11
  FILE_REF_PROP,
12
12
  Internal,
13
+ type Json,
13
14
  ModuleFilePath,
14
15
  ModulePath,
15
16
  SerializedFileSchema,
@@ -18,9 +19,8 @@ import {
18
19
  ValidationFix,
19
20
  } from "@valbuild/core";
20
21
  import {
21
- filterRoutesByPatterns,
22
- validateRoutePatterns,
23
- type SerializedRegExpPattern,
22
+ resolveSchemaSourceFixes,
23
+ type SchemaSourceSnapshot,
24
24
  } from "@valbuild/shared/internal";
25
25
  import { getFileExt } from "./utils/getFileExt";
26
26
  import ts from "typescript";
@@ -64,16 +64,6 @@ export type ValidationError = {
64
64
  fixes?: ValidationFix[];
65
65
  };
66
66
 
67
- // Cache types for avoiding redundant service.get() calls
68
- export type KeyOfCache = Map<
69
- string, // moduleFilePath + modulePath key
70
- { source: unknown; schema: { type: string } | undefined }
71
- >;
72
- export type RouterModulesCache = {
73
- loaded: boolean;
74
- modules: Record<string, Record<string, unknown>>;
75
- };
76
-
77
67
  export type FixHandlerContext = {
78
68
  sourcePath: SourcePath;
79
69
  validationError: ValidationError;
@@ -95,9 +85,6 @@ export type FixHandlerContext = {
95
85
  remoteFilesCounter: number;
96
86
  remote: IValRemote;
97
87
  project: string | undefined;
98
- // Caches for validation
99
- keyOfCache: KeyOfCache;
100
- routerModulesCache: RouterModulesCache;
101
88
  };
102
89
 
103
90
  export type FixHandlerResult = {
@@ -180,56 +167,6 @@ export async function handleFileMetadata(
180
167
  return { success: true, shouldApplyPatch: true };
181
168
  }
182
169
 
183
- export async function handleKeyOfCheck(
184
- ctx: FixHandlerContext,
185
- ): Promise<FixHandlerResult> {
186
- if (
187
- !ctx.validationError.value ||
188
- typeof ctx.validationError.value !== "object" ||
189
- !("key" in ctx.validationError.value) ||
190
- !("sourcePath" in ctx.validationError.value)
191
- ) {
192
- return {
193
- success: false,
194
- errorMessage: `Unexpected error in ${ctx.sourcePath}: ${ctx.validationError.message} (Expected value to be an object with 'key' and 'sourcePath' properties - this is likely a bug in Val)`,
195
- };
196
- }
197
-
198
- const { key, sourcePath } = ctx.validationError.value as {
199
- key: unknown;
200
- sourcePath: unknown;
201
- };
202
-
203
- if (typeof key !== "string") {
204
- return {
205
- success: false,
206
- errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'key' to be a string - this is likely a bug in Val)`,
207
- };
208
- }
209
-
210
- if (typeof sourcePath !== "string") {
211
- return {
212
- success: false,
213
- errorMessage: `Unexpected error in ${sourcePath}: ${ctx.validationError.message} (Expected value property 'sourcePath' to be a string - this is likely a bug in Val)`,
214
- };
215
- }
216
-
217
- const res = await checkKeyIsValid(
218
- key,
219
- sourcePath,
220
- ctx.service,
221
- ctx.keyOfCache,
222
- );
223
- if (res.error) {
224
- return {
225
- success: false,
226
- errorMessage: res.message,
227
- };
228
- }
229
-
230
- return { success: true };
231
- }
232
-
233
170
  export async function handleRemoteFileUpload(
234
171
  ctx: FixHandlerContext,
235
172
  ): Promise<FixHandlerResult> {
@@ -486,203 +423,6 @@ export async function handleRemoteFileCheck(): Promise<FixHandlerResult> {
486
423
  return { success: true, shouldApplyPatch: true };
487
424
  }
488
425
 
489
- // Helper function
490
- export async function checkKeyIsValid(
491
- key: string,
492
- sourcePath: string,
493
- service: Service,
494
- cache: KeyOfCache,
495
- ): Promise<{ error: false } | { error: true; message: string }> {
496
- const [moduleFilePath, modulePath] =
497
- Internal.splitModuleFilePathAndModulePath(sourcePath as SourcePath);
498
-
499
- const cacheKey = `${moduleFilePath}::${modulePath}`;
500
- let keyOfModuleSource: unknown;
501
- let keyOfModuleSchema: { type: string } | undefined;
502
-
503
- const cached = cache.get(cacheKey);
504
- if (cached) {
505
- keyOfModuleSource = cached.source;
506
- keyOfModuleSchema = cached.schema;
507
- } else {
508
- const keyOfModule = await service.get(moduleFilePath, modulePath, {
509
- source: true,
510
- schema: true,
511
- validate: false,
512
- });
513
- keyOfModuleSource = keyOfModule.source;
514
- keyOfModuleSchema = keyOfModule.schema as { type: string } | undefined;
515
- cache.set(cacheKey, {
516
- source: keyOfModuleSource,
517
- schema: keyOfModuleSchema,
518
- });
519
- }
520
-
521
- if (keyOfModuleSchema && keyOfModuleSchema.type !== "record") {
522
- return {
523
- error: true,
524
- message: `Expected key at ${sourcePath} to be of type 'record'`,
525
- };
526
- }
527
- if (
528
- keyOfModuleSource &&
529
- typeof keyOfModuleSource === "object" &&
530
- key in keyOfModuleSource
531
- ) {
532
- return { error: false };
533
- }
534
- if (!keyOfModuleSource || typeof keyOfModuleSource !== "object") {
535
- return {
536
- error: true,
537
- message: `Expected ${sourcePath} to be a truthy object`,
538
- };
539
- }
540
- const alternatives = findSimilar(key, Object.keys(keyOfModuleSource));
541
- return {
542
- error: true,
543
- message: `Key '${key}' does not exist in ${sourcePath}. Closest match: '${alternatives[0].target}'. Other similar: ${alternatives
544
- .slice(1, 4)
545
- .map((a) => `'${a.target}'`)
546
- .join(", ")}${alternatives.length > 4 ? ", ..." : ""}`,
547
- };
548
- }
549
-
550
- /**
551
- * Check if a route is valid by scanning all router modules
552
- * and validating against include/exclude patterns
553
- */
554
- export async function checkRouteIsValid(
555
- route: string,
556
- include: SerializedRegExpPattern | undefined,
557
- exclude: SerializedRegExpPattern | undefined,
558
- service: Service,
559
- valFiles: string[],
560
- cache: RouterModulesCache,
561
- ): Promise<{ error: false } | { error: true; message: string }> {
562
- // 1. Scan all val files to find modules with routers (use cache if available)
563
- if (!cache.loaded) {
564
- for (const file of valFiles) {
565
- const moduleFilePath = `/${file}` as ModuleFilePath;
566
- const valModule = await service.get(moduleFilePath, "" as ModulePath, {
567
- source: true,
568
- schema: true,
569
- validate: false,
570
- });
571
-
572
- // Check if this module has a router defined
573
- if (valModule.schema?.type === "record" && valModule.schema.router) {
574
- if (valModule.source && typeof valModule.source === "object") {
575
- cache.modules[moduleFilePath] = valModule.source as Record<
576
- string,
577
- unknown
578
- >;
579
- }
580
- }
581
- }
582
- cache.loaded = true;
583
- }
584
-
585
- const routerModules = cache.modules;
586
-
587
- // 2. Check if route exists in any router module
588
- let foundInModule: string | null = null;
589
- for (const [moduleFilePath, source] of Object.entries(routerModules)) {
590
- if (route in source) {
591
- foundInModule = moduleFilePath;
592
- break;
593
- }
594
- }
595
-
596
- if (!foundInModule) {
597
- // Route not found in any router module
598
- let allRoutes = Object.values(routerModules).flatMap((source) =>
599
- Object.keys(source),
600
- );
601
-
602
- if (allRoutes.length === 0) {
603
- return {
604
- error: true,
605
- message: `Route '${route}' could not be validated: No router modules found in the project. Use s.record(...).router(...) to define router modules.`,
606
- };
607
- }
608
-
609
- // Filter routes by include/exclude patterns for suggestions
610
- allRoutes = filterRoutesByPatterns(allRoutes, include, exclude);
611
-
612
- const alternatives = findSimilar(route, allRoutes);
613
-
614
- return {
615
- error: true,
616
- message: `Route '${route}' does not exist in any router module. ${
617
- alternatives.length > 0
618
- ? `Closest match: '${alternatives[0].target}'. Other similar: ${alternatives
619
- .slice(1, 4)
620
- .map((a) => `'${a.target}'`)
621
- .join(", ")}${alternatives.length > 4 ? ", ..." : ""}`
622
- : "No similar routes found."
623
- }`,
624
- };
625
- }
626
-
627
- // 3. Validate against include/exclude patterns
628
- const patternValidation = validateRoutePatterns(route, include, exclude);
629
- if (!patternValidation.valid) {
630
- return {
631
- error: true,
632
- message: patternValidation.message,
633
- };
634
- }
635
-
636
- return { error: false };
637
- }
638
-
639
- /**
640
- * Handler for router:check-route validation fix
641
- */
642
- export async function handleRouteCheck(
643
- ctx: FixHandlerContext,
644
- ): Promise<FixHandlerResult> {
645
- const { sourcePath, validationError, service, valFiles, routerModulesCache } =
646
- ctx;
647
-
648
- // Extract route and patterns from validation error value
649
- const value = validationError.value as
650
- | {
651
- route: unknown;
652
- include?: { source: string; flags: string };
653
- exclude?: { source: string; flags: string };
654
- }
655
- | undefined;
656
-
657
- if (!value || typeof value.route !== "string") {
658
- return {
659
- success: false,
660
- errorMessage: `Invalid route value in validation error: ${JSON.stringify(value)}`,
661
- };
662
- }
663
-
664
- const route = value.route;
665
-
666
- // Check if the route is valid
667
- const result = await checkRouteIsValid(
668
- route,
669
- value.include,
670
- value.exclude,
671
- service,
672
- valFiles,
673
- routerModulesCache,
674
- );
675
-
676
- if (result.error) {
677
- return {
678
- success: false,
679
- errorMessage: `${sourcePath}: ${result.message}`,
680
- };
681
- }
682
-
683
- return { success: true };
684
- }
685
-
686
426
  export async function handleUniqueFolderCheck(
687
427
  ctx: FixHandlerContext,
688
428
  ): Promise<FixHandlerResult> {
@@ -791,14 +531,17 @@ export async function handleCheckAllFiles(
791
531
  return { success: true, shouldApplyPatch: true };
792
532
  }
793
533
 
794
- // Fix handler registry
795
- export const currentFixHandlers: Record<ValidationFix, FixHandler> = {
534
+ // Fix handler registry. `keyof:check-keys` and `router:check-route` are
535
+ // resolved upfront by the shared resolveSchemaSourceFixes — they never reach
536
+ // this registry, so they're excluded from the key set.
537
+ export const currentFixHandlers: Record<
538
+ Exclude<ValidationFix, "keyof:check-keys" | "router:check-route">,
539
+ FixHandler
540
+ > = {
796
541
  "image:check-metadata": handleFileMetadata,
797
542
  "image:add-metadata": handleFileMetadata,
798
543
  "file:check-metadata": handleFileMetadata,
799
544
  "file:add-metadata": handleFileMetadata,
800
- "keyof:check-keys": handleKeyOfCheck,
801
- "router:check-route": handleRouteCheck,
802
545
  "image:upload-remote": handleRemoteFileUpload,
803
546
  "file:upload-remote": handleRemoteFileUpload,
804
547
  "image:download-remote": handleRemoteFileDownload,
@@ -863,12 +606,24 @@ export async function* runValidation({
863
606
 
864
607
  let errors = 0;
865
608
 
866
- // Create caches that persist across all file validations
867
- const keyOfCache: KeyOfCache = new Map();
868
- const routerModulesCache: RouterModulesCache = {
869
- loaded: false,
870
- modules: {},
871
- };
609
+ // Build a single schema/source snapshot up front so the shared resolver
610
+ // can resolve keyof:check-keys / router:check-route references that span
611
+ // multiple val files.
612
+ const snapshot: SchemaSourceSnapshot = { schemas: {}, sources: {} };
613
+ for (const file of valFiles) {
614
+ const moduleFilePath = `/${file}` as ModuleFilePath;
615
+ const valModule = await service.get(moduleFilePath, "" as ModulePath, {
616
+ source: true,
617
+ schema: true,
618
+ validate: false,
619
+ });
620
+ if (valModule.schema) {
621
+ snapshot.schemas[moduleFilePath] = valModule.schema;
622
+ }
623
+ if (valModule.source !== undefined) {
624
+ snapshot.sources[moduleFilePath] = valModule.source as Json;
625
+ }
626
+ }
872
627
 
873
628
  async function* validateFile(file: string): AsyncGenerator<ValidationEvent> {
874
629
  const moduleFilePath = `/${file}` as ModuleFilePath; // TODO: check if this always works? (Windows?)
@@ -896,8 +651,16 @@ export async function* runValidation({
896
651
  let fixedErrors = 0;
897
652
  if (valModule.errors) {
898
653
  if (valModule.errors.validation) {
899
- for (const [sourcePath, validationErrors] of Object.entries(
654
+ // Resolve schema/source fixes (keyof:check-keys, router:check-route)
655
+ // against the snapshot before per-error dispatch. Resolved errors
656
+ // are dropped; invalid references come back with rewritten messages
657
+ // and fixes cleared, so they fall through the "no fixes" branch.
658
+ const resolvedValidationErrors = resolveSchemaSourceFixes(
900
659
  valModule.errors.validation,
660
+ snapshot,
661
+ );
662
+ for (const [sourcePath, validationErrors] of Object.entries(
663
+ resolvedValidationErrors,
901
664
  )) {
902
665
  for (const v of validationErrors) {
903
666
  if (!v.fixes || v.fixes.length === 0) {
@@ -943,8 +706,6 @@ export async function* runValidation({
943
706
  remoteFilesCounter,
944
707
  remote,
945
708
  project,
946
- keyOfCache,
947
- routerModulesCache,
948
709
  });
949
710
 
950
711
  // Yield any events from handler
@@ -1064,33 +825,3 @@ export async function* runValidation({
1064
825
  yield { type: "summary-success" };
1065
826
  }
1066
827
  }
1067
-
1068
- // GPT generated levenshtein distance algorithm:
1069
- export const levenshtein = (a: string, b: string): number => {
1070
- const [m, n] = [a.length, b.length];
1071
- if (!m || !n) return Math.max(m, n);
1072
-
1073
- const dp = Array.from({ length: m + 1 }, (_, i) => i);
1074
-
1075
- for (let j = 1; j <= n; j++) {
1076
- let prev = dp[0];
1077
- dp[0] = j;
1078
-
1079
- for (let i = 1; i <= m; i++) {
1080
- const temp = dp[i];
1081
- dp[i] =
1082
- a[i - 1] === b[j - 1]
1083
- ? prev
1084
- : Math.min(prev + 1, dp[i - 1] + 1, dp[i] + 1);
1085
- prev = temp;
1086
- }
1087
- }
1088
-
1089
- return dp[m];
1090
- };
1091
-
1092
- export function findSimilar(key: string, targets: string[]) {
1093
- return targets
1094
- .map((target) => ({ target, distance: levenshtein(key, target) }))
1095
- .sort((a, b) => a.distance - b.distance);
1096
- }