fix(validator): bound the data_visualization consistency checks - #86
Merged
StephenTangCook merged 1 commit intoSep 1, 2026
Merged
Conversation
`checkDataVisualizationConsistency` counted each category with `labels.filter(...)`, rescanning every label once per category — O(categories × labels) per series. A 217 KB payload (5,000 categories, 5,000 data points) cost ~270 ms of CPU where a normal payload of the same size costs under a millisecond. `validateBlockKit` ran the cross-payload helpers whether or not the schema had passed, so the schema's own `maxItems` caps never constrained that path: a payload reached the quadratic loop precisely by being schema-invalid. The same helper emitted one error per series × uncovered category, and `validateBlockKit` aggregated helper results with `errors.push(...helper())`. 25,000 categories (212 KB, under the API's 256 KB cap) produced ~150,000 strings, and the spread exceeded V8's argument limit — an uncaught `RangeError: Maximum call stack size exceeded`. - count the labels once into a `Map`, then look each category up in O(1): O(categories + labels) instead of O(categories × labels) - stop after 100 mismatch messages per series and summarize the rest, so unbounded input reaching the helper directly through `/helpers` can't turn into an unbounded array - cap `axis_config.categories` at 20 items, alongside the existing caps on `series` (6) and `data` (20) — a series must carry exactly one data point per category, so more than 20 was never satisfiable - skip the cross-payload helpers once the schema has rejected the payload, closing the whole "bypass the schema's bounds by being schema-invalid" path rather than this one instance of it - append helper results with a loop rather than a spread - catch anything thrown out of `validateBlockKit` in the worker's `/v1/validate` handler and return a structured 500 Measured on the reported payloads: 5,000 × 5,000 goes from ~270 ms to ~8 ms, and 25,000 categories returns a verdict instead of throwing. Behavior changes worth knowing about: - an invalid payload's `errors[]` no longer mixes schema and caveat-helper messages; schema errors come back on their own. The `valid` verdict is unchanged — schema errors alone already made it `false`. - `categories` now generates a tuple union in `types.generated.ts`, as every other capped array in the schema already does, so TypeScript consumers assigning a plain `string[]` will need a typed literal. Released as a patch deliberately: consumers on `^0.1.x` would not pick up a minor bump, and no runtime verdict changes. - payloads with more than 20 categories now fail the schema. They were already invalid — with 20-item `data` arrays they could never cover more than 20 categories — so no previously-valid payload changes verdict. Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MAfoobRJmr4GAMETtDaveE
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
slack-block-kit-validator | e87190f | Commit Preview URL Branch Preview URL |
Sep 01 2026, 12:30 AM |
StephenTangCook
deleted the
claude/quadratic-categories-labels-loop-9homkr
branch
September 1, 2026 00:37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
checkDataVisualizationConsistencycounted each category withlabels.filter(...), rescanning the whole label array once per category — O(categories × labels) per series. A 217 KB payload (5,000 categories, 5,000 data points) cost ~270 ms of CPU where a normal payload of the same size costs under a millisecond, on a public unauthenticated endpoint.Two things compounded it:
validateBlockKitthe Ajv result only gated anerrors.push(), so the schema's ownmaxItemscaps (series: 6,data: 20) never constrained the path — a payload reached the quadratic loop precisely by being schema-invalid.axis_config.categorieshad nomaxItemswhile both its siblings did, and helper results were aggregated witherrors.push(...helper()). 25,000 categories (212 KB, under the API's 256 KB cap) produced ~150,000 error strings and the spread exceeded V8's argument limit — an uncaughtRangeError: Maximum call stack size exceeded, which the worker turned into an unhandled error rather than a response.Reproduced on
mainbefore the change and re-measured after:RangeErrorType of change
src/slack-block-kit.schema.json)Changes
check-data-visualization-consistency.ts— count the labels once into aMap, then look each category up in O(1): O(categories + labels) instead of O(categories × labels).check-data-visualization-consistency.ts— stop after 100 mismatch messages per series and summarize the rest. The helper is exported on its own via/helpers, where no schema has run, so nothing upstream bounds its input; this keeps an unbounded payload from becoming an unbounded array (150,000 strings → 606 in the reported case).slack-block-kit.schema.json— capaxis_config.categoriesat 20 items, alongside the existing caps onseries(6) anddata(20).validate-block-kit.ts— skip the cross-payload helpers once the schema has rejected the payload. This is the part worth the most attention: it closes the whole "bypass the schema's bounds by being schema-invalid" path rather than this one instance of it.validate-block-kit.ts— append helper results with a loop (pushAll) instead of a spread, for all eleven helpers.worker/src/validate.ts— catch anything thrown out ofvalidateBlockKitand return a structured 500, so an unanticipated throw is JSON with CORS and rate-limit headers rather than a bare runtime error. Documented as a500response in the OpenAPI spec.Three behavior changes reviewers should weigh:
errors[]no longer mixes schema and caveat-helper messages — schema errors come back on their own, and you re-run to see the cross-payload ones. Thevalidverdict is unchanged either way, since schema errors alone already made itfalse. Documented in the README's API reference.categoriesnow generates a tuple union intypes.generated.ts, as every other capped array in this schema already does, so a TypeScript consumer assigning a plainstring[]will need a typed literal. Kept as afix:(patch) deliberately: consumers on^0.1.xwould not pick up a minor bump, and nothing about the runtime verdict changes. Happy to re-tag it as breaking if you would rather have the signal than the automatic upgrade.dataarray is capped at 20 and must carry exactly one point per category, so more than 20 categories was never satisfiable — so no previously-valid payload changes verdict.One test payload was corrected rather than adapted:
worker/test/index.test.tsasserted the surface-compat rule using analertblock with atitlefield, which the schema rejects (alertrequirestext). It only passed because helpers ran on schema-invalid input; it now uses a validalertblock and exercises the rule it names.Testing
pnpm testpasses — 521 tests (6 new)pnpm typecheckpassespnpm lintpassespnpm validate-schemapasses (if the schema changed)test/covering the changeEach new test was run against the pre-fix source to confirm it fails there: the linearity guard (4,017 ms vs. a 1,000 ms budget), the per-series cap (24,999 errors vs. 101), the schema cap, the helper short-circuit, and the oversized-chart case (
RangeError).worker/tests pass too (34), including a new end-to-end check that a 25,000-category body returns a verdict instead of a 500.Schema changes
datalimit of 20 and the one-point-per-category ruleChecklist
README.mdGenerated by Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.