<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Salman Ahmad]]></title><description><![CDATA[Salman Ahmad is a Computer Science student and self-taught Full-Stack Developer from Pakistan, exploring software engineering, artificial intelligence, LLMs, and modern web technologies. Follow his journey through technical articles, project breakdowns, programming concepts, and open-source contributions.]]></description><link>https://ahmmikun.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Wed, 04 Nov 2026 21:14:37 GMT</lastBuildDate><atom:link href="https://ahmmikun.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Building GitRoasted with Kiro CLI: From Idea to a Live AI-Powered App]]></title><description><![CDATA[Built GitRoasted, an AI-powered GitHub analyzer with personalized roasts and developer scoring using Kiro CLI.]]></description><link>https://ahmmikun.hashnode.dev/building-gitroasted-with-kiro-cli-from-idea-to-a-live-ai-powered-app</link><guid isPermaLink="true">https://ahmmikun.hashnode.dev/building-gitroasted-with-kiro-cli-from-idea-to-a-live-ai-powered-app</guid><category><![CDATA[Kiro]]></category><category><![CDATA[claude]]></category><category><![CDATA[Web Development]]></category><category><![CDATA[AI]]></category><category><![CDATA[Next.js]]></category><category><![CDATA[GitHub]]></category><dc:creator><![CDATA[Salman Ahmad]]></dc:creator><pubDate>Sun, 01 Nov 2026 20:38:04 GMT</pubDate><enclosure url="https://cdn.hashnode.com/uploads/covers/6a9b464d07bbe7df8164120d/48e5b264-c833-4fc5-852a-61820622b7db.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Every developer has looked at their own GitHub profile and thought, "Is this actually good?"</p>
<p>I wanted a tool that answers that honestly, and with a bit of humour. You type a GitHub username. You get a score out of 1000, a funny roast, and a clear list of things to fix. You also get a link you can share.</p>
<p>That is <strong>GitRoasted</strong>. In this blog I will explain how I built it, why I made certain choices, and what I would do again.</p>
<p>Live demo: <a href="https://git-roasted-ai.vercel.app">git-roasted-ai.vercel.app</a><br />Code: <a href="https://github.com/ahmmikun/GitRoasted">github.com/ahmmikun/GitRoasted</a></p>
<hr />
<h2>Where the Idea Came From</h2>
<p>I want to be honest here. The idea of roasting GitHub profiles already existed. There is an earlier project also called GitRoasted by MdKasif0, and I used it as inspiration for features like a leaderboard, comparison and improvement tips.</p>
<p>I did not copy its code, text or design. I built my own scoring system, my own architecture and my own UI. But I think it is right to say where the spark came from.</p>
<p>The first version (the MVP) was very small:</p>
<ol>
<li>User enters a username.</li>
<li>App fetches the profile and repositories.</li>
<li>AI writes a roast, a score and some tips.</li>
<li>Result is saved in MongoDB and shown on a public link.</li>
</ol>
<p>No login. No signup. That was a deliberate choice. A tool like this only works if people can try it in five seconds.</p>
<hr />
<h2>The Stack</h2>
<table>
<thead>
<tr>
<th>Part</th>
<th>What I used</th>
</tr>
</thead>
<tbody><tr>
<td>Framework</td>
<td>Next.js 16 (App Router) with React 19</td>
</tr>
<tr>
<td>Language</td>
<td>TypeScript in strict mode</td>
</tr>
<tr>
<td>Database</td>
<td>MongoDB Atlas with Mongoose</td>
</tr>
<tr>
<td>Validation</td>
<td>Zod</td>
</tr>
<tr>
<td>Testing</td>
<td>Vitest and fast-check</td>
</tr>
<tr>
<td>Hosting</td>
<td>Vercel (serverless)</td>
</tr>
<tr>
<td>Styling</td>
<td>Hand-written Neo-Brutalist CSS</td>
</tr>
</tbody></table>
<p>I also used Kiro to work in a spec-driven way. I wrote requirements first, broke them into tasks, and then built each task. You can still see this in my code comments, which refer to requirement numbers. It kept a project with many moving parts organised.</p>
<hr />
<h2>The Full Flow\</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6a9b464d07bbe7df8164120d/ca9c4f74-80a6-46c9-a59e-2a88f696ad38.jpg" alt="" style="display:block;margin:0 auto" />

