---
title: mock-screens
pageLayout: default
slug: hinterland/prep/mock-screens
permalink: https://aarnphm.xyz/hinterland/prep/mock-screens.md
generator:
  quartz: v4.6.0
  hostedProvider: Cloudflare
  baseUrl: aarnphm.xyz
full: https://aarnphm.xyz/llms-full.txt
---
# mock screens

Four 60-minute mocks that mirror the composite flow in `00-recon/intel.md` section 2: one problem, ladder revealed rung by rung, later rungs held hostage to your pace. Same mechanics and the same rubric every time, so scores compare across days.

## mechanics

1. Set a countdown timer to 60:00 and stop mid-sentence when it fires.
2. Work in a blank scratch file, CoderPad style. The interviewer script below is your only spec. Do not open `notes.md`, `solutions.py`, or the stub docstrings during the hour, because the docstrings leak the exact error contract you are supposed to enumerate yourself.
3. Speak out loud the whole hour: the clarifying questions, the API contract, the roundtrip property, every edge case as you think of it. Communication is 20 of 100 points and silence forfeits them.
4. Write `assert decode(encode(xs)) == xs` style checks in the scratch file as you go, and run them.
5. After the timer, port your code onto the named stubs in `problems.py` (rename to the stub signatures) and run the harness filtered to the mock’s cases; the filter command is listed per screen. The harness is the correctness grader.
6. Grade with the rubric immediately, while you still remember where you hesitated.

Playing the interviewer: read each script line at its listed minute, and answer clarifying questions only from the script. When a checkpoint slips more than 5 minutes, skip forward and record the slip in the grade; that is exactly what a real interviewer does by silently shrinking your ladder.

## rubric, all four screens

| axis                   | points | scoring                                                                                                                                                                                                                      |
| ---------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| correctness on vectors | 40     | 40 times the pass fraction of the mock’s harness cases after porting                                                                                                                                                         |
| edge-case enumeration  | 25     | 3 points per class named unprompted (empty, zero, 127/128, width max, truncated, too long, overlong policy, chunk-boundary straddle, DoS bound), capped at 25; half credit when the interviewer had to prompt it             |
| code clarity and API   | 15     | masks named as constants, no magic numbers beyond `0x7F`/`0x80`, one consistent error policy, streaming state explicit and minimal                                                                                           |
| communication          | 20     | 5 each: clarifying questions asked in minutes 0 to 5; roundtrip property stated and tested first; each rung’s limitation volunteered before the interviewer raises it; complexity wrap that includes a bytes-per-value ratio |

Reading the total: 85 or above is a strong-hire signal. 70 to 84 passes with gaps, so redo the weak axis’s drills the next morning. Below 70, re-run the same mock two days later after re-solving its stubs cold.

Screen D swaps the encoding-specific rows’ content, same points: the edge-case classes are size-1 window, window not yet full, duplicates and negatives into the median, median before any add, float drift of the running sum, cost above capacity, backwards clock, and $\rho \ge 1$ instability; the complexity-wrap ratio is a hockey-stick multiple instead of bytes-per-value.

## screen A: the varint ladder (Datadog shape)

Stubs to solve cold, in order, all in `03-varint/problems.py`: `encode_uvarint`, `decode_uvarint`, `decode_uvarint_seq`, `zigzag_encode`, `zigzag_decode`, `encode_svarint`, `decode_svarint`.

Grade with: `python3 test_problems.py 2>&1 | rg -i 'uvarint|svarint|zigzag|seq'`

| minutes | phase     | interviewer script                                                                                                                                                                                                                                                         |
| ------- | --------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0-5     | clarify   | ”Encode a list of unsigned integers into a byte array, and decode it back. You choose the format.” Answers if asked: unsigned for now; values fit in u64; output is `bytes`; decode input is untrusted; one-shot buffer.                                                   |
| 5-25    | core      | ”Start simple: every integer is between 0 and 255.” Expects the one-byte codec plus a running roundtrip test within minutes. At 15: “what breaks this?” (wants: 256 does, and a named fix: length prefix or continuation-bit varint).                                      |
| 25-45   | extension | At 25: “values can now be any u64, and small values should stay small.” (wants LEB128; length prefix is acceptable if the tradeoff is argued.) At 38: “now negatives, and $-1$ must stay cheap.” (wants zigzag; say that two’s-complement-as-u64 spends 10 bytes on $-1$.) |
| 45-55   | hardening | ”I am going to feed your decoder garbage.” Cases in order: `[]` and `b''`; `[0]`; `[127, 128]`; `[2**64 - 1]`; `b'\x80'`; `b'\x80' * 10 + b'\x01'`; `b'\x80\x00'`. Expects a stated policy per malformed class before the guard is coded.                                  |
| 55-60   | wrap      | ”Complexity, and when would this encoding be the wrong choice?” (wants: O(total bytes) time, O(1) state, 1 to 10 bytes per value, a 2-4x cut against fixed width on small-int data; fixed width wins on uniformly large values and on random access.)                      |

