alirezan.dev
  • Home
  • Blog
  • Resume
  • Course
  • Home
  • Blog
  • Resume
  • Course

© 2026 AliReza Noori. All rights reserved.

Course/Observing Default AI Behaviour/Identifying Default Strengths and Gaps
Lesson 5Observing Default AI BehaviourFree Preview

Identifying Default Strengths and Gaps

Analyse your baseline results. What does Claude get right? What does it miss? And what would a senior developer have done differently?

What You'll Learn

Now that you've run the baseline, let's break down what happened. We'll look at what Claude gets right, and more importantly, the gaps that reveal why project configuration matters. These gaps are exactly what we'll fix in Section 3.

Note: The specific output you got might differ slightly from what's described below. That's normal with AI. But the patterns, the types of decisions Claude makes and the gaps it leaves, will be very similar.

What Claude Gets Right

Let's give credit where it's due. Claude nails the fundamentals:

  • Working infinite scroll. The feature functions correctly. Prompts load as you scroll, and it stops when there are no more.
  • Correct React patterns. Proper 'use client' directive, useEffect for the Intersection Observer, useState for managing state.
  • Proper TypeScript. Types for the prompt data, typed props, no any types.
  • The page stays a server component. Claude correctly identifies that only the list portion needs to be interactive and extracts a client component rather than converting the entire page.

If your only measure is "does the feature work?", Claude passes. The code is functional, readable and well-structured.

But working code isn't the bar. The bar is code that belongs in your project.

The Gaps

Here's what Claude got "wrong." Not wrong as in broken, but wrong in terms of scalability and best practices. We'll go through each one and explain why it matters, regardless of what tech stack you're coming from.

1. Hand-Rolled SVG Spinner Instead of Skeleton Loaders

Claude likely built a simple spinner, maybe a rotating circle with a CSS animation or a brief "Loading..." text. It works, but it's not what modern production apps do.

Why this matters: When content is loading, users experience less perceived wait time if they can see the shape of what's about to appear. That's what skeleton loaders do. They render grey placeholder blocks that match the layout of the cards. Apps like Instagram, LinkedIn, YouTube and Notion all use this pattern. A spinner tells the user "something is happening." A skeleton tells the user "here's what's coming." The UX difference is significant, and skeleton loaders are the standard across production web and mobile apps today.

With the right project rules, Claude would use a Skeleton component that matches the card dimensions instead of hand-rolling a spinner.

2. Zero Tests

Count the test files Claude created. The answer is almost certainly none.

Why this matters: Claude built a feature with pagination logic, an Intersection Observer, edge case handling (end of list, empty states), and shipped all of that with zero test coverage. Tests aren't just about catching bugs. They document how a feature is supposed to behave. When another developer (or Claude in a future session) modifies the infinite scroll logic, tests tell them if they've broken something.

A well-tested version of this feature would cover: Does the first batch render correctly? Does scrolling to the bottom trigger the next batch? What happens when there are fewer prompts than the page size? Does the "end of list" state appear at the right time?

Claude doesn't skip tests because it can't write them. It skips them because nothing in the project says they're required. No testing convention, no test framework set up, no rule that says "every feature needs tests."

3. No Design System

Your project has cn() from lib/utils.ts, which is the utility function that comes with shadcn/ui (a popular React component library). But no actual component library is installed. Claude saw that and just built everything from scratch with raw Tailwind classes.

Why this matters: Without a design system, every component Claude creates is a one-off. The card styles are inline. The spinner is hand-crafted. The end-of-list message is plain styled text. Over time, this means every feature introduces its own visual language. Cards in the infinite scroll list look slightly different from cards elsewhere. Buttons have inconsistent padding. Loading states look different on every page.

A design system solves this by providing shared, tested components (cards, buttons, inputs, skeletons) that enforce visual consistency. Here's the interesting part: a senior developer joining a project with no design system would raise the question before building. "Should we set up a component library first so everything is consistent?" Claude doesn't ask that question. It just builds with what's available.

4. Inconsistent Export Style

Claude might use export default function PromptList() in one file and export function PromptCard() in another, or it might use named exports everywhere. The specific choice varies between runs.

