Status: implemented (TypeScript, Python, Java). All 99 vendored problems plus 18 authored problems (tries / math-geometry / bit-manipulation, under
authored/<slug>/index.ts, seeded byprisma/seed-authored.ts) have a case file underjudging/problems/and are judged in all three languages via the harnesses underharnesses/. Reference solutions used to gate every case file live inprisma/refs/(TS; authored problems use theirindex.ts),prisma/refs/py/, andprisma/refs/java/; run one withbun prisma/run-harness.ts <lang> <case.json> <solution>.
A problem's test cases are language-agnostic: twoSum([3,4,5,6], 7) → [0,1] is
true whether you solve it in Python, Go, or Rust. What's TS-specific in this repo
is only that the vendored cases are embedded inside TypeScript assertion code
(vendor/neetcode-150/<problem>/index.test.ts).
So instead of hand-writing a test suite per language (99 files × N languages), we:
- Extract each problem's cases once into a neutral JSON file.
- Author a manifest once per problem: entry point, argument/return types, comparator.
- Write one generic harness per language that reads the JSON, calls the solution, and emits a standard result protocol.
Adding a language then ≈ writing one harness + a few (de)serializers, reusing all the data and manifests. This is the harness+comparator layer DESIGN.md §5 deliberately skipped to ship TS-only fast — build it only when multi-language is the goal.
| Bucket | Count | What it needs |
|---|---|---|
| Plain value-in → value-out (ints/strings/arrays) | ~75 | Trivial extract; exact comparator |
Structured I/O (ListNode/TreeNode/graphs/grids) |
~24 | Per-language (de)serializers; structural comparator |
| Order-insensitive / multiple-valid output | ~11 | Per-problem comparator (sort/set/validator) |
| Design / sequence (LRU, MinStack, TimeMap) | a handful | sequence case shape (operations + per-call expected) |
(Buckets overlap.) The one-time cost is concentrated in the ~35 non-plain problems. The 75 plain ones are as trivial as they look.
One file per problem: judging/problems/<id>.json. It carries the manifest and
the cases so everything for a problem lives together.
args is a JSON array of the positional arguments. Values are plain JSON; the
harness reconstructs typed inputs from argTypes (see §3).
{
"id": "reverse-linked-list",
"kind": "function",
"entry": "reverseList",
"argTypes": ["ListNode"], // [0,1,2,3] is a serialized list, not an array
"returnType": "ListNode",
"comparator": "linkedlist",
"cases": [{ "name": "basic", "args": [[0, 1, 2, 3]], "expected": [3, 2, 1, 0] }]
}The harness deserializes [0,1,2,3] into the language's ListNode before calling,
and serializes the returned node back to an array before comparing. Trees use
level-order-with-nulls ([3, null, 9, 20]); grids use int[][] / char[][].
{
"id": "anagram-groups",
"kind": "function",
"entry": "groupAnagrams",
"argTypes": ["string[]"],
"returnType": "string[][]",
"comparator": "unordered-nested", // inner groups and outer list both order-free
"cases": [
{ "name": "basic", "args": [["act","pots","tops","cat","stop","hat"]],
"expected": [["act","cat"],["pots","tops","stop"],["hat"]] }
]
}For problems that accept any valid answer (e.g. "return any one pair"), use a
validator comparator (§2) instead of a fixed expected.
These are not one input→output; they're a sequence of method calls with an expected result per call.
{
"id": "lru-cache",
"kind": "sequence",
"class": "LRUCache",
"names": { "python": "LRUCache" },
"comparator": "exact",
"cases": [
{ "name": "evicts LRU",
"steps": [
{ "method": "constructor", "args": [2], "expected": null },
{ "method": "put", "args": [1, 10], "expected": null },
{ "method": "get", "args": [1], "expected": 10 },
{ "method": "put", "args": [2, 20], "expected": null },
{ "method": "put", "args": [3, 30], "expected": null },
{ "method": "get", "args": [2], "expected": -1 }
] }
]
}serialize-and-deserialize-binary-tree and string-encode-decode have
implementation-defined intermediate encodings, so asserting an exact encoded
string (as the vendored tests do) is too strict. kind: "roundtrip" judges
decode(encode(data)) == data on a fresh instance of class:
{
"id": "string-encode-decode",
"kind": "roundtrip",
"class": "Codec",
"encode": "encode", // method names
"decode": "decode",
"dataType": "string[]", // or "TreeNode" — (de)serialized via §3
"comparator": "exact",
"cases": [{ "name": "basic", "data": ["neet", "code"] }]
}Two optional manifest keys on kind: "function" problems:
"resultFrom": <argIndex>— the solution returns nothing; the harness serializes that (mutated) argument after the call instead of the return value (reorder-linked-list,islands-and-treasure,surrounded-regions)."mustCopy": <argIndex>— the case fails if the returned structure shares any node (identity) with that input argument (clone-graph,copy-linked-list-with-random-pointer), so returning the input can't pass.
judging/problems/ is a gitignored build artifact (~88MB with all generated
cases). The committed sources are:
judging/curated/<id>.json— canonical manifests + hand-authored cases (edit these, not the built files)judging/generators/<id>.ts— random + stress case generators
bun prisma/build-cases.ts builds whatever is missing (it runs automatically
via the predev/prestart npm hooks, so a fresh checkout self-heals on first
npm run dev; a full rebuild takes ~8s). npm run build:cases forces a full
rebuild. Generation is seeded per slug, so rebuilds are deterministic.
Hand-picked cases can't catch wrong-complexity solutions, so problems can carry machine-generated cases (LeetCode-style "hidden tests"):
judging/generators/<id>.ts— seeded random-input generator exportingcases(rng)(small/medium inputs), optionalstress(rng)(large inputs sized so one-complexity-class-worse solutions TLE within the 10s budget in every language, while the optimal Python solution stays well under it), andbrute(an independent second-oracle algorithm).bun prisma/generate-cases.ts <slug>...— computesexpectedwith theprisma/refs/<slug>.tsreference, cross-checks every non-stress case againstbruteunder the manifest's comparator, and appendsgen:/stress:cases to the case file (idempotent: re-running replaces them; RNG is seeded per slug). Case files with big stress arrays are written one-case-per-line, not pretty-printed.- Problems where the answer must be unique (two-integer-sum-ii's "exactly one solution") construct inputs that provably satisfy that (e.g. target = sum of the two largest distinct values).
- Bun's JIT is fast: an O(n²) loop over n=100k finishes in ~3s, so array stress
cases use n≈300k (~4.5e10 ops). Every stress case was negative-tested: the
canonical naive solution must actually get verdict
tle. - Caveat: binary-search-family problems can't TLE a linear scan at sane input sizes; their stress cases only add correctness coverage at scale.
comparator is one of:
| Name | Meaning |
|---|---|
exact |
Deep structural equality of JSON values. |
float |
Numeric equality within a tolerance (1e-6); also for arrays of floats. |
unordered |
Compare as multisets (sort then deep-equal). |
unordered-nested |
Order-free at both the outer and inner level (subsets, three-sum, group-anagrams). |
set |
Compare as sets (dedupe + order-free). |
linkedlist |
Serialize result ListNode → array, then exact. |
tree |
Serialize result TreeNode → level-order-with-nulls, then exact. |
validator:<name> |
No fixed expected; call a per-problem validator <name>(args, output) → bool. For multiple-valid-answer problems. |
Comparators live once in a language-agnostic sense but are implemented per language inside each harness's shared comparator module. The manifest only names the kind; each harness knows how to run it.
validator functions are the one piece of genuine per-problem logic that must be
ported per language (rare — only the handful of "any valid answer" problems).
Primitives and containers map directly to each language's natural type:
int, long, float, bool, string, char
int[], string[], int[][], char[][] (grid), ...
ListNode, TreeNode, Graph (adjacency form), GraphNode (clone-graph)
ListNode[] — array of serialized lists: [[1,2],[3]] (merge-k-lists)
ListNodeCycle — {"values":[1,2,3],"pos":1}; tail.next -> values[pos], -1 = none
ListNodeRandom — [[val, randomIndex|null], ...] (copy-random-list format)
GraphNode uses the LeetCode clone-graph adjacency form (adj[i] = the vals of
node i+1's neighbors, vals are 1..n); harnesses sort neighbor lists when
serializing a returned graph so neighbor order can't fail a case. In Python and
Java the graph/random-list node class is the LeetCode-style Node (injected /
compiled alongside); in TypeScript plain structural objects work.
Structured types (ListNode, TreeNode, Graph, GraphNode) require a small
(de)serialization module per language:
ListNode: array[a,b,c]↔ singly-linked chain.TreeNode: level-order array withnullholes ↔ binary tree.GraphNode: adjacency list (LeetCode clone-graph format) ↔ node graph.
This is the bulk of per-language work for the ~24 structured problems. Write it once per language; it's reused across every structured problem.
Each language ships one harness program under harnesses/<language>/:
harnesses/
typescript/ harness.ts serde.ts comparators.ts
python/ harness.py serde.py comparators.py
go/ harness.go ...
At judge time the executor writes into a temp dir: the user's solution, the
problem JSON (case.json), and copies the language harness in. The harness:
- Reads
case.json(path via argv) and imports the user's solution module. - Resolves the entry point:
names[language]or the language's natural casing ofentry/class. - For each case: deserialize
argsperargTypes, invoke the entry (or replaystepsforkind: "sequence"), serialize the result perreturnType. - Compare against
expectedusingcomparator. - Emit the standard protocol (below). Exit
0iff every case passed.
The harness prints one line per case plus a summary, each prefixed LLJUDGE:
LLJUDGE {"i":0,"name":"basic","status":"pass"}
LLJUDGE {"i":1,"name":"negatives","status":"fail","got":[],"want":[0,2]}
LLJUDGE-SUMMARY {"passed":1,"total":2}
A single parser lib/harnessparse.ts (sibling of lib/bunparse.ts) reads these
lines → { passed, total, failing[] }. This is the key win over parsing
bun test / pytest / go test output differently per language.
Crashes / compile errors → no LLJUDGE-SUMMARY line → verdict error (mirror the
bunparse "parsedOk" logic). Wall-clock timeout in the executor → tle.
lib/executor.ts— addjudgeHarness({ problemDir, language, solutionCode, caseJson }): temp dir → write solution (solution.<ext>) +case.json+ copyharnesses/<language>/*→spawnCapture(runtimeFor(language))→parseHarness(stdout+stderr). Reuse the existing timeout/cleanup machinery.runtimeForalready exists.lib/harnessparse.ts— new pure parser for theLLJUDGEprotocol; unit-test it likelib/bunparse.test.ts.app/api/judge/route.ts— acceptlanguage; loadjudging/problems/<id>.json; fortypescripteither keep the currentbun testpath or route through the harness (see §6); for others usejudgeHarness. Also acceptsmode: "run" | "submit"(run skips hiddengen:/stress:cases; nothing is logged) andcustom: unknown[][](user-supplied argument arrays; the expected answer is computed by the reference solution). Every returned test carries truncatedinput/expected/gotpreviews plus ahiddenflag, and function harnesses emitgoton pass as well as fail to support this.prisma/schema.prisma— replace the singleProblem.judged: Booleanwith per-language support, e.g.judgedLanguages Json(["typescript","python"]) or aProblemLanguagejoin table. The UI gates Submit on the selected language being in that set (today's gate incomponents/EditorPanel.tsxisisTs && problem.judged).- Starter code — generate per-language starters from the manifest
(
entry+argTypes+returnType→ a signature stub). This replaces the TS-ASTlib/starter.tsfor non-TS languages; TS can keep AST-derived starters.
- Protocol + parser first. Define
LLJUDGE, writelib/harnessparse.ts+ tests. - TypeScript harness as the reference. Build
harnesses/typescript/and a TS serde/comparator set. Authorjudging/problems/<id>.jsonfor a handful of problems by hand (one plain, one linked-list, one unordered, one sequence). - Regression-gate against the existing TS suite. Run both the current
bun testpath and the new harness path on those problems; verdicts must match. This proves the case data + comparators are faithful before scaling. - Extractor. Write a one-time script that parses each
vendor/.../index.test.tsand emits a draftjudging/problems/<id>.json. The ~75 plain problems extract cleanly; flag structured/sequence/comparator problems for human review — do not trust auto-extractedexpectedfor those. - Add a language (e.g. Python). Port the harness + serde + comparators. No new
case data needed — it reuses
judging/problems/*.json. - Per-language reference verification. Mirror
prisma/verify.ts: generate a known-correct reference solution per problem in the new language, run it through the harness, and only markjudgedLanguages += languagewhen all cases pass. (Recall 16/99 TS references were already buggy — expect a similar cleanup pass, and the auto-translated cases add their own risk. Verification is non-optional.) - UI. Flip the Submit gate to allow any language in
judgedLanguages; generate per-language starter code.
- Migrate TS onto the harness, or keep dual paths? Uniform (one parser, one
flow) vs. zero-regression-risk (leave the working
bun testpath alone). Lean: migrate after step 3 proves parity, so there's a single code path long-term. - Where do cases live? Files under
judging/(version-controlled, diffable, recommended) vs. a DB table. Files are easier to review and regenerate. validatorcomparators are the only per-problem logic ported per language. Keep them tiny and few; prefer a fixedexpected+ an order-free comparator wherever the problem actually has a unique answer up to ordering.- Coverage honesty. As with the TS gaps (see the
vendor-coverage-gapsmemory and DESIGN.md §15),log/surface which problems are judged in which languages so the UI never shows a green check it can't back up.
{ "id": "two-integer-sum", "kind": "function", "entry": "twoSum", // canonical name; per-language overrides below "names": { "python": "two_sum" }, // optional; default = camelCase `entry` "argTypes": ["int[]", "int"], // positional, matches the function signature "returnType": "int[]", "comparator": "exact", "cases": [ { "name": "basic", "args": [[3, 4, 5, 6], 7], "expected": [0, 1] }, { "name": "negatives", "args": [[-3, 4, 3, 90], 0], "expected": [0, 2] } ] }