DEV Community

Cover image for One SQL Update Reopened a Closed Hackathon. The Checker Still Exited 0.
Sai Ram Dash
Sai Ram Dash

Posted on

One SQL Update Reopened a Closed Hackathon. The Checker Still Exited 0.

A closed hackathon accepted a submission with HTTP 200.

The fixture event was closed. The official checker tried to submit to it anyway, and the server was supposed to refuse. This was meant to be the boring part of Manak, the hackathon judging portal I built.

The cause was in my demo seed. While opening community voting, I had used one parameter for two deadlines:

`update event set voting_mode = 'open', voting_open_at = :o,
 voting_close_at = :c, voting_credits = 100,
 submissions_close_at = :c where id = :e`

// The same :c binding served both deadlines.
c: systemClock.now() + 3600000 * 48
Enter fullscreen mode Exit fullscreen mode

Two deadlines, one binding. The closed fixture event got another 48 hours, the stored window allowed the submission, and the server did what its data told it to. I had written the data.

Historical regression and repair. Reusing a deadline binding changed which submissions the event allowed. Current seed implementation.

Six probes passed. The closed-event probe failed, taking the first-tier acceptance gate with it. The checker still exited with code zero. The report contained the answer; the process status did not carry it. An old green report and a successfully completed command were both poor substitutes for reading what the running demo had actually done.

I changed the seed to read the imported submission deadline and bind it separately. The completed fixture showcase keeps its voting window closed too. The corrected official receipt reads:

T1  gallery is public ................. PASS
T1  project from fixtures shown ....... PASS
T1  closed event refuses submissions .. PASS
T2  judge sees own scores ............. PASS
T2  judge cannot see peer scores ...... PASS
T2  participant blocked ............... PASS
T2  csv export works .................. PASS

claimed T1 T2 T3 T4, verified T1 T2
note: claimed but not verified: T3 T4
Enter fullscreen mode Exit fullscreen mode

Seven passing probes, two verified tiers. Both belong in the description of that result.

An interactive demo needs its own event state. Its convenience settings cannot rewrite the canonical fixture's rules. That small repair also explains the larger build: a judging system has to preserve the conditions under which a decision is valid. The first condition I had broken was the deadline.

Build the event around the score

Manak—मानक, “standard”—takes an event from submissions to signed awards. Organizers define tracks and a versioned weighted rubric. Teams submit projects. Eligible judges receive assignments, keep private drafts and submit scores. Organizers publish a frozen result revision, record award decisions and issue signed certificates.

That sequence became the useful unit of design. Calculating a score is one stage. Getting the right evidence into it, deciding when it becomes official and retaining what that decision meant are the surrounding stages.

The runtime uses Node's HTTP, SQLite, crypto and test APIs, native TypeScript stripping and server-rendered HTML. There are zero runtime npm packages; TypeScript and Node type definitions remain development dependencies. The main forms work without browser JavaScript. The enhanced editor and offline verifier use it.

A declared command registry feeds capabilities, validation and OpenAPI, although some transport routes sit outside it. I also wrote the request schema layer: the same field declarations drive validation, JSON Schema and form rendering. URL-encoded forms can opt into converting strings to numbers; JSON clients must send typed numbers. Unknown fields are refused. A form's "5" and a JSON client's 5 need a deliberate contract before either reaches a score column. The package graph is small. The amount of application behavior I own is less cooperative. Request schema, stack and setup.

A spreadsheet leaves deadlines, permissions and publication snapshots to a process. A live ranking page makes the latest ordering easy to see, but still needs a rule about what later edits do to an announced result. I put those obligations into explicit application state. The cost is more transitions, migrations and tests. The benefit is being able to ask the code where a rule is enforced.

Approach What it makes easy What still needs an owner
Spreadsheet and manual process Inspecting and changing scores Permissions, deadline enforcement, snapshots and award records
Live ranking page Showing the latest ordering Deciding whether later edits should move a published result
Manak's explicit workflow Enforcing transitions and retaining decision context Application state, migrations, tests and operational maintenance

Design tradeoffs, not a benchmark or a claim about every competing product. Each approach leaves a different part of the judging process for someone to maintain.

