<?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: Pranav Kishan</title>
    <description>The latest articles on DEV Community by Pranav Kishan (@pranav_kishan_).</description>
    <link>https://dev.to/pranav_kishan_</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%2F3669047%2Fe14eda30-1493-460f-bccf-2742af9d8310.jpg</url>
      <title>DEV Community: Pranav Kishan</title>
      <link>https://dev.to/pranav_kishan_</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pranav_kishan_"/>
    <language>en</language>
    <item>
      <title>When AI Gets the Tickets Right but the Totals Wrong</title>
      <dc:creator>Pranav Kishan</dc:creator>
      <pubDate>Sun, 11 Oct 2026 10:45:22 +0000</pubDate>
      <link>https://dev.to/pranav_kishan_/when-ai-gets-the-tickets-right-but-the-totals-wrong-2o71</link>
      <guid>https://dev.to/pranav_kishan_/when-ai-gets-the-tickets-right-but-the-totals-wrong-2o71</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;p&gt;An AI model can classify hundreds of support tickets, organize them into categories, and produce a summary. But do the numbers in that summary actually agree with the records it just listed?&lt;/p&gt;

&lt;p&gt;That question led me to build &lt;strong&gt;Cross-View Conservation&lt;/strong&gt;, a benchmark that tests whether a model can keep two representations of the same answer consistent as the workload grows.&lt;/p&gt;

&lt;p&gt;The most interesting finding was that getting individual items right did not guarantee that the final summary added up.&lt;/p&gt;

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

&lt;p&gt;Cross-View Conservation evaluates deterministically generated synthetic support tickets at three workload sizes: &lt;strong&gt;100, 400, and 800 tickets&lt;/strong&gt;. I use three fixed data seeds at each size, giving nine cases per model.&lt;/p&gt;

&lt;p&gt;Each ticket contains fields such as its region, customer tier, service, severity, and status. The model receives explicit classification rules, applied in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;If &lt;code&gt;status=closed&lt;/code&gt;, assign the ticket to &lt;code&gt;CLOSED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Otherwise, assign it to &lt;code&gt;ESCALATE&lt;/code&gt; if the customer tier is enterprise and severity is at least 3, or if the service is security and severity is at least 4.&lt;/li&gt;
&lt;li&gt;Otherwise, assign it to &lt;code&gt;NORMAL&lt;/code&gt; if the customer tier is business and severity is at least 2, or the region is EU and severity is at least 2.&lt;/li&gt;
&lt;li&gt;All remaining tickets go to &lt;code&gt;QUEUE&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For example, this synthetic ticket belongs to &lt;code&gt;CLOSED&lt;/code&gt; because the first rule takes priority:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TCK-0001 | region=APAC | tier=enterprise | service=search | severity=5 | status=closed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each model must return two views of the same classification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;View A: Detailed partitions.&lt;/strong&gt; List ticket IDs under &lt;code&gt;ESCALATE&lt;/code&gt;, &lt;code&gt;NORMAL&lt;/code&gt;, &lt;code&gt;QUEUE&lt;/code&gt;, and &lt;code&gt;CLOSED&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;View B: Aggregate summary.&lt;/strong&gt; Report each category's ticket count and total severity, together with the overall ticket count.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The model does not receive the hidden reference labels. It must classify the records and generate its own summary.&lt;/p&gt;

&lt;p&gt;The severity totals require adding the severity values of tickets in each category. At 800 tickets, that means potentially hundreds of additions. The prompt provides no calculator, code execution, or external tools. The benchmark therefore tests rule application, ID tracking, long addition, and consistency between two representations of an answer.&lt;/p&gt;

&lt;p&gt;My primary metric is &lt;strong&gt;exact self-consistency&lt;/strong&gt;. The evaluator recomputes counts and severity sums from the model's own lists and compares them with the reported summary. A mismatch of even one unit in a required total fails the case.&lt;/p&gt;