<p>When someone submits a username, this is what happens:</p>
<ol>
<li><strong>Validate</strong> the username.</li>
<li><strong>Check the cache.</strong> If this user was roasted recently, return the saved result. No GitHub call, no AI call.</li>
<li><strong>Check the rate limit.</strong> Only on a cache miss.</li>
<li><strong>Fetch</strong> profile, repositories and contribution data from GitHub.</li>
<li><strong>Calculate the score</strong> with plain code.</li>
<li><strong>Ask the AI</strong> to write the roast.</li>
<li><strong>Validate</strong> the AI output.</li>
<li><strong>Save</strong> to MongoDB with a unique slug.</li>
<li><strong>Return</strong> a shareable page at <code>/r/[slug]</code>.</li>
</ol>
<p>Most of my design decisions come from one question: <em>what can go wrong at each step?</em></p>
<hr />
<h2>The Most Important Decision: The AI Does Not Decide the Score</h2>
<p>My first idea was simple. Send the profile to an AI and ask, "Give this developer a score out of 100."</p>
<p>I dropped it quickly. AI models are not consistent. The same profile can get 72 today and 81 tomorrow. And a model can simply invent numbers.</p>
<p>So I split the work in two:</p>
<ul>
<li><strong>Code calculates the score.</strong> It is deterministic. Same profile in, same score out.</li>
<li><strong>AI only writes the words.</strong> The roast, the tone, the jokes.</li>
</ul>
<p>And I added one more safety step. Even if the AI returns its own score, I overwrite it:</p>
<pre><code class="language-ts">// Whatever score the model returned, replace it with the real one
// calculated by our analyzer. The model can never change the number.
return withCanonicalScore(parsed.value, analyzerScore);
</code></pre>
<p>This one function is why people can trust the number. The AI can be funny, but it cannot lie about your score.</p>
<hr />
<h2>The 1000-Point Developer Score\</h2>
<p>\</p>
<img src="https://cdn.hashnode.com/uploads/covers/6a9b464d07bbe7df8164120d/3bb133dc-11e0-4f6b-8f92-2d8a34a1993b.jpg" alt="" style="display:block;margin:0 auto" />

