Skip to content

feat(mcp): add patch_dashboard, get_dashboard_tile, search_dashboards tools (HDX-4139)#2343

Merged
kodiakhq[bot] merged 13 commits into
mainfrom
brandon/mcp-dashboard-improvements
Jun 2, 2026
Merged

feat(mcp): add patch_dashboard, get_dashboard_tile, search_dashboards tools (HDX-4139)#2343
kodiakhq[bot] merged 13 commits into
mainfrom
brandon/mcp-dashboard-improvements

Conversation

@brandon-pereira
Copy link
Copy Markdown
Member

@brandon-pereira brandon-pereira commented May 26, 2026

What

Add three new MCP dashboard tools for more granular, efficient operations alongside the existing full create/update path:

  • hyperdx_get_dashboard_tile — Retrieve a single tile by tileId without loading the full dashboard
  • hyperdx_patch_dashboard — Update dashboard name/tags and/or replace a single tile by tileId in one call. Unmentioned tiles and fields are preserved. Layout fields fall back to existing values when omitted.
  • hyperdx_search_dashboards — Search dashboards by name (case-insensitive partial match) and/or tags

Why

The current MCP dashboard tooling requires passing the full dashboard object on every update — making it slow and token-heavy for targeted edits like changing one tile's query or renaming a dashboard. These tools let the LLM do targeted edits without resubmitting everything.

Closes HDX-4139

Additional improvements

  • Add cross-referencessave_dashboard description now mentions patch_dashboard as the preferred tool for single-tile updates.
  • Document Lucene substring matching — Prominently documented that field:value is ilike (substring match, not equality) and field:val* is prefix-within-substring (not true prefix). Added to tool-level WHERE_DESCRIPTION, query guide prompt, and common mistakes section.
  • Split test file — Broke dashboards.test.ts (3040 lines) into 8 focused files under __tests__/dashboards/ with a shared setup helper.

Testing

  • 96 integration tests across 8 test files, all passing
  • TypeScript type check clean
  • ESLint 0 errors

… tools (HDX-4139)

Add three new MCP dashboard tools for more granular operations:

- hyperdx_get_dashboard_tile: retrieve a single tile by tileId without
  loading the full dashboard
- hyperdx_patch_dashboard: update dashboard name/tags and/or replace a
  single tile by tileId in one call, preserving all other tiles and
  falling back to existing layout when omitted
- hyperdx_search_dashboards: search dashboards by name (case-insensitive)
  and/or tags

Additional improvements:
- Fix empty parameter schema on patch_dashboard and search_dashboards
  caused by Zod .refine() wrapping (moved cross-field validation to
  handler body so inputSchema stays a plain z.object)
- Add 'prefer patch_dashboard' hint to save_dashboard description
- Document Lucene substring matching limitation prominently in tool
  descriptions and query guide prompt (field:value is ilike not equality,
  field:val* is prefix-within-substring not true prefix)
- Split monolithic dashboards.test.ts (3040 lines) into 8 focused test
  files under __tests__/dashboards/
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 26, 2026

🦋 Changeset detected

Latest commit: 3f1ce2d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 3 packages
Name Type
@hyperdx/api Patch
@hyperdx/app Patch
@hyperdx/otel-collector Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented May 26, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
hyperdx-oss Ready Ready Preview, Comment Jun 2, 2026 4:55pm
hyperdx-storybook Ready Ready Preview, Comment Jun 2, 2026 4:55pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

E2E Test Results

All tests passed • 193 passed • 3 skipped • 1261s

Status Count
✅ Passed 193
❌ Failed 0
⚠️ Flaky 2
⏭️ Skipped 3

Tests ran across 4 shards in parallel.

View full report →

@brandon-pereira brandon-pereira marked this pull request as ready for review May 26, 2026 21:58
@github-actions github-actions Bot added the review/tier-3 Standard — full human review required label May 26, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

🔴 Tier 4 — Critical

Touches auth, data models, config, tasks, OTel pipeline, ClickHouse, or CI/CD.

Why this tier:

  • Critical-path files (2):
    • packages/api/src/routers/external-api/v2/dashboards.ts
    • packages/api/src/routers/external-api/v2/utils/dashboards.ts