Checkpoints. Minute 12: API stated and the roundtrip skeleton written. Minute 25: one-byte codec passing its own test with the 256 limitation already volunteered. Minute 40: uvarint pair passing 0, 1, 127, 128, 300, 16383, 16384. Minute 50: svarint round-trips $-1$ in one byte. Minute 55: the three malformed classes named (truncated, too long, overlong) with distinct handling.

## screen B: reader, then deframer (two-question format)

Stubs to solve cold, in order: `BinaryReader` in `04-byte-streams/problems.py` (methods in order: `__init__`/`tell`/`seek`, `read_u8`, `read_u16_le`, `read_u32_le`, `read_bytes`, `read_cstring`, `read_uvarint`), then `FrameDeframer` in `05-streaming/problems.py`.

Grade with: `python3 test_problems.py 2>&1 | rg 'BinaryReader'` in 04, and `python3 test_problems.py 2>&1 | rg -i 'deframer'` in 05.

| minutes | phase     | interviewer script                                                                                                                                                                                                                                                                                                                                                                                      |
| ------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0-5     | clarify   | Q1: “Build a sequential reader over a byte buffer: `read_u8`, `read_u16_le`, `read_u32_le`, `read_bytes(n)`, `read_cstring`, `read_uvarint`, plus `tell` and `seek`. Any failed read must leave the cursor where it was.”                                                                                                                                                                               |
| 5-25    | core      | Silent until 15, then: “the buffer has 2 bytes left and I call `read_u32_le`. Where is the cursor afterwards?” (wants: unmoved; check length before consuming, advance only on success.) At 20: “bound `read_uvarint`: at most 10 bytes, value below $2^{64}$.“                                                                                                                                         |
| 25-45   | extension | At 25, Q2: “New question. Bytes arrive from a socket in arbitrary chunks. The stream is repeated frames of `<uvarint length><payload>`. Write `feed(chunk) -> list[bytes]` returning the payloads completed by this call, and `finish()`.” At 35: “a frame header claims 1 GiB. What does your code do, and at which byte?” (wants: raise the moment the length decodes, before buffering any payload.) |
| 45-55   | hardening | ”Feed the same stream one byte at a time. Same output?” Cases: zero-length frames yield `b''`; the length varint split across chunks; a payload split across chunks; `finish()` with a dangling half frame (torn mid-length and torn mid-payload); then the challenge “your `buf = buf[n:]` is quadratic, fix the bookkeeping.”                                                                         |
| 55-60   | wrap      | ”Memory bound of the deframer, and work per byte?” (wants: at most max\_frame plus one varint buffered, O(total bytes) work via an offset cursor or chunk list instead of re-slicing.)                                                                                                                                                                                                                  |

Checkpoints. Minute 25: `BinaryReader` fixed-width reads and `read_uvarint` passing, with a test that shows the cursor unmoved after a failed read. Minute 45: deframer correct when the whole stream arrives in one `feed`. Minute 55: identical output byte-at-a-time; the harness all-splits cases are the judge.

## screen C: timeseries codec, then streaming UTF-8 (the twist)

Stubs to solve cold, in order: `delta_varint_encode` and `delta_varint_decode` in `06-codecs/problems.py`, then `Utf8StreamValidator` in `05-streaming/problems.py`.

Grade with: `python3 test_problems.py 2>&1 | rg 'dv_'` in 06, and `python3 test_problems.py 2>&1 | rg -i 'utf8'` in 05.

