<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Adarsh N</title>
    <description>The latest articles on DEV Community by Adarsh N (@adarsh_n_cfb490febe0ea8fa).</description>
    <link>https://dev.to/adarsh_n_cfb490febe0ea8fa</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3990672%2F60bf0ae0-f4d6-4864-a2c4-38b39825e88c.jpg</url>
      <title>DEV Community: Adarsh N</title>
      <link>https://dev.to/adarsh_n_cfb490febe0ea8fa</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/adarsh_n_cfb490febe0ea8fa"/>
    <language>en</language>
    <item>
      <title>Building FlowOps AI: How I Designed a Volunteer's Co-Pilot for FIFA World Cup 2026 Stadiums</title>
      <dc:creator>Adarsh N</dc:creator>
      <pubDate>Sat, 18 Jul 2026 14:17:40 +0000</pubDate>
      <link>https://dev.to/adarsh_n_cfb490febe0ea8fa/building-flowops-ai-how-i-designed-a-volunteers-co-pilot-for-fifa-world-cup-2026-stadiums-3ne1</link>
      <guid>https://dev.to/adarsh_n_cfb490febe0ea8fa/building-flowops-ai-how-i-designed-a-volunteers-co-pilot-for-fifa-world-cup-2026-stadiums-3ne1</guid>
      <description>&lt;p&gt;When I first read the brief for this challenge, my instinct was to build something for everyone. Fans need navigation help. Organizers need oversight. Volunteers need coordination. Venue staff need operational data. Why not build one app that covers it all?&lt;/p&gt;

&lt;p&gt;That instinct was wrong, and figuring out why shaped almost everything that came after.&lt;/p&gt;

&lt;h2&gt;
  
  
  Picking one problem instead of four
&lt;/h2&gt;

&lt;p&gt;During the challenge's explainer session, one comment stuck with me: an app trying to serve every persona ends up serving none of them well. So I made a deliberate choice to narrow the scope. FlowOps AI is built for exactly one person: the stadium volunteer standing at a gate, trying to keep fans moving safely during a match with eighty thousand people in the building.&lt;/p&gt;

&lt;p&gt;That single decision changed the whole shape of the product. Instead of a general dashboard, I built a focused tool around two things a volunteer actually needs in the moment: knowing where the crowd is building up, and being able to communicate with a fan who doesn't speak their language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making the AI actually reason
&lt;/h2&gt;

&lt;p&gt;A lot of "AI powered" apps are really just a normal dashboard with a chatbot bolted on. I wanted to avoid that trap, so the core design rule for FlowOps AI was simple: if a plain if-else statement could produce the same output, the AI wasn't doing its job.&lt;/p&gt;

&lt;p&gt;Instead of showing a volunteer a raw number like "Gate D: 91% capacity," the app asks Gemini to reason over the live gate and facility data and produce something a person can act on immediately. For example: Gate D is at 91% capacity and climbing fast, Gate C nearby is also elevated, and both share the same entry point from the metro station. Redirect incoming fans to Gate E, which is sitting comfortably under capacity, and send two volunteers to intercept the queue before it reaches the turnstiles.&lt;/p&gt;

&lt;p&gt;That's a genuinely different kind of output than a percentage bar. It's explaining the problem, not just measuring it.&lt;/p&gt;

&lt;p&gt;The same principle applies to the multilingual assistant. A basic translation tool doesn't care whether someone is asking where the bathroom is or reporting a medical emergency. I wanted the assistant to pick up on urgency and tone, not just swap words between languages, so a volunteer helping a distressed fan gets a different kind of translated response than one helping someone who just wants directions to the nearest food stall.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting judges bring their own data
&lt;/h2&gt;

&lt;p&gt;Since I don't have access to real stadium sensor feeds, the app runs on a simulated crowd data engine that mimics gates filling and emptying over time. But a fully synthetic demo has an obvious weakness: anyone evaluating it can't tell if the reasoning is actually responding to the data or just reciting a script.&lt;/p&gt;

&lt;p&gt;So I added what I ended up calling the Judge's Override. It's a CSV upload feature that lets anyone drop in their own gate and facility data, and the app immediately swaps its live feed for that uploaded dataset, pauses the simulator, and lets the AI reasoning respond to whatever was just uploaded. It felt important that this wasn't hidden away in a settings menu. It sits right on the dashboard as its own labeled control panel, because the whole point is that someone should be able to test the reasoning themselves, not just take my word for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug that only showed up with two tabs open
&lt;/h2&gt;