Review process: Deep review from a domain expert. Synchronous walkthrough may be required.
SLA: Schedule synchronous review within 2 business days.

Stats
  • Production files changed: 11
  • Production lines changed: 1283 (+ 6109 in test files, excluded from tier calculation)
  • Branch: brandon/mcp-dashboard-improvements
  • Author: brandon-pereira

To override this classification, remove the review/tier-4 label and apply a different review/tier-* label. Manual overrides are preserved on subsequent pushes.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 26, 2026

Deep Review

Scope: PR #2343 (brandon/mcp-dashboard-improvements) vs e3eeb50 — ~1.3k production LOC across new MCP tools (patchDashboard, getDashboardTile, searchDashboards), a shared validator extraction in routers/external-api/v2/utils/dashboards.ts, schema tightening in schemas.ts, and a test-file split.

🔴 P0/P1 -- must fix

  • packages/api/src/mcp/tools/dashboards/schemas.ts:312 -- The new .min(1) on tile name rejects legacy/REST-created tiles whose internal config.name is empty, but convertTileToExternalChart in packages/api/src/routers/external-api/v2/utils/dashboards.ts:465 still emits name: tile.config.name ?? '' on read, so a clickstack_get_dashboard → mutate-some-other-tile → clickstack_save_dashboard round-trip fails Zod on the unchanged legacy tile and the LLM has no diff context to interpret the error.
    • Fix: Coerce empty/missing names to a placeholder inside convertTileToExternalChart (or relax the MCP schema to z.string() matching REST), so the round-trip succeeds and the new constraint applies only to genuinely new tiles.
    • adversarial, correctness, testing