This is why the portal is larger than a score form and a sort. Team ownership, a judge's conflict, a rubric edit and an organizer's correction all change which actions should be possible. Giving each one a visible state costs code, but it also gives the next stage something more precise than an assumption to work with.

Put the rule where the write happens

My governing rule is: if breaking a rule would invalidate a decision, enforce it at the write boundary and keep the evidence needed to inspect it later.

This is the database trigger for submission timestamps:

create trigger project_submission_window before update of status, submitted_at on project
when new.status = 'submitted' and (new.submitted_at <
  (select submissions_open_at from event where id = new.event_id) or new.submitted_at >=
  (select submissions_close_at from event where id = new.event_id))
begin select raise(abort, 'project submission timestamp outside event window'); end;
Enter fullscreen mode Exit fullscreen mode

It checks the supplied timestamp against the stored window. It cannot establish that the window itself is correct. Once my seed moved the deadline, the trigger had a perfectly valid reason to allow the submission. Constraints, authorized state transitions and fixture setup have separate jobs; the opening bug crossed the gap between them. Invariant triggers.

Refusals needed a boundary too. A denied write should leave no audit-ledger effects. My isolation check sends requests through actual sockets: 102 operations, six witnesses and two renderings make 1,224 requests. It records 674 refusals, including 418 writes, with zero ledger appends from refused requests and zero chain breaks.

The test also rejects an easy false positive. A parser error or rate-limit response cannot stand in for an authorization refusal. Otherwise I could congratulate the permission check for a request that never reached it. The proof covers the declared command matrix; transport exceptions remain outside that coverage. Isolation proof.

Collect evidence the model can compare

Judges see subsets of projects. Scheduling therefore changes what the statistical model can know.

If two panels judge disjoint groups, a low-scoring panel might be stricter, or its projects might be weaker. Scores alone cannot identify the relative panel offsets. No amount of arithmetic supplies the missing connection.

The scheduler starts with the most constrained projects and assigns work to eligible judges carrying lighter loads. It then uses augmenting paths to fill slots that a local placement can leave stranded. An existing assignment can move to make room for another, provided every edge remains eligible and every capacity still holds. If the requested coverage is impossible, the shortfall stays visible. Quietly relaxing a conflict would make the schedule look complete by changing what “complete” meant.

For small cases, I can give the scheduler an unusually stubborn reviewer: enumerate every answer. The test covers all 512 eligibility graphs for three projects and three judges, targets of one and two reviews, and capacities of [1, 2, 1]. It compares the result with every admissible edge subset. This is practical at that size and avoids making the oracle another copy of the scheduling algorithm. It establishes those small cases, without pretending to prove every possible event. Scheduler, completeness test.

Collecting the right evidence also means keeping working state out of it. The loader selects submitted ballots for the resolved published rubric version and excludes memberships whose evidence has been excluded. Drafts cannot move the fit. A submitted ballot must cover every criterion, and scores retain their rubric version. Once a version has scores, database triggers reject adding, editing or deleting its criteria. Otherwise a later rubric edit could change the meaning of an earlier judgment without the judge touching it. Evidence loader.

Make the model compete with simpler answers

The rubric model fits:

score(project, judge)
  = grand mean + project effect + judge offset + residual
Enter fullscreen mode Exit fullscreen mode

It also estimates each judge's scale and shrinks it toward one. A judge who gives every project the same score carries little ordering information. Dividing by a tiny spread should not turn that into a powerful opinion. Scale floors and information weights handle that case. Model implementation.

The machinery then owes a comparison. I tested raw means, per-judge z-scores and the fitted model against planted rankings in synthetic events. The displayed regimes quantize and clamp scores to 1–5. The full sweep covers 59 configurations over 20 seeds, or 1,180 event runs.

The balanced case uses the production scheduler. The correlated cases deliberately substitute assignments in which judges are grouped by leniency and projects increasingly prefer one judge cohort. Planted project quality is generated independently. The experiment changes which judge groups see which projects; it does not establish a true ranking for a real event.

Assignment regime Raw mean Per-judge z-score Fitted model
Balanced 0.7865 0.8369 0.7935
Mildly correlated 0.7630 0.8564 0.8942
Strongly correlated 0.7046 0.8565 0.8907