&lt;p&gt;Self-consistency is not the same as correctness. A summary can agree with an incorrectly classified list. That is why I also track item coverage, duplicate assignments, classification accuracy against a deterministic reference, count agreement, and severity-total agreement.&lt;/p&gt;

&lt;p&gt;I wanted to separate these failure modes instead of hiding them inside one accuracy score.&lt;/p&gt;

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

&lt;p&gt;I chose six models across Anthropic, OpenAI, and Google to compare behavior across providers and different model offerings. The lineup includes two Gemini Flash versions, making their results particularly interesting to compare under the same task design.&lt;/p&gt;

&lt;p&gt;The current Kaggle leaderboard shows these results:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Cases passed&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Opus 5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;9/9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;100.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Claude Haiku 5.5&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;7/9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;77.8%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-6 Luna&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;55.6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.8 Flash&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4/9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;44.4%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini 3.7 Flash&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;3/9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-5.4 mini&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0/9&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each case contributes one ninth of the score. The task code averages all nine cases and assigns zero to caught execution failures. Under that scoring logic, Claude Opus 5's displayed score of &lt;code&gt;1.00&lt;/code&gt; corresponds to &lt;strong&gt;9/9&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;There is an important distinction between official results and my earlier validation. In that separate validation run, five Claude Opus 5 cases completed and passed, while four calls were blocked by provider quota. Those validation results are not the official Kaggle score.&lt;/p&gt;

&lt;h3&gt;
  
  
  Evaluation settings
&lt;/h3&gt;

&lt;p&gt;Every case ran in a fresh conversation using &lt;code&gt;llm.prompt(prompt)&lt;/code&gt;. I did not supply external tools, set an explicit reasoning override, specify a custom output-token cap, or set a model-generation seed. Provider and model defaults applied. The data seeds were fixed.&lt;/p&gt;

&lt;p&gt;This is an end-to-end comparison under the available default settings, not a controlled comparison of reasoning effort or token efficiency. Models can spend different numbers of output tokens.&lt;/p&gt;

&lt;p&gt;The current leaderboard displays &lt;code&gt;GPT-6 Luna&lt;/code&gt;, while an earlier validation log identifies &lt;code&gt;openai/gpt-5.6-luna&lt;/code&gt;. I preserve the logged identifier when discussing that validation finding rather than assume the two labels are interchangeable.&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%2F0dgoz88d9qxnvnc3crtm.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%2F0dgoz88d9qxnvnc3crtm.png" alt="Self-consistency chart" width="800" height="428"&gt;&lt;/a&gt;&lt;/p&gt;

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

&lt;h3&gt;
  
  
  1. Correct classifications do not guarantee correct aggregation
&lt;/h3&gt;

&lt;p&gt;In one &lt;strong&gt;100-ticket validation case&lt;/strong&gt;, the model logged as &lt;code&gt;openai/gpt-5.6-luna&lt;/code&gt; classified all 100 tickets correctly, yet its reported &lt;code&gt;QUEUE&lt;/code&gt; severity total was off by &lt;strong&gt;24&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The individual classifications were correct in that case, but the aggregate summary still contradicted the records.&lt;/p&gt;

&lt;p&gt;A classification-only benchmark could mark the labels correct and miss this inconsistency entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Matching counts can still hide incorrect severity totals
&lt;/h3&gt;

&lt;p&gt;Gemini 3.7 Flash produced another revealing pattern in validation.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;400 tickets&lt;/strong&gt;, it classified all 400 tickets correctly in all three seeds, and the category counts matched. Yet its severity totals were wrong in every case, with absolute discrepancies ranging from &lt;strong&gt;1 to 19&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;These are not necessarily large numerical errors. But exact-match evaluation is intentionally strict: even a small discrepancy means the summary no longer agrees precisely with the detailed result.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;800 tickets&lt;/strong&gt;, category counts drifted as well. In the observed validation cases, the &lt;code&gt;QUEUE&lt;/code&gt; count was off by between &lt;strong&gt;4 and 8&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The saved October 10 official Kaggle run is consistent with this pattern: all three 800-ticket Gemini 3.7 Flash cases failed the exact aggregate-consistency checks.&lt;/p&gt;