🟡 P2 -- recommended

  • packages/api/src/mcp/tools/dashboards/schemas.ts:603-653 -- mcpTileSchema and mcpPatchTileSchema are plain z.union over nine arms with no discriminator and the SQL arm last, so a tile that carries both builder fields (sourceId, select) and SQL fields (configType: 'sql', connectionId, sqlTemplate) is parsed by the first matching builder arm and the SQL-only keys are silently stripped, causing convertToInternalTileConfig to persist the wrong tile shape.

    • Fix: Convert to z.discriminatedUnion keyed on config.configType (or split the SQL branch out so the remaining eight discriminate on config.displayType), or call .strict() on each arm's config so unknown fields force the union to fall through.
    • adversarial, api-contract
  • packages/api/src/routers/external-api/v2/utils/dashboards.ts:1213-1223 -- The new cleanupDashboardAlerts branch using displayTypeSupportsBuilderAlerts (which returns true only for Line/StackedBar/Number) now silently deletes alerts attached to builder Table/Pie/Heatmap/Search/Markdown tiles on every save and patch, even when the tile config was untouched, and there is no changeset note or test asserting the new deletion scope.

    • Fix: Confirm whether builder alerts can exist on these display types in any prior schema; if yes, gate the deletion behind an explicit displayType change and document the behavior in the changeset.
    • api-contract
  • packages/api/src/mcp/tools/dashboards/searchDashboards.ts:60-95 -- The handler returns a bare array when results.length <= SEARCH_RESULTS_LIMIT but a { results, truncated, hint } object when it exceeds the limit, forcing every caller to branch on Array.isArray and there is no outputSchema to formalise either shape.

    • Fix: Always return the object shape (with truncated: false and no hint in the non-truncated case) and declare an outputSchema on the tool registration.
    • api-contract
  • packages/api/src/routers/external-api/v2/dashboards.ts:2106-2126 -- The PUT handler now does findOne first and short-circuits with res.sendStatus(404) before validateDashboardTiles runs, so a request with an invalid body against a missing-or-other-team dashboard now returns a bare 404 where it previously returned 400 with a structured validation message, and the precedence change is not documented in the OpenAPI block.

    • Fix: Note the 404-vs-400 precedence change in the route's OpenAPI description and the changeset, and add a regression test pinning the new ordering.
    • adversarial, api-contract, correctness
  • packages/api/src/mcp/tools/dashboards/patchDashboard.ts:158-216 -- The handler reads existingInternalTile at T1 and merges its x/y/w/h/containerId/tabId into the positional-$ update, but the write replaces the whole tiles.$ element, so a concurrent save_dashboard (or UI drag/move) that mutated the same tile between read and write has its layout/container change silently reverted by the patch.

    • Fix: Use per-field dotted-path updates (tiles.$.config, tiles.$.x, …) and $set only the fields the caller explicitly provided, or add an optimistic-concurrency guard (tile-level updatedAt/version) to the queryFilter.
    • adversarial, correctness, reliability
  • packages/api/src/routers/external-api/v2/utils/dashboards.ts:321-365 + packages/api/src/mcp/tools/dashboards/schemas.ts:485-503 -- The heatmap self-heal emits valueExpression: '' on read while mcpHeatmapSelectItemSchema.valueExpression enforces .min(1), so any dashboard with a single legacy/corrupt heatmap tile is permanently wedged for MCP save: unrelated edits cannot persist, and patch_dashboard cannot repair the broken tile because its schema rejects '' too.

    • Fix: Have the read path emit a sentinel the caller can detect (or drop legacy unrecoverable heatmap tiles like PromQL tiles three lines up) and add an explicit recovery path the LLM can invoke.
    • adversarial, correctness
  • .changeset/mcp-dashboard-improvements.md:2 -- Frontmatter declares '@hyperdx/api': patch while the body itself labels the tile name change as **Breaking (minor):**, so changeset-driven version-bump tooling will route this PR as a non-breaking patch and downstream consumers parsing the file for a breaking-change signal will not be alerted.

    • Fix: Bump the frontmatter from patch to minor (or relax .min(1) so the change is no longer breaking) and reconcile with the body.
    • api-contract, project-standards
  • packages/api/src/mcp/__tests__/dashboards/patchDashboard.test.ts -- The new alert-cleanup behavior (single-tile scope at patchDashboard.ts:243-257) and the 'containerId' in incoming / 'tabId' in incoming merge semantics at patchDashboard.ts:159-169 have no tests; the 'preserve layout when not specified' case only covers x/y/w/h, leaving the central preserve-vs-clear contract for container refs unpinned.

    • Fix: Add tests that (1) attach an alert to a Line tile, patch it to pie, and assert that alert is removed while a sibling alert is untouched, and (2) cover container ref preservation on omit, switching on explicit value, and the legacy empty-string self-heal.
    • correctness, testing
  • packages/api/src/mcp/__tests__/dashboards/searchDashboards.test.ts -- The truncation branch (limit(SEARCH_RESULTS_LIMIT + 1), slice to 100, envelope with truncated: true and hint) has no test, so a regression that flipped the threshold, returned the wrong envelope shape, or dropped the hint would not surface.

    • Fix: Seed 101 dashboards and assert the response parses as { results, truncated: true, hint } with results.length === 100; seed 100 and assert the non-truncated shape.
  • packages/api/src/mcp/__tests__/dashboards/patchDashboard.test.ts:398-449 -- The concurrent delete test removes the tile before the patch tool is invoked, so the handler returns at the read-time miss path on patchDashboard.ts:124; the write-time positional-$ miss path at patchDashboard.ts:218-234 (with the distinct error string and the name/tags rejection contract) is never exercised.

    • Fix: Spy on Dashboard.findOne to delete the tile inside its resolve (between the read and the findOneAndUpdate) and assert the write-time miss error string and the partial-update rejection.
    • adversarial, testing
