<?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: ujja</title>
    <description>The latest articles on DEV Community by ujja (@ujja).</description>
    <link>https://dev.to/ujja</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%2F439943%2F774d42c6-75e1-4247-8688-30020ce8f40a.png</url>
      <title>DEV Community: ujja</title>
      <link>https://dev.to/ujja</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ujja"/>
    <language>en</language>
    <item>
      <title>AI Engineering Is Easy. So How Do We Change How We Work?</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Thu, 15 Oct 2026 00:24:59 +0000</pubDate>
      <link>https://dev.to/ujja/ai-engineering-is-easy-so-how-do-we-change-how-we-work-3lik</link>
      <guid>https://dev.to/ujja/ai-engineering-is-easy-so-how-do-we-change-how-we-work-3lik</guid>
      <description>&lt;p&gt;In &lt;a href="https://dev.to/ujja/ai-engineering-is-easy-changing-how-we-work-is-hard-39j4"&gt;Part 1&lt;/a&gt;, I wrote about the uncomfortable part of AI-assisted engineering: the tools are becoming remarkably good at producing things, while the harder question is what happens around them.&lt;/p&gt;

&lt;p&gt;If an agent can write code in minutes, generate a plan in seconds, and create dozens of tasks before we've finished our coffee, our old way of working starts to look a little strange. Not because the process was bad, but because the bottleneck has moved.&lt;/p&gt;

&lt;p&gt;We used to spend much of our time producing things. Now we need to spend more time deciding what should be produced, providing the right context, and verifying whether the result is actually what we wanted.&lt;/p&gt;

&lt;p&gt;Recognising the need for change is one thing; figuring out how to make it without creating even more process is another.&lt;/p&gt;

&lt;p&gt;I don't think we need an entirely new engineering methodology for AI. We need to make a few important parts of our existing process explicit and automate what we can. I think of this as &lt;strong&gt;building a harness around the agent&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the work, not the agent
&lt;/h2&gt;

&lt;p&gt;It's tempting to start with questions like, "Which agent should we use?" or "What's the perfect prompt?" I think that is backwards.&lt;/p&gt;

&lt;p&gt;Start with the unit of work. What does the agent receive? What can it change? What does "done" mean? How do we know the result is correct, and what happens when it isn't?&lt;/p&gt;

&lt;p&gt;These questions matter whether you're using Claude, Codex, Cursor, an internal agent, or something that doesn't exist yet. The tools will change, but the underlying method shouldn't have to.&lt;/p&gt;

&lt;p&gt;A useful agent workflow has six basic components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Context&lt;/strong&gt; — what the agent needs to know&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tools&lt;/strong&gt; — what it can act on&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loop&lt;/strong&gt; — what one unit of work looks like&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verification&lt;/strong&gt; — how we establish correctness&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guardrails&lt;/strong&gt; — what it must not touch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt; — what happened and what changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need a sophisticated platform to start. You just need to be explicit about these six things.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Make the boundaries boringly clear
&lt;/h2&gt;

&lt;p&gt;Giving an agent access to everything can make the first demo impressive, but it can also make the first mistake much more expensive.&lt;/p&gt;

&lt;p&gt;Before expanding its access, establish clear boundaries around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repositories and branches it can modify&lt;/li&gt;
&lt;li&gt;Environments it can access and whether it can deploy&lt;/li&gt;
&lt;li&gt;Secrets and external systems it can access&lt;/li&gt;
&lt;li&gt;Actions that always require human approval&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The exact list will vary, but the boundaries should be documented and enforced wherever possible.&lt;/p&gt;

&lt;p&gt;If the answer depends on who's running the agent, you don't have a boundary; you have a convention. Conventions are easy to lose when people, tools, and teams change.&lt;/p&gt;

&lt;p&gt;As agents become more capable, being deliberate about where their autonomy ends becomes just as important as deciding what they can do.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Stop treating gates as documentation
&lt;/h2&gt;

&lt;p&gt;A process can have ten gates written down and still have none in practice. If someone can skip a step simply by forgetting it, it's an instruction rather than a gate.&lt;/p&gt;

&lt;p&gt;A useful gate has three things: a condition that must be satisfied, someone accountable for checking it, and a mechanism that makes skipping it visible.&lt;/p&gt;

&lt;p&gt;Automate what you can. Run specification checks automatically, validate consistency between artefacts, and stop the workflow when a required check fails.&lt;/p&gt;

&lt;p&gt;Other decisions genuinely need human judgement, such as approving scope or determining whether a change meets the customer's needs.&lt;/p&gt;

&lt;p&gt;The goal isn't to automate every decision. It's to automate what doesn't require one and make the remaining decisions explicit. That should reduce the burden on people rather than add another layer of process.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Don't put humans in the loop everywhere
&lt;/h2&gt;

&lt;p&gt;"Human in the loop" can easily become "human reads everything the agent does". If someone has to inspect every intermediate step, we've simply added an AI assistant to the old workflow and asked a person to supervise every keystroke.&lt;/p&gt;

&lt;p&gt;Instead, involve people where judgement matters and let automated checks handle what they can.&lt;/p&gt;

&lt;p&gt;A workflow might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Agent produces
      ↓
Automated checks
      ↓
Human reviews decisions requiring judgement
      ↓
Agent continues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The human needs to determine whether the direction is right, the result matches the intended outcome, and the work is safe to continue.&lt;/p&gt;

&lt;p&gt;This is also why the unit of work matters. A large ticket gives the agent a large surface area for mistakes, while a small, independently verifiable slice makes the work easier to understand and review.&lt;/p&gt;

&lt;p&gt;The objective isn't to remove humans from the workflow, but to reserve their attention for decisions that benefit from their experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Make scope impossible to misunderstand
&lt;/h2&gt;

&lt;p&gt;Agents are very good at doing exactly what you asked, which isn't always the same as doing what you meant.&lt;/p&gt;

&lt;p&gt;For every piece of work, make three things explicit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What's included&lt;/strong&gt; in the scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What's excluded&lt;/strong&gt; from the scope&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What defines done&lt;/strong&gt;, including the evidence required&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Exclusions are particularly important because a future-state idea can quietly become an implementation requirement while everyone assumes they're still discussing the same thing.&lt;/p&gt;

&lt;p&gt;One piece of product intent can produce many implementation slices. The slice isn't the feature; it's the unit in which we deliver and verify part of it.&lt;/p&gt;

&lt;p&gt;Without clear scope, an agent can optimise for completing the task in front of it rather than delivering the outcome we actually wanted. Good specifications help constrain those assumptions without requiring us to prescribe every implementation detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Turn repeated prompts into checks
&lt;/h2&gt;

&lt;p&gt;If you keep telling an agent, "Remember to check X," ask whether X should actually be a check.&lt;/p&gt;

&lt;p&gt;Prompts help communicate intent, but automated checks are more reliable for enforcing objectively verifiable requirements.&lt;/p&gt;

&lt;p&gt;If a requirement is testable, test it. If two documents need to agree, compare them. If a branch must not be modified directly, enforce it. If a file must never be changed, block it. If evidence is required before completion, check for it.&lt;/p&gt;

&lt;p&gt;Every time you add another paragraph to a prompt, consider whether that instruction could become an automated check instead.&lt;/p&gt;

&lt;p&gt;Not everything can be automated, and prompts will remain useful for communicating context and intent. But when correctness can be checked mechanically, relying on the agent to remember an instruction is often weaker than enforcing it in the workflow.&lt;/p&gt;

&lt;p&gt;Over time, prompts can become smaller while the system becomes more reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Stop keeping the important context in people's heads
&lt;/h2&gt;

&lt;p&gt;Another pattern that doesn't scale is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I've got a really good prompt that I use with my agent."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That's useful for an individual, but it's not a team process.&lt;/p&gt;

&lt;p&gt;If an agent needs specific context to work correctly, that context should live somewhere shared, versioned, and reviewable. The same applies to skills, instructions, and workflows.&lt;/p&gt;

&lt;p&gt;Otherwise, everyone builds their own operating system for their agent, and the team ends up with different behaviours while assuming everyone follows the same process.&lt;/p&gt;

&lt;p&gt;Shared context doesn't mean identical prompts for every team. It means important rules shouldn't depend on someone's local setup.&lt;/p&gt;

&lt;p&gt;We don't need to document everything, but information that repeatedly affects decisions should be available where the work happens. If it's important enough for an agent to make the right decision, it's probably important enough for the team to maintain and review.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Treat verification as a separate capability
&lt;/h2&gt;

&lt;p&gt;As generation becomes cheaper and faster, verification becomes an increasingly important part of the workflow.&lt;/p&gt;

&lt;p&gt;Asking the same model that produced something to confirm its correctness isn't enough. A confident explanation isn't independent evidence.&lt;/p&gt;

&lt;p&gt;Depending on the work, verification might involve automated tests, static analysis, schema validation, comparisons against specifications, independent reviews, acceptance testing, or evaluations against known examples.&lt;/p&gt;

&lt;p&gt;The principle is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't measure how much the agent produced. Measure how confidently you can accept what it produced.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A thousand generated lines aren't particularly useful if nobody can establish that they're the right thousand lines. More output can even increase the review burden.&lt;/p&gt;

&lt;p&gt;Productivity should therefore include how quickly we move from generation to justified acceptance, not just how quickly we generate something.&lt;/p&gt;

&lt;p&gt;Building verification into small, independent work units also makes it easier to catch problems early, understand what went wrong, and correct them before they spread.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Make the workflow survive a tooling change
&lt;/h2&gt;

&lt;p&gt;Building the entire engineering method around one CLI or agent creates unnecessary coupling. Tools, commands, and models will change, so the underlying workflow shouldn't need to change with every new release.&lt;/p&gt;

&lt;p&gt;Instead of defining the method as "run command X, then Y, then Z", define the work itself: specify behaviour, resolve ambiguity, design the solution, decompose the work, implement a small slice, and validate it.&lt;/p&gt;

&lt;p&gt;Then map those activities to whichever tools the team uses.&lt;/p&gt;

&lt;p&gt;Today that might be one agent; tomorrow it might be another or a combination of agents and conventional automation. The workflow remains useful because it's based on engineering activities rather than a particular command sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Start with the smallest useful harness
&lt;/h2&gt;

&lt;p&gt;You don't need to redesign engineering before using AI. Start with one workflow and answer six questions:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Context:&lt;/strong&gt; What does the agent need to know, and where can others find and review it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tools:&lt;/strong&gt; What can it do, and which capabilities should remain outside its reach?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Loop:&lt;/strong&gt; What's the smallest independently deliverable unit that creates useful progress?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification:&lt;/strong&gt; How do we know the result is correct rather than merely plausible?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Guardrails:&lt;/strong&gt; What must it never touch, regardless of who's running it?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Observability:&lt;/strong&gt; What do we record so others can understand what happened, what changed, and why?&lt;/p&gt;

&lt;p&gt;Once these are clear, try the workflow where the pain is already visible. You don't need to start with every team, repository, or a 40-page AI engineering policy.&lt;/p&gt;

&lt;p&gt;Run it, identify where people still compensate manually, and fix those points before expanding the approach. The harness should evolve from actual friction rather than an attempt to design the perfect process upfront.&lt;/p&gt;

&lt;h2&gt;
  
  
  The goal isn't more process
&lt;/h2&gt;

&lt;p&gt;There's a natural reaction to all of this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"So we're adding more gates, more checks, more documentation and more meetings?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That would miss the point.&lt;/p&gt;

&lt;p&gt;Some meetings should become automated checks, some knowledge should become shared context, some repeated prompt instructions should become enforceable rules, and some large tickets should become smaller slices. Other work people currently do manually should simply disappear.&lt;/p&gt;

&lt;p&gt;The process should become more explicit but less heavy.&lt;/p&gt;

&lt;p&gt;There's also an irony here: AI makes it easier to generate plans, documentation, tickets, and checks, but that doesn't mean we need more of them. We still need to decide which artefacts and controls actually create value.&lt;/p&gt;

&lt;p&gt;The opportunity isn't just getting a faster developer. It's redesigning the system that developer works inside, including the parts we've accepted for years simply because nobody had a compelling reason to change them.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical way to get started
&lt;/h2&gt;

&lt;p&gt;If I were starting from scratch, I'd take these steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pick one workflow&lt;/strong&gt; rather than trying to change the entire organisation at once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Define the six harness components:&lt;/strong&gt; context, tools, loop, verification, guardrails, and observability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set the agent's boundaries&lt;/strong&gt; and identify the three most important gates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automate what can be checked automatically&lt;/strong&gt;, especially repetitive checks people often forget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assign an owner to every remaining gate&lt;/strong&gt; so decisions have clear accountability.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make scope and exclusions explicit&lt;/strong&gt; rather than leaving the agent to guess what "done" means.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Break work into independently verifiable slices&lt;/strong&gt; to keep the impact of mistakes manageable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate generation from verification&lt;/strong&gt; rather than relying on the agent to grade its own homework.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure where humans still get stuck&lt;/strong&gt; and use those points to guide the next improvement.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The point isn't to build a perfect AI engineering process. It's to make the system good enough that, as agents improve, we can benefit from their capabilities instead of discovering that our surrounding process has become the new bottleneck.&lt;/p&gt;

&lt;h2&gt;
  
  
  The next bottleneck
&lt;/h2&gt;

&lt;p&gt;AI-assisted engineering isn't primarily a tooling problem. The tools will continue to improve, but we still need to decide how much autonomy we want, where human judgement belongs, what evidence is enough to trust a result, and how to make those decisions visible to the team.&lt;/p&gt;

&lt;p&gt;The teams that benefit most won't necessarily be the ones with the most powerful agents. They'll be the ones whose engineering systems provide clear work, sufficient context, appropriate boundaries, useful feedback loops, and reliable ways to verify the results.&lt;/p&gt;

&lt;p&gt;That's what the harness is for: making it possible to let agents move faster without turning every increase in autonomy into more risk and review overhead.&lt;/p&gt;

&lt;p&gt;We may spend less time producing code, plans, and documentation by hand, but we'll still need to think carefully about intent, boundaries, trade-offs, and evidence.&lt;/p&gt;

&lt;p&gt;AI can make the work of building easier. Changing how we work is still ours to do.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwareengineering</category>
      <category>discuss</category>
      <category>agents</category>
    </item>
    <item>
      <title>I Built a Political Accountability Agent That Queries Sanity</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Sun, 04 Oct 2026 11:51:16 +0000</pubDate>
      <link>https://dev.to/ujja/i-built-a-political-accountability-agent-that-queries-sanity-404n</link>
      <guid>https://dev.to/ujja/i-built-a-political-accountability-agent-that-queries-sanity-404n</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path One: Ship an Agent That Queries Real Content&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;True Oath is a political accountability ledger starting with Australia and designed to expand to other countries. It is designed to answer a question that ordinary keyword search cannot answer reliably:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What did a political party promise, what evidence shows what happened next, and where is the evidence still incomplete or contradictory?&lt;/p&gt;
&lt;/blockquote&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%2F4h15ug8b2s1lyxutv5ik.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%2F4h15ug8b2s1lyxutv5ik.png" alt="Image1" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project models political accountability as linked, source-grounded records rather than as a list of unverified claims. A promise is connected to its manifesto, government, evidence records, dates, status, confidence, and source URLs.&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%2Fb7xikaobukg0roxkhdcj.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%2Fb7xikaobukg0roxkhdcj.png" alt="Image8" width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The first jurisdiction is Australia. The initial evidence plan combines:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;election manifestos and policy documents;&lt;/li&gt;
&lt;li&gt;Australian Parliamentary Budget Office election-commitment costings;&lt;/li&gt;
&lt;li&gt;federal budgets and budget papers;&lt;/li&gt;
&lt;li&gt;parliamentary records and legislation;&lt;/li&gt;
&lt;li&gt;departmental and statutory-authority reports;&lt;/li&gt;
&lt;li&gt;official outcome statistics; and&lt;/li&gt;
&lt;li&gt;carefully labelled independent assessments.&lt;/li&gt;
&lt;/ul&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%2F28lzavzp8pzr91sat09v.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%2F28lzavzp8pzr91sat09v.png" alt="Image6" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The interface presents each promise with a status such as &lt;code&gt;Kept&lt;/code&gt;, &lt;code&gt;Partially kept&lt;/code&gt;, &lt;code&gt;In progress&lt;/code&gt;, &lt;code&gt;Not started&lt;/code&gt;, &lt;code&gt;Broken&lt;/code&gt;, &lt;code&gt;Reversed&lt;/code&gt;, or &lt;code&gt;Unverifiable&lt;/code&gt;. The system is intentionally conservative: an absent search result is not treated as proof that a promise was broken.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I Built It
&lt;/h3&gt;

&lt;p&gt;Election season is arriving in one country after another, and the question many voters eventually ask is simple: what actually happened? Campaign language is memorable, but the details are scattered across budget papers, legislation, departmental updates, audits, statistics, court records, and later explanations. It is easy to remember a promise and much harder to audit it fairly.&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%2F4ahgwlr9z5md7edqbfcr.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%2F4ahgwlr9z5md7edqbfcr.png" alt="Image2" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;True Oath is my attempt to make that audit navigable. The point is not to manufacture a score for every political claim. It is to preserve the promise, follow the evidence, show contradictions, and leave uncertainty visible when the record is incomplete. &lt;strong&gt;&lt;em&gt;Can Sanity help us keep our sanity?&lt;/em&gt;&lt;/strong&gt; That is the experiment, with the pun fully intended.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The True Oath Sanity Studio is deployed at:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://true-oath.sanity.studio/" rel="noopener noreferrer"&gt;https://true-oath.sanity.studio/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Next.js web app is deployed to Vercel from the &lt;code&gt;web/&lt;/code&gt; directory:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://true-oath.vercel.app/" rel="noopener noreferrer"&gt;https://true-oath.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is named &lt;code&gt;true-oath&lt;/code&gt;, the clean &lt;code&gt;true-oath.vercel.app&lt;/code&gt; domain is publicly reachable, and Vercel SSO deployment protection has been disabled for this read-only demo. During development, run it locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;web
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its first screen is an investigative ledger: readers can search and filter the four promise files, select a case file, inspect confidence and linked evidence counts, and jump to the integrity method. The dashboard now includes a status-distribution chart, evidence-depth bars, and an average-confidence readout so the corpus can be scanned before a reader opens an individual file. Lucide icons, responsive layouts, keyboard-friendly controls, status colors, subtle motion, and mobile overflow states are included in the interface.&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%2Fo4x9jbqcrc52h8xbrbfs.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%2Fo4x9jbqcrc52h8xbrbfs.png" alt="Image3" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The Sanity dataset contains the first independently collected Australia corpus: 15 public sources, 4 promises, 4 evidence records, 4 milestones, 4 outcome indicators, 4 independent assessments, 3 competing claims, 1 manifesto, 1 government record, and 4 integrity events. The &lt;code&gt;production&lt;/code&gt; dataset is now public-read for judging and API inspection. Public reads do not grant anonymous editing or uploads.&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%2Foojv6zqnc7brzvjvz2wc.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%2Foojv6zqnc7brzvjvz2wc.png" alt="Image 7" width="800" height="406"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/ujjavala/true-oath" rel="noopener noreferrer"&gt;https://github.com/ujjavala/true-oath&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The repository contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;web/&lt;/code&gt; — the Next.js accountability interface;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sanity/&lt;/code&gt; — the Sanity Studio and schema;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sanity/schemaTypes/index.ts&lt;/code&gt; — the structured content model;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;README.md&lt;/code&gt; — local setup and deployment notes.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project currently builds with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;web &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run lint
&lt;span class="nb"&gt;cd &lt;/span&gt;sanity &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Used Sanity
&lt;/h2&gt;

&lt;p&gt;Sanity is the evidence layer for True Oath. The Studio schema separates the main concepts instead of flattening them into one article:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;government&lt;/code&gt; records the governing party or coalition and its time period;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;manifesto&lt;/code&gt; records the election document and the party that published it;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;promise&lt;/code&gt; records a specific commitment and its current assessment;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;source&lt;/code&gt; preserves the publisher, URL, date, and source category;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;evidence&lt;/code&gt; records a dated finding and whether it supports, partially supports, contradicts, or is neutral toward a promise;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;milestone&lt;/code&gt; records announcements, funding, legislation, starts, delays, cancellations, and delivery events;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;indicator&lt;/code&gt; records measurable outcomes against a defined baseline, target, period, and unit;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;assessment&lt;/code&gt; records an independent verdict and its reasoning; and&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;claim&lt;/code&gt; records competing explanations or interpretations without silently treating them as verified facts.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;integrityEvent&lt;/code&gt; records corruption and integrity signals separately from promise verdicts, including mechanism, evidence status, official-finding status, confidence, source, and whether a demonstrated effect on implementation exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current promise records are linked to milestones, outcome indicators, independent assessments, and competing claims. That means an agent can distinguish “a policy was announced”, “a measurable output changed”, “an assessor gave a verdict”, and “a government or public interpretation explains why” instead of collapsing those into one status field.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sanity Services Used
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Sanity Studio and schema.&lt;/strong&gt; I used a hosted Sanity Studio as the editorial surface for the project. The schema is the first important design decision: it keeps a campaign promise separate from the source that announced it, the evidence that tests it, the milestone that records implementation, the indicator that measures an outcome, and the assessment that explains a verdict. The model also has &lt;code&gt;claim&lt;/code&gt; and &lt;code&gt;integrityEvent&lt;/code&gt; documents so competing explanations and corruption-related records are not silently merged into a promise score.&lt;/p&gt;

&lt;p&gt;The core promise model is defined in &lt;code&gt;sanity/schemaTypes/index.ts&lt;/code&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="nf"&gt;defineType&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;promise&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;document&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;title&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;status&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;baseline&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;target&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;milestones&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;milestone&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;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;indicators&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;of&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;indicator&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="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Content Lake and dataset.&lt;/strong&gt; The structured records live in the separate True Oath project &lt;code&gt;wak4l160&lt;/code&gt;, dataset &lt;code&gt;production&lt;/code&gt;. The dataset is public-read so judges can inspect the records, but writes still require authorization. The current corpus has 57 documents: sources, promises, evidence, milestones, indicators, assessments, claims, integrity events, a manifesto, and a government record.&lt;/p&gt;

