AI 资讯
How I Fixed a Git Merge Conflict (Without Losing My Code)
We’ve all been there: you are busy coding, and suddenly you get a scary error saying your code conflicts with a teammate's work. Git won't let you pull their changes because it's afraid of overwriting your unsaved files. Instead of panicking, I used git stash. This command acts like a temporary clipboard—it safely hides your current work away so your workspace becomes completely clean. With a clean screen, I was able to safely update the project, fix the conflicting lines of code by hand, and make sure everything compiled perfectly. Once the team's updates were safely pushed, I ran git stash pop to bring my hidden work right back. Git wasn't trying to break my project; it was just protecting my code. Next time you get stuck, remember the golden rule: Stash your work, fix the conflict, and pop it back!
AI 资讯
What Actually Controls Your Building's HVAC System? Meet the DDC Controller
Most people working in offices never think about why the temperature stays comfortable throughout the day. The cooling adjusts automatically. Fresh air increases when occupancy rises. Fans start and stop without anyone touching a switch. Behind all of this is a device that most building occupants have never heard of: the DDC Controller. The Hidden Computer Inside Every Modern Building Walk into a mechanical room and you'll find equipment everywhere: Air Handling Units (AHUs) Chillers Pumps Cooling Towers VAV Boxes All these systems need coordination. If the supply air temperature rises above its target, something has to react. If occupancy increases, fresh air must increase. If a fan trips, alarms must be generated. This is where a DDC controller comes in. Think of it as a small industrial computer dedicated to one job: keeping a building running efficiently. A Typical Day in the Life of a DDC Controller Imagine an AHU supplying air to an office floor. At 9:00 AM employees begin arriving. The return air temperature starts increasing. The DDC controller notices this through a temperature sensor. Within seconds it: Reads the sensor value Compares it against the setpoint Calculates the cooling demand Adjusts the chilled water valve Verifies fan operation Repeats the process No operator is required. No manual intervention is needed. The controller quietly performs these calculations all day. Why Not Just Use a PLC? This is one of the most common questions from engineers entering building automation. PLCs and DDC controllers are both programmable devices. However, they were designed for different worlds. A PLC excels at: Manufacturing lines Packaging machines Process control High-speed sequencing A DDC controller excels at: HVAC control Energy optimization Occupancy schedules Comfort management BACnet communication Both can control equipment. The difference is what they were originally built for. The Four Signals Every BMS Engineer Learns First If you're new to building
AI 资讯
Streaming LLM responses in TypeScript: SSE, ReadableStream, and the React 19 useChat hook.
Streaming LLM responses in TypeScript: SSE, ReadableStream, and the React 19 useChat hook. The first time I wired an LLM response that streamed token by token instead of arriving as one lump after 4 seconds, I shipped it to production the same afternoon. The difference in perceived speed is that obvious to anyone who has used ChatGPT and then tried a non-streaming competitor. Users will wait 8 seconds if they can see the cursor moving. They will not wait 4 seconds for a blank screen. This tutorial walks the full stack from scratch. By the end you have a working Next.js API route that streams from an LLM over Server-Sent Events, a frontend that parses the stream manually with ReadableStream , and then the same UI rebuilt with the Vercel AI SDK's useChat hook so you can see what the abstraction actually buys you. TL;DR Layer What you build Key API Next.js route Streams LLM output as SSE streamText + toUIMessageStreamResponse Vanilla client Parses the stream by hand ReadableStream , TextDecoderStream React 19 client Managed state + cancellation useChat from ai/react Edge cases Backpressure, cancel, tool chunks AbortController , partial JSON guard 1. Why streaming matters A standard fetch returns after the entire response body is ready. For short completions, that is fine. For anything over 100 tokens, users see a spinner, then a wall of text, then confusion about whether the app is fast or slow. Streaming changes the shape of that experience. The first token lands in under 300ms for most hosted models. The user starts reading while the model is still writing. Perceived latency drops by 60 to 70 percent even if the total time to complete the response does not change. Cost transparency is the second reason to care. When you stream, you count tokens as they arrive. If your route has a budget ceiling and the response is going to blow past it, you can cut the stream at 800 tokens without ever waiting for the full completion. That cut is not possible with a blocking call. 2.
AI 资讯
Optimizing RAG at Scale: Chunking, Retrieval, and the Bayesian Search That Cut Latency 40%
Optimizing RAG at Scale: Chunking, Retrieval, and the Bayesian Search That Cut Latency 40% How we moved from "semantic search + hope" to a measured, tunable retrieval pipeline with 95% recall@10 The RAG Reality Check Everyone ships RAG the same way: chunk by 512 tokens, embed with text-embedding-3-small , top-k=5, stuff into context. It works for demos. Then you hit production: Legal contracts: 512 tokens splits clauses mid-sentence API docs: 1000-token chunks drown signal in noise Customer tickets: Conversational context needs overlap, not fixed windows Latency: 500ms embedding + 200ms vector search + 300ms LLM = 1s+ per query We rebuilt our retrieval layer from first principles. Here's what actually moves metrics. Chunking: One Size Fits None # rag/chunking.py from abc import ABC , abstractmethod from dataclasses import dataclass @dataclass class Chunk : text : str metadata : dict token_count : int chunk_id : str class ChunkingStrategy ( ABC ): @abstractmethod def chunk ( self , document : str , metadata : dict ) -> list [ Chunk ]: ... class FixedTokenChunker ( ChunkingStrategy ): """ Baseline. Good for homogeneous content. """ def __init__ ( self , chunk_size = 512 , overlap = 50 ): self . chunk_size = chunk_size self . overlap = overlap class RecursiveChunker ( ChunkingStrategy ): """ Respects structure: markdown headers, code blocks, paragraphs. """ def __init__ ( self , separators = [ " \n ## " , " \n ### " , " \n\n " , " \n " , " " ], chunk_size = 512 ): self . separators = separators self . chunk_size = chunk_size class SemanticChunker ( ChunkingStrategy ): """ Uses embedding similarity to find natural boundaries. """ def __init__ ( self , model = " text-embedding-3-small " , threshold = 0.7 ): self . model = model self . threshold = threshold class AgenticChunker ( ChunkingStrategy ): """ LLM decides boundaries. Expensive but highest quality for complex docs. """ def __init__ ( self , model = " gpt-4o-mini " ): self . model = model Our production config by
AI 资讯
The Bug Report Said "Refresh Logs Me Out." It Was Actually Two Bugs.
Originally published on the MyTreda engineering blog: read here A support message described what looked like one confusing mobile bug. It turned out to be two independent ones — a cross-site cookie getting blocked by Safari's ITP, and a React Hook Form autofill desync — that just happened to both only show up on mobile. Full breakdown, code, and fixes below.
AI 资讯
What Makes a WordPress Developer Truly AI-Ready?
Artificial intelligence is changing the way websites are planned, built, managed, and improved. WordPress developers now have access to tools that can help with coding, content creation, customer support, automation, analytics, and search engine optimization. However, using an AI plugin does not automatically make someone an AI-ready developer. A genuinely AI-ready WordPress developer understands how to combine technical experience, business thinking, automation, and human judgment. The goal is not to add AI everywhere. The goal is to use it where it solves a real problem. AI Should Solve a Clear Business Problem Many businesses make the mistake of choosing an AI tool before deciding what they actually need. A better approach starts with a practical challenge. For example, a company may want to: Respond to customer questions more quickly Organize website enquiries Improve WooCommerce product recommendations Automate repetitive administrative work Connect website forms with a CRM Generate content drafts Analyze customer behaviour Improve internal support processes An experienced developer will first study the workflow, the expected result, the available data, and the possible risks. Only after that should a suitable plugin, API, automation platform, or custom solution be selected. This approach prevents businesses from spending money on features that look impressive but provide little value. AI-Generated Code Still Needs Human Review AI coding tools can produce functions, snippets, plugin ideas, and debugging suggestions within seconds. That speed is useful, especially when a developer is working with repetitive tasks or unfamiliar code. The danger is that AI-generated code may look correct while containing hidden problems. It can include: Outdated WordPress functions Weak security practices Plugin compatibility issues Poor database queries Unnecessary scripts Incorrect assumptions Performance problems That is why generated code should never be added directly to a li
AI 资讯
Understanding "Skills" in LangChain: Loading Expertise On-Demand
Here's a problem every agent builder eventually runs into: your assistant needs to be good at a lot of things. Writing SQL. Composing emails. Debugging code. Maybe more later. So where do all those instructions go? The obvious answer is: cram them all into the system prompt. But that "obvious" answer causes real problems as your agent grows. In this post, I'll walk through those problems first, then show how the skills pattern in LangChain fixes them, using a small working example. The problem: one giant prompt Let's say you want a single assistant that can write SQL, draft emails, and help debug code. The naive approach is one big system prompt: You are a helpful assistant. When writing SQL: - Write queries using only SELECT, INSERT, UPDATE, DELETE - Always use parameterized queries to prevent SQL injection - Format SQL keywords in UPPERCASE - Include brief comments for complex queries When writing emails: - Keep emails concise and professional - Use clear subject lines - Structure: greeting -> purpose -> details -> call to action -> sign-off - Adjust tone based on context When debugging: - First reproduce the error - Check logs and error messages - Isolate the root cause - Suggest a fix with explanation ... This works fine for three domains. But keep adding more — legal reviewing, data analysis, customer support scripts, code review standards — and you run into a few concrete issues: 1. Prompt bloat. Every single request pays the token cost of every domain's instructions, even if the user only asked about SQL. That's slower and more expensive for no benefit. 2. Instructions start to blur together. When the SQL rules, the email rules, and the debugging rules all sit in the same block of text, the model has to sift through everything at once. It's easy for instructions from one domain to bleed into another, or for the model to lose track of which rule applies where. 3. It doesn't scale. Every time you want to add a new specialty, you're editing one long, increasingl
AI 资讯
Context Is King: Rethinking Domain Ownership, Product, and the "Spec Phase"
If you’ve spent any time recently writing detailed product requirement documents or meticulously...
科技前沿
Meme Monday
Meme Monday! Today's cover image comes from the last thread. DEV is an inclusive space! Humor in...
AI 资讯
Inkling
Open weights 975B multimodal model built for fine-tuning Discussion | Link
产品设计
UltraPod
Turn your old iPhone into a music-first dumbphone Discussion | Link
AI 资讯
ADA: an open-source AI data analyst that shows its math
I watched my sister paste a CSV into on of our LLM overlords and ask a question then reword it and get a different number. That's the problem with letting an LLM both read your question and trust it's math. ADA splits the two jobs. Drop in a CSV or Excel file, get a dashboard, anomaly detection, a forecast, and plain-English answers, all calculated locally using pandas (locally in the sense of never leaving the server and going to the AI apis.). The LLM layer that you can use (it's optional) but that only sees column names and never your rows, and the whole thing works with zero API key if need be (not the LLM part of course). MIT licensed, solo-built, open to contributors (in fact would need help to make it move out of LLMs and have a few open issues). I wish to make it as LLM free as possible as I want to make it deterministic. Live demo: automated-data-analyst.streamlit.app Repo: https://github.com/saineshnakra/automated-data-analyst
AI 资讯
We built an AI board of directors on Qwen. Then we asked it whether we should migrate to Qwen.
Solo founders and small operators make big, irreversible calls alone. Expand into a new market or not. Kill the product line or keep bleeding on it. A real board of advisors would catch the bad ones, but real boards are expensive and slow, and most people running a 10-person company are never getting one. Steven and I kept circling this problem, so when the Qwen hackathon came around we built FounderOS: an AI board of directors. You bring one decision, eight specialist agents argue about it, and you get a board memo back. The thing we cared about most, and the reason we didn't just build another chatbot wrapper, is that the disagreement survives into the memo. If the board didn't align, you can read who dissented and what would change their mind. Live demo: https://founderos-zeta.vercel.app Repo: https://github.com/VincentJulijanto/FounderOS Video: https://youtu.be/X6x_u6IWHog The board It's a LangGraph state machine underneath. A Scout frames the options first. Market Intelligence pulls cited benchmarks. Four analysts (Trend, Finance, Growth, Capability) run in parallel with asyncio.gather. Then the Skeptic gets everyone's work and attacks the weakest assumption it can find, a debate engine detects the conflicts and runs rebuttal rounds, and the Chair writes the memo. We'd both seen multi-agent demos where five agents produce one suspiciously smooth answer, which means somewhere in the pipeline the disagreement got averaged out. We wanted the opposite. Agents revise their positions when the counterargument is good, and if a conflict doesn't resolve within the round limit it ships in the memo as attributed dissent instead of disappearing. Everything runs on Qwen through the DashScope API. We split by role: qwen-turbo does the fast work (Scout, the analysts, research, the memory index) and qwen-plus does the heavy reasoning (Skeptic, Chair, the debate itself). A full board run comes out to roughly two cents and about two minutes. The two cents part is what makes the
AI 资讯
FROST 深度:为什么 AI Agent 需要「家族谱系」?
FROST 深度:为什么 AI Agent 需要「家族谱系」? 前言:一个古老的管理学问题 你有没有想过,为什么人类社会的组织方式大多是「层级制」? 从部落、到王国、到现代公司,我们习惯了一种结构: 有人决策,有人执行,有人监督 。 但当我们构建 AI Agent 系统时,为什么大多数框架都把 Agent 做成「孤岛」?每个 Agent 有自己的记忆、自己的工具、自己的行为逻辑——像一个没有家族传承的独立个体。 FROST 的核心理念是:Agent 应该有谱系、记忆和荣誉感。 1. 什么是「家族治理」? FROST 引入了生物学的隐喻: Agent 家族 。 细胞会死,但谱系会存续。 Agent 会消亡,但宪法会传承。 资产会永存。 在这个框架里,有三个关键角色: 祖辈(Elder) :定义不可违背的规则,是系统的「宪法」 父辈(Parent) :负责领域协调,可以递归委托子任务 孙辈(Child) :执行具体任务,用完即散 这不是简单的角色扮演,而是一套 结构化的治理协议 : 协议一:层级 Store 继承 祖先 Store 对后代是只读的。后代只能继承和扩展,不能篡改祖先的记忆。 # 祖辈定义的宪法 ancestor_store = Store () ancestor_store . save ( " constitution " , { " rule_1 " : " always_validate_before_act " , " rule_2 " : " never_expose_raw_context " }) # 子孙只能继承,不能修改宪法 child_store = ChildStore ( ancestor_store ) 协议二:SOP 宪法校验 每个 SOP(标准操作流程)在执行前,必须经过祖辈审核。 # 子孙编写的 SOP child_sop = [ " fetch_user_data " , " process_payment " , " send_confirmation " ] # 执行前必须经过宪法校验 if not elder . validate_sop ( child_sop ): raise PermissionError ( " SOP violates constitution " ) 协议三:编排层级限制 禁止越级 spawn。父辈只能调度子辈,不能跨代指挥。 class Elder : max_spawn_generation = 0 # 祖辈不能 spawn class Parent : max_spawn_generation = 1 # 只能 spawn 子辈 class Child : max_spawn_generation = 2 # 只能 spawn 孙辈 协议四:选择性持久化 只有经过父辈审核的产出,才能进入家族记忆库。 # 子孙的临时产出 temp_result = child . execute ( task ) # 父辈决定是否收割 if parent . approve ( temp_result ): parent . merge_from ( child ) # 吸收有价值的结果 2. 为什么 Agent 需要「记忆传承」? 当前大多数 Agent 框架的痛点: 每个对话都是全新的开始 。 你问 ChatGPT 一道数学题,它不会记得你上周问过类似的题目。你用 AutoGPT 做项目,它不会继承你之前调试的经验。 FROST 的解决方案是 分层的记忆系统 : 层级 生命周期 用途 瞬时记忆 单次任务 工作区,只读不存 世代记忆 代际传递 子辈可继承祖先数据 宪法记忆 永久 不可修改,家族共享 经验记忆 按需收割 父辈选择性地吸收有价值产出 class HierarchicalStore ( Store ): """ 层级记忆系统 """ def __init__ ( self , ancestor_store = None ): self . ancestor = ancestor_store # 只读祖先记忆 self . local = {} # 本地可写记忆 def save ( self , key , value ): if key in self . ancestor : raise ValueError ( " Cannot override ancestor memory " ) self . local [ key ] = value def load ( self , key ): if key in self . local : return self . local [ key ] if key in self . ancesto
AI 资讯
Strip Location From Both Halves of an iOS Live Photo Before Upload
A helpful comment on my EXIF test suggested using a metadata scrubber. That is useful for ordinary still images, but a mobile upload contract must define every asset it sends. An iOS Live Photo can include both a photo resource and a paired video resource. The failure case is simple: the JPEG derivative has no GPS EXIF, while metadata or an original file associated with the paired video survives in the upload or retry path. Build the resource inventory first: let resources = PHAssetResource . assetResources ( for : asset ) for resource in resources { print ( resource . type , resource . originalFilename ) } Then make privacy assertions per output, not per UI selection: Artifact Required check still derivative no EXIF GPS/location fields paired video derivative no location metadata upload manifest only derived filenames retry queue no path to original resources temporary directory deleted after success or cancellation My lifecycle test would start an upload, force the app into the background after the still image is prepared, kill it while the paired video is processing, and relaunch. Recovery must either regenerate both safe derivatives or delete the incomplete pair. It must never mix a scrubbed still with an original video. Apple’s PHAssetResource API exposes the resources associated with a Photos asset. That enumeration should become evidence in the test: fail when an unexpected resource type is present rather than silently uploading it. Also verify what reaches the server. Device-side inspection alone misses multipart manifests, queued originals, and server-generated previews. Download the stored pair in a test environment and run the same metadata assertions again. A “remove location” button needs a precise scope. For Live Photos, the user reasonably expects it to cover the complete paired asset and every retry copy—not just the JPEG they can see.
AI 资讯
Test a Saga When Compensation Times Out and the Message Is Delivered Twice
A saga test that stops after “payment succeeded, inventory failed, refund called” assumes compensation is reliable. It is another distributed operation, so test it under the same faults. Use this sequence: 1. payment capture succeeds 2. inventory reservation times out 3. refund succeeds at provider 4. refund response is lost 5. compensation message is delivered again 6. late inventory-failed event arrives again The invariant is not “refund endpoint called once.” It is: captured amount - confirmed refunded amount = final charged amount and final charged amount is never negative Persist an inbox record for consumed message IDs and an outbox record for each intended side effect. Give the provider request a stable idempotency key derived from saga and compensation step: { "sagaId" : "order-42" , "step" : "refund-payment-v1" , "idempotencyKey" : "order-42:refund-payment:v1" , "amount" : 4900 } A deterministic simulator should permute duplicate delivery, delayed acknowledgement, worker crash, and out-of-order events. After every run, assert one terminal order state, at most one economic refund, and a complete evidence trail. “Already refunded” must reconcile to success only after amount and payment identity match. AWS describes coordination choices and rollback behavior in its Saga pattern guidance . The implementation detail that deserves its own test is durable compensation progress: a process restart cannot erase whether the external side effect occurred. Alert on sagas stuck in compensating , but do not let an operator click “retry” with a new key. The runbook should first query provider state, compare amount and currency, and resume the same operation identity. Exactly-once delivery is not required to preserve money. Stable operation identity plus reconciliation is.
AI 资讯
Ship a Restart-Safe Upload CLI That Survives an Expired Resume URL
My upload CLI looked restart-safe until I tested two failures together: the process crashed at 63%, and the signed resume URL expired before restart. The checkpoint needs identity, not credentials: { "file" : "release.tar.gz" , "fingerprint" : "sha256:..." , "uploadId" : "up_123" , "localOffset" : 66060288 , "updatedAt" : "2026-07-19T12:00:00Z" } Do not persist the signed URL. On restart, exchange the durable upload ID for fresh authorization, query the server offset, then reconcile: const remote = await headUpload ( freshUrl ); if ( remote > file . size ) throw new Error ( " invalid remote offset " ); if ( remote !== checkpoint . localOffset ) { await saveCheckpoint ({ ... checkpoint , localOffset : remote }); } await sendFrom ( file , remote , freshUrl ); The server offset wins because a crash can occur after bytes are accepted but before the local checkpoint is renamed. Save checkpoints through write-to-temp plus atomic rename so a partial JSON file cannot destroy recovery. My fixture matrix includes: Failure Expected behavior crash after remote commit rewind/advance to remote offset expired URL refresh without creating a second upload changed local file stop on fingerprint mismatch missing remote upload ask before starting over checkpoint write interrupted retain previous valid checkpoint The tus resumable upload protocol specifies offset discovery and conflict handling that are useful even if the service uses a smaller custom protocol. The key idea is explicit reconciliation, not assuming client memory is authoritative. I also shipped upload status , upload forget , and an exportable checkpoint directory. Recovery is a user-facing feature; if users cannot inspect or remove state, “resumable” becomes hidden lock-in.
AI 资讯
Reproduce SQLite WAL Checkpoint Starvation With One Forgotten Reader
A SQLite service can keep answering health checks while its WAL grows without a successful checkpoint. One forgotten read transaction is enough to reproduce the risk. Run a controlled drill: Enable WAL mode and create a small table. Open connection A, begin a read transaction, and keep it open. From connection B, commit batches of writes for 60 seconds. Sample WAL bytes and checkpoint results every second. Release A and observe recovery. PRAGMA journal_mode = WAL ; PRAGMA wal_checkpoint ( PASSIVE ); Record the three checkpoint counters plus duration, WAL file size, oldest transaction age, write latency, and free disk bytes. A green SELECT 1 says nothing about checkpoint progress. My fault-injection gate looks like this: Given: one reader holds its transaction When: 10,000 writes commit Then: writes remain bounded And: WAL growth triggers an alert before the disk budget When: the reader closes Then: checkpoint progress resumes and WAL size converges Do not start with an automatic TRUNCATE loop. A busy result is evidence of contention; aggressive checkpoints can add latency without fixing the reader lifecycle. First identify long transactions, ensure result iterators close, and define a maximum transaction age. SQLite’s WAL documentation explains that a checkpoint must stop when it reaches pages beyond an active reader’s end mark. This is why the drill needs concurrency rather than a synthetic file-growth assertion. Operational thresholds should be tied to a disk budget: alert when projected WAL growth reaches the remaining safe window, not at an arbitrary file size. The runbook should name how to find the oldest reader, how to shed writes, and when a restart is safer than waiting. A health check is useful only when it measures the subsystem that is failing.
AI 资讯
Make Multipart Upload Abort Idempotent Before Orphaned Parts Start Billing You
Multipart finalization is not the only retry boundary. Abort can succeed in object storage while the HTTP response is lost, leaving your database convinced the upload is active. Model abort as a state transition, not a button wired directly to an SDK call: active → aborting → aborted └──→ cleanup_pending The endpoint should own an idempotency key and durable intent: await db . transaction ( async tx => { const upload = await tx . lockUpload ( uploadId ); if ( upload . state === " aborted " ) return ; await tx . markAborting ( uploadId , idempotencyKey ); await outbox . enqueue ( " abort-multipart " , { uploadId , storageKey }); }); A worker calls storage. If a retry receives NoSuchUpload , do not blindly fail: reconcile whether the upload was already completed, already aborted, or expired. Only the “already absent because abort succeeded” path may converge to aborted ; completion requires a separate terminal state. Test this sequence: Step Fault Required invariant persist abort intent process dies outbox resumes work storage abort succeeds response is lost retry does not reactivate upload duplicate message delivered twice one terminal state client retries same key same operation result cleanup scan stale active row reconcile before deleting Amazon S3’s AbortMultipartUpload API notes that in-progress part uploads may still succeed around an abort and recommends verifying that parts are gone. That makes a post-abort verification pass part of correctness, not optional housekeeping. Expose abort_requested_at , attempt count, last storage result, and remaining-part count. Alert on age in aborting , then run a lifecycle cleanup policy as a backstop—not as a substitute for application reconciliation.
AI 资讯
Reject Image Polyglots After EXIF Removal, Before They Reach Your CDN
Removing EXIF GPS protects one privacy boundary. It does not prove that an uploaded JPEG contains only a JPEG. A useful hostile fixture is a valid image followed by bytes for another format. Many decoders stop at JPEG’s end marker and display the picture successfully, while downstream scanners, content sniffers, or accidental downloads may interpret the trailing payload differently. Build a fixture set rather than trusting extensions: clean.jpg valid JPEG exif-gps.jpg valid JPEG with location jpeg-plus-zip.jpg JPEG followed by ZIP bytes jpeg-plus-html.jpg JPEG followed by HTML truncated.jpg missing end marker oversized-dimensions.jpg small file, dangerous decode cost My upload gate has independent layers: Store the original in a non-public quarantine location. Enforce byte-size and decoded-dimension limits. Decode with a maintained image library. Re-encode pixels into a new file, discarding metadata and trailing bytes. Verify the output’s signature, dimensions, and complete parse. Publish only the derived object under a server-generated name. A boundary check can detect bytes after the end marker, but re-encoding is the stronger transformation because it creates a new representation from decoded pixels. Keep the original inaccessible and delete it according to a tested lifecycle. assert ( outputSize <= limit ); assert ( decoded . width * decoded . height <= pixelBudget ); assert ( parseConsumesEntireFile ( output )); assert ( noLocationMetadata ( output )); The OWASP File Upload Cheat Sheet recommends defense in depth: allowlisted types, generated filenames, size limits, storage separation, and content validation. No single MIME header or metadata scrubber replaces that chain. The important privacy lesson is broader than EXIF: inspect every representation that survives the upload pipeline, and publish only a constrained derivative.