&lt;p&gt;One of the more interesting problems I ran into wasn't really a bug in the traditional sense. It was an architecture gap that only revealed itself under a specific condition: multiple browser tabs open to the same app at once.&lt;/p&gt;

&lt;p&gt;Each tab was running its own independent crowd simulation and writing those updates straight to the shared database. With no coordination between tabs, two tickers writing to the same record at overlapping moments caused values to jump erratically, sometimes landing two different gates at exactly 100% capacity within a few seconds of each other. It looked like a data bug, but it was really a concurrency problem.&lt;/p&gt;

&lt;p&gt;Since I couldn't rule out a scenario where a judge opens the app on two devices, or where a teammate and I both have it open while demoing, I changed the architecture rather than patching around the symptom. The simulated ticker now runs entirely client side and never writes to the shared database. The database is reserved specifically for syncing judge-uploaded data, which only happens through a deliberate user action. It's a small change, but it meant the live database became a reliable sync point for the one thing that actually needs syncing, instead of a shared write target for every open tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the deployment plan had to change
&lt;/h2&gt;

&lt;p&gt;The original plan was to deploy on Google Cloud Run. That fell apart when Cloud Billing repeatedly rejected a UPI payment method with an error that, it turns out, a lot of other developers in India have run into as well. After a few failed attempts and a little lost money, I switched to Firebase Hosting instead, which didn't require a billing account at all for a static build like this one.&lt;/p&gt;

&lt;p&gt;In hindsight it's arguably the better fit anyway. The app was already using Firebase for the database, so hosting the static frontend there too meant one less service to configure and document, without giving up anything the app actually needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting this kind of build
&lt;/h2&gt;

&lt;p&gt;Choose a narrow problem and go deep rather than wide. Make sure your AI is doing something a simple script genuinely couldn't. Build in a way for evaluators to test your logic with their own inputs rather than asking them to trust a canned demo. And when your infrastructure plan runs into a wall you can't control, don't be precious about the original architecture. Firebase Hosting wasn't part of the plan on day one, but it got the job done without costing me anything I actually needed.&lt;/p&gt;

&lt;p&gt;You can find the full source code, including the project specification files that guided the build from the start, here: &lt;a href="https://github.com/adarsh52581/FlowOps-AI" rel="noopener noreferrer"&gt;https://github.com/adarsh52581/FlowOps-AI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you're working on something similar for FIFA 2026 or any large scale event tech problem, I'd love to hear how you're approaching it.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>react</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>I Built a 'Living World' Eco App with AI - Here's Every Architectural Decision That Mattered</title>
      <dc:creator>Adarsh N</dc:creator>
      <pubDate>Thu, 18 Jun 2026 10:41:42 +0000</pubDate>
      <link>https://dev.to/adarsh_n_cfb490febe0ea8fa/i-built-a-living-world-eco-app-with-ai-heres-every-architectural-decision-that-mattered-142</link>
      <guid>https://dev.to/adarsh_n_cfb490febe0ea8fa/i-built-a-living-world-eco-app-with-ai-heres-every-architectural-decision-that-mattered-142</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built &lt;a href="https://github.com/adarsh52581/EcoAscent-Live-Green" rel="noopener noreferrer"&gt;EcoAscent&lt;/a&gt;; a gamified eco-awareness app where a hand-drawn SVG world visually decays or flourishes based on your logged actions. This post documents the problem I was actually solving, why I picked React + Vitest + TanStack Start, how my prompts evolved across three distinct phases, and the two architectural decisions that made the whole thing work cleanly.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The Problem Nobody Told Me to Solve
&lt;/h2&gt;

&lt;p&gt;Most eco-tracking apps fail for the same reason fitness apps fail: they turn a deeply personal, emotionally charged behaviour change into a spreadsheet. You log your actions, you see a number go up, you feel nothing, you stop.&lt;/p&gt;

&lt;p&gt;The PromptWars Virtual Challenge 3 brief was deliberately open: &lt;em&gt;build something that raises environmental awareness&lt;/em&gt;. The lazy interpretation is a CO₂ dashboard. Bar charts, a running total, maybe a comparison to "the average person." Useful, yes. Motivating, no.&lt;/p&gt;

&lt;p&gt;The root problem I chose to target was this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;People don't lack data about climate change. They lack emotional connection to their own impact.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That framing changed every decision that followed. I wasn't building a tracker. I was building a &lt;em&gt;feedback loop&lt;/em&gt;; one where small actions produce visible, immediate, satisfying changes in a world you feel ownership over.&lt;/p&gt;