&lt;p&gt;Three cases at each workload size are not enough to establish a universal scaling law. More repeated evaluations are needed to determine how stable the pattern is.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Duplicate assignments can invalidate the partition itself
&lt;/h3&gt;

&lt;p&gt;In one &lt;strong&gt;800-ticket validation run&lt;/strong&gt;, GPT-5.4 mini listed &lt;strong&gt;710 of the 800 ticket IDs more than once&lt;/strong&gt;, producing &lt;strong&gt;1,857 ID listings for 800 tickets&lt;/strong&gt;. The most frequently repeated ID appeared four times.&lt;/p&gt;

&lt;p&gt;Its output was not a partition: a valid partition requires each input ticket to appear exactly once across the four categories.&lt;/p&gt;

&lt;p&gt;This is different from miscalculating a summary over otherwise valid lists. It also shows why duplicate and coverage diagnostics matter alongside the headline score.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A refusal is different from a wrong answer
&lt;/h3&gt;

&lt;p&gt;In two larger-workload validation cases, the model logged as &lt;code&gt;openai/gpt-5.6-luna&lt;/code&gt; declined to produce the requested classification and summary. One response stated:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I'm unable to reliably produce the exact classification and summaries for all 800 tickets without risking omissions or misclassification.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A refusal is different from confidently returning incorrect totals. Both affect whether the task was completed, but they reveal different behaviors. That is why I examine individual outputs and execution coverage rather than treating every zero score as the same kind of failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. A related benchmark asks a different question
&lt;/h3&gt;

&lt;p&gt;The closest related work I found is &lt;a href="https://dev.to/gde/count-it-or-compute-it-when-a-tool-returns-rows-the-models-that-count-them-right-spend-the-tokens-2hae"&gt;&lt;em&gt;Count It or Compute It: When a Tool Returns Rows, the Models That Count Them Right Spend the Tokens&lt;/em&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;That benchmark tests whether models count rows returned by a tool, comparing approaches that return matching IDs with approaches that provide the count directly. Its reported findings discuss a relationship between output-token use and counting accuracy in that particular setup.&lt;/p&gt;

&lt;p&gt;Cross-View Conservation asks a different question. It tests whether a model's &lt;strong&gt;own ticket partition and its summary of that partition agree&lt;/strong&gt; as the batch grows. The model generates both views instead of receiving a list from an external tool and being asked to count it.&lt;/p&gt;

&lt;p&gt;The two benchmarks are complementary. One studies counting tool-returned rows; the other studies consistency between two representations of a model-generated answer. My results do not establish what happened inside a model's hidden reasoning, nor do they show that spending more tokens necessarily fixes the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Would Measure Next
&lt;/h2&gt;

&lt;p&gt;This is an initial benchmark, not a definitive ranking of model quality. Nine cases per model, variable outputs, provider quotas, and differences in provider defaults all limit what can be concluded.&lt;/p&gt;

&lt;p&gt;I would prioritize three follow-up experiments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Increase repetitions.&lt;/strong&gt; Use more seeds and report completed, failed, and blocked cases separately, with an explicit denominator for every model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vary workload difficulty independently of size.&lt;/strong&gt; Change category balance, ticket ambiguity, and severity distributions separately to identify conditions associated with failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare generation strategies.&lt;/strong&gt; Test whether asking a model to produce the detailed lists first and calculate the summary in a separate step improves consistency compared with producing both views together.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I would also report output-token usage alongside the failure diagnostics. That could help characterize the relationship between output length, consistency, cost, and workload size without assuming that more tokens necessarily mean better reasoning.&lt;/p&gt;

&lt;p&gt;The useful next step is not simply to add more models. It is to understand which failure modes recur, under which conditions, and whether a different generation strategy reduces them.&lt;/p&gt;

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

