-
Notifications
You must be signed in to change notification settings - Fork 373
Add schema path heuristic to suggest correct location for misplaced frontmatter fields #18320
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -15,12 +15,22 @@ var schemaSuggestionsLog = logger.New("parser:schema_suggestions") | |||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Constants for suggestion limits and field generation | ||||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||||
| maxClosestMatches = 3 // Maximum number of closest matches to find | ||||||||||||||||||||||||||||
| maxSuggestions = 5 // Maximum number of suggestions to show | ||||||||||||||||||||||||||||
| maxAcceptedFields = 10 // Maximum number of accepted fields to display | ||||||||||||||||||||||||||||
| maxExampleFields = 3 // Maximum number of fields to include in example JSON | ||||||||||||||||||||||||||||
| maxClosestMatches = 3 // Maximum number of closest matches to find | ||||||||||||||||||||||||||||
| maxSuggestions = 5 // Maximum number of suggestions to show | ||||||||||||||||||||||||||||
| maxAcceptedFields = 10 // Maximum number of accepted fields to display | ||||||||||||||||||||||||||||
| maxExampleFields = 3 // Maximum number of fields to include in example JSON | ||||||||||||||||||||||||||||
| maxPathSearchDistance = 2 // Maximum Levenshtein distance for high-confidence path suggestions | ||||||||||||||||||||||||||||
| maxPathSuggestions = 3 // Maximum number of path locations to suggest | ||||||||||||||||||||||||||||
| schemaTraversalMaxDepth = 15 // Maximum recursion depth when traversing schema | ||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // schemaFieldLocation represents a location in the schema where a field is valid as a property. | ||||||||||||||||||||||||||||
| type schemaFieldLocation struct { | ||||||||||||||||||||||||||||
| FieldName string // the actual field name in the schema (may differ from query if fuzzy match) | ||||||||||||||||||||||||||||
| SchemaPath string // the parent schema path where this field is valid (e.g., "/on", "/safe-outputs") | ||||||||||||||||||||||||||||
| Distance int // Levenshtein distance from the query field name (0 = exact match) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // generateSchemaBasedSuggestions generates helpful suggestions based on the schema and error type. | ||||||||||||||||||||||||||||
| // frontmatterContent is the raw YAML frontmatter text, used to extract the user's typed value for enum suggestions. | ||||||||||||||||||||||||||||
| func generateSchemaBasedSuggestions(schemaJSON, errorMessage, jsonPath, frontmatterContent string) string { | ||||||||||||||||||||||||||||
|
|
@@ -56,9 +66,23 @@ func generateSchemaBasedSuggestions(schemaJSON, errorMessage, jsonPath, frontmat | |||||||||||||||||||||||||||
| invalidProps := extractAdditionalPropertyNames(errorMessage) | ||||||||||||||||||||||||||||
| acceptedFields := extractAcceptedFieldsFromSchema(schemaDoc, jsonPath) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| var suggestions []string | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if len(acceptedFields) > 0 { | ||||||||||||||||||||||||||||
| schemaSuggestionsLog.Printf("Found %d accepted fields for invalid properties %v", len(acceptedFields), invalidProps) | ||||||||||||||||||||||||||||
| return generateFieldSuggestions(invalidProps, acceptedFields) | ||||||||||||||||||||||||||||
| if s := generateFieldSuggestions(invalidProps, acceptedFields); s != "" { | ||||||||||||||||||||||||||||
| suggestions = append(suggestions, s) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Search the whole schema for where these fields belong (path heuristic) | ||||||||||||||||||||||||||||
| if s := generatePathLocationSuggestion(invalidProps, schemaDoc, jsonPath); s != "" { | ||||||||||||||||||||||||||||
| schemaSuggestionsLog.Printf("Found path location suggestion: %s", s) | ||||||||||||||||||||||||||||
| suggestions = append(suggestions, s) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if len(suggestions) > 0 { | ||||||||||||||||||||||||||||
| return strings.Join(suggestions, ". ") | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
|
|
@@ -475,3 +499,180 @@ func extractYAMLValueAtPath(yamlContent, jsonPath string) string { | |||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| return "" | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // collectSchemaPropertyPaths recursively collects all (fieldName, parentPath) pairs from a JSON schema document. | ||||||||||||||||||||||||||||
| // It traverses properties, oneOf/anyOf/allOf, and items to build a complete picture of valid fields across the schema. | ||||||||||||||||||||||||||||
| func collectSchemaPropertyPaths(schemaDoc any, currentPath string, depth int) []schemaFieldLocation { | ||||||||||||||||||||||||||||
| if depth > schemaTraversalMaxDepth { | ||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| schemaMap, ok := schemaDoc.(map[string]any) | ||||||||||||||||||||||||||||
| if !ok { | ||||||||||||||||||||||||||||
| return nil | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| var results []schemaFieldLocation | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Collect fields from properties and recurse into each property's schema | ||||||||||||||||||||||||||||
| if properties, ok := schemaMap["properties"].(map[string]any); ok { | ||||||||||||||||||||||||||||
| for fieldName, fieldSchema := range properties { | ||||||||||||||||||||||||||||
| results = append(results, schemaFieldLocation{FieldName: fieldName, SchemaPath: currentPath}) | ||||||||||||||||||||||||||||
| sub := collectSchemaPropertyPaths(fieldSchema, currentPath+"/"+fieldName, depth+1) | ||||||||||||||||||||||||||||
| results = append(results, sub...) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
Comment on lines
+503
to
+523
|
||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Recurse into oneOf/anyOf/allOf variants (schema composition keywords) | ||||||||||||||||||||||||||||
| for _, keyword := range []string{"oneOf", "anyOf", "allOf"} { | ||||||||||||||||||||||||||||
| if variants, ok := schemaMap[keyword].([]any); ok { | ||||||||||||||||||||||||||||
| for _, variant := range variants { | ||||||||||||||||||||||||||||
| sub := collectSchemaPropertyPaths(variant, currentPath, depth+1) | ||||||||||||||||||||||||||||
| results = append(results, sub...) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Recurse into items for array schemas | ||||||||||||||||||||||||||||
| if items, ok := schemaMap["items"].(map[string]any); ok { | ||||||||||||||||||||||||||||
| sub := collectSchemaPropertyPaths(items, currentPath, depth+1) | ||||||||||||||||||||||||||||
| results = append(results, sub...) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| return results | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // findFieldLocationsInSchema searches the entire schema for where the given field name is valid as a property. | ||||||||||||||||||||||||||||
| // It first attempts an exact match, then falls back to fuzzy matching with a high-confidence distance threshold. | ||||||||||||||||||||||||||||
| // The currentPath is excluded so we never suggest the same location that triggered the error. | ||||||||||||||||||||||||||||
| func findFieldLocationsInSchema(schemaDoc any, targetField, currentPath string) []schemaFieldLocation { | ||||||||||||||||||||||||||||
| allLocations := collectSchemaPropertyPaths(schemaDoc, "", 0) | ||||||||||||||||||||||||||||
| targetLower := strings.ToLower(targetField) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| seen := make(map[string]bool) | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| // Collect exact matches first | ||||||||||||||||||||||||||||
| var exactMatches []schemaFieldLocation | ||||||||||||||||||||||||||||
| for _, loc := range allLocations { | ||||||||||||||||||||||||||||
| if loc.SchemaPath == currentPath { | ||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| key := loc.FieldName + "|" + loc.SchemaPath | ||||||||||||||||||||||||||||
| if seen[key] { | ||||||||||||||||||||||||||||
| continue | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| seen[key] = true | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if strings.ToLower(loc.FieldName) == targetLower { | ||||||||||||||||||||||||||||
| loc.Distance = 0 | ||||||||||||||||||||||||||||
| exactMatches = append(exactMatches, loc) | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||
| if len(exactMatches) > 0 { | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
| if len(exactMatches) > 0 { | |
| if len(exactMatches) > 0 { | |
| // Sort exact matches by schema path (and field name) for stable output | |
| sort.Slice(exactMatches, func(i, j int) bool { | |
| if exactMatches[i].SchemaPath != exactMatches[j].SchemaPath { | |
| return exactMatches[i].SchemaPath < exactMatches[j].SchemaPath | |
| } | |
| return exactMatches[i].FieldName < exactMatches[j].FieldName | |
| }) |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generatePathLocationSuggestion calls findFieldLocationsInSchema for each invalid property, and findFieldLocationsInSchema traverses the entire schema each time via collectSchemaPropertyPaths. For multiple invalid fields this becomes repeated full-schema walks. Consider collecting all property paths once per generatePathLocationSuggestion call (or building an index map[fieldName][]schemaFieldLocation) and reusing it for each invalid property.
| var parts []string | |
| for _, prop := range invalidProps { | |
| locations := findFieldLocationsInSchema(schemaDoc, prop, currentPath) | |
| // Cache field locations per property name within this call to avoid redundant schema walks | |
| locationCache := make(map[string][]schemaFieldLocation) | |
| var parts []string | |
| for _, prop := range invalidProps { | |
| locations, ok := locationCache[prop] | |
| if !ok { | |
| locations = findFieldLocationsInSchema(schemaDoc, prop, currentPath) | |
| locationCache[prop] = locations | |
| } |
Copilot
AI
Feb 25, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
formatSchemaPathForDisplay returns "the root level", but generatePathLocationSuggestion always wraps display strings in single quotes. This can produce messages like "... belongs under 'the root level'", which reads a bit oddly. Consider handling the root case specially (e.g., no quotes, or display /).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
generateSchemaBasedSuggestionsjoins multiple suggestion strings with". ", butgenerateFieldSuggestionsoften ends with?(or no terminal punctuation). This can produce awkward output like"Did you mean 'x'?. 'x' belongs under ..."and then callers append another.prefix. Consider normalizing suggestion punctuation (e.g., join with a single space, or trim trailing punctuation and then join with., or ensure each component is sentence-safe before concatenation).