&lt;p&gt;That world needed to look alive. It needed to react. It needed to make you &lt;em&gt;feel&lt;/em&gt; the difference between doing nothing and planting a tree.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Solution: A "Living World" That Breathes With You
&lt;/h2&gt;

&lt;p&gt;EcoAscent renders a handcrafted SVG ecosystem. Trees, clouds, a sun, water, sky; all of it transitions between three emotional states driven by your cumulative CO₂ offset score:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;CO₂ saved&lt;/th&gt;
&lt;th&gt;Sky&lt;/th&gt;
&lt;th&gt;Trees&lt;/th&gt;
&lt;th&gt;Feel&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;🌱 Thriving&lt;/td&gt;
&lt;td&gt;&amp;gt; 80 kg&lt;/td&gt;
&lt;td&gt;Deep blue, bright&lt;/td&gt;
&lt;td&gt;Full green canopy&lt;/td&gt;
&lt;td&gt;Hopeful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🌤 Recovering&lt;/td&gt;
&lt;td&gt;20–80 kg&lt;/td&gt;
&lt;td&gt;Hazy amber&lt;/td&gt;
&lt;td&gt;Sparse, yellowing&lt;/td&gt;
&lt;td&gt;Tense&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;🌫 Critical&lt;/td&gt;
&lt;td&gt;&amp;lt; 20 kg&lt;/td&gt;
&lt;td&gt;Grey, smoggy&lt;/td&gt;
&lt;td&gt;Bare branches&lt;/td&gt;
&lt;td&gt;Urgent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every transition is animated. The world doesn't snap between states; it &lt;em&gt;breathes&lt;/em&gt; through them. Clouds slow down when the world is healthy, speed up when it's not. The sun dims and warms. The trees lose leaves gradually.&lt;/p&gt;

&lt;p&gt;The app is entirely client-side. No backend, no API keys, no accounts. Your actions live in &lt;code&gt;localStorage&lt;/code&gt;. This was an intentional choice (more on that in the architecture section).&lt;/p&gt;




&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flpykdddnxl4pgyv09f6o.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Flpykdddnxl4pgyv09f6o.png" alt=" " width="800" height="399"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tool Choices and Why
&lt;/h2&gt;

&lt;h3&gt;
  
  
  React + TypeScript
&lt;/h3&gt;

&lt;p&gt;Not a controversial pick, but my reasoning was specific. The world state is a pure transformation: &lt;code&gt;actions[] → worldScore → worldState&lt;/code&gt;. React's component model maps perfectly onto that: a score change triggers a re-render, the SVG transitions update, done. TypeScript let me encode the world state as a discriminated union (&lt;code&gt;'thriving' | 'recovering' | 'critical'&lt;/code&gt;), which made it impossible to accidentally render an undefined state.&lt;/p&gt;

&lt;p&gt;I also wanted components that a code-quality AI evaluator would recognise as intentionally small and single-responsibility. React's composability makes that easier to enforce than a monolith framework.&lt;/p&gt;

&lt;h3&gt;
  
  
  TanStack Start
&lt;/h3&gt;

&lt;p&gt;I chose TanStack Start over Next.js for one deliberate reason: for a fully client-side app, Next.js carries server-rendering infrastructure I'd never use, and a bundled SSR framework inflates the dependency tree. TanStack Start gave me file-based routing, a clean Vite build pipeline, and TypeScript-first ergonomics without a kilobyte of unused server code.&lt;/p&gt;

&lt;p&gt;It also signals to evaluators that I made &lt;em&gt;considered&lt;/em&gt; choices, not just "I always use Next.js."&lt;/p&gt;

&lt;h3&gt;
  
  
  Vitest
&lt;/h3&gt;

&lt;p&gt;Vitest shares Vite's config pipeline, which means zero configuration divergence between dev and test environments. My &lt;code&gt;worldState&lt;/code&gt; function is pure; no side effects, no DOM, no network - so Vitest runs its tests in milliseconds without a browser. That's not an accident; I designed the core logic to be purely testable from the start (see architecture section).&lt;/p&gt;

&lt;p&gt;The test suite covers: action scoring edge cases, corrupt &lt;code&gt;localStorage&lt;/code&gt; recovery, negative totals clamped to zero, and world state threshold transitions at exact boundary values.&lt;/p&gt;

&lt;h3&gt;
  
  
  No external UI library
&lt;/h3&gt;

