<?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: Keshav Varshney</title>
    <description>The latest articles on DEV Community by Keshav Varshney (@ikeshavvarshney).</description>
    <link>https://dev.to/ikeshavvarshney</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%2F3840062%2F87c1b3dd-df21-473e-9f20-8b66c32ccc70.jpg</url>
      <title>DEV Community: Keshav Varshney</title>
      <link>https://dev.to/ikeshavvarshney</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ikeshavvarshney"/>
    <language>en</language>
    <item>
      <title>I built a hackathon judging platform, and the test data had no winner in it</title>
      <dc:creator>Keshav Varshney</dc:creator>
      <pubDate>Wed, 07 Oct 2026 18:13:58 +0000</pubDate>
      <link>https://dev.to/ikeshavvarshney/i-built-a-hackathon-judging-platform-and-the-test-data-had-no-winner-in-it-57jh</link>
      <guid>https://dev.to/ikeshavvarshney/i-built-a-hackathon-judging-platform-and-the-test-data-had-no-winner-in-it-57jh</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built podium, a self-hostable hackathon submission and judging&lt;br&gt;
platform, in 72 hours: Express, Prisma and Postgres behind a Next.js client,&lt;br&gt;
one &lt;code&gt;docker compose up&lt;/code&gt;, 438 tests. The part of the brief that looked like a&lt;br&gt;
formality was "a documented way to even out judges who score harshly or&lt;br&gt;
generously". It turned out to be the whole project. This is the story of a&lt;br&gt;
metric that reached a perfect score by construction and proved nothing, a&lt;br&gt;
judge with two ballots who could move a project 23 places, a test data set&lt;br&gt;
with no signal in it, and a feature I shipped on day one and deleted on day&lt;br&gt;
three.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/ikeshavvarshney/podium" rel="noopener noreferrer"&gt;https://github.com/ikeshavvarshney/podium&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo video: &lt;a href="https://www.youtube.com/watch?v=3qqdSy8PHTQ" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=3qqdSy8PHTQ&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Screens and walkthrough: &lt;a href="https://podium-demo.pages.dev/" rel="noopener noreferrer"&gt;https://podium-demo.pages.dev/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  The problem that looked easy
&lt;/h2&gt;

&lt;p&gt;Judges do not share a scale. One gives 4 out of 5 because nothing is ever a&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Another gives 5 because the demo worked. Average their raw scores and you
are not ranking projects, you are ranking which judges each project happened
to draw.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The textbook fix is one line. For each judge, subtract their mean and divide&lt;br&gt;
by their standard deviation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;z(ballot) = (total - mean_j) / sd_j
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A project's score is the mean of the z-scores it received. I wrote that on&lt;br&gt;
the first day, the tests passed, and the results screen showed raw rank next&lt;br&gt;
to normalized rank with little arrows between them. It looked finished.&lt;/p&gt;
&lt;h2&gt;
  
  
  The metric that scored 100% and proved nothing
&lt;/h2&gt;

&lt;p&gt;The organizers ship a shared data set, &lt;code&gt;fixtures.json&lt;/code&gt;: 40 projects, 30&lt;br&gt;
judges, 126 score rows. After my importer refused one duplicate submission&lt;br&gt;
and its four ballots, 122 ballots remained.&lt;/p&gt;

&lt;p&gt;I wanted a number for the README, so I split the variance in those ballots&lt;br&gt;
by which judge cast them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Variance explained by the judge&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw weighted total&lt;/td&gt;
&lt;td&gt;27.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-judge z-score&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;27% down to zero. That is a fantastic table, and it is worthless.&lt;br&gt;
Standardizing sets every judge's mean to zero by definition. The judge&lt;br&gt;
effect did not go to zero because the method is good. It went to zero&lt;br&gt;
because that is what subtraction does. Any method that subtracts the judge&lt;br&gt;
mean scores 0.0% here, including a bad one.&lt;/p&gt;

&lt;p&gt;The number I had was measuring my arithmetic, not my ranking. The real&lt;br&gt;
question is whether the normalized order is closer to the truth, and for&lt;br&gt;
that you need a truth.&lt;/p&gt;
&lt;h2&gt;
  
  
  The data set with no winner in it
&lt;/h2&gt;

&lt;p&gt;So I looked at the other column: how much of the variance is explained by&lt;br&gt;
which project a ballot was for.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Explained by judge&lt;/th&gt;
&lt;th&gt;Explained by project&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw weighted total&lt;/td&gt;
&lt;td&gt;27.0%&lt;/td&gt;
&lt;td&gt;30.9%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-judge z-score, shrunk&lt;/td&gt;
&lt;td&gt;3.5%&lt;/td&gt;
&lt;td&gt;33.0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-judge z-score, unshrunk&lt;/td&gt;
&lt;td&gt;0.0%&lt;/td&gt;
&lt;td&gt;33.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;About a third looks like signal until you ask what chance alone gives. With&lt;br&gt;
40 groups over 122 ballots, pure noise explains 32.2%. The fixture sits&lt;br&gt;
right on it.&lt;/p&gt;

&lt;p&gt;I checked a second way. Split each project's ballots at random into two&lt;br&gt;
halves, rank the projects by each half, and correlate the two rankings. If&lt;br&gt;
the scores carry any information about the projects, the halves agree. Over&lt;br&gt;
400 random splits:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;raw totals        -0.005
normalized         0.023
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Zero. The fixture's scores carry judge effects and almost no project effect.&lt;br&gt;
Which is fair: it is test data for checking that a portal works, not a&lt;br&gt;
recorded hackathon. But it meant the benchmark I had planned to prove my&lt;br&gt;
method on could not tell a good method from a random one. No method can&lt;br&gt;
recover an order that is not there.&lt;/p&gt;

&lt;p&gt;This was the benchmark that disappointed me, and I am glad it did before I&lt;br&gt;
published a table built on it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building a truth to measure against
&lt;/h2&gt;