🔵 P3 nitpicks (12)
  • packages/api/src/mcp/tools/dashboards/patchDashboard.ts:94-104 -- setPayload: Record<string, unknown> and queryFilter: Record<string, unknown> discard the typing the sibling saveDashboard.ts:322-324 got from Partial<IDashboard>, so a typo like setPayload.tagz = uniq(tags) would compile.

    • Fix: Type the operands as UpdateQuery<DashboardDocument>['$set'] and FilterQuery<DashboardDocument>.
    • kieran-typescript, maintainability
  • packages/api/src/mcp/tools/dashboards/patchDashboard.ts:116, 137-146, 154, 172 -- Four as casts (as { id: string }[], an inline structural cast, as Partial<ExternalDashboardTileWithId>, and trailing as ExternalDashboardTileWithId) launder the discriminated mcpPatchTileSchema union into the REST shape, so a future divergence between the MCP and REST tile shapes will compile.

    • Fix: Export type McpPatchTile = z.infer<typeof mcpPatchTileSchema> from schemas.ts, type inputTile against it, and use satisfies ExternalDashboardTileWithId on the merge result.
    • kieran-typescript, maintainability, project-standards
  • packages/api/src/mcp/__tests__/dashboards/setup.ts:28-31 -- Four null as any initializers in a new test-setup file combine both prohibited patterns from agent_docs/code_style.md, weakening type-checking across every test that imports ctx.

    • Fix: Type ctx via Awaited<ReturnType<typeof getLoggedInAgent>> plus the Source/Connection document types, or initialise lazily as undefined with non-null assertions at usage sites.
    • kieran-typescript, project-standards
  • packages/api/src/mcp/tools/dashboards/schemas.ts:313-366, 618-630 -- mcpPatchTileLayoutSchema repeats the six layout-field constraints from mcpTileLayoutSchema (x: 0..23, w: 1..24, etc.) and the nine-arm patch union literally restates the save union with only the layout wrapper swapped, so the next layout-constraint tweak must land in two places.

    • Fix: Derive mcpPatchTileLayoutSchema via mcpTileLayoutSchema.omit({ id: true }).partial() and build mcpPatchTileSchema from the save union with .partial() semantics on layout.
  • packages/api/src/mcp/__tests__/dashboards/patchDashboard.test.ts -- The two cross-field validation branches (Provide at least one of: name, tags, or tileId+tile and tileId and tile must both be provided or both omitted at patchDashboard.ts:42-71) and the Invalid dashboard ID branch at patchDashboard.ts:73-78 are uncovered.

    • Fix: Add three small tests calling patchDashboard with { dashboardId } only, { dashboardId, tileId } without tile, and { dashboardId: 'not-valid' }, asserting each error string.
  • packages/api/src/mcp/__tests__/dashboards/saveDashboard.test.ts -- The new .min(1) on tile name (schemas.ts:314) has no regression test asserting that clickstack_save_dashboard rejects tiles: [{ name: '', config: {…} }], so the constraint can silently be removed by a future change.

    • Fix: Add a single test asserting isError with a message mentioning name.
  • packages/api/src/mcp/tools/dashboards/patchDashboard.ts:80-89, 212-216, 251-257 -- Dashboard.findOne and findOneAndUpdate are unwrapped, so a transient Mongo rejection (including an alert-cleanup failure after a successful tile write) propagates through withToolTracing as an opaque JSON-RPC transport error, inconsistent with searchDashboards.ts:97-111 which returns structured isError content.

    • Fix: Pick one pattern across all dashboard MCP tools — either catch in each handler, or move the catch into withToolTracing so every tool emits structured isError on rejection.
    • correctness, reliability
  • packages/api/src/routers/external-api/v2/utils/dashboards.ts:780-784, 1146 -- fetchSourcesForValidation is a private one-line wrapper whose docstring says it exists so callers outside controllers/sources don't need a second import, but the only caller is one line above in the same file.

    • Fix: Inline the call into validateDashboardTiles and drop the wrapper plus the SourceForValidation alias that exists only to name its awaited return type.
  • packages/api/src/routers/external-api/v2/utils/dashboards.ts:938, 1146-1150 -- validateDashboardTiles fetches sources once at the top of its Promise.all, then getInvalidOnClickSearchSources calls getSources(team) again inside the same Promise.all, doubling the Mongo round-trip on every save/patch.

    • Fix: Refactor getInvalidOnClickSearchSources to accept a pre-fetched sources argument, mirroring getMissingSources and getHeatmapTilesWithIncompatibleSources.
    • maintainability, reliability
  • packages/api/src/mcp/tools/dashboards/getDashboardTile.ts:18-35, packages/api/src/mcp/tools/dashboards/patchDashboard.ts:27-37, packages/api/src/mcp/tools/dashboards/searchDashboards.ts:21-30 -- None of the three new tools declare an outputSchema, so the LLM cannot programmatically negotiate the return shape and a future change to the handler payload will silently break consumer parsers.

    • Fix: Declare outputSchema on each new registerTool call (and refactor searchDashboards to a single object shape so the schema is expressible).
  • packages/api/src/mcp/tools/dashboards/getDashboardTile.ts:66 -- The tile not found error lists ids from externalDashboard.tiles, which filters out PromQL tiles (no external representation), so an LLM that saw a PromQL tile id in the UI hits a confusing Available tile IDs: … list that omits it.

    • Fix: Source the available-id list from existingDashboard.tiles rather than the converted external representation.
  • packages/api/src/mcp/tools/dashboards/index.ts:12 -- export * from './schemas'; has no external consumers — sibling tool files import the schemas directly via ./schemas, so the barrel re-export adds no surface and obscures which symbols are part of the module's public API.

    • Fix: Drop the line or commit to the barrel and remove the direct sibling imports.

