Writing / AI Engineering
Can copy-paste beat code generation?
Copying vetted source worked best overall. It still failed where components were connected, and every dashboard broke on mobile.
Article
Imagine asking an AI coding agent to build an admin screen. The screen needs familiar parts: buttons, form fields, menus, tables and dialogs. Good versions of those parts already exist in component libraries.
The practical question is simple: should the agent write those parts again, or copy the tested source and spend its effort on the product-specific work?
Executive summary
Copying vetted source worked best overall
Arm C built successfully in every matched run, wrote the least custom application code and had the fewest accessibility findings.
Generating the interface directly came second
Arm A also built reliably and often looked more polished, but it repeated more control logic and missed more interaction edge cases.
Reading the documentation and rewriting the components worked worst
Arm B failed to build in five of 16 runs and cost more than either alternative.
Copying was not a complete solution
Correct components still lost behaviour when the application connected them incorrectly, and every dashboard broke at a phone-sized viewport.
These findings come from 88 runs across eight interface tasks, followed by a manual production pass over 48 matched high-reasoning outputs. The experiment used one inexpensive model, so the ranking is evidence from this study rather than a universal law.
Three ways to build the same interface
A component is a reusable interface part, such as a button or dialog. An API is its rulebook: the names, options and behaviours that other code must use correctly.
The experiment compared three ways an agent could work:
Arm A — generate everything
The model writes the interface directly, mostly with ordinary HTML elements and styling.
Arm B — retrieve, then generate
The model reads the real component documentation, but rewrites the component source itself.
Arm C — copy, then compose
A tool copies the vetted component files exactly; the model writes only the application code that connects them.
I use materialisation to mean that last copying step: the component becomes a real project file without passing through model output. I use primitive for a foundational component such as a button, input, dialog or menu.
Figure 1
Three source paths create two useful comparisons
The model, task prompt and component-free starting template stayed the same.
Model weights → all interface source
Registry API → model recreates every line
Registry source → CLI copies components; model writes the glue
The first version of this study compared only arms A and C. That made the result look like a tie: arm A had no external component rulebook to break, while arm C received the correct files intact. Neither approach asked the model to recreate a known component API.
Arm B supplied the missing test. It gave the model the correct documentation and then asked it to rewrite the component. Five of 16 projects failed to build. The earlier study had not disproved the predicted failure; it had omitted the condition that could produce it.
The two useful comparisons are therefore different. A versus B asks whether reading the correct API helps. B versus C asks whether copying the implementation is safer than recreating it. Only the second comparison isolates the benefit of materialisation.
Every run used DeepSeek-V4-Flash, the same task, and the same empty starting project. Both registry arms could search and read the component documentation. Only arm C could install the source directly.
A successful build is not the same as a working product
The compiler is the software that translates and checks source code before it ships. It catches broken imports and invalid types, but it cannot tell whether a menu ignores Escape or whether keyboard focus disappears after a dialog closes.
The experiment therefore checked each result in four ways:
Build check
Can the project compile and ship?
Browser test
Do its menus, forms and dialogs behave correctly when someone uses them?
Accessibility scan
Does an automated check find serious barriers for disabled users?
Composition audit
Did the application connect reusable components in the way needed to preserve their built-in behaviour?
That last boundary is the composition seam: the point where a correct reusable component meets product-specific application code. A dialog can contain the right focus behaviour and still lose it if the application fails to identify which button opened the dialog.
Evidence table
What each scoring layer can see
A green result in one layer does not imply a shippable or usable product.
| Layer | Runs flagged | Catches | Cannot see |
|---|---|---|---|
| Compiler + production build | 5 | Broken imports, invented properties and wrong API shapes | Anything that compiles |
| Browser behaviour | 7 | Dismissal, focus and state across routes | Type errors when the development server still opens |
| Accessibility scan | 9 | Serious and critical rule violations | Broken behaviour outside the rules |
| Composition audit | 10 | Useful primitive behaviour discarded by the connecting code | Interfaces without a Radix-backed primitive |
The disagreements between these layers matter. One arm B command-palette run failed the compiler with two errors and still passed all five browser checks. The development server could display it, but a production build could not ship it. A browser-only score would have called that run perfect.
The scorers were tested before the experiment. A hand-built reference had to pass cleanly, while a deliberately broken version had to fail only the checks tied to its injected defects. On the 15-check dashboard, the reference scored 15 of 15 and the negative control scored 12 of 15, missing exactly the three intended checks.
Missing evidence was also kept separate from a clean result. If an accessibility bundle was absent, the run reported “not run”, not zero violations. One of the 88 runs was not runtime-scored, so runtime conclusions use the 87 runs with browser evidence.
Where the failures appeared
Most runs worked. The failures were concentrated enough to show in one ledger rather than being scattered across everything the model wrote.
Figure 2
The complete defect ledger
88 runs across 8 tasks. Counts are defects or failed checks, not affected runs.
| Task | Arm A · Generate | Arm B · Retrieve | Arm C · Materialise |
|---|---|---|---|
| Login form | No defect found | Not run | No defect found |
| Users table | 1 contrast · 4 behaviour | 2 build · 1 contrast | 2 contrast |
| Kanban board | No defect found | Not run | No defect found |
| Command palette | No defect found | 2 build · 1 behaviour · 1 seam | No defect found |
| Variant matrix | No defect found | Not run | No defect found |
| Form errors | No defect found | No defect found | No defect found |
| Compound state | 11 contrast | 1 build · 6 contrast · 4 seam | 4 seam |
| Dashboard | 1 behaviour | No defect found | 1 behaviour · 1 seam |
Five runs failed to build, all in arm B. Seven runs failed at least one automated browser behaviour. Nine runs had serious or critical accessibility findings; every one was a colour-contrast issue. The composition audit found ten cases where application code discarded behaviour that a reusable component already provided.
Arm A’s browser failures were mainly the users-table menu at lower reasoning effort, plus one dashboard focus failure. Arm C had one dashboard focus failure. Arm B added one command-palette focus failure even when its project could otherwise run.
Rewriting known components broke five builds
The original prediction was specific: a model that rewrites an existing library component will sometimes invent a reference or use the library incorrectly. Arm B created exactly that condition, and five of its 16 projects failed to build.
Evidence table
Every build failure in the programme
All five came from arm B: the path that knew the real API and tried to reproduce its source.
| Run | Compiler codes | What went wrong |
|---|---|---|
| c1_command_palette · seed s2 | TS2769 | Passed an onClick property to a component that does not accept it. |
| c1_command_palette · seed s3 | TS2604 / TS2786 | Used Slot as a component when the real component is Slot.Root. |
| c4_compound_state · seed s4 | TS2604 / TS2786 | Repeated the same Slot namespace mistake on another task. |
| t3_users_table · seed s1 | TS2307 ×2 | Imported two module paths that do not exist. |
| t3_users_table · seed s4 | TS2307 ×2 | Imported two different module paths that do not exist; this previously unscored run was recovered and rebuilt during the manual audit. |
Arm A avoided these particular mistakes by using ordinary interface elements instead of the library. Arm C avoided them by copying the real component files. Only arm B knew what the component should be and still tried to recreate it from model output.
The fifth failure was not a new run. The original snapshot had no build result for one users-table output. Recovering its final source from the trace and rebuilding it revealed two library import paths that did not exist, the same failure that had already broken another arm B table. The earlier count of four therefore understated the production result by one.
The automated error label also missed two of the five failures. It counted references to things that did not exist, but not incorrect uses of names that did exist. The compiler codes and exact causes remain in the evidence table above.
Arm B could also drift from the documented component without breaking the build. On the task combining tabs, a table and a dialog, three of four arm B runs either omitted or misused a documented button behaviour.
The correction to the earlier article is direct: “zero hallucination errors across 70 runs” did not show that the mechanism was absent. It showed that the experiment had no condition in which it could appear.
Reading the documentation was not enough
Knowing the correct API was not enough. On every task completed by all three arms, arm B cost roughly twice as much as arm A, around three times as much as arm C, wrote the most source and was the only arm that failed to build.
Figure 3
Cost and model-authored code
Mean per completed high-effort run on the five tasks tested in all three arms.
Cost per run · USD
Lines written by the model
Moving from B to C reversed both effects. Spend returned to roughly arm A’s level and the build failures disappeared. The useful step was not giving the model more information. It was removing the need to rewrite source that already existed.
The task combining tabs, a table and a dialog makes the difference clear. Arm B wrote 836 lines. Arm C wrote 258 lines and copied the existing components. Reading the API made the model do more work; copying removed that unnecessary work from model output.
This does not mean tools or retrieval are intrinsically expensive. It means this particular workflow asked the model to study an API and then rebuild an implementation that already existed. The extra turns and source generation bought no measured improvement.
Copying reduced AI-written code, not repository size
“Copy-paste writes less code” is only true if code means lines emitted by the model.
Arm C wrote fewer application lines, but its copied component files still became part of the shipped project. When those files are counted, arm C carried between 1.46 and 2.26 times arm A’s total source on the five tasks shared by all three arms.
Figure 4
Lines of code that actually ship
Solid segments were written by the model. Hatched segments were copied verbatim from the registry.
Users table
Command palette
Form errors
Compound state
Dashboard
On the dashboard, arm A wrote six buttons directly inside the page. Arm C copied one reusable Button component containing its visual variants, keyboard-focus styling, disabled states and error states. Arm A did not create a smaller version of that component; it created no reusable button layer at all.
Arm B shows what that layer costs when a model writes it. Its totals landed close to arm C: 836 versus 922 lines on compound state, and 673 versus 736 on the users table. The layer is similar in size whoever produces it. The important difference is that five of 16 arm B runs did not compile.
The source shape changed too. Arm A usually concentrated work in one file and repeated raw elements and styling strings. Arms B and C had a component layer and far less repetition.
Evidence table
Shape of the model-authored source
All completed runs. The A-to-B/C comparison is confounded by arm A’s instruction to build the interface directly.
| Arm | Runs | Files / run | Runs with components folder | Raw buttons + inputs | Repeated class strings / 100 lines |
|---|---|---|---|---|---|
| Arm A · Generate | 36 | 1.4 | 5 of 36 | 4.8 | 1.63 |
| Arm B · Retrieve | 16 | 6.5 | 16 of 16 | 1.8 | 0.18 |
| Arm C · Materialise | 36 | 1.2 | 1 of 36 | 0.1 | 0.12 |
This comparison has an important caveat. Arm A was told to “build the UI directly”, while the other arms were told where component files belong. The prompt itself nudged arm A towards page-specific code, so the difference reflects both architecture and instruction wording.
The B-to-C contrast survives this problem. Both arms were told to create the same layer and differed only in who wrote it. Their structural shape was similar; their build correctness was not.
Copied components bring behaviour with them
Copied source brought two useful things that the original hypothesis barely mentioned: colour choices and behaviour that the prompt never had to request.
The automated accessibility result was narrower than accessibility as a whole. Every serious or critical finding concerned colour contrast. The scan found no missing label or other structural barrier.
Evidence table
Serious and critical accessibility findings
High-effort runs. Every finding was colour contrast; the scan found no structural accessibility failure.
| Task | Arm A | Arm B | Arm C |
|---|---|---|---|
| Compound state | 11 | 6 | 0 |
| Users table | 1 | 1 | 2 |
| All other tasks | 0 | 0 | 0 |
| Total | 12 | 7 | 2 |
The total favours arm C, but one task explains most of the difference. The tabs-and-dialog task produced 11 contrast findings in arm A, six in arm B and none in arm C. On the users table, arm C had two findings while both other arms had one. Copying did not eliminate contrast problems; it carried a better colour palette on one influential task.
The behaviour result is sharper. The users-table prompt never asked for its row menu to close on Escape. Arm A had to remember to add that behaviour. Arm C inherited it from the copied dropdown component.
Figure 5
Escape-to-close when the prompt never asked for it
Users-table task, split by reasoning effort. The test opens a row menu, presses Escape and checks that it closes.
No extra reasoning
High reasoning effort
Without extra reasoning, arm A failed all three runs while arm C passed all three. At high reasoning effort, arm A passed three of four and arm C passed all four. Arm A’s correctness depended on its reasoning budget. Arm C’s did not; the behaviour arrived in the file.
This is the clearest benefit of copying in the experiment. Mature source can carry behaviour that nobody remembered to request.
Correct components can still be connected incorrectly
Copied behaviour only helps when the application connects it correctly.
The copied dialog knows how to return keyboard focus to the button that opened it. But the application still has to identify that button as the opener. When it does not, the behaviour remains in the component file but never reaches the user.
The composition audit looked for that broken connection wherever the same dialog library appeared, whether arm C had copied it or arm B had recreated it. Looking only at copied files would have missed four of the ten findings.
Evidence table
Orphaned triggers in Radix-backed interfaces
46 of 88 runs were eligible for this audit.
| Task | Eligible runs | Flagged | Where the finding appeared |
|---|---|---|---|
| Compound state | 8 | 8 | 4 arm B + 4 arm C · every eligible run |
| Dashboard | 2 | 1 | Arm C; arm B connected the trigger |
| Command palette | 7 | 1 | Arm B; one arm C run repaired focus manually |
| Other five tasks | 29 | 0 | No finding |
| Total | 46 | 10 · 21.7% | Arm A had no eligible Radix-backed interface |
Ten of 46 eligible runs failed to connect the opening button correctly, or 21.7%. Eight came from the task combining tabs, a table and a dialog: every arm B and arm C run. The defect was not peculiar to copying. It appeared whenever the interface made the dialog awkward to connect.
In that task, the dialog lived in one part of the page while its opening button lived inside a tab. Connecting them properly required rearranging the page structure. Every eligible run took the easier shortcut and controlled the dialog separately.
I tested this explanation with one controlled change. I connected arm C’s existing dashboard button through the dialog’s intended opening mechanism and changed nothing else. The browser score moved from 14 of 15 to 15 of 15, and focus returned to the button. The same structural repair fixed the tabs-and-dialog task.
The compiler, production build and accessibility scan could not see this defect. The application built, opened without an error and reported no serious accessibility violation. More documentation was not an obvious fix either: the model had read the relevant dialog guidance and used the equivalent menu connection correctly elsewhere.
No component library can supply all of the product-specific code that connects its parts.
What the three approaches looked like
The compound-state task is where the argument becomes visible. It combines tabs, a filterable table and a dialog that writes into the list behind it. All three arms passed the five specified browser checks. All three lost focus to the document after the dialog closed: arm A because it never implemented restoration, arms B and C because they had the underlying behaviour and routed around it.