&lt;p&gt;The fix was a seeded simulation where every project has a known latent&lt;br&gt;
quality. 40 projects, 30 judges, 3 reviews each, 500 simulated events. Judges&lt;br&gt;
differ in severity (mean 45 to 85) and in spread (4 to 16 points). One judge&lt;br&gt;
in ten gives every project the same score. Then measure Spearman correlation&lt;br&gt;
between each method's ranking and the true order:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Mean Spearman with the true order&lt;/th&gt;
&lt;th&gt;Beats raw in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Raw mean&lt;/td&gt;
&lt;td&gt;0.749&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rank average&lt;/td&gt;
&lt;td&gt;0.838&lt;/td&gt;
&lt;td&gt;86% of events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-judge z-score, unshrunk&lt;/td&gt;
&lt;td&gt;0.865&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per-judge z-score, shrunk&lt;/td&gt;
&lt;td&gt;0.889&lt;/td&gt;
&lt;td&gt;100% of events&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is evidence. It runs with &lt;code&gt;npm run proof&lt;/code&gt;, it is seeded so it&lt;br&gt;
reproduces exactly, and a unit test asserts the numbers so the document&lt;br&gt;
cannot drift from the code.&lt;/p&gt;

&lt;p&gt;The last two rows are where the maths fought back.&lt;/p&gt;
&lt;h2&gt;
  
  
  The judge with two ballots
&lt;/h2&gt;

&lt;p&gt;Look at what plain z-scoring does to a judge who only scored a few projects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One ballot.&lt;/strong&gt; Their mean is that ballot. &lt;code&gt;z = 0&lt;/code&gt;, whatever they gave. A
5 and a 1 are the same vote.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two ballots.&lt;/strong&gt; Their mean is the midpoint and their standard deviation
is half the gap. The z-scores are always exactly &lt;code&gt;+1&lt;/code&gt; and &lt;code&gt;-1&lt;/code&gt;. A judge
who gave 80 and 79 has put the two projects two standard deviations
apart.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every ballot identical.&lt;/strong&gt; Standard deviation zero. Division by zero.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fixture has all three. 9 of its 30 judges cast fewer than three ballots,&lt;br&gt;
and &lt;code&gt;jdg_07&lt;/code&gt; gave three projects exactly 75.&lt;/p&gt;

&lt;p&gt;On the fixture, the unshrunk method moved one project, Dry Harbour, from&lt;br&gt;
31st to 8th. Two of its ballots came from judges with fewer than three&lt;br&gt;
ballots. It had not been scored well. It had been scored by people whose&lt;br&gt;
scale was manufactured from one or two data points.&lt;/p&gt;

&lt;p&gt;The fix is the standard empirical-Bayes one: do not trust a thin judge's own&lt;br&gt;
statistics. Pull them toward the panel's.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mean_j* = (n_j * mean_j + 1 * m) / (n_j + 1)        m = mean of all ballots
sd_j*   = sqrt((n_j * var_j + 3 * v) / (n_j + 3))   v = pooled within-judge variance

z = (total - mean_j*) / sd_j*      and 0 for a flat judge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A judge with twenty ballots is read almost entirely on their own scale. A&lt;br&gt;
judge with one is read mostly on the panel's. Dry Harbour now moves from&lt;br&gt;
31st to 17th instead of 8th, and in the simulation agreement with the true&lt;br&gt;
order goes from 0.865 to 0.889.&lt;/p&gt;

&lt;p&gt;Three things I would say about this before a reviewer does:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The priors were tuned on the simulation.&lt;/strong&gt; One ballot for the mean,
three for the variance (variances are noisier). I chose them where the
benchmark was best, so the 0.889 is an in-sample number.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The flat judge gets zero, not a penalty.&lt;/strong&gt; Someone who scores
everything 75 has told you nothing about order. Counting them as zero is
the honest reading. They are also named on the results screen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shrinkage keeps 3.5% of the judge effect on purpose.&lt;/strong&gt; With two
ballots, a judge's mean is as much their projects as their severity.
Removing all of it is the bug, not the goal.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Pairwise mode, and a modest win
&lt;/h2&gt;

&lt;p&gt;The second judging mode asks judges to order small groups instead of scoring&lt;br&gt;
a rubric. Borda count is the obvious tally. I also fitted a Bradley-Terry&lt;br&gt;
model with the MM algorithm, which weighs a win by the strength of the&lt;br&gt;
project beaten, so a project that drew an easy group is not rewarded for the&lt;br&gt;
draw.&lt;/p&gt;

&lt;p&gt;On 300 simulated events: Borda 0.827, Bradley-Terry 0.854, better in 82% of&lt;br&gt;
events. That is a real gain and a small one. I publish the Bradley-Terry&lt;br&gt;
ranking and show Borda beside it, because a ranking an organizer cannot&lt;br&gt;
sanity-check against a simpler one is a ranking they will not trust.&lt;/p&gt;
&lt;h2&gt;
  
  
  The debugging story: a function's name was the documentation
&lt;/h2&gt;

&lt;p&gt;The OpenAPI document is generated from the live Express routes. To know who&lt;br&gt;
may call a route, the generator walks the middleware stack and reads each&lt;br&gt;
function's name:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ROLE_BY_MIDDLEWARE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&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;code&gt;requireAuth&lt;/code&gt; means signed in, &lt;code&gt;requireEventAdmin&lt;/code&gt; means event admin. It&lt;br&gt;
worked for two days.&lt;/p&gt;

&lt;p&gt;On the last day I replaced the single admin check with area-scoped access:&lt;br&gt;
an admin can be limited to Announcements, or Judging, or any of ten areas.&lt;br&gt;
&lt;code&gt;requireEventAdmin&lt;/code&gt; became a factory:&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="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;requirePermission&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;areas&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;EventArea&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;assertPermission&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;eventContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;areas&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every route kept working. Every permission test passed. And 69 operations&lt;br&gt;
in the published API document silently lost their access label, because an&lt;br&gt;
arrow function returned from a factory has the name &lt;code&gt;""&lt;/code&gt;. The API still&lt;br&gt;
refused the requests. The documentation now said anyone could make them.&lt;/p&gt;