&lt;p&gt;I imported the public records as linked Sanity documents instead of flattening them into one JSON blob:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx sanity documents create data/australia.json &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--project-id&lt;/span&gt; wak4l160 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--dataset&lt;/span&gt; production &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--replace&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes relationships queryable. For example, an assessment can point to a promise and its source, while an indicator can preserve the measurement period and unit independently from the political language that motivated it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Embeddings.&lt;/strong&gt; Dataset embeddings are enabled for the accountability text fields. This is intended to support conceptual retrieval when a question uses different language from the source, while the structured fields and GROQ-style filtering preserve exact relationships and status values.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sanity Context and MCP.&lt;/strong&gt; The project has a Context endpoint at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://api.sanity.io/v1/context/organizations/oqf9m6vy6/mcp/true-oath-context
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The agent client in &lt;code&gt;agent/mcp-client.mjs&lt;/code&gt; speaks JSON-RPC over MCP. It initializes a session, lists the tools, then supports both the GROQ-style route and the Knowledge Base route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tools&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tools/list&lt;/span&gt;&lt;span class="dl"&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;available&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;groq_query&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="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tools/call&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;groq_query&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;accountabilityQuery&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;else&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;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tools/call&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;initial_context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;arguments&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="c1"&gt;// Extract the Knowledge Base id, then search its entries.&lt;/span&gt;
  &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;call&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tools/call&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="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;knowledge_base_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;knowledgeBase&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;promise evidence government milestone assessment integrity&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;return&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;entries&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint currently exposes the Knowledge Base tools &lt;code&gt;initial_context&lt;/code&gt;, &lt;code&gt;knowledge_base_search&lt;/code&gt;, and &lt;code&gt;knowledge_base_read&lt;/code&gt;. The Knowledge Base exists as &lt;code&gt;kbgnQdlEqXlP&lt;/code&gt;, but its build is blocked by the organization’s beta index quota and currently reports zero entries. The public dataset and embeddings are ready; the remaining setup is to rebuild the Knowledge Base when quota is available or switch the Context endpoint source to &lt;code&gt;wak4l160.production&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sanity CLI and deployment.&lt;/strong&gt; The Sanity CLI is used for schema builds, hosted Studio deployment, document import, dataset visibility checks, and embedding setup. The Studio is deployed at &lt;a href="https://true-oath.sanity.studio/" rel="noopener noreferrer"&gt;https://true-oath.sanity.studio/&lt;/a&gt;. The web experience is a separate Next.js app deployed at &lt;a href="https://true-oath.vercel.app/" rel="noopener noreferrer"&gt;https://true-oath.vercel.app/&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sanity Functions and Agent Actions
&lt;/h3&gt;

&lt;p&gt;The project now uses Sanity's native automation layer for evidence-quality&lt;br&gt;
review. A &lt;code&gt;reviewTask&lt;/code&gt; document type gives the Studio a durable queue for&lt;br&gt;
missing sources, broken references, stale or incomplete evidence, and integrity&lt;br&gt;
status checks. The custom Studio Structure menu puts &lt;code&gt;Review queue&lt;/code&gt; first and&lt;br&gt;
filters it to open and in-review tasks.&lt;/p&gt;

&lt;p&gt;The Blueprint in &lt;code&gt;sanity/sanity.blueprint.ts&lt;/code&gt; provisions the&lt;br&gt;
&lt;code&gt;true-oath-review-automation&lt;/code&gt; Stack, a project-scoped robot token, and the&lt;br&gt;
&lt;code&gt;review-queue-on-content-change&lt;/code&gt; document Function. It watches published&lt;br&gt;
evidence, milestones, indicators, assessments, and integrity events. When a&lt;br&gt;
document needs attention, the Function creates a deduplicated task containing&lt;br&gt;
the affected document reference, severity, explanation, and suggested next&lt;br&gt;
step. It never changes a promise status or publishes a political verdict.&lt;/p&gt;

&lt;p&gt;Agent Actions provide a second, deliberately constrained workflow. The helper&lt;br&gt;
in &lt;code&gt;sanity/scripts/draft-review-agent-action.mjs&lt;/code&gt; uses Sanity's schema-aware&lt;br&gt;
Generate action to draft &lt;code&gt;reviewerNotes&lt;/code&gt; for a selected review task. The action&lt;br&gt;
requires the deployed schema ID and a server-side token, and Sanity's default&lt;br&gt;
draft-only behavior remains enabled. A human must review the note and decide&lt;br&gt;
whether to resolve, dismiss, or keep the task open.&lt;/p&gt;

&lt;p&gt;This gives True Oath event-driven review, schema-aware drafting, and an&lt;br&gt;
inspectable Studio workflow without asking AI to decide whether a promise was&lt;br&gt;
kept. The Function and robot token were deployed through the&lt;br&gt;
&lt;code&gt;true-oath-review-automation&lt;/code&gt; Blueprint Stack and verified as completed.&lt;/p&gt;
&lt;h3&gt;
  
  
  Challenges With Sanity
&lt;/h3&gt;

&lt;p&gt;The hardest part was not storing documents; it was choosing boundaries that preserve uncertainty. A budget measure is not the same thing as a delivered outcome, and a government explanation is not automatically an independent finding. That is why the schema has separate milestones, indicators, assessments, claims, and integrity events.&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%2Fe03nmofq4bggelvl38nc.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%2Fe03nmofq4bggelvl38nc.png" alt="Image4" width="800" height="435"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There was also a tooling mismatch to handle. The first agent draft expected a &lt;code&gt;groq_query&lt;/code&gt; tool, but the configured Knowledge Base endpoint exposed &lt;code&gt;initial_context&lt;/code&gt;, &lt;code&gt;knowledge_base_search&lt;/code&gt;, and &lt;code&gt;knowledge_base_read&lt;/code&gt;. The client now detects the available tool contract instead of assuming one response shape. Finally, Context authentication is organisation-scoped, while ordinary Content API tokens are project-scoped; keeping those credentials separate is important for both security and a working MCP connection.&lt;/p&gt;

&lt;p&gt;The intended corpus is deliberately broad. It will combine election material with budgets, legislation, Hansard, committee reports, audits, regulator and court records, departmental progress reports, statistical releases, program dashboards, procurement records, official explanations, independent assessments, and competing claims. This lets the agent investigate not only whether a promise was met, but what changed, why delivery may have slipped, whether the stated reason is supported, and whether different sources contradict one another.&lt;/p&gt;

&lt;p&gt;The integrity slice adds public NACC and ANAO material. It includes an official bribery finding connected to contract bidding, an official misuse-of-office finding, a conflict-of-interest audit, and a NACC case where a perceived conflict was not substantiated as abuse of office. The last example is intentional: True Oath must be able to say that a claim was investigated and not proven. It should not call something “rigged” merely because a news report or political claim uses that language. The agent must preserve the source's status and only connect an integrity event to a promise when the evidence establishes that link.&lt;/p&gt;

&lt;p&gt;The intended Path One agent will connect to the True Oath Sanity Context MCP endpoint. It will retrieve the relevant promises and evidence, follow their source references, compare claims across documents, and answer questions such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which 2022 Australian federal promises are still in progress?&lt;/li&gt;
&lt;li&gt;Which commitments have official budget or legislative evidence?&lt;/li&gt;
&lt;li&gt;Which promises have conflicting evidence from different sources?&lt;/li&gt;
&lt;li&gt;Which assessments should remain &lt;code&gt;Unverifiable&lt;/code&gt; because the promise was not specific enough?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the part that requires structured content. A keyword search can find the word “housing”, but it cannot reliably follow the relationship between a manifesto commitment, a budget measure, a later law, and an outcome assessment.&lt;/p&gt;

&lt;p&gt;Planned Context configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Source: True Oath Sanity production dataset
Scope: Australia political promises and evidence
Retrieval: Sanity Context MCP
Output rule: preserve source links, dates, confidence, and unresolved conflicts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Project ID: &lt;code&gt;wak4l160&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Dataset: &lt;code&gt;production&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Dataset visibility: &lt;code&gt;public&lt;/code&gt; (read-only to unauthenticated API clients; writes still require authorization)&lt;/li&gt;
&lt;li&gt;Organization: &lt;code&gt;oqf9m6vy6&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Hosted Studio: &lt;a href="https://true-oath.sanity.studio/" rel="noopener noreferrer"&gt;https://true-oath.sanity.studio/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Studio app ID: &lt;code&gt;uk5zu82laqqn2uoecp2yikrq&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Knowledge Base public ID: &lt;code&gt;kbgnQdlEqXlP&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Blueprint Stack: &lt;code&gt;true-oath-review-automation&lt;/code&gt; (&lt;code&gt;ST-rietwjrd38&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Deployed Function: &lt;code&gt;review-queue-on-content-change&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Public dataset inspection endpoint:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://wak4l160.api.sanity.io/v2025-08-15/data/query/production?query=count%28%2A%29" rel="noopener noreferrer"&gt;https://wak4l160.api.sanity.io/v2025-08-15/data/query/production?query=count%28%2A%29&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The True Oath read-only MCP endpoint now exists at &lt;code&gt;https://api.sanity.io/v1/context/organizations/oqf9m6vy6/mcp/true-oath-context&lt;/code&gt;. The endpoint is authenticated and currently points to the created &lt;code&gt;kbgnQdlEqXlP&lt;/code&gt; Knowledge Base, but that Knowledge Base reports 0 entries because the organization's beta index quota is exhausted. The final agent run therefore still requires either rebuilding that Knowledge Base after quota is available or switching the endpoint source to the public &lt;code&gt;wak4l160.production&lt;/code&gt; dataset with embeddings and the accountability GROQ filter.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Comes Next
&lt;/h2&gt;

&lt;p&gt;True Oath starts with a small Australian corpus, but the underlying model is meant to travel. The next step is to add more federal elections, then extend the same source-linked structure to state governments and other countries where reliable public records are available. Each new jurisdiction would bring its own election documents, budget conventions, laws, agencies, statistical releases, and standards for official findings.&lt;/p&gt;

&lt;p&gt;Future versions could add time-series indicators, parliamentary voting histories, procurement and grant data, scheduled milestone reminders, and comparison views that show how different sources describe the same event. The current Function queue and draft Agent Action create a foundation for those extensions, while a richer agent could explain why two claims conflict, identify the missing document that would resolve an uncertainty, and carry a human reviewer’s resolution forward as a durable editorial decision in Sanity.&lt;/p&gt;

&lt;p&gt;The hard problems are as important as the features. A promise can be vague, a target can change, an outcome can be affected by events outside a government’s control, and a media report can flatten a complicated audit into a dramatic headline. Sources can disagree without one being obviously false. That is why True Oath keeps confidence, provenance, dates, status reasons, and “no finding” cases visible instead of forcing every record into a binary score. The current Knowledge Base indexing quota and the work required to curate a public MCP session are practical constraints too.&lt;/p&gt;

&lt;p&gt;The goal is not to replace political judgment with an automated verdict. It is to make judgment better informed, easier to inspect, and easier to revise when the evidence changes. If Sanity can keep the content structured and the agent can keep its claims tied to that structure, voters get something more useful than another feed of opinions: a living record they can question.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>A Sanity Check for AI-Generated Cyber Attack Reconstructions</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Sat, 03 Oct 2026 08:31:17 +0000</pubDate>
      <link>https://dev.to/ujja/a-sanity-check-for-ai-generated-cyber-attack-reconstructions-31bm</link>
      <guid>https://dev.to/ujja/a-sanity-check-for-ai-generated-cyber-attack-reconstructions-31bm</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/sanity-2026-09-16"&gt;Sanity Challenge, Path Two: Vibe-Code Something Strange&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Cyber Autopsy is a cyber-forensics investigation room for asking structured&lt;br&gt;
questions about an incident: what happened, what came before it, which evidence&lt;br&gt;
supports it, what remains unknown, and what was knowable at a specific point in&lt;br&gt;
time.&lt;/p&gt;

&lt;p&gt;The project started as a Kaggle-style benchmark for evaluating whether an AI&lt;br&gt;
can reconstruct an attack from fragmented reports without presenting guesses as&lt;br&gt;
facts. I added Sanity as the structured content layer, an MCP-compatible&lt;br&gt;
investigation server, and a forensic-themed web interface.&lt;/p&gt;

&lt;p&gt;The core model preserves the relationships that make an investigation useful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Evidence ──supports──&amp;gt; Event ──precedes/enables/causes──&amp;gt; Event
Evidence ──supports/contradicts──&amp;gt; Claim
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The UI presents each case as a small investigation console. Investigators can&lt;br&gt;
select a case, read its incident description, ask a question, and inspect the&lt;br&gt;
result as a reconstruction, structured event relationships, uncertainty list,&lt;br&gt;
contradictory claims, and expandable evidence with source provenance.&lt;/p&gt;
&lt;h3&gt;
  
  
  Why this helps cybersecurity investigations
&lt;/h3&gt;

&lt;p&gt;Cybersecurity autopsy is an evidence-reconstruction problem. Analysts rarely&lt;br&gt;
receive one perfect timeline; they receive authentication logs, endpoint&lt;br&gt;
observations, threat reports, analyst notes, negative findings, and competing&lt;br&gt;
claims. The difficult part is deciding how those pieces relate without&lt;br&gt;
overstating what they prove.&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%2Fpimzb8i698dsdnomb2y5.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%2Fpimzb8i698dsdnomb2y5.png" alt="Image1" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Cyber Autopsy uses Sanity to make that reasoning inspectable. An analyst can&lt;br&gt;
follow an event back to its supporting evidence, move through the attack graph,&lt;br&gt;
check whether a claim is contradicted, and distinguish &lt;code&gt;confirmed&lt;/code&gt;, &lt;code&gt;inferred&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;attempted&lt;/code&gt;, and &lt;code&gt;unknown&lt;/code&gt;. This is useful for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;reconstructing initial access, lateral movement, persistence, exfiltration,
and impact&lt;/li&gt;
&lt;li&gt;producing incident timelines that retain provenance instead of flattening
everything into a narrative summary&lt;/li&gt;
&lt;li&gt;testing what investigators could have known at a day-one cutoff&lt;/li&gt;
&lt;li&gt;comparing the same evidence under different actor framings&lt;/li&gt;
&lt;li&gt;teaching analysts and evaluating agents on calibrated uncertainty&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The result is closer to a digital incident autopsy than a generic search box:&lt;br&gt;
the system preserves the chain of evidence, the gaps in the record, and the&lt;br&gt;
reasoning boundaries around each conclusion.&lt;/p&gt;
&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The deployed demo is available at &lt;a href="https://cyber-autopsy.vercel.app" rel="noopener noreferrer"&gt;https://cyber-autopsy.vercel.app&lt;/a&gt;.&lt;br&gt;
It serves the forensic UI and the Sanity-backed &lt;code&gt;/api/cases&lt;/code&gt; and&lt;br&gt;
&lt;code&gt;/api/investigate&lt;/code&gt; routes and is publicly reachable for the competition demo.&lt;/p&gt;

&lt;p&gt;Run the project locally when you want to inspect or modify it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt;.&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%2Fpn4tmknfdt9h53om0w3j.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%2Fpn4tmknfdt9h53om0w3j.png" alt="Image2" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;The repository contains the Python benchmark, Sanity Studio, importer, MCP&lt;br&gt;
server, local UI, tests, and documentation.&lt;/p&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/ujjavala/cyber-autopsy" rel="noopener noreferrer"&gt;https://github.com/ujjavala/cyber-autopsy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Deployment: &lt;a href="https://cyber-autopsy.vercel.app" rel="noopener noreferrer"&gt;https://cyber-autopsy.vercel.app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The most relevant implementation files are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;studio-cyber-autopsy/schemaTypes/index.ts  Sanity content model
scripts/sanity-seed.mjs                    repeatable JSONL importer
agent/sanity-context.mjs                   scoped GROQ investigation context
agent/mcp-server.mjs                       MCP tool surface
web/app.js                                 investigation UI behavior
web/style.css                              cyber-forensics console theme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  My Build Process
&lt;/h2&gt;

&lt;h3&gt;
  
  
  AI-native IDE
&lt;/h3&gt;

&lt;p&gt;I built the project collaboratively in Codex, using the repository as the&lt;br&gt;
shared workspace. I started by reading the existing benchmark, data model,&lt;br&gt;
frontend, Sanity Studio, and environment configuration. The implementation was&lt;br&gt;
then developed in small verified phases so each change could be tested against&lt;br&gt;
the real Sanity dataset and the existing Python benchmark.&lt;/p&gt;
&lt;h3&gt;
  
  
  Prompts that worked
&lt;/h3&gt;

&lt;p&gt;The most effective prompts were specific about both the user experience and&lt;br&gt;
the evidence model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Integrate the existing Cyber Autopsy benchmark with Sanity. Preserve the
relationships between evidence, events, claims, sources, and investigation
cases. Add a repeatable importer and a context layer that can answer scoped
questions without leaking later evidence into temporal-cutoff cases.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The Sanity dataset can be empty or partially populated. Make the UI handle
empty arrays and API errors explicitly instead of throwing when reading length.
Test every investigation scenario after the fix.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Make the interface feel like a cyber-forensics lab: improve hierarchy,
case descriptions, icons, status signals, loading feedback, responsive layout,
and subtle motion while keeping the investigation workflow easy to scan.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Where the build got stuck
&lt;/h3&gt;

&lt;p&gt;The first Sanity-backed UI path assumed that the selected case always had a&lt;br&gt;
populated evidence array. When the dataset was empty or the query returned an&lt;br&gt;
unexpected shape, the browser surfaced:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Cannot read properties of undefined (reading 'length')
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The correction was to make the context and UI defensive: seed and validate the&lt;br&gt;
dataset, distinguish a missing case from an empty evidence scope, and render a&lt;br&gt;
clear error or empty state instead of dereferencing an undefined array.&lt;/p&gt;

&lt;p&gt;Another important correction was temporal scoping. CASE-004 shares the&lt;br&gt;
RansomHub incident with CASE-001 but only exposes day-one evidence. The context&lt;br&gt;
now filters events, relationships, claims, and evidence against the selected&lt;br&gt;
case boundary before returning the investigation result.&lt;/p&gt;
&lt;h3&gt;
  
  
  Course corrections and testing
&lt;/h3&gt;

&lt;p&gt;The build evolved through these corrections:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Added seven Sanity document types instead of storing one large incident
blob: incidents, sources, evidence, events, relationships, claims, and
investigation cases.&lt;/li&gt;
&lt;li&gt;Added explicit references and reverse-reference patches so provenance can be
followed in both directions.&lt;/li&gt;
&lt;li&gt;Added scoped GROQ queries and MCP tools for case listing and investigation.&lt;/li&gt;
&lt;li&gt;Added case descriptions because the case IDs alone were too cryptic for a
human investigator.&lt;/li&gt;
&lt;li&gt;Reworked the UI into a forensic console with status badges, trace labels,
loading state, evidence panels, and uncertainty sections.&lt;/li&gt;
&lt;li&gt;Added a favicon, README startup guide, Sanity integration write-up, and
challenge-specific documentation.&lt;/li&gt;
&lt;li&gt;Ran the full Python suite, Sanity import validation, Studio build, syntax
checks, browser scenarios, and browser console-error checks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The final verification included 229 Python tests, a Sanity inventory of 457&lt;br&gt;
production documents, live CASE-001 and CASE-004 investigations, evidence&lt;br&gt;
provenance checks, cutoff checks, and zero browser console errors. The hosted&lt;br&gt;
Sanity Context Knowledge Base was also created and checked through its&lt;br&gt;
read-only MCP tools; that hosted integration is the Path One capability, while&lt;br&gt;
this post focuses on the strange forensic product and its build process.&lt;/p&gt;

&lt;p&gt;The Sanity layer also now includes a native review workflow. The Studio schema&lt;br&gt;
has a &lt;code&gt;reviewTask&lt;/code&gt; document type, and the custom Structure menu exposes an open&lt;br&gt;
Review queue for unresolved evidence-quality work. A Blueprint deployed the&lt;br&gt;
&lt;code&gt;cyber-autopsy-review-automation&lt;/code&gt; Stack with a project-scoped robot token and a&lt;br&gt;
&lt;code&gt;review-queue-on-content-change&lt;/code&gt; Function. It checks published evidence,&lt;br&gt;
events, relationships, and claims for missing source/evidence links and creates&lt;br&gt;
deduplicated tasks without changing forensic verdicts.&lt;/p&gt;

&lt;p&gt;The repository also includes a draft-only Agent Action helper. Sanity's&lt;br&gt;
schema-aware Generate action can draft reviewer notes for a selected task, but&lt;br&gt;
it cannot publish the task, change its severity, or decide attribution. That&lt;br&gt;
separation lets the build use Sanity automation while keeping the investigator&lt;br&gt;
in control of the final interpretation.&lt;/p&gt;
&lt;h3&gt;
  
  
  Sanity integration in the build
&lt;/h3&gt;
&lt;h4&gt;
  
  
  How Sanity helped
&lt;/h4&gt;

&lt;p&gt;Sanity gave the project a durable, queryable content layer for the benchmark's&lt;br&gt;
investigation graph. Before the integration, the dataset was primarily a group&lt;br&gt;
of local JSONL files. That was reproducible, but it made the content harder to&lt;br&gt;
inspect, connect, and reuse across an agent, a Studio, and a browser UI.&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%2Fgltxgzs2inboe5mj4wrw.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%2Fgltxgzs2inboe5mj4wrw.png" alt="Image3" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;With Sanity, the same investigation content is modelled as linked documents.&lt;br&gt;
Evidence, events, claims, relationships, incidents, and sources can be queried&lt;br&gt;
independently or traversed together. This helped in four practical ways:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Grounding:&lt;/strong&gt; an MCP-capable agent can retrieve the exact evidence and relationships
relevant to a case instead of relying on a loose text search.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traceability:&lt;/strong&gt; every result can point back to evidence and source
provenance, which makes an analyst's review possible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope control:&lt;/strong&gt; case-specific references and cutoff rules can be enforced
in the query layer, preventing later facts from leaking into an earlier
reconstruction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reuse:&lt;/strong&gt; the same structured content model powers Sanity Studio, the
importer, MCP server, validation workflow, and forensic UI.&lt;/li&gt;
&lt;/ol&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%2Ffokf7zme85om7hgxx0yp.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%2Ffokf7zme85om7hgxx0yp.png" alt="Image4" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Sanity therefore acts as the investigation knowledge base, while the context&lt;br&gt;
layer acts as the reasoning boundary. The agent is not asked to remember an&lt;br&gt;
incident or invent a timeline; it queries structured content and returns a&lt;br&gt;
calibrated reconstruction.&lt;/p&gt;
&lt;h4&gt;
  
  
  Sanity features used