Reviewers (9): correctness, testing, maintainability, project-standards, security, api-contract, reliability, kieran-typescript, adversarial, previous-comments.

Testing gaps:

  • No REST v2 POST/PUT golden test asserting the migrated 400-message bodies stayed byte-identical post-validateDashboardTiles extraction.
  • No test exercises mcpTileSchema arm selection for a mixed builder+SQL payload.
  • No test creates a tile inside a container, patches it without specifying containerId/tabId, and asserts the refs are preserved across the patch.
  • No test seeds a heatmap tile with empty valueExpression, GETs it via REST, and immediately PUTs the same body to confirm whether validation surfaces the issue and how the caller can recover.

searchDashboards:
- Escape regex metacharacters in query before passing to MongoDB $regex
  to prevent injection (e.g. '[' throwing BadValue, '.foo' matching
  unintended substrings, catastrophic backtracking on patterns like
  '(a+)+$')
- Cap query length at 200 chars via Zod schema
- Wrap Dashboard.find in try/catch returning isError on failure

patchDashboard:
- Work directly with persisted internal tiles array instead of
  round-tripping all tiles through convertToExternalDashboard (which
  strips orphaned container refs from unrelated tiles via self-heal
  logic at utils/dashboards.ts:407-424)
- Convert only the single patched tile to internal format via
  convertToInternalTileConfig
- Use positional $set (tiles.<index>) instead of full-array $set so
  concurrent patches on different tiles don't clobber each other
searchDashboards:
- Regex metacharacters treated as literals (parentheses, brackets, dots)
- Invalid regex chars like '[' don't throw

patchDashboard:
- Positional update verified via raw DB read: untouched tiles are
  byte-identical before and after patching a sibling tile
P0 — patchDashboard concurrent write race:
- Replace `tiles.${tileIndex}` positional set with `tiles.id` in the
  query filter + `tiles.$` positional operator. A concurrent
  save_dashboard that replaces the tiles array can no longer cause us
  to overwrite an unrelated tile at a stale numeric index.
- null result from findOneAndUpdate now returns a specific error
  message indicating the tile may have been removed concurrently.

P2 — patchDashboard missing alert cleanup:
- Restore cleanupDashboardAlerts call after tile update, scoped to
  the single patched tile. Catches displayType changes to configs
  that don't support alerts.

P2 — patch tile name required when it should be optional:
- Make `name` optional on mcpPatchTileLayoutSchema so the LLM can
  send a config-only patch without re-specifying the tile title.
  The merge fallback (incoming.name ?? existing name) is now reachable.
- Add .min(1) to both mcpTileLayoutSchema.name and
  mcpPatchTileLayoutSchema.name to reject blank tile titles at the
  schema boundary.

P2 — searchDashboards empty query/tags bypass:
- Replace `query === undefined` guard with length checks so
  `query: ''` and `tags: []` are rejected instead of silently
  returning all dashboards.
@brandon-pereira
Copy link
Copy Markdown
Member Author

Note for reviewer: most of the diff is refactoring the tests to be multiple files. Would suggest focusing on the src

searchDashboards:
- Use lodash escapeRegExp instead of reimplemented helper
- Inline type narrowing to eliminate query! non-null assertion
- Tighten tags schema to z.string().min(1) to reject tags: ['']
- Add .limit(100) with truncation hint when results exceed cap
- Log errors before returning user-facing message

patchDashboard:
- Coerce legacy empty-string containerId/tabId to undefined in merge
  (mirrors convertTileToExternalChart self-heal for legacy docs)
- Expand concurrent-miss error to state name/tags were also discarded

schemas:
- Drop unused id field from mcpPatchTileLayoutSchema (tileId is the
  authoritative selector; inner id was silently overridden)

