Skip to content

Align with agent skills - #128

Draft
nslavin wants to merge 13 commits into
mainfrom
align-with-agent-skills
Draft

Align with agent skills#128
nslavin wants to merge 13 commits into
mainfrom
align-with-agent-skills

Conversation

@nslavin

@nslavin nslavin commented Jul 29, 2026

Copy link
Copy Markdown

Align style_builder_tool with the Standard-first workflow

Separates the Mapbox Standard and Classic code paths in style_builder_tool, and
fixes the options that appeared supported on the wrong target while doing nothing.

Both bases previously shared one code path gated on nine scattered isUsingStandard
checks. Options belonging to the other target were accepted and dropped, so a caller
could set global_settings.mode: "dark" on Standard, or slot on Classic, and get a
success report describing a style that ignored them. A single StyleTarget descriptor
now owns the difference, and wrong-target options are rejected with a pointer to the
one that works rather than silently discarded.

Principal fixes

  • Classic bases were indistinguishable. Only base_style === 'standard' was ever
    branched on, so all eight Classic values produced byte-identical output: dark-v11
    was a light map and satellite-v9 had no imagery. The base name now determines
    light/dark and whether mapbox.satellite raster tiles sit underneath.
  • action: "hide" did nothing on Standard. Hiding worked by omitting the layer,
    which the import redraws. It now sets the matching standard_config toggle and
    reports which; features with no toggle (water, landuse, roads) are rejected with an
    alternative rather than reported as hidden.
  • Custom data was inexpressible. New custom_sources accepts GeoJSON and vector
    tilesets, with slot, emissive strength and line-occlusion-opacity applied — the
    properties that were lost when layers had to be hand-authored and uploaded raw.
  • standard_config covered 26 of 44 documented properties. The schema strips
    unknown keys, so the remaining 18 could be set and dropped without error. A test
    pins the list against the reference documentation.
  • An empty Classic layers array reported success for a lone background layer.
    It is now a redirect naming the three things the caller likely meant.

Full per-fix detail, including the data-driven styling and layer-id collision fixes,
is in CHANGELOG.md.