&lt;/h4&gt;
&lt;h5&gt;
  
  
  1. Structured document schemas
&lt;/h5&gt;

&lt;p&gt;Instead of storing one large incident document, I modeled the investigation as&lt;br&gt;
separate document types. This matches the way a forensic analyst works: an&lt;br&gt;
incident is the case context, evidence is an observation, an event is a&lt;br&gt;
reconstruction, a claim is an assertion, and a relationship explains how two&lt;br&gt;
events connect.&lt;/p&gt;

&lt;p&gt;The separation gives each kind of information a clear validation and query&lt;br&gt;
boundary. For example, an &lt;code&gt;event&lt;/code&gt; has a status and confidence, while an&lt;br&gt;
&lt;code&gt;evidence&lt;/code&gt; document has a timestamp, observation type, entities, and source&lt;br&gt;
references. A &lt;code&gt;relationship&lt;/code&gt; has a source event, target event, relation type,&lt;br&gt;
and supporting evidence.&lt;/p&gt;
&lt;h5&gt;
  
  
  2. References and graph-shaped content
&lt;/h5&gt;

&lt;p&gt;The most important Sanity feature here is the reference field. Evidence does&lt;br&gt;
not copy an event's text, and a relationship does not copy both event records.&lt;br&gt;
They point to the canonical documents:&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="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sourceEvent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="na"&gt;validation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rule&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;rule&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;required&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;evidence&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;defineArrayMember&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;evidence&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns Sanity into a navigable investigation graph. The agent can traverse&lt;br&gt;
&lt;code&gt;case -&amp;gt; incident -&amp;gt; event -&amp;gt; evidence -&amp;gt; source&lt;/code&gt;, then separately retrieve&lt;br&gt;
&lt;code&gt;relationship&lt;/code&gt; and &lt;code&gt;claim&lt;/code&gt; documents for the same incident. In the UI, that&lt;br&gt;
becomes a reconstruction with visible provenance instead of an unsupported&lt;br&gt;
paragraph.&lt;/p&gt;
&lt;h5&gt;
  
  
  3. Field validation and controlled statuses
&lt;/h5&gt;

&lt;p&gt;The Studio schema validates required identifiers and references, constrains&lt;br&gt;
confidence to the range &lt;code&gt;0..1&lt;/code&gt;, and uses controlled options for evidence and&lt;br&gt;
event status. The available status taxonomy is deliberately forensic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;confirmed · inferred · attempted · failed · unknown
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claims additionally support &lt;code&gt;contradicted&lt;/code&gt;. These values are used in the UI to&lt;br&gt;
separate established facts from uncertain or failed steps. This prevents a&lt;br&gt;
missing observation from silently becoming a confirmed event and makes&lt;br&gt;
calibrated uncertainty part of the content model.&lt;/p&gt;
&lt;h5&gt;
  
  
  4. GROQ projections and dereferencing
&lt;/h5&gt;

&lt;p&gt;The context layer uses GROQ projections to request only the fields required by&lt;br&gt;
an investigation. The &lt;code&gt;-&amp;gt;&lt;/code&gt; operator dereferences related documents so one&lt;br&gt;
response can contain the case, incident summary, evidence, and source metadata&lt;br&gt;
without the browser making a request for every individual record.&lt;/p&gt;

