Product and Business

AI Is a Mirror

AI-generated code often looks more sophisticated than the thinking behind it. That does not mean the thinking has improved.

8 min read

I have been developing software since 1998, which means I have spent nearly three decades watching people find new and creative ways to build the wrong thing.

Artificial intelligence has changed the speed of that process. It has not necessarily changed the outcome.

The current sales pitch around AI-assisted development is that it gives everyone access to the capabilities of a software engineer. Describe what you want, wait a few moments, and receive working code. When the result is incomplete, ask for a revision. Continue until the application looks roughly like the idea you had in your head.

This works surprisingly often.

It also creates a dangerous misunderstanding about what software engineering actually is.

AI can generate formal, well-organized and technically plausible versions of a user’s instructions. It can introduce patterns the user does not know by name. It can explain its decisions with impressive confidence. It can produce in an afternoon what might have taken a developer several days.

But underneath all of that capability, AI often functions as a mirror.

It reflects the depth of the person directing it. It expands that person’s assumptions, communication style and understanding of the problem into code. It adds vocabulary, structure and speed, but it does not automatically repair a weak mental model.

If the person using it understands software architecture, data integrity, security, deployment, failure states and long-term maintenance, those concerns tend to appear in the work.

If they do not, the generated application may still look complete. It will simply contain a more polished version of everything they failed to consider.

That distinction is becoming increasingly visible.

A non-technical user might ask an AI system to add a feature. An experienced engineer is more likely to first ask what existing systems the feature affects, where its data should live, which rules must be enforced on the server, how permissions should work, what happens when part of the process fails and how the change will be tested.

The difference is not that one person knows the correct magic words.

The difference is that one of them knows the questions exist.

This is why two people can use the same model, in the same codebase, with access to the same tools, and produce dramatically different results. One appears to have hired a capable engineering team. The other has hired an extremely fast typist with no authority to question the assignment.

The AI is not necessarily worse in the second case. It is being directed through a narrower understanding of the work.

This is where “vibe coding” becomes more than a mildly insulting label.

At its most charitable, vibe coding is an exploratory way to make software. The user describes the desired result, evaluates what appears, reports what feels wrong and repeats the process. It can be useful for prototypes, experiments, internal tools and ideas that need to become tangible before they can be evaluated.

The problem begins when that method is mistaken for engineering.

Software rarely exists as a collection of isolated screens and features. A seemingly minor change can affect database relationships, background jobs, permissions, notifications, analytics, billing, caching, deployment or some assumption made elsewhere in the system years ago.

A person who does not understand those relationships will often prompt from the top down:

“Add this feature.”

“Make this button work.”

“Fix this error.”

“That didn’t work. Try something else.”

“Now the other page is broken.”

The AI responds locally because the user has framed the work locally. It addresses what is visible, waits for the next failure and then addresses that. Progress occurs through repeated correction rather than through an initial understanding of the system.

This can eventually produce working software. It can also produce 60 small commits where six deliberate ones would have been enough.

An engineer tends to approach the same work differently.

Before generating code, the engineer builds a model of the change. What is the intended behavior? Where is that behavior owned? What components depend on it? What invariants must remain true? Which layers need modification? What would prove the work is complete?

The deliverable is then broken into parts that follow the architecture of the application rather than the visible sequence of the user interface.

That decomposition matters more than prompt cleverness.

The strongest AI-assisted engineering rarely comes from one enormous, perfectly worded prompt. It comes from giving the model a well-defined problem, relevant context, explicit constraints and a sequence of work that can be verified at each stage.

The engineer is not asking the AI to discover the entire job while performing it. The engineer is reducing ambiguity before execution begins.

That changes the character of the output.

Instead of repeatedly patching symptoms, the model can work from causes. Instead of improvising a data structure while building the interface, it can begin with the domain model. Instead of discovering authorization requirements after exposing a route, it can treat access control as part of the original design. Instead of declaring success because a screen renders, it can verify behavior across the layers the feature touches.

The AI still makes mistakes. Experienced engineers do too. The difference is that the engineer has a framework for recognizing them.

This is an important limitation in nearly every conversation about AI and programming. Code generation is treated as though producing code was the central intellectual act of software development.

It is not.

The harder work is deciding what the system should do, forming an accurate model of how it should work, identifying what can go wrong and determining whether the result is actually correct.

AI can participate in all of those activities. It can challenge assumptions, inspect dependencies, propose designs, generate tests and identify risks. I use it for exactly that. In the hands of an experienced engineer, it is far more valuable as a reasoning partner than as an autocomplete system.

But it must be invited into that role.

If every prompt assumes the requested feature is correct, the architecture is irrelevant and visible output equals completion, the model will usually cooperate. It has been asked to produce, not to exercise judgment.

This creates an uncomfortable problem for non-technical founders and clients. The generated code can look authoritative enough to conceal the absence of engineering behind it.

There are folders with professional names. There are database migrations. There are automated tests, although no one may understand what they actually prove. The AI provides a confident summary explaining that the implementation is robust, scalable and production-ready.

The language of engineering is present.

Engineering judgment may not be.

That does not mean non-engineers should be excluded from building software. AI has made experimentation more accessible, and that is genuinely valuable. A founder can now test an idea before raising money or hiring a team. A domain expert can create a useful internal tool without waiting months for development resources. Someone learning to program can receive immediate explanations and examples that would once have been difficult to obtain.

The mistake is not using AI to work beyond your current knowledge.

That is one of its best uses.

The mistake is assuming that a plausible result proves there was nothing more to know.

AI can help someone cross a knowledge gap, but only if the gap is acknowledged. A user who asks, “What am I failing to consider?” will get a very different kind of assistance from one who asks only, “Can you make this work?”

The first question invites scrutiny. The second invites completion.

This difference also explains why AI is unlikely to make experienced engineers irrelevant anytime soon. It may reduce the value of manually producing routine code. It may eliminate some implementation work entirely. It will certainly allow smaller teams to accomplish more.

But as the volume and speed of generated code increase, judgment becomes more important, not less.

Someone still needs to decide whether the implementation fits the system. Someone needs to distinguish an architectural solution from a convenient patch. Someone needs to know when the model has misunderstood the domain, weakened a security boundary or created a maintenance problem that will not become visible for six months.

Most importantly, someone needs to define the work before an AI confidently solves the wrong version of it.

The divide ahead is not simply between people who use AI and people who do not. That argument is already obsolete.

The meaningful divide is between those who use AI to accelerate disciplined thinking and those who use it to avoid thinking they do not yet know how to do.

Both groups will produce software. Both will point to impressive gains in speed. Both may reach a functioning demo.

The difference appears later, when the application has real users, real data, changing requirements and consequences for being wrong.

By then, the mirror has already done its work.

AI does not merely generate what you ask for. It reveals how you understand the thing you asked it to build.

Sometimes it fills in the blanks.

Sometimes it decorates them.

Knowing the difference is still an engineering skill.