<?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: Rajpriyan S</title>
    <description>The latest articles on DEV Community by Rajpriyan S (@rajpriyan_s).</description>
    <link>https://dev.to/rajpriyan_s</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%2F4100339%2F563f4439-d2af-4aeb-b00e-05436d9b1140.jpg</url>
      <title>DEV Community: Rajpriyan S</title>
      <link>https://dev.to/rajpriyan_s</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rajpriyan_s"/>
    <language>en</language>
    <item>
      <title>I Built the Platform That Judges Hackathons in 72 Hours. The Hardest Bugs Were About Evidence, Not Features.</title>
      <dc:creator>Rajpriyan S</dc:creator>
      <pubDate>Fri, 02 Oct 2026 10:03:41 +0000</pubDate>
      <link>https://dev.to/rajpriyan_s/i-built-the-platform-that-judges-hackathons-in-72-hours-the-hardest-bugs-were-about-evidence-not-ph8</link>
      <guid>https://dev.to/rajpriyan_s/i-built-the-platform-that-judges-hackathons-in-72-hours-the-hardest-bugs-were-about-evidence-not-ph8</guid>
      <description>&lt;p&gt;A hackathon platform has one job that actually matters. When it says &lt;em&gt;"this project came 14th, and here is why,"&lt;/em&gt; that sentence has to still be true next week.&lt;/p&gt;

&lt;p&gt;That sounds obvious. It is not. I built &lt;strong&gt;Verdict Ledger&lt;/strong&gt; for &lt;a href="https://dogfoodhack.com/" rel="noopener noreferrer"&gt;DOGFOOD 2026&lt;/a&gt;, the Hackathon Raptors event where everyone builds the same submission-and-judging portal and the organisers plan to self-host the winner. Most of my hard problems were not features. They were about &lt;strong&gt;evidence&lt;/strong&gt;: scores that change after a ranking is computed, explanations that quietly rewrite themselves, audit logs that fail to chain, and judge data that leaks through a page nobody thought to protect.&lt;/p&gt;

&lt;p&gt;This is the write-up of what actually broke, what I built to stop it, and where my own claims stop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The short version&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Claimed &lt;strong&gt;T1 + T2&lt;/strong&gt;. Official checker: &lt;strong&gt;7/7 PASS&lt;/strong&gt;. T3 and T4 deliberately &lt;em&gt;not&lt;/em&gt; claimed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;273 tests pass&lt;/strong&gt;, none skipped, in about 90 seconds, with only the standard library's &lt;code&gt;unittest&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A normalization method I can defend in a table: it moves &lt;strong&gt;32 of 41&lt;/strong&gt; fixture projects, and one project jumps &lt;strong&gt;17 places&lt;/strong&gt; because a single judge's lone score was neutralised.&lt;/li&gt;
&lt;li&gt;Publishing a ranking built on stale scores returns &lt;strong&gt;HTTP 409&lt;/strong&gt;. Explanations read from an immutable snapshot.&lt;/li&gt;
&lt;li&gt;Every bug below was found by an audit pass over my own build, and each one has a regression test that I &lt;strong&gt;watched fail&lt;/strong&gt; before trusting it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/rajpriyanid-creator/dogfood" rel="noopener noreferrer"&gt;github.com/rajpriyanid-creator/dogfood&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The brief looks like CRUD. It is not.
&lt;/h2&gt;

&lt;p&gt;On paper the product is simple: auth, teams, submissions, a gallery, a judge console. But the brief describes an event as a &lt;strong&gt;ten-stage pipeline&lt;/strong&gt;, each stage with its own state feeding the next, and it names the stage where things break: normalization.&lt;/p&gt;

&lt;p&gt;Two sentences in the spec set the real bar:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A judge must never see another judge's scores. If this only works because the UI hides a button, it does not work.&lt;/p&gt;

&lt;p&gt;"We averaged the scores" is an answer, and it is a weak one. Tell us what you did about the judge who marks everything a 3.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Everything below is my attempt to answer those two sentences honestly.&lt;/p&gt;




&lt;h2&gt;
  
  
  Decision zero: pick a tier and tell the truth about it
&lt;/h2&gt;

&lt;p&gt;I claimed &lt;strong&gt;T1 and T2&lt;/strong&gt; and wrote, in the README, that T3 (voting, comments) and T4 (REST API, certificates) are out of scope. The scoring says a clean T2 beats a broken T4, and overclaiming costs more than the tier was worth.&lt;/p&gt;

&lt;p&gt;The part I care most about is &lt;em&gt;how&lt;/em&gt; the README reports status. Every feature gets three columns instead of one tick:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Area&lt;/th&gt;
&lt;th&gt;Implemented&lt;/th&gt;
&lt;th&gt;Tested&lt;/th&gt;
&lt;th&gt;Verified by official checker&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Judge cannot read peer scores&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (1 check)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deadline enforced server-side&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes (1 check)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hash-chained audit log&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;not covered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Normalization freshness gate&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;not covered&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;T3 (voting, comments)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;no&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;td&gt;-&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;"Implemented" and "verified" are different claims. The official checker exercises only &lt;strong&gt;seven behaviours&lt;/strong&gt;. Most of what I built, including what I am proudest of, is &lt;strong&gt;not covered by it&lt;/strong&gt;, and the README says so out loud. The &lt;code&gt;votes&lt;/code&gt; and &lt;code&gt;comments&lt;/code&gt; tables exist in the schema as a starting point, but nothing reads or writes them, and &lt;code&gt;SECURITY.md&lt;/code&gt; states that "this feature doesn't exist yet" instead of describing an attack surface that is not there.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: one process, one file
&lt;/h2&gt;

&lt;p&gt;One Flask process serves server-rendered HTML for humans and a JSON API under &lt;code&gt;/api&lt;/code&gt; for the checker. One SQLite file is the entire persistence layer. No queue, no cache, no frontend build step, no client-side JavaScript.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;browser / curl ──▶ Flask (app.py: routes)
                      │
                      ├─ core.py           identity + role-guard decorators
                      ├─ auth.py           sessions, PBKDF2 passwords, token hashing
                      ├─ events.py         the one timestamp → state mapping
                      ├─ assignment.py     deterministic judge assignment
                      ├─ normalization.py  pure maths, no DB dependency
                      ├─ audit.py          hash-chained log
                      └─ db.py ──▶ SQLite (named Docker volume)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The data flows in one strict direction, and I wrote it down because every bug below is a break in this chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AUTHORIZATION → SUBMISSION → JUDGING → NORMALIZATION
              → HISTORICAL EVIDENCE → FRESHNESS CHECK → PUBLISH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The lifecycle is one function
&lt;/h3&gt;

&lt;p&gt;Deadline bugs happen when every route does its own date comparison. So &lt;code&gt;events.py&lt;/code&gt; maps timestamps to &lt;strong&gt;exactly one state&lt;/strong&gt;, and every permission is a lookup against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UPCOMING → OPEN → SUBMISSIONS_CLOSED → JUDGING → PUBLISHED   (ARCHIVED: sticky)
&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;Action&lt;/th&gt;
&lt;th&gt;Allowed while&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;create/edit/submit project, create/join team&lt;/td&gt;
&lt;td&gt;OPEN&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;score a project&lt;/td&gt;
&lt;td&gt;SUBMISSIONS_CLOSED or JUDGING&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;run normalization&lt;/td&gt;
&lt;td&gt;SUBMISSIONS_CLOSED or JUDGING&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;publish&lt;/td&gt;
&lt;td&gt;past OPEN, and a normalization run exists&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The status is always computed from the &lt;strong&gt;server's clock&lt;/strong&gt;, never from a client flag. A test sends spoofed &lt;code&gt;event_status&lt;/code&gt; and &lt;code&gt;submissions_close&lt;/code&gt; fields and confirms they are ignored.&lt;/p&gt;




&lt;h2&gt;
  
  
  Role isolation: if I can &lt;code&gt;curl&lt;/code&gt; it, it is broken
&lt;/h2&gt;

&lt;p&gt;The brief's isolation matrix is blunt: a judge reads their own scores and nothing else. I decided the permission model explicitly instead of inheriting defaults:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;th&gt;Can do&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;visitor&lt;/td&gt;
&lt;td&gt;browse the gallery and submitted projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;participant&lt;/td&gt;
&lt;td&gt;create/join a team, create/edit/submit projects for their own team&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;judge&lt;/td&gt;
&lt;td&gt;read own scores, review projects assigned to them&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;organizer&lt;/td&gt;
&lt;td&gt;everything under &lt;code&gt;/organizer&lt;/code&gt;, CSV export&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;admin&lt;/td&gt;
&lt;td&gt;organizer powers. &lt;strong&gt;Not&lt;/strong&gt; a participant or judge.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The admin decision.&lt;/strong&gt; It is tempting to make admin a superuser. I made admin &lt;em&gt;organiser powers only&lt;/em&gt;. An account that can both administer an event and score projects in it is exactly the conflict of interest a judging platform exists to prevent. A test asserts that admin gets 403 on &lt;code&gt;/team&lt;/code&gt;, &lt;code&gt;/projects/new&lt;/code&gt;, &lt;code&gt;/judge&lt;/code&gt; and &lt;code&gt;/api/judge/scores&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Identity comes from one place.&lt;/strong&gt; &lt;code&gt;core.py::current_identity&lt;/code&gt; resolves the requester from their session token and nothing else. No code path reads a role, user id, judge id or team id from the query string, form body, JSON body or header and trusts it. Where a request &lt;em&gt;names&lt;/em&gt; an id, such as &lt;code&gt;?judge=jdg_01&lt;/code&gt;, it is either ignored or compared against the session and refused on mismatch.&lt;/p&gt;

&lt;p&gt;You can check this in 30 seconds. Log in as &lt;code&gt;judge2&lt;/code&gt; and ask for judge 1's scores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET /api/judge/scores?judge=jdg_01      →  403 Forbidden
GET /api/judges/jdg_01/scores           →  403 Forbidden   (path form, same rule)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The test that enforces it: 43 declared policies
&lt;/h3&gt;

&lt;p&gt;Rules are only as good as the next route someone adds. &lt;code&gt;tests/test_access_matrix.py&lt;/code&gt; declares a policy for &lt;strong&gt;43 method-and-route combinations&lt;/strong&gt;, then hits every one of them as every identity (anonymous, participant, judge, organizer, admin) and fails if any combination behaves differently from its declared policy. Forget to declare a new route and the build breaks.&lt;/p&gt;

&lt;p&gt;The part I got wrong at first is the oracle. A naive version asserts "refused means 401 or 403." But plenty of correct refusals share those codes: a bad password returns 401, a closed event returns 403, a judge not assigned to a project returns 403. None of those are &lt;em&gt;role&lt;/em&gt; decisions, so a test that accepts any 403 can pass for the wrong reason. The final test only accepts the role guard's own exact refusal bodies:&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;guard_refused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;401&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authentication required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;403&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forbidden&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;should_be_refused&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;who&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;allowed&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;guard_refused&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;should_be_refused&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;failures&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rule&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; as &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;who&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: got &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  A test that passed for the wrong reason
&lt;/h3&gt;

&lt;p&gt;A second, nastier version of the same lesson. My cross-team test claimed to prove that a participant cannot submit on behalf of another team. It passed. But it only passed because the fixture event happens to be closed, so the request failed on the &lt;em&gt;deadline&lt;/em&gt;, never reaching the team-spoof condition at all.&lt;/p&gt;

&lt;p&gt;Once draft creation stopped being deadline-gated, I rewrote the test to assert the real claim directly: send a hostile &lt;code&gt;team_id: "live_tm_2"&lt;/code&gt; and check that the resulting project is attached to the caller's &lt;em&gt;own&lt;/em&gt; team. A green test that is green for an unrelated reason is worse than no test, because it tells you the door is locked when you never tried the handle.&lt;/p&gt;




&lt;h2&gt;
  
  
  The judge who marks everything a 3
&lt;/h2&gt;

&lt;p&gt;The official fixture is a closed historical event: &lt;strong&gt;41 projects, 40 teams, 30 judges, 8 tracks and 126 scores&lt;/strong&gt;, deliberately built with the edge cases every evaluation system must survive. I ran my normalization against it and wrote down what the data actually looks like.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fact about the fixture&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Global mean raw score (demo weights)&lt;/td&gt;
&lt;td&gt;3.568&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Global standard deviation&lt;/td&gt;
&lt;td&gt;0.671&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judge averages&lt;/td&gt;
&lt;td&gt;from &lt;strong&gt;2.000&lt;/strong&gt; to &lt;strong&gt;4.250&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Judges with 2 or fewer reviews&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;8 of 30&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Zero-variance judges&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;3&lt;/strong&gt;: &lt;code&gt;jdg_01&lt;/code&gt; (n=1), &lt;code&gt;jdg_23&lt;/code&gt; (n=1), &lt;code&gt;jdg_07&lt;/code&gt; (n=3, all 4.0)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A judge average range of 2.25 points on a 5-point scale is the whole problem in one number. Averaging raw scores lets judge severity decide the podium.&lt;/p&gt;

&lt;h3&gt;
  
  
  The method, stated plainly
&lt;/h3&gt;

&lt;p&gt;It is a &lt;strong&gt;transparent, sample-size-shrunk location-scale heuristic inspired by empirical-Bayes reasoning&lt;/strong&gt;. It is explicitly &lt;strong&gt;not&lt;/strong&gt; full empirical Bayes: there is no hierarchical model and no fitted prior, and &lt;code&gt;k&lt;/code&gt; is fixed, not estimated. I refuse to call it more than it is, in the code and in the docs.&lt;/p&gt;

&lt;p&gt;For each review, &lt;code&gt;S = Σ(wᵢ × xᵢ)&lt;/code&gt;. Then, with &lt;code&gt;k = 3&lt;/code&gt; as a fixed design parameter, each judge's mean and variance are shrunk toward the global ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;μ*_j = (n_j·μ_j + k·μ_g) / (n_j + k)
v*_j = (n_j·v_j + k·v_g) / (n_j + k)

z = (S − μ*_j) / σ*_j
N = clip(μ_g + z·σ_g, 1, 5)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and for a judge whose &lt;em&gt;observed&lt;/em&gt; variance is exactly zero:&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;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;μ_g&lt;/span&gt;        &lt;span class="c1"&gt;# no relative signal, so the review carries no discrimination
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code is the formula, with no hidden state, which is why a rerun is reproducible by construction:&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;normalize_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;judge_stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;global_stats&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;5.0&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;judge_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zero_variance&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;global_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;judge_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shrunk_sd&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# not reachable with k &amp;gt; 0, but guarded rather than dividing by zero
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;global_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw_score&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;judge_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shrunk_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;judge_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shrunk_sd&lt;/span&gt;
    &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;global_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;global_stats&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sd&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why &lt;code&gt;k = 3&lt;/code&gt;? It is a chosen constant, not derived from the data. A judge with 5 or more reviews, the bulk of the fixture, is only lightly shrunk, while a judge with 1 or 2 reviews is pulled hard toward the global distribution. A couple of data points should not be trusted like a dozen.&lt;/p&gt;

&lt;p&gt;Two rules I made on purpose: &lt;strong&gt;missing reviews are never imputed as zero&lt;/strong&gt; (a judge's statistics use only reviews they actually submitted), and &lt;strong&gt;&lt;code&gt;n = 1&lt;/code&gt; is not a special case&lt;/strong&gt;. A single review has zero variance by definition, so it falls naturally into the zero-variance branch.&lt;/p&gt;

&lt;h3&gt;
  
  
  What it actually does to the ranking
&lt;/h3&gt;

&lt;p&gt;I ran the real &lt;code&gt;run_normalization&lt;/code&gt; over the real fixture with the demo weights (40% functionality, 35% quality, 25% innovation, which is my own demo choice, not something the fixture specifies):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Measure&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Projects whose rank changed&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;32 of 41&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Projects that moved 3 or more places&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;13&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rank correlation, raw vs normalized (Spearman)&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.927&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Top 5 overlap&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;4 of 5&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The top of the table barely changes (Iron Switch stays first), which is reassuring. The middle is where judge severity was doing damage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The biggest riser: "Dry Harbour" (&lt;code&gt;prj_07&lt;/code&gt;), rank 31 → 14.&lt;/strong&gt; Its raw average was 3.340 and its normalized average is 3.667. The reason is one review. &lt;code&gt;jdg_01&lt;/code&gt; is a judge with a single review in the whole event, and that review was a harsh 2.0 for this project. One data point cannot say whether a judge is strict or this project is weak, so it is pulled back to the global mean (3.57). The other four reviewers scored it 2.35, 3.65, 4.6 and 4.1.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The biggest faller: "Small Relay" (&lt;code&gt;prj_19&lt;/code&gt;), rank 17 → 30.&lt;/strong&gt; Its two reviews were 3.15 from &lt;code&gt;jdg_29&lt;/code&gt; and a flat 4.0 from &lt;code&gt;jdg_07&lt;/code&gt;. That 4.0 is one of three identical scores from a judge who rated everything the same, so it is also neutralised, to 3.57. The project loses the benefit of a generous score that carried no information.&lt;/p&gt;

&lt;p&gt;I am telling you about the second one on purpose. The method is doing what it says, but a 13-place drop from neutralising one flat 4.0 is exactly the kind of effect a skeptical organiser should be able to see and argue with. That is why the &lt;code&gt;explain&lt;/code&gt; page shows the raw score, the normalized score, and each judge's shrinkage.&lt;/p&gt;

&lt;h3&gt;
  
  
  The limits, written down
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;JUDGING.md&lt;/code&gt; has a "Limitations of the method" section, and I stand by every line:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;k = 3&lt;/code&gt; is fixed, not tuned.&lt;/strong&gt; A different event might warrant a different value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The zero-variance rule is a bright line.&lt;/strong&gt; A judge giving 4, 4, 4 hits the fallback, while 4, 4, 4.5 does not. It is exact equality, not a threshold.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cross-track calibration.&lt;/strong&gt; Normalization runs across the whole event, so a systematically stricter track is not adjusted separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a peer-reviewed method.&lt;/strong&gt; It is a documented, transparent heuristic, not validated against ground truth, and I claim no such validation.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The bugs an audit pass found in my own build
&lt;/h2&gt;

&lt;p&gt;After the first working version, an audit pass over my own code found real defects. I kept every one visible in the test suite as a labelled regression test, because the story of what broke is more useful than a clean-looking repo.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The audit log that could not verify itself
&lt;/h3&gt;

&lt;p&gt;The hash-chained log was supposed to be tamper-evident. In the first version, the &lt;code&gt;seq&lt;/code&gt; column was never populated, so every row had &lt;code&gt;seq = NULL&lt;/code&gt;, &lt;code&gt;ORDER BY seq&lt;/code&gt; ordered nothing in particular, and &lt;code&gt;verify_chain()&lt;/code&gt; returned &lt;code&gt;False&lt;/code&gt; as soon as a &lt;strong&gt;second&lt;/strong&gt; record existed. A tamper-evidence feature that reports tampering on a perfectly healthy log is worse than none.&lt;/p&gt;

&lt;p&gt;The fix made &lt;code&gt;seq&lt;/code&gt; an autoincrementing rowid alias. There was a second problem hiding behind it: two concurrent writers could both read "the last row" before either inserted, and both chain to the same predecessor. So the append now takes SQLite's write lock &lt;em&gt;before&lt;/em&gt; the read:&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;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BEGIN IMMEDIATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# acquire the write lock up front
&lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT hash FROM audit_events ORDER BY seq DESC LIMIT 1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;prev_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;genesis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;row_hash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;_hash_row&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prev_hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actor_role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;resource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;detail_json&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO audit_events (...) VALUES (...)&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="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rollback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. The integrity dashboard that collapsed 30 judges into one row
&lt;/h3&gt;

&lt;p&gt;The organiser integrity page used a bare &lt;code&gt;COUNT(*)&lt;/code&gt; with no &lt;code&gt;GROUP BY&lt;/code&gt;, so the whole event collapsed into &lt;strong&gt;one row&lt;/strong&gt;. A second flaw sat behind it: the query joined on &lt;code&gt;assignments&lt;/code&gt;, so a score with no assignment row simply vanished.&lt;/p&gt;

&lt;p&gt;The regression tests parse the rendered per-judge table and pin exact values, and the expected numbers are &lt;strong&gt;recomputed independently from &lt;code&gt;fixtures.json&lt;/code&gt;&lt;/strong&gt;, not copied from the app's output, so a collapsed or arbitrary result cannot pass just by containing the right judge ids somewhere on the page. When I re-added the bad query to check, &lt;strong&gt;11 of the 19 integrity tests failed&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Drafts were public by predictable id
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;/projects/&amp;lt;id&amp;gt;&lt;/code&gt; had no visibility check at all, so a draft project's detail page was fully public to anyone who guessed an id like &lt;code&gt;prj_42&lt;/code&gt;. The fix returns &lt;strong&gt;404, not 403&lt;/strong&gt;, unless the project is submitted, or the caller is on the owning team, or is an organiser. A 403 would confirm the project exists. Drafts are also excluded from the gallery and from other projects' duplicate-candidate lists.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. A score that changed weights but kept its old rubric version
&lt;/h3&gt;

&lt;p&gt;Rubrics are versioned, and each score pins the version it was given under. In an early version, updating a score with a newer rubric left the &lt;strong&gt;old&lt;/strong&gt; &lt;code&gt;rubric_version_id&lt;/code&gt; in place while recomputing &lt;code&gt;raw_weighted&lt;/code&gt; against the &lt;strong&gt;new&lt;/strong&gt; weights. The stored record claimed one rubric and used another.&lt;/p&gt;

&lt;p&gt;The key regression test publishes rubric v2 with wildly different weights (5% / 5% / 90%), then re-runs normalization and asserts that every existing score's raw value is &lt;strong&gt;unchanged&lt;/strong&gt;, because each score must still be computed with its own version's weights. If normalization used the newest rubric for everything, every raw score would change. When two versions are mixed in one result, the result's &lt;code&gt;rubric_version_id&lt;/code&gt; is &lt;code&gt;NULL&lt;/code&gt; rather than picking a proxy, and normalization refuses incompatible score ranges instead of pooling incomparable values.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Scoring that stayed open forever
&lt;/h3&gt;

&lt;p&gt;There was no server-side judging-state enforcement at all, so scores could be created or edited regardless of the event's state, including after results were published. That is where the single-function lifecycle above came from. Once judging starts, tracks, prizes, judges, rubric and assignments are also frozen.&lt;/p&gt;

&lt;h3&gt;
  
  
  6. A half-seeded database that looked fully seeded
&lt;/h3&gt;

&lt;p&gt;The seed functions each committed their own work, so a failure partway through left a partially seeded database that the old &lt;code&gt;is_seeded()&lt;/code&gt; check ("any events exist?") would mistake for complete on the next boot. The seed is now &lt;strong&gt;one transaction with a completion marker written last&lt;/strong&gt;, and a test forces a mid-seed failure to prove it.&lt;/p&gt;

&lt;h3&gt;
  
  
  7. Sessions that never ended
&lt;/h3&gt;

&lt;p&gt;Once created, a session authenticated forever, and logout only cleared the browser cookie. The same token presented through a raw header kept working. Sessions now expire after 24 hours, and logout sets &lt;code&gt;revoked_at&lt;/code&gt; on the server-side row so the token is rejected whether it arrives as a cookie or a bearer header. Revoking one session does not touch anyone else's.&lt;/p&gt;




&lt;h2&gt;
  
  
  Evidence that cannot move: the freshness gate and the snapshot
&lt;/h2&gt;

&lt;p&gt;This is the idea the project is built around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 1: publishing a ranking built on scores that no longer exist.&lt;/strong&gt; The flow is normalize → inspect → publish. If a score is edited in the gap, the stored ranking was computed from data that is gone. When a run is created, I store a SHA-256 &lt;strong&gt;fingerprint of the judging state&lt;/strong&gt;, and publish recomputes it. A mismatch returns &lt;strong&gt;HTTP 409&lt;/strong&gt; and the organiser must re-normalize.&lt;/p&gt;

&lt;p&gt;The fingerprint covers every result-affecting input: rubric versions and their criteria (key, weight, min, max), judge assignments, score records, and criterion-level values, all sorted deterministically. It deliberately does &lt;strong&gt;not&lt;/strong&gt; include timestamps or comments. The docstring states the rule I held myself to: it "must exactly match the normalization engine's actual inputs," so that a change to any result-affecting input changes the fingerprint, and a change to a non-result field does not cause a false alarm.&lt;/p&gt;

&lt;p&gt;The regression test is the demo flow: normalize, then edit one criterion value with &lt;code&gt;value = value + 0.1&lt;/code&gt;, then publish and assert a 409 whose message mentions "stale", then assert that no publication happened.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Failure mode 2: an explanation that rewrites itself.&lt;/strong&gt; If the "why did this change?" page read live &lt;code&gt;score_criteria&lt;/code&gt;, then normalizing, publishing and &lt;em&gt;then&lt;/em&gt; editing a score would silently change the explanation of a historical result. So when a run is created, the exact criterion values it consumed are copied into &lt;code&gt;normalization_run_criteria&lt;/code&gt;, and the review comment into &lt;code&gt;review_normalizations&lt;/code&gt;. &lt;code&gt;explain_result()&lt;/code&gt; reads only from those snapshots. The snapshot is write-once, and a new run creates its own.&lt;/p&gt;

&lt;p&gt;The two mechanisms are a pair. The &lt;strong&gt;fingerprint&lt;/strong&gt; stops a &lt;em&gt;new&lt;/em&gt; result being built on moved evidence, and the &lt;strong&gt;snapshot&lt;/strong&gt; stops an &lt;em&gt;old&lt;/em&gt; result's evidence being rewritten.&lt;/p&gt;




&lt;h2&gt;
  
  
  A hash-chained audit log, and an honest sentence about its limits
&lt;/h2&gt;

&lt;p&gt;Every write action, including denied access attempts, gets a row whose hash covers its own fields plus the previous row's hash. &lt;code&gt;audit.verify_chain()&lt;/code&gt; recomputes it from &lt;code&gt;"genesis"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The README calls it &lt;strong&gt;tamper-evident, not tamper-proof&lt;/strong&gt;, and &lt;code&gt;SECURITY.md&lt;/code&gt; says why. Someone with write access to the SQLite file can rewrite the whole table, recomputing every hash correctly, and the chain will verify as intact. True tamper-&lt;em&gt;proofing&lt;/em&gt; would need an external anchor, such as publishing the chain's tip hash somewhere the operator does not control. I did not build that, so I do not claim it. The chain also only covers what passes through &lt;code&gt;audit.record()&lt;/code&gt;, so a direct SQL edit to a &lt;code&gt;scores&lt;/code&gt; row is not logged and not detected.&lt;/p&gt;




&lt;h2&gt;
  
  
  Small security decisions that cost almost nothing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Session tokens are hashed.&lt;/strong&gt; The raw token (&lt;code&gt;secrets.token_hex(20)&lt;/code&gt;, 160 bits) is returned once. The database stores only &lt;code&gt;SHA-256(token)&lt;/code&gt; as the primary key, so a leaked database does not leak live sessions. Passwords use PBKDF2-HMAC-SHA256 with 120,000 iterations and a per-user salt.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;repo_url&lt;/code&gt; is parsed, not pattern-matched.&lt;/strong&gt; Only &lt;code&gt;http&lt;/code&gt; and &lt;code&gt;https&lt;/code&gt; with a hostname are accepted, case-insensitively, on both create and edit. &lt;code&gt;javascript:&lt;/code&gt;, &lt;code&gt;file:&lt;/code&gt; and &lt;code&gt;data:&lt;/code&gt; return 400.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rate limiting&lt;/strong&gt; covers login, team-join and judge-invite and returns &lt;strong&gt;429 with &lt;code&gt;Retry-After&lt;/code&gt;&lt;/strong&gt;. It is in memory and process-local, so it resets on restart and is not shared across workers. The docs say so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Judge "invitation" is direct provisioning.&lt;/strong&gt; There is no mail service offline, so the organiser creates the judge and a random password is shown once. There is no password-reset flow, by design.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Invite codes&lt;/strong&gt; are &lt;code&gt;secrets.token_hex(6)&lt;/code&gt;, 48 bits of entropy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The one I would flag to any organiser:&lt;/strong&gt; the fixed demo tokens (&lt;code&gt;org_demo_token&lt;/code&gt;, &lt;code&gt;jdg_a_demo_token&lt;/code&gt; and friends) are published in the repo and &lt;strong&gt;never expire&lt;/strong&gt;, so &lt;code&gt;.dogfood.toml&lt;/code&gt; stays valid across restarts. Anyone who has read the repo has organiser access to a default install. &lt;code&gt;SECURITY.md&lt;/code&gt; says in bold to delete or rotate them before running a real event.&lt;/p&gt;




&lt;h2&gt;
  
  
  Offline means really offline
&lt;/h2&gt;

&lt;p&gt;The rule is that &lt;code&gt;docker compose up&lt;/code&gt; produces a working, seeded portal with the network off.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vendored wheels.&lt;/strong&gt; Flask and its six transitive dependencies sit in &lt;code&gt;vendor/&lt;/code&gt; as 7 wheels, and the Dockerfile installs with &lt;code&gt;pip install --no-index --find-links=/app/vendor&lt;/code&gt;. There is no &lt;code&gt;&amp;lt;script src="https://..."&amp;gt;&lt;/code&gt; anywhere in a template and no web fonts, only system stacks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;An atomic, idempotent seed&lt;/strong&gt;, as described in bug 6.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A two-event model.&lt;/strong&gt; &lt;code&gt;evt_01&lt;/code&gt; is the supplied fixture, loaded verbatim and kept closed and published. Its dates are never altered, because the checker's "closed event refuses submissions" test depends on that date already being in the past. &lt;code&gt;evt_live_2026&lt;/code&gt; is an open demo event with its own teams, judges, rubric and assignments, so the whole create → submit → assign → judge → normalize → publish flow can run live without touching historical evidence.&lt;/p&gt;

&lt;p&gt;Two fixture details are handled as evidence, not errors. The duplicate &lt;strong&gt;"Dry Harbour"&lt;/strong&gt; pair (&lt;code&gt;prj_07&lt;/code&gt; and &lt;code&gt;prj_41&lt;/code&gt;) loads as two independent rows and each project's page surfaces a "potential duplicate submission" callout. And because the fixture only records outcomes, not the assignment process, seeding reconstructs one &lt;code&gt;assignments&lt;/code&gt; row per fixture score, while the assignment engine itself is exercised on the live event.&lt;/p&gt;




&lt;h2&gt;
  
  
  How I know my tests are not lying
&lt;/h2&gt;

&lt;p&gt;A passing test proves little until it has been seen to fail. For the audited defects and the security boundaries, I temporarily reintroduced the bug or weakened a guard and confirmed the suite went red, then restored the code. Re-adding the bare &lt;code&gt;COUNT(*)&lt;/code&gt; failed 11 of 19 integrity tests. Removing a role guard, widening a role, or adding an unguarded route each fail &lt;code&gt;test_access_matrix.py&lt;/code&gt;. Using the newest rubric's weights for every score fails the rubric-mixing regression.&lt;/p&gt;

&lt;p&gt;This is a manual practice, not an automated mutation-testing tool, and I did not apply it to every test. The README says exactly that.&lt;/p&gt;

&lt;p&gt;The numbers at the end of the build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -m unittest discover -s tests -p "test_*.py"
Ran 273 tests in 87.364s
OK

python3 run.py .dogfood.toml
claimed T1 T2, verified T1 T2   (7/7 PASS)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The official checker's receipt is committed as &lt;code&gt;acceptance-report.txt&lt;/code&gt;, generated from a clean &lt;code&gt;docker compose up --build -d&lt;/code&gt; with the byte-identical supplied &lt;code&gt;run.py&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I cut, and what I do not regret
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client-side JavaScript.&lt;/strong&gt; Every action is a full page load. With no client logic there is no client logic to trust, so the server is the only place a rule can live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;T3 and T4.&lt;/strong&gt; I would rather say "this feature doesn't exist yet" than ship half-working endpoints.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email invitations and password reset.&lt;/strong&gt; No mail service offline, so I did not fake one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TLS.&lt;/strong&gt; Plain HTTP only. It belongs behind a TLS-terminating proxy for any real deployment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Collusion detection.&lt;/strong&gt; &lt;code&gt;SECURITY.md&lt;/code&gt; says "None, and none is claimed." Two colluding judges look identical, from score values alone, to two judges who genuinely agree. The audit log supports a human investigation, and that is all.&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;My audit log has a gap I documented and did not close. Most business writes &lt;strong&gt;commit first and append their audit row afterwards&lt;/strong&gt;. A crash in that narrow window can leave a mutation with no audit row. The hash chain itself is atomic, but the pairing of "change the data" and "record that I changed it" is not one transaction.&lt;/p&gt;

&lt;p&gt;Closing it needs a larger refactor so that both writes share one transaction, and I ran out of window before I ran out of plan. For a platform whose whole thesis is that evidence cannot quietly go missing, that is the honest weak spot, and I would fix it first.&lt;/p&gt;

&lt;p&gt;The second gap I think about is &lt;strong&gt;result replay&lt;/strong&gt;. A normalization run stores everything needed to recompute it, and re-running gives identical numbers, which is tested. But there is no button that recomputes a published result and reports a match. A skeptical organiser still has to take my word, or my tests' word, that it reproduces.&lt;/p&gt;




&lt;h2&gt;
  
  
  What I would tell someone building this next
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Decide what your evidence is before you decide what your features are.&lt;/strong&gt; The brief reads like a CRUD app. The product is a set of claims, and each claim needs evidence that cannot move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Check that your test is red for the right reason.&lt;/strong&gt; Two of my worst bugs hid behind tests that passed for an unrelated cause: a team-spoof test that only failed on the deadline, and a role test that accepted any 403. Make the oracle as specific as the claim.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Make forgetting a failing test.&lt;/strong&gt; Forty-three declared policies, enforced against every role, mean a new route cannot ship without a decision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Name your limits in writing.&lt;/strong&gt; A fixed &lt;code&gt;k&lt;/code&gt;, a tamper-evident log, an in-memory rate limiter, a crash gap in the audit trail. Each costs one sentence in the README, and having a reader discover it later costs trust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Show the awkward result.&lt;/strong&gt; A method that makes one project fall 13 places is more convincing when you point at it yourself.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final scorecard
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;T1 + T2 claimed. 7/7 official checks PASS.&lt;/strong&gt; T3 and T4 explicitly not claimed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;273 tests pass&lt;/strong&gt;, none skipped, standard library only.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend-enforced isolation&lt;/strong&gt;, with 43 declared route policies checked against 5 identities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalization:&lt;/strong&gt; documented formula, &lt;code&gt;k = 3&lt;/code&gt;, 32 of 41 projects re-ranked, Spearman 0.927 against raw.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Freshness gate&lt;/strong&gt; (stale publish returns 409) and an &lt;strong&gt;immutable snapshot&lt;/strong&gt; for explanations.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seven audited defects&lt;/strong&gt;, each with a regression test I watched fail.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline end to end:&lt;/strong&gt; vendored wheels, seeded SQLite, no CDN, no client-side JS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Known limitations written down&lt;/strong&gt;, including the audit crash gap.&lt;/li&gt;
&lt;/ul&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/rajpriyanid-creator/dogfood
&lt;span class="nb"&gt;cd &lt;/span&gt;dogfood
docker compose up              &lt;span class="c"&gt;# portal at http://localhost:8080&lt;/span&gt;
python3 run.py .dogfood.toml   &lt;span class="c"&gt;# the official checker, unmodified&lt;/span&gt;
python3 &lt;span class="nt"&gt;-m&lt;/span&gt; unittest discover &lt;span class="nt"&gt;-s&lt;/span&gt; tests &lt;span class="nt"&gt;-p&lt;/span&gt; &lt;span class="s2"&gt;"test_*.py"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://github.com/rajpriyanid-creator/dogfood" rel="noopener noreferrer"&gt;github.com/rajpriyanid-creator/dogfood&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Read first: &lt;code&gt;JUDGING.md&lt;/code&gt; (formula, worked fixture examples, limits), &lt;code&gt;SECURITY.md&lt;/code&gt; (threat by threat, with residual limits), &lt;code&gt;ARCHITECTURE.md&lt;/code&gt; (lifecycle, design decisions, how the tests were validated)&lt;/li&gt;
&lt;li&gt;Event: &lt;a href="https://dogfoodhack.com/" rel="noopener noreferrer"&gt;dogfoodhack.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Built for DOGFOOD 2026 by Rajpriyan S . Thanks to @hackathonraptors for a brief that rewards honesty over feature counts.&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  DOGFOOD #HackathonRaptors #Python #Security #OpenSource
&lt;/h1&gt;

</description>
      <category>hackathonraptors</category>
      <category>python</category>
      <category>security</category>
      <category>opensource</category>
    </item>
    <item>
      <title>We Deleted `pip install` From a Dev Tool. Here's Everything That Broke.</title>
      <dc:creator>Rajpriyan S</dc:creator>
      <pubDate>Tue, 01 Sep 2026 14:54:35 +0000</pubDate>
      <link>https://dev.to/rajpriyan_s/we-deleted-pip-install-from-a-dev-tool-heres-everything-that-broke-3kg5</link>
      <guid>https://dev.to/rajpriyan_s/we-deleted-pip-install-from-a-dev-tool-heres-everything-that-broke-3kg5</guid>
      <description>&lt;p&gt;&lt;em&gt;Submitted for the **Side Quest · The Write-Up&lt;/em&gt;* — judged on insight, not audience size. This post covers, in order: what we reimplemented, what the standard library made genuinely painful, the package our own code made look unnecessary, and the specific edge case that cost us real debugging time.*&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;CodeIntel Doctor Zero&lt;/strong&gt; — a single-file, offline codebase doctor that scores your repo's health, catches leaked secrets, finds duplicate code, and tells you exactly what to fix first. Built for &lt;strong&gt;Zero Dependency Hack 2026, Track A — Developer Tools &amp;amp; CLI&lt;/strong&gt;, with &lt;strong&gt;zero third-party runtime dependencies&lt;/strong&gt;, &lt;strong&gt;87 passing tests&lt;/strong&gt;, and a &lt;strong&gt;bit-for-bit reproducible build&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3vqcuyylkvrkjummz3bt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3vqcuyylkvrkjummz3bt.png" alt="CodeIntel Doctor Zero overview dashboard, dark theme, showing a 57/100 health score, file stats, and a prioritized findings list" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The local browser UI, rendered from the project's real &lt;code&gt;ui.html&lt;/code&gt; against the repo's own published fixture data — no mockup tool, this is the actual product.&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The one-line pitch
&lt;/h2&gt;

&lt;p&gt;Point it at any repository. In under a second, it hands you a &lt;strong&gt;0–100 health score you can actually audit line by line&lt;/strong&gt;, a prioritized "fix this first" list, an offline secret scanner, a real duplicate-code detector, local search, and a self-contained HTML report — all from &lt;strong&gt;one Python file, running on nothing but the standard library.&lt;/strong&gt;&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/rajpriyanid-creator/codeintel-doctor
&lt;span class="nb"&gt;cd &lt;/span&gt;codeintel-doctor-zero
python3 build.py          &lt;span class="c"&gt;# verifies runtime, runs 87 tests, builds a deterministic artifact&lt;/span&gt;
python3 codeintel.py analyze &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;pip install&lt;/code&gt;. No Docker. No API key. No internet connection — ever.&lt;/p&gt;




&lt;h2&gt;
  
  
  The constraint that started it
&lt;/h2&gt;

&lt;p&gt;Every dev-tool hackathon has the same shortcut available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pip install click rich pygments radon chardet jinja2 pytest ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Seven or eight battle-tested libraries, and you're 80% of the way to a polished CLI before lunch.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Zero Dependency Hack 2026&lt;/strong&gt; removed that shortcut on purpose. Track A's brief was blunt: build a real developer tool with &lt;strong&gt;zero third-party runtime dependencies.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That reframes the actual question. It's no longer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can Python scan a repository?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Obviously — yes. The real question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;How much genuine repository intelligence can you build when the entire package ecosystem is off the table — and can you prove it, not just claim it?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That second half — &lt;em&gt;prove it&lt;/em&gt; — is what separates a gimmick from an engineering submission. So the project treats "zero dependencies" as a claim with receipts, not a badge in a README. Running &lt;code&gt;verify_zero_deps.py&lt;/code&gt; parses &lt;code&gt;codeintel.py&lt;/code&gt;'s actual &lt;code&gt;import&lt;/code&gt; statements with Python's own &lt;code&gt;ast&lt;/code&gt; module, checks every one against &lt;code&gt;sys.stdlib_module_names&lt;/code&gt;, scans for &lt;code&gt;subprocess&lt;/code&gt; calls, scans for network-capable imports, and scans for vendored packages. The real, generated output — captured in &lt;code&gt;deps-proof.txt&lt;/code&gt; — ends with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ZERO-DEPENDENCY CHECK: PASS
THIRD-PARTY RUNTIME DEPS: 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anyone can clone the repo and run that check themselves. That's the whole point.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we would normally have installed
&lt;/h2&gt;

&lt;p&gt;We didn't try to rebuild entire ecosystems — only the exact slice each capability needed.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;Typical dependency&lt;/th&gt;
&lt;th&gt;Stdlib foundation&lt;/th&gt;
&lt;th&gt;What we actually built&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CLI parsing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;click&lt;/code&gt; / &lt;code&gt;typer&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;argparse&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;commands, flags, validation, CI-ready exit codes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Terminal styling&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;rich&lt;/code&gt; / &lt;code&gt;colorama&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;sys&lt;/code&gt;, &lt;code&gt;os&lt;/code&gt;, raw ANSI SGR&lt;/td&gt;
&lt;td&gt;TTY-aware severity coloring + &lt;code&gt;NO_COLOR&lt;/code&gt; fallback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tables&lt;/td&gt;
&lt;td&gt;&lt;code&gt;tabulate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;strings + terminal I/O&lt;/td&gt;
&lt;td&gt;deterministic table renderer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Repo traversal&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;pathspec&lt;/code&gt; / &lt;code&gt;glob&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;os.walk&lt;/code&gt;, &lt;code&gt;fnmatch&lt;/code&gt;, &lt;code&gt;pathlib&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;ignore rules, symlink-loop safety, depth limits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Encoding detection&lt;/td&gt;
&lt;td&gt;&lt;code&gt;chardet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;UTF-8 probing + fallback&lt;/td&gt;
&lt;td&gt;text/binary classification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Language ID&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pygments&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;extensions + shebangs&lt;/td&gt;
&lt;td&gt;heuristic language detection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Complexity&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;radon&lt;/code&gt; / &lt;code&gt;lizard&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;code&gt;re&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;estimated control-flow complexity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate detection&lt;/td&gt;
&lt;td&gt;MinHash-style libs&lt;/td&gt;
&lt;td&gt;&lt;code&gt;hashlib&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;sliding-window fingerprint + verify&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Secret scanning&lt;/td&gt;
&lt;td&gt;dedicated scanners&lt;/td&gt;
&lt;td&gt;&lt;code&gt;re&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;pattern rules, confidence, masking, baselines&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;HTML reports&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Jinja2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;str.format&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;self-contained, zero-asset HTML report&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Test runner&lt;/td&gt;
&lt;td&gt;&lt;code&gt;pytest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;unittest&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;87-test regression suite&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;STDLIB.md&lt;/code&gt; documents &lt;strong&gt;17 real substitutions&lt;/strong&gt;, each with an honest note on where the hand-rolled version is &lt;em&gt;not&lt;/em&gt; a drop-in replacement. Our terminal renderer, for instance, never tries to become &lt;code&gt;rich&lt;/code&gt; — it only does severity colors, bold headers, TTY detection, and a plain fallback. That's all the product needed, so that's all we built.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the standard library made genuinely painful
&lt;/h2&gt;

&lt;p&gt;The table above makes it look like every substitution was a clean swap. It wasn't. This is the part most "we went dependency-free!" posts skip, and it's the actual insight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Color support isn't just "print ANSI codes."&lt;/strong&gt; &lt;code&gt;colorama&lt;/code&gt;/&lt;code&gt;rich&lt;/code&gt; quietly handle Windows for you. The standard library does not. Legacy &lt;code&gt;cmd.exe&lt;/code&gt; needs &lt;code&gt;SetConsoleMode&lt;/code&gt; called on the console handle via &lt;code&gt;ctypes&lt;/code&gt; before ANSI escapes render at all — otherwise a Windows user sees literal &lt;code&gt;\x1b[36m&lt;/code&gt; garbage in their terminal instead of color. That's not in any stdlib color tutorial; you find it the first time someone runs your tool on a non-Windows-Terminal machine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;fnmatch&lt;/code&gt; is not &lt;code&gt;.gitignore&lt;/code&gt;.&lt;/strong&gt; &lt;code&gt;pathspec&lt;/code&gt; gives you real gitignore semantics for free: negation patterns (&lt;code&gt;!keep-this.py&lt;/code&gt;), directory-only patterns (&lt;code&gt;build/&lt;/code&gt;), and &lt;code&gt;**&lt;/code&gt; globstars. &lt;code&gt;fnmatch&lt;/code&gt; only does flat shell globs. Reimplementing "ignore like git ignores" correctly — including negation overriding an earlier broad match — took noticeably longer than the actual file-walking logic around it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;json.dumps&lt;/code&gt; is not deterministic by default.&lt;/strong&gt; Dict key order, float repr, and separator whitespace can all vary in ways that silently break a byte-for-byte reproducibility test. Getting the determinism suite to pass meant explicitly forcing &lt;code&gt;sort_keys=True&lt;/code&gt;, pinning separators, and controlling float formatting everywhere a score or percentage got serialized.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;unittest&lt;/code&gt; has no &lt;code&gt;pytest.mark.parametrize&lt;/code&gt;.&lt;/strong&gt; Every "run this same check against 10 fixture variations" test became a hand-rolled loop with &lt;code&gt;self.subTest(...)&lt;/code&gt;, which works but is far more ceremony than a decorator.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;There is no safe recursive directory walker built in.&lt;/strong&gt; &lt;code&gt;os.walk&lt;/code&gt; will happily follow a symlink into a loop unless you tell it not to — and even then, avoiding &lt;em&gt;directory&lt;/em&gt; symlink loops specifically (as opposed to just not following symlinks at all) needs manual &lt;code&gt;(st_dev, st_ino)&lt;/code&gt; tracking that a library like &lt;code&gt;pathspec&lt;/code&gt; or &lt;code&gt;scandir&lt;/code&gt;-based tools give you out of the box.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are hard problems. They're just the fifty small taxes a package normally pays for you, silently, that become your problem the moment you remove it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The package our own code made look unnecessary
&lt;/h2&gt;

&lt;p&gt;If we had to name one: &lt;strong&gt;a dedicated secret-scanning tool&lt;/strong&gt; — the &lt;code&gt;detect-secrets&lt;/code&gt; / &lt;code&gt;trufflehog&lt;/code&gt; category of product.&lt;/p&gt;

&lt;p&gt;Those are real, well-built tools with entropy analysis, plugin architectures, and CI integrations. Ours is roughly 150 lines of &lt;code&gt;re&lt;/code&gt; patterns, a confidence heuristic, and a masking function — and for the specific job of &lt;em&gt;"tell a developer there might be a hardcoded credential before they commit it,"&lt;/em&gt; it does the same practical job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;severity + confidence + file + line + masked evidence + suggested fix
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We're not claiming feature parity — no entropy scoring, no plugin system, no maintained rule database that updates as new credential formats appear. But the honest finding is that for the 80% case a small team actually needs — &lt;em&gt;"did someone just commit a password"&lt;/em&gt; — you don't need to install a scanning product. You need about a page and a half of regular expressions and the discipline to always mask what you find. That's a genuinely useful thing to know before your next &lt;code&gt;pip install&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Show, don't tell: the actual output
&lt;/h2&gt;

&lt;p&gt;This isn't a mockup. This is what &lt;code&gt;doctor --explain-score&lt;/code&gt; prints on a deliberately messy fixture repo — every point lost, every finding, generated from a real scan:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F628ikwaaa07lvianb4tz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F628ikwaaa07lvianb4tz.png" alt="Terminal screenshot showing codeintel.py doctor output with a full health-score breakdown by dimension and a HIGH severity finding for an oversized module" width="800" height="607"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's the difference between &lt;strong&gt;analysis&lt;/strong&gt; ("here's some data") and &lt;strong&gt;diagnosis&lt;/strong&gt; ("here's what's wrong, why it matters, and what to do about it"). Every point lost is traceable to named, counted evidence — nothing is a magic number.&lt;/p&gt;

&lt;p&gt;Compare that to the plain &lt;code&gt;analyze&lt;/code&gt; summary on the same repo:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ python3 codeintel.py analyze fixtures/messy-repo --plain
CODEINTEL DOCTOR ZERO
Repository: messy-repo
Health: 57/100

Files:               7
Lines:           1,014
Functions:          45
Classes:             0
Tests:               0

Languages
  Python          99.5%
  Markdown         0.5%

Top Risk Areas
  01 src/api/routes.py            68
  02 src/auth/login.py            10
  03 src/auth/register.py         10

Findings
  CRITICAL             0
  HIGH                 2
  MEDIUM              24
  LOW                  2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  And when you want it in a browser: the local UI
&lt;/h2&gt;

&lt;p&gt;The CLI is the primary interface, but &lt;code&gt;python3 codeintel.py ui .&lt;/code&gt; spins up the exact same analysis behind a single-page dashboard, served locally on &lt;code&gt;127.0.0.1&lt;/code&gt; with &lt;strong&gt;zero network requests&lt;/strong&gt; — nothing leaves your machine, and there's nothing to configure.&lt;/p&gt;

&lt;p&gt;The sidebar mirrors the CLI one-to-one: &lt;strong&gt;Overview, Doctor, Security, Duplicates, Structure, Search, Explain file.&lt;/strong&gt; Same data everywhere, just a different lens on it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Overview&lt;/strong&gt; — the health ring, per-dimension bars, the "needs attention" queue, and language mix, all from one scan:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3vqcuyylkvrkjummz3bt.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3vqcuyylkvrkjummz3bt.png" alt="CodeIntel overview dashboard with 57/100 health score circle, dimension bars for structure/maintainability/security/testing/documentation/duplication/hygiene, and a findings sidebar" width="800" height="440"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Doctor&lt;/strong&gt; — the same prioritized findings as &lt;code&gt;doctor --explain-score&lt;/code&gt;, but as a sortable table with severity, location, and the suggested fix right next to it:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnurx65v6afx4lxt6tga6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnurx65v6afx4lxt6tga6.png" alt="CodeIntel Doctor tab showing a table of findings ranked HIGH to LOW with file locations and recommendations" width="800" height="280"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security&lt;/strong&gt; — masked evidence, confidence-scored signals, nothing phrased as a certainty:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsveh3dmrtjdclabh4rzn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsveh3dmrtjdclabh4rzn.png" alt="CodeIntel Security tab showing counts of critical high medium and low findings and a table of masked credential evidence" width="800" height="240"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Beyond these three, &lt;strong&gt;Duplicates&lt;/strong&gt; shows the sliding-window fingerprint clusters with exact file:line ranges on both sides of every match, &lt;strong&gt;Structure&lt;/strong&gt; surfaces likely entry points and a scanned-file table, &lt;strong&gt;Search&lt;/strong&gt; does live client-side text/symbol lookup against the current scan, and &lt;strong&gt;Explain file&lt;/strong&gt; drills into a single file's symbols, complexity, and findings.&lt;/p&gt;

&lt;p&gt;The important architectural decision here isn't the UI polish — it's that the browser layer &lt;strong&gt;does not run a second analysis engine.&lt;/strong&gt; It renders the exact same &lt;code&gt;AnalysisReport&lt;/code&gt; object that the CLI and the JSON/HTML report use. That means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CLI · JSON · HTML · Browser  →  one analysis model
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;instead of four independent implementations that could quietly disagree with each other. One &lt;code&gt;Export JSON&lt;/code&gt; button, and the browser view and the CLI's &lt;code&gt;--json&lt;/code&gt; output are byte-for-byte the same data.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hardest problem: what even counts as "duplicate code"?
&lt;/h2&gt;

&lt;p&gt;A naive duplicate detector compares blocks of text directly — slow, and noisy. CodeIntel's pipeline instead does:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;source → normalize → tokenize → sliding windows → SHA-256 fingerprints
       → candidate matches → content verification → duplicate clusters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the more interesting engineering decision isn't the algorithm — it's the honesty about its limits. The detector catches &lt;strong&gt;near-verbatim duplicates after normalization&lt;/strong&gt;. It does not claim to catch semantically identical code with renamed variables, and it says so directly in its own docs. So the finding is phrased as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Possible duplicated block"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;never:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"These two functions are semantically identical."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That restraint matters more than it sounds like it should. A tool that overclaims what it detects is a tool nobody trusts in CI.&lt;/p&gt;




&lt;h2&gt;
  
  
  Security scanning, without a security package
&lt;/h2&gt;

&lt;p&gt;The scanner looks for the usual credential-shaped trouble: private keys, API-key-like strings, hardcoded passwords, credential-bearing URLs, JWT-like values, disabled TLS verification, debug configs left on. It's pure &lt;code&gt;re&lt;/code&gt; pattern matching — no vulnerability database, no cloud lookup, and it never pretends otherwise. Every finding ships with severity, confidence, file, line, masked evidence, and a suggested fix — and the actual secret value is &lt;strong&gt;never&lt;/strong&gt; written anywhere, in any output mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;DB_PASSWORD = "hunt********rd2024"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because it makes zero network calls (mechanically verified, not assumed), your source code never leaves your machine while it's scanned. That's a genuine privacy property — but the docs are careful to separate that from a security guarantee: &lt;strong&gt;offline means private, not automatically safe.&lt;/strong&gt; Teams can accept known findings with &lt;code&gt;--baseline&lt;/code&gt; so CI only flags what's actually new.&lt;/p&gt;




&lt;h2&gt;
  
  
  Reproducibility isn't a claim — it's a receipt
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;build.py&lt;/code&gt; produces a deterministic zipapp: fixed timestamps, fixed permissions, normalized metadata, sorted archive contents, then hashes the result. Here's an actual run, captured twice, from &lt;code&gt;deps-proof.txt&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Build 1 SHA-256: 276c1dc98360db0d61295258a3f6ee4073b7148c36155636c90cea91dc21401
Build 2 SHA-256: 276c1dc98360db0d61295258a3f6ee4073b7148c36155636c90cea91dc21401

MATCH: yes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same input, same bytes, twice. That's a stronger sentence than "the build is reproducible" — it's a claim you can rerun yourself in thirty seconds.&lt;/p&gt;

&lt;p&gt;The determinism guarantee doesn't stop at the build. A dedicated test suite (&lt;code&gt;test_determinism.py&lt;/code&gt;) checks byte-identical JSON across repeated runs, stable file ordering, stable finding ordering, deterministic HTML output, and bounded health scores — because a repo scanner that gives you a different answer every time is worse than useless in a CI pipeline.&lt;/p&gt;




&lt;h2&gt;
  
  
  CI-ready, not just demo-ready
&lt;/h2&gt;

&lt;p&gt;This is the part that turns a hackathon toy into something a real team could adopt tomorrow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 codeintel.py doctor &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--fail-on&lt;/span&gt; high
python3 codeintel.py security &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--baseline&lt;/span&gt; security-baseline.json &lt;span class="nt"&gt;--fail-on&lt;/span&gt; medium
&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;Exit code&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;policy failure — a finding met/exceeded &lt;code&gt;--fail-on&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;usage / argument error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;filesystem / path error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;internal error&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Drop it into a pipeline, gate merges on health regressions, and it never phones home. &lt;code&gt;--json&lt;/code&gt; for machines, &lt;code&gt;--plain&lt;/code&gt;/&lt;code&gt;--no-color&lt;/code&gt; for logs, &lt;code&gt;--baseline&lt;/code&gt; so a team only sees genuinely new security findings instead of re-litigating accepted ones every run.&lt;/p&gt;




&lt;h2&gt;
  
  
  The edge case that ate an afternoon
&lt;/h2&gt;

&lt;p&gt;The test cases that matter aren't clean files — they're the ones that break naive assumptions: Unicode filenames, binary content, NUL bytes, invalid encodings, huge files, deeply nested directories, &lt;strong&gt;symlink loops&lt;/strong&gt;, permission errors, empty repos, malformed baselines.&lt;/p&gt;

&lt;p&gt;The one that actually cost real time was a symlink loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;repo/
  src/
  link -&amp;gt; .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;link&lt;/code&gt; points back at the repo root. A naive recursive walker — which is exactly what an early version of the scanner was — turns that into unbounded recursion: &lt;code&gt;repo/link/link/link/link/...&lt;/code&gt; until Python hits its recursion limit or the process just hangs, depending on how the traversal was written. The failure mode is ugly precisely because it &lt;em&gt;looks&lt;/em&gt; like the tool is doing real work — CPU pinned, no error, no output — right up until it isn't.&lt;/p&gt;

&lt;p&gt;The fix wasn't "add a depth limit and call it done," because a depth limit just turns an infinite loop into a very slow, very wrong scan of the same directory a thousand times. The real fix was refusing to follow directory symlinks at all, which — as a side effect — also closes off a class of "read outside the intended tree" attacks for free. That one bug is the reason the traversal layer tracks visited directories instead of trusting the filesystem to behave.&lt;/p&gt;

&lt;p&gt;It's now a permanent regression test, and it's the best argument in the whole project for why "the standard library gave us the primitives, not the product": &lt;code&gt;os.walk&lt;/code&gt; will follow that symlink into the loop by default. Nothing warns you. You find out the hard way, once.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we deliberately did not build
&lt;/h2&gt;

&lt;p&gt;Scope discipline is part of the engineering, not a footnote:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Not a compiler or a full AST analyzer for every language&lt;/li&gt;
&lt;li&gt;Not a CVE database&lt;/li&gt;
&lt;li&gt;Not a code-coverage engine&lt;/li&gt;
&lt;li&gt;Not a semantic vector-search system&lt;/li&gt;
&lt;li&gt;Not a language server or cloud platform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Symbol detection and complexity are regex/heuristic, and the output always says "estimated" or "probable" — never asserts certainty. We'd rather ship:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A fast, explainable first-pass signal."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;than:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"This understands your entire codebase."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That honesty is documented up front in &lt;code&gt;STDLIB.md&lt;/code&gt; and the README's own limitations section — not buried, not discovered by a disappointed judge halfway through the demo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Single file, real structure
&lt;/h2&gt;

&lt;p&gt;The entire runtime lives in one file — &lt;code&gt;codeintel.py&lt;/code&gt;, the single-file bonus target — but it's organized into 24 clearly delimited sections (&lt;code&gt;# == SECTION&lt;/code&gt;) from constants and data models through the terminal/HTML renderers to &lt;code&gt;main()&lt;/code&gt;. One shared &lt;code&gt;AnalysisReport&lt;/code&gt;, produced once per scan, feeds every renderer — CLI, JSON, HTML, and the local browser UI shown above.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A single-file constraint changes the packaging model. It does not require abandoning engineering structure.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  The scoreboard
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project:            CodeIntel Doctor Zero
Track:               A — Developer Tools &amp;amp; CLI
Runtime:             Python 3.8+ (built/tested on 3.12)
Third-party deps:    0
Network requests:    0
Implementation:      single codeintel.py (~2,500 lines, 24 sections)
Automated tests:     87 passing
STDLIB substitutions: 17, documented with honest limitations
Reproducible build:  verified — identical SHA-256 across two builds
Commands:            analyze · doctor · search · security · tree ·
                     duplicates · explain · report · ui · demo-color
Bonus targets hit:   Package Killer (terminal engine) · STDLIB Log ·
                     single-file · reproducible build · CI exit codes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  What zero dependencies actually taught us
&lt;/h2&gt;

&lt;p&gt;Removing the package registry doesn't prove third-party packages are bad. It just forces you to see the layers sitting underneath them.&lt;/p&gt;

&lt;p&gt;A table was a renderer we had to design. A duplicate was a definition we had to draw a line around. A health score was a model we had to make explainable. A security alert was a confidence threshold we had to choose and defend. Once the packages disappeared, every one of those decisions became ours to own — and to prove.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That's the actual submission for Zero Dependency Hack 2026: not "look, no packages," but a working, tested, reproducible developer tool where every design decision that would normally hide inside a &lt;code&gt;pip install&lt;/code&gt; is visible, documented, and honest about its own limits.&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/rajpriyanid-creator/codeintel-doctor" rel="noopener noreferrer"&gt;https://github.com/rajpriyanid-creator/codeintel-doctor&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Demo:&lt;/strong&gt; &lt;a href="https://youtu.be/W4I1-tRH7sE" rel="noopener noreferrer"&gt;https://youtu.be/W4I1-tRH7sE&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Built for:&lt;/strong&gt; Zero Dependency Hack 2026 — Track A, Developer Tools &amp;amp; CLI · Side Quest, The Write-Up&lt;/p&gt;

&lt;p&gt;Tagging &lt;strong&gt;@HackathonRaptors&lt;/strong&gt; — thanks for running Side Quest and rewarding the write-up over the vote count.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; hackathonraptors· cli · opensource · showdev · hackathon · zerodependency · python  &lt;/p&gt;

</description>
      <category>python</category>
      <category>hackathon</category>
      <category>opensource</category>
      <category>hackathonraptors</category>
    </item>
  </channel>
</rss>