&lt;p&gt;Explore the task, inspect the results, and compare the models on Kaggle:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.kaggle.com/benchmarks/pranavkishan/testings/versions/1" rel="noopener noreferrer"&gt;Cross-View Conservation: Do AI Models' Answers Still Add Up at Scale?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The idea is simple: an answer should not merely look plausible. When it contains detailed records and a summary of those records, the two views should agree.&lt;/p&gt;

&lt;p&gt;Cross-View Conservation turns that expectation into a measurable test and investigates what happens as the workload grows.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>kagglechallenge</category>
      <category>ai</category>
      <category>machinelearning</category>
    </item>
    <item>
      <title>The Judge That Never Guesses</title>
      <dc:creator>Pranav Kishan</dc:creator>
      <pubDate>Sun, 20 Sep 2026 18:05:12 +0000</pubDate>
      <link>https://dev.to/pranav_kishan_/the-judge-that-never-guesses-43l9</link>
      <guid>https://dev.to/pranav_kishan_/the-judge-that-never-guesses-43l9</guid>
      <description>&lt;h1&gt;
  
  
  The Judge That Never Guesses
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;How an AI pipeline that fixes its own bugs still refuses to trust its own opinion.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every autonomous system eventually faces the same temptation: let the thing that did the work also decide whether the work was any good. It's efficient. It's tidy. It's also how you end up with a pipeline that reports success on a run that quietly made everything worse. Klyro was built to resist that temptation on purpose, and the reasoning behind it is worth walking through in full.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't Let the Model Grade Its Own Homework
&lt;/h2&gt;

&lt;p&gt;There's an obvious shortcut Klyro doesn't take: asking the same LLM that proposed a fix whether the fix actually worked. It would take one extra prompt to wire up, and it would be easy to trust right up until the first time it's wrong in a way nobody catches. Language models are persuasive by design. Ask one to defend its own patch and it will find a reason the numbers look fine, even when they don't.&lt;/p&gt;

&lt;p&gt;So the verdict on every run comes from deterministic code, never a model call. No prompt, no interpretation, no room for a confident-sounding rationalization. Just a fixed set of numeric thresholds applied to numbers a load test actually produced. The Investigator gets to be creative, proposing whatever fix it thinks will help. The Evaluator does not get that luxury, and that asymmetry is the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trap of Self-Grading
&lt;/h2&gt;

&lt;p&gt;It's worth being specific about why this matters. An LLM asked to evaluate its own output isn't lying, exactly. It's doing what it was trained to do: produce a plausible, coherent answer to the question it was asked. "Did this help?" is a question with a plausible-sounding "yes" available almost regardless of the data, especially once the model has already committed, in an earlier turn, to believing the fix was a good idea.&lt;/p&gt;

&lt;p&gt;That failure mode doesn't show up in a demo. It shows up three weeks later, when someone finally cross-checks a "validated" run against the raw metrics and finds the p95 latency barely moved. By then the bad verdict has already shaped a decision. Klyro closes that gap by never letting the question reach a model in the first place.&lt;/p&gt;

&lt;h2&gt;
  
  
  Changed Is Not the Same as Validated
&lt;/h2&gt;

&lt;p&gt;The Evaluator draws a specific, unforgiving line between two things that sound similar and aren't. Performance can change after a patch without that change meaning anything worth celebrating. A fix might shave milliseconds off one endpoint while quietly pushing error rates up somewhere else, and "changed" would technically be true.&lt;/p&gt;

&lt;p&gt;"Validated" is a stricter claim, and Klyro only makes it when three conditions hold at once:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Signal&lt;/th&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;p95 latency&lt;/td&gt;
&lt;td&gt;Improves by at least 10%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Error rate&lt;/td&gt;
&lt;td&gt;Moves by no more than 0.5 percentage points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU utilization&lt;/td&gt;
&lt;td&gt;Stays at or under 95%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Miss any one of those and the run reports a change, not a validated optimization. That distinction, one word doing a lot of quiet work, is where a system that's honest about what an LLM's fix actually accomplished lives. It's the difference between "something happened" and "something worth trusting happened."&lt;/p&gt;