| minutes | phase                 | interviewer script                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ------- | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0-5     | clarify               | ”Compress a list of int64 timestamps. Mostly increasing, small gaps, occasional backwards jitter. Decode must be exact.”                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| 5-25    | core                  | Expects the stack named before coding: delta, then zigzag for negative gaps, then uvarint. At 12: “why zigzag instead of encoding the delta’s two’s complement directly?” (wants: $-1$ as u64 is 10 varint bytes, zigzag gives 1.) At 20: “walk `[1000, 1005, 1004, 1010]` onto the wire.” (wants `D0 0F 0A 01 0C`, 5 bytes against 32 raw.)                                                                                                                                                                                                                                                         |
| 25-45   | extension, then twist | At 25: “first value $-2^{63}$, second value $2^{63} - 1$. Does your encoder survive?” (wants: the gap is $2^{64} - 1$, outside int64, so compute deltas mod $2^{64}$; wrapped it is $-1$, one byte after zigzag, and the decoder adds mod $2^{64}$ to recover exactly. C unsigned subtraction wraps free, Python needs `& M64`.) At 35, Q2: “Park the codec. UTF-8 bytes arrive in chunks: `feed(chunk) -> bool` meaning valid so far, `finish() -> bool`. Strict mode: overlongs, surrogates, and anything above U+10FFFF are invalid. A sequence torn at a chunk edge stays valid until `finish`.” |
| 45-55   | hardening             | Q2 cases: `C3` then `A9` in separate feeds is valid; after `E0`, a continuation below `A0` rejects (overlong); `ED A0 80` rejects (surrogate); after `F4`, a continuation above `8F` rejects (beyond U+10FFFF); once invalid, every later `feed` returns False; `finish()` with a dangling lead byte returns False.                                                                                                                                                                                                                                                                                  |
| 55-60   | wrap                  | ”How much state between feeds, in bytes?” (wants: O(1), a remaining-continuation count plus the first-continuation range constraint, never a buffered sequence. For Q1, the 6.4:1 ratio on the worked vector.)                                                                                                                                                                                                                                                                                                                                                                                       |

Checkpoints. Minute 25: the codec round-trips the worked vector and the 5-against-32 ratio was said out loud. Minute 35: int64-extreme wrap handled with its errors named (truncated, too long, overflow). Minute 50: the validator passes every split case above. Minute 55: both error taxonomies stated, codec and UTF-8.

## screen D: metrics stream, then rate limiter (Datadog tech-screen alternate)

Stubs to solve cold, in order: `MovingAverage` then `StreamingMedian` in `07-stream-algorithms/problems.py`, then `TokenBucket` in `08-queueing/problems.py`.

Grade with: `python3 test_problems.py 2>&1 | rg -i 'movingaverage|median'` in 07, and `python3 test_problems.py 2>&1 | rg -i 'token bucket'` in 08.

| minutes | phase     | interviewer script                                                                                                                                                                                                                                                                                                                                                                                                                                                                                               |
| ------- | --------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 0-5     | clarify   | ”A metric stream arrives one float at a time. Return the average of the last k values after every arrival, O(1) per value, bounded memory.” Answers if asked: k is fixed at construction; before k values arrive, average what you have; values untrusted only in magnitude, not type.                                                                                                                                                                                                                           |
| 5-20    | warmup    | Expects a circular buffer plus a running sum within minutes, with a running test. At 12: “why not keep a deque and sum it each call?” (wants: O(k) per call against O(1); and the running sum drifts in float over long streams — name periodic recompute or Kahan as the fix.)                                                                                                                                                                                                                                  |
| 20-40   | core      | At 20: “New question. Same stream, now the exact running median after every arrival.” (wants two heaps: max-heap low half, min-heap high half, sizes within 1, add O(log n), median O(1).) At 30: “duplicates? negatives? median before any value?” At 35: “and the median over only the last k values?” (wants: lazy deletion, LC 480 — discussion only, no code.)                                                                                                                                              |
| 40-52   | extension | At 40: “The intake needs a rate limiter: sustained r per second, bursts up to B, per client key.” (wants token bucket with lazy refill: store (tokens, last), add (now - last) \* r capped at B, all-or-nothing deduct; two floats per key, no timers, no threads, injected clock.) At 48: “one request costs more than B. What happens?” (wants: never admissible no matter how long the bucket idles; reject up front, do not wait or loop.)                                                                   |
| 52-60   | wrap      | ”Your service runs at 95 percent utilization and p50 just doubled. What happened, and what do you do?” (wants: at $\rho = 0.95$ mean sojourn is already $20\,E[S]$ and the curve is convex, so a small drift in arrivals or service time moves latency in multiples — and retries amplify $\lambda$ exactly then; respond by shedding at admission with a bounded queue and adding capacity, since near $\rho = 1$ a few percent capacity halves the wait; sanity-check with $L = \lambda W$ over one boundary.) |

Checkpoints. Minute 20: `MovingAverage` passing the size-3 vectors with the deque limitation and float drift volunteered. Minute 40: `StreamingMedian` correct on duplicates and descending inserts, both complexities stated. Minute 52: `TokenBucket` admits burst-then-sustained, clamps a backwards clock, and rejects cost above capacity without looping. Minute 58: the hockey-stick row said from memory ($\rho$ 0.5/0.8/0.9/0.95/0.99 gives 2/5/10/20/100 service times).