&lt;p&gt;One test caught it: the one that fetches &lt;code&gt;/api/openapi.json&lt;/code&gt; and checks that&lt;br&gt;
admin routes are labelled. The fix is to name the inner function and attach&lt;br&gt;
the areas to it, which the generator now publishes as &lt;code&gt;x-required-area&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;requirePermission&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;requirePermission&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;areas&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The lesson I took is narrower than "test your docs". Reading &lt;code&gt;fn.name&lt;/code&gt; is&lt;br&gt;
reflection, and a refactor that changes nothing about behaviour can change&lt;br&gt;
everything about what reflection sees. If documentation is derived from&lt;br&gt;
code, the derivation needs its own assertion.&lt;/p&gt;

&lt;p&gt;A second one from the same hour, shorter. My end-to-end check for "a limited&lt;br&gt;
admin is refused on settings" reported that the limit did not work. The API&lt;br&gt;
returned 200. The middleware was correct. The account I had picked as the&lt;br&gt;
limited admin was the instance operator, who&lt;br&gt;
bypasses area checks by design. The product was right and the test was&lt;br&gt;
wrong, and a 200 where I expected 403 looks identical either way.&lt;/p&gt;
&lt;h2&gt;
  
  
  The design I abandoned
&lt;/h2&gt;

&lt;p&gt;On day one I built &lt;strong&gt;judge track scope&lt;/strong&gt;: an organizer could restrict a&lt;br&gt;
judge to certain tracks, and mark a track as restricted. It had a column, a&lt;br&gt;
dialog, a rule inside the assignment engine and its own tests.&lt;/p&gt;

&lt;p&gt;On day three I dropped it. Migration 0026 is two lines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"event_memberships"&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="nv"&gt;"track_scope"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"tracks"&lt;/span&gt; &lt;span class="k"&gt;DROP&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="nv"&gt;"restricted"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reason is that the platform already had an answer to "which projects&lt;br&gt;
does this judge see": the assignment. A judge reads exactly the projects an&lt;br&gt;
organizer assigned and nothing else, and that boundary is enforced in the&lt;br&gt;
API and tested with &lt;code&gt;curl&lt;/code&gt;. Track scope was a second answer to the same&lt;br&gt;
question. Two sources of truth meant every path had to consult both, the&lt;br&gt;
auto-balancer had to explain why it left a project under-reviewed, and the&lt;br&gt;
organizer had a setting that could quietly contradict what they had just&lt;br&gt;
done by hand.&lt;/p&gt;

&lt;p&gt;The same clean-up hit "reviews per project". It started as a hard cap that&lt;br&gt;
blocked manual assignment past the number. It is now called "Judges per&lt;br&gt;
project" and is only the target the auto-balancer aims for. An organizer who&lt;br&gt;
wants a sixth opinion on a close call can simply assign one.&lt;/p&gt;

&lt;p&gt;Both removals made the judging model smaller: one function decides whether a&lt;br&gt;
judge may review a project, and it checks one thing, that it is not their&lt;br&gt;
own team's.&lt;/p&gt;
&lt;h2&gt;
  
  
  The schema I would redo
&lt;/h2&gt;

&lt;p&gt;Two decisions from the last day, both made because they were fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Admin areas are a &lt;code&gt;TEXT[]&lt;/code&gt; with a magic string.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="nv"&gt;"event_memberships"&lt;/span&gt; &lt;span class="k"&gt;ADD&lt;/span&gt; &lt;span class="k"&gt;COLUMN&lt;/span&gt; &lt;span class="nv"&gt;"permissions"&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[]::&lt;/span&gt;&lt;span class="nb"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;UPDATE&lt;/span&gt; &lt;span class="nv"&gt;"event_memberships"&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="nv"&gt;"permissions"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ARRAY&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'ALL'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="nv"&gt;"role"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'ADMIN'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The column sits on every membership row, including participants and judges,&lt;br&gt;
where it means nothing. Postgres cannot check that an entry is a real area,&lt;br&gt;
so validity lives in one Zod transform. And &lt;code&gt;'ALL'&lt;/code&gt; is a sentinel sharing a&lt;br&gt;
namespace with real values. The rest of the schema leans on the database for&lt;br&gt;
correctness: one ballot per judge per project is a unique index, the audit&lt;br&gt;
log is append-only by trigger. This column is the exception. It should be an&lt;br&gt;
enum array with a check that it is empty unless the role is ADMIN, or a&lt;br&gt;
small join table.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Flagging a project reuses the submission status.&lt;/strong&gt; An organizer can remove&lt;br&gt;
a project from public view with a reason. I stored that as&lt;br&gt;
&lt;code&gt;status = DISQUALIFIED&lt;/code&gt; plus three flag columns, which was attractive&lt;br&gt;
because every query that already filtered on &lt;code&gt;SUBMITTED&lt;/code&gt; excluded flagged&lt;br&gt;
projects for free: gallery, judge queues, ballots, results. The cost shows&lt;br&gt;
up on restore, which has to put the status back and can only assume it was&lt;br&gt;
&lt;code&gt;SUBMITTED&lt;/code&gt;. Visibility and lifecycle are two different facts and I stored&lt;br&gt;
them in one field.&lt;/p&gt;

&lt;p&gt;The decision I would keep is the one everything else hangs on: a role is not&lt;br&gt;
a property of an account, it is a row joining an account to an event. The&lt;br&gt;
same person is a judge in one event and a participant in another, and no&lt;br&gt;
request body ever gets to say who the caller is.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I did not build
&lt;/h2&gt;

&lt;p&gt;Stated here instead of left for someone to find.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Community voting counts accounts, not people.&lt;/strong&gt; Registration needs no
email verification, because the platform has to run with no mail server.
The threat model marks Sybil accounts as reduced, not stopped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collusion checks flag, they do not prove.&lt;/strong&gt; Lockstep judge pairs,
outlier ballots, flat judges and shared organizations are surfaced before
publishing. Two judges who vary their scores just enough stay under every
threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The acceptance checker verifies two of my four tiers.&lt;/strong&gt; It has no probes
for T3 or T4, so those claims rest on my own tests and a manual
walkthrough.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A failed seed does not fail the boot.&lt;/strong&gt; The entrypoint runs the seed
with &lt;code&gt;|| echo "seed skipped"&lt;/code&gt;. If the fixture import throws, the API still
starts and reports healthy, minus the event. I found this after
submitting, while reading my own entrypoint to answer a different
question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uploads live in Postgres.&lt;/strong&gt; Fine for a few hundred images and it keeps
the install to one database. Not fine for a large archive.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  By the numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build window&lt;/td&gt;
&lt;td&gt;72 hours, 140 commits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Server&lt;/td&gt;
&lt;td&gt;about 13,400 lines of TypeScript&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Client&lt;/td&gt;
&lt;td&gt;about 19,500 lines, Next.js 15 and React 19&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;438 across 38 files, about 5,400 lines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migrations&lt;/td&gt;
&lt;td&gt;26, applied from an empty database on boot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API&lt;/td&gt;
&lt;td&gt;159 operations on 121 paths, OpenAPI 3.1 generated from the routes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Acceptance checker&lt;/td&gt;
&lt;td&gt;7 of 7 probes passing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Clean install&lt;/td&gt;
&lt;td&gt;cloned from GitHub, healthy in 83 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Raw mean against the true order&lt;/td&gt;
&lt;td&gt;0.749&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shrunk z-score against the true order&lt;/td&gt;
&lt;td&gt;0.889&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  What the time actually went on
&lt;/h2&gt;