&lt;p&gt;Tailwind utility classes only, no component library. Deliberate choice. An evaluator looking at the bundle should see that I know how to write accessible HTML and CSS without a dependency doing it for me. Every &lt;code&gt;aria-*&lt;/code&gt; attribute, every &lt;code&gt;&amp;lt;label htmlFor&amp;gt;&lt;/code&gt;, every &lt;code&gt;role="alert"&lt;/code&gt; is hand-authored.&lt;/p&gt;




&lt;h2&gt;
  
  
  How My Prompts Evolved: Three Phases
&lt;/h2&gt;

&lt;p&gt;This is the part most people skip in write-ups, and it's the part the PromptWars rubric explicitly weights. Here's exactly how my prompting changed across the build.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Specification Prompts (Days 1–2)
&lt;/h3&gt;

&lt;p&gt;My earliest prompts were discovery-focused. I wasn't asking for code yet; I was asking the AI to pressure-test my problem framing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example prompt from this phase:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I want to build an eco-awareness app that isn't a dashboard. 
The core interaction should feel more like a game than a tracker. 
What are the failure modes of gamified behaviour-change apps, 
and how should I design against them specifically for 
environmental awareness?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response identified three failure modes I hadn't considered: &lt;em&gt;novelty decay&lt;/em&gt; (the game stops being interesting after the first week), &lt;em&gt;perfection paralysis&lt;/em&gt; (users disengage when they miss a day), and &lt;em&gt;abstraction distance&lt;/em&gt; (logging "took the bus" feels disconnected from climate outcomes). Each one became a design constraint.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Prompting for critique before prompting for solutions saves significant rework. The AI is a better devil's advocate when it hasn't already written the code it's reviewing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: Architecture Prompts (Days 3–4)
&lt;/h3&gt;

&lt;p&gt;Once I had solid requirements, I moved to system design. My prompts became more specific and constraint-laden.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example prompt from this phase:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nx"&gt;I&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;m building a React + TypeScript app where the entire UI state 
derives from a list of logged eco-actions. I want:
- A pure function that takes Action[] and returns a WorldState object
- A single React hook that wraps localStorage persistence and exposes 
  derived values
- No global state library (no Redux, no Zustand)
- The function must be unit-testable without a DOM

Give me the TypeScript types and function signatures first, 
then implementation.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the constraints embedded in the prompt: no global state library, pure function, types before implementation. This is &lt;em&gt;intent-driven orchestration&lt;/em&gt;; I'm not asking "write me a state manager," I'm telling the AI what architectural properties I need and asking it to work within those properties.&lt;/p&gt;

&lt;p&gt;The output from this phase became the backbone of the entire codebase.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; Specifying &lt;em&gt;constraints&lt;/em&gt; in prompts produces architecturally coherent code. Specifying just &lt;em&gt;goals&lt;/em&gt; produces working code that fights your architecture later.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Refinement Prompts (Days 5–7)
&lt;/h3&gt;

&lt;p&gt;The final phase was iterative polish. My prompts in this phase looked like code reviews:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Example prompt from this phase:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Here is my current LivingWorld.tsx component. It's 340 lines. 
The SVG path data for each world state is inline.
Identify every place where:
1. A constant should be extracted to a separate file
2. A sub-component boundary should be introduced
3. An accessibility attribute is missing

Give me a refactored file structure, not the code itself.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Asking for a &lt;em&gt;file structure&lt;/em&gt; rather than code kept me in control of the actual implementation while using the AI for architectural review. This is the phase that produced &lt;code&gt;worldPalettes.ts&lt;/code&gt;, the split between &lt;code&gt;WorldScene.tsx&lt;/code&gt; and &lt;code&gt;WorldOverlay.tsx&lt;/code&gt;, and the ARIA audit that caught the missing &lt;code&gt;role="progressbar"&lt;/code&gt; on the health bar.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I learned:&lt;/strong&gt; In the refinement phase, ask the AI to review and structure; not to write. You produce better code when you implement the recommendations yourself than when you paste AI output directly.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Two Architectural Decisions That Defined the Project
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Decision 1: &lt;code&gt;worldState()&lt;/code&gt; as a Pure Function
&lt;/h3&gt;