The balanced row spoils the clean story: the simpler z-score baseline wins. The fitted model's worst balanced seed is 0.6147. Its lead in the two displayed correlated regimes is an advantage under particular assignment conditions, which is more useful than declaring normalization solved.

Production ranking uses the fitted model. The normalization sandbox exposes alternative calculations for comparison, including raw means and within-reviewer z-scores, but is read-only; it does not select a different publication method. That makes the losing balanced result relevant to the system I actually built. The comparison is there to inspect the choice, not to make its cost disappear. Production ranking, sandbox.

The bundled fixtures answer a different question. They contain 41 projects, 30 registered judges and 126 imported ballots. Dry Relay moves from raw rank four to adjusted rank one; Slow Trail moves from six to nineteen. There is no known true ranking in those fixtures. Movement shows that the model changes decisions, not that it improves them. The import's equal criterion weights are an implementation choice, not an asserted organizer rule. Fixture report.

Giving the fit more computation produced another awkward result. Three outer rounds with a 200-iteration inner cap did not converge on the fixture data. Twelve rounds with a 1,000-iteration cap settled the fit. In the separate holdout check, which keeps some ballots out of fitting and predicts them, weighted-score RMSE increased from 1.017 to 1.038. The optimizer was more settled. Its predictions were slightly worse.

The larger iteration budget is the production default. It does not guarantee convergence on another event, so the warning still matters. Settling the fit and testing its predictions remain separate jobs.

Keep a second kind of judgment in its own model

Rubric judging asks how a project performed on weighted criteria. Pairwise judging asks which of two projects a reviewer prefers. Those observations can both be useful without sharing a numerical scale.

Manak uses a separate Bradley–Terry model for pairwise evidence. Its fitted strengths describe relative preference. They are not rubric-score units, so adding them to a normalized rubric result would manufacture a conversion the observations do not supply. A skipped comparison is excluded from the pairwise input rather than quietly counted as evidence for either project.

Connectivity matters here too. A set of preferences inside one group does not establish its position relative to an unconnected group. Missing, disconnected or nonconverged pairwise evidence must not produce confident finalist probabilities. The implementation keeps those checks between the fit and the recommendation.

The same restraint applies to uncertainty in rubric results. Overlapping approximate 95% bands can call for more evidence; they do not establish that the projects are equivalent. A request for another review is a useful output when the current evidence cannot support the precision of the proposed decision. Another decimal place is cheaper to generate, but it answers a different question.

Give publication an explicit boundary

A live dashboard should reflect new evidence. A ceremony needs a stable account of the decision that was actually made.

Manak stores a publication revision with the rubric version, algorithm and options, evidence digest, ledger head and report. The publication command builds the report and stores the revision in a transaction. The outer transaction uses SQLite's BEGIN IMMEDIATE; nested operations use savepoints. Evidence is hashed in stable primary-key order while the write lock is held, and the mutation and ledger append commit together. The report, digest and revision therefore share a transaction instead of being separately committed descriptions of a moving event. Public results and explanations read the stored report.

The regression test is deliberately rude to this boundary: publish revision one, then change a ballot. The published project results must stay fixed. An explicit second publication creates revision two, and award decisions attached to revision one stop applying. Carrying them forward would attach a decision to evidence the organizer had never approved. Publication code, regression tests.

The audit ledger uses a linear SHA-256 hash chain. That makes continuity inspectable. Someone with database write access can still recompute the whole chain, so detecting that rewrite requires a head anchor retained independently. The anchor is part of the design's trust condition; the local chain alone cannot make the database tamper-proof.

Awards are explicit organizer decisions attached to a publication revision. Only those decisions can mint placement certificates. A sorted list supplies evidence. The organizer still owns the award.

Build an ending that survives the server

A certificate record becomes a canonical ordered field array, then a SHA-256 digest and an Ed25519 signature. The signed fields bind the issuer key, publication revision and evidence digest. Version three also binds the template presentation and logo hash. A certificate can look convincing while its record is wrong, so the signed content needs its own verification path.

An offline browser or a standard-library Python verifier can check the signature. Both need an independent reason to trust the issuer public key. Neither can discover a later revocation, establish an external identity or prove the award was deserved. Those limits determine what the verification result means. Certificate implementation, Python verifier.