&lt;p&gt;The z-score took an afternoon. What took the rest was finding out that the&lt;br&gt;
first three ways I tried to show it worked could not have shown it failing.&lt;br&gt;
A variance table that reads zero for any method that subtracts a mean. A&lt;br&gt;
data set where no ranking is recoverable. A two-ballot judge who looks like&lt;br&gt;
a confident expert.&lt;/p&gt;

&lt;p&gt;The question that sorted them out was the same each time: could this number&lt;br&gt;
have come out badly? If the answer is no, it is not evidence, however good&lt;br&gt;
it looks in a README.&lt;/p&gt;

&lt;p&gt;The fixture had no winner in it. The useful thing the platform can do with&lt;br&gt;
data like that is say so.&lt;/p&gt;




&lt;p&gt;Built for &lt;a href="https://dogfoodhack.com" rel="noopener noreferrer"&gt;DOGFOOD 2026&lt;/a&gt;, the 72 hour hackathon run&lt;br&gt;
by Hackathon Raptors, where the brief is to build the platform a hackathon&lt;br&gt;
is judged on. The normalization proof, the judging methodology, the threat&lt;br&gt;
model and the full API document are in the repo.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/ikeshavvarshney/podium" rel="noopener noreferrer"&gt;https://github.com/ikeshavvarshney/podium&lt;/a&gt;&lt;br&gt;
Demo video: &lt;a href="https://www.youtube.com/watch?v=3qqdSy8PHTQ" rel="noopener noreferrer"&gt;https://www.youtube.com/watch?v=3qqdSy8PHTQ&lt;/a&gt;&lt;br&gt;
Screens and walkthrough: &lt;a href="https://podium-demo.pages.dev/" rel="noopener noreferrer"&gt;https://podium-demo.pages.dev/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>postgres</category>
    </item>
    <item>
      <title>My Go tests never executed a line of the JavaScript they were testing</title>
      <dc:creator>Keshav Varshney</dc:creator>
      <pubDate>Thu, 03 Sep 2026 14:40:42 +0000</pubDate>
      <link>https://dev.to/ikeshavvarshney/my-go-tests-never-executed-a-line-of-the-javascript-they-were-testing-4m0m</link>
      <guid>https://dev.to/ikeshavvarshney/my-go-tests-never-executed-a-line-of-the-javascript-they-were-testing-4m0m</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built a Real User Monitoring tool as one Go binary with an empty&lt;br&gt;
dependency manifest, zero entries in &lt;code&gt;go.mod&lt;/code&gt;. 245 test functions, &lt;code&gt;go vet&lt;/code&gt;&lt;br&gt;
clean, CI green on every push, a reproducible build verified on every commit.&lt;br&gt;
Six bugs survived all of it. This is the story of a build that stamped a git&lt;br&gt;
SHA into a binary I had called deterministic, an &lt;code&gt;int64&lt;/code&gt; that saturated into&lt;br&gt;
six-year chart buckets, a demo page that was genuinely slow and reported&lt;br&gt;
green, and a fix that was correct, deployed, and invisible for an hour.&lt;/p&gt;

&lt;p&gt;None of them were findable from Go. That is the actual subject of this post.&lt;/p&gt;


&lt;h2&gt;
  
  
  The premise is a joke that turned out to be the product
&lt;/h2&gt;

&lt;p&gt;RUM tells you how slow your site feels to real visitors. Every mainstream way&lt;br&gt;
of collecting it ships 30 to 60KB of third-party JavaScript from someone&lt;br&gt;
else's CDN, executing before your own code, on every page view. The tool that&lt;br&gt;
measures page weight is itself page weight.&lt;/p&gt;

&lt;p&gt;So the target was a beacon small enough that the size is the argument. It&lt;br&gt;
landed at &lt;strong&gt;942 bytes&lt;/strong&gt;, against 7,226 for Google's &lt;code&gt;web-vitals&lt;/code&gt;. Everything&lt;br&gt;
else followed from refusing to make that number worse: no framework, because&lt;br&gt;
a framework is a bundle; no chart library, because a chart library is 60KB to&lt;br&gt;
draw six lines; no database driver, because the thing being stored is five&lt;br&gt;
floats per page view.&lt;/p&gt;

&lt;p&gt;Writing that was the easy half. The question that decides whether any of it&lt;br&gt;
is real is: how do you know it works?&lt;/p&gt;


&lt;h2&gt;
  
  
  The gap: no Go test can execute JavaScript
&lt;/h2&gt;

&lt;p&gt;Testing a browser beacon from a Go suite means testing everything except the&lt;br&gt;
beacon. I could prove &lt;code&gt;/b.js&lt;/code&gt; was served with the right MIME type and ETag,&lt;br&gt;
that it stayed under its 1024-byte budget, that the ingest endpoint parsed a&lt;br&gt;
payload I hand-wrote and the API read it back. All of that passed, and none&lt;br&gt;
of it proves &lt;code&gt;PerformanceObserver&lt;/code&gt; fires, that &lt;code&gt;visibilitychange&lt;/code&gt; triggers&lt;br&gt;
the flush, or that CLS session-window arithmetic produces a number a browser&lt;br&gt;
would agree with.&lt;/p&gt;