&lt;p&gt;The most important architectural choice was to make the world state derivation completely pure; no side effects, no hooks, no DOM access:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/eco/worldState.ts&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;WORLD_THRESHOLDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;THRIVING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// kg CO₂ offset&lt;/span&gt;
  &lt;span class="na"&gt;RECOVERING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WorldStateLevel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thriving&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recovering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;WorldState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WorldStateLevel&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Derives the current world state from a cumulative CO₂ offset score.
 * Pure function; no side effects, fully unit-testable.
 *
 * @param score - Total kg CO₂ offset (clamped to 0 if negative)
 * @returns WorldState object describing the current ecological level
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;worldState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;WorldState&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clamped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clamped&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;WORLD_THRESHOLDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;THRIVING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thriving&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clamped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your world is flourishing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Every action you take is writing a greener story.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;clamped&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;WORLD_THRESHOLDS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RECOVERING&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recovering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clamped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your world is healing&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Keep going - the balance is starting to shift.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;level&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clamped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;headline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Your world needs you&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Small actions compound. Start anywhere.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why does this matter? Because it makes the entire application trivially testable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/lib/eco/__tests__/worldState.test.ts&lt;/span&gt;

&lt;span class="nf"&gt;describe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;worldState&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;returns thriving at exactly the threshold&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;worldState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;thriving&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;returns recovering in the middle band&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;worldState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recovering&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;clamps negative scores to 0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;worldState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nf"&gt;it&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;returns critical for a new user with no actions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;worldState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;level&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every test runs in under 2ms, in Node, with no React tree, no localStorage mock, no JSDOM. This is only possible because the business logic lives outside the component tree entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The architectural lesson:&lt;/strong&gt; If you find yourself writing &lt;code&gt;vi.mock('react')&lt;/code&gt; in a business logic test, you've coupled your logic to your UI. Extract it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Decision 2: &lt;code&gt;useEcoState&lt;/code&gt; as a Single Source of Truth
&lt;/h3&gt;

&lt;p&gt;Every piece of UI state in EcoAscent flows from one hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// src/hooks/useEcoState.ts&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useCallback&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;worldState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/eco/worldState&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/eco/actions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;STORAGE_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ecoascent_actions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;loadActions&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;seedActions&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// First-load experience&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Corrupt storage - recover gracefully&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="cm"&gt;/**
 * Single source of truth for all EcoAscent state.
 * Derives world state, score, and action log from persisted actions.
 * Components read from this hook; they never write to localStorage directly.
 */&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;useEcoState&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setActions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loadActions&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;co2Kg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;world&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useMemo&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;worldState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;logAction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setActions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
      &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resetActions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useCallback&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;localStorage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;STORAGE_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;setActions&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;logAction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;resetActions&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This hook is the only thing in the app that touches &lt;code&gt;localStorage&lt;/code&gt;. Every component receives props from it. This creates a clear data flow: &lt;code&gt;localStorage → hook → derived state → components → UI&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The benefit isn't just cleanliness. It means that when I wrote component tests, I could mock exactly one thing (&lt;code&gt;useEcoState&lt;/code&gt;) and test every component in isolation with predictable state. No component had to know how actions were persisted.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I'd Do Differently
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Add a Lighthouse CI step from day one.&lt;/strong&gt; I added performance optimisation late and had to retrofit &lt;code&gt;React.memo&lt;/code&gt; and &lt;code&gt;useMemo&lt;/code&gt; into components that were already wired together. Starting with a performance budget enforced in CI would have kept me honest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write the blog post first.&lt;/strong&gt; I don't mean this facetiously; writing this post forced me to articulate &lt;em&gt;why&lt;/em&gt; I made each decision. Several of those articulations revealed decisions I hadn't consciously made at all, which meant I couldn't defend them. A "decisions.md" written before coding would have made the architecture more intentional and the code review phase faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Seed the world on first load from the beginning.&lt;/strong&gt; A first-time user landing on an empty, grey, critical world is exactly the opposite of the emotional hook the app is supposed to deliver. I added seed actions in the final day. It should have been line one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Repo + Live Demo
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/adarsh52581/EcoAscent-Live-Green" rel="noopener noreferrer"&gt;github.com/adarsh52581/EcoAscent-Live-Green&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://world-heal-hero.lovable.app/" rel="noopener noreferrer"&gt;https://world-heal-hero.lovable.app/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project docs:&lt;/strong&gt; &lt;code&gt;/project-docs&lt;/code&gt; - architecture decisions, prompt evolution log, and component map&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;/project-docs&lt;/code&gt; folder contains the full prompt evolution log with before/after examples, the architectural decision record, and a component responsibility map. If you're building for a judged challenge and you're not keeping a prompt log - start now. The process &lt;em&gt;is&lt;/em&gt; the submission.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built for PromptWars Virtual Challenge 3. Questions, feedback, or thoughts on intent-driven AI orchestration? Drop them in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>react</category>
      <category>vitest</category>
      <category>typescript</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