&lt;h2&gt;
  
  
  Same Workload, Same Data, Every Time
&lt;/h2&gt;

&lt;p&gt;None of those thresholds mean anything if the before and after runs aren't genuinely comparable. A p95 improvement measured against a lighter workload or a warmer cache isn't evidence of anything except that the test changed, and a system that lets its comparison drift is a system quietly grading on a curve without admitting it.&lt;/p&gt;

&lt;p&gt;Klyro treats that risk the way a controlled experiment would. Task CPU, memory, and replica count stay identical across both runs. The exact same k6 workload hits both. The database gets the same discipline: db-init runs a full DROP and CREATE SCHEMA followed by a fresh seed before every single run, never an INSERT-only reset that could leave leftover rows from a previous attempt quietly nudging the numbers one way or another. If the "after" run looks better, it has to be because the patch made it better. There's no easier lane for it to have found instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Patch Has to Prove It Is What It Claims to Be
&lt;/h2&gt;

&lt;p&gt;The trust story doesn't stop at measurement. It starts earlier, at the moment a patch is proposed at all. Before any patch from the Investigator gets anywhere near a rebuild, it has to carry the &lt;code&gt;original_sha256&lt;/code&gt; of the file it targets, checked against that file exactly as it exists right now. A mismatch means the patch doesn't apply, full stop, no partial credit and no best-effort merge.&lt;/p&gt;

&lt;p&gt;That check is paired with something even simpler: a three-file allowlist the Investigator is boxed into, covering only the files a performance fix should ever need to touch. Together, those two guardrails mean the thing the Evaluator eventually judges is provably the exact change that was proposed, applied to the exact file it claimed to target. Nothing smuggled in. Nothing silently dropped. By the time a number reaches the Evaluator, its provenance is no longer a question anyone has to take on faith.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Guardrails Compound
&lt;/h2&gt;

&lt;p&gt;Looked at individually, the sha256 check, the file allowlist, and the deterministic Evaluator each solve a narrow problem. Looked at together, they form a chain of custody that runs from the moment a fix is imagined to the moment a verdict is printed. A patch can't drift from what it claims to be. A verdict can't be shaped by the same reasoning that produced the patch. Each link only has to hold its own weight, which is exactly what makes the whole chain trustworthy instead of merely convincing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Different Kinds of Trust
&lt;/h2&gt;

&lt;p&gt;Trusting an LLM to notice a slow database query and propose a fix is one kind of trust. It's a bet on creativity and pattern recognition, and it's fine to lose that bet occasionally, because a bad patch just fails to validate. Trusting a system to tell you, honestly, whether that fix actually helped is a completely different kind of trust, and conflating the two is how systems end up reporting success that doesn't survive a second look.&lt;/p&gt;

&lt;p&gt;Klyro keeps those jobs in separate hands on purpose. An LLM proposes. A guard verifies the proposal is what it claims to be. Plain deterministic code renders the verdict. None of the three steps has to trust the others' judgment, only their outputs, and that's precisely why the whole pipeline can be trusted at all.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Designing a Zero-Knowledge Persistent Personal Information Manager with Client-Side Encryption</title>
      <dc:creator>Pranav Kishan</dc:creator>
      <pubDate>Sat, 20 Dec 2025 16:25:10 +0000</pubDate>
      <link>https://dev.to/pranav_kishan_/designing-a-zero-trust-personal-information-manager-with-client-side-encryption-4pb5</link>
      <guid>https://dev.to/pranav_kishan_/designing-a-zero-trust-personal-information-manager-with-client-side-encryption-4pb5</guid>
      <description>&lt;p&gt;I am a B.Tech Computer Science undergraduate at &lt;strong&gt;Amrita Vishwa Vidyapeetham&lt;/strong&gt; who enjoys building privacy-focused systems and learning by deploying real software end-to-end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Motivation