tests:
- Switch JSON.stringify byte-equality to toEqual (order-insensitive)
- Add tests: config-only patch preserves name, concurrent tile
  removal detected, empty query/tags/both rejected

changeset:
- Note .min(1) contract tightening on tile name
Extract shared validateDashboardTiles() helper in utils/dashboards.ts
that consolidates the ~95-line tile validation block previously
duplicated across 5 call sites (REST v2 POST/PUT, MCP save create/
update, MCP patch). Net -355 lines.

The helper runs all 7 checks in order: container refs, missing sources,
missing connections, source/connection mismatches (was REST-only, now
shared with MCP), heatmap source-kind gate, onClick dashboard targets,
onClick search source validation.

Also move getSourceConnectionMismatches from a private function in the
REST router to a shared export in utils — MCP callers now get the same
source/connection mismatch validation that was previously REST-only.

Fix cleanupDashboardAlerts to also catch builder tiles with
alert-incompatible display types (Pie, Table, Heatmap, Search,
Markdown). Previously only raw SQL tiles with incompatible types were
cleaned up, leaving stale alert documents on builder tiles that changed
to an unsupported displayType.
@github-actions github-actions Bot added review/tier-4 Critical — deep review + domain expert sign-off and removed review/tier-3 Standard — full human review required labels May 27, 2026
knudtty
knudtty previously approved these changes May 28, 2026
Copy link
Copy Markdown
Contributor

@knudtty knudtty left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking

Comment thread packages/api/src/mcp/tools/dashboards/patchDashboard.ts
Comment thread packages/api/src/mcp/tools/dashboards/patchDashboard.ts Outdated
Comment thread packages/api/src/mcp/tools/dashboards/patchDashboard.ts
Comment thread packages/api/src/mcp/tools/dashboards/searchDashboards.ts Outdated
# Conflicts:
#	packages/api/src/mcp/tools/query/schemas.ts
…hboard tools

Move inline inputSchema definitions from patchDashboard.ts and
searchDashboards.ts into schemas.ts as mcpPatchDashboardSchema and
mcpSearchDashboardsSchema. Also replace unicode escape \u2014 with
literal em dash in patch description.
…-improvements

# Conflicts:
#	packages/api/src/mcp/__tests__/dashboards.test.ts
#	packages/api/src/mcp/prompts/dashboards/content.ts
@kodiakhq kodiakhq Bot merged commit a19ba54 into main Jun 2, 2026
20 of 22 checks passed
@kodiakhq kodiakhq Bot deleted the brandon/mcp-dashboard-improvements branch June 2, 2026 18:06
jordan-simonovski pushed a commit that referenced this pull request Jun 3, 2026
… tools (HDX-4139) (#2343)

## What

Add three new MCP dashboard tools for more granular, efficient operations alongside the existing full create/update path:

- **`hyperdx_get_dashboard_tile`** — Retrieve a single tile by `tileId` without loading the full dashboard
- **`hyperdx_patch_dashboard`** — Update dashboard name/tags and/or replace a single tile by `tileId` in one call. Unmentioned tiles and fields are preserved. Layout fields fall back to existing values when omitted.
- **`hyperdx_search_dashboards`** — Search dashboards by name (case-insensitive partial match) and/or tags

## Why

The current MCP dashboard tooling requires passing the full dashboard object on every update — making it slow and token-heavy for targeted edits like changing one tile's query or renaming a dashboard. These tools let the LLM do targeted edits without resubmitting everything.

Closes HDX-4139

## Additional improvements

- **Add cross-references** — `save_dashboard` description now mentions `patch_dashboard` as the preferred tool for single-tile updates.
- **Document Lucene substring matching** — Prominently documented that `field:value` is `ilike` (substring match, not equality) and `field:val*` is prefix-within-substring (not true prefix). Added to tool-level `WHERE_DESCRIPTION`, query guide prompt, and common mistakes section.
- **Split test file** — Broke `dashboards.test.ts` (3040 lines) into 8 focused files under `__tests__/dashboards/` with a shared setup helper.

## Testing

- 96 integration tests across 8 test files, all passing
- TypeScript type check clean
- ESLint 0 errors
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

automerge review/tier-4 Critical — deep review + domain expert sign-off

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants