<?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: Manjeet Singh</title>
    <description>The latest articles on DEV Community by Manjeet Singh (@manusingh).</description>
    <link>https://dev.to/manusingh</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%2F4152835%2Fa7a07ce6-6009-4212-9ba0-1f8c03285e46.png</url>
      <title>DEV Community: Manjeet Singh</title>
      <link>https://dev.to/manusingh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/manusingh"/>
    <language>en</language>
    <item>
      <title>I misread my profiler, my rewrite made it slower, and two lines cut memory by 60%</title>
      <dc:creator>Manjeet Singh</dc:creator>
      <pubDate>Tue, 03 Nov 2026 15:53:00 +0000</pubDate>
      <link>https://dev.to/manusingh/i-misread-my-profiler-my-rewrite-made-it-slower-and-two-lines-cut-memory-by-60-ja3</link>
      <guid>https://dev.to/manusingh/i-misread-my-profiler-my-rewrite-made-it-slower-and-two-lines-cut-memory-by-60-ja3</guid>
      <description>&lt;p&gt;My laptop said the program peaked at 71 MB. The scoring machine said 175 MiB.&lt;/p&gt;

&lt;p&gt;Same binary. Same data. Same Docker image. Neither number was wrong.&lt;/p&gt;

&lt;p&gt;That was the first of many times during &lt;a href="https://speeddemon.dev" rel="noopener noreferrer"&gt;Speed Demon 2026&lt;/a&gt;, the 72-hour performance championship run by &lt;strong&gt;Hackathon Raptors&lt;/strong&gt;, when a measurement I trusted turned out to be correct and useless at the same time. Over 106 logged steps I found that almost every mistake I made had the same shape: &lt;strong&gt;I read a true number that was answering a different question than the one I was asking.&lt;/strong&gt; The profiler answered "where did the samples land", not "what would disappear if I removed this". The instruction counter answered "how much work", not "what is the core waiting for". My laptop answered "how fast is this on my laptop".&lt;/p&gt;

&lt;p&gt;This post is the list of those questions, what each wrong reading cost me, and the measurement that finally answered the right one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The task, and a machine you never touch
&lt;/h2&gt;

&lt;p&gt;I'll stay on one of the four tracks, Track B. Answer 50,000 search queries over a 254 MB corpus of 250,000 documents. 62% are BOOL queries (&lt;code&gt;AND&lt;/code&gt;, &lt;code&gt;OR&lt;/code&gt;, &lt;code&gt;NOT&lt;/code&gt;, applied strictly left to right), 38% are exact phrases, and every output line has to match the reference byte for byte. The reference Python takes &lt;strong&gt;56.35 s and 2.2 GB&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Each submission is scored as one number:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;K = (T_ref/T)^0.5 × (M_ref/M)^0.2 × (S_ref/S)^0.15 × (D_ref/D)^0.15
points = 50 × log10(K)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;T is wall time, M the container's peak memory, S the line count after a formatter, D the dependency count. So halving the time multiplies K by 1.41, halving memory by 1.15, and doubling the code costs 10%. Keep those weights in mind: twice in this story they decided which "obvious" improvement was actually a loss.&lt;/p&gt;

&lt;p&gt;The scoring machine is an Intel i5-13500 with turbo off. You push a commit, wait in a queue, and two numbers come back. Here is what that machine is, next to what I was developing on:&lt;/p&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;Scoring box&lt;/th&gt;
&lt;th&gt;My laptop&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CPU&lt;/td&gt;
&lt;td&gt;i5-13500, fixed 2.5 GHz&lt;/td&gt;
&lt;td&gt;Ryzen 7 7840HS, boosting near 5 GHz&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Threads used&lt;/td&gt;
&lt;td&gt;12 on 6 P-cores (two per core)&lt;/td&gt;
&lt;td&gt;12 of 16&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;L2&lt;/td&gt;
&lt;td&gt;1.25 MB per core, shared by two threads&lt;/td&gt;
&lt;td&gt;1 MB per core&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vectors&lt;/td&gt;
&lt;td&gt;AVX2, no AVX-512&lt;/td&gt;
&lt;td&gt;AVX-512&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Environment&lt;/td&gt;
&lt;td&gt;bare container&lt;/td&gt;
&lt;td&gt;Docker inside WSL2, about 9 µs per page fault&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What you get back&lt;/td&gt;
&lt;td&gt;T and M&lt;/td&gt;
&lt;td&gt;anything you want&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Almost every lesson below is a row of this table biting me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Belief 1: "Peak memory is what I allocate"
&lt;/h2&gt;

&lt;p&gt;It isn't. Peak memory on the board is the cgroup's &lt;code&gt;memory.peak&lt;/code&gt;, which charges every resident page. What becomes resident depends on the kernel, not just on you.&lt;/p&gt;

&lt;p&gt;My scan wrote its output into many blocks per thread, each filled a little at a time. At any moment most of them were half written. On my laptop the WSL2 kernel ran transparent huge pages in &lt;code&gt;madvise&lt;/code&gt; mode: a process gets 2 MB pages only where it asks. The scoring machine evidently ran &lt;code&gt;always&lt;/code&gt;, so the first byte written into a fresh region could pull in a whole 2 MB page. Many half-written blocks per thread, times twelve threads, is roughly the 100 MiB I couldn't see.&lt;/p&gt;

&lt;p&gt;The fix was two lines of C++:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;sys/prctl.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;&lt;span class="n"&gt;prctl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PR_SET_THP_DISABLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// a block written in part stays small pages&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Board run&lt;/th&gt;
&lt;th&gt;T&lt;/th&gt;
&lt;th&gt;M&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;before&lt;/td&gt;
&lt;td&gt;run-5c032275f3b6&lt;/td&gt;
&lt;td&gt;0.301 s&lt;/td&gt;
&lt;td&gt;175.2 MiB&lt;/td&gt;
&lt;td&gt;60.51&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;13&lt;/td&gt;
&lt;td&gt;the two lines&lt;/td&gt;
&lt;td&gt;run-e6924d8063c9&lt;/td&gt;
&lt;td&gt;0.292 s&lt;/td&gt;
&lt;td&gt;69.6 MiB&lt;/td&gt;
&lt;td&gt;64.85&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Sixty percent less memory and 4.3 points, for two lines.&lt;/p&gt;

&lt;p&gt;Then I made the same mistake again. When I rewrote the program in Rust I left the line out, and memory crept back to 69.1 MiB on a version that should have been under 50. This time I didn't guess. I switched my laptop VM to THP &lt;code&gt;always&lt;/code&gt;, reran both builds, and watched the difference appear locally (about 5 MB), then vanish with &lt;code&gt;PR_SET_THP_DISABLE&lt;/code&gt;. On the box the same line took that version from 69.1 to 48.8 MiB (run-174007cb966d).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The question I should have asked:&lt;/strong&gt; not "how much do I allocate?" but "what will this kernel make resident when I touch it?" If your laptop and your target disagree about memory, look at the boring knobs first: transparent huge pages, the allocator's mmap threshold, how freed memory goes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  Belief 2: "Shorter code will score higher"
&lt;/h2&gt;

&lt;p&gt;On day two the C++ was 968 counted lines against a reference of 91, and lines are in the formula. I worked out that a version at 60% of the size and the same speed would score about 7% more, and I rewrote it from scratch in Rust: 612 lines, correct on every dataset on its first build.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Board run&lt;/th&gt;
&lt;th&gt;T&lt;/th&gt;
&lt;th&gt;M&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;C++ (step 23)&lt;/td&gt;
&lt;td&gt;run-c2b149a34d40&lt;/td&gt;
&lt;td&gt;0.232 s&lt;/td&gt;
&lt;td&gt;52.7 MiB&lt;/td&gt;
&lt;td&gt;968&lt;/td&gt;
&lt;td&gt;67.95&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rust rewrite (step 24)&lt;/td&gt;
&lt;td&gt;run-880032be5ecf&lt;/td&gt;
&lt;td&gt;0.253 s&lt;/td&gt;
&lt;td&gt;65.6 MiB&lt;/td&gt;
&lt;td&gt;612&lt;/td&gt;
&lt;td&gt;67.55&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The arithmetic explains it exactly. Fewer lines are worth (968/612)^0.15 = &lt;strong&gt;+7.1%&lt;/strong&gt;. But 9% more time costs (0.232/0.253)^0.5 = &lt;strong&gt;-4.2%&lt;/strong&gt;, and 25% more memory costs (52.7/65.6)^0.2 = &lt;strong&gt;-4.3%&lt;/strong&gt;. Net: -1.8% of K, which is the 0.40 points the board took away.&lt;/p&gt;

&lt;p&gt;A rewrite throws away dozens of small decisions you made without writing them down (the THP line was one of them). I put the C++ back on the board and kept improving the Rust until it actually won, two steps later (0.241 s, 58.0 MiB, 68.86 points). The rewrite was right; shipping it on an estimate was not.&lt;/p&gt;

&lt;p&gt;Hold on to that arithmetic, because near the end of the contest it pointed the other way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Belief 3: "The profiler shows me where the time goes"
&lt;/h2&gt;

&lt;p&gt;A sampled profile on the laptop said building the posting lists took 25% of all cycles. I spent an evening on that phase. Then I built a variant with its stores switched off, and the run got under 2% faster.&lt;/p&gt;

&lt;p&gt;Sampling inside that VM attributed cycles to the wrong threads. The profile was answering "where did the timer interrupts land", and in a VM with twelve busy threads that is a different question from "what does this code cost".&lt;/p&gt;

&lt;p&gt;From then on I used one method for cost: &lt;strong&gt;ablation.&lt;/strong&gt; Build a variant with exactly one part disabled, count what disappears (user cycles, instructions, wall time), put it back. It is slower than reading a flame graph, and I have never been misled by it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Belief 4: "Fewer instructions means faster"
&lt;/h2&gt;

&lt;p&gt;This is the one I'm most glad I got wrong, because the way it broke taught me how the box actually works.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;perf stat&lt;/code&gt; said the scan retired 136 instructions per token over 36.3M tokens. The disassembly showed why: the per-document loop kept 16 values live and reloaded most of them from the stack on every token, and the hash took 25 instructions. That is a deeply satisfying thing to find. I split the loop into short ones, made the hash one 128-bit multiply, and cut 10% of the whole program's instructions (6,516M to 5,881M).&lt;/p&gt;

&lt;p&gt;The box's answer, run-152f40943619: &lt;strong&gt;0.1407 s, against 0.1389 s before.&lt;/strong&gt; Ten percent less work, zero time saved.&lt;/p&gt;

&lt;p&gt;So I asked a different question: what is the core &lt;em&gt;waiting&lt;/em&gt; for? Cache-miss counters, plus ablation, gave two answers I should have looked for first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phrase checks cost 144M instructions but 7.6M of the scan's 26 to 29M last-level cache misses.&lt;/strong&gt; Each check read four scattered arrays (anchor starts, anchor records, query spans, query terms) to reject one window. Cheap in instructions, expensive in misses.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A 107 KB per-thread array existed to drop a term's repeats within a document.&lt;/strong&gt; It was read and written at a random place for every list-term token. I counted what it actually did: 10.94M of 11.02M list-term occurrences were already new to their document. It removed &lt;strong&gt;0.7%&lt;/strong&gt; of the postings and cost a likely cache miss on most tokens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The fixes were plain. Each phrase anchor got a 16-byte record holding everything needed to reject a window, prefetched for every anchor in a document before the other loops run. The dedupe array went away entirely; the few repeats are dropped later, when the lists are built from sequential data.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Change&lt;/th&gt;
&lt;th&gt;Board run&lt;/th&gt;
&lt;th&gt;T&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;86&lt;/td&gt;
&lt;td&gt;baseline&lt;/td&gt;
&lt;td&gt;run-26e4b60b0e60&lt;/td&gt;
&lt;td&gt;0.1389 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;87&lt;/td&gt;
&lt;td&gt;10% fewer instructions&lt;/td&gt;
&lt;td&gt;run-152f40943619&lt;/td&gt;
&lt;td&gt;0.1407 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;88&lt;/td&gt;
&lt;td&gt;phrase records prefetched&lt;/td&gt;
&lt;td&gt;run-18961f21c438&lt;/td&gt;
&lt;td&gt;0.1279 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;89&lt;/td&gt;
&lt;td&gt;dedupe array deleted&lt;/td&gt;
&lt;td&gt;run-1406c8e99675&lt;/td&gt;
&lt;td&gt;0.1243 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Later I tried the instruction route once more, on purpose: vectorizing the per-document loops cut 7% of instructions and bought 1% of time (run-807c1ffd2504). Same answer.&lt;/p&gt;

&lt;p&gt;Two things to take from this. First, with two hyperthreads per core and a lot of memory traffic, the core's instruction throughput is not the limit; its wait for memory is, and the other hyperthread happily soaks up whatever instructions you remove. Second, and more general: &lt;strong&gt;count what a structure actually does before you optimize it.&lt;/strong&gt; I had spent effort making that dedupe array fast. The right number to know was "0.7%".&lt;/p&gt;

&lt;h2&gt;
  
  
  Belief 5: "I can't measure the machine I can't touch"
&lt;/h2&gt;

&lt;p&gt;By now I trusted the box over my laptop, but the box only tells you two numbers. So I made the program tell me more &lt;em&gt;through&lt;/em&gt; those two numbers.&lt;/p&gt;

&lt;p&gt;The trick: time a phase inside the program, then encode that duration into the outputs the board reports. For memory, touch one 4 KB page per 4 µs of the phase and keep the pages until exit, so peak memory rises by an amount you can decode. For wall time, sleep for ten times another phase before exiting, so the noise of a single run matters ten times less.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Phase A's duration, written into peak memory: one touched page per 4 µs.&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;phase_a&lt;/span&gt;&lt;span class="nf"&gt;.as_micros&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nb"&gt;usize&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;mmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;null_mut&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;pages&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0x22&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="n"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.for_each&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="nf"&gt;write_volatile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="nf"&gt;.add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;

&lt;span class="c1"&gt;// Phase B's duration, written into wall time, ten times over.&lt;/span&gt;
&lt;span class="nn"&gt;std&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;thread&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;phase_b&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One such run (run-d59c352f1a2a) came back 8.4 MiB heavier and 64 ms slower than the normal run. Decoded: &lt;strong&gt;8.6 ms parsing the queries, 6.4 ms of serial setup.&lt;/strong&gt; Fifteen milliseconds of a 124 ms run on one thread while eleven threads waited. A later run of the same kind found the answer phase at 29 ms on the box, against 16 ms on my laptop: almost exactly the ratio of the two clocks.&lt;/p&gt;

&lt;p&gt;The parse was the embarrassing part. I was splitting a 1.8 MB query file byte by byte, then word by word, about 28 instructions per byte, on one thread, while the corpus itself already went through a 64-byte AVX2 class mask. Reading the queries with the same mask halved the parse's instructions on the laptop (36.9M to 18.4M). On the box it was worth about 10 ms: &lt;strong&gt;0.1243 s to 0.1144 s&lt;/strong&gt; (run-fa63626812c0), an 8% step from code that was nowhere near the hot loop.&lt;/p&gt;

&lt;p&gt;These measurement runs scored terribly on purpose (71.04 and 66.70 points). They were the best runs I made.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The general version:&lt;/strong&gt; if a black box only reports scalars, make your program speak through them. Any benchmark harness that reports time and memory can be turned into a crude profiler of itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Belief 6: "If it's faster on my laptop, it's faster"
&lt;/h2&gt;

&lt;p&gt;Not every disagreement went my way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A smaller term table (1 MB instead of 2 MB) cut about 5M last-level misses on the laptop. On the box: &lt;strong&gt;0.1243 s to 0.1335 s&lt;/strong&gt;. Longer probe chains cost more than the misses saved.&lt;/li&gt;
&lt;li&gt;Turning lists into bitmaps sooner made my laptop's answer phase about 10% faster. On the box: &lt;strong&gt;0.1144 s to 0.1489 s&lt;/strong&gt; (run-1aa9c7fc4435), a third slower. More 31 KB bitmaps were zeroed, filled and counted on a machine where two threads share 1.25 MB of L2.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;My laptop had more cache per thread, twice the clock, AVX-512, and page faults that cost 9 µs because of the VM. Of course it disagreed. I stopped treating laptop results as measurements and started treating them as hypotheses that the box would accept or reject.&lt;/p&gt;

&lt;h2&gt;
  
  
  Belief 7: "The slow phase needs to be faster"
&lt;/h2&gt;

&lt;p&gt;The biggest late gain came from asking a different kind of question: not "how do I make the answer phase faster", but "why does anyone have to wait for it?"&lt;/p&gt;

&lt;p&gt;The old program scanned the whole corpus, built one index, then answered. The new design reads the corpus in phases of about 50 MB. Each phase gets its own small 16-bit postings and bitmaps, every query is answered against that phase into a running tally (count, id sum, ten smallest ids), and the next phase reuses the memory. Two things fall out of that. No full index ever exists, so memory drops. And the answers for phase &lt;em&gt;k&lt;/em&gt; run on whichever threads have already finished scanning phase &lt;em&gt;k+1&lt;/em&gt;, so the answer phase overlaps the scan instead of waiting for all of it, filling time when those threads would otherwise sit idle.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Design&lt;/th&gt;
&lt;th&gt;Board run&lt;/th&gt;
&lt;th&gt;T&lt;/th&gt;
&lt;th&gt;M&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;99&lt;/td&gt;
&lt;td&gt;one index, then answers&lt;/td&gt;
&lt;td&gt;run-71af5d224eec&lt;/td&gt;
&lt;td&gt;0.1102 s&lt;/td&gt;
&lt;td&gt;47.8 MiB&lt;/td&gt;
&lt;td&gt;739&lt;/td&gt;
&lt;td&gt;77.32&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;102&lt;/td&gt;
&lt;td&gt;phases with running tallies&lt;/td&gt;
&lt;td&gt;run-be2f78572d3a&lt;/td&gt;
&lt;td&gt;0.0907 s&lt;/td&gt;
&lt;td&gt;41.4 MiB&lt;/td&gt;
&lt;td&gt;1,103&lt;/td&gt;
&lt;td&gt;78.70&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here is the arithmetic from belief 2, pointing the other way. 364 more lines cost (739/1103)^0.15 = &lt;strong&gt;-5.8%&lt;/strong&gt;. But 18% less time is worth (0.1102/0.0907)^0.5 = &lt;strong&gt;+10.2%&lt;/strong&gt;, and 13% less memory (47.8/41.4)^0.2 = &lt;strong&gt;+2.9%&lt;/strong&gt;. Net +6.8% of K, about 1.4 points, which is what the board gave.&lt;/p&gt;

&lt;p&gt;Same formula, same kind of trade, opposite verdict. Neither "shorter is better" nor "faster is better" is a rule. The weights are.&lt;/p&gt;

&lt;p&gt;I then tried slicing the phases differently: 1 MiB slices gave 0.0973 s, 128 KiB slices 0.0953 s. The 256 KiB slices stayed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Belief 8: "All my datasets pass, so it's correct"
&lt;/h2&gt;

&lt;p&gt;While vectorizing a loop that reads eight ids at a time, I found a buffer that grew by &lt;code&gt;2 × n&lt;/code&gt; entries whenever fewer than 32 were left. A 64-byte window can add 32 tokens. Growing by &lt;code&gt;2n&lt;/code&gt; only guarantees that room once &lt;code&gt;n&lt;/code&gt; reaches 24. So the first document each thread read could write past its buffer, if its first 64-byte windows held more than 16 one- or two-letter tokens.&lt;/p&gt;

&lt;p&gt;All six datasets passed, including the hand-built edge cases. The bug had been on the board for ten steps. I generated 5,000 documents made only of single letters and digits, took the reference's answer, and ran mine: &lt;strong&gt;heap corruption, abort.&lt;/strong&gt; The fix was one expression, &lt;code&gt;2 * n + 32&lt;/code&gt;, and that dense dataset now runs in every check before a push.&lt;/p&gt;

&lt;p&gt;The spec's edge cases test the spec. They cannot test &lt;em&gt;your&lt;/em&gt; invariants, because the spec doesn't know about your buffers. Whenever you write a bound, write the input that sits exactly on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is still wrong
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Track B ends at &lt;strong&gt;0.0907 s, 41.4 MiB, 78.70 points&lt;/strong&gt;, about 620 times faster than the reference. When I last checked, the leader was at &lt;strong&gt;82.41 points and 0.064 s&lt;/strong&gt;. I don't know how they get there, and that gap is real.&lt;/li&gt;
&lt;li&gt;Query terms are matched by 47 bits of their hash with no byte comparison. The chance that some non-query token in a run collides with a query term is about 3 in 10,000. It has never happened on any dataset, but "never happened" is not "can't happen", and a second tag checked only on a hit would close it cheaply.&lt;/li&gt;
&lt;li&gt;My laptop is still a bad model of the box. I've learned which of its answers to distrust, not how to make it agree.&lt;/li&gt;
&lt;li&gt;The other tracks ended at 86.16 (log aggregation), 81.93 (CSV join) and 71.92 (shortest paths). Track D in particular never got the kind of breakthrough described here.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The rules I'd give myself on day one
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Before you trust a number, say which question it answers.&lt;/strong&gt; Most of my wasted days came from a correct number answering the wrong question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Measure cost by removal.&lt;/strong&gt; "What disappears when this is gone" is very hard to misread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;On shared cores, count misses before instructions.&lt;/strong&gt; My 10% instruction cut bought nothing; deleting one array that did 0.7% of useful work bought 3%.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Count what a structure does before you speed it up.&lt;/strong&gt; The answer may be "almost nothing".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the target measure itself.&lt;/strong&gt; Two scalars are enough to carry a phase timing out of a machine you can't log into.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Treat the environment as code.&lt;/strong&gt; Two lines about huge pages beat a day of tuning, twice.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do the scoring arithmetic every time.&lt;/strong&gt; The same trade of lines for speed lost me 0.4 points once and won me 1.4 later.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Check it yourself
&lt;/h2&gt;

&lt;p&gt;The repo includes the organisers' checker, which builds a track offline and runs it in the same sandbox as the board. With Docker and Python, and the sample data generated as the repo's notes describe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/manusingh090/speed-demon-2026-entry
&lt;span class="nb"&gt;cd &lt;/span&gt;speed-demon-2026-entry
python check.py b &lt;span class="nt"&gt;--data&lt;/span&gt; data/b-sample &lt;span class="nt"&gt;--expected&lt;/span&gt; eeeec1e9319da98a6f3c664c78f93e7078fd603c25c2bb3027174e81e3638a8f
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every row of &lt;code&gt;track-b/PERF.md&lt;/code&gt; names the board run that measured it, including the failures and the deliberately terrible measurement runs, so any number in this post can be checked against the public board.&lt;/p&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://speeddemon.dev" rel="noopener noreferrer"&gt;Speed Demon 2026&lt;/a&gt;, by Hackathon Raptors: rules, scoring and the live board&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://www.kernel.org/doc/html/latest/admin-guide/mm/transhuge.html" rel="noopener noreferrer"&gt;Transparent Hugepage Support&lt;/a&gt;, Linux kernel documentation on the &lt;code&gt;always&lt;/code&gt; and &lt;code&gt;madvise&lt;/code&gt; modes&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://man7.org/linux/man-pages/man2/prctl.2.html" rel="noopener noreferrer"&gt;prctl(2)&lt;/a&gt;, for &lt;code&gt;PR_SET_THP_DISABLE&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/manusingh090/speed-demon-2026-entry/blob/speed-demon/track-b/PERF.md" rel="noopener noreferrer"&gt;The full Track B log&lt;/a&gt;: 106 steps, each with its board run&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Thanks to Hackathon Raptors for a contest where the scoring machine had the last word on every claim, especially the ones I was sure about.&lt;/p&gt;

</description>
      <category>performance</category>
      <category>rust</category>
      <category>hackathonraptors</category>
      <category>speeddemon</category>
    </item>
    <item>
      <title>Seven green checks and a team page that named its judges</title>
      <dc:creator>Manjeet Singh</dc:creator>
      <pubDate>Fri, 02 Oct 2026 06:29:50 +0000</pubDate>
      <link>https://dev.to/manusingh/seven-green-checks-and-a-team-page-that-named-its-judges-57en</link>
      <guid>https://dev.to/manusingh/seven-green-checks-and-a-team-page-that-named-its-judges-57en</guid>
      <description>&lt;p&gt;Shipshape, the hackathon portal I built for DOGFOOD 2026, gives every team a history panel on its team page and submission page: who joined, who edited the project, when it went in. Once judging started, that panel would also have shown the team lines like these (the real log format; the names come from the test I wrote afterwards):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Jules Judge submitted a score for "Quiet Hours"&lt;/p&gt;

&lt;p&gt;Jules Judge declared a conflict with "Quiet Hours": I mentored them&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Only that team's members can open those pages. So the people being judged could read who was reviewing them, when each judge scored, whether a judge changed a submitted score, and a judge's private reason for stepping aside (its first 120 characters). Role isolation was the part of the portal I had tested hardest. None of those tests opened a team page, and none of the seven checks in the organizers' acceptance checker does either.&lt;/p&gt;

&lt;p&gt;Teams submit projects, judges score them on a rubric, the scores are normalized so one harsh judge cannot sink a project, and the public can vote. It is Django 5.2 on SQLite. The brief came in four tiers (T1 teams and submissions, T2 judging, T3 community voting and comments, T4 an API, webhooks, signed records and more) under hard rules: 72 hours, and &lt;code&gt;docker compose up&lt;/code&gt; must bring up a seeded portal with the network off. The organizers supply the acceptance checker and a &lt;code&gt;fixtures.json&lt;/code&gt; every portal must load. The fixture has traps: 41 project records although the brief says "forty" (one a resubmission three minutes before the close), 30 judges, 126 scores, and a judge who gave everything a 4.&lt;/p&gt;

&lt;p&gt;This is the list of things I believed while building it, grouped by where they broke rather than when.&lt;/p&gt;

&lt;h2&gt;
  
  
  I thought the features were the hard part
&lt;/h2&gt;

&lt;p&gt;I read the brief as a list of features until I put "deadline actually stops submissions" next to the check that tests it. The checker POSTs &lt;code&gt;{"title": "dogfood-late-submission-probe", "summary": "probe"}&lt;/code&gt; to a closed event and passes on any status from 400 to 499; the spec says it does not inspect why you refused. So a 404 would pass. So would a CSRF failure, or a complaint about the missing fields.&lt;/p&gt;

&lt;p&gt;A portal could pass that check for the wrong reason, so the order of checks is the design, from the view's first version: signed in (401), event exists (404), JSON from this site (415, a 403 &lt;code&gt;cross_origin&lt;/code&gt;, or 400), then the deadline (403 &lt;code&gt;submissions_closed&lt;/code&gt;), and only then the team (403 &lt;code&gt;no_team&lt;/code&gt;) and the fields (400).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;read_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# The deadline comes before anything else about the payload: a late
# request is refused as late, whatever it contains.
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;assert_accepting_edits&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;WindowError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;record_refusal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;save the project&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deadline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;iso_utc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submissions_close_at&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ApiError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;403&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no_team&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Join or start a team for this event before submitting.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The test runs with Django's CSRF enforcement switched on, to prove the 403 is the deadline and not a missing token. Every write path re-reads the event row inside its transaction and checks the server clock. The deadline instant itself counts as closed. A refusal is logged after the rollback, so its audit line survives.&lt;/p&gt;

&lt;p&gt;The other sentence that was harder than it read was "with the network off", which for two days I read as a rule about runtime, until another machine ran my &lt;code&gt;FROM python:3.12-slim&lt;/code&gt; build and it went looking for downloads. The base image and the wheels now live in the repo, built &lt;code&gt;FROM scratch&lt;/code&gt; and installed &lt;code&gt;--no-index&lt;/code&gt;. I picked bzip2 even though its archive is 36.9 MB to xz's 27.1 MB, because Docker unpacks xz by calling an external &lt;code&gt;xz&lt;/code&gt; program wherever the Docker engine runs, but unpacks bzip2 itself, with Go's standard library. An offline Docker-in-Docker box then caught compose trying to pull the image from Docker Hub before building it; &lt;code&gt;pull_policy: never&lt;/code&gt; fixed that.&lt;/p&gt;

&lt;h2&gt;
  
  
  I thought a z-score would fix harsh judges
&lt;/h2&gt;

&lt;p&gt;In the reference example that came with the judging brief, a judge called J-harsh averages 1.71. Fixture judge &lt;code&gt;jdg_02&lt;/code&gt; averages 4.22. A 3 from the first is praise; from the second, a complaint. The usual fix is a z-score: how far a mark sits from that judge's own average, in units of that judge's own spread. A project's score is the mean of its judges' z-scores.&lt;/p&gt;

&lt;p&gt;On the fixture, that formula breaks in two places. With exactly two scores, a plain z-score is always plus or minus 0.7071, because the gap between the marks cancels out of the formula. A judge who gave 3.0 and 3.1 speaks as loudly as one who gave 1 and 5. Seven fixture judges have exactly two counted scores, so plain z-scores would turn all 14 of their marks into full-strength votes. And &lt;code&gt;jdg_07&lt;/code&gt;, the judge who gave everything a 4, has zero spread: a division by zero.&lt;/p&gt;

&lt;p&gt;The fix is shrinkage. Each judge starts with kappa imaginary projects of population-typical spread, and their own marks pull them away from it as they pile up:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;sigma_j^2 = ((n_j - 1) * s_j^2 + kappa * sigma_pop^2) / (n_j - 1 + kappa)&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;With kappa at its default of 5, the two-score judges separate by their actual gap: 0.277 for a third of a point, 0.534 for two thirds, 0.757 for a full point. That last one is above 0.707, so shrinkage can push a z-score up as well as down: when a judge's own spread is wider than the population's (0.65), shrinkage pulls it toward 0.65 and the z-score rises. And every z that &lt;code&gt;jdg_07&lt;/code&gt; produces comes out exactly 0, a neutral vote instead of a crash.&lt;/p&gt;

&lt;p&gt;My first test of this used kappa 0.001 as "plain" and failed with &lt;code&gt;AssertionError: 0.6214804438775124 not greater than 0.7&lt;/code&gt;. The test was wrong, not the engine: even a thousandth of an imaginary project pulls a nearly flat judge toward the population (0.7071 at kappa 0, 0.6215 at 0.001). The test now pins 0.7071 at kappa 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  I thought plain z-scores were close enough
&lt;/h2&gt;

&lt;p&gt;The reference picked kappa 5 with no evidence, so I measured: Spearman rank correlation with a hidden true quality (1.0 means the same order), 1,000 simulated events per row, seed 2026, three reviews per project. Condensed from &lt;code&gt;python src/judging/engine.py --trials 1000&lt;/code&gt;, which gives the same numbers when rerun:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                               raw    plain z  kappa 5  kappa 25  kappa 5 beats raw
reference judges, 12 projects  0.766  0.838    0.841    0.841     75% of events
reference judges, 40 projects  0.809  0.891    0.890    0.889     98%
fixture-like, 30 judges        0.802  0.812    0.830    0.832     69%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The "plain z" column is computed at kappa 0.01, so a judge with no spread does not divide by zero; kappa 0 gives the same column to three decimals.&lt;/p&gt;

&lt;p&gt;With the reference's judges, most of the win is just centring each judge on their own mean, and any kappa from 2 to 25 lands within about 0.003 of plain z. The third row is shaped like the real fixture, 30 judges with about four whole-number scores each, and there plain z barely moves: 0.802 to 0.812. Rerun at true plain z (kappa 0), it beats the raw average in only 55% of those events (56% at the kappa 0.01 the table uses), close to a coin flip, where kappa 5 wins 69%. So plain z-scores went.&lt;/p&gt;

&lt;p&gt;Kappa 5 still fails to beat the raw average in about one fixture-like event in three, so the results page counts informative reviews (a review counts only if its judge has two or more scores with some spread) and flags projects ranked on fewer than two. Three fixture judges carry no signal, 5 of the 122 counted reviews, and one project is flagged: Small Relay. Normalization moves 33 of 40 projects, median 2 places. Dry Harbour climbs from 29th to 8th: the judge who dragged it down scored nothing else, so that lone mark normalizes to neutral, and most of its other judges marked it above their own averages (&lt;code&gt;jdg_26&lt;/code&gt; gave 4.67 against a 3.70 average).&lt;/p&gt;

&lt;h2&gt;
  
  
  I thought my rankings were deterministic
&lt;/h2&gt;

&lt;p&gt;JUDGING.md once promised that ties were broken by raw average, then project id, "so the order is always deterministic". It quoted the fixture's biggest moves from my Windows machine: Glass Beacon up 10 places, Paper Anchor down 10. Before handing judging over, I rechecked every documented figure against a fresh seed, and that was the first time the proof ran inside a Linux container. It said up 8 and down 9.&lt;/p&gt;

&lt;p&gt;Three projects average exactly 3.50, and the old code handed out positions, not ranks, so the tie went to whichever float came out a hair larger:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# before: positions 1, 2, 3, even for equal averages
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;_rank&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reviewed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;z_mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
    &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;position&lt;/span&gt;

&lt;span class="c1"&gt;# after: competition ranking (1, 2, 2, 4), values compared at 9 decimal places
&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;value&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Raw average 3.50&lt;/th&gt;
&lt;th&gt;Raw rank, Windows&lt;/th&gt;
&lt;th&gt;Raw rank, container&lt;/th&gt;
&lt;th&gt;Raw rank, fixed&lt;/th&gt;
&lt;th&gt;Normalized rank&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Glass Beacon&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Open Kiln&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Paper Anchor&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;21&lt;/td&gt;
&lt;td&gt;19&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The fix was competition ranking at nine decimals, a fixed score read order and &lt;code&gt;test_equal_scores_share_a_rank_whatever_order_they_were_summed_in&lt;/code&gt;, which took the suite from 125 tests to 126. The rebuilt container and a fresh local seed then printed the proof identically.&lt;/p&gt;

&lt;p&gt;My tests could not see it because they all ran on one machine. Working the arithmetic again, I think my docs also blame the wrong thing: they say "summation order" between two databases, but the weighted sum always walks the criteria in a fixed order. The likely culprit is Python:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python 3.11, each criterion weighted 1/3
  marks (3, 4, 4) sum to 3.666666666666666
  marks (3, 3, 5) sum to 3.6666666666666665
  Glass Beacon's raw mean: 3.4999999999999996
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Windows venv runs 3.11, so Glass Beacon sorts last of the three despite the best z-score. The container runs 3.12.14, whose &lt;code&gt;sum()&lt;/code&gt; switched to a more accurate algorithm: all three are exactly 3.5, the old key falls through to the z-score, and Glass Beacon comes first. Both orders I saw follow from that.&lt;/p&gt;

&lt;p&gt;A day later another test failed only in the Linux image, with &lt;code&gt;AssertionError: 3.0 == 3.0&lt;/code&gt;. It compared whichever of two tied projects came first, both at 3.0 before and after the change it checked, so it had never tested what it claimed.&lt;/p&gt;

&lt;h2&gt;
  
  
  I thought quadratic voting would stop a loud minority
&lt;/h2&gt;

&lt;p&gt;The community vote was going to be quadratic, the textbook answer to a loud minority: n votes on one project cost n squared credits. My first simulation disagreed. A fifth of the voters backing one weak project won 100% of events, worse than one person, one vote at 96%, because a bloc member puts 5 votes on the target while a sincere voter's favourite gets 2 or 3. So I added a ceiling of 3 votes per project per ballot and reran with 1,000 events per row, 40 projects and 300 voters. "Seen" is how many projects a voter actually looks at:&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;Best project wins, nobody gaming, 10 seen&lt;/th&gt;
&lt;th&gt;Same, 20 seen&lt;/th&gt;
&lt;th&gt;20% bloc wins, 10 seen&lt;/th&gt;
&lt;th&gt;10% bloc, 3 identities each, 10 seen&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One person, one vote&lt;/td&gt;
&lt;td&gt;79%&lt;/td&gt;
&lt;td&gt;90%&lt;/td&gt;
&lt;td&gt;95%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quadratic, no ceiling&lt;/td&gt;
&lt;td&gt;64%&lt;/td&gt;
&lt;td&gt;84%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quadratic, ceiling of 3&lt;/td&gt;
&lt;td&gt;48%&lt;/td&gt;
&lt;td&gt;82%&lt;/td&gt;
&lt;td&gt;57%&lt;/td&gt;
&lt;td&gt;100%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At 20 projects seen, the ceiling held a 20% bloc to 0% wins where the uncapped version let it win 40%. The cost is in the first column: when nobody games the vote, one person, one vote crowns the best project (the highest hidden quality) 79% of the time at 10 seen; my capped method manages 48%. It orders the whole field better (rank correlation 0.984 against 0.926), but the winner is the number people remember.&lt;/p&gt;

&lt;p&gt;The last column is the same for every method: a bloc whose members each hold three identities wins every event. Past that point the work is in who gets a ballot: three access modes (an open link, email links stored only as hashes, or signed-in accounts) and a detector that flags bursts, shared devices and aliased addresses.&lt;/p&gt;

&lt;p&gt;Ballot order mattered too. With one shared order and voters looking at about 10 projects, 94% of the top three came from the first ten listed, and the best project won 13% of the time. A stable shuffle per voter, seeded by an HMAC of who the voter is and never stored, lifted that to 51%.&lt;/p&gt;

&lt;h2&gt;
  
  
  I thought isolation meant guarding the judges' endpoints
&lt;/h2&gt;

&lt;p&gt;By the time the T3 brief repeated the isolation requirement, judge queries started from the signed-in judge, anything outside a judge's assignments answered 404 like a missing id, and a request for a peer's scores was refused with 403 before any lookup, tested with three spellings of another judge and one id that belongs to nobody.&lt;/p&gt;

&lt;p&gt;This time the question was not what a judge can reach, but where judging data ends up. A grep for history in the templates, then a list of judging log calls that pass &lt;code&gt;team=&lt;/code&gt;, found it. In T1, the team history panel read &lt;code&gt;Activity.objects.filter(event=event, team=team)&lt;/code&gt; with no filter on the kind of entry. In T2, five judging actions (assigning by hand, unassigning, submitting a score, changing one, declaring a conflict) started tagging their audit lines with the team. Neither change was wrong alone, but together they sent T2's judging lines into a panel T1 had built to show teammates who joined and who edited.&lt;/p&gt;

&lt;p&gt;Every isolation test probed judging endpoints, and so do the checker's; this leak ran from judges to the team through a generic audit log. The seed imports the fixture's 126 scores without logging anything, which is likely why walkthroughs on seeded data showed nothing odd. The fix is one helper both pages must go through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Entries a team must never see in its own history: who judges their project,
# when a judge scored it, a judge's conflict and their reason, and anything
# about the community vote. They stay in the organizers' activity log.
&lt;/span&gt;&lt;span class="n"&gt;STAFF_ONLY_VERBS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judge.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;judging.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voting.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;team_history&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Activity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;STAFF_ONLY_VERBS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verb__startswith&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_related&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;actor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Its test plants a conflict reason in capitals, &lt;code&gt;I MENTORED THEM&lt;/code&gt;, and asserts it reaches neither team page but still reaches the organizers' log. The weakness was built in from the start: a deny list keyed on how verbs are spelled.&lt;/p&gt;

&lt;p&gt;"Hidden looks exactly like missing" failed once more the next day, in a threat-model test comparing the API's answer for a draft with its answer for an id that does not exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AssertionError: {'error': 'not_found', 'detail': 'No such project, or nothing you can see.'} != {'error': 'not_found', 'detail': 'Nothing here, or nothing you can see.'}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two polite 404s, one sentence apart: enough for anyone counting up through project ids to tell a hidden draft from an empty slot. The hidden case now raises the same &lt;code&gt;Http404&lt;/code&gt; as a missing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Some of my tests pass because the attack still works
&lt;/h2&gt;

&lt;p&gt;When the tests moved out of the Django project, running the suite from inside &lt;code&gt;src/&lt;/code&gt; printed &lt;code&gt;Ran 0 tests in 0.000s OK&lt;/code&gt;: a pass that touched nothing. The test runner now defaults to the &lt;code&gt;tests/&lt;/code&gt; folder, so it cannot quietly find zero tests.&lt;/p&gt;

&lt;p&gt;The threat model, one of the two bonus challenges I took on, lists 43 attacks: 28 stopped, 3 capped or flagged, 1 logged only and 11 open. The doc and the tests share attack ids, and four of the open attacks have tests with &lt;code&gt;_gap_&lt;/code&gt; in the name that pass because the attack works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_SY5_gap_a_fresh_browser_gets_a_fresh_ballot_on_an_open_link&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voting:ballot_link&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;link_token&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# a private window, or cookies cleared
&lt;/span&gt;        &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;browser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;voting:api_ballot&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slug&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;?link=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;link_token&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                     &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;votes&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}}),&lt;/span&gt; &lt;span class="n"&gt;content_type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Ballot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;objects&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Close that hole and the test goes red, which forces a change to the doc that admits it. A suite that records only what works cannot tell you when your list of weaknesses has gone stale.&lt;/p&gt;

&lt;p&gt;Writing the threat model turned up four more holes besides the 404 wording. A passed deadline could be moved later under scores already given (now refused once a score or ballot exists). Submitted projects were public before the deadline, handing early ideas to teams still working (now private by default). One inbox could cast several ballots (more on that below). And nothing looked for a judge boosting a friend.&lt;/p&gt;

&lt;p&gt;For that last one I flag a project when one judge's normalized score sits far from the rest of its panel. I first picked a threshold of 2.0, because only 4 of 106 honest gaps on the fixture reached it. Then the simulation ran: at 2.0 the flag caught only 25% of single-judge boosts. The sweep, over 1,000 simulated events:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;threshold   1 colluder caught   2 colluders caught   honest projects flagged
1.0         86%                 84%                  29.7%
1.25        75%                 70%                  14.5%
1.5         58%                 56%                  5.8%
1.75        40%                 40%                  2.1%
2.0         25%                 28%                  0.6%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I settled on 1.5: it catches 58% of single boosts and flags 5.8% of honest projects, two or three in every 40, a list short enough for an organizer to read. On the real fixture the flag fires far more often: on 8 of the 32 projects it can check, against the simulated 5.8%, and the 8 projects with only two reviews can never be checked. Two colluders on a three-judge panel still get a bottom-half project into the top three in 24% of events. The cheapest defence was already there: random assignment puts a given judge on a given friend's panel about one time in ten.&lt;/p&gt;

&lt;p&gt;On its first run, my HTTP attack probe reported two attacks as working. Both were bugs in the probe, and the doc still lists them. Once fixed, the probe ran 14 attacks and the portal stopped all 14.&lt;/p&gt;

&lt;h2&gt;
  
  
  I thought an email address was a voter
&lt;/h2&gt;

&lt;p&gt;Until 29 hours into the build (for the first 20 hours that email ballots existed), an email ballot belonged to the address as typed, under a unique constraint on (event, email). My own anti-abuse detector knew better: its &lt;code&gt;canonical_email&lt;/code&gt; drops &lt;code&gt;+tags&lt;/code&gt; and Gmail dots, so &lt;code&gt;ada+2@x.org&lt;/code&gt; is &lt;code&gt;ada@x.org&lt;/code&gt;. But the ballot, the per-address link limit and the own-team check all used the typed address, so one inbox could hold one ballot per spelling, merely flagged. My docs even claimed &lt;code&gt;+tags&lt;/code&gt; were dropped everywhere.&lt;/p&gt;

&lt;p&gt;The fix touched five code paths and zero migrations. &lt;code&gt;Ballot.email&lt;/code&gt; started holding a different kind of value under the same constraint name, &lt;code&gt;one_ballot_per_email&lt;/code&gt;, with a compatibility clause in place of a data migration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;voter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;kind&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;Ballot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Kind&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EMAIL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Ballots from before inboxes were used keep the address as typed.
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;qs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email__in&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;inbox&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;voter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;voter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replaying the change shows what that shortcut costs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A ballot saved before the change as &lt;code&gt;kim+1@x.org&lt;/code&gt; is missed when the inbox returns as &lt;code&gt;kim+2@x.org&lt;/code&gt;, so one inbox gets two ballots. Only databases that held email ballots before the change are exposed; a fresh seed has none.&lt;/li&gt;
&lt;li&gt;The archive importer writes addresses raw, so &lt;code&gt;lee+1@x.org&lt;/code&gt; and &lt;code&gt;lee+2@x.org&lt;/code&gt; import as two ballots.&lt;/li&gt;
&lt;li&gt;With no stored inbox, the own-team check runs a leading-wildcard &lt;code&gt;LIKE&lt;/code&gt; and filters in Python. With 50 Gmail accounts in the table, all 50 were loaded for one check.&lt;/li&gt;
&lt;li&gt;The typed spelling is lost from the ballot (&lt;code&gt;Mal.Lory@googlemail.com&lt;/code&gt; is stored as &lt;code&gt;mallory@gmail.com&lt;/code&gt;; only the email-link row keeps it).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What I would write now: keep &lt;code&gt;email_as_typed&lt;/code&gt;, add an indexed &lt;code&gt;inbox&lt;/code&gt; column computed at write time, put the unique constraint on (event, inbox), and give &lt;code&gt;EmailPass&lt;/code&gt; and &lt;code&gt;User&lt;/code&gt; the same column. The importer could not get around a rule the database enforces. The &lt;code&gt;RunPython&lt;/code&gt; backfill would fail on any inbox already holding two ballots, and that failure is useful: it forces the "which ballot wins" decision my compatibility clause skips. One cost stays: on a provider where &lt;code&gt;a+b@&lt;/code&gt; and &lt;code&gt;a@&lt;/code&gt; are different people, they share a ballot.&lt;/p&gt;

