Skip to content

Commit a84957c

Browse files
[tools] Add compare_styles_tool for style comparison
Implements a comprehensive style comparison tool that performs deep structural comparison of two Mapbox styles, identifying additions, removals, and modifications in layers, sources, and properties. Key features: - Deep object comparison with detailed diff reporting - Layer comparison by ID instead of array index - Metadata filtering (ignoreMetadata flag) - Support for JSON strings or objects as input - Structured output with difference paths and summaries The tool extends BaseTool for offline operation (no API calls). Special handling for Mapbox style layers ensures layers are compared by their ID property rather than position in the array. Test coverage: 16 comprehensive test cases covering all features including identical detection, property/layer differences, metadata handling, error cases, and nested changes. Documentation: Added comprehensive README documentation for all three validation tools (validate_geojson_tool, validate_expression_tool, compare_styles_tool) with usage examples and feature descriptions. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent e279a15 commit a84957c

File tree

7 files changed

+957
-0
lines changed

7 files changed

+957
-0
lines changed

README.md

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ https://github.com/user-attachments/assets/8b1b8ef2-9fba-4951-bc9a-beaed4f6aff6
2626
- [GeoJSON Preview tool (Beta)](#geojson-preview-tool-beta)
2727
- [Coordinate Conversion tool](#coordinate-conversion-tool)
2828
- [Bounding Box tool](#bounding-box-tool)
29+
- [Validation Tools](#validation-tools)
30+
- [validate_geojson_tool](#validate_geojson_tool)
31+
- [validate_expression_tool](#validate_expression_tool)
32+
- [compare_styles_tool](#compare_styles_tool)
2933
- [Resources](#resources)
3034
- [Observability \& Tracing](#observability--tracing)
3135
- [Features](#features)
@@ -453,6 +457,151 @@ An array of four numbers representing the bounding box: `[minX, minY, maxX, maxY
453457
- "Calculate the bounding box of this GeoJSON file" (then upload a .geojson file)
454458
- "What's the bounding box for the coordinates in the uploaded parks.geojson file?"
455459

460+
#### Validation Tools
461+
462+
##### validate_geojson_tool
463+
464+
Validates GeoJSON data structure against the GeoJSON specification (RFC 7946), performing comprehensive format and semantic validation.
465+
466+
**Parameters:**
467+
468+
- `geojson` (string or object, required): GeoJSON content to validate. Can be provided as:
469+
- A JSON string that will be parsed
470+
- A GeoJSON object
471+
472+
**Supported GeoJSON types:**
473+
474+
- Point, MultiPoint
475+
- LineString, MultiLineString
476+
- Polygon, MultiPolygon
477+
- GeometryCollection
478+
- Feature, FeatureCollection
479+
480+
**Validation checks:**
481+
482+
- Type field presence and validity
483+
- Coordinates array structure
484+
- Position format (longitude, latitude, optional altitude)
485+
- Coordinate range validation (longitude: -180 to 180, latitude: -90 to 90)
486+
- Ring closure for Polygons
487+
- Minimum coordinate requirements per geometry type
488+
- Feature properties validation
489+
- Nested geometry validation in Collections
490+
491+
**Returns:**
492+
493+
```json
494+
{
495+
"valid": true,
496+
"errors": [],
497+
"warnings": [],
498+
"summary": {
499+
"type": "FeatureCollection",
500+
"featureCount": 5,
501+
"geometryTypes": ["Point", "LineString"]
502+
}
503+
}
504+
```
505+
506+
**Example prompts:**
507+
508+
- "Validate this GeoJSON file" (then upload a .geojson file)
509+
- "Check if my GeoJSON is valid"
510+
- "Is this GeoJSON structure correct?"
511+
512+
##### validate_expression_tool
513+
514+
Validates Mapbox GL JS style expressions against the Mapbox Style Specification, checking syntax, types, and semantic correctness.
515+
516+
**Parameters:**
517+
518+
- `expression` (array or object, required): Mapbox expression to validate
519+
- `expectedType` (string, optional): Expected return type (e.g., "boolean", "number", "string", "color")
520+
- `context` (string, optional): Context where expression will be used ("layer", "filter", "paint", "layout")
521+
522+
**Validation checks:**
523+
524+
- Operator existence and validity
525+
- Argument count and types
526+
- Type coercion rules
527+
- Nested expression validation
528+
- Context-appropriate operators
529+
- Return type matching
530+
531+
**Supported operators:**
532+
533+
- Mathematical: `+`, `-`, `*`, `/`, `%`, `^`, `sqrt`, `log10`, `ln`, `abs`, `ceil`, `floor`, `round`
534+
- Comparison: `==`, `!=`, `<`, `>`, `<=`, `>=`
535+
- Logical: `all`, `any`, `!`, `case`, `match`
536+
- String: `concat`, `downcase`, `upcase`
537+
- Type conversion: `to-boolean`, `to-number`, `to-string`, `to-color`
538+
- Lookup: `get`, `has`, `in`, `index-of`
539+
- Decision: `coalesce`, `step`, `interpolate`
540+
541+
**Returns:**
542+
543+
```json
544+
{
545+
"valid": true,
546+
"errors": [],
547+
"warnings": [],
548+
"expressionType": "number"
549+
}
550+
```
551+
552+
**Example prompts:**
553+
554+
- "Validate this Mapbox expression: ['get', 'population']"
555+
- "Check if this filter expression is valid"
556+
- "Is this paint property expression correct?"
557+
558+
##### compare_styles_tool
559+
560+
Compares two Mapbox styles and reports structural differences in layers, sources, and properties.
561+
562+
**Parameters:**
563+
564+
- `styleA` (string or object, required): First Mapbox style to compare (JSON string or style object)
565+
- `styleB` (string or object, required): Second Mapbox style to compare (JSON string or style object)
566+
- `ignoreMetadata` (boolean, optional): Ignore metadata fields like id, owner, created, modified, draft, visibility
567+
568+
**Comparison features:**
569+
570+
- Deep structural comparison of style objects
571+
- Layer comparison by ID (not position)
572+
- Source comparison with nested property checks
573+
- Property-level difference detection
574+
- Metadata filtering for logical comparisons
575+
576+
**Returns:**
577+
578+
```json
579+
{
580+
"identical": false,
581+
"differences": [
582+
{
583+
"path": "layers[id=\"background\"].paint.background-color",
584+
"type": "modified",
585+
"valueA": "#ffffff",
586+
"valueB": "#000000",
587+
"description": "Value changed from \"#ffffff\" to \"#000000\""
588+
}
589+
],
590+
"summary": {
591+
"totalDifferences": 1,
592+
"added": 0,
593+
"removed": 0,
594+
"modified": 1
595+
}
596+
}
597+
```
598+
599+
**Example prompts:**
600+
601+
- "Compare these two Mapbox styles and show me the differences"
602+
- "What changed between my old style and new style?"
603+
- "Compare styles ignoring metadata fields"
604+
456605
## Agent Skills
457606

458607
This repository includes [Agent Skills](https://agentskills.io) that provide domain expertise for building maps with Mapbox. Skills teach AI assistants about map design, security best practices, and common implementation patterns.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Mapbox, Inc.
2+
// Licensed under the MIT License.
3+
4+
import { z } from 'zod';
5+
6+
export const CompareStylesInputSchema = z.object({
7+
styleA: z
8+
.union([z.string(), z.record(z.unknown())])
9+
.describe('First Mapbox style (JSON string or style object)'),
10+
styleB: z
11+
.union([z.string(), z.record(z.unknown())])
12+
.describe('Second Mapbox style (JSON string or style object)'),
13+
ignoreMetadata: z
14+
.boolean()
15+
.optional()
16+
.describe('Ignore metadata fields like id, owner, created, modified')
17+
});
18+
19+
export type CompareStylesInput = z.infer<typeof CompareStylesInputSchema>;
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright (c) Mapbox, Inc.
2+
// Licensed under the MIT License.
3+
4+
import { z } from 'zod';
5+
6+
const DifferenceSchema = z.object({
7+
path: z.string().describe('JSON path to the difference'),
8+
type: z.enum(['added', 'removed', 'modified']).describe('Type of difference'),
9+
valueA: z.unknown().optional().describe('Value in style A (if exists)'),
10+
valueB: z.unknown().optional().describe('Value in style B (if exists)'),
11+
description: z.string().optional().describe('Human-readable description')
12+
});
13+
14+
export const CompareStylesOutputSchema = z.object({
15+
identical: z.boolean().describe('Whether the styles are identical'),
16+
differences: z.array(DifferenceSchema).describe('List of differences found'),
17+
summary: z
18+
.object({
19+
totalDifferences: z.number().describe('Total number of differences'),
20+
added: z.number().describe('Number of additions in style B'),
21+
removed: z.number().describe('Number of removals from style A'),
22+
modified: z.number().describe('Number of modifications')
23+
})
24+
.describe('Summary of differences')
25+
});
26+
27+
export type CompareStylesOutput = z.infer<typeof CompareStylesOutputSchema>;
28+
export type Difference = z.infer<typeof DifferenceSchema>;

0 commit comments

Comments
 (0)