&lt;/h2&gt;

&lt;p&gt;The inception of &lt;strong&gt;InfoStuffs&lt;/strong&gt; was not driven by a desire to build just another productivity application. It started with a very specific user requirement from my sister. She needed a digital space to organize personal documents and sensitive notes but refused to use standard cloud services such as Google Keep or Notion.&lt;/p&gt;

&lt;p&gt;Her constraint was simple but technically demanding: &lt;strong&gt;she wanted the convenience of the cloud without trusting the cloud provider with her plaintext data.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This challenge became the foundation of InfoStuffs. My goal shifted from building a simple web application to architecting a &lt;strong&gt;Zero-Knowledge Information Management System&lt;/strong&gt; that prioritizes privacy by default.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem Statement
&lt;/h2&gt;

&lt;p&gt;Modern productivity tools generally fall into two categories:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Convenient SaaS:&lt;/strong&gt; (e.g., Notion, Keep) store user data in plaintext or use server-managed keys, leaving data vulnerable to internal leaks or database breaches.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-Hosted:&lt;/strong&gt; (e.g., Obsidian, Nextcloud) offer strong privacy but are difficult to access and maintain across multiple devices.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;InfoStuffs bridges this gap. The system had to be secure enough that a &lt;strong&gt;complete server-side compromise would yield nothing but garbage data&lt;/strong&gt;, yet accessible via a standard web browser on any device.&lt;/p&gt;

&lt;h2&gt;
  
  
  High-Level Architecture
&lt;/h2&gt;

&lt;p&gt;To satisfy these constraints, InfoStuffs uses a decoupled, cloud-native architecture with clearly separated responsibilities.&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%2F8eu2329chvc21mlh7o61.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%2F8eu2329chvc21mlh7o61.png" alt="Architecture diagram for infostuffs" width="800" height="871"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; React (Vite) with Material UI. Responsibilities include UI rendering and &lt;strong&gt;client-side cryptographic operations&lt;/strong&gt; (encryption/decryption).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend:&lt;/strong&gt; Node.js and Express, utilizing a &lt;strong&gt;Serverless Architecture&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local Development:&lt;/strong&gt; Runs as a &lt;strong&gt;fully dockerized monolithic container&lt;/strong&gt;, ensuring a consistent development environment that mirrors production dependencies.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Production Deployment:&lt;/strong&gt; Deployed to &lt;strong&gt;Vercel&lt;/strong&gt; as &lt;strong&gt;stateless Serverless Functions&lt;/strong&gt;, allowing the API to scale to zero when idle (cost-efficient) while maintaining a single Express codebase.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Database:&lt;/strong&gt; MongoDB Atlas for storing encrypted metadata and ciphertext.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Authentication:&lt;/strong&gt; Clerk. Delegating identity management reduced the attack surface for auth flows (MFA, session management).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Storage:&lt;/strong&gt; Supabase Storage, used strictly for isolating binary objects (images and PDFs) via obfuscated paths and client-side encrypted blobs.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Security by Design: The Zero-Knowledge Vault
&lt;/h2&gt;

&lt;p&gt;Security was not an optional feature; it was the primary architectural constraint.&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%2Fwcpmbhd9mxxeeiyn5n7r.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%2Fwcpmbhd9mxxeeiyn5n7r.png" alt="Sequence diagram flow" width="800" height="771"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The Problem with Static Keys
&lt;/h3&gt;

&lt;p&gt;In my initial design, I used a static encryption key stored in the server's environment variables (&lt;code&gt;VITE_SECRET_KEY&lt;/code&gt;). I quickly realized this was a critical flaw. If an attacker or a compromised hosting environment were to expose environment variables, they could decrypt everyone's data. The key was &lt;strong&gt;visible&lt;/strong&gt;, which violated the core concept of Zero-Knowledge.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Solution: User-Derived Cryptography
&lt;/h3&gt;