The tests check an original certificate, change its recipient name and require verification to fail. A different public key must fail too. A separate test issues legacy, v2 and v3 records, including Unicode, and invokes the Python CLI against them. The valid records pass; altering the v3 recipient name makes the CLI exit one. That tests the handoff between the issuer and a second implementation of the verifier. Signature tests, Python verifier tests.


Data needs an exit too. The archive roundtrip proof exports 162 rows across 33 table files, imports them, exports again and finds zero differing files. It also feeds the importer eleven damaged archives. All are refused, with zero rows committed by those refusals. A failed import that leaves half an event behind would be a rather expensive interpretation of “failed.”

The difficult part is making the stages agree

Several of these choices trade convenience for a decision that retains its meaning:

Decision What it preserves What it costs
Keep conflicts and capacities hard Eligible judging evidence Coverage may remain incomplete
Wait for community voting to close before publishing A completed evidence window Publication must wait
Bind awards to publication revisions The result the organizer approved Decide again after republishing
Require an explicit placement decision Organizer ownership of the award Record the placement before minting
Withhold unsupported pairwise probabilities An honest account of the evidence Sometimes collect another review

Testing those choices meant pushing on the connections: enumerate assignments, change a ballot after publication, damage an archive, inspect the ledger after a refused write. The full 15-stage API lifecycle then joins the pieces, including refusing publication while voting is open and allowing it after voting closes.

On 1 October 2026, the local checks ran again with Node 24.18 on Windows, including the official checker against a clean seeded local demo. Keeping the scope beside the count makes the results easier to use:

Check Recorded result Scope
Official acceptance after repairs 7 probes pass; T1/T2 verified T3/T4 remain unverified by this checker
Project-authored extended checker 18 verified, 0 failed, 7 partial, 3 blocked Additional probes, not official tier acceptance
Test suite 608 tests: 607 pass, 0 fail, 1 skip Local run; skipped Windows SIGTERM-related case
Integrated API workflow 15 stages pass Local lifecycle, including the voting/publication boundary
Command isolation 1,224 requests; 674 refusals; 418 refused writes; 0 refused-request ledger appends Declared command matrix through sockets
Assignment enumeration 512 eligibility graphs, two target values Three projects, three judges, fixed capacities
Normalization sweep 1,180 synthetic events Rank recovery against planted truth under constructed assumptions
Archive roundtrip 162 rows; 33 table files; 0 export differences Exported payloads; 11 damaged archives refused
TypeScript check Passes without diagnostics Development compiler, separate from runtime dependencies

The extended checker is mine. Its ledger probe checks links and the head, rather than independently rehashing every payload. Its webhook probe covers payload generation, rather than establishing receiver delivery and retry behavior. Those are reasons to keep the deeper tests and proof reports, not to relabel a partial probe as a complete workflow. Some blocked probes concern voting and shuffle on the fixture whose voting window is closed—the completed state preserved by the opening repair.

The proof reports are reproducible too. This command checks the isolation report against its committed result:

$ npm run prove:isolation -- --check
prove:isolation OK — 1224 requests, 6 witnesses, 674 refusals, none of which appended to the ledger; docs/proof/isolation.md reproduced byte for byte.
Enter fullscreen mode Exit fullscreen mode

A report that can disagree with the code is more useful than another green badge.

In my previous write-up, I looked at the responsibilities that remain after removing dependencies. Manak made that responsibility concrete at a different level: every stage can follow its local rules while the event as a whole is wrong.

The opening report had already demonstrated it. The server accepted a timestamp inside the stored window. The checker completed. The failed probe revealed that the window no longer represented the closed event. The repair was only a separate SQL binding, but understanding why it mattered required keeping all three facts together.

The score is the compact output of this work. What I want to outlive the leaderboard is the explanation: what the judges saw, what the model did with it and exactly which result the organizer approved. Building the portal meant making those answers survive each transition, including the ones introduced by my own setup code.

Source code · Demo · Demo video

Sai Ram Dash (Hardik), Nastik AI. Built for the Hackathon Raptors;

Top comments (0)