What AI-assisted development actually costs
This site was built mostly by a language model, with me reviewing. That part is unremarkable now. The part worth writing down is the list of things it got wrong, because every one of them was fluent, plausible, and would have shipped.
Every failure below is in this repository’s git history at the commit cited. None are illustrative.
A false claim about my own field
The site said my 2016 research on Probabilistic Latent Semantic Analysis was
a precursor to the Transformer architecture that powers today’s LLMs.
PLSA is a probabilistic topic model. Its descendants are LDA and distributional semantics. It has nothing to do with attention and it is not on any lineage that ends at an LLM. The honest through-line is latent structure in text, which is the same problem embeddings solve, and a smaller claim.
Two things make this the one I think about.
It propagated.
The same framing was baked into the default description prop in BaseLayout.astro, so it was in the page’s meta tag, and therefore in search results and link previews, not just the visible prose.
Then a later commit made it look better sourced.
d5a73fc lists as an improvement: “Link Probabilistic Latent Semantic Analysis to Wikipedia”.
A Wikipedia link was added to the false sentence.
The article does not support the claim.
The claim now had a footnote.
No linter catches this and the build was green. The only defence is someone who knows the subject well enough to notice that a plausible, adjacent, well-written sentence is not true.
A scale comparison chosen because the numbers matched
d5a73fc added two comparison bars under the National ANPR Service work:
| figure | bar width | |
|---|---|---|
| National ANPR Service | 90M / day | 95% |
| Instagram daily uploads | ~95M / day | 100% |
A number plate read is a small structured record arriving from fixed, wired infrastructure. An Instagram upload is a multi-megabyte media object from an untrusted mobile client, needing transcode, storage tiering and fan-out to a social graph. They are not the same unit of work. Putting them side by side at near-identical widths tells the reader the comparison was reverse-engineered from two numbers that happened to be close.
The same commit styled the Instagram bar in Instagram’s brand colours, which added an implied affiliation to a comparison that was already unearned.
8009096 replaced it with a neutral reference bar and a real peer: NHS Spine, at 43M structured messages a day.
Copy that sounds strong and reads wrong to a specialist
From the same paragraph:
No retries, no room for error.
Any distributed systems reviewer reads that as an admission the pipeline had no retry mechanism, which is a defect, not a boast. The true statement is narrower and more interesting. An individual read is unrepeatable, because the car has driven past, so a dropped read cannot be recovered by retrying it. The pipeline retries; the event does not.
A light-mode text hierarchy that was flat, and had never applied
Two independent bugs, stacked, both shipped.
The intended values were already broken:
html.light .text-heading { color: #171717; } /* 17.18:1 on #fafafa */
html.light .text-body { color: #171717; } /* the same value */
html.light .text-muted { color: #111827; } /* 17.00:1, within 1% of body */
html.light .text-subtle { color: #1f2937; } /* 14.06:1, the only real step */
Heading and body were the identical hex. The top three steps of a four-step ramp were visually one weight.
Worth being precise, because my first write-up of this got it backwards and called the ramp inverted.
It was not.
#111827 has slightly higher relative luminance than #171717, since the green channel is 24 against 23 and green carries 0.7152 of the luminance sum.
The defect was flatness, not inversion.
Then the second bug meant none of those rules ever ran.
--color-muted, --color-subtle and --color-accent were declared in Tailwind v4’s @theme block.
Tailwind emits a .text-muted utility for every --color-* token, into the utilities cascade layer, which outranks the @layer base where the hand-written override lived.
Light mode silently rendered the dark theme greys on a near-white background:
| class | as shipped | WCAG AA needs |
|---|---|---|
.text-muted |
2.42:1 | 4.5:1 |
.text-subtle |
3.31:1 | 4.5:1 |
.text-accent |
2.44:1 | 4.5:1 |
Three live accessibility failures, in the theme half the visitors get, from a naming collision between a design token and a utility class. Nothing warned, and the override was right there in the file when I reviewed it.
c898890 fixed it by moving the tokens into @theme inline pointing at theme-flipping custom properties, so both routes resolve to the same value.
An animation that assumed JavaScript, for the entire first screen
The intro was a full-viewport section.
Every element inside it carried an inline opacity: 0 and anime.js animated them in on load.
With JavaScript disabled, or the script erroring, the first full screen rendered blank. And because the zeroed opacity was an inline style, no stylesheet could undo it.
d93d10d inverted it: a js class set on <html> before paint, with reveal rules gated behind html.js inside a prefers-reduced-motion: no-preference block.
The readable static page is the default and the animation has to earn its way in.
Unverifiable numbers in display type
582,000+ person-hours, set at text-3xl font-bold in a gradient.
The work is real.
The figure is an internal estimate, and + on an estimate claims a floor nobody had established.
8009096 softened it and labelled it as estimated.
A 4.9 figure labelled “Trustpilot rating”, in display type with no review count.
The rating was not invented, it is still on tranzitt.co.uk.
The missing denominator was the problem: a 4.9 from nine reviews and a 4.9 from nine thousand are different claims.
8009096 removed it.
Neither was invented. Both came from things I said. A model will render any number you give it in the largest available font, and it cannot check provenance or precision.
CSS that reads correctly and does nothing
The 404 page used py-24 with justify-center on a section with no height of its own, so it centred its content inside its own content box.
A no-op that looks like centring in the diff.
8263c2e replaced it with min-height: calc(100dvh - var(--page-chrome)).
The same commit fixed a horizontal overflow.
The intro’s inline width: 100vw with margin-left: calc(-50vw + 50%), the standard full-bleed trick, overran the document by the scrollbar gutter width, because 100vw includes it and the percentage does not.
Decoration presented as design
An infinite background-position animation on a background-clip: text element.
It repainted a glyph mask every frame, on every page, forever, to convey nothing.
It was on the name in the h1, on the stat figures and on the timeline dots, as a three-stop blue to violet to pink gradient.
8009096 deleted the keyframes and cut the gradient to two stops with two sanctioned consumers.
Earlier rounds
The same review loop caught a 15-second scroll lock during the intro with no way to skip it, a header invisible on every page except the home page because its visibility was driven by the home page’s intro sequence, and scroll reveals built on anime.js’s onScroll that never fired for elements already in the viewport on load, leaving permanent blank voids.
That last one is why the site uses a native IntersectionObserver for reveals.
What I had to own
Factual accuracy. Nobody else could know the PLSA claim was false.
The job title. The generated copy invented an inflated one. Two grand-sounding words, neither on my contract.
Deletion. Two sentences had to go: “Most of my code lives in private repos”, which reads as an excuse for an empty GitHub profile, and “I probably should’ve stuck with research”, which undercuts the career the page then spends 800 words describing.
The first one came back, reworded: “rather than keep a shelf of my own demo repos, I push patterns and fixes into the AWS repositories other people already build on”. Same excuse, better prose. A near-verbatim copy had also reached a second file with a paragraph defending it.
Accessibility verification. Every contrast failure above passed the build. Ratios have to be computed against the composited background, alpha-blending tinted surfaces over the base first. They live as comments beside each token so the next change has to confront them.
Judgement about evidence. That “estimated” is the honest word, that a review score without a denominator is not a fact, and that a flattering comparison is worth less than no comparison.
Where this leaves me
Everything above was caught by reading the diff. The repo now has two automated gates, a type check and a Content-Security-Policy check, and neither would have caught any of these.
I still build this way, and I read every line.