&lt;p&gt;To fix this, I removed the static key entirely. I implemented &lt;strong&gt;PBKDF2 (Password-Based Key Derivation Function 2)&lt;/strong&gt; on the client side.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;When a user logs in, they enter a &lt;strong&gt;Vault Password&lt;/strong&gt;. This password is never transmitted or stored and exists only transiently in the client’s memory.&lt;/li&gt;
&lt;li&gt;The browser runs PBKDF2 to derive a temporary 256-bit AES-GCM key in memory.&lt;/li&gt;
&lt;li&gt;This key is used to encrypt notes, titles, and file paths &lt;em&gt;before&lt;/em&gt; the network request is even formed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The server only ever sees (and stores) ciphertext. If the database administrator (me) were to look at the data, I would see nothing but unreadable strings.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Ephemeral Access to Media
&lt;/h3&gt;

&lt;p&gt;For file storage, I avoided public buckets entirely.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Encrypted Paths:&lt;/strong&gt; The database stores an &lt;em&gt;encrypted&lt;/em&gt; string pointing to the file path (e.g., "user/123/image.jpg" is encrypted).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Obfuscated Storage:&lt;/strong&gt; Uploaded files generate randomized UUID storage paths, so the storage provider never sees the original filenames.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On-Demand Access:&lt;/strong&gt; When a user unlocks their vault, the client decrypts the path and requests a &lt;strong&gt;Signed URL&lt;/strong&gt; from Supabase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-Limited:&lt;/strong&gt; Signed URLs are valid for one hour. This prevents "link sharing" leaks and ensures that even if a URL is intercepted, it becomes useless quickly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Zero-Knowledge Offline Vault (PWA)
&lt;/h3&gt;

&lt;p&gt;To make the app truly resilient, I engineered a complete offline mode without sacrificing the encryption model.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Offline Caching:&lt;/strong&gt; During an online session, raw encrypted ciphertexts of the user's notes are saved into IndexedDB; plaintext is never stored on disk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instant Offline Detection:&lt;/strong&gt; A module-level event interceptor and a fast connectivity probe immediately detect offline states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth Bypass &amp;amp; Key Derivation:&lt;/strong&gt; If offline, Clerk initialization is bypassed entirely. The app regenerates the AES-GCM key dynamically using the Vault Password and the cached, plaintext Clerk User ID as the PBKDF2 salt.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Infrastructure Evolution: Solving the Cost Problem
&lt;/h2&gt;

&lt;p&gt;One of the most valuable learning experiences came from adapting the infrastructure to real-world cost constraints.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: The "Enterprise" Trap (GCP)
&lt;/h3&gt;

&lt;p&gt;My initial deployment used Google Cloud Platform with &lt;strong&gt;Cloud Run&lt;/strong&gt; and &lt;strong&gt;Cloud Build&lt;/strong&gt;. While this was an industry-standard "Enterprise" setup, it introduced significant problems for a personal project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High Costs:&lt;/strong&gt; Paying for load balancers, container registry storage, and compute time quickly added up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complexity:&lt;/strong&gt; Managing IAM roles and build triggers for a simple app was overkill.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Phase 2: The "Serverless Monolith" (Vercel)
&lt;/h3&gt;