&lt;p&gt;The second redo is the audit log. &lt;code&gt;Activity&lt;/code&gt; serves three audiences (the team, organizers, platform admins), and who may read a row is decided by its verb's spelling: of 25 verbs logged with a team, 5 are hidden only by their prefix. I would add an &lt;code&gt;audience&lt;/code&gt; column, set when the line is written and defaulting to staff, so a verb nobody thought about stays hidden instead of showing up on a team page.&lt;/p&gt;

&lt;p&gt;One choice I would keep: nothing derived is stored. The whole fixture ranking is recomputed per request in about 25 to 30 ms and 6 queries on my machine. So when I noticed, about 32 hours in, that organizers had no button to publish the judges' results, the fix was three columns on the judging settings (&lt;code&gt;results_published_at&lt;/code&gt;, &lt;code&gt;results_published_by&lt;/code&gt;, &lt;code&gt;share_feedback&lt;/code&gt;) and no stored ranking to freeze: the public page runs the same calculation as the organizers' one.&lt;/p&gt;

&lt;h2&gt;
  
  
  One rubric per event, on purpose
&lt;/h2&gt;

&lt;p&gt;The reference design allowed a rubric per track. Shipshape has one per event, and I would make that cut again. A z-score compares a judge's mark with that judge's own mean and spread, which only means something on one scale. In the fixture, 7 of the 30 judges scored in two tracks, giving 50 of the 126 scores, including &lt;code&gt;jdg_24&lt;/code&gt; and &lt;code&gt;jdg_26&lt;/code&gt;, the two judges whose session cookies &lt;code&gt;.dogfood.toml&lt;/code&gt; hands the checker. Per-track rubrics would have put 40% of the scores on two scales inside one judge's z-score. Organizers keep weights that stay editable after scoring starts, which is cheap because totals are recomputed on every request.&lt;/p&gt;

&lt;p&gt;I also skipped reliability weighting of judges, because early scores would decide how much later ones count. I did not attempt the pairwise judging bonus, and I will not dress that up as a principle: I took on the threat model and API-first instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  I thought I should claim every tier I built
&lt;/h2&gt;

&lt;p&gt;The checker has seven checks, three for T1 and four for T2, and its rule for a verified tier fits in three lines (plus a loop that drops any tier above one that failed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;verified&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TIERS&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;checks&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tier&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There are no T3 or T4 checks, so &lt;code&gt;any&lt;/code&gt; is false for them and no portal can get them verified. The spec says overclaiming is the one thing that costs points. I built all four tiers, and for about a day and a half my report ended &lt;code&gt;claimed T1 T2 T3 T4, verified T1 T2&lt;/code&gt;; my first commit carried it. Then I split the claims: &lt;code&gt;.dogfood.toml&lt;/code&gt; claims what a program can confirm, and the README claims T3, T4 and the two bonuses next to the tests behind them. All seven checks pass, and the report now ends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T2  judge cannot see peer scores ...... PASS
T2  participant blocked ............... PASS
T2  csv export works .................. PASS

claimed T1 T2, verified T1 T2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I held the API docs to the same rule. Checking every &lt;code&gt;/api/v1/&lt;/code&gt; answer against its OpenAPI schema during the tests found five places the docs were wrong, and showed my 307 tests reached only 73 of 134 endpoints, so a run now fails if any endpoint goes unexercised. A Schemathesis run reached about 1,000 requests with no server error before I stopped it, and the docs say that rather than claiming a pass.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the failures actually were
&lt;/h2&gt;

&lt;p&gt;The formulas were the part I could prove fastest, because a simulation has no stake in my being right. The method I shipped beats the raw average in 69% of fixture-like events; plain z-scores do it in 55%. I would rather print both numbers than print a ranking and call it fair. Every serious failure here happened somewhere else: the wording of a 404, which Python summed the marks, which column a ballot was keyed on, where an audit line ended up.&lt;/p&gt;

&lt;p&gt;Checking facts for this post, I seeded a fresh fixture, set the results date three days ahead, gave "Best in show" to Glass Signal and opened the winning team's page as one of its members. Awards stay private until the results date, so organizers can decide winners before the ceremony, and the public awards list correctly returned 0 rows. The team page returned 200 with "Best in show" in its history, from this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;log_activity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;award.given&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gave “&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;” to “&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;”&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;project&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;project&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;award.&lt;/code&gt; is not on the deny list. No test covers it, and it is still there as I write this: the opening scene again, with a different verb. Adding &lt;code&gt;award.&lt;/code&gt; to the list would close this one. The &lt;code&gt;audience&lt;/code&gt; column would have kept it closed without anyone having to remember.&lt;/p&gt;

&lt;h3&gt;
  
  
  Try it
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/manusingh090/Shipshape.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Shipshape
docker compose up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No network is needed. Open &lt;a href="http://localhost:8080" rel="noopener noreferrer"&gt;http://localhost:8080&lt;/a&gt;, choose Sign in and press one of the Be buttons: Be Rosa is the organizer, Be Priya a team captain, Be Tom a participant with no team, Be Ada and Be Diego are judges. The tables come from &lt;code&gt;python src/judging/engine.py --trials 1000&lt;/code&gt; (add &lt;code&gt;--collusion&lt;/code&gt; for the collusion simulation) and &lt;code&gt;python src/voting/method.py --trials 1000&lt;/code&gt; (add &lt;code&gt;--seen 20&lt;/code&gt; for the 20-seen column and &lt;code&gt;--order&lt;/code&gt; for ballot order), which need only plain Python.&lt;/p&gt;

&lt;h3&gt;
  
  
  References
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The DOGFOOD 2026 brief and acceptance checker, in the repo as &lt;code&gt;tests/acceptance/spec.md&lt;/code&gt; and &lt;code&gt;tests/acceptance/run.py&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;JUDGING.md in the repo: section 4 (normalization and the Monte Carlo proof), section 10 (the vote simulations), section 11 (the threat model)&lt;/li&gt;
&lt;li&gt;Schemathesis: &lt;a href="https://schemathesis.readthedocs.io/" rel="noopener noreferrer"&gt;https://schemathesis.readthedocs.io/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Shipshape: &lt;a href="https://github.com/manusingh090/Shipshape" rel="noopener noreferrer"&gt;https://github.com/manusingh090/Shipshape&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Google Slides : &lt;a href="https://docs.google.com/presentation/d/1z8AnsudKyrrzO9azSSoI51wdYGTVh7hNFrpsKxTI_Eg/edit?usp=sharing" rel="noopener noreferrer"&gt;https://docs.google.com/presentation/d/1z8AnsudKyrrzO9azSSoI51wdYGTVh7hNFrpsKxTI_Eg/edit?usp=sharing&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>django</category>
      <category>python</category>
      <category>showdev</category>
      <category>hackathonraptors</category>
    </item>
  </channel>
</rss>
