Senior Frontend Engineer at Horsefly Analytics, Manchester. I build production TypeScript and React for a labour-market analytics platform, and I work on the AWS and AI-agent infrastructure behind it.
Most of what I enjoy is the unglamorous half: parser edge cases, streaming protocols, the failure modes that only show up under real input.
I contribute fixes upstream to packages the JavaScript ecosystem depends on. Each one below is a bug I reproduced, traced to a root cause, and verified against a differential build before opening.
Merged and released
benjamn/recast#1438— comment tokens sit inloc.tokensfor the babel, TypeScript and flow parsers but not for esprima or acorn, so a comment between a node and its enclosing parenthesis hid the parenthesis fromhasParens()and the printer emitted a second pair. This broke recast's own documentedprint(parse(source)) === sourceidentity on unmodified source. Verified across 5,076 generated inputs: 2,540 identity violations fixed, zero regressions. Shipped in0.23.20— a package with ~139M downloads a month.postcss/postcss-selector-parser#330— unclosed[,(and a trailing|threw a rawTypeErrorinstead of the parser's own error. Shipped in7.1.5— ~574M downloads a month.corsairdev/corsair#111— improved the Telegram integration plugin.
In review
Shopify/flash-list#2444—initialScrollIndexopened the list on the wrong item whenever rows were taller than the 200px the layout manager seeds unmeasured items with. The corrective pass recomputes positions only as far as the target index, so the rows after it keep positions derived from the stale estimate and the layout array stops being sorted at that boundary. The binary search over it then returns an item nowhere near the one asked for: requesting index 250 renders item 333. Reproduced in the project's own Jest harness, then fixed in one line plus a regression test that fails onmain. A P1 open since June, in a package with ~7M downloads a month.expo/expo#48960— a rule foreslint-plugin-expocatching credentials held inEXPO_PUBLIC_environment variables, which are inlined into the app bundle in plain text and readable by anyone with the app. Matches on_separated name segments rather than substrings, soEXPO_PUBLIC_AUTHORITYandEXPO_PUBLIC_MONKEYstay quiet.shadcn-ui/ui#11463— the Tailwind prefix transform rebuilt class literals as quoted source text by hand, so the step that stripped the delimiters also stripped every quote inside the class value.[stroke='#fff']became[stroke=#fff], which is not valid CSS, so the browser silently discarded the rule. Found by reading the CLI source rather than from a bug report. Verified over all 1,435 registry components: 1,404 byte-identical, zero regressions.postcss/postcss-selector-parser#335— attribute selectors with no valid attribute name threw a rawTypeError, or emitted the literal stringundefinedinto CSS. Verified against 43,200 generated selectors.postcss/postcss-selector-parser#336—$dropped from attribute names, breaking Sass interpolation like[#{$attr}].postcss/postcss-selector-parser#337— lossless mode dropped trailing whitespace when a selector ended before any node was created.- Fixes also pending review in
hast-util-from-parse5,eslint-plugin-import,stacktrace-parser,xml-jsandeslint-plugin-react-native.
- llm-guard — validating and securing LLM prompts, in TypeScript.
- react-native-virtual-list — a virtualized
list that holds its scroll position when item heights are only known after they render. Item offsets are
prefix sums over a Fenwick tree rather than a position array that gets partially rebuilt, which makes
them non-decreasing by construction and rules out the class of bug above. Aimed at react-native-web,
which does not implement
maintainVisibleContentPositionat all. - bedrock-ui-stream — bridges AWS Bedrock Agent Runtime event streams to the Vercel AI SDK UI message stream protocol. Handles the parts that bite: chunk boundaries splitting multi-byte characters, partial tool-call state, and redaction on the error path.
Lint rules for React Native. General JavaScript linters do not know what a WebView is, that
AsyncStorage writes to disk unencrypted, or that a FlatList inside a ScrollView renders every row.
These cover only that gap, and each rule is statically detectable rather than heuristic where it can be.
- eslint-plugin-rn-security — unsafe
WebViewconfiguration, credentials inAsyncStorage, cleartext endpoints, unvalidated deep links, and credentials written to the device log. - eslint-plugin-react-native-performance
— nested virtualized lists, lists inside a
ScrollView, missing and index-basedkeyExtractor, and props rebuilt inline on every render. - eslint-plugin-react-native-platform
— platform-specific APIs called without a guard, styles that only exist on one platform, and
Platform.selectwith keys that will never match.
- MSc Computer Science, Manchester Metropolitan University
- AWS Certified Solutions Architect
- Cyber Runway Launch 2024 — the DSIT-funded national cyber accelerator delivered by Plexal
- Volunteer mentor, Manchester Metropolitan University
TypeScript · JavaScript · React · Next.js · Node.js · AWS · PostgreSQL · Docker · Terraform