Also included

  • MCP prompts and resource://mapbox-style-layers updated to Standard-first guidance;
    create-and-preview-style previously emitted Standard instructions for Classic bases.
  • New npm run eval:tools harness scoring tool selection and generated style JSON
    against deterministic predicates, using the real tool definitions off the registry.
  • skills/ is now a pointer to mapbox/mapbox-agent-skills.
  • npm run lint extended to scripts/*.mjs, previously covered by no linter.

Verification

  • 656 tests pass (46 files); tsc --noEmit and eslint clean.
  • Converted filter expressions and generated styles are validated against
    @mapbox/mapbox-gl-style-spec in tests.
  • 13 commits, 27 files, +4,887 / −571.

Breaking changes

None to tool names or output schemas. Inputs that previously succeeded while being
ignored are now rejected with guidance: global_settings on Standard,
standard_config and slot on Classic, standard_config.showRoadsAndTransit,
action: "hide" on an untoggleable Standard feature, an empty Classic layers
array, and a custom_sources key colliding with composite or satellite.

nslavin and others added 13 commits July 28, 2026 17:58
style_builder_tool defaults to Standard and exposes lightPreset, so it could
already generate the one combination that is broken by construction: a custom
fill/line/circle layer with no emissive strength, on a night-lit basemap. The
word "emissive" did not appear anywhere in the repo.

Two changes to what the tool emits on Standard styles:

- Every custom layer gets a slot. Omitting slot is not a neutral default on
  Standard — the layer lands above every basemap layer including street labels.
  When a caller leaves it off, infer one from the geometry type (fill/raster →
  bottom, line/circle/heatmap → middle, symbol → top) and report the choice in
  corrections rather than applying it silently. fill-extrusion keeps no slot,
  since 3D geometry participates in the scene's depth.

- fill/line/circle layers get emissive strength 1. These three default to 0,
  which lets the scene light the layer into shadow under dusk and night. Symbol
  layers are excluded because icon- and text-emissive-strength already default
  to 1, and fill-extrusion is excluded because it should be lit by the scene.

Also corrects the guidance the tool gives about itself. The description called
slot "OPTIONAL" and described omitting it as a default; the schema described
'top' as "above all base map features (default)" while the description of the
same field correctly said "behind Place and Transit labels". Both now match the
style spec, and neither claims a default that the optional field does not have.

global_settings.mode:'dark' predates light presets and only recolors the layers
this tool emits, so on Standard it produced a half-dark map fighting the
basemap. It is now Classic-only, with a correction pointing at
standard_config.lightPreset:'night'. Same for background_color, which Standard
supplies through the import. label_color was never read by the implementation
at all, and now says so instead of looking functional.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The prompts were the furthest out of alignment in the repo: slot, emissive,
lightPreset and standard all appeared zero times across all seven of them,
while style_builder_tool in the same server says "ALWAYS use standard".

create-and-preview-style defaulted to streets-v12 and, in step 3, told the
agent to hand-author a classic style from scratch — a raw streets-v8 source
plus a #f0f0f0 background layer — bypassing style_builder_tool entirely. It now
defaults to standard, builds through the tool, and starts from an import rather
than a layer list. Dark maps keep base_style standard and set lightPreset.

design-data-driven-style shipped a red→green RdYlGn ramp as its recommended
diverging scheme, roughly 270 lines above its own "Use red/green combinations
(colorblind-unfriendly)" warning. Replaced with ColorBrewer RdBu, matching
mapbox-data-visualization-patterns, with the reasoning stated where the ramp is
rather than only in the summary. The categorical example keeps Set1: red and
green in an unordered palette with a legend is fine, and the rule is about
ordered ramps, so the distinction is now spelled out instead of implied.

Every example layer in that prompt also gained a slot and emissive strength,
including circle — the case cartography's original rule omitted, and the one
that matters most since circles are usually the foreground data. The heatmap
example gains a slot but explicitly no emissive strength, because there is no
heatmap-emissive-strength property to set.

prepare-style-for-production validated expressions and text contrast but
checked neither of the two things that actually break a Standard style, both of
which produce perfectly valid JSON. Added a step that flags custom layers
missing a slot and fill/line/circle layers missing emissive strength, without
flagging symbol layers or fill-extrusion, which are correct as they are. Its
contrast check no longer assumes a hand-authored background either — it asks
which lightPreset the text sits on, and requires passing both when the app
switches presets at runtime.

The mapbox-style-layers resource is what style_builder_tool points agents at
for layer authoring, and it had no mention of slots, emissive strength or
occlusion in 345 lines. It now leads with a Standard section and lists the
three emissive-strength properties and line-occlusion-opacity alongside the
paint properties they belong with, including their real defaults.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README's create-and-preview-style example asked for "a dark-themed map for
nighttime navigation" and passed base_style: "dark-v11" — the exact anti-pattern
the prompt now avoids. It keeps standard and notes why the light preset is the
better control: it relights the basemap coherently and can change at runtime
without reloading the style.

docs/STYLE_BUILDER.md had no mention of slots or emissive strength. Added a
Standard section covering config-before-layers, the three slots, and the
emissive-strength defaults, plus troubleshooting entries for the four symptoms
these actually present as — data covering the street labels, a layer that looks
right by day and vanishes at night, a route disappearing behind buildings, and
dark mode that only half-applies.

skills/README.md advertised "10 comprehensive skills" and listed 10; the skills
repo ships 19. The nine missing ones were data-visualization, flutter,
geospatial-operations, location-grounding, both mcp-*-patterns, both search-*,
and store-locator. Regrouped so the categories fit what is actually there, and
added a short section naming mapbox-cartography as the source of truth for
map-design doctrine, with the specific rules that keep showing up as bugs — so
the next person changing one of them knows both repos have to move together.

Added the Mapbox design vocabulary this guidance depends on to the cspell
dictionary, mirroring the same change in mapbox-agent-skills. This does not fix
the pre-existing spellcheck failures elsewhere in the repo, which are mostly
test fixtures and style-spec field names; spellcheck is not part of CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
One existing test asserted the behavior this change corrects — it required that
custom layers on a Standard style carry no slot, on the reasoning that "no slot
means the layer appears above all existing layers." That is accurate about what
the renderer does and wrong about what anyone wants, so it now asserts a slot is
always present and that the inferred choice is reported rather than silent.

New coverage for the failures that were previously unrepresented:

- Slot inference per geometry type, and that explicit slots still win
- Emissive strength on fill/line/circle, absent on symbol, absent on Classic
  styles where there is no lighting to shadow the layer
- global_settings.mode:'dark' on Standard steering to lightPreset
- The diverging ramp asserts the RdYlGn endpoints (#d7191c, #1a9641, #a6d96a)
  are gone and the RdBu ones are present. A test that only checked for the word
  "Diverging" passed against the red→green ramp for as long as it shipped
- No heatmap-emissive-strength, which does not exist as a property
- The style-layers resource documents all three emissive properties, occlusion
  opacity, and that symbol layers need nothing

prepare-style-for-production's step assertions move from 5 to 6 now that the
Standard placement check sits between contrast and optimization.

The tool-list snapshot updates for the rewritten style_builder_tool description.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Flagged in review as a probable Classic-style leftover on the Standard branch.
It isn't one, and the reason is worth writing down where the line is.

A sprite has to match the icon vocabulary of the data source, not the basemap
being imported. The symbol layers this tool emits reference icons by maki name —
either ["get", "maki"] straight off the Streets v8 maki field, which carries 101
names like "restaurant" and "cafe", or a literal default like "marker-15". The
Streets sprite is the sprite built for that data, so it resolves those names.

Standard's own icons cannot serve those layers. An import is a separate scope
whose contents are not addressable from the importing style's layers, and
Standard carries its own iconography rather than exposing maki names. Per the
style spec, a sprite is required once any layer uses icon-image or a *-pattern
property, and it is the root style's sprite that serves the root style's layers.

So the value stays. The test pins the pairing rather than the string, so anyone
switching the sprite has to confront the icon names that depend on it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Most of this PR is text a model reads — tool descriptions, prompts, resources —
and unit tests can only assert that the text shipped, not that it changes agent
behavior. So this adds an eval for the surface an agent actually consumes.

scripts/eval-tools.mjs hands the model the real tool definitions off the
registry (descriptions plus zod schemas converted to JSON Schema), gives it a
task, and inspects the tool calls it makes. Where the model calls
style_builder_tool the tool is executed for real — it needs no network — so the
checks run against the style JSON that would actually ship. Checks are
deterministic predicates, not an LLM judge, because the output is structured;
that keeps the score stable so a move in it means a move in behavior.

Five cases: dark mode, custom data layers, config-before-layers, a diverging
ramp, and whether style creation routes through the builder.

Against origin/main: 83.3%, 88.9%, 83.3% over three runs. With this branch:
100% three times. The checks that consistently failed on main were exactly the
rules this PR adds — emissive strength on custom layers, line-occlusion-opacity
on routes, and a colorblind-safe diverging scheme.

The eval also found what unit tests could not. create_style_tool's schema said a
style "must include: version (8), sources, layers" and never mentioned imports,
so an agent reading it hand-authors a Classic style — the tool builds nothing,
it uploads whatever it is handed. Both its description and that schema now name
imports, point at style_builder_tool, and say dark mode is lightPreset:'night'.

Worth recording for whoever picks up the next round: style_builder_tool cannot
add a user's own data. It only restyles Streets v8 basemap layers, with no
sources parameter and no GeoJSON path, so the slot and emissive-strength
handling it now does never reaches the commonest case — the user's own zones,
routes and points. That style gets hand-authored and uploaded through
create_style_tool, where the improved description is the only thing steering it.
A builder that accepts a GeoJSON source would close it properly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r own data

Four open items, and they turned out to share a root cause: the builder treated
Standard and Classic as one code path with nine scattered `isUsingStandard`
checks, one of them a positional boolean threaded into layer creation. Every new
rule added another branch, and options for the wrong target looked supported
while doing almost nothing.

The divergence now lives in one place. A StyleTarget descriptor says what each
target is: whether slots apply, whether the scene is lit, whether a background
layer is needed, and which options belong to the other target. Shared machinery
— Streets v8 resolution, filters, paint and expression generation — stays shared.

Wrong-target options are now rejected with a pointer to the option that works,
rather than silently dropped. `global_settings.label_color` on Standard names
colorPlaceLabels; `standard_config` on Classic says Classic has no config
surface. Enforced at the tool boundary rather than in the schema because tool
registration reads `inputSchema.shape`, which only exists on a plain object
schema — a discriminated union would break registration in every MCP client, and
its `oneOf` output is the kind of complex schema this repo has already been
bitten by.

custom_sources closes the gap the eval exposed. The builder could only restyle
Streets v8 basemap layers, so a user's zones, route or store points had to be
hand-authored and uploaded raw, which is where slots and emissive strength got
lost. Declare a GeoJSON or vector source and point a layer at it with source_id.
Placement deliberately differs from basemap layers: a basemap fill belongs in
`bottom` under the roads, but a fill of your own data is an overlay and belongs
in `middle`. Lines get line-occlusion-opacity, since the canonical user line is a
route and basemap roads are correctly hidden by buildings.

label_color now works. It was declared but never read, and reinstating it
naively still did nothing: the generic per-property defaults had already put a
literal text-color in place and the new code deferred to it. Precedence is now
explicit — a colour on the layer, then label_color, then the dark-mode default,
all beating the generic default.

Icons come from each layer's own icon field. The maki branch was keyed on
'poi_labels' and 'transit', neither of which is a real source layer, so it was
unreachable and every symbol layer picked up a hardcoded "marker-15" — one
generic pin for every feature, and a pin on every city label. Now poi_label and
airport_label resolve ['get','maki'], transit_stop_label prefers maki over
network because network is null for most stops, and place_label gets no icon.

Eval: 19/19 across three runs, including a new check that the model actually
finds and uses custom_sources rather than falling back to hand-authoring.
Baseline on origin/main was 83.3–88.9%.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four things the builder reported doing and didn't:

`action: "hide"` hid a feature by omitting its layer, which works on
Classic — the tool authors the whole stack there — but not on Standard,
where the basemap keeps drawing it through the import. The summary said
"Hidden" either way. On Standard the tool now sets the matching config
toggle (poi_label → showPointOfInterestLabels, and so on) and names it.
Water, landuse and the road network have no toggle, so `hide` on those
is rejected with a pointer to `theme` and the `color*` overrides.

Only `base_style === 'standard'` was ever branched on, so all eight
Classic values produced byte-identical output: `dark-v11` was a light
`#f8f4f0` map and `satellite-v9` had no imagery. The base name now
decides light vs dark and, for the satellite bases, adds a
`mapbox.satellite` raster in place of the background. Bases within a
group stay equivalent rather than being given invented differences —
a Classic base is not an import and cannot reproduce the named style.

`slot` sits on the layer, not the top level, so it was invisible to the
wrong-target check and was the last cross-target option still dropped
in silence. It is now rejected on Classic.

Layers from `custom_sources` now report their inferred slot, as basemap
layers already did, and a fill is told about `slot: "bottom"` — the
overlay default is right for a zone and wrong for a choropleth.

Recolouring a basemap feature on Standard still generates the layer,
since an overdraw is right for a filtered subset, but names the config
property that retints the basemap itself.

Prompts, docs and the eval follow: `create-and-preview-style` gave
Classic the Standard import block and `lightPreset`, `build-custom-map`
and `setup-mapbox-project` read theme words as Classic style names,
`debug-mapbox-integration` gains a phase for the Standard failures that
throw nothing, `design-data-driven-style` routes data through
`custom_sources`, and the eval covers the Classic path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… ids

Review of the previous commit turned up four things the new Standard
`hide` path got wrong, all from one cause: it ran before layer
resolution and keyed the toggle map off the raw `layer_type`.

`layer_type: "pois"` with `filter_properties: { maki: "restaurant" }`
resolves to `poi_label` for every other action, but `hide` rejected it
as unhideable — in a message that went on to list `poi_label` as
hideable. The pass now sits after resolution and keys off the resolved
source layer.

A layer of your own from `custom_sources` was told to use `theme:
"faded"` and the `color*` overrides. Your data isn't in the import, so
`hide` there is omission on either target; the toggles are advice about
the basemap and don't apply.

An unrecognised `layer_type` got a verdict about what Standard can hide
instead of the suggestion list, which diagnoses the wrong problem. It
now returns exactly what the same input returns under any other action.

A `hide` contradicting an explicit `show*` value resolved it in silence.
The hide still wins; it now says so.

Separately, `custom_sources` is merged last so neither target branch can
clobber it, which meant an entry keyed `composite` — or `satellite` on a
satellite base — replaced the basemap source rather than joining it, and
every basemap layer then read the caller's data. Those ids are now
rejected up front with a suggested rename.

Also: the summary takes the build result instead of re-deriving the
Classic settings and the hide toggles from the input, the five
guidance-only early returns in `buildStyle` collapse into one helper,
and the `hide-basemap-feature` eval case loses a check that could not
fail — `layers` is required by the schema, so `Array.isArray` on it was
always true.

Tests cover each of the five behaviours, plus the two gaps the review
noted: `satellite-streets-v12` (the other imagery base) and
`label_color` against a dark mode the base name implied.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A stricter pass over the previous commit, which had propagated one wrong
toggle and left a comment claiming more than the code did.

`hide` on a building set `show3dObjects`, which is the whole 3D group —
buildings, trees, landmarks and facades. So hiding buildings also
stripped the 3D trees and landmarks nobody asked about, while the
correction line reported only the buildings. It now sets
`show3dBuildings`, and that property is exposed on `standard_config`
alongside `show3dObjects` for callers who do want all 3D gone.

`standard_config.showRoadsAndTransit` was accepted and did nothing.
Per Mapbox's docs it is a Standard *Satellite* property; the plain
Standard style this tool imports has no such config, and `base_style`
offers no Satellite value, so it landed an inert key in the import.
The wrong-target check only compared Standard against Classic, never a
property belonging to a different Standard. It stays in the schema on
purpose — the schema strips unknown keys, so deleting it would trade one
silence for another.

The `generateSummary` docstring added last commit claimed everything
came from the build result, while the layer description six lines down
still re-derived from the raw input — so a layer passed as "pois" was
described by a name the built style never used. The resolved source
layer is now threaded through with the toggles, and the docstring says
what is and isn't read from the input.

Contradiction reporting is completed rather than half-done: two layers
hiding one feature emit one correction instead of two, and hiding a
feature while also drawing it is now named — as a filtered-subset
technique when the drawn layer has a filter, and as a plain mistake when
it doesn't. Both are keyed by the hidden feature, not by the layer that
hid it; keying by layer was a duplicate bug this commit's own probe
caught before it shipped.

Also fixes a test name I had mangled dodging an apostrophe, and covers
the gaps: the reserved-source-id check firing on a source no layer
references, duplicate hides, and hide-plus-draw both ways.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e surface

Review pass over the `custom_sources` path this branch added. Every
defect below is the same one the branch exists to remove: an option
accepted by the schema and then silently dropped.

`createUserDataLayer` wrote the literal `color` only, so `expression`
and `property_based`/`property_values` were ignored. A choropleth over
your own data therefore came out as a fill with **no `fill-color` at
all** — which the spec draws as opaque black over the whole map, with
nothing in the corrections to say so. The same `design-data-driven-style`
prompt this branch rewrote routes choropleths through exactly this path.
Colour now goes through the shared expression builder, and a category
`match` always gets a fallback arm: without one an unlisted value draws
nothing, so `color` sets it and the colour used is reported when it
wasn't given.

Fixing that surfaced a second bug in `generateExpression`: it returned
`config.expression` whatever property it was asked for, so a layer with
both `expression` and `opacity` got the same colour ramp in
`fill-opacity`, where the spec expects a number. The expression
describes the colour and is applied only there; the zoom-ramp maths
moved to `zoomRamp` so the new path can ask for a ramp without
inheriting the colour escape hatches.

Layer ids collided. They derive from what a layer draws, so a filtered
pair off one GeoJSON source both came out `zones-fill`, as did the same
basemap feature asked for twice. Duplicate ids are invalid per the spec
and the collision was silent; they are suffixed rather than rejected,
since asking for a feature twice is legitimate.

`render_type: "symbol"` over your own data got no `layout` at all, and a
symbol layer with neither `text-field` nor `icon-image` renders nothing —
present, valid, invisible. It is labelled from `name` now, with the
assumption reported because the tool cannot read the source to check it.
Still no `icon-image`: a literal resolves against the Streets sprite and
would pin every feature identically, the defect the basemap path just
lost.

A `fill-extrusion` layer reported `inferred slot "undefined"` and then
warned about drawing over the street labels — the opposite of what the
code chose, which is to leave real 3D geometry unslotted on purpose.

`standard_config` exposed 26 of the 44 documented Standard properties.
Because the schema strips unknown keys, the other 18 could be set and
dropped with no error. Added the indoor group, the POI land-use colours,
the granular 3D toggles, `colorLand`, `colorBuildings` and
`fuelingStationModePointOfInterestLabels`; a test pins the list against
the reference docs so the next upstream addition fails a test instead of
going quiet. `colorLand` also fixes the `background_color` rejection,
which pointed at `colorWater`/`colorGreenspace` — neither of which is
the land colour, and naming the wrong replacement undercuts the point of
rejecting the option at all.

The build summary walked a written-out list covering 8 of 15 `show*`
toggles and 6 of 22 `color*` overrides, so most of the config surface
was applied to the import and then left out of the report, and anything
added to the schema was omitted by default. It is derived from what was
actually set now. Same class of defect as the `hide` bug: the summary
claiming less than the build did.

Finally, the surface itself. This branch had taken the per-request cost
of the two changed tools from 11,564 to 38,687 characters of description
plus JSON Schema — paid on every request to this server, styling task or
not, and `style_builder_tool` alone had become 54% of it. The
reference-shaped material (the slot table, the emissive-strength
explanation, the Streets v8 examples) already exists verbatim in
`resource://mapbox-style-layers`, which is fetched on demand, and the
per-field detail in the input schema the model is given anyway. The
descriptions keep only the rules that change the shape of a call, and
the duplicated essays come off `slot`, `action`, `base_style`,
`custom_sources` and `render_type`. `style_builder_tool`'s description
is now 2,722 characters, 26 below where this branch found it, and the
whole 23-tool surface is 32,386 against 28,823 on main — the remaining
growth being the 19 config properties above.

Not verified: `npm run eval:tools`, which needs a live API key. Every
rule its seven cases assert is preserved, but only a real run shows
whether a model acts on 2,722 characters as it did on 7,577.

Also: `setup-mapbox-project` suggested a satellite recipe
(`base_style: "standard"` plus an imagery layer) that this tool cannot
build, since `custom_sources` takes GeoJSON and vector tilesets only;
the eval destructured a `TOOLS_WITH_UI` export that does not exist and
masked the `undefined` with `|| []`, so a tool moved out of `CORE_TOOLS`
would have vanished from the measured surface; and `npm run lint` now
covers `scripts/*.mjs`, which was the one source file going through no
linter or formatter.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A Classic base authors nothing, so `layers: []` produced a lone background
layer reported as a finished style. It is also where a caller lands having
read `base_style: "dark-v11"` as "give me dark-v11" — the likeliest reading,
and the one thing this tool cannot do, since it builds new self-contained
styles rather than fetching existing ones. Rejected now, naming all three
readings: reference the real style, configure Standard instead, or list what
a self-contained stack should draw. Standard stays exempt, where a
config-only style with no layers of its own is the normal shape. A merely
thin Classic stack is still built, with the shortfall named.

The custom-source path was also still dropping things the rest of the tool
rejects: a layer with no `color` came out opaque black, bare hex reached the
style unparseable, a `fill-extrusion` had no height and so drew nothing, and
`zoom_based` was accepted with nothing to ramp. A user `fill-extrusion` also
took the overlay slot, contradicting the reasoning applied to a basemap one
40 lines away — 3D geometry depth-sorts against the scene, and any slot
flattens it. Colour normalisation is now one shared helper, which also stops
a named colour becoming `#red`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three silent-drop paths on the custom-source layer, which is the least
covered of the new code, plus the eval-harness robustness gaps.

An object `filter` was assigned straight to `layer.filter`. The basemap
path resolves that shorthand against Streets v8 field metadata; custom
data has none, so the object went through verbatim and the spec rejects
it — `validate_style_tool` says "array expected, object found", but only
once `create_style_tool` has the JSON. Converted to an expression and
reported, since the object form is what a caller carries over from a
basemap layer in the same call. A bare string or number is rejected:
there is no property to test against.

A heatmap dropped `color`, `expression`, `property_based` and `opacity`.
`heatmap-color` is a ramp over ["heatmap-density"] rather than a colour,
so it is absent from the colour table, and `heatmap-opacity` was missing
from the opacity table too — both lookups returned nothing and the whole
block was skipped, leaving a layer with no paint reported as built
successfully. This is the render type `design-data-driven-style` names
for point data. `width` on a non-line layer is now named rather than
dropped, without inferring `circle-radius` from a field called width.

The eval hardcoded `thinking: {type: "disabled"}`, which Claude Fable 5
rejects outright and which reduces how readily any model reaches for
tools — the only thing the eval scores. Effort moves to EVAL_EFFORT.
Cases run under allSettled so one rate limit no longer discards every
other result, MAX_TURNS exhaustion is marked TRUNCATED instead of scored
as a complete transcript, and a schema that fails to convert warns.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant