The Drift Series Part Two: 12 Failures Users Will See
Part 1 covered five drift examples with room to breathe. This one highlights 12 more findings across reliability, security, and accessibility that users will notice and feel.
Jonathan Gordon
•

Part 2 of The Drift Series. Part 1 covered five drift examples with room to breathe. This one highlights 12 more findings across reliability, security, and accessibility that users will notice and feel.
Key findings
ReWeaver AI’s DriftDetector scanned 20 open-source repositories across nine production-readiness dimensions, and researchers hand-reviewed a sample of the resulting findings by reading the actual code. This post covers 12 of those findings, grouped by the dimension they broke: five reliability bugs where a failure is caught and then discarded or reported as success, two security issues involving a guessable file name and an overly permissive CORS header, and five accessibility issues that affect keyboard and screen-reader users.
Finding | Repo Type | Domain | Severity |
|---|---|---|---|
| Docs-and-whiteboard workspace | Reliability | Warning |
| Full-stack Vue framework | Reliability | Warning |
| Docs-and-whiteboard workspace | Reliability | Warning |
| Docs-and-whiteboard workspace | Reliability | Warning |
| Self-hostable backend platform | Reliability | Warning |
| Cross-platform API client | Security | Warning |
| Team knowledge base | Security | Critical |
| LLM knowledge base platform | Accessibility | Warning |
| Cross-platform API client | Accessibility | Warning |
| Link attribution and short-link platform | Accessibility | Warning |
| LLM knowledge base platform | Accessibility | Warning |
| Full-stack Vue framework | Accessibility | Warning |
Reliability: The application misbehaves and nothing says so.
Each of these is a path where a failure is caught and then discarded, or reported as success.
1. A job queue whose return handler is an empty function
REPO: Docs-and-whiteboard workspace
DETECTORS: error-masking-catchall
SEVERITY: Warning
THE FINDING IN ONE LINE: A job queue’s completion handler is an empty arrow function, so whatever the queue was meant to do on return — retry, record, release — silently never happens.
150 this.logger.error(Queue Worker [${queue}] error, error);
151 });
152
153 worker.on('completed', (job, result) => {
154 this.handleJobReturn(job, result).catch(() => {
155 /* noop */
156 });
157 });
158
159 this.logger.log(
WHAT IT COSTS: A job’s completion handling is discarded by an empty arrow function. Whatever the queue was meant to do on return — retry, record, release — doesn’t happen, and the queue reports nothing.
2. Every cache write can fail, and the function still returns true
REPO: Full-stack Vue framework
DETECTORS: catch-warn-no-throw, error-masking-catchall
SEVERITY: Warning
THE FINDING, IN ONE LINE: A build cache’s write path swallows every error and closes the file handle regardless of outcome, so a failed write is indistinguishable from a successful one.
304 }
305
306 fd = await open(filePath, 'w')
307 await fd.writeFile(file.data!)
308 } catch (err) {
309 console.error(err)
310 } finally {
311 await fd?.close()
312 }
313 }
Caches are supposed to be safe to get wrong: if the write fails, you just do the work again. That only holds if the caller finds out the write failed. This is the build cache in a major framework.
WHAT HAPPENS: Every write is wrapped so a failure can’t escape. The error is printed, the file handle is closed, and the function returns normally regardless of whether the write actually succeeded.
WHAT IT COSTS: The caller is told the cache was written. Its next run trusts a cache that may be empty or partial, and the only evidence is a line in a console nobody is reading during a build.
3. Import failures become console warnings
REPO: Docs-and-whiteboard workspace
DETECTORS: success-false-return
SEVERITY: Warning
THE FINDING IN ONE LINE: A blob-import routine logs failures to the console and returns { success: false } instead of throwing, so a caller that doesn’t inspect the result sees a completed import that silently dropped files.
58 }
59 return { success: false, blobId, error: 'Blob not found' };
60 } catch (error) {
61 console.error(Failed to process blob: ${blobId}, error);
62 return { success: false, blobId, error };
63 }
64 })
65 );
66
67 const failures = results.filter(r => !r.success);
WHAT HAPPENS: Each imported blob is processed inside a try/catch; on failure the code logs to console.error and returns { success: false, ... } instead of throwing. The results are later filtered for failures — but nothing requires the caller to actually check that array.
WHAT IT COSTS: A user imports a document and some of its images are silently absent. The failure is a value the caller may not inspect, and a warning in a console the user will never open.
4. A desktop app that overwrites your state file in place
REPO: Docs-and-whiteboard workspace
DETECTORS: non-atomic-file-write
SEVERITY: Warning
THE FINDING, IN ONE LINE: A desktop app’s save path writes directly to the state file with no write-to-temp-then-rename step, so an interrupted write can leave the file truncated or empty.
114 debounceTime(1000),
115 exhaustMapWithTrailing(() => {
116 return fromPromise(async () => {
117 try {
118 await fs.promises.writeFile(
119 this.filepath,
120 JSON.stringify(this.data, null, 2),
121 'utf-8'
122 );
123 } catch (err) {
WHAT HAPPENS: The save path writes straight to this.filepath — no write-to-temp-file-then-rename, no atomic swap. If the process is killed, the machine loses power, or the write is interrupted for any reason partway through, the original file has already been truncated, and the new content never finished landing.
WHAT IT COSTS: A save that’s interrupted at the wrong moment doesn’t fail loudly — it leaves the state file half-written or empty. The next launch loads a corrupted or missing file, with nothing in between to explain why.
5. An install stream started and never awaited
REPO: Self-hostable backend platform for web and mobile apps
DETECTORS: fire-and-forget-async
SEVERITY: Warning
THE FINDING, IN ONE LINE: A retry handler fires an async install-stream call without await as its last statement, so a rejection has no handler left in scope to receive it.
990 }
991
992 const installId = activeInstall?.installId || getInstallLock?.()?.installId || generateInstallId
993 storeInstallId?.(installId);
994 startInstallStream(installId, { retryStep: stepId });
995 };
996
997 list.addEventListener('click', (event) => {
WHAT IT COSTS: startInstallStream is called without await as the last statement of a retry handler. If the stream rejects, the rejection has nowhere to go — the handler has already returned, and the installer is left waiting on something that’s already stopped.
Security: User data is exposed more than the code intends
Security vulnerabilities are some of the most expensive errors to find and fix.
6. A temporary file whose name anyone can guess
REPO: A cross-platform API client
DETECTORS: security-insecure-random
SEVERITY: Warning
THE FINDING, IN ONE LINE: The temp file holding an in-flight request body (potentially including private attachments) is named with Math.random() and written into the shared system temp directory.
16 export async function buildMultipart(params: RequestBodyParameter[]) {
17 return new Promise(async (resolve, reject) => {
18 const filePath = path.join(os.tmpdir(), Math.random() + '.body');
19
20 const writeStream = fs.createWriteStream(filePath);
Predictable is not the same as random. Math.random() is fast, fine for animation, and unsuitable for anything an attacker benefits from guessing. This one names a file.
WHAT HAPPENS: The temporary file holding an in-flight request body is named from Math.random() and written into the shared system temp directory — a location other processes on the machine can read.
WHAT IT COSTS: A temp path built from Math.random() is guessable, and this one holds the body of an in-flight multipart request — including, potentially, private attachments.
7. Attachments served to any origin that asks
THE FINDING IN ONE LINE: A knowledge base’s attachment endpoint sets Access-Control-Allow-Origin: * unconditionally, making private file attachments readable from any website a user’s browser will let ask.
REPO: Team knowledge base
DETECTORS: security-cors-wildcard-server
SEVERITY: Critical
132 ctx.remove("X-Frame-Options");
133 }
134
135 ctx.set("Accept-Ranges", "bytes");
136 ctx.set("Access-Control-Allow-Origin", "*");
137
138 ctx.set("Cache-Control", cacheHeader);
139 ctx.set("Content-Type", contentType);
140 ctx.set(
141 "Content-Security-Policy",
Access-Control-Allow-Origin: * tells every website on the internet that it may read this response. On a public asset, that’s fine. On a private one, it isn’t. This is a knowledge base serving file attachments.
WHAT HAPPENS: The wildcard is set unconditionally on the attachment response, right next to headers that are being handled carefully — four lines earlier, the code deliberately removes X-Frame-Options for a specific case. The origin header got no such consideration.
WHAT IT COSTS: Attachment bytes become readable cross-origin by any site the browser will let ask. The loosening a few lines up is intentional and scoped; this wildcard applies to every attachment, not just the ones that need it.
Accessibility: The product works, unless you’re someone it doesn’t work for: keyboard users, screen-reader users, individuals who can’t tap precisely.
8. The focus ring is not missing. It was removed.
REPO: LLM knowledge base platform
DETECTORS: missing-focus-ring
SEVERITY: Warning
THE FINDING, IN ONE LINE: A theme applies boxShadow: none to _focusVisible on the universal CSS selector, deliberately removing the keyboard focus indicator from every element in the app at once.
1 body input,
2 body select {
3 --input-font-size: var(--chakra-fontSizes-sm) !important;
4 }
5
6 .chakra-tooltip {
WHAT HAPPENS: If you use a keyboard rather than a mouse, the focus ring shows you where you are on a page. Removing it doesn’t simplify the design; it removes the only cursor some people have. This was removed deliberately: the theme applies boxShadow: none to _focusVisible on the universal selector—not a missing style, but an explicit rule applied to every element in the application at once.
WHAT IT COSTS: Keyboard users lose the only indicator of where they are, everywhere at once, and no per-component fix restores it.
9. A password toggle that won’t say whether the password is showing
REPO: Cross-platform API client
DETECTORS: toggle-aria-state-missing
SEVERITY: Warning
THE FINDING, IN ONE LINE: An icon-only password-visibility button has no accessible name and no aria-pressed, so a screen reader can report neither what the control does nor whether it’s currently on.
55
56 )}
57 <RaInput className={twMerge('h-full w-full rounded-sm p-2')} />
58 {isPassword && (
59 <Button onPress={() => setIsPasswordVisible(!isPasswordVisible)} variant="text">
60 <Icon icon={eye${isPasswordVisible ? '-slash' : ''}} />
61
62 )}
63
An icon button says nothing to a screen reader unless someone gives it a name. A toggle says nothing about its state unless someone reports it. This one does neither on a password field.
WHAT HAPPENS: The button contains an eye icon and no text. There’s no accessible name and no aria-pressed, so the control has neither an identity nor a state that assistive technology can read.
WHAT IT COSTS: A screen-reader user cannot find out whether their password is currently visible on screen.
10. h1, then h3, on the page that says you’re done
REPO: Link attribution and short-link platform
DETECTORS: heading-level-skip
SEVERITY: Warning
THE FINDING, IN ONE LINE: An onboarding success page jumps from an h1 heading directly to h3, telling screen-reader users navigating by heading level that they skipped a section that was never there.
99 "mt-8 flex w-full max-w-[400px] flex-col gap-3",
100 "animate-slide-up-fade motion-reduce:animate-fade-in [--offset:10px] [animation-delay:250ms] [animation-duration:0.5s] [animation-fill-mode:both]",
101 )}
102 >
103 <h3 className="text-content-emphasis font-semibold">Complete setup</h3>
104
105 <div className="divide-border-subtle border-border-subtle bg-bg-muted flex flex-col divide-y overflow-hidden rounded-lg border">
106 {[
107 {
108 icon: Globe,
WHAT IT COSTS: The onboarding success page jumps from h1 straight to h3. A screen-reader user navigating by heading level is told there’s a section they skipped — and there isn’t.
11. A cursor that blinks forever, whatever you asked for
REPO: LLM knowledge base platform
DETECTORS: animation-unbounded-iteration
SEVERITY: Warning
THE FINDING, IN ONE LINE: An infinite blink animation ships with no prefers-reduced-motion handling anywhere in the app, ignoring the one setting designed to stop exactly this kind of motion.
11 .animation {
12 height: 20px;
13
14 &::after {
15 display: inline-block;
16 content: '';
17 width: 3px;
18 height: 14px;
19 transform: translate(4px, 2px) scaleY(1.3);
WHAT IT COSTS: An infinite blink animation, with no prefers-reduced-motion handling anywhere in the app source. For a reader with vestibular sensitivity, that setting exists precisely so this stops — and here, it doesn’t.
12. The error page is the least navigable page
REPO: Full-stack Vue framework
DETECTORS: missing-landmarks
SEVERITY: Warning
THE FINDING, IN ONE LINE: A framework’s default error page has no main landmark, so a screen-reader user must traverse the entire page instead of skipping straight to the error message. Landmarks let a screen-reader user skip past navigation to the thing they came for. The page that needs them most is the one shown when something has gone wrong. This is a framework’s default error page.
1 <!DOCTYPE html>
2 <html>
3 <head>
4 <title>{{ messages.status }} - {{ messages.statusText }} | {{ messages.appName }}</title>
5 <meta charset="utf-8" />
6 <meta content="width=device-width,initial-scale=1.0,minimum-scale=1.0" name="viewport" />
WHAT HAPPENS: The rendered error page is a div, an h1, an h2, and a paragraph. There’s no main element, so there’s nothing to skip to. The user has to traverse the whole page to reach the message explaining what broke.
WHAT IT COSTS: This is the page a user reaches when something has already gone wrong, and it’s the one page with no way to skip straight to the explanation.
Frequently asked questions
What is code drift?
Code drift is the gap between what a codebase does and what its own standards, defaults, and prior decisions say it should do — accumulating silently because it doesn’t fail a build or a test.
What does this post cover?
This post covers 12 findings from a 20-repository code drift study, grouped into three categories: five reliability bugs where failures are caught and discarded instead of surfaced, two security issues involving a guessable file name and an overly permissive CORS header, and five accessibility issues affecting keyboard and screen-reader users.
Is DriftDetector free to use?
Yes. ReWeaver AI’s DriftDetector scans public GitHub repositories at no cost and with no signup at drift.reweaver.ai.
What production-readiness dimensions does DriftDetector check?
DriftDetector checks nine dimensions: accessibility, reliability, testability, user experience, architecture, security & privacy, AI code governance, maintainability, and design consistency.
Does ReWeaver AI DriftDetector require an LLM or API tokens?
No. DriftDetector is a deterministic, rule-based scanner — it doesn’t call an LLM or consume API tokens, and scanning the same repository twice returns identical findings.
How does this post relate to the rest of The Drift Series?
This is Part 2 of a four-part series. Part 1 covered five reliability and accessibility failures in depth; Part 3 will examine the same findings but attribute each one to an AI agent, a person, or both; Part 4 measures whether AI-written code actually drifts more than human-written code.
Next in the series: Who Wrote the Drift? The same kind of findings, but this time with a name attached to who wrote the line: a person, an AI agent, or both.
We found all 12 of these with DriftDetector, our free deterministic scanner — no LLM guessing, just nine fixed dimensions checked the same way every time. Try it on a repo of your own: drift.reweaver.ai
———————————————
JONATHAN GORDON is the Founder & CEO of ReWeaver AI, a platform that detects design-code drift at the point of generation in AI-assisted development. With nearly three decades of experience, he has shaped developer tools and enterprise software at Google, Apple, Microsoft, Oracle, and SAP. He holds two patents and specializes in human-centered design for complex systems, AI/ML integration, and developer tooling.