&lt;p&gt;To solve the deployment cost problem, I re-architected the stack to run for &lt;strong&gt;$0/month&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Local Development (Dockerized):&lt;/strong&gt; I kept the convenience of a containerized environment. A single &lt;code&gt;docker-compose up&lt;/code&gt; spins up the Frontend, Backend, and Database services. This ensures that the development environment is isolated and reproducible on any machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production Deployment (Serverless):&lt;/strong&gt; Instead of paying for a permanently running container (which costs money even when idle), I refactored the Express application to run on &lt;strong&gt;Vercel Serverless Functions&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Result:&lt;/strong&gt; I effectively have a "Serverless Monolith." I develop it like a standard monolithic app (easy to debug, easy to run locally in Docker) but deploy it as distributed functions. This gives me the best of both worlds: &lt;strong&gt;Zero infrastructure management&lt;/strong&gt; and &lt;strong&gt;Zero cost&lt;/strong&gt; for personal use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I intentionally avoided microservices, as the domain does not yet justify multiple bounded contexts, and premature service decomposition would increase complexity and attack surface without tangible benefits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Technical Challenges &amp;amp; Performance Optimizations
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Environment Variable Visibility&lt;/strong&gt;&lt;br&gt;
Relying on &lt;code&gt;.env&lt;/code&gt; files for security was a mistake. The migration to user-derived keys solved this, but it required handling edge cases like "Lost Passwords." Since I no longer had the key, I had to implement a &lt;strong&gt;"Nuclear Reset"&lt;/strong&gt; feature. This allows users to wipe their unrecoverable data and start fresh, prioritizing security over recovery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Client-Side Decryption Bottlenecks&lt;/strong&gt;&lt;br&gt;
Rendering a vault full of encrypted images and text placed heavy load on the UI thread. I implemented two major optimizations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Intersection Observer Deferral:&lt;/strong&gt; Encrypted image attachments are only requested, downloaded, and decrypted when they scroll into the viewport.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High Performance Base64 Parser:&lt;/strong&gt; I replaced slow native &lt;code&gt;Uint8Array.from()&lt;/code&gt; callbacks with custom loops, significantly reducing CPU overhead during decryption.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;3. The Docker vs. Serverless Routing Mismatch&lt;/strong&gt;&lt;br&gt;
One of the most complex challenges was reconciling the difference between a running Docker container and Vercel's file-system routing. In my local Docker container, Express handled all routing internally. However, when deployed to Vercel, requests to sub-paths (like &lt;code&gt;/api/info/nuke&lt;/code&gt;) were hitting Vercel's 404 handler before reaching my Express app. I resolved this by creating a Vercel-compatible entry point (&lt;code&gt;api/info.js&lt;/code&gt;) and a rewrite rule that pipes all sub-route traffic directly into the Express instance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Production OAuth Strictness&lt;/strong&gt;&lt;br&gt;
A critical UI bug emerged in production: users completing Google Sign-In were dumped onto a blank white screen at &lt;code&gt;/sso-callback&lt;/code&gt;. My SPA lacked a dedicated route to handle strict server-side redirects enforced in production. I architected a dedicated &lt;strong&gt;"OAuth Landing Pad"&lt;/strong&gt; component that intercepts the OAuth token, displays a branded loading state, and seamlessly completes the handshake before forwarding the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  Future Roadmap
&lt;/h2&gt;

&lt;p&gt;While InfoStuffs is fully functional, I plan to explore:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Redis Caching:&lt;/strong&gt; To reduce database reads for frequently accessed (encrypted) metadata.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;React Native Mobile App:&lt;/strong&gt; Wrapping the existing logic to allow biometric vault unlocking (FaceID) instead of typing the password.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;InfoStuffs is more than a note-taking application. It is a practical exploration of &lt;strong&gt;Zero-Knowledge Engineering&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;By addressing the real-world problems of &lt;strong&gt;data visibility&lt;/strong&gt; and &lt;strong&gt;cloud costs&lt;/strong&gt;, I built a system where privacy is enforced by mathematics, not by policy. It satisfies a real user need while serving as a valuable learning experience in full-stack security and DevOps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository:&lt;/strong&gt; &lt;a href="https://github.com/Destroyer795/InfoStuffs" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Live Deployment:&lt;/strong&gt; &lt;a href="https://info-stuffs.vercel.app" rel="noopener noreferrer"&gt;Link&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Note: The live deployment requires authentication and a vault password. The core security properties are enforced client-side and are best understood via the architecture discussion above.&lt;/p&gt;
&lt;/blockquote&gt;

</description>
      <category>webdev</category>
      <category>cloud</category>
      <category>security</category>
      <category>systemdesign</category>
    </item>
  </channel>
</rss>