&lt;p&gt;For example, the case query projects the incident and joins its evidence and&lt;br&gt;
events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*[_type == "investigationCase" &amp;amp;&amp;amp; caseId == $caseId][0] {
  caseId, mode, temporalCutoff, framingActor,
  incident-&amp;gt;{incidentId, title, description, actor, impact},
  evidence[]-&amp;gt;{evidenceId, timestamp, type, description, status,
    source[]-&amp;gt;{sourceId, title, publisher, url}},
  events[]-&amp;gt;{eventId, description, timestamp, status,
    evidence[]-&amp;gt;{evidenceId}}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is useful for security analysis because the response remains structured.&lt;br&gt;
The agent can filter on timestamps, statuses, evidence IDs, and relationship&lt;br&gt;
types instead of trying to recover those distinctions from prose.&lt;/p&gt;
&lt;h5&gt;
  
  
  5. Parameterized, scoped queries
&lt;/h5&gt;

&lt;p&gt;Queries are parameterized with &lt;code&gt;caseId&lt;/code&gt; and &lt;code&gt;incidentRef&lt;/code&gt;. The selected case&lt;br&gt;
defines the visible evidence set, and the context derives a set of allowed&lt;br&gt;
evidence IDs before returning events, relationships, or claims. A relationship&lt;br&gt;
is included only when its supporting evidence and both endpoint events are in&lt;br&gt;
scope.&lt;/p&gt;

&lt;p&gt;This is how the implementation handles temporal safety. &lt;code&gt;CASE-004&lt;/code&gt; is not just&lt;br&gt;
a label in the UI; its Sanity references define the evidence boundary. A&lt;br&gt;
day-one question can also apply an additional D1 filter before the result is&lt;br&gt;
rendered. The model cannot accidentally cite a later ransomware event as if it&lt;br&gt;
were known on day one.&lt;/p&gt;
&lt;h5&gt;
  
  
  6. Sanity Content API
&lt;/h5&gt;

&lt;p&gt;The Node context uses Sanity's Content API with the project ID, dataset, GROQ&lt;br&gt;
query, and optional server-side token. The token is loaded from &lt;code&gt;.env&lt;/code&gt; on the&lt;br&gt;
server and is never exposed to the browser. The browser talks to the local web&lt;br&gt;
server, while the local server talks to Sanity.&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%2F4x4qi3tnjdyzomxw0zsw.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%2F4x4qi3tnjdyzomxw0zsw.png" alt="Image5" width="800" height="405"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That separation gives the project a safer shape:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser UI → local investigation API → Sanity Content API
                                      ↑
                              server-side token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same API boundary also makes failures explicit. A missing case produces a&lt;br&gt;
clear error, while an empty evidence set is handled as an empty investigation&lt;br&gt;
scope rather than causing a client-side &lt;code&gt;length&lt;/code&gt; exception.&lt;/p&gt;
&lt;h5&gt;
  
  
  7. Sanity Studio, Structure Tool, and Vision
&lt;/h5&gt;

&lt;p&gt;The project includes a standalone Sanity Studio configured for the same project&lt;br&gt;
and &lt;code&gt;production&lt;/code&gt; dataset. The Structure Tool provides the editing workspace&lt;br&gt;
for the seven document types, with previews showing useful forensic identifiers&lt;br&gt;
such as &lt;code&gt;CASE-004&lt;/code&gt;, &lt;code&gt;E-021&lt;/code&gt;, or an event description.&lt;/p&gt;

&lt;p&gt;The Vision Tool is enabled for inspecting and testing GROQ directly in Studio.&lt;br&gt;
That is useful when developing an investigation query: I can verify a case&lt;br&gt;
projection, inspect references, and test a cutoff query against the real&lt;br&gt;
dataset before wiring it into the MCP context.&lt;/p&gt;
&lt;h5&gt;
  
  
  8. Transactional mutations and repeatable import
&lt;/h5&gt;

&lt;p&gt;The importer uses Sanity mutations in batches rather than hand-editing hundreds&lt;br&gt;
of documents. It creates or replaces the normalized documents in dependency&lt;br&gt;
order, then applies reverse-reference patches for evidence-to-event,&lt;br&gt;
evidence-to-claim, and contradiction links.&lt;/p&gt;

&lt;p&gt;The import is repeatable and testable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run sanity:validate       &lt;span class="c"&gt;# validate source inventory&lt;/span&gt;
node scripts/sanity-seed.mjs &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
npm run sanity:seed           &lt;span class="c"&gt;# write the content to Sanity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the benchmark a reproducible migration path while keeping the live&lt;br&gt;
knowledge base editable in Studio. It also means the dataset can be rebuilt if&lt;br&gt;
the schema gains another forensic field later.&lt;/p&gt;
&lt;h5&gt;
  
  
  9. MCP as the agent boundary
&lt;/h5&gt;

&lt;p&gt;Sanity stores and retrieves the content; the MCP server exposes investigation&lt;br&gt;
capabilities to an agent host. The server provides &lt;code&gt;list_investigation_cases&lt;/code&gt;&lt;br&gt;
and &lt;code&gt;investigate_case(caseId, question)&lt;/code&gt;. Internally, those tools call the same&lt;br&gt;
Sanity context used by the web UI.&lt;/p&gt;

&lt;p&gt;This division is intentional. Sanity is responsible for content modeling,&lt;br&gt;
relationships, querying, and provenance. The context layer is responsible for&lt;br&gt;
case scoping and graph filtering. The MCP layer is responsible for making those&lt;br&gt;
grounded operations available to an agent. The agent can therefore reason over&lt;br&gt;
retrieved evidence without receiving unrestricted access to the whole dataset.&lt;/p&gt;
&lt;h4&gt;
  
  
  10. Functions, Blueprints, and Agent Actions
&lt;/h4&gt;

&lt;p&gt;The project now uses Sanity's native automation layer for content quality&lt;br&gt;
control. A &lt;code&gt;reviewTask&lt;/code&gt; document type gives the Studio a durable queue for&lt;br&gt;
missing sources, missing evidence, broken graph links, temporal review, and&lt;br&gt;
conflict review. The custom Studio structure puts &lt;code&gt;Review queue&lt;/code&gt; at the top and&lt;br&gt;
shows open and in-review tasks first.&lt;/p&gt;

&lt;p&gt;The Blueprint in &lt;code&gt;studio-cyber-autopsy/sanity.blueprint.ts&lt;/code&gt; provisions the&lt;br&gt;
&lt;code&gt;cyber-autopsy-review-automation&lt;/code&gt; Stack, a project-scoped robot token, and the&lt;br&gt;
&lt;code&gt;review-queue-on-content-change&lt;/code&gt; document Function. It watches published&lt;br&gt;
evidence, events, relationships, and claims. When an expected link is missing,&lt;br&gt;
the Function creates a deduplicated &lt;code&gt;reviewTask&lt;/code&gt; with a severity, explanation,&lt;br&gt;
suggested action, and reference to the affected document. It does not rewrite&lt;br&gt;
the incident graph, decide attribution, or publish a conclusion.&lt;/p&gt;

&lt;p&gt;Agent Actions are used as a separate, draft-only reviewer assist. The helper in&lt;br&gt;
&lt;code&gt;studio-cyber-autopsy/scripts/draft-review-agent-action.mjs&lt;/code&gt; asks Sanity's&lt;br&gt;
schema-aware Generate action to draft concise reviewer notes for one task. It&lt;br&gt;
requires the deployed schema ID and a server-side write token, targets only&lt;br&gt;
&lt;code&gt;reviewerNotes&lt;/code&gt;, and leaves the task status, severity, and final decision to a&lt;br&gt;
human reviewer. Sanity's default draft behavior is deliberately preserved.&lt;/p&gt;

&lt;p&gt;The Function and robot token are deployed through Blueprint rather than hidden&lt;br&gt;
in the web app. This keeps write credentials server-side and makes the&lt;br&gt;
automation inspectable in the Sanity project. Both remote deployment operations&lt;br&gt;
completed successfully.&lt;/p&gt;
&lt;h4&gt;
  
  
  Content model
&lt;/h4&gt;

&lt;p&gt;Sanity stores seven document types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;incident&lt;/code&gt;: case narrative, actor, impact, confidence, and source references&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;source&lt;/code&gt;: publisher, URL, publication date, type, and reliability&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;evidence&lt;/code&gt;: an observation with timestamp, status, description, and provenance&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;event&lt;/code&gt;: a reconstructed action or state with evidence references&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;relationship&lt;/code&gt;: a typed link between events&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;claim&lt;/code&gt;: an assertion that can be supported or contradicted&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;investigationCase&lt;/code&gt;: a benchmark scope with mode, cutoff, and actor framing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The schema is defined with Sanity's typed helpers:&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="nf"&gt;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;supportsEvents&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;array&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;defineArrayMember&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;reference&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;to&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;event&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;defineField&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;relationship&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;precedes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;enables&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;causes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;depends_on&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;IDs from the benchmark remain explicit and stable: &lt;code&gt;INC-001&lt;/code&gt;, &lt;code&gt;E-001&lt;/code&gt;, &lt;code&gt;N01&lt;/code&gt;,&lt;br&gt;
and &lt;code&gt;CASE-004&lt;/code&gt;. This makes the imported content easy to inspect in Studio and&lt;br&gt;
keeps evidence references understandable in the investigation output.&lt;/p&gt;
&lt;h4&gt;
  
  
  Importing the knowledge base
&lt;/h4&gt;

&lt;p&gt;The JSONL files under &lt;code&gt;data/&lt;/code&gt; remain the reproducible source of truth. The&lt;br&gt;
importer maps them into Sanity documents:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;incidents.jsonl       → incident
sources.jsonl         → source
evidence.jsonl        → evidence
attack_graphs.nodes   → event + claim
attack_graphs.edges   → relationship
benchmark_cases.jsonl → investigationCase
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The importer writes documents in dependency order and applies reverse evidence&lt;br&gt;
references in a second pass because events and evidence point to each other.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run sanity:validate
node scripts/sanity-seed.mjs &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
npm run sanity:seed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The current Sanity production dataset contains 457 documents: 445 imported&lt;br&gt;
Cyber Autopsy documents plus 12 existing project documents. A curated&lt;br&gt;
150-document source from this dataset is currently indexed in the hosted&lt;br&gt;
Context Knowledge Base.&lt;/p&gt;
&lt;h4&gt;
  
  
  Context queries and agent behavior
&lt;/h4&gt;

&lt;p&gt;&lt;code&gt;agent/sanity-context.mjs&lt;/code&gt; uses scoped GROQ queries. It does not download the&lt;br&gt;
entire dataset for every question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;*[_type == "investigationCase" &amp;amp;&amp;amp; caseId == $caseId][0] {
  caseId, mode, temporalCutoff, framingActor,
  incident-&amp;gt;{incidentId, title, actor, actorType},
  evidence[]-&amp;gt;{evidenceId, timestamp, type, description, status,
    source[]-&amp;gt;{sourceId, title, publisher, url}},
  events[]-&amp;gt;{eventId, description, timestamp, status,
    evidence[]-&amp;gt;{evidenceId}}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context then resolves relationships and claims and filters them against the&lt;br&gt;
case's allowed evidence IDs. That filtering is the temporal safety boundary:&lt;br&gt;
&lt;code&gt;CASE-004&lt;/code&gt; can answer what was knowable at the end of day one without leaking&lt;br&gt;
later ransomware evidence from the full case.&lt;/p&gt;

&lt;p&gt;The MCP server exposes two tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;list_investigation_cases
investigate_case(caseId, question)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The investigation function maps question intent to structured graph operations:&lt;br&gt;
predecessors, enablers, supporting evidence, statuses, cutoff checks, actor&lt;br&gt;
framing, and contradictions. The result is deterministic, grounded in Sanity&lt;br&gt;
content, and designed to be passed to a model without requiring another model&lt;br&gt;
API key in this repository.&lt;/p&gt;
&lt;h2&gt;
  
  
  Sanity Project Details
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project ID: 41l9o4xn
Dataset: production
Organization ID: oqf9m6vy6
Knowledge Base ID: kbZHPSeMHnZt
Hosted Studio: https://cyber-autopsy-ujjavala.sanity.studio/
Blueprint Stack: cyber-autopsy-review-automation (ST-6kxszxpvyw)
Deployed Function: review-queue-on-content-change
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The public Sanity project details are intentionally included so the structured&lt;br&gt;
content model can be inspected. The hosted Studio is deployed from the Sanity&lt;br&gt;
Studio schema, and the Context Knowledge Base is built from a curated source&lt;br&gt;
of the production dataset. The server-side tokens stay in &lt;code&gt;.env&lt;/code&gt;, are ignored&lt;br&gt;
by Git, and are never sent to the browser.&lt;/p&gt;
&lt;h2&gt;
  
  
  Agent Session
&lt;/h2&gt;

&lt;p&gt;The local agent is available through the MCP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run agent:mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The project also has a hosted Sanity Context MCP endpoint for agents that can&lt;br&gt;
connect to MCP. It is the read-only retrieval path used for the Path One&lt;br&gt;
integration and exposes the Knowledge Base through &lt;code&gt;initial_context&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;knowledge_base_search&lt;/code&gt;, and &lt;code&gt;knowledge_base_read&lt;/code&gt;. The endpoint URL is kept&lt;br&gt;
in the repository's environment configuration rather than hard-coded into the&lt;br&gt;
application, and no token is included in this post. The curated native-session&lt;br&gt;
record is included inline below for transparency.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt: Query the hosted Sanity Context Knowledge Base read-only and explain
how CASE-001 moved from initial access to ransomware deployment.

MCP calls: initial_context -&amp;gt; knowledge_base_search -&amp;gt; knowledge_base_read.
The search returned RansomHub-related records and the read returned
dataset-backed provenance identifiers. The agent preserved the boundary that
retrieved content must not be stitched into unsupported claims. Codex reached
the real content but hit its usage limit before the final narrative answer.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser UI uses the same investigation context and exposes the workflow in&lt;br&gt;
a more approachable way:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Choose a case from the case selector. The description explains the incident
before you ask a question.&lt;/li&gt;
&lt;li&gt;Read the case badges for full, temporal cutoff, progressive,
counterfactual, or actor-framing scope.&lt;/li&gt;
&lt;li&gt;Enter a question or choose a suggested investigation prompt.&lt;/li&gt;
&lt;li&gt;Run the investigation and inspect the trace-numbered reconstruction.&lt;/li&gt;
&lt;li&gt;Expand evidence records to verify the source and provenance yourself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Useful sessions to capture for the final submission are:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CASE-001: How did the attacker move from initial access to ransomware deployment?
CASE-001: What evidence supports the Rclone event?
CASE-004: What could we have known at the end of day one?
CASE-011/012: What does the evidence establish regardless of actor framing?
CASE-001: Are there conflicting claims?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Local Verification
&lt;/h3&gt;

&lt;p&gt;The current build has been checked with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;.venv/bin/pytest &lt;span class="nt"&gt;-q&lt;/span&gt;
npm run sanity:validate
npm &lt;span class="nt"&gt;--prefix&lt;/span&gt; studio-cyber-autopsy run build &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="nt"&gt;--no-auto-updates&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser smoke checks cover the full investigation, the CASE-004 temporal&lt;br&gt;
cutoff, case descriptions, evidence rendering, and zero browser console errors.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>sanitychallenge</category>
      <category>sanity</category>
      <category>ai</category>
    </item>
    <item>
      <title>I Benchmarked 10 AI Models to Reconstruct Real Cyber Attacks</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Thu, 01 Oct 2026 15:33:59 +0000</pubDate>
      <link>https://dev.to/ujja/i-asked-10-ai-models-to-reconstruct-real-cyber-attacks-2o3j</link>
      <guid>https://dev.to/ujja/i-asked-10-ai-models-to-reconstruct-real-cyber-attacks-2o3j</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/kaggle-2026-09-23"&gt;Kaggle Benchmarking Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Benchmarked
&lt;/h2&gt;

&lt;p&gt;Cyber incident reports rarely arrive as a clean, complete timeline. They describe what investigators or security teams could observe, mix those observations with analysis, and leave some questions unanswered. That is exactly where an AI-generated summary can become risky: it may sound convincing while quietly turning an inference into a fact or filling a gap with an event that the evidence never established.&lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;Cyber Autopsy&lt;/strong&gt; to test a narrower, practical question: when given pieces of a reported cyber incident, can a model reconstruct what happened while showing which evidence supports each claim and admitting what remains uncertain? The model must produce a timeline, connect events with causal or temporal links, cite evidence, and distinguish confirmed activity from inference, failed attempts, contradictions, and unknowns. A plausible attack story is not enough; unsupported certainty should count against it.&lt;/p&gt;

&lt;p&gt;I chose this problem because incident reconstruction depends on more than recognizing familiar attack techniques. An analyst needs to know whether a step was observed or inferred, whether an attempt actually succeeded, and how one event led to another. Those distinctions can disappear in a fluent summary. Measuring them separately makes it easier to see whether a model is recovering the evidence or merely telling a likely-sounding story.&lt;/p&gt;

&lt;p&gt;The first evaluation uses seven tasks built from four public reports. They include a detailed human-operated ransomware intrusion and vendor-reported campaigns involving AI-assisted activity. These are useful real-world case studies, but they are not a controlled contest between human and AI attackers: the reports differ in detail, evidence source, and corroboration. I also included two versions of one case with identical evidence but different actor framing, to see whether that wording changes the model's reconstruction.&lt;/p&gt;

&lt;p&gt;This is a pilot, not a claim that AI attackers are more or less capable than people. It evaluates model reconstructions of reported incidents, not live attack behavior. The scorer is deterministic and reports an Evidence-Grounded Reconstruction Score (EGRS), alongside event recall and precision, causal-link quality, evidence attribution, status accuracy, uncertainty calibration, and hallucination-related measures. The temporal cutoff and framing pair are exploratory comparisons; only the framing pair holds the evidence fixed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Real-world case studies
&lt;/h3&gt;

&lt;p&gt;The seven tasks are built from four public incident reports, not invented scenarios:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;RansomHub intrusion (CASE-001 and CASE-004):&lt;/strong&gt; &lt;a href="https://thedfirreport.com/2025/06/30/hide-your-rdp-password-spray-leads-to-ransomhub-deployment/" rel="noopener noreferrer"&gt;The DFIR Report's “Hide Your RDP: Password Spray Leads to RansomHub Deployment”&lt;/a&gt; describes a human-operated intrusion using password spraying and RDP, credential access, Rclone exfiltration, and eventual RansomHub deployment. CASE-004 reuses this incident but cuts off the evidence at the end of day one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GTG-1002 espionage campaign (CASE-002, CASE-011, CASE-012):&lt;/strong&gt; Anthropic's &lt;a href="https://www.anthropic.com/news/disrupting-AI-espionage" rel="noopener noreferrer"&gt;incident report&lt;/a&gt; and &lt;a href="https://assets.anthropic.com/m/ec212e6566a0d47/original/Disrupting-the-first-reported-AI-orchestrated-cyber-espionage-campaign.pdf" rel="noopener noreferrer"&gt;technical report&lt;/a&gt; describe an alleged AI-orchestrated campaign against roughly 30 targets. CASE-011 and CASE-012 use identical evidence with human versus AI-agent framing; they test framing sensitivity, not whether the real-world actor was human or AI. The campaign details and attribution are vendor-reported, not independently verified victim-side telemetry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GTG-2002 “vibe hacking” extortion (CASE-003):&lt;/strong&gt; Anthropic's &lt;a href="https://www.anthropic.com/news/detecting-countering-misuse-aug-2025" rel="noopener noreferrer"&gt;August 2025 misuse report&lt;/a&gt; describes a Claude Code-assisted data-extortion operation affecting at least 17 organisations. The ransom-note images in that report were simulated recreations and are excluded from the benchmark evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-enabled credential harvesting (CASE-013):&lt;/strong&gt; Google GTIG/Mandiant's &lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/from-prompting-to-autonomy-the-evolution-of-adversarial-ai" rel="noopener noreferrer"&gt;September 2026 report&lt;/a&gt; describes an AI-assisted campaign that reportedly harvested thousands of credentials in under six hours. The victim and model are undisclosed, and the claims remain vendor-reported.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are real reported incidents, but the evidence quality is not uniform: the RansomHub case is reconstructed from host and network telemetry described by The DFIR Report, while the AI-actor case studies rely on security-vendor reporting. The benchmark labels that distinction rather than treating the cases as equally observed or directly comparable.&lt;/p&gt;

&lt;h3&gt;
  
  
  The seven tasks, in plain language
&lt;/h3&gt;

&lt;p&gt;The short IDs are just labels: &lt;code&gt;INC&lt;/code&gt; means the source incident, and &lt;code&gt;CASE&lt;/code&gt; means the particular benchmark task. Each task gives the model an evidence packet and asks for the best-supported reconstruction, not a free-form guess.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CASE-001, the full RansomHub intrusion:&lt;/strong&gt; Put the reported activity in order, from password spraying and remote access through the later intrusion and ransomware deployment. The reference reconstruction contains 28 events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-002, the GTG-1002 espionage report:&lt;/strong&gt; Reconstruct Anthropic's account of the reported campaign, including what the operators attempted, what succeeded or failed, and what the report does not establish. The reference contains 17 events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-003, the extortion operation:&lt;/strong&gt; Reconstruct Anthropic's shorter account of a Claude Code-assisted data-extortion operation. This report has less step-by-step detail, so the reference reconstruction is smaller, with 8 events.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-004, only the first day of the RansomHub case:&lt;/strong&gt; Revisit CASE-001 with later evidence removed. The model should not be penalized for events the supplied evidence cannot yet support; only 15 events are scored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-011, GTG-1002 framed as human-operated:&lt;/strong&gt; Use the campaign evidence while describing the operator as human-led.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-012, the same evidence framed as AI-operated:&lt;/strong&gt; Keep the evidence identical to CASE-011 and change only the actor framing. Comparing these two scores gives an early look at sensitivity to wording; it cannot tell us who really operated the campaign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-013, AI-enabled credential harvesting:&lt;/strong&gt; Reconstruct Google's public account of a reported credential-harvesting campaign, keeping the sequence and links grounded in what that report says. Its reference reconstruction contains 7 events.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Models Tested
&lt;/h2&gt;

&lt;p&gt;I ran ten models from several providers against the same seven Kaggle tasks: &lt;strong&gt;Gemini 3.7 Flash&lt;/strong&gt;, &lt;strong&gt;Gemma 4 26B A4B&lt;/strong&gt;, &lt;strong&gt;GLM-5&lt;/strong&gt;, &lt;strong&gt;Grok 4.20 Reasoning&lt;/strong&gt;, &lt;strong&gt;GPT-5.6 Luna&lt;/strong&gt;, &lt;strong&gt;GPT-5.6 Sol&lt;/strong&gt;, &lt;strong&gt;GPT-5.4 mini&lt;/strong&gt;, &lt;strong&gt;Claude Sonnet 5&lt;/strong&gt;, &lt;strong&gt;Claude Opus 5&lt;/strong&gt;, and &lt;strong&gt;Qwen 3 Coder 480B&lt;/strong&gt;. The current benchmark view has a score for every model-task pair. Kaggle's overall score aggregates these seven tasks, which include related variants of the same incidents.&lt;/p&gt;

&lt;p&gt;The table is the Kaggle leaderboard snapshot fetched on &lt;strong&gt;2 October 2026&lt;/strong&gt;, after duplicate and failing task attachments were removed and the earlier evaluated versions restored. CASE-001 through CASE-011 use v3; CASE-012 and CASE-013 use their republished v1 versions. Values are EGRS percentages (Kaggle's 0-1 scores multiplied by 100). EGRS rewards recovering supported events and links, citing evidence, and representing uncertainty, while penalizing unsupported events. It is specific to this evidence-reconstruction task, not a general measure of intelligence or cybersecurity ability. Kaggle's overall score now matches the equal-weight mean across the seven task rows. Since some tasks are related variants of the same incidents, this is descriptive rather than an independent-sample leaderboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Findings
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kaggle task&lt;/th&gt;
&lt;th&gt;Gemini Flash&lt;/th&gt;
&lt;th&gt;Gemma 4&lt;/th&gt;
&lt;th&gt;GPT-5.6 Luna&lt;/th&gt;
&lt;th&gt;GLM-5&lt;/th&gt;
&lt;th&gt;Grok 4.20&lt;/th&gt;
&lt;th&gt;Claude Sonnet 5&lt;/th&gt;
&lt;th&gt;Claude Opus 5&lt;/th&gt;
&lt;th&gt;GPT-5.6 Sol&lt;/th&gt;
&lt;th&gt;GPT-5.4 mini&lt;/th&gt;
&lt;th&gt;Qwen 3 Coder&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/ujjavalasingh/cyber-autopsy-case-001-reconstruct-inc-001-full/3" rel="noopener noreferrer"&gt;CASE-001: Full RansomHub intrusion&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;70.55&lt;/td&gt;
&lt;td&gt;82.38&lt;/td&gt;
&lt;td&gt;71.27&lt;/td&gt;
&lt;td&gt;81.31&lt;/td&gt;
&lt;td&gt;75.71&lt;/td&gt;
&lt;td&gt;73.75&lt;/td&gt;
&lt;td&gt;78.14&lt;/td&gt;
&lt;td&gt;78.06&lt;/td&gt;
&lt;td&gt;66.06&lt;/td&gt;
&lt;td&gt;70.90&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/ujjavalasingh/cyber-autopsy-case-002-reconstruct-inc-002-full/3" rel="noopener noreferrer"&gt;CASE-002: GTG-1002 espionage campaign&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;79.72&lt;/td&gt;
&lt;td&gt;77.21&lt;/td&gt;
&lt;td&gt;80.31&lt;/td&gt;
&lt;td&gt;76.22&lt;/td&gt;
&lt;td&gt;83.60&lt;/td&gt;
&lt;td&gt;65.05&lt;/td&gt;
&lt;td&gt;68.16&lt;/td&gt;
&lt;td&gt;76.71&lt;/td&gt;
&lt;td&gt;69.68&lt;/td&gt;
&lt;td&gt;76.30&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/ujjavalasingh/cyber-autopsy-case-003-reconstruct-inc-003-full/3" rel="noopener noreferrer"&gt;CASE-003: Reported data-extortion operation&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;76.31&lt;/td&gt;
&lt;td&gt;92.11&lt;/td&gt;
&lt;td&gt;84.50&lt;/td&gt;
&lt;td&gt;83.80&lt;/td&gt;
&lt;td&gt;84.97&lt;/td&gt;
&lt;td&gt;73.11&lt;/td&gt;
&lt;td&gt;71.50&lt;/td&gt;
&lt;td&gt;76.12&lt;/td&gt;
&lt;td&gt;84.35&lt;/td&gt;
&lt;td&gt;70.31&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/ujjavalasingh/cyber-autopsy-case-004-reconstruct-inc-001-temporal-cutoff-d1/3" rel="noopener noreferrer"&gt;CASE-004: RansomHub, first-day evidence only&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;79.57&lt;/td&gt;
&lt;td&gt;84.79&lt;/td&gt;
&lt;td&gt;80.70&lt;/td&gt;
&lt;td&gt;72.37&lt;/td&gt;
&lt;td&gt;80.42&lt;/td&gt;
&lt;td&gt;74.00&lt;/td&gt;
&lt;td&gt;72.66&lt;/td&gt;
&lt;td&gt;74.51&lt;/td&gt;
&lt;td&gt;72.55&lt;/td&gt;
&lt;td&gt;63.70&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/ujjavalasingh/cyber-autopsy-case-011-reconstruct-inc-002-framing-framed-as-human/3" rel="noopener noreferrer"&gt;CASE-011: GTG-1002, human framing&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;85.44&lt;/td&gt;
&lt;td&gt;80.84&lt;/td&gt;
&lt;td&gt;82.04&lt;/td&gt;
&lt;td&gt;72.80&lt;/td&gt;
&lt;td&gt;80.12&lt;/td&gt;
&lt;td&gt;77.09&lt;/td&gt;
&lt;td&gt;65.13&lt;/td&gt;
&lt;td&gt;76.43&lt;/td&gt;
&lt;td&gt;66.80&lt;/td&gt;
&lt;td&gt;66.80&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/ujjavalasingh/cyber-autopsy-case-012-reconstruct-inc-002-framing-framed-as-ai-agent-republished/1" rel="noopener noreferrer"&gt;CASE-012: GTG-1002, AI-agent framing&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;77.13&lt;/td&gt;
&lt;td&gt;76.91&lt;/td&gt;
&lt;td&gt;79.82&lt;/td&gt;
&lt;td&gt;76.83&lt;/td&gt;
&lt;td&gt;70.87&lt;/td&gt;
&lt;td&gt;78.42&lt;/td&gt;
&lt;td&gt;69.74&lt;/td&gt;
&lt;td&gt;69.42&lt;/td&gt;
&lt;td&gt;68.48&lt;/td&gt;
&lt;td&gt;67.53&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://www.kaggle.com/benchmarks/tasks/ujjavalasingh/cyber-autopsy-case-013-reconstruct-inc-004-full-republished/1" rel="noopener noreferrer"&gt;CASE-013: Google's reported credential-harvesting campaign&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;89.33&lt;/td&gt;
&lt;td&gt;88.33&lt;/td&gt;
&lt;td&gt;88.75&lt;/td&gt;
&lt;td&gt;82.50&lt;/td&gt;
&lt;td&gt;87.83&lt;/td&gt;
&lt;td&gt;77.50&lt;/td&gt;
&lt;td&gt;52.47&lt;/td&gt;
&lt;td&gt;79.07&lt;/td&gt;
&lt;td&gt;78.50&lt;/td&gt;
&lt;td&gt;68.25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Kaggle overall&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;79.72&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;83.22&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;81.06&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;77.98&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;80.50&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;74.13&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;68.26&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;75.76&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;72.35&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;69.11&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are single runs, not stable model rankings. Gemma has the highest displayed overall score (83.22), followed by GPT-5.6 Luna (81.06) and Grok 4.20 (80.50). The standout case score is Gemma's 92.11 on the short extortion-report task; that is a case-specific result, not proof of general model superiority.&lt;/p&gt;

&lt;p&gt;Several patterns stand out in these single runs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Removing duplicates changed the aggregate, not the case results.&lt;/strong&gt; With one row per task, Kaggle's overall now matches the simple seven-task mean. The model order consequently differs from the earlier duplicate-inflated view; Grok is third overall in this snapshot.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The winner changes by case.&lt;/strong&gt; Gemma leads CASE-001 (82.38), CASE-003 (92.11), and CASE-004 (84.79); Grok leads CASE-002 (83.60); Gemini leads CASE-011 (85.44) and CASE-013 (89.33); GPT-5.6 Luna leads CASE-012 (79.82). The overall leader is not the top model on every task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first-day cutoff scores higher in this snapshot.&lt;/strong&gt; Gemini scores 79.57 on the day-one CASE-004 and 70.55 on full CASE-001, a 9.02-point gap. CASE-004 has a smaller gold graph (15 vs. 28 events), so this does not show that less evidence makes reconstruction easier.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The framing pair splits by model.&lt;/strong&gt; With identical evidence, human-framed minus AI-agent-framed scores range from +9.25 for Grok to -4.61 for Claude Opus 5. Gemini's gap is +8.31; five models score higher with human framing and five with AI-agent framing. This is an exploratory wording-sensitivity signal, not evidence about who operated the real campaign.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-013 has a wide spread.&lt;/strong&gt; Gemini scores 89.33, while Claude Opus 5 scores 52.47 on the same reported campaign. That 36.86-point gap is a prompt- and case-specific result; one run cannot establish a stable capability difference.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CASE-013's reference has seven gold events, while the richer INC-001 case has 28. Raw EGRS should not be read as a ranking of real-world incident difficulty or attacker behavior. The Kaggle leaderboard provides the composite score; I have not treated older component-level run outputs as if they were measurements from these restored task versions. Next I would repeat each condition with multiple seeds and inspect whether missed causal links and failed actions recur. There are no repeated-trial confidence intervals, and all model comparisons here are single runs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Kaggle Gotchas
&lt;/h2&gt;

&lt;p&gt;Building the benchmark involved a few Kaggle-specific failure modes that are worth recording because they affect task visibility and score continuity:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Task versions and benchmark versions are separate.&lt;/strong&gt; Pushing an existing task creates a new task version; it does not automatically change the version pinned in the benchmark. Scores belong to the exact task version that produced them. Switching a benchmark row to a new version does not carry the old models' scores across, so a new version can show &lt;code&gt;Fail&lt;/code&gt; or blank cells until those models are run again.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Task metadata has a 255-character limit.&lt;/strong&gt; CASE-001 v4 was explicitly rejected because its description exceeded the limit. CASE-002's description also exceeded 255 characters, so we shortened it before publishing v5. Check both fields before pushing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An uploaded notebook is not necessarily a usable task.&lt;/strong&gt; CASE-012 and CASE-013 uploads returned an entity-save error. Kaggle created version records that remained &lt;code&gt;Unspecified&lt;/code&gt;, had no completed run, and could not be selected in &lt;strong&gt;Add Tasks&lt;/strong&gt;. The benchmark displayed &lt;code&gt;Untitled Task&lt;/code&gt; for those attachments. The recovery was to remove the failing attachments and add the earlier evaluated task versions, which restored the seven named rows and their saved scores.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The first model call can block task creation.&lt;/strong&gt; Fresh CASE-012/013 task records hit Gemini HTTP 429 (“model is currently experiencing heavy load”). A one-time bootstrap using a fixed model can also make subsequent model evaluations fail Kaggle's requirement to call &lt;code&gt;kbench.llm&lt;/code&gt;. Keep the task model-agnostic, and do not assume a successful run under one fixed model makes it reusable across the leaderboard.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Completed&lt;/code&gt; is not the same as “all models finished.”&lt;/strong&gt; It confirms that task creation/its initial run completed. Check per-model run statuses separately, and distinguish an unrun model on a new version from a task-creation failure.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Visibility is independent of successful creation.&lt;/strong&gt; The benchmark and each task have their own visibility. Keep tasks private while they are drafts; make both the tasks and benchmark public only when the task records are valid and ready to share.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For the current results, the benchmark is pinned to the previously evaluated versions: CASE-001 through CASE-011 v3 and the republished CASE-012/013 v1 tasks. This keeps the benchmark rows named and retains their model scores while the failed newer uploads remain separate task records.&lt;/p&gt;

&lt;h3&gt;
  
  
  Code Walkthrough
&lt;/h3&gt;

&lt;p&gt;The model is asked for a structured reconstruction rather than a free-form incident summary. An event has a description, an evidence status, and evidence IDs; relationships connect event IDs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReconstructedEvent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# confirmed | inferred | unknown | attempted | failed
&lt;/span&gt;    &lt;span class="n"&gt;evidence_ids&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ReconstructedRelationship&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;source_event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;target_event_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;relationship&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;  &lt;span class="c1"&gt;# precedes | enables | causes | depends_on
&lt;/span&gt;
&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Reconstruction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReconstructedEvent&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;relationships&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ReconstructedRelationship&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;unknown_steps&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The task sends the incident packet with that schema, normalizes the structured response, and passes it to the deterministic scorer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;build_user_prompt&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;llm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prompt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;schema&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Reconstruction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temperature&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;prediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;to_prediction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;score_prediction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GOLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VALID_EVIDENCE_IDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;egrs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Event matching is one-to-one. Text similarity proposes candidate matches, cited evidence gives a fixed bonus, and a threshold filters weak matches:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;label_similarity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;description&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;gn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;label&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pe_ev&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;gn_ev&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pe_ev&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;gn_ev&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;EVIDENCE_BONUS&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;MATCH_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pe&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;event_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;gn&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;EGRS combines recovery, precision, graph links, evidence attribution, status, uncertainty, and failed-action recognition, while subtracting a hallucination penalty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;egrs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;100.0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;recall&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.20&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;precision&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;link_f1&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;evidence_attribution&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;status_accuracy&lt;/span&gt;
    &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;unknown_calibration&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;failed_recognition&lt;/span&gt;
    &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;hallucination_rate&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 makes unsupported certainty costly while giving the model credit for preserving uncertainty and citing the evidence behind its reconstruction.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Benchmark
&lt;/h2&gt;

&lt;p&gt;The live &lt;a href="https://www.kaggle.com/benchmarks/ujjavalasingh/cyber-autopsy-benchmark" rel="noopener noreferrer"&gt;Cyber Autopsy Kaggle benchmark and leaderboard&lt;/a&gt; groups the seven restored task versions. Scores above were fetched on 2 October 2026. Individual evaluated task versions, including the restored CASE-012 and CASE-013 records, are linked in the results table.&lt;/p&gt;

&lt;h3&gt;
  
  
  Expanded Case Set
&lt;/h3&gt;

&lt;p&gt;Since collecting those leaderboard results, I expanded the case set with seven follow-on tasks. They will use the same sequential model-evaluation workflow as the existing tasks. The cases broaden the evidence types and incident behaviors under study:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;CASE-014, the Medicare statistics portal incident:&lt;/strong&gt; Australian Government &lt;a href="https://www.pm.gov.au/media/press-conference-new-york" rel="noopener noreferrer"&gt;briefings&lt;/a&gt; describe an AI agent that encountered blocks, tried alternative ways to obtain information, and accessed infrastructure behind a public statistics portal. The personal Medicare claims and payments system is separate; officials said no personal information was believed accessed at the time, with the investigation still underway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-015, the Hong Kong transfer scam:&lt;/strong&gt; an official &lt;a href="https://www.info.gov.hk/gia/general/202406/26/P2024062600192p.htm" rel="noopener noreferrer"&gt;Legislative Council reply&lt;/a&gt; describes a phishing email, a prerecorded executive video meeting, follow-up instructions by instant message, and transfers totalling about HK$200 million. The source does not name the company, and police's account of the source media remains qualified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-016, the BumbleBee-to-Akira intrusion:&lt;/strong&gt; a &lt;a href="https://thedfirreport.com/2026/06/29/from-bing-search-to-ransomware-bumblebee-and-adaptixc2-deliver-akira-3/" rel="noopener noreferrer"&gt;forensic report&lt;/a&gt; traces a poisoned software-search result through intrusion, data theft, and ransomware. That report also describes a separate Swisscom case, which is explicitly excluded from this task's timeline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-017 and CASE-018, Midnight Blizzard:&lt;/strong&gt; Microsoft's &lt;a href="https://www.microsoft.com/en-us/msrc/blog/2024/01/microsoft-actions-following-attack-by-nation-state-actor-midnight-blizzard" rel="noopener noreferrer"&gt;January disclosure&lt;/a&gt; and &lt;a href="https://www.microsoft.com/en-us/msrc/blog/2024/03/update-on-microsoft-actions-following-attack-by-nation-state-actor-midnight-blizzard" rel="noopener noreferrer"&gt;March update&lt;/a&gt; form two knowledge snapshots of the same intrusion. This pair tests whether a reconstruction changes appropriately as disclosure evolves; it is not two independent incidents.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-019, Change Healthcare:&lt;/strong&gt; the initial &lt;a href="https://www.sec.gov/Archives/edgar/data/731766/000073176624000045/unh-20240221.htm" rel="noopener noreferrer"&gt;SEC filing&lt;/a&gt; and CEO's &lt;a href="https://www.finance.senate.gov/imo/media/doc/0501_witty_testimony.pdf" rel="noopener noreferrer"&gt;Senate testimony&lt;/a&gt; describe the access path, ransomware timeline, service disruption, and evolving patient-data findings. The CEO's attribution and impact statements remain source-attributed claims.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CASE-020, UNC5537 and Snowflake customer instances:&lt;/strong&gt; &lt;a href="https://cloud.google.com/blog/topics/threat-intelligence/unc5537-snowflake-data-theft-extortion" rel="noopener noreferrer"&gt;Mandiant's campaign report&lt;/a&gt; adds credential-theft, cloud data access, and extortion. It is deliberately modeled as a multi-victim campaign, not a single victim or a breach of Snowflake's corporate systems.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These additions broaden the evaluation set, but they do not create a controlled human-versus-AI experiment: actor type, source quality, and incident context differ. The gold graphs are undergoing independent review; the leaderboard snapshot above remains the results for the seven task versions already shown there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;Repository: &lt;a href="https://github.com/ujjavala/cyber-autopsy" rel="noopener noreferrer"&gt;https://github.com/ujjavala/cyber-autopsy&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;The next version will expand the internet-sourced dataset and move further into behavioural analysis.&lt;/p&gt;

&lt;p&gt;The questions I'm interested in include:&lt;/p&gt;

&lt;p&gt;How do AI and human cyber activity differ when the evidence is genuinely comparable?&lt;br&gt;
What kinds of failures are common in AI cyber activity?&lt;br&gt;
How do AI agents adapt after failed actions?&lt;br&gt;
How often do guardrails and environmental controls intervene?&lt;br&gt;
How much human intervention is involved?&lt;br&gt;
Can models recognise when they don't have enough evidence?&lt;br&gt;
Does actor framing systematically affect reconstruction?&lt;/p&gt;

&lt;p&gt;For now, Cyber Autopsy is a small first step toward answering those questions.&lt;/p&gt;

&lt;p&gt;The benchmark, dataset, scoring code, and evaluation results are all available on Kaggle.&lt;/p&gt;

&lt;p&gt;If you're interested in seeing how different models reconstruct the same cyber evidence — or in digging into where those reconstructions go wrong — have a look at the benchmark and results.&lt;/p&gt;

&lt;p&gt;The interesting part isn't just who gets the highest number.&lt;/p&gt;

&lt;p&gt;It's what the model thought happened, what the evidence actually says, and where those two things diverge.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>When Your AI Agent Reads Outdated Docs, Who Checks the Docs?</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Thu, 01 Oct 2026 01:52:23 +0000</pubDate>
      <link>https://dev.to/ujja/when-your-ai-agent-reads-outdated-docs-who-checks-the-docs-4ec1</link>
      <guid>https://dev.to/ujja/when-your-ai-agent-reads-outdated-docs-who-checks-the-docs-4ec1</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/mlh-hackathon"&gt;MLH x DEV Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;Docgrity&lt;/strong&gt;, a documentation integrity tool that finds where your documentation disagrees with itself before it confuses you — or the next AI agent working on your codebase.&lt;/p&gt;

&lt;p&gt;The idea came from a surprisingly simple problem.&lt;/p&gt;

&lt;p&gt;I opened one of my own documentation files and realised I didn't believe a word of it.&lt;/p&gt;

&lt;p&gt;With AI-assisted coding, every agent session can leave more documentation behind: &lt;code&gt;PLAN.md&lt;/code&gt;, &lt;code&gt;TODO.md&lt;/code&gt;, implementation notes, architecture decisions, and sometimes another architecture document because the agent didn't notice the first one existed.&lt;/p&gt;

&lt;p&gt;Eventually, you can end up with a repository full of documentation and no reliable way to know what's still true.&lt;/p&gt;

&lt;p&gt;And now there's a bigger problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI agents read those docs too.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A stale or contradictory document isn't just confusing for a developer anymore. It becomes context for the next agent, which can then make decisions based on information that was never correct, or is no longer true.&lt;/p&gt;

&lt;p&gt;So I built Docgrity to make documentation itself something we can check.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does Docgrity find?
&lt;/h3&gt;

&lt;p&gt;Docgrity scans documentation and surfaces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicates&lt;/strong&gt; — different documents saying essentially the same thing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contradictions&lt;/strong&gt; — conflicting claims, shown side by side&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open questions&lt;/strong&gt; — unanswered questions and stale TODOs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is that every finding comes with &lt;strong&gt;evidence&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No finding without a receipt.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The source text is quoted verbatim and verified against the actual file before a finding is reported. If the system can't prove a finding, it gets dropped.&lt;/p&gt;

&lt;p&gt;Docgrity currently works across repositories and Confluence.&lt;/p&gt;

&lt;p&gt;For repositories, there is a VS Code extension, CLI, and GitHub Action.&lt;/p&gt;

&lt;p&gt;For Confluence, I'm building an early-stage Forge app that scans spaces for the same documentation integrity problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I built it
&lt;/h3&gt;

&lt;p&gt;I didn't want another documentation tool that asks developers to maintain more metadata or another service that needs to own their documentation.&lt;/p&gt;

&lt;p&gt;The goal was something much simpler:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make the documentation you already have more trustworthy.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Docgrity is local-first, has zero infrastructure, doesn't require a hosted service, and is MIT licensed.&lt;/p&gt;

&lt;p&gt;Try it out:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://ujjavala.github.io/docgrity-vscode-site/index.html" rel="noopener noreferrer"&gt;Docgrity for VS Code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.npmjs.com/package/docgrity" rel="noopener noreferrer"&gt;Docgrity CLI on npm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/ujjavala/docgrity-vscode" rel="noopener noreferrer"&gt;Docgrity on GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://ujjavala.github.io/docgrity-site/index.html" rel="noopener noreferrer"&gt;Docgrity for Confluence&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The easiest way to see Docgrity is to run it against a repository containing documentation that has evolved over time.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; docgrity
docgrity scan &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CLI scans the documentation and produces findings with the supporting evidence behind each one.&lt;/p&gt;

&lt;p&gt;There is also a VS Code experience with an interactive dashboard, clickable evidence, and draft GitHub issues that you can review before posting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nt"&gt;--install-extension&lt;/span&gt; ujjavala.docgrity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And Docgrity can run in CI through GitHub Actions, so documentation checks can become part of the development workflow.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ujjavala/docgrity-vscode/action@main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Confluence, Docgrity scans a space and surfaces the same kinds of problems across pages rather than limiting the analysis to a Git repository.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ujjavala.github.io/docgrity-vscode-site/index.html" rel="noopener noreferrer"&gt;Try Docgrity for VS Code&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ujjavala.github.io/docgrity-site/index.html" rel="noopener noreferrer"&gt;Try Docgrity for Confluence&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docgrity can use &lt;strong&gt;&lt;a href="https://ollama.com/" rel="noopener noreferrer"&gt;Ollama&lt;/a&gt;&lt;/strong&gt; for local AI inference in the CLI.&lt;/p&gt;

&lt;p&gt;This was important to the design because documentation can contain information that teams don't necessarily want to send to an external AI service.&lt;/p&gt;

&lt;p&gt;With Ollama, the analysis can run locally, keeping the documentation and model interaction on the developer's machine.&lt;/p&gt;

&lt;p&gt;The AI isn't treated as the final authority, either. Docgrity uses it to identify potential duplicates, contradictions, and unanswered questions, but then verifies the resulting findings against the actual source documents.&lt;/p&gt;

&lt;p&gt;That led to one of the core design principles of the project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No finding without a receipt.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The model can suggest something is wrong. The evidence has to prove it.&lt;/p&gt;

&lt;p&gt;I also built the Confluence integration as an Atlassian Forge app, allowing the same documentation-integrity approach to be applied to documentation that lives outside the repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hackathon Experience
&lt;/h2&gt;

&lt;p&gt;The hackathon gave me a good excuse to take an idea that had been sitting in my head and turn it into something people can actually run.&lt;/p&gt;

&lt;p&gt;What started as a question — &lt;em&gt;"What if we could automatically tell when our documentation stopped agreeing with itself?"&lt;/em&gt; — became a working tool across a CLI, VS Code extension, GitHub Action, and Confluence integration.&lt;/p&gt;

&lt;p&gt;The part I'll remember most is how quickly an idea can grow once you start building it.&lt;/p&gt;

&lt;p&gt;The original problem sounds small: &lt;strong&gt;documentation goes stale&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;But once AI agents are involved, documentation becomes part of the context those agents use to make decisions. That makes documentation quality much more interesting — and much more important — than I initially thought.&lt;/p&gt;

&lt;p&gt;I ended up building Docgrity around that idea:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation integrity for you, your team, and your agent friends. 🤖&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you have a repository or Confluence space you think you know well, run a scan.&lt;/p&gt;

&lt;p&gt;If it finds something that makes you go &lt;em&gt;"wait… that's still in there?"&lt;/em&gt; — I'd love to hear about it.&lt;/p&gt;

</description>
      <category>mlhacks</category>
      <category>devchallenge</category>
      <category>hackathon</category>
      <category>opensource</category>
    </item>
    <item>
      <title>🤖 What happens when your AI agent trusts outdated docs? I built Docgrity to find the gaps.</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Tue, 08 Sep 2026 11:07:13 +0000</pubDate>
      <link>https://dev.to/ujja/what-happens-when-your-ai-agent-trusts-outdated-docs-i-built-docgrity-to-find-the-gaps-485h</link>
      <guid>https://dev.to/ujja/what-happens-when-your-ai-agent-trusts-outdated-docs-i-built-docgrity-to-find-the-gaps-485h</guid>
      <description>&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/ujja/meet-docgrity-documentation-integrity-for-you-your-team-and-your-agent-friends-14gm" class="crayons-story__hidden-navigation-link"&gt;🚨📢Launching Docgrity 🚀: Documentation integrity for you, your team, and your agent friends. 🤖&lt;/a&gt;
    &lt;div class="crayons-article__cover crayons-article__cover__image__feed"&gt;
      &lt;iframe src="https://www.youtube.com/embed/cxu0m8UrYxo" title="🚨📢Launching Docgrity 🚀: Documentation integrity for you, your team, and your agent friends. 🤖"&gt;&lt;/iframe&gt;
    &lt;/div&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/ujja" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F439943%2F774d42c6-75e1-4247-8688-30020ce8f40a.png" alt="ujja profile" class="crayons-avatar__image"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/ujja" class="crayons-story__secondary fw-medium m:hidden"&gt;
              ujja
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                ujja
                &lt;a href="/++"&gt;&lt;img alt="Subscriber" class="subscription-icon" src="https://assets.dev.to/assets/subscription-icon-805dfa7ac7dd660f07ed8d654877270825b07a92a03841aa99a1093bd00431b2.png"&gt;&lt;/a&gt;
                
              
              &lt;div id="story-author-preview-content-4579531" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/ujja" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F439943%2F774d42c6-75e1-4247-8688-30020ce8f40a.png" class="crayons-avatar__image" alt=""&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;ujja&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/ujja/meet-docgrity-documentation-integrity-for-you-your-team-and-your-agent-friends-14gm" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Sep 5&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/ujja/meet-docgrity-documentation-integrity-for-you-your-team-and-your-agent-friends-14gm" id="article-link-4579531"&gt;
          🚨📢Launching Docgrity 🚀: Documentation integrity for you, your team, and your agent friends. 🤖
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/mlhacks"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;mlhacks&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/hackathon"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;hackathon&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/devchallenge"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;devchallenge&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/ujja/meet-docgrity-documentation-integrity-for-you-your-team-and-your-agent-friends-14gm" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/exploding-head-daceb38d627e6ae9b730f36a1e390fca556a4289d5a41abb2c35068ad3e2c4b5.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="18" height="18"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;16&lt;span class="hidden s:inline"&gt;&amp;nbsp;reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;span class="crayons-story__favorited"&gt;
              &lt;span class="favorited-marker"&gt;
                &lt;span&gt;
                  

                &lt;/span&gt;
                &lt;span class="hidden"&gt;
                  

                &lt;/span&gt;
              &lt;/span&gt;
            &lt;/span&gt;
            &lt;a href="https://dev.to/ujja/meet-docgrity-documentation-integrity-for-you-your-team-and-your-agent-friends-14gm#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              

              2&lt;span class="hidden s:inline"&gt;&amp;nbsp;comments&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            3 min read
          &lt;/small&gt;
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;

&lt;/div&gt;



&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://ujjavala.github.io/docgrity-site/index.html" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;ujjavala.github.io&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://github.com/ujjavala/docgrity-vscode" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fopengraph.githubassets.com%2Fa2f3a2d5b6e23e8e9ffd5ce3157625e793bcc0a63d7ea0e953c35658b2cf3a67%2Fujjavala%2Fdocgrity-vscode" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://github.com/ujjavala/docgrity-vscode" rel="noopener noreferrer" class="c-link"&gt;
            GitHub - ujjavala/docgrity-vscode: Find contradictions, duplicates and open questions in your repo's markdown docs — and raise GitHub issues to fix them. Powered by your own Copilot subscription. · GitHub
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            Find contradictions, duplicates and open questions in your repo's markdown docs — and raise GitHub issues to fix them. Powered by your own Copilot subscription. - ujjavala/docgrity-vscode
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fgithub.githubassets.com%2Ffavicons%2Ffavicon.svg"&gt;
          github.com
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;br&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://www.producthunt.com/products/docgrity" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;producthunt.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


</description>
      <category>ai</category>
      <category>tooling</category>
      <category>npm</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built PadForward because sometimes you just need a pad</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Sat, 05 Sep 2026 13:39:59 +0000</pubDate>
      <link>https://dev.to/ujja/i-built-padforward-because-sometimes-you-just-need-a-pad-1ljl</link>
      <guid>https://dev.to/ujja/i-built-padforward-because-sometimes-you-just-need-a-pad-1ljl</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-09-03"&gt;Weekend Challenge: Generosity Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;💗 Live demo:&lt;/strong&gt; &lt;a href="https://padforward-generosity.vercel.app/" rel="noopener noreferrer"&gt;padforward-generosity.vercel.app&lt;/a&gt; · &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/ujjavala/padforward" rel="noopener noreferrer"&gt;github&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&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%2F8t7bvqmenyltuwm844jg.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%2F8t7bvqmenyltuwm844jg.png" alt="Landing page" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A few days ago, I unexpectedly got my period while I was out.&lt;/p&gt;

&lt;p&gt;I hadn't planned for it, I didn't have a pad with me, and I happened to be near a train station, so I did what seemed like the obvious thing — I went looking for one there.&lt;/p&gt;

&lt;p&gt;I expected that a train station would have some way of helping with something as basic as a sanitary product.&lt;/p&gt;

&lt;p&gt;It didn't.&lt;/p&gt;

&lt;p&gt;I had money. I could have bought one if I could find a shop, and I had enough options to eventually figure it out. But in that moment, I still ended up using toilet paper.&lt;/p&gt;

&lt;p&gt;And that got me thinking.&lt;/p&gt;

&lt;p&gt;Not just about people who can't afford menstrual products, but about how &lt;strong&gt;needing a pad unexpectedly can happen to anyone&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;You forget to put one in your bag. Your period comes earlier than expected. You thought you had one left but don't. You're travelling. You're somewhere unfamiliar. You simply didn't think you'd need one that day.&lt;/p&gt;

&lt;p&gt;It happens to the best of us.&lt;/p&gt;

&lt;p&gt;And then there is the other problem that nobody really talks about enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Does anyone have a pad?"
&lt;/h2&gt;

&lt;p&gt;Even if someone around me had a spare pad, would I actually walk up to them and ask?&lt;/p&gt;

&lt;p&gt;Maybe.&lt;/p&gt;

&lt;p&gt;But probably not.&lt;/p&gt;

&lt;p&gt;We say periods are normal, and they absolutely are. But that doesn't mean asking a random person for a pad in a crowded train station suddenly feels completely normal.&lt;/p&gt;

&lt;p&gt;There is that little moment of hesitation.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Do I really want to ask someone?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Who do I ask?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What if they don't have one?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;What if this becomes awkward?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And if you're already stressed because you've unexpectedly got your period, the last thing you want is another problem to solve.&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;PadForward&lt;/strong&gt; came from.&lt;/p&gt;

&lt;h3&gt;
  
  
  So, what is PadForward?
&lt;/h3&gt;

&lt;p&gt;PadForward is a community-powered network for finding and donating menstrual products in public places. There are already organisations doing important work like distributing free period products, installing vending machines and helping people find access points. PadForward isn't trying to replace those efforts. I'm interested in what happens when you add a community-powered, supply-aware layer on top of them.&lt;/p&gt;

&lt;p&gt;The basic idea is pretty simple: &lt;strong&gt;if you need a pad, find one nearby without having to ask someone, and if you have spare pads, help put them somewhere they're likely to be needed.&lt;/strong&gt;&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%2Fzz7jzeyiyyeytf0u192z.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%2Fzz7jzeyiyyeytf0u192z.png" alt="Findign a pad" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Think of it as a community layer sitting on top of places people already use — train stations, bus stations, universities, workplaces, community centres and eventually other public spaces.&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%2Fa6ibqf0t5mh7bbz0jhes.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%2Fa6ibqf0t5mh7bbz0jhes.png" alt="Station view" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Someone might donate 20 pads today.&lt;/p&gt;

&lt;p&gt;Someone else might need one tomorrow.&lt;/p&gt;

&lt;p&gt;Those two people never have to meet.&lt;/p&gt;

&lt;p&gt;They don't need to know each other's names.&lt;/p&gt;

&lt;p&gt;They don't even need to know who helped whom.&lt;/p&gt;

&lt;p&gt;PadForward simply connects the two.&lt;/p&gt;

&lt;h3&gt;
  
  
  If you need one, you shouldn't have to ask
&lt;/h3&gt;

&lt;p&gt;The first thing I wanted to get right was the emergency experience.&lt;/p&gt;

&lt;p&gt;Imagine you're at Central Station and suddenly realise you need a pad.&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%2Fbgif4wq71sn9087m5m4x.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%2Fbgif4wq71sn9087m5m4x.png" alt="Central Bus Interchange" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You open PadForward and tap &lt;strong&gt;"I Need a Pad."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;You don't need an account.&lt;/p&gt;

&lt;p&gt;You don't need to explain what happened.&lt;/p&gt;

&lt;p&gt;You don't need to post a request.&lt;/p&gt;

&lt;p&gt;You just see what's nearby.&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Central Station&lt;/strong&gt;&lt;br&gt;
🟢 Available&lt;br&gt;
Verified 12 minutes ago&lt;br&gt;
4 min walk&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Town Hall&lt;/strong&gt;&lt;br&gt;
🟡 A few reported&lt;br&gt;
Verified 28 minutes ago&lt;br&gt;
6 min walk&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Museum&lt;/strong&gt;&lt;br&gt;
🔴 None reported&lt;br&gt;
Verified 18 minutes ago&lt;br&gt;
3 min walk&lt;/p&gt;

&lt;p&gt;You pick the closest available location and get directions.&lt;/p&gt;

&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;No awkward conversation required.&lt;/p&gt;

&lt;p&gt;And afterwards, instead of asking for anything back, PadForward can simply say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Someone helped you today. When you're able, help someone else.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;
  
  
  But where do the pads come from?
&lt;/h3&gt;

&lt;p&gt;This is where the other half of PadForward comes in.&lt;/p&gt;

&lt;p&gt;Maybe I have a packet of pads at home and want to donate some.&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%2Fod5zg913k767zkjkv13o.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%2Fod5zg913k767zkjkv13o.png" alt="Donate" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I don't necessarily know which station needs them.&lt;/p&gt;

&lt;p&gt;I don't want to spend an hour researching where to take them.&lt;/p&gt;

&lt;p&gt;So I can tell PadForward:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;I have 20 pads. Where should I donate them?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;PadForward looks at the current community supply information and the need at different locations and recommends somewhere that needs them.&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%2Fdav5kmey4h5lzdf8a8yb.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%2Fdav5kmey4h5lzdf8a8yb.png" alt="Donate in station" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Museum Station&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🔴 Critical&lt;br&gt;
Need Score: 94/100&lt;br&gt;
No recent community supply reported&lt;br&gt;
High estimated demand&lt;/p&gt;

&lt;p&gt;So instead of simply donating somewhere, I'm donating &lt;strong&gt;where the network needs them most&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That's the part I really like about this idea.&lt;/p&gt;

&lt;p&gt;It turns a small thing sitting in my cupboard into something that might be exactly where another person needs it.&lt;/p&gt;
&lt;h3&gt;
  
  
  And yes, I'm using Solana for the donation
&lt;/h3&gt;

&lt;p&gt;I also wanted the Solana integration to actually mean something in the product, rather than adding it just so I could say I used it :)&lt;/p&gt;

&lt;p&gt;So PadForward has a &lt;strong&gt;"Donate a Pad"&lt;/strong&gt; flow using Solana.&lt;/p&gt;

&lt;p&gt;The idea is that you can sponsor a pad — or several pads — through a Solana transaction, with the donation recorded transparently on-chain.&lt;/p&gt;

&lt;p&gt;The physical fulfilment happens through the PadForward community and partner network, so the blockchain isn't pretending to magically move a physical pad from one place to another.&lt;/p&gt;

&lt;p&gt;Instead, it gives us a transparent digital record of the generosity behind it.&lt;/p&gt;

&lt;p&gt;So the flow becomes:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Donate a pad → Solana transaction → PadForward donation → community supply point → someone can access a pad when they need one.&lt;/strong&gt;&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%2Fgy87xd72hc7vhmkoj8et.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%2Fgy87xd72hc7vhmkoj8et.png" alt="Solana donation" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I like this because the unit of generosity is really easy to understand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One donation. One pad. One small act that could make someone's day a little easier.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And you don't need to be a crypto person to use it.&lt;/p&gt;

&lt;p&gt;The blockchain is underneath the experience rather than becoming the experience.&lt;/p&gt;
&lt;h3&gt;
  
  
  I didn't want another chatbot
&lt;/h3&gt;

&lt;p&gt;The other technology I really wanted to use properly was Google AI.&lt;/p&gt;

&lt;p&gt;I could have added a chatbot that answered questions about periods, but that didn't feel particularly useful.&lt;/p&gt;

&lt;p&gt;Instead, PadForward has an AI agent that can actually interact with the application.&lt;/p&gt;

&lt;p&gt;You can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I suddenly got my period and I'm at Central."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent can understand that you need a nearby pad, find relevant locations, check their current community supply and help you choose one.&lt;/p&gt;

&lt;p&gt;Or you can say:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I have 20 pads to donate."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent can look at the current need across nearby stations and recommend where they would be most useful.&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%2Fyv6etv3r7evitqreh6if.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%2Fyv6etv3r7evitqreh6if.png" alt="Where to donate" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can even report something naturally:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"The donation box at Town Hall is empty."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The AI can understand that this is a supply report and pass it through the application's backend rather than just replying with a paragraph of text.&lt;/p&gt;

&lt;p&gt;So Gemini isn't there just to have a conversation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It's there to help the network make decisions and take actions.&lt;/strong&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  And what if the AI isn't available at all?
&lt;/h2&gt;

&lt;p&gt;This mattered to me, because the moment you most need PadForward is exactly the moment you might be underground at a train station with one bar of signal.&lt;/p&gt;

&lt;p&gt;So the assistant degrades gracefully instead of just failing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Gemini first.&lt;/strong&gt; When the server and the Gemini API are reachable, the full agent runs — real tool calls, live network data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Your browser's built-in AI second.&lt;/strong&gt; If the network is unreachable, PadForward tries the browser's built-in on-device AI (Chrome's Prompt API, powered by Gemini Nano). It runs entirely offline, grounded in the last station data the app cached — and it's told explicitly to never invent availability and to remind you the data may be stale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plain heuristics last.&lt;/strong&gt; If there's no built-in AI either, a small deterministic engine answers from the cached map data: closest place that last reported supply if you need a pad, highest need score if you want to donate.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The answer might get simpler at each step down, but you never get a spinner and a shrug.&lt;/p&gt;
&lt;h3&gt;
  
  
  Snap the box — AI that counts pads from a photo
&lt;/h3&gt;

&lt;p&gt;Supply reports are the lifeblood of the network, so I wanted reporting to take &lt;em&gt;zero&lt;/em&gt; effort.&lt;/p&gt;

&lt;p&gt;On any station page — and right inside the Find flow when you pick up a pad — instead of counting pads and tapping buttons, you can just photograph the donation box. Gemini vision looks at the photo and returns a strict JSON estimate — count, confidence, and a one-line description — which prefills a real supply report you confirm with one tap.&lt;/p&gt;

&lt;p&gt;And it follows the same philosophy as the assistant: if you're offline, the exact same flow runs on your &lt;strong&gt;browser's built-in multimodal AI&lt;/strong&gt; (Chrome's Prompt API), entirely on-device. The photo is analyzed and discarded either way — never uploaded, never stored.&lt;/p&gt;
&lt;h3&gt;
  
  
  What if the map says there are pads, but there aren't?
&lt;/h3&gt;

&lt;p&gt;This was another thing I didn't want to fake.&lt;/p&gt;

&lt;p&gt;Real-world inventory is messy.&lt;/p&gt;

&lt;p&gt;I can't claim that PadForward always knows exactly how many pads are sitting inside a station.&lt;/p&gt;

&lt;p&gt;Instead, the community helps keep the information fresh.&lt;/p&gt;

&lt;p&gt;Anyone can report:&lt;/p&gt;

&lt;p&gt;🟢 &lt;strong&gt;Plenty&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🟡 &lt;strong&gt;A few&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;🔴 &lt;strong&gt;None&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;⚪ &lt;strong&gt;Not sure&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The report also has a timestamp, so you might see:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🟢 Available — verified 14 minutes ago&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;rather than pretending the information is permanently accurate.&lt;/p&gt;

&lt;p&gt;Reports from community champions and multiple recent reports can give us more confidence, while old or conflicting reports can lower it.&lt;/p&gt;

&lt;p&gt;Over time, this could become a really interesting data problem too — figuring out where demand is happening and when stations tend to run low.&lt;/p&gt;
&lt;h3&gt;
  
  
  The people who keep it going
&lt;/h3&gt;

&lt;p&gt;Some people might want to do more than make a one-off donation.&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;Pad Champions&lt;/strong&gt; come in.&lt;/p&gt;

&lt;p&gt;You could adopt a local station and help keep an eye on its community supply.&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%2F05348suzhdi3jzlebbfs.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%2F05348suzhdi3jzlebbfs.png" alt="Pad Champion" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A champion could verify that a donation point still has supplies, report when it is empty, confirm a restock or help coordinate donations.&lt;/p&gt;

&lt;p&gt;The idea is that PadForward shouldn't just be a map that people open when they have a problem.&lt;/p&gt;

&lt;p&gt;It should become a little community network that people help maintain.&lt;/p&gt;
&lt;h3&gt;
  
  
  The bigger idea is the network
&lt;/h3&gt;

&lt;p&gt;This is the part that excites me most.&lt;/p&gt;

&lt;p&gt;The actual product isn't really the map.&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%2F25dwlfrl8pdvt54q29vv.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%2F25dwlfrl8pdvt54q29vv.png" alt="Impact" width="799" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's the network behind it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Someone has spare pads
          ↓
PadForward finds where they're needed
          ↓
Community supply point
          ↓
Someone unexpectedly needs one
          ↓
They find it without asking
          ↓
When they're able, they pay it forward
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The donor may never know who received the pad.&lt;/p&gt;

&lt;p&gt;The person who received it may never know who donated it.&lt;/p&gt;

&lt;p&gt;And that's completely fine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Generosity doesn't always need a face attached to it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Sometimes it can just be:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Someone left one for you."&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  There's a lot more I want to do with it
&lt;/h3&gt;

&lt;p&gt;For the hackathon, I'm starting with public transport locations because they make the problem very tangible.&lt;/p&gt;

&lt;p&gt;But the same idea could work in universities, workplaces, libraries, community centres, airports, shelters and other places where people spend time.&lt;/p&gt;

&lt;p&gt;I'd also like to explore route-based discovery, so someone travelling from Parramatta to Central could ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Is there somewhere I can get a pad without making a big detour?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;There is also a lot of interesting work to do around demand prediction, restock alerts, voice accessibility, multilingual support and using analytics to understand where the network needs help.&lt;/p&gt;

&lt;p&gt;Snowflake could help with the longer-term demand and supply analytics, while ElevenLabs could make the experience more accessible through voice.&lt;/p&gt;

&lt;p&gt;But those are extensions.&lt;/p&gt;

&lt;p&gt;The core idea stays the same.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why PadForward?
&lt;/h3&gt;

&lt;p&gt;I started with a very ordinary, slightly inconvenient experience.&lt;/p&gt;

&lt;p&gt;I unexpectedly needed a pad.&lt;/p&gt;

&lt;p&gt;I couldn't find one.&lt;/p&gt;

&lt;p&gt;And I realised that even though I had the means to solve the problem eventually, &lt;strong&gt;getting something so basic wasn't as easy as it should have been.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That made me think about everyone else who might find themselves in the same situation.&lt;/p&gt;

&lt;p&gt;Not just someone who can't afford a pad.&lt;/p&gt;

&lt;p&gt;Anyone.&lt;/p&gt;

&lt;p&gt;Because sometimes you forget.&lt;/p&gt;

&lt;p&gt;Sometimes you don't expect it.&lt;/p&gt;

&lt;p&gt;Sometimes you're just in the wrong place at the wrong time.&lt;/p&gt;

&lt;p&gt;And sometimes there might actually be someone nearby who has exactly what you need — but asking them feels like a bigger deal than it should.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PadForward is my attempt to remove that gap.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No asking.&lt;/p&gt;

&lt;p&gt;No explaining.&lt;/p&gt;

&lt;p&gt;Just access.&lt;/p&gt;

&lt;p&gt;And if you have one to spare?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pass it forward. 💗&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The demo is live at &lt;strong&gt;&lt;a href="https://padforward-generosity.vercel.app/" rel="noopener noreferrer"&gt;padforward-generosity.vercel.app&lt;/a&gt;&lt;/strong&gt; — no account, no sign-up.&lt;/p&gt;

&lt;p&gt;A few things to try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://padforward-generosity.vercel.app/find" rel="noopener noreferrer"&gt;Find a pad&lt;/a&gt;&lt;/strong&gt; — share your location (or browse the demo stations) and see nearby community supply with walking times.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://padforward-generosity.vercel.app/donate" rel="noopener noreferrer"&gt;Donate&lt;/a&gt;&lt;/strong&gt; — ask &lt;em&gt;"Where should my donation go?"&lt;/em&gt;, pick the critical station, and watch it flip from 🔴 to 🟢 — optionally paying with SOL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://padforward-generosity.vercel.app/assistant" rel="noopener noreferrer"&gt;Ask PadForward&lt;/a&gt;&lt;/strong&gt; — try &lt;em&gt;"I need a pad nearby"&lt;/em&gt;, &lt;em&gt;"I have 20 pads to donate"&lt;/em&gt;, or &lt;em&gt;"The donation box at Town Hall is empty"&lt;/em&gt; and watch the agent call real application tools.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://padforward-generosity.vercel.app/impact" rel="noopener noreferrer"&gt;Impact&lt;/a&gt;&lt;/strong&gt; — see the network's live numbers update as you interact.&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;All stations are clearly-labelled &lt;strong&gt;demo community points&lt;/strong&gt; at real Sydney station coordinates — supply figures are demo data, not real inventory.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/ujjavala/padforward" rel="noopener noreferrer"&gt;https://github.com/ujjavala/padforward&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;For the hackathon the whole product ships as a &lt;strong&gt;single Next.js 14 app on Vercel&lt;/strong&gt; — the UI plus API route handlers with an in-memory demo store. The &lt;strong&gt;designed architecture&lt;/strong&gt; (FastAPI + PostgreSQL/PostGIS behind the exact same API contract) lives in &lt;code&gt;services/api&lt;/code&gt; with docker-compose and a full pytest suite; the frontend switches to it with one env var.&lt;/p&gt;

&lt;p&gt;The interesting pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An agent, not a chatbot (Google AI).&lt;/strong&gt; Gemini 2.0 Flash gets a registry of 11 validated application tools (&lt;code&gt;find_nearby_stations&lt;/code&gt;, &lt;code&gt;get_highest_need_locations&lt;/code&gt;, &lt;code&gt;report_supply_status&lt;/code&gt;, &lt;code&gt;create_donation&lt;/code&gt;, …) and decides which to call — multi-round function calling, and it may only state availability that comes from tool results. It can never touch the data store directly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A four-tier AI fallback chain.&lt;/strong&gt; Gemini → a deterministic intent engine using the &lt;em&gt;same tools&lt;/em&gt; → the browser's built-in on-device AI (Chrome Prompt API / Gemini Nano) grounded in cached station data when you're fully offline → plain heuristics over the cached map. The answer gets simpler at each step, but you never get a spinner and a shrug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vision supply reports.&lt;/strong&gt; Photograph a donation box and Gemini vision (or the browser's built-in multimodal AI when offline) counts the pads and prefills a supply report — photo analyzed and discarded, never stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Solana donations.&lt;/strong&gt; Sponsoring pads produces a quote (pads → SOL) and records the donation with a transaction signature. With devnet enabled, it submits a &lt;strong&gt;real Memo transaction to Solana devnet&lt;/strong&gt; (auto-airdrop funded) and links to Solana Explorer; otherwise it's transparently labelled &lt;em&gt;devnet (simulated)&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A deterministic, ML-ready need score.&lt;/strong&gt; Every station gets a 0–100 score (40% supply shortage, 20% estimated demand, 15% staleness, 15% recent requests, 10% historical demand) in an isolated module — that's what routes donations to where they help most.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trust over false precision.&lt;/strong&gt; Supply is community-reported (🟢/🟡/🔴/⚪) with timestamps and a confidence model — champion reports and agreeing recent reports raise confidence, conflicts lower it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Google AI&lt;/strong&gt; — Gemini function-calling agent over real application tools, vision-based photo supply reports, plus the offline Chrome built-in AI (Gemini Nano) fallback for both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Solana&lt;/strong&gt; — donation flow with SOL quotes and on-chain (devnet) Memo transaction records — the blockchain underneath the experience, not the experience.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>gemini</category>
      <category>solana</category>
    </item>
    <item>
      <title>🚨📢Launching Docgrity 🚀: Documentation integrity for you, your team, and your agent friends. 🤖</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Sat, 05 Sep 2026 04:55:38 +0000</pubDate>
      <link>https://dev.to/ujja/meet-docgrity-documentation-integrity-for-you-your-team-and-your-agent-friends-14gm</link>
      <guid>https://dev.to/ujja/meet-docgrity-documentation-integrity-for-you-your-team-and-your-agent-friends-14gm</guid>
      <description>&lt;p&gt;Today I'm launching Docgrity — a tool for finding where your documentation disagrees with itself, before it confuses you or the next AI agent working on your codebase.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR&lt;br&gt;
Docgrity is a documentation integrity tool for repos and Confluence. It finds duplicate or contradictory documentation, surfaces unanswered questions, and gives you the evidence behind each finding so you can decide what needs fixing.&lt;br&gt;
Try it here:&lt;br&gt;
VS Code / CLI / GitHub Actions: &lt;a href="https://github.com/ujjavala/docgrity-vscode" rel="noopener noreferrer"&gt;https://github.com/ujjavala/docgrity-vscode&lt;/a&gt;&lt;br&gt;
VS Code site: &lt;a href="https://ujjavala.github.io/docgrity-vscode-site/index.html" rel="noopener noreferrer"&gt;https://ujjavala.github.io/docgrity-vscode-site/index.html&lt;/a&gt;&lt;br&gt;
CLI: &lt;a href="https://www.npmjs.com/package/docgrity" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/docgrity&lt;/a&gt;&lt;br&gt;
Confluence: &lt;a href="https://ujjavala.github.io/docgrity-site/index.html" rel="noopener noreferrer"&gt;https://ujjavala.github.io/docgrity-site/index.html&lt;/a&gt;&lt;br&gt;
And if you’re feeling extra supportive, feel free to show the launch some love on Product Hunt or Crowdstax. ❤️&lt;br&gt;
Product Hunt: &lt;a href="https://www.producthunt.com/products/docgrity" rel="noopener noreferrer"&gt;https://www.producthunt.com/products/docgrity&lt;/a&gt;&lt;br&gt;
Crowdstax: &lt;a href="https://www.crowdstax.com/products/docgrity" rel="noopener noreferrer"&gt;https://www.crowdstax.com/products/docgrity&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The idea came from a pretty simple problem. I opened one of my own docs recently and realised I didn't believe a word of it.&lt;/p&gt;

&lt;p&gt;AI-assisted coding has a side effect nobody really talks about: &lt;strong&gt;every agent session leaves documentation behind.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;PLAN.md&lt;/code&gt;. &lt;code&gt;TODO.md&lt;/code&gt;. Implementation notes. Architecture docs. Then another architecture doc because the agent didn't notice the first one.&lt;/p&gt;

&lt;p&gt;Eventually, you have a repo full of markdown and no idea what's still true.&lt;/p&gt;

&lt;p&gt;And now there's a bigger problem:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI agents read those docs too.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Stale or contradictory documentation isn't just confusing for developers anymore. It's context for the next agent.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Docgrity&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with your repo
&lt;/h2&gt;

&lt;p&gt;Docgrity scans your repository's Markdown and finds:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Duplicates&lt;/strong&gt; — docs saying essentially the same thing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contradictions&lt;/strong&gt; — conflicting claims, shown side by side&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open questions&lt;/strong&gt; — unanswered questions and stale TODOs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And every finding comes with evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No finding without a receipt.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The source text is quoted verbatim and verified against the actual file before a finding is reported. If the model can't prove it, the finding gets dropped.&lt;/p&gt;

&lt;h3&gt;
  
  
  VS Code
&lt;/h3&gt;

&lt;p&gt;An interactive dashboard inside VS Code, with clickable evidence and draft GitHub issues you approve before posting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;code &lt;span class="nt"&gt;--install-extension&lt;/span&gt; ujjavala.docgrity
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://ujjavala.github.io/docgrity-vscode-site/index.html" rel="noopener noreferrer"&gt;Docgrity for VS Code&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  CLI
&lt;/h3&gt;

&lt;p&gt;Prefer the terminal? The CLI is read-only and local-first, with Ollama by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; docgrity
docgrity scan &lt;span class="nt"&gt;--open&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.npmjs.com/package/docgrity" rel="noopener noreferrer"&gt;Docgrity on npm&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Action
&lt;/h3&gt;

&lt;p&gt;You can also run documentation checks in CI, with deduplicated issues that automatically close when findings are resolved.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ujjavala/docgrity-vscode/action@main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/ujjavala/docgrity-vscode" rel="noopener noreferrer"&gt;Docgrity on GitHub&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  And now, Confluence
&lt;/h2&gt;

&lt;p&gt;Docs don't only live in repositories.&lt;/p&gt;

&lt;p&gt;I'm also building &lt;strong&gt;Docgrity for Confluence&lt;/strong&gt;, an early-stage Forge app that scans your Confluence spaces for the same problems: duplicates, contradictions and unanswered questions.&lt;/p&gt;

&lt;p&gt;It's currently available through a direct install rather than the Atlassian Marketplace.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://ujjavala.github.io/docgrity-site/index.html" rel="noopener noreferrer"&gt;Install / try Docgrity for Confluence&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It's early, but the idea is the same: make the knowledge your team — and your AI agents — rely on a little more trustworthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Zero infrastructure
&lt;/h2&gt;

&lt;p&gt;I wanted Docgrity to be useful without adding another service that needs to own your documentation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero infrastructure. No servers. No telemetry. MIT licensed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The goal is simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation integrity for you, your team, and your agent friends. 🤖&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Run a scan on a repo or Confluence space you think you know.&lt;/p&gt;

&lt;p&gt;If it finds something that makes you go &lt;em&gt;"wait… that's still in there?"&lt;/em&gt; — I'd love to hear about it.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>mlhacks</category>
      <category>hackathon</category>
      <category>devchallenge</category>
    </item>
    <item>
      <title>AI Engineering Is Easy. Changing How We Work Is Hard</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Fri, 04 Sep 2026 09:24:10 +0000</pubDate>
      <link>https://dev.to/ujja/ai-engineering-is-easy-changing-how-we-work-is-hard-39j4</link>
      <guid>https://dev.to/ujja/ai-engineering-is-easy-changing-how-we-work-is-hard-39j4</guid>
      <description>&lt;p&gt;AI engineering sounds fancy. New terms are everywhere: agentic development, AI-native engineering, spec-driven development, and now AI harness engineering. Underneath all the terminology, though, something genuinely useful is happening. AI can now help with requirements, challenge a &lt;a href="https://en.wikipedia.org/wiki/Product_requirements_document" rel="noopener noreferrer"&gt;PRD&lt;/a&gt;, explore UX ideas, reason about architecture, create implementation plans, write code and validate the result.&lt;/p&gt;

&lt;p&gt;The obvious question is what AI can do. The more interesting question is whether the way we build software is ready for it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The workflow is changing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A workflow we've been exploring breaks development into five stages: &lt;strong&gt;requirements, refinement, planning, build and validation&lt;/strong&gt;. The stages themselves aren't new, but AI can now participate in each one. It can take existing product inputs, help clarify the problem, question assumptions, identify gaps in a PRD and then turn a well-defined requirement into a plan and eventually implementation tasks.&lt;/p&gt;

&lt;p&gt;This puts more emphasis on the quality of the requirements. A human involved in a project might understand what “improve the experience” means because they've had several conversations about it. An agent doesn't have that shared history. It needs the problem, scope, constraints, edge cases and expected outcome to be explicit. That doesn't mean writing enormous specifications; it means using AI to help make the requirements precise before we start building.&lt;/p&gt;

&lt;p&gt;AI can actually be a useful, slightly annoying reviewer here, asking what happens when something fails, whether a requirement is testable, whether two parts of the document contradict each other and what we haven't considered yet. It can also help compare different versions of a PRD or have one model review another's output, making gaps easier to spot. The important part is that AI is helping us uncover ambiguity, not making the decisions for us.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Maybe coding isn't the bottleneck&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;This becomes more interesting when we look at where teams actually spend their time. Complex work can involve several rounds between product, UX, requirements and engineering before development can properly begin. That's often necessary, but it can also create significant bottlenecks. If an AI agent can produce a working implementation quickly, waiting two weeks for a requirement to be clarified becomes a much bigger problem than it used to be.&lt;/p&gt;

&lt;p&gt;This suggests that engineers need to be involved earlier rather than receiving requirements only once they're considered finished. UX needs to be part of the conversation early too. A rough prototype or wireframe can expose gaps in a requirement much faster than another round of discussion, and AI makes creating those lightweight prototypes much cheaper. Requirements, UX and technical design can become an iterative loop rather than a series of handoffs.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;More documentation isn't always better&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The instinct with AI is often to give it more documentation: more PRDs, more architecture diagrams, more wiki pages and more context. But if several documents describe the same capability differently, we're not really giving the agent better context; we're giving it more ways to get confused.&lt;/p&gt;

&lt;p&gt;What the agent needs is a way to navigate the system. A clear project structure, focused documentation, useful agent instructions, architecture decisions that explain why something exists and a well-organised codebase can be far more useful than one giant specification. The goal isn't to give AI everything we know. It is to make it easy for AI to find what it needs when it needs it.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The ticket might be the problem&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;AI also makes me question how we slice work. A ticket that takes weeks or months is difficult for an agent to reason about, and arguably isn't a particularly useful unit of work for humans either. “Build authentication” is very different from breaking the problem into password reset, token validation, password updates and the associated tests.&lt;/p&gt;

&lt;p&gt;That doesn't mean turning every feature into dozens of tiny tickets. We just need work that has a clear purpose, manageable scope and a definition of done. If something takes months, it might be a project hiding in a Jira costume.&lt;/p&gt;

&lt;p&gt;At the same time, not everything needs the full AI lifecycle. A large feature may benefit from structured requirements, refinement, planning and validation, while a small BAU change might only need a prompt and a developer. If we apply the same process to everything, we risk replacing one form of bureaucracy with another. The workflow should match the complexity of the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The agent needs to see the real system&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;There is also a fairly fundamental requirement: &lt;strong&gt;the agent needs to understand the real system&lt;/strong&gt;. Giving it a PRD without access to the relevant codebase means it is still making assumptions. With the code, it can find existing patterns, understand constraints, reuse functionality and spot when a proposed solution doesn't fit.&lt;/p&gt;

&lt;p&gt;Of course, giving AI access to source code introduces security, licensing, privacy and organisational considerations, so AI adoption isn't simply a matter of choosing the right model. Some of the biggest barriers have nothing to do with the model at all.&lt;/p&gt;

&lt;p&gt;And once multiple developers and agents are working in parallel, things get even more interesting. Each agent has its own context and can make decisions based on what it currently sees. One agent can change something another doesn't know about, or two agents can make perfectly reasonable decisions that don't work well together. This makes good engineering practices even more important: small changes, clear boundaries, good tests, frequent reviews and consistent project rules.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;So, are we ready?&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Probably not completely, and that's fine. We don't need to jump straight into autonomous software development. We can take real pieces of work, try these workflows, see where they break and improve the process as we go.&lt;/p&gt;

&lt;p&gt;Because AI isn't just changing how quickly we write code. It is exposing everything around the code that slows us down: unclear requirements, oversized tickets, late UX involvement, documentation drift, handoffs, access restrictions and decisions that take days to make.&lt;/p&gt;

&lt;p&gt;That's why I think the idea of an &lt;strong&gt;AI harness&lt;/strong&gt; is bigger than prompts and agent configuration. The harness is the environment we create around the AI: how we define work, how product, UX and engineering collaborate, how knowledge is structured, how repositories are organised, how work is validated and what access the agent has.&lt;/p&gt;

&lt;p&gt;The AI might be the new part, but the way we work around it is what will determine whether it actually makes us faster.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI engineering is easy. Changing how we work is hard.&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>agents</category>
      <category>software</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Turning Ayurveda’s Six Rasas Into CSS Art</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Sat, 15 Aug 2026 03:16:21 +0000</pubDate>
      <link>https://dev.to/ujja/rasa-turning-ayurvedas-six-rasas-into-css-art-4go6</link>
      <guid>https://dev.to/ujja/rasa-turning-ayurvedas-six-rasas-into-css-art-4go6</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, CSS Art&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;What does comfort food look like when you don't use food photography?&lt;/p&gt;

&lt;p&gt;That was the question behind &lt;strong&gt;Rasa&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Rasa is an interactive culinary atlas exploring India through &lt;strong&gt;Shadrasa&lt;/strong&gt;, the six tastes described in Ayurveda:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Madhura&lt;/strong&gt; — sweet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Amla&lt;/strong&gt; — sour&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lavana&lt;/strong&gt; — salty&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Katu&lt;/strong&gt; — pungent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tikta&lt;/strong&gt; — bitter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kashaya&lt;/strong&gt; — astringent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to turn those six tastes into something visual rather than simply presenting them as text or cards.&lt;/p&gt;

&lt;p&gt;The result is a tongue-shaped interactive map.&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%2Frin2mx4qa5m9pq76wtni.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%2Frin2mx4qa5m9pq76wtni.png" alt="Six guided taste receptors are distributed across the tongue" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One important clarification: the familiar "tongue taste map" is scientifically inaccurate. Taste receptors are distributed across the tongue. In Rasa, the tongue is purely a &lt;strong&gt;cultural and artistic navigation metaphor&lt;/strong&gt;, not an anatomy diagram or medical claim.&lt;/p&gt;

&lt;p&gt;The idea was to make something that felt like opening an old food journal rather than browsing a database.&lt;/p&gt;

&lt;h2&gt;
  
  
  The CSS Art
&lt;/h2&gt;

&lt;p&gt;The tongue is probably the most obvious piece of CSS art in the project.&lt;/p&gt;

&lt;p&gt;It isn't an image.&lt;/p&gt;

&lt;p&gt;It is a layered CSS illustration with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Six selectable rasa regions&lt;/li&gt;
&lt;li&gt;A draggable, bounded CSS 3D orbit&lt;/li&gt;
&lt;li&gt;Pointer-responsive lighting&lt;/li&gt;
&lt;li&gt;Pan and zoom&lt;/li&gt;
&lt;li&gt;Keyboard orbit and navigation&lt;/li&gt;
&lt;li&gt;A dedicated control to centre the 3D view&lt;/li&gt;
&lt;li&gt;Papillae and surface texture&lt;/li&gt;
&lt;li&gt;A median sulcus&lt;/li&gt;
&lt;li&gt;Subtle veins and mucosal variation&lt;/li&gt;
&lt;li&gt;Rim lighting&lt;/li&gt;
&lt;li&gt;Subsurface colour&lt;/li&gt;
&lt;li&gt;Counter-scaled labels that remain readable while zooming&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The tongue can be explored almost like a little map.&lt;/p&gt;

&lt;p&gt;Select a rasa and the camera moves towards that region, revealing the corresponding field notes.&lt;/p&gt;

&lt;p&gt;The 3D effect does not use WebGL, canvas or a 3D library. JavaScript only captures pointer and keyboard input and updates a few CSS custom properties. Perspective, depth, lighting, counter-rotation and shadows remain CSS work.&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%2F1vof5ofgk44aykd3f8bn.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%2F1vof5ofgk44aykd3f8bn.png" alt="Nima, the bitterness guide" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That interaction was important to me.&lt;/p&gt;

&lt;p&gt;I didn't want the CSS art to be something you simply look at.&lt;/p&gt;

&lt;p&gt;I wanted it to become part of the interface.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making Food Without Food Images
&lt;/h2&gt;

&lt;p&gt;The other fun constraint was food itself.&lt;/p&gt;

&lt;p&gt;Instead of using food photography, every dish has a small declarative &lt;strong&gt;plate-art recipe&lt;/strong&gt;.&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%2F8a69r98ez652ynqr6wsi.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%2F8a69r98ez652ynqr6wsi.png" alt="Pulihora, a South Indian dish" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The renderer combines things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Vessel&lt;/li&gt;
&lt;li&gt;Food form&lt;/li&gt;
&lt;li&gt;Colours&lt;/li&gt;
&lt;li&gt;Garnishes&lt;/li&gt;
&lt;li&gt;Texture&lt;/li&gt;
&lt;li&gt;Lighting&lt;/li&gt;
&lt;li&gt;Serving temperature&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The renderer supports vessels including thalis, plates, bowls, kadhais, clay pots, banana leaves and leaf cones.&lt;/p&gt;

&lt;p&gt;Food forms include rounds, stacks, curries, grains, crescents, cones, cubes and strands.&lt;/p&gt;

&lt;p&gt;CSS then adds details such as oil sheen, highlights, contact shadows, steam, heat haze, condensation and cold mist.&lt;/p&gt;

&lt;p&gt;So a dish isn't a static illustration.&lt;/p&gt;

&lt;p&gt;It is data that becomes an illustration.&lt;/p&gt;

&lt;p&gt;That was one of my favourite parts of building Rasa.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six Handcrafted Taste Guides
&lt;/h2&gt;

&lt;p&gt;Each rasa is accompanied by a guide with a distinct silhouette, costume, hairstyle, expression and role:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Guddi&lt;/strong&gt;, the Culinary Archivist for sweet&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ami&lt;/strong&gt;, the Fermentation Botanist for sour&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Neer&lt;/strong&gt;, the Coastal Navigator for salty&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tara&lt;/strong&gt;, the Spice Route Chronicler for pungent&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nima&lt;/strong&gt;, the Ethnobotanical Elder for bitter&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jamu&lt;/strong&gt;, the Tea and Tannin Cartographer for astringent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The characters are built from nested HTML and CSS rather than generated avatars, SVG artwork or image files. Gradients, borders, pseudo-elements, clipping and transforms create their faces, clothing, hair, accessories and tools.&lt;/p&gt;

&lt;p&gt;They can greet the visitor, scout ingredients and share lore. Their mouths respond while they speak, and gestures such as walking, waving and pointing are finite rather than perpetual. This keeps the characters playful without turning the page into a wall of constant motion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making CSS Feel Less Like CSS
&lt;/h2&gt;

&lt;p&gt;A lot of the work was experimenting with the things CSS is surprisingly good at:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gradients + shadows + transforms + masks + pseudo-elements + 3D transforms&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Individually, none of those are particularly exotic.&lt;/p&gt;

&lt;p&gt;Combined carefully, they can start to feel much closer to illustration.&lt;/p&gt;

&lt;p&gt;I also added a small layer of atmospheric "ingredient wind" across the site.&lt;/p&gt;

&lt;p&gt;Different rasas get their own ingredient:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Jaggery for sweet&lt;/li&gt;
&lt;li&gt;Mango leaf for sour&lt;/li&gt;
&lt;li&gt;Salt crystal for salty&lt;/li&gt;
&lt;li&gt;Chilli for pungent&lt;/li&gt;
&lt;li&gt;Neem leaf for bitter&lt;/li&gt;
&lt;li&gt;Jamun for astringent&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The ingredients drift across the page like leaves in a breeze.&lt;/p&gt;

&lt;p&gt;It is intentionally subtle.&lt;/p&gt;

&lt;p&gt;It can also be paused, disappears in forced-colour mode, and respects reduced-motion preferences.&lt;/p&gt;

&lt;h2&gt;
  
  
  CSS Art With a Purpose
&lt;/h2&gt;

&lt;p&gt;One thing I wanted to avoid was making a collection of pretty CSS experiments that didn't actually help the user.&lt;/p&gt;

&lt;p&gt;The visual language is tied to the content.&lt;/p&gt;

&lt;p&gt;The tongue represents the six tastes.&lt;/p&gt;

&lt;p&gt;The plate art represents the food.&lt;/p&gt;

&lt;p&gt;The notebook changes atmosphere depending on the dish.&lt;/p&gt;

&lt;p&gt;Hot dishes introduce warmth, steam and heat shimmer.&lt;/p&gt;

&lt;p&gt;Chilled dishes introduce condensation, frost and cool mist.&lt;/p&gt;

&lt;p&gt;Festival dishes receive restrained gold details.&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%2Fp94a5vew39p5tdy2jf2e.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%2Fp94a5vew39p5tdy2jf2e.png" alt="Festive dishes" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Street food gets a subtle field-stamp treatment.&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%2Ftnkzhulskvpuyppgy5o3.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%2Ftnkzhulskvpuyppgy5o3.png" alt="Street food dokhla" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The visual effects are therefore part of the storytelling rather than decoration added afterwards.&lt;/p&gt;

&lt;h2&gt;
  
  
  84 Dishes Instead of Six
&lt;/h2&gt;

&lt;p&gt;The six tastes are only the entry point.&lt;/p&gt;

&lt;p&gt;Rasa contains &lt;strong&gt;84 dishes&lt;/strong&gt;, with 14 primary dishes for each rasa, covering all 28 Indian states and multiple Union Territories across eight culinary macro-regions.&lt;/p&gt;

&lt;p&gt;The dataset intentionally goes beyond the usual shorthand for Indian food.&lt;/p&gt;

&lt;p&gt;There are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Everyday meals&lt;/li&gt;
&lt;li&gt;Breakfasts&lt;/li&gt;
&lt;li&gt;Snacks&lt;/li&gt;
&lt;li&gt;Street foods&lt;/li&gt;
&lt;li&gt;Festival foods&lt;/li&gt;
&lt;li&gt;Temple foods&lt;/li&gt;
&lt;li&gt;City specialities&lt;/li&gt;
&lt;li&gt;Folk and Adivasi traditions&lt;/li&gt;
&lt;li&gt;Fermented foods&lt;/li&gt;
&lt;li&gt;Coastal cuisines&lt;/li&gt;
&lt;li&gt;Mountain cuisines&lt;/li&gt;
&lt;li&gt;Sweets&lt;/li&gt;
&lt;li&gt;Drinks&lt;/li&gt;
&lt;li&gt;Pickles&lt;/li&gt;
&lt;li&gt;Breads&lt;/li&gt;
&lt;li&gt;Rice dishes&lt;/li&gt;
&lt;li&gt;Curries&lt;/li&gt;
&lt;li&gt;Preserved foods&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal was breadth rather than stereotype.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Technical Side
&lt;/h2&gt;

&lt;p&gt;Rasa is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 18&lt;/li&gt;
&lt;li&gt;TypeScript 5.7&lt;/li&gt;
&lt;li&gt;Vite 6&lt;/li&gt;
&lt;li&gt;Tailwind CSS 4&lt;/li&gt;
&lt;li&gt;Lucide React&lt;/li&gt;
&lt;li&gt;Handcrafted CSS character art&lt;/li&gt;
&lt;li&gt;Playwright&lt;/li&gt;
&lt;li&gt;axe-core&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application is data-driven.&lt;/p&gt;

&lt;p&gt;The six rasas, dishes, regions, festivals, spices and guides are represented as domain models rather than being embedded directly into components.&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%2F52pmxjxhhdw53r802aoj.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%2F52pmxjxhhdw53r802aoj.png" alt="Indian Regions" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That makes the CSS renderer reusable.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;Dish&lt;/code&gt; can describe what something is, while its &lt;code&gt;PlateArt&lt;/code&gt; recipe describes how it should look.&lt;/p&gt;

&lt;p&gt;That separation made it much easier to keep adding dishes without creating a new component for every food.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility Still Applies to CSS Art
&lt;/h2&gt;

&lt;p&gt;One of the interesting challenges was making something visually experimental still behave like a normal application.&lt;/p&gt;

&lt;p&gt;The map is keyboard-operable.&lt;/p&gt;

&lt;p&gt;The rasa controls, field-note tabs, guide commands, notebook corners and CSS 3D orbit are keyboard-operable.&lt;/p&gt;

&lt;p&gt;Interactive controls have accessible names and 44 × 44px minimum targets.&lt;/p&gt;

&lt;p&gt;There is a skip link.&lt;/p&gt;

&lt;p&gt;Notebook page changes are announced to screen readers.&lt;/p&gt;

&lt;p&gt;The experience supports reduced motion, persistent motion controls, forced colours, 200% zoom, text-spacing overrides and 320px reflow. Native dish names carry language metadata, page and guide changes use polite status announcements, and textured-paper text now uses darker ink colours for enhanced contrast.&lt;/p&gt;

&lt;p&gt;I also added 13 Playwright accessibility and interaction journeys. They run axe with WCAG 2.0, 2.1 and 2.2 A, AA and AAA tags, exercise all six rasa states and all field-note tabs, and check keyboard selection, reduced motion, text spacing, target sizes, finite guide animation, CSS 3D keyboard orbit and notebook corner navigation.&lt;/p&gt;

&lt;p&gt;The latest automated run passes all 13 journeys with no axe violations or unresolved structural ARIA checks. Textured gradients can prevent automated tools from resolving every contrast pair, so the underlying ink-on-paper colour pairs were also reviewed directly. This still is not a claim that automation proves complete WCAG AAA conformance: manual testing remains necessary for reading order, language and pronunciation, cognitive clarity, and real assistive-technology behaviour.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;The biggest lesson was that CSS art becomes much more interesting when it has a job.&lt;/p&gt;

&lt;p&gt;It is tempting to ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"How can I make this look cool with CSS?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A better question turned out to be:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What should this visual communicate?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once the answer was clear, the implementation became much easier.&lt;/p&gt;

&lt;p&gt;The tongue needed depth because it was a map.&lt;/p&gt;

&lt;p&gt;The food needed texture because it represented serving and preparation.&lt;/p&gt;

&lt;p&gt;The notebook needed page turns because the content was designed as a field journal.&lt;/p&gt;

&lt;p&gt;The current notebook has clickable outer page corners as well as conventional Previous and Next controls. Each corner uses a small rounded CSS curl: the right edge lifts subtly upward and left, while the left edge lifts upward and right. The fold, underside, highlight and shadow are all CSS, while the button underneath remains semantic and keyboard accessible.&lt;/p&gt;

&lt;p&gt;The motion needed to communicate state instead of simply moving things around.&lt;/p&gt;

&lt;p&gt;That became one of the design principles for the entire project:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Motion should explain state or create atmosphere, never prevent reading.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🍛 &lt;strong&gt;Try Rasa:&lt;/strong&gt; &lt;a href="https://rasa-mu.vercel.app/" rel="noopener noreferrer"&gt;https://rasa-mu.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The source code is available on GitHub:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ujjavala" rel="noopener noreferrer"&gt;
        ujjavala
      &lt;/a&gt; / &lt;a href="https://github.com/ujjavala/rasa" rel="noopener noreferrer"&gt;
        rasa
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Rasa — The Tongue Map of India&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;An interactive culinary atlas that explores India through &lt;strong&gt;Shadrasa&lt;/strong&gt;, Ayurveda's six tastes: sweet, sour, salty, pungent, bitter, and astringent.&lt;/p&gt;
&lt;p&gt;Rasa is a React experience built for the DEV Frontend Challenge themes &lt;strong&gt;CSS Art: Comfort Food&lt;/strong&gt; and &lt;strong&gt;Perfect Landing: Comfort Food&lt;/strong&gt;. It combines an editorial landing page, a zoomable CSS-art tongue map, character-led exploration, regional food history, and a page-turning field notebook containing 84 dishes from across India.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; the familiar “tongue taste map” is scientifically inaccurate. Taste receptors are distributed across the tongue. This project uses the tongue only as a cultural and artistic navigation metaphor for the six rasas; it is not an anatomy chart or medical guidance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why this project exists&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Indian food is often flattened into a small set of restaurant dishes. Rasa was created to show a wider table: home cooking, street food, temple food, festival dishes…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ujjavala/rasa" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Youtube link: &lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Ayx92pdAXPI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;There are plenty of things I'd like to explore further:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ingredient and state-based search&lt;/li&gt;
&lt;li&gt;Seasonal food trails&lt;/li&gt;
&lt;li&gt;Audio pronunciation for native dish names&lt;/li&gt;
&lt;li&gt;More community-reviewed cultural sources&lt;/li&gt;
&lt;li&gt;Shareable notebook pages&lt;/li&gt;
&lt;li&gt;Deeper VoiceOver, NVDA, TalkBack and keyboard-only testing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For now, Rasa is my little experiment in asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How far can CSS go when you stop treating it as styling and start treating it as an illustration medium?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And perhaps more importantly:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can a website about comfort food make you hungry without using a single food photograph?&lt;/strong&gt; 🍚&lt;/p&gt;

</description>
      <category>frontendchallenge</category>
      <category>devchallenge</category>
      <category>css</category>
      <category>react</category>
    </item>
    <item>
      <title>Designing a Landing Page That Feels Like Opening a Food Journal</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Sat, 15 Aug 2026 03:16:15 +0000</pubDate>
      <link>https://dev.to/ujja/rasa-designing-a-landing-page-that-feels-like-opening-a-food-journal-mb9</link>
      <guid>https://dev.to/ujja/rasa-designing-a-landing-page-that-feels-like-opening-a-food-journal-mb9</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, Perfect Landing&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;I built &lt;strong&gt;Rasa&lt;/strong&gt;, an interactive culinary atlas that explores India through the six tastes of &lt;strong&gt;Shadrasa&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;sweet, sour, salty, pungent, bitter and astringent.&lt;/strong&gt;&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%2Fmvaaku7oppfe0wzfsw2o.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%2Fmvaaku7oppfe0wzfsw2o.png" alt="Image showing India is more than just butter chicken. Also has a rasa wheel on the right" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The starting point was simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if a food atlas felt less like a database and more like opening a traveller's field journal?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That idea shaped the entire landing page.&lt;/p&gt;

&lt;p&gt;Instead of starting with a grid of dishes or a traditional restaurant-style hero, I wanted the page to feel editorial, tactile and slightly mysterious.&lt;/p&gt;

&lt;p&gt;The landing page introduces the six tastes through a CSS-built rasa mandala and then invites the visitor into the atlas.&lt;/p&gt;

&lt;p&gt;From there, the experience expands into an interactive tongue map, character-led exploration, regional food history and a page-turning culinary notebook containing 84 dishes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Landing Page Is the Beginning, Not the Destination
&lt;/h2&gt;

&lt;p&gt;I approached the hero as an invitation rather than a summary.&lt;/p&gt;

&lt;p&gt;The visitor first encounters the concept of Rasa.&lt;/p&gt;

&lt;p&gt;Then the six tastes.&lt;/p&gt;

&lt;p&gt;Then the tongue.&lt;/p&gt;

&lt;p&gt;Then the exploration.&lt;/p&gt;

&lt;p&gt;That progression was deliberate.&lt;/p&gt;

&lt;p&gt;I didn't want to put every feature into the first screen.&lt;/p&gt;

&lt;p&gt;The landing page establishes the mental model first:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rasa → taste → region → dish → story&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Once that relationship is understood, the rest of the application becomes easier to navigate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Tongue?
&lt;/h2&gt;

&lt;p&gt;There is a familiar visual idea of dividing the tongue into taste regions.&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%2F6ep0vtpdeyupoo18ngdq.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%2F6ep0vtpdeyupoo18ngdq.png" alt="Has a 3D tongue on which personified taste guides stand" width="800" height="409"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Taste receptors are distributed across the tongue, so Rasa does &lt;strong&gt;not&lt;/strong&gt; present this as an anatomy chart.&lt;/p&gt;

&lt;p&gt;Instead, I used the tongue as a visual metaphor.&lt;/p&gt;

&lt;p&gt;Six regions.&lt;/p&gt;

&lt;p&gt;Six tastes.&lt;/p&gt;

&lt;p&gt;Six entry points into a much larger culinary landscape.&lt;/p&gt;

&lt;p&gt;The tongue is interactive, zoomable and keyboard accessible. It also supports a shallow, bounded 3D orbit: visitors can drag the backdrop, use arrow keys, hold Shift for larger steps, or press Home and the dedicated centre control to reset the view.&lt;/p&gt;

&lt;p&gt;Selecting a rasa moves the camera towards that region and opens its field notes.&lt;/p&gt;

&lt;p&gt;The depth effect is still CSS-first. JavaScript captures input and updates CSS custom properties; perspective, layer separation, lighting, shadows and character depth are rendered in CSS. There is no canvas, WebGL or 3D dependency.&lt;/p&gt;

&lt;p&gt;So the hero concept eventually becomes the primary navigation mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Landing Page to Field Journal
&lt;/h2&gt;

&lt;p&gt;The biggest design decision was to avoid making the experience feel like a food catalogue.&lt;/p&gt;

&lt;p&gt;India has far too many food traditions for that approach to feel right.&lt;/p&gt;

&lt;p&gt;Rasa explores:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Everyday home cooking&lt;/li&gt;
&lt;li&gt;Street food&lt;/li&gt;
&lt;li&gt;Festival and temple food&lt;/li&gt;
&lt;li&gt;City specialities&lt;/li&gt;
&lt;li&gt;Folk and Adivasi traditions&lt;/li&gt;
&lt;li&gt;Fermented foods&lt;/li&gt;
&lt;li&gt;Coastal cuisines&lt;/li&gt;
&lt;li&gt;Mountain cuisines&lt;/li&gt;
&lt;li&gt;Regional ingredients and techniques&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The current dataset contains &lt;strong&gt;84 dishes&lt;/strong&gt; across eight culinary macro-regions, representing all 28 Indian states and multiple Union Territories.&lt;/p&gt;

&lt;p&gt;Each dish includes context around its history, ingredients, technique, occasions and regional identity.&lt;/p&gt;

&lt;p&gt;The landing page therefore needed to communicate &lt;strong&gt;exploration&lt;/strong&gt;, not simply consumption.&lt;/p&gt;

&lt;h2&gt;
  
  
  Meet the Guides
&lt;/h2&gt;

&lt;p&gt;I also wanted the interface to have some personality.&lt;/p&gt;

&lt;p&gt;Each rasa has a guide with a distinct identity:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Taste&lt;/th&gt;
&lt;th&gt;Guide&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Madhura · sweet&lt;/td&gt;
&lt;td&gt;Guddi&lt;/td&gt;
&lt;td&gt;Culinary Archivist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amla · sour&lt;/td&gt;
&lt;td&gt;Ami&lt;/td&gt;
&lt;td&gt;Fermentation Botanist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Lavana · salty&lt;/td&gt;
&lt;td&gt;Neer&lt;/td&gt;
&lt;td&gt;Coastal Navigator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Katu · pungent&lt;/td&gt;
&lt;td&gt;Tara&lt;/td&gt;
&lt;td&gt;Spice Route Chronicler&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tikta · bitter&lt;/td&gt;
&lt;td&gt;Nima&lt;/td&gt;
&lt;td&gt;Ethnobotanical Elder&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kashaya · astringent&lt;/td&gt;
&lt;td&gt;Jamu&lt;/td&gt;
&lt;td&gt;Tea and Tannin Cartographer&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each guide can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Greet&lt;/li&gt;
&lt;li&gt;Scout ingredients&lt;/li&gt;
&lt;li&gt;Reveal lore&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The portraits are handcrafted from nested HTML and CSS. Each guide has a different figure, skin tone, costume, hairstyle, accessories and expression, with Guddi, Tara and Nima represented as women and Ami, Neer and Jamu as men. They use no generated avatar package, SVG character asset or image file.&lt;/p&gt;

&lt;p&gt;Their walking, waving, scouting and lore gestures are intentionally finite rather than constant idle animation. Speech bubbles restart for each command, and the mouth moves briefly while a guide talks.&lt;/p&gt;

&lt;p&gt;That was another deliberate choice.&lt;/p&gt;

&lt;p&gt;I wanted characters to feel responsive rather than noisy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Notebook
&lt;/h2&gt;

&lt;p&gt;The deeper you go into the experience, the more the interface changes.&lt;/p&gt;

&lt;p&gt;Instead of a conventional card grid, dishes appear in a two-page field notebook.&lt;/p&gt;

&lt;p&gt;You can turn pages.&lt;/p&gt;

&lt;p&gt;You can also use the lifted outer corners: the right corner turns forward and the left corner turns backward. They are semantic buttons with subtle rounded CSS curls that rise inward like the edge of a real page, while the existing Previous, Next and direct-jump controls remain available.&lt;/p&gt;

&lt;p&gt;Jump directly to a dish.&lt;/p&gt;

&lt;p&gt;Move backwards and forwards.&lt;/p&gt;

&lt;p&gt;See regional and contextual annotations.&lt;/p&gt;

&lt;p&gt;Read the native name with appropriate language metadata.&lt;/p&gt;

&lt;p&gt;Follow the dish to further context.&lt;/p&gt;

&lt;p&gt;The notebook also changes atmosphere based on the food.&lt;/p&gt;

&lt;p&gt;A piping-hot dish can introduce warmth, steam and heat shimmer.&lt;/p&gt;

&lt;p&gt;A chilled dish can introduce frost, condensation and cool mist.&lt;/p&gt;

&lt;p&gt;Festival foods receive restrained gold detailing.&lt;/p&gt;

&lt;p&gt;Street foods receive a subtle field-stamp treatment.&lt;/p&gt;

&lt;p&gt;This was my way of making the interface feel like a physical object rather than another collection of UI cards.&lt;/p&gt;

&lt;h2&gt;
  
  
  Designing for Accessibility From the Start
&lt;/h2&gt;

&lt;p&gt;A visually heavy landing page can very quickly become inaccessible.&lt;/p&gt;

&lt;p&gt;So accessibility wasn't treated as something to check after the animations were finished.&lt;/p&gt;

&lt;p&gt;The application includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Semantic landmarks and headings&lt;/li&gt;
&lt;li&gt;A skip link&lt;/li&gt;
&lt;li&gt;Keyboard-operable navigation&lt;/li&gt;
&lt;li&gt;Roving tab navigation&lt;/li&gt;
&lt;li&gt;Visible focus states&lt;/li&gt;
&lt;li&gt;Accessible icon controls&lt;/li&gt;
&lt;li&gt;44 × 44px minimum interactive targets&lt;/li&gt;
&lt;li&gt;Screen-reader announcements&lt;/li&gt;
&lt;li&gt;Reduced-motion support&lt;/li&gt;
&lt;li&gt;Persistent pause/play motion control&lt;/li&gt;
&lt;li&gt;Forced-colour support&lt;/li&gt;
&lt;li&gt;200% zoom support&lt;/li&gt;
&lt;li&gt;320px reflow&lt;/li&gt;
&lt;li&gt;Text-spacing resilience&lt;/li&gt;
&lt;li&gt;Keyboard control for the CSS 3D orbit and notebook corners&lt;/li&gt;
&lt;li&gt;Enhanced ink-on-paper contrast&lt;/li&gt;
&lt;li&gt;Automated axe checks through Playwright across all six rasa states and every field-note tab&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The principle was simple:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The experience should remain useful when the visual effects disappear.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If someone disables animation, uses keyboard navigation, zooms the page or doesn't have pointer input, the content and navigation still need to make sense.&lt;/p&gt;

&lt;p&gt;The current suite contains 13 accessibility and interaction journeys and passes in Chromium. It runs axe using WCAG 2.0, 2.1 and 2.2 A, AA and AAA tags, and now rejects unresolved structural ARIA findings as well as violations. Automation still cannot certify every AAA success criterion, especially where manual judgement, language, pronunciation, reading clarity or real assistive technology is involved, so manual review remains part of the roadmap.&lt;/p&gt;

&lt;h2&gt;
  
  
  Motion With Meaning
&lt;/h2&gt;

&lt;p&gt;There is quite a lot of movement in Rasa.&lt;/p&gt;

&lt;p&gt;But every type of movement has a reason.&lt;/p&gt;

&lt;p&gt;Guide movement communicates an interaction.&lt;/p&gt;

&lt;p&gt;The camera movement communicates navigation.&lt;/p&gt;

&lt;p&gt;Notebook movement communicates page direction.&lt;/p&gt;

&lt;p&gt;Steam and heat haze communicate serving temperature.&lt;/p&gt;

&lt;p&gt;Ingredient wind creates atmosphere.&lt;/p&gt;

&lt;p&gt;And the global motion control lets the user pause the experience.&lt;/p&gt;

&lt;p&gt;The application also respects &lt;code&gt;prefers-reduced-motion&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This led to one of my favourite principles from the project:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Movement should explain state or create atmosphere, never prevent reading.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Visual Language
&lt;/h2&gt;

&lt;p&gt;I wanted the interface to feel somewhere between:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;a field journal + an old map + a recipe notebook + a museum exhibit.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Brass.&lt;/p&gt;

&lt;p&gt;Paper.&lt;/p&gt;

&lt;p&gt;Ink.&lt;/p&gt;

&lt;p&gt;Food vessels.&lt;/p&gt;

&lt;p&gt;Spice textures.&lt;/p&gt;

&lt;p&gt;Map controls.&lt;/p&gt;

&lt;p&gt;Shadows.&lt;/p&gt;

&lt;p&gt;Atmospheric movement.&lt;/p&gt;

&lt;p&gt;Most of those visual elements are created with CSS rather than remote food photography.&lt;/p&gt;

&lt;p&gt;That constraint ended up becoming one of the most interesting parts of the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Stack
&lt;/h2&gt;

&lt;p&gt;The application is built with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;React 18&lt;/li&gt;
&lt;li&gt;TypeScript 5.7 in strict mode&lt;/li&gt;
&lt;li&gt;Vite 6&lt;/li&gt;
&lt;li&gt;Tailwind CSS 4&lt;/li&gt;
&lt;li&gt;Lucide React&lt;/li&gt;
&lt;li&gt;Handcrafted HTML and CSS character art&lt;/li&gt;
&lt;li&gt;ESLint&lt;/li&gt;
&lt;li&gt;Stylelint&lt;/li&gt;
&lt;li&gt;Playwright&lt;/li&gt;
&lt;li&gt;axe-core&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The application is deliberately data-driven.&lt;/p&gt;

&lt;p&gt;Rasas, dishes, regions, festivals, spices and explorers are represented as data models.&lt;/p&gt;

&lt;p&gt;React components are responsible for rendering those models rather than containing the cultural content themselves.&lt;/p&gt;

&lt;p&gt;That made it possible to build a fairly elaborate experience without turning the components into giant collections of hard-coded content.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Enjoyed Most
&lt;/h2&gt;

&lt;p&gt;The most satisfying part was seeing a landing-page idea evolve into an actual world.&lt;/p&gt;

&lt;p&gt;At first it was just:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;six tastes.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;six tastes → six regions → six guides.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;guides → dishes → regions → festivals → spices.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Eventually it became an atlas containing 84 dishes and multiple ways to explore them.&lt;/p&gt;

&lt;p&gt;That progression made the project feel much more like a small product than a challenge submission.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Improve Next
&lt;/h2&gt;

&lt;p&gt;There is still plenty I would like to add.&lt;/p&gt;

&lt;p&gt;Some ideas are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Search by ingredient, state, season and technique&lt;/li&gt;
&lt;li&gt;Audio pronunciation for native dish names&lt;/li&gt;
&lt;li&gt;User-created food trails&lt;/li&gt;
&lt;li&gt;Shareable notebook pages&lt;/li&gt;
&lt;li&gt;More community-reviewed citations and oral-history sources&lt;/li&gt;
&lt;li&gt;More manual accessibility testing with VoiceOver, NVDA and TalkBack&lt;/li&gt;
&lt;li&gt;Additional contrast review for complex textured and gradient surfaces&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🍛 &lt;strong&gt;Explore Rasa:&lt;/strong&gt; &lt;a href="https://rasa-mu.vercel.app/" rel="noopener noreferrer"&gt;https://rasa-mu.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Source Code
&lt;/h2&gt;

&lt;p&gt;The project is open source on GitHub:&lt;/p&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/ujjavala" rel="noopener noreferrer"&gt;
        ujjavala
      &lt;/a&gt; / &lt;a href="https://github.com/ujjavala/rasa" rel="noopener noreferrer"&gt;
        rasa
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Rasa — The Tongue Map of India&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;An interactive culinary atlas that explores India through &lt;strong&gt;Shadrasa&lt;/strong&gt;, Ayurveda's six tastes: sweet, sour, salty, pungent, bitter, and astringent.&lt;/p&gt;
&lt;p&gt;Rasa is a React experience built for the DEV Frontend Challenge themes &lt;strong&gt;CSS Art: Comfort Food&lt;/strong&gt; and &lt;strong&gt;Perfect Landing: Comfort Food&lt;/strong&gt;. It combines an editorial landing page, a zoomable CSS-art tongue map, character-led exploration, regional food history, and a page-turning field notebook containing 84 dishes from across India.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; the familiar “tongue taste map” is scientifically inaccurate. Taste receptors are distributed across the tongue. This project uses the tongue only as a cultural and artistic navigation metaphor for the six rasas; it is not an anatomy chart or medical guidance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why this project exists&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Indian food is often flattened into a small set of restaurant dishes. Rasa was created to show a wider table: home cooking, street food, temple food, festival dishes…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/ujjavala/rasa" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Youtube link:   &lt;iframe src="https://www.youtube.com/embed/Ayx92pdAXPI" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thought
&lt;/h2&gt;

&lt;p&gt;I started with a comfort-food challenge.&lt;/p&gt;

&lt;p&gt;I ended up building a small interactive atlas.&lt;/p&gt;

&lt;p&gt;That was probably the most enjoyable part of the challenge.&lt;/p&gt;

&lt;p&gt;Instead of asking:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How do I make a landing page about food?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I asked:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"How would a food atlas feel if you could walk into it?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rasa is my attempt at that answer.&lt;/p&gt;

&lt;p&gt;A little paper.&lt;/p&gt;

&lt;p&gt;A little spice.&lt;/p&gt;

&lt;p&gt;A little CSS magic.&lt;/p&gt;

&lt;p&gt;And a very large table. 🍛&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>You Know What's Worse Than Not Being Able to Log In?</title>
      <dc:creator>ujja</dc:creator>
      <pubDate>Thu, 13 Aug 2026 12:38:44 +0000</pubDate>
      <link>https://dev.to/ujja/you-know-whats-worse-than-not-being-able-to-log-in-5379</link>
      <guid>https://dev.to/ujja/you-know-whats-worse-than-not-being-able-to-log-in-5379</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/bugsmash"&gt;DEV's Summer Bug Smash: Smash Stories&lt;/a&gt; powered by &lt;a href="https://sentry.io/" rel="noopener noreferrer"&gt;Sentry&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You Know What's Worse Than Not Being Able to Log In?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being told everything worked right up until you try to actually use your account.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Yes, that was a real bug.&lt;/p&gt;

&lt;p&gt;And, somehow, I ended up being pulled into another authentication mystery. At this point, I’m starting to think authentication bugs have a personal grudge against me. 😅&lt;/p&gt;

&lt;p&gt;In my &lt;a href="https://dev.to/ujja/you-know-what-happens-when-theres-a-bug-in-authentication-absolutely-nothing-g0m"&gt;previous Smash Story&lt;/a&gt;, I wrote about a bug where users simply couldn't log in. This time, the problem was sneakier because most of the flow looked completely healthy. The user was approved, the background task ran, the email and SMS arrived, and Cognito had a user.&lt;/p&gt;

&lt;p&gt;Then the user actually tried to use their account.&lt;/p&gt;

&lt;p&gt;And everything fell apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  It Started With Two User Pools
&lt;/h2&gt;

&lt;p&gt;The authentication setup was fairly large and had evolved over time, so there wasn't one shiny User Pool doing everything.&lt;/p&gt;

&lt;p&gt;We had an older Cognito User Pool supporting existing authentication flows, including mobile-based signup, while a newer User Pool handled a newer flow where users received an email containing their PIN. Both pools were intentional because they supported different parts of the authentication journey.&lt;/p&gt;

&lt;p&gt;That wasn't the problem.&lt;/p&gt;

&lt;p&gt;The interesting part was that the application database had its own representation of a user, while Cognito had another. On top of that, some of the work connecting those two systems happened asynchronously.&lt;/p&gt;

&lt;p&gt;As long as everyone agreed about who the user was, nobody cared.&lt;/p&gt;

&lt;p&gt;The moment they disagreed, authentication became very interested.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tiny Timing Window
&lt;/h2&gt;

&lt;p&gt;The problem appeared in the partner and dependant journey.&lt;/p&gt;

&lt;p&gt;A member could create a partner or dependant during signup or later from the member details area. A relevant non-member user would then approve the account, which scheduled an asynchronous task called &lt;code&gt;SendingEmailsAfterApprovalBot&lt;/code&gt; in a &lt;code&gt;TaskList&lt;/code&gt; database table.&lt;/p&gt;

&lt;p&gt;That task ran every 15 minutes, and once it executed, the partner or dependant received an email and SMS containing a code. They could then confirm their account, set a password and sign in.&lt;/p&gt;

&lt;p&gt;Sounds reasonable, right?&lt;/p&gt;

&lt;p&gt;Now imagine this.&lt;/p&gt;

&lt;p&gt;A partner gets approved at 10:01 AM, so the background task is scheduled and everyone moves on with their lives. At 10:05 AM, before the task has run, the partner changes their name.&lt;/p&gt;

&lt;p&gt;That tiny change matters because the username for partner and dependant accounts is derived from their details. The database now has the &lt;strong&gt;new username&lt;/strong&gt;, while Cognito may still know about the &lt;strong&gt;old username&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Nothing has visibly broken yet.&lt;/p&gt;

&lt;p&gt;The task is still waiting patiently in the database, completely unaware that the identity it is about to process has changed underneath it.&lt;/p&gt;

&lt;p&gt;Then, 15 minutes later, it wakes up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cognito Says, "I've Seen This Email Before"
&lt;/h2&gt;

&lt;p&gt;The background task tries to create the Cognito user using the latest username stored in the database.&lt;/p&gt;

&lt;p&gt;Cognito rejects the creation because the email address is already associated with an existing user, but that user has a different username.&lt;/p&gt;

&lt;p&gt;And there is an important detail here: &lt;strong&gt;the Cognito username is immutable&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;So we weren't dealing with a simple "just update the username" situation. We had two systems describing the same person with different usernames, and Cognito was quite reasonably refusing to create another user with the same email.&lt;/p&gt;

&lt;p&gt;The database was saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This is the current username for this person."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Cognito was effectively saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I already know this email, and it belongs to someone with another username."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Same human.&lt;/p&gt;

&lt;p&gt;Different digital identity.&lt;/p&gt;

&lt;p&gt;And that mismatch was enough to break the journey.&lt;/p&gt;

&lt;h2&gt;
  
  
  Everything Looked Fine... Until It Didn't
&lt;/h2&gt;

&lt;p&gt;What made this bug particularly sneaky was what happened next.&lt;/p&gt;

&lt;p&gt;The user could still receive the expected email and SMS. There was no obvious failure screaming for attention. The asynchronous task had run, notifications had gone out and Cognito already had a user associated with the email.&lt;/p&gt;

&lt;p&gt;So the user did what any reasonable person would do: they followed the instructions.&lt;/p&gt;

&lt;p&gt;They entered their code, tried to confirm their account and attempted to set their password.&lt;/p&gt;

&lt;p&gt;Except the identity represented in Cognito didn't match the latest identity represented in the database.&lt;/p&gt;

&lt;p&gt;The user wasn't looking at a database synchronisation problem. They weren't thinking about immutable Cognito usernames or asynchronous jobs. They had received a perfectly legitimate code and were simply trying to finish creating their account.&lt;/p&gt;

&lt;p&gt;From their perspective, the system had basically said:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Here's your code. Everything is ready!"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Actually... never mind."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was the bug we had to chase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Enter Sentry
&lt;/h2&gt;

&lt;p&gt;By this point, we knew something was going wrong somewhere between the database, the background task and Cognito, but knowing &lt;em&gt;where&lt;/em&gt; it broke was another story.&lt;/p&gt;

&lt;p&gt;This is where Sentry became extremely useful. The error context gave us the details around the Cognito failure, while the breadcrumbs helped reconstruct the events leading up to it. Instead of seeing a Cognito error in isolation and assuming we simply had a duplicate-user problem, we could look at what happened before the failure and connect it back to the user's journey.&lt;/p&gt;

&lt;p&gt;The important clue was that Cognito was rejecting the user creation because the email was already associated with an existing user. That told us &lt;em&gt;what&lt;/em&gt; Cognito didn't like, but the surrounding context helped explain &lt;em&gt;why&lt;/em&gt; we had reached that point in the first place.&lt;/p&gt;

&lt;p&gt;We could see the relevant user and operation context around the failure, follow the activity leading up to the Cognito call, and use the correlation ID to connect the same operation across services and logs. That made it much easier to piece together the timeline rather than chasing individual log entries and hoping they belonged to the same request.&lt;/p&gt;

&lt;p&gt;And once the timeline was visible, the problem suddenly made sense:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;approval → delayed task → name changed → username changed → Cognito already has the email → creation rejected → identities drift apart → confirmation fails&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was the moment the bug stopped looking mysterious.&lt;/p&gt;

&lt;p&gt;The code told us what &lt;em&gt;should&lt;/em&gt; happen. Sentry's error context, breadcrumbs and correlated logs helped us see what &lt;em&gt;actually&lt;/em&gt; happened.&lt;/p&gt;

&lt;p&gt;And honestly, that's one of the best parts of having good observability during an authentication investigation. The error tells you where the system complained; the breadcrumbs help you understand the story that led it there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix Was Simpler Than the Investigation
&lt;/h2&gt;

&lt;p&gt;Once we understood the problem, the fix was surprisingly straightforward.&lt;/p&gt;

&lt;p&gt;We already checked whether a Cognito user existed for the email, but we were treating that as enough. Instead, we changed the logic to ask: &lt;em&gt;"Does the correct Cognito user for the current database record exist?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If the email existed in Cognito but belonged to an outdated username, we deleted that user and recreated it with the latest username from the database. Since Cognito usernames are immutable, recreating the user was necessary to bring the two systems back into alignment.&lt;/p&gt;

&lt;p&gt;The important change wasn't the delete-and-recreate itself. It was recognising that &lt;strong&gt;the database was the source of truth for the current username&lt;/strong&gt;, while Cognito could still be holding an older representation of the same person.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Weird Scenario Was the Important One
&lt;/h2&gt;

&lt;p&gt;The happy path already worked, so the interesting tests were the ones where state changed between steps.&lt;/p&gt;

&lt;p&gt;We tested the normal signup flows, partner and dependant creation, existing Cognito users, missing Cognito users and, most importantly, the scenario that exposed the bug: a partner or dependant was approved, changed their name before &lt;code&gt;SendingEmailsAfterApprovalBot&lt;/code&gt; ran, and then the background process tried to create their Cognito user using the latest database state.&lt;/p&gt;

&lt;p&gt;We then verified the part that actually mattered: could they receive their PIN, confirm their account, set their password and successfully sign in?&lt;/p&gt;

&lt;p&gt;Because nobody cares about our beautifully synchronised identity records. They just want to get into their account.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Interesting Part Wasn't Actually Cognito
&lt;/h2&gt;

&lt;p&gt;Looking back, this wasn't really a Cognito bug. The database wasn't broken, and neither was the scheduled task. Each component was behaving according to its own rules.&lt;/p&gt;

&lt;p&gt;The problem was the assumption connecting them: that the identity we started processing would still be the identity we needed when the asynchronous process eventually ran.&lt;/p&gt;

&lt;p&gt;Then someone changed their name.&lt;/p&gt;

&lt;p&gt;That small change was enough to make the database username different from the immutable username already held by Cognito. Since the email was already taken, Cognito rejected the new user creation and the two systems drifted apart.&lt;/p&gt;

&lt;p&gt;That's the lesson I took from it: whenever identity exists in multiple systems, you need to be explicit about &lt;strong&gt;which system is the source of truth&lt;/strong&gt; and what happens when those representations disagree.&lt;/p&gt;

&lt;p&gt;Because eventually, they will.&lt;/p&gt;

&lt;p&gt;Someone will change their name. A background job will run later. A legacy system will already have a record. And Cognito will quite reasonably tell you that it has seen that email before.&lt;/p&gt;

&lt;h2&gt;
  
  
  And That's What Made This Bug So Sneaky
&lt;/h2&gt;

&lt;p&gt;The approval worked, the task ran, the notification was sent, Cognito had a user and the user followed the instructions. Yet somewhere between those perfectly reasonable steps, the identity had drifted.&lt;/p&gt;

&lt;p&gt;From the user's perspective, none of the underlying details mattered. They didn't know there were two User Pools, that the old pool handled mobile signup while the newer one sent the email PIN, or that a background task ran every 15 minutes.&lt;/p&gt;

&lt;p&gt;They simply knew they had done everything they were asked to do and still couldn't get into their account.&lt;/p&gt;

&lt;p&gt;Which brings me back to the opening question.&lt;/p&gt;

&lt;p&gt;What's worse than not being able to log in?&lt;/p&gt;

&lt;p&gt;Being told everything worked right up until you try to actually use your account.&lt;/p&gt;

&lt;p&gt;At least a failed login is honest. This bug was more like:&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Congratulations! Everything worked!"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;"Really? Because I'm still locked out."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;And that's probably the biggest lesson I took away from it: when debugging authentication, don't just look at the login screen. Follow the identity through the entire system, use your observability tools to reconstruct what actually happened, and find the moment when the different pieces stopped agreeing about who that user was.&lt;/p&gt;

&lt;p&gt;Because sometimes the authentication bug isn't hiding at login.&lt;/p&gt;

&lt;p&gt;Sometimes it's hiding in a background job, quietly waiting for someone to change their name.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Auth strikes again. 🔐🐛&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>bugsmash</category>
      <category>distributedsystems</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