<p>The score is split into eight dimensions. Each one has its own maximum, and together they add up to exactly 1000.</p>
<table>
<thead>
<tr>
<th>Dimension</th>
<th>Max</th>
<th>What it measures</th>
</tr>
</thead>
<tbody><tr>
<td>Impact</td>
<td>250</td>
<td>Stars and forks</td>
</tr>
<tr>
<td>Consistency</td>
<td>200</td>
<td>Contributions over the last year and active weeks</td>
</tr>
<tr>
<td>Quality</td>
<td>150</td>
<td>Descriptions, licenses, topics, demo links, README</td>
</tr>
<tr>
<td>Community</td>
<td>150</td>
<td>Followers and follower/following balance</td>
</tr>
<tr>
<td>Diversity</td>
<td>100</td>
<td>Number of languages used</td>
</tr>
<tr>
<td>Experience</td>
<td>75</td>
<td>Account age</td>
</tr>
<tr>
<td>Activity</td>
<td>50</td>
<td>Recently updated repositories</td>
</tr>
<tr>
<td>Bonuses</td>
<td>25</td>
<td>Extras like a profile README or website</td>
</tr>
</tbody></table>
<h3>Why Impact Has the Biggest Share</h3>
<p>Stars are the clearest signal that other people found your work useful, so Impact gets the largest slice.</p>
<h3>Fairness: Logarithmic Scoring</h3>
<p>If score grew in a straight line with stars, one viral repository would beat everything else. So I used a logarithmic curve:</p>
<pre><code class="language-ts">// Early progress is rewarded a lot.
// Later progress gives smaller and smaller gains.
// Result is always between 0 and 1.
Math.log10(1 + value) / Math.log10(1 + target)
</code></pre>
<p>Going from 0 to 50 stars feels big and is rewarded. Going from 4,950 to 5,000 barely changes anything. This is much fairer for new developers.</p>
<h3>Consistency Rewards Steady Work</h3>
<p>Consistency is split into two parts: <strong>60% volume</strong> (how many contributions) and <strong>40% spread</strong> (how many weeks you were active out of 52). So someone who codes every week beats someone who did one huge weekend.</p>
<h3>What If We Don't Have the Data?</h3>
<p>Contribution history needs GitHub's GraphQL API. If that is not available, I do not punish the user, and I do not pretend I have verified data either. The score falls back to an estimate and is <strong>capped at half credit</strong>. The breakdown clearly says it is an estimate.</p>
<h3>Tiers</h3>
<p>The final number maps to a tier and grade, from "Getting Started" (F) up to "Elite" (S, 900+).</p>
<hr />
<h2>The AI Fallback Chain\</h2>
<img src="https://cdn.hashnode.com/uploads/covers/6a9b464d07bbe7df8164120d/a3b4d483-33f2-4c51-82b3-bd8ad244c555.jpg" alt="" style="display:block;margin:0 auto" />