The three results converge visually despite taking different source paths. Arm A uses a native selector and more local presentational decisions. Arms B and C share the structure of a component system.



The original arm A and arm C compound-state workspaces had been pruned. These captures come from workspaces rebuilt from durable experiment artefacts: generated files replayed from traces and copied files restored from the pinned registry snapshot. They are faithful reconstructions, not byte-identical originals.
At application scale, the same convergence appears in a four-route operations dashboard. Arm A adds pill badges, chart gridlines and stronger local visual decisions. Arm C inherits structural details such as a close button that the prompt never requested. Arm B builds a similar layer itself at greater cost.



Copied components supplied structure. The model still made the presentation choices. Neither result was uniformly better, and the experiment did not score visual quality.



All three dashboard menus opened and closed on Escape. The earlier users-table gap did not become a universal law. The model could remember dismissal in a larger task; the copied component carried it without depending on that recall.
Manual browser testing made the ranking clearer
The automated checks answer narrow questions consistently. They do not tell you what it feels like to put every result through a release pass. I therefore reconstructed and browser-tested all 48 matched high-reasoning outputs: 16 per arm across command palettes, forms, users tables, compound workspaces and dashboards. I also checked one representative result from every arm and task at a 390-pixel viewport.
This was follow-up quality assurance, not another experimental arm. It preserved the original tasks and outputs, then tested production builds, full workflows, focus, empty states, persistence and responsive behaviour. The line counts below use clean final source, exclude the unchanged project template and count copied files as shipped code even when the model did not author them.
Evidence table
What the manual production pass changed
Forty-eight reconstructed high-effort outputs: 16 per arm across the five shared tasks, plus one representative 390px check for every arm and task.
| Measure | Arm A · generate | Arm B · retrieve | Arm C · copy |
|---|---|---|---|
| Production builds | 16 of 16 | 11 of 16 | 16 of 16 |
| Application / glue lines per run | 358 | 239 | 237 |
| Primitive lines per run | 0 | 412 · model-written | 450 · copied |
| Total shipped lines per run | 358 | 652 | 688 |
| Dashboard at 390px | Not usable | Not usable | Not usable |
| Practical ranking | Second | Third | First |
The ranking is arm C first, arm A second and arm B third. Arm C built every output, kept application logic separate from reusable components and recorded the lowest accessibility defect count. Arm A also built every output and was often the most visually polished, but its custom controls produced inconsistent edge cases. Arm B hand-wrote an entire reusable layer and failed to build in five runs, including two users tables that could not render because their generated component files imported paths that did not exist.
The individual tasks add useful context:
Command palettes
All 12 opened, kept keyboard focus inside, filtered results and closed with Escape. One arm B palette did not return focus to its opening button.
Forms
All nine showed errors, described them correctly, focused the first invalid field and submitted. This task barely distinguished the approaches.
Users tables
Arms A and C completed sorting, search, paging, menus and deletion. One arm A menu did not close with Escape. Two arm B tables could not render because their generated component files imported paths that did not exist.
Tabbed workspaces
All 12 preserved filters, new tasks, project names and state across tabs. Every dialog still returned focus to the page body instead of its opening button.
The dashboards expose a shared failure that the desktop screenshots hide. All three completed the requested filters, menus, incident flow, routing and settings persistence. At 390 pixels, all three kept a 256-pixel sidebar, squeezed the main content to roughly 119 pixels and forced horizontal scrolling. Forms and command palettes fitted; wide tables were mixed, with arm B and arm C containing them inside scroll regions while one representative arm A table widened the whole page. Copying a component layer improved consistency, but it did not supply an application-level responsive strategy.
The source totals sharpen the wording around code volume. Arm A averaged 358 application lines. Arm C averaged 237 application lines plus 450 copied component lines, or 688 shipped lines in total. Arm A therefore wrote about 121 additional custom application lines per run compared with arm C, but arm C still shipped the larger repository. The defensible claim is that copy-paste reduces model-authored custom code, not total code.
The experiment also exposed measurement problems
The test harness—the software that ran and scored each project—can create misleading results of its own. Six defects had already distorted a reported number before they were found and fixed.
Evidence table
Six defects found in the measuring apparatus
Each had already distorted a reported number before it was corrected.
| Defect | What it distorted |
|---|---|
| Reasoning echo counted as re-sent context | Made 23.9% of input tokens look wasteful even though the endpoint resolved that reasoning server-side at no charge. |
| The token guard counted cheap cache hits | Stopped a tool-heavy arm C run at 627,000 tokens after only $0.016 of spend. |
| Task time-outs were parsed but not used | Silently ignored the longer time budgets declared by two tasks. |
| A forced rerun appended to an old trace | Could restore files from a previous run even when the manifest described only the new one. |
| Run workspaces were not durable | Left 70 of 88 runs without a complete workspace; generated source was recoverable, copied files had to be rebuilt. |
| A cheap re-score erased browser results | Running the scorer without its runtime flag rebuilt the payload and deleted previously collected browser evidence. |
There was also a separate seventh issue in the experimental prompt: arm A’s instruction to build the interface “directly”. It did not corrupt a stored number, but it made the code-shape comparison partly a prompt comparison. I keep it separate from the six apparatus defects because the remedy is a new experimental arm, not a scorer repair.
Two earlier checks failed for a more basic reason: they inspected implementation shape instead of observable behaviour. One demanded a particular input element and marked a correct component as broken. Another read an attribute and marked a menu as working without ever opening it. The final checks drive the interface and require a visible state change.
The accounting also needs context. Dependencies were prewarmed, so installation cost was not measured. High-reasoning runs used three or four seeds per task, and three tasks also received three no-extra-reasoning seeds. Reasoning mode removed temperature control, so variation was observed rather than suppressed; within one arm and task, token use varied by roughly three times.
All figures on this page are derived from the experiment data rather than typed as claims. The local data snapshot records 88 runs, 70 high-reasoning runs, 87 runtime-scored runs and total model spend of $0.3844.
What we can say with confidence
The failure mechanism is real, but narrow. Recreating source against a known external API produced invented imports, an invented property and incorrect API usage in five of 16 arm B runs. Copying removed that failure class because the model no longer rewrote those lines. Arm A avoided the same risk by not using the library at all.
Retrieval alone bought nothing measurable here. It increased spend and model-authored source while producing every build failure in the corpus. The benefit came when the source file arrived intact.
Copy-paste was not the smallest repository. It produced the least model-authored code and shipped the most total code. Those extra copied lines carried reusable states, styling and behaviour that arm A often did not build as a component layer.
The defect that survived was the seam. More than one in five eligible runs discarded behaviour that was already available, and every eligible run failed the composition audit on the task designed to stress that connection. Correct source does not guarantee correct composition.
What this experiment cannot prove
This remains a small experiment: one inexpensive model, eight tasks and three or four seeds in each high-effort cell. Arm B covered five of the eight tasks. The accessibility difference is driven mainly by one task. The scorer did not measure visual design quality, screen-reader experience, maintenance after later edits or dependency-install cost.
The most valuable next test is a much stronger frontier model. It may reproduce the library rules correctly, removing arm B’s build-failure gap. That would narrow the case for copying even further.
The next structural test is a neutral version of arm A, called arm A′: build the interface and extract reusable components where useful. That would show whether arm A’s page-specific style was the model’s preference or a consequence of the prompt.
After that comes a blocks-based version of arm C. Instead of copying isolated buttons, menus and dialogs, it would copy larger interface sections that already connect those parts. The question is whether moving more connecting code into the registry reduces the 21.7% seam rate or merely moves the next seam elsewhere.
So, can copy-paste beat code generation?
Copy-paste can beat code generation, but not because generation is generally unreliable. Arm A completed 36 runs, including 27 at high reasoning effort, without a build failure.
It wins when the alternative is asking a model to recreate a specific external API from memory. It can also carry a palette and behaviours that nobody remembered to request. Those advantages end where the application must connect the pieces.
The honest answer is therefore conditional: copy the parts whose contracts already exist, generate the product-specific work, and test the seam between them as seriously as the component itself.
Design to code needs a contract, not a better prompt.
AI can write convincing UI with the wrong design system. The reliable path is to retrieve the exact contract, compose only what is needed, and verify the result.
Workflow design: turning templates into leverage.
How a well-organised template library makes recurring work faster, clearer, and easier to improve.
Design reviews as a delivery mechanism.
Treat reviews as a repeatable decision system that creates momentum, clarity, and a visible record of what changes next.
Planning and prioritisation as a two-level design system.
A practical system for connecting product bets with the delivery decisions that keep design work moving.