Why this matters: This one is more subtle, but it affects developer experience at scale. With default exports, you can import the component as any name: import Whatever from './PromptList'. That sounds flexible, but it means the same component can be referenced by different names across your codebase, making it harder to search for and refactor. Named exports enforce a single name: import { PromptList } from './PromptList'. The name is consistent everywhere, and your IDE can auto-complete and refactor it reliably.

Neither approach is "wrong," but most large-scale projects pick one and stick with it. Without a rule, Claude picks whatever feels natural in the moment, and that choice can change from session to session.

5. Hardcoded Color Values

Look at the Tailwind classes Claude used. You'll likely see border-zinc-800, bg-zinc-900, text-zinc-400 repeated throughout the component. These are raw color values.

Why this matters: Hardcoded color values work fine until you need to change your theme. If your project decides to shift from zinc to slate, or move to a custom palette, you'd need to find and replace every instance across every component. Semantic tokens solve this by abstracting colors into purpose-based names: bg-card instead of bg-zinc-900, text-muted-foreground instead of text-zinc-400. Change the token definition once, and every component updates automatically.

This is especially relevant if your project might support multiple themes, or if you want visual consistency as the codebase grows.

6. Didn't Use cn()

The project has a cn() utility in lib/utils.ts. It combines clsx (for conditional class logic) and tailwind-merge (for resolving conflicting Tailwind classes). Claude likely didn't use it anywhere in the new component.

Why this matters: cn() is the standard way to handle conditional or dynamic class names in Tailwind-based projects. Instead of writing template literals like `${isLoading ? 'opacity-50' : ''} border rounded-lg`, you'd write cn('border rounded-lg', isLoading && 'opacity-50'). It's cleaner, it handles edge cases (like conflicting Tailwind classes) and it's the pattern every other component in a well-configured project would use. When Claude doesn't pick up existing utilities, it creates inconsistency: some components use cn(), others use template literals, and a third batch might use ternary expressions inline.

7. Hardcoded Magic Numbers

Claude likely used values like PAGE_SIZE = 12 and a 300ms setTimeout delay right in the component. These aren't explained or configurable.

Why this matters: "Magic numbers" are literal values in code with no explanation of why they were chosen. Why 12? Why not 10, 15 or 20? Why 300ms and not 200ms? In a small project, this is fine. In a growing codebase, these values end up scattered across components with no central place to tune them. A better approach is to extract them into named constants with comments explaining the rationale, or centralise configuration values so they can be adjusted without hunting through component files.

The Senior Teammate Mental Model

Think of it this way: right now, Claude is like a skilled freelance developer who drops into your project for a day. They're fast, they write clean code, and the features they build work correctly. But they don't know your standards because nobody told them. They use a spinner because that's a safe default. They skip tests because they weren't asked for them. They don't suggest a design system because that's a team-level decision they don't feel empowered to make.

What I want to show you is how to change that. By giving Claude your project's memory, your standards, your conventions, you shift it from a competent stranger into a senior teammate. One that says "hey, should we set up shadcn/ui before building this?" or "I'll add skeleton loaders since that's our loading pattern" or "let me write tests using our AAA convention."

"But I Could Have Just Asked For All That"

You're right. You could write a prompt that says "use skeleton loaders, write tests, use named exports, use cn() for class names and don't hardcode color values." And Claude would follow those instructions perfectly.

But you'd have to write that prompt every time. Every new feature, every new session, every new file. You'd have to remember every convention, every pattern, every preference, and include it all before Claude starts working. Miss one, and Claude falls back to its own defaults.

The question isn't whether Claude can follow your standards. It's whether you can make it remember them so you don't have to.

What Comes Next

In Section 3, we'll create a CLAUDE.md file and start encoding these standards. Then we'll run the same infinite scrolling prompt again and compare the results. The feature will still work (it always did), but the decisions around loading patterns, test coverage, design system usage and code conventions will match your project every time.

Checkpoint: This is the end of Section 2. The checkpoint/s3 branch has the project in this exact state.

Previous:Testing AI Without Any ConfigurationNext:How Claude Remembers Your Project

On this page

What You'll LearnWhat Claude Gets RightThe Gaps1. Hand-Rolled SVG Spinner Instead of Skeleton Loaders2. Zero Tests3. No Design System4. Inconsistent Export Style5. Hardcoded Color Values6. Didn't Use `cn()`7. Hardcoded Magic NumbersThe Senior Teammate Mental Model"But I Could Have Just Asked For All That"What Comes Next