<p>AI providers fail. They hit rate limits, return broken JSON, or go down. I did not want my app to show an error page because one provider had a bad day.</p>
<p>So I built a chain. The app tries providers in order:</p>
<ol>
<li>OpenRouter</li>
<li>Gemini</li>
<li>OpenAI</li>
<li>Grok</li>
</ol>
<p>If every provider fails, it uses a <strong>rule-based roast engine</strong> I wrote myself. If even that fails, it returns a hardcoded default roast.</p>
<pre><code class="language-ts">// Try each provider one by one.
// Move to the next one if the call fails OR the output is invalid.
for (const provider of providers) {
  // call provider -&gt; validate with Zod -&gt; return on success
}
// All failed -&gt; rule-based roast -&gt; default roast
</code></pre>
<p>The orchestrator <strong>never throws</strong>. Every result also stores which provider and model were used, and whether a fallback happened, so I can see what really happens in production.</p>
<h3>Never Trust Model Output</h3>
<p>A provider returning something does not mean it is usable. Every response goes through a Zod schema first. If the shape is wrong, it counts as a failure and the next provider is tried.</p>
<hr />
<h2>Caching and Rate Limiting</h2>
<p>AI calls and GitHub calls cost time and money, and a public tool with no login can be abused. I handled both with MongoDB.</p>
<h3>Cache</h3>
<p>Before doing any work, I check if this username was roasted recently (24 hours by default). If yes, return the saved roast. Nothing else runs.</p>
<p>I also keep a separate profile snapshot (6 hours by default) that feeds the leaderboard, compare and improve pages. That means GitHub is queried at most once per user per window, even if five different features need the same data.</p>
<h3>Rate Limit</h3>
<p>I limit each IP to 5 fresh roasts per hour, stored in a MongoDB collection.</p>
<p>Two details I like:</p>
<ul>
<li><strong>Cached results do not count.</strong> If you open a roast that already exists, it is free.</li>
<li><strong>The decision is a pure function.</strong> It takes the existing record, the current time, the limit and the window, and returns allowed or blocked. No database inside it, so it is easy to test.</li>
</ul>
<pre><code class="language-ts">// Pure function: same input always gives the same decision
computeRateLimitDecision(existing, now, limit, windowMs)
</code></pre>
<p>I used MongoDB instead of memory because on Vercel each request can run on a different serverless instance. A counter in memory would reset all the time.</p>
<hr />
<h2>Features Built on Top of the Score</h2>
<p>Once the score existed, new features were easy, because they all use the same analysis service.</p>
<h3>Leaderboard</h3>
<p>Every successful analysis updates a snapshot. The leaderboard shows a podium for the top developers, with search and pagination.</p>
<h3>Compare</h3>
<p>Enter two usernames and see them side by side. I made one rule: <strong>no one is "better"</strong>. The page only shows measured differences, and highlights which dimensions caused the score gap, so the result is explainable.</p>
<h3>Improve</h3>
<p>This gives a roadmap with three groups: Quick Wins, Short-term and Long-term. Every tip is triggered by a real measured problem. If your repos have no licence, you get a licence tip. If nothing is wrong, nothing is shown. There is no generic "code more" advice, and no fake promises like "+100 points".</p>
<hr />
<h2>Handling Failures Without Crashing</h2>
<p>A rule I followed everywhere: <strong>expected failures are returned, not thrown.</strong></p>
<pre><code class="language-ts">// Instead of throwing, return a typed result
{ ok: true, analysis }
{ ok: false, kind: "not_found" | "upstream_error", message }
</code></pre>
<p>A username that does not exist, a GitHub rate limit and a network error all become clear results that the API can turn into the right HTTP response. Even a database problem will not block a user: if the snapshot save fails, the roast is still delivered.</p>
<hr />
<h2>Testing: Properties Instead of Examples</h2>
<p>I have 289 tests across 22 suites. The part I enjoyed most was <strong>property-based testing</strong> with fast-check.</p>
<p>Normal tests check one example: "this profile should give 640". Property tests check rules that must always be true, for thousands of random inputs:</p>
<ul>
<li>Total score is always a whole number between 0 and 1000.</li>
<li>Each dimension is between 0 and its maximum.</li>
<li>Total equals the sum of all dimensions.</li>
<li>All maximums add up to exactly 1000.</li>
<li>Same input always gives the same score.</li>
</ul>
<p>This caught edge cases I would never have written by hand, like empty profiles and missing fields.</p>
<hr />
<h2>Challenges and What I Learned</h2>
<p><strong>1. Keeping AI creative but controlled.</strong> The fix was giving the AI the words and keeping the numbers for code.</p>
<p><strong>2. Making scoring feel fair.</strong> Logarithmic curves and capped estimates made a big difference.</p>
<p><strong>3. Serverless has no memory.</strong> Anything that must survive between requests (cache, rate limits) has to live in a database.</p>
<p><strong>4. Small details make it feel real.</strong> Slugs like <code>username-x7k92</code> make every roast link short, unique and shareable.</p>
<p><strong>5. Plan first.</strong> Writing requirements before code saved me from rewriting things later.</p>
<hr />
<h2>What's Next</h2>
<ul>
<li>More signals in the score, like pull request and review activity.</li>
<li>Better contribution data without needing extra tokens.</li>
<li>More roast personalities.</li>
</ul>
<hr />
<h2>Try It</h2>
<p>Enter your username and see where you stand. If you disagree with your score, the code is open, so you can see exactly how it was calculated.</p>
<ul>
<li>Live demo: <a href="https://git-roasted-ai.vercel.app">git-roasted-ai.vercel.app</a></li>
<li>GitHub repo: <a href="https://github.com/ahmmikun/GitRoasted">ahmmikun/GitRoasted</a></li>
</ul>
<hr />
<p><strong>About the author</strong></p>
<p>I'm Salman Ahmad, a developer who enjoys building things people can actually use.</p>
<ul>
<li>Portfolio: <a href="https://salmanahmad.tech">salmanahmad.tech</a></li>
<li>GitHub: <a href="https://github.com/ahmmikun">@ahmmikun</a></li>
<li>LinkedIn: <a href="https://www.linkedin.com/in/ahmmikun">in/ahmmikun</a></li>
<li>X: <a href="https://x.com/ahmmikun">@ahmmikun</a></li>
</ul>
]]></content:encoded></item></channel></rss>