&lt;p&gt;Go tests embed the file, serve it, assert on its bytes, and never execute a&lt;br&gt;
line of it. To a Go test, JavaScript is a byte slice.&lt;/p&gt;

&lt;p&gt;So I drove real Chrome over the DevTools Protocol. Node 22 ships a global&lt;br&gt;
&lt;code&gt;WebSocket&lt;/code&gt;, so the harness needed nothing installed:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;webSocketDebuggerUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;CDP&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/json/version`&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;webSocketDebuggerUrl&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&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;tests/*.mjs (external harness)     drives real Chrome over CDP
        │  Page.navigate, Runtime.evaluate, Input.dispatchMouseEvent
        ▼
vitals binary                      serves dashboard, demo pages, beacon.min.js
        │  beacon executes for real, PerformanceObserver fires for real
        ▼
/api/collect  →  /api/series, /api/events (EventSource)
        │
        ▼
45 scripted assertions against what the browser actually measured
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The harness lives outside the repository, because a browser automation&lt;br&gt;
dependency inside a zero-dependency project would be absurd.&lt;/p&gt;

&lt;p&gt;Two details from building it are worth passing on. Chrome 152 removed&lt;br&gt;
&lt;code&gt;Emulation.setPageVisibilityState&lt;/code&gt;, the usual way to fake a tab switch, so I&lt;br&gt;
replaced it with what a visitor actually does: open a second tab and activate&lt;br&gt;
it. And I was clicking with &lt;code&gt;element.click()&lt;/code&gt;, which made INP appear only&lt;br&gt;
intermittently, because synthetic clicks do not reliably produce Event Timing&lt;br&gt;
entries. Switching to &lt;code&gt;Input.dispatchMouseEvent&lt;/code&gt; made it deterministic and&lt;br&gt;
produced the single best piece of evidence in the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;metric inp collected by the browser  p75=600.00 ms (poor, n=1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;600.00ms against a handler deliberately blocking for exactly 600ms.&lt;/p&gt;




&lt;h2&gt;
  
  
  Six bugs, in increasing order of "that would have shipped"
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The reproducible build that wasn't
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;make repro&lt;/code&gt; builds twice and compares hashes. CI does the same on a clean&lt;br&gt;
runner and runs &lt;code&gt;cmp&lt;/code&gt;. Both passed from the day I wrote them. The README&lt;br&gt;
said the binary contains no build timestamp, git SHA, or injected version,&lt;br&gt;
which is what makes the output byte-identical. False. I caught it only&lt;br&gt;
because a hash changed between two commits that touched nothing but&lt;br&gt;
Markdown.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;go version &lt;span class="nt"&gt;-m&lt;/span&gt; vitals
&lt;span class="go"&gt;build   vcs.revision=6acd604c28b79a3db52850287e061b98c0018d7e
build   vcs.time=2026-08-30T10:56:20Z
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since Go 1.18 the toolchain stamps the git revision, a timestamp, and a&lt;br&gt;
module pseudo-version into any binary built inside a repository. On by&lt;br&gt;
default. The fix is one flag, &lt;code&gt;-buildvcs=false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The fix is not the interesting part. My test checked a weaker property than&lt;br&gt;
my claim. "Two builds at the same commit produce identical bytes" and "the&lt;br&gt;
binary contains no commit metadata" are different sentences. Only the first&lt;br&gt;
was under test. The second was what the README asserted, and a passing test&lt;br&gt;
sat next to it the whole time looking like evidence.&lt;/p&gt;

&lt;p&gt;I proved the fix the way I should have proved the claim: build, make an&lt;br&gt;
empty commit so the SHA changes, build again, compare. Most&lt;br&gt;
reproducible-build advice stops at &lt;code&gt;-trimpath&lt;/code&gt; and &lt;code&gt;-ldflags=-buildid=&lt;/code&gt;. The&lt;br&gt;
VCS stamp is newer, quieter, and it undermines you without saying anything.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. The int64 that saturated
&lt;/h3&gt;

&lt;p&gt;I loaded the dashboard and read the caption under the chart:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1 of 48 buckets have samples, 3202560 min per bucket
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Six years per bucket. On a chart labelled "last 24 hours".&lt;/p&gt;

&lt;p&gt;The dashboard calls &lt;code&gt;/api/series?from=24h&lt;/code&gt; and sends no &lt;code&gt;to&lt;/code&gt;, because "now"&lt;br&gt;
is the obvious default. An open range end was normalised to the year 9999.&lt;br&gt;
Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;To&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sub&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rng&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;From&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;span&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Buckets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;time.Duration&lt;/code&gt; is an &lt;code&gt;int64&lt;/code&gt; of nanoseconds, topping out near 292 years.&lt;br&gt;
Subtracting 1970 from 9999 does not error, does not panic, does not warn. It&lt;br&gt;
saturates. Divide by 48 and you get six-year buckets, rendered without&lt;br&gt;
complaint.&lt;/p&gt;

&lt;p&gt;No test caught it because I wrote the tests alongside the API, thinking&lt;br&gt;
about the API, so every one passed both &lt;code&gt;from&lt;/code&gt; and &lt;code&gt;to&lt;/code&gt; like a well-behaved&lt;br&gt;
caller. The dashboard, the only real client this API has, passes one. The&lt;br&gt;
bug lived exactly in the gap between how I documented the endpoint and how&lt;br&gt;
the only thing calling it actually calls it.&lt;/p&gt;

&lt;p&gt;The regression test does not assert a bucket width. It asserts that no&lt;br&gt;
combination of parameters can produce a span longer than a century, because&lt;br&gt;
a span that long means an open end leaked through and the arithmetic&lt;br&gt;
saturated.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. The demo that demonstrated nothing
&lt;/h3&gt;

&lt;p&gt;The project ships four deliberately broken demo pages so the dashboard shows&lt;br&gt;
something other than a wall of green. One is "heavy image": a hero of 2,600&lt;br&gt;
inline SVG shapes, meant to tank Largest Contentful Paint. It is genuinely&lt;br&gt;
expensive to render.&lt;/p&gt;

&lt;p&gt;In Chrome it measured &lt;strong&gt;220ms&lt;/strong&gt;. Good band. Green.&lt;/p&gt;

&lt;p&gt;An inline &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; element is not an LCP candidate. The spec is specific&lt;br&gt;
about what counts: &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt;, &lt;code&gt;&amp;lt;image&amp;gt;&lt;/code&gt; inside an SVG, video poster frames,&lt;br&gt;
background images, and block-level text. A root inline &lt;code&gt;&amp;lt;svg&amp;gt;&lt;/code&gt; is not on&lt;br&gt;
that list. The page burned real paint time while LCP quietly reported on a&lt;br&gt;
paragraph above it.&lt;/p&gt;

&lt;p&gt;This one stings because it is precisely the failure a performance dashboard&lt;br&gt;
exists to prevent: the page was slow, the metric said fast, both were true,&lt;br&gt;
and the gap was a spec detail I had not read carefully enough.&lt;/p&gt;

&lt;p&gt;Fixed with an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; and a data URI so it is actually a candidate, plus a&lt;br&gt;
deliberate main-thread block so it is slow on any hardware rather than only&lt;br&gt;
on a bad laptop. It reports 2852ms now. My first attempt used a 4200ms block&lt;br&gt;
to force the poor band, which broke differently: the page was still blocked&lt;br&gt;
when the tab hid, so the beacon flushed before any paint entry existed and&lt;br&gt;
sent a record containing only TTFB. Worse demo, worse data. The page now&lt;br&gt;
explains the trap in its own copy, because the trap is more interesting than&lt;br&gt;
the demo.&lt;/p&gt;
&lt;h3&gt;
  
  
  4. I measured my own tooling and called it a result
&lt;/h3&gt;

&lt;p&gt;The headline claim is a size comparison. That claim is the entire pitch, so&lt;br&gt;
it had better be clean. My first numbers were not: I measured our beacon&lt;br&gt;
with Go's gzip and theirs with Python's. Same algorithm, different&lt;br&gt;
implementations, a few percent apart. Nobody would ever have caught it. It&lt;br&gt;
would have been a property of my toolchain, presented as a property of my&lt;br&gt;
code, in the one number the project rests on.&lt;/p&gt;

&lt;p&gt;So I wrote a tool that runs every file through one compressor:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Raw&lt;/th&gt;
&lt;th&gt;Gzipped&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;vitals beacon&lt;/td&gt;
&lt;td&gt;942 B&lt;/td&gt;
&lt;td&gt;571 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vitals full beacon&lt;/td&gt;
&lt;td&gt;2,656 B&lt;/td&gt;
&lt;td&gt;1,415 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;web-vitals.iife.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7,226 B&lt;/td&gt;
&lt;td&gt;2,601 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;web-vitals.attribution.iife.js&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12,505 B&lt;/td&gt;
&lt;td&gt;4,172 B&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;7.7x smaller raw, 4.6x gzipped. And still not a fair fight, in both&lt;br&gt;
directions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unfair to us:&lt;/strong&gt; &lt;code&gt;web-vitals&lt;/code&gt; only measures. It hands each metric to a&lt;br&gt;
callback and leaves transport entirely to you, so a real deployment adds&lt;br&gt;
reporting code on top of those bytes. Our 942 already include JSON&lt;br&gt;
serialisation, &lt;code&gt;sendBeacon&lt;/code&gt;, a &lt;code&gt;fetch&lt;/code&gt; fallback, and flush-on-hide.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Unfair to them:&lt;/strong&gt; &lt;code&gt;web-vitals&lt;/code&gt; does more per metric: back-forward cache&lt;br&gt;
restoration, prerendering and &lt;code&gt;activationStart&lt;/code&gt;, soft navigations,&lt;br&gt;
attribution to the element that caused the bad LCP, real INP grouped by&lt;br&gt;
&lt;code&gt;interactionId&lt;/code&gt; rather than the longest single event, and years of Safari&lt;br&gt;
workarounds.&lt;/p&gt;

&lt;p&gt;At 942 bytes I did none of that. So I wrote a second beacon that does five&lt;br&gt;
of those six, at 2,656 bytes, and kept it as a separate file rather than&lt;br&gt;
folding it into the first. Two reasons, one technical and one not. The&lt;br&gt;
technical one: most sites want the small one, and paying 1.7KB for prerender&lt;br&gt;
correction on a site that never prerenders is exactly the trade this project&lt;br&gt;
exists to argue against. The other: the sub-1KB number is the headline&lt;br&gt;
claim, and quietly redefining "the beacon" to mean the bigger file is the&lt;br&gt;
move I would criticise in someone else's README.&lt;/p&gt;

&lt;p&gt;Neither beacon does the Safari and Firefox work. That is years of&lt;br&gt;
accumulated browser bug knowledge, not something you reproduce by reading a&lt;br&gt;
spec. A comparison is more convincing with its caveats than without, because&lt;br&gt;
a reader who spots an unstated one stops trusting the rest of your numbers&lt;br&gt;
too.&lt;/p&gt;
&lt;h3&gt;
  
  
  5. The cache that outlived the bug
&lt;/h3&gt;

&lt;p&gt;I broke &lt;code&gt;dash.js&lt;/code&gt; with a one-character mistake, a literal newline inside a&lt;br&gt;
string literal, which is a syntax error. The whole script failed to parse,&lt;br&gt;
so the dashboard rendered nothing. Every Go test passed. Of course they did.&lt;br&gt;
&lt;code&gt;node --check&lt;/code&gt; is now in the pre-finish checklist, alongside &lt;code&gt;make check&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The instructive part came next. I fixed it, rebuilt, restarted, and the page&lt;br&gt;
was still blank:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;Cache-Control: public, max-age=3600
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The asset names carry no content hash. A browser that had already fetched&lt;br&gt;
the broken script would not ask again for an hour. Server fixed, disk&lt;br&gt;
fixed, user still broken, and no amount of restarting changes that, because&lt;br&gt;
nothing on the server participates in the decision.&lt;/p&gt;

&lt;p&gt;The fix is a policy split rather than a blanket. The dashboard's own scripts&lt;br&gt;
are now &lt;code&gt;no-cache&lt;/code&gt;, which still caches them but forces a revalidation that&lt;br&gt;
answers &lt;code&gt;304&lt;/code&gt; with an empty body. The beacon keeps the long &lt;code&gt;max-age&lt;/code&gt;,&lt;br&gt;
because it is fetched by every page view of an instrumented site, where a&lt;br&gt;
conditional request per view is a real cost. Same file server, two&lt;br&gt;
policies, chosen by who is asking and how often.&lt;/p&gt;

&lt;p&gt;A long cache lifetime is a promise you cannot revoke. It is only safe when&lt;br&gt;
the URL changes with the content. If you are not hashing names, you are&lt;br&gt;
betting you will never ship a bug.&lt;/p&gt;
&lt;h3&gt;
  
  
  6. The half-open window that dropped the present
&lt;/h3&gt;

&lt;p&gt;An end-to-end test started failing about half the time. Same code, same&lt;br&gt;
machine, same command.&lt;/p&gt;

&lt;p&gt;The dashboard's default window ends at "now". The store's range is&lt;br&gt;
half-open, &lt;code&gt;[from, to)&lt;/code&gt;. A measurement recorded in the same clock tick as&lt;br&gt;
the request that reads it lands exactly on the exclusive bound and falls&lt;br&gt;
outside its own window. On Windows, where the wall clock is coarse enough&lt;br&gt;
that two calls microseconds apart return the same value, that is a coin&lt;br&gt;
flip.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IsZero&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;            &lt;span class="c"&gt;// excludes anything stamped at this instant&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every test passing an explicit &lt;code&gt;to&lt;/code&gt; was fine. Only the default was wrong,&lt;br&gt;
and the default is what the only real client sends: the same gap as bug 2,&lt;br&gt;
in a different corner of the same function. The failure mode was "I just&lt;br&gt;
recorded a measurement and it is not there", intermittently, which is the&lt;br&gt;
worst kind of bug report to receive from yourself.&lt;/p&gt;

&lt;p&gt;The fix is one millisecond, and the reasoning matters more than the line.&lt;br&gt;
Records are stored at millisecond resolution, so ending the default window&lt;br&gt;
one millisecond after now includes the present instant and nothing that has&lt;br&gt;
not happened yet. An explicit &lt;code&gt;to&lt;/code&gt; is left exactly where the caller put it.&lt;/p&gt;

&lt;p&gt;A flaky test is a race you have already reproduced. I nearly re-ran it and&lt;br&gt;
moved on.&lt;/p&gt;


&lt;h2&gt;
  
  
  The bug the whole suite was structurally blind to
&lt;/h2&gt;

&lt;p&gt;This one arrived last and it is the one I would most want a reviewer to&lt;br&gt;
see. The dashboard has a button that copies a report as a prompt for an AI&lt;br&gt;
agent. Clicking it threw:&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="nx"&gt;Cannot&lt;/span&gt; &lt;span class="nx"&gt;read&lt;/span&gt; &lt;span class="nx"&gt;properties&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nf"&gt;undefined &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reading&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;good&lt;/span&gt;&lt;span class="dl"&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 Go struct tagged one field &lt;code&gt;json:"Distribution"&lt;/code&gt; with a capital D. The&lt;br&gt;
dashboard, the API documentation, and the contract test all said&lt;br&gt;
&lt;code&gt;distribution&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Here is why nothing caught it: &lt;code&gt;encoding/json&lt;/code&gt; matches field names&lt;br&gt;
case-insensitively when decoding. The contract test unmarshals the response&lt;br&gt;
into a struct tagged &lt;code&gt;json:"distribution"&lt;/code&gt;, and it bound happily to&lt;br&gt;
&lt;code&gt;"Distribution"&lt;/code&gt;. It passed against either spelling. Every Go test in the&lt;br&gt;
repository was structurally blind to this, and only a browser, which is not&lt;br&gt;
case-insensitive about anything, could ever see it.&lt;/p&gt;

&lt;p&gt;The regression test therefore does not use a struct. It decodes into&lt;br&gt;
&lt;code&gt;map[string]any&lt;/code&gt; and walks every key in the document:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="sc"&gt;'A'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;first&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="sc"&gt;'Z'&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"key %s%s starts upper-case; a JavaScript client "&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
        &lt;span class="s"&gt;"reading the documented lower-case name gets undefined"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&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;Decoding into a map is the entire point. It is the only way to assert the&lt;br&gt;
key a non-Go client actually receives, rather than the key Go is willing to&lt;br&gt;
accept.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I did not build
&lt;/h2&gt;

&lt;p&gt;Stated here rather than left for someone to notice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The binary segment storage format was planned and cut.&lt;/strong&gt; The design called&lt;br&gt;
for compacting sealed day logs into a hand-written binary format with varint&lt;br&gt;
delta timestamps and a string dictionary. It stayed JSONL. That decision now&lt;br&gt;
has a number attached rather than a shrug: replaying 100,000 records takes&lt;br&gt;
&lt;strong&gt;593ms&lt;/strong&gt;, of which roughly 500ms is &lt;code&gt;encoding/json&lt;/code&gt; on the read path. A&lt;br&gt;
million records is about six seconds and 1.6GB to open. That 500ms is&lt;br&gt;
exactly what the segment format would have removed, and it is the honest&lt;br&gt;
size of what cutting it gave up.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;There is no authentication.&lt;/strong&gt; The dashboard and API are open to anyone who&lt;br&gt;
can reach the port. Collection is rate limited per client address, but&lt;br&gt;
forwarded headers are ignored because they are trivially spoofable, which&lt;br&gt;
means behind a reverse proxy every visitor shares one bucket and one&lt;br&gt;
derived session id.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;reportAllChanges&lt;/code&gt; is not implemented.&lt;/strong&gt; &lt;code&gt;web-vitals&lt;/code&gt; can emit every LCP&lt;br&gt;
candidate as it changes. This sends one record per page view on hide, which&lt;br&gt;
is the whole reason it fits in 942 bytes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The decision I would take back
&lt;/h2&gt;

&lt;p&gt;The beacon exists as two files: &lt;code&gt;beacon.src.js&lt;/code&gt;, readable and commented, and&lt;br&gt;
&lt;code&gt;beacon.min.js&lt;/code&gt;, minified by hand. There is no minifier in the project,&lt;br&gt;
because a minifier is a build dependency and the project has no build step.&lt;/p&gt;

&lt;p&gt;The cost is that a human keeps two files in sync, and humans drift. I&lt;br&gt;
mitigated it rather than solved it. Tests assert that every metric key,&lt;br&gt;
every observed entry type, and the collection endpoint appear in both&lt;br&gt;
files, and that the minified file is actually minified rather than a copy&lt;br&gt;
of the readable one under the wrong name. That catches the catastrophic&lt;br&gt;
mistake. It does not catch a subtle logic change applied to one file and&lt;br&gt;
not the other.&lt;/p&gt;

&lt;p&gt;A real minifier would make the drift structurally impossible, and I would&lt;br&gt;
take the build step. The zero-dependency rule was about &lt;em&gt;runtime&lt;/em&gt;&lt;br&gt;
dependencies, and I let it bleed into build tooling where it bought nothing&lt;br&gt;
but risk.&lt;/p&gt;




&lt;h2&gt;
  
  
  By the numbers
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Beacon&lt;/td&gt;
&lt;td&gt;942 B raw, 571 B gzipped, 1024 B budget enforced by the build&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full beacon&lt;/td&gt;
&lt;td&gt;2,656 B raw, 1,415 B gzipped, separate 2816 B budget&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;vs &lt;code&gt;web-vitals&lt;/code&gt; 4.2.4&lt;/td&gt;
&lt;td&gt;7.7x smaller raw, 4.6x gzipped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dependencies&lt;/td&gt;
&lt;td&gt;0. &lt;code&gt;go list -m all&lt;/code&gt; prints one line&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;~13,750 lines, of which ~8,100 are tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tests&lt;/td&gt;
&lt;td&gt;245 functions, 10 benchmarks, 1 fuzz target, 20+ table-driven&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;~3,400 lines, no framework, no bundler, no build step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Packages replaced&lt;/td&gt;
&lt;td&gt;41, each with a note on where the original is better&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser checks&lt;/td&gt;
&lt;td&gt;45 scripted, in real Chrome 152&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Commits&lt;/td&gt;
&lt;td&gt;50, with the tree building and &lt;code&gt;make check&lt;/code&gt; passing at every one&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of the later features came from measuring rather than reasoning.&lt;br&gt;
Benchmarking the store turned up a genuine quadratic path: an out-of-order&lt;br&gt;
insert rebuilt both secondary indexes, &lt;strong&gt;0.7 to 1.0 ms against 66 µs&lt;/strong&gt; for&lt;br&gt;
shifting them instead, and out-of-order arrival was not the rare event I had&lt;br&gt;
assumed. The collector stamps the wall clock and takes the store lock as a&lt;br&gt;
separate step, so concurrent page views land reversed constantly. And "you&lt;br&gt;
lose at most two seconds on a crash" was still only a sentence, so I wrote a&lt;br&gt;
test that builds the binary, runs it as a child process, feeds it 500&lt;br&gt;
measurements, and kills it. A clean &lt;code&gt;Close&lt;/code&gt; had never tested the thing the&lt;br&gt;
sentence claimed.&lt;/p&gt;

&lt;p&gt;Said out loud rather than buried: percentiles are read from histogram&lt;br&gt;
buckets and carry up to 4.9% relative error on millisecond metrics and&lt;br&gt;
0.0025 absolute on CLS, while band counts are exact. The small beacon&lt;br&gt;
approximates INP as the longest event over 16ms, so it is pessimistic in&lt;br&gt;
the tail, which is why the full beacon exists. Firefox and Safari are&lt;br&gt;
untested, and so are the full beacon's own bfcache, soft-navigation, and&lt;br&gt;
prerender paths: reviewed against the specs, never watched running in a&lt;br&gt;
browser. That is the largest honest gap in the project, and it is why the&lt;br&gt;
small beacon remains the default.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the time actually went on
&lt;/h2&gt;

&lt;p&gt;Anyone can write a &lt;code&gt;PerformanceObserver&lt;/code&gt; that compiles and a Go server that&lt;br&gt;
serves. The beacon is 942 bytes; it took an afternoon. What the rest of it&lt;br&gt;
went on was building enough ways to catch myself being wrong: a dependency&lt;br&gt;
checker that fails the build on a &lt;code&gt;require&lt;/code&gt; block, a CDN reference, or a web&lt;br&gt;
font; a size tool that measures every file with one compressor; a CI job&lt;br&gt;
that builds twice and runs &lt;code&gt;cmp&lt;/code&gt;; a fuzz target on the only place the&lt;br&gt;
program reads untrusted input; a test that kills a real process; forty-five&lt;br&gt;
scripted checks in a real browser, because my own suite could not execute&lt;br&gt;
the file it was shipping.&lt;/p&gt;

&lt;p&gt;Every one of those exists because a claim I had written down in English&lt;br&gt;
turned out not to be a claim anything was checking. That translation step,&lt;br&gt;
from the sentence in the README to the assertion in the test, is where all&lt;br&gt;
six bugs lived.&lt;/p&gt;

&lt;p&gt;Write the claim in plain English, then ask what would have to be true. It&lt;br&gt;
is the cheapest tool in the list and it found more than any of the others.&lt;/p&gt;

&lt;p&gt;The tool measures page weight and contributes 942 bytes of it. That was the&lt;br&gt;
whole idea, and it survived contact with a real browser. Eventually.&lt;/p&gt;

&lt;p&gt;Built for the &lt;a href="https://zerodepshack.com/" rel="noopener noreferrer"&gt;Zero Dependency Hackathon 2026&lt;/a&gt;, the&lt;br&gt;
72 hour dependency-free build hackathon run by &lt;a href="https://raptors.dev" rel="noopener noreferrer"&gt;Hackathon Raptors&lt;/a&gt;, Track D (Data &amp;amp; Storage). The full project,&lt;br&gt;
&lt;code&gt;STDLIB.md&lt;/code&gt;, the browser test harness, and the size comparison tool are all in&lt;br&gt;
the repo.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/ikeshavvarshney/vitals" rel="noopener noreferrer"&gt;https://github.com/ikeshavvarshney/vitals&lt;/a&gt;&lt;/p&gt;

</description>
      <category>go</category>
      <category>webperf</category>
      <category>testing</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
