I10 · Part 2 · How it runs
The mock interview engine
A loop planner, a pure session reducer and a hiring committee, all in the browser.
10On this page
The pieces
/mock runs a whole interview loop: a recruiter screen, coding graded by real tests,
the technical deep dives, system design, behaviour and the offer conversation. The logic is in
twelve modules under lib/mock, and none of them import React. The UI in
app/mock is a lobby, a room, a debrief and a readiness dashboard. The engine is pure
functions over plain data, so the 66 test cases in the four tests/mock*.test.ts
files can drive a whole loop without rendering anything.
The bank stays on the server
lib/mock/bank.ts assembles the questions. For talk rounds it reads the interview
rounds in content/interview-data.ts and the JavaScript and React question banks, and
it parses each answer's seniority ladder out of its HTML. For coding rounds it wraps the site's
exercises, with function exercises for the coding stage and component exercises for machine
coding. That is a lot of content, and the lobby needs almost none of it.
So only two app files import the bank's functions. app/mock/page.tsx is a server
component that calls mockCatalog() and passes the lobby a summary with titles, who
runs each stage, and a count of questions. app/mock/bank/[stage]/route.ts is a
route handler with dynamic = "force-static", dynamicParams = false and
generateStaticParams over the 12 stages, so the build writes one JSON file per stage
and any other stage name is a 404. The client files import only types from the bank. There is no
server-only guard package; the import graph is what keeps the bank off the client.
On the client, fetchStage keeps one promise per stage and forgets it if the fetch
fails, so a retry works. The lobby starts fetching the plan's stages when the reader hovers or
focuses Start, and loads the room's code in idle time. The commit that set this up measured the
lobby going from 628 KB of script and 462 KB of question fetches to 237 KB and 12 KB.
Planning a loop
A loop is configured by role (frontend, full-stack, backend), seniority (junior, mid, senior),
company type (service, product, SaaS, agency) and intensity (quick, standard, full).
planLoop walks the 12 stages in order and applies each stage's rule from
STAGE_RULES: which roles and seniorities it applies to, a weight (essential,
standard or full) compared against the intensity, and whether that kind of company actually runs
the round. A plan with no coding round gets one inserted.
| Intensity | Talk questions per stage | Coding problems per stage |
|---|---|---|
| Quick | 2 | 1 |
| Standard | 3 | 2 |
| Full | 4 | 2 |
Machine coding gets one problem, or two on a full loop. Time slots are 3, 4 or 5 minutes per spoken answer by seniority, 15 minutes per coding function and 20 per component.
lib/mock/styles.ts adds four company styles on top: Amazon-style, big tech, product
startup and service company. A style can force stages in or out, add questions to a stage, mark
stages as core, prefer certain source rounds, ask for harder coding, and name stages where a
lean-no is a veto. Amazon-style, for example, forces a behavioural stage, adds two questions to
it, prefers the Leadership Principles round, and puts it under a veto.
Building a session
buildSession takes the plan, the fetched banks and a seed. The random source is a
32-bit xorshift, so one seed always gives the same loop, which is what makes the tests
deterministic. pickItems shuffles each pool and ranks items by fit. A talk question
scores higher when its level matches the reader's seniority or when the style prefers its source,
and is dropped when it is marked only for other seniorities or companies. A coding problem scores
on level and on topic: JavaScript and React for a frontend loop, data structures otherwise. No question is asked twice in a
loop. Each talk question that has follow-ups gets one, drawn at random from its list.
The reducer
A session moves through four steps per question: brief (the stage introduction),
answer, followup and review. reduce(session,
action) handles twelve action types, and every case checks the step first, so a stray
click in the wrong step is ignored. MockApp keeps the session in React state,
dispatches into this pure function, and writes the session to
groundwork:mock:current 250 ms after each change. A reload offers to resume, and
resuming restarts the clock on the current question.
The adaptive interviewer
While building, every question after the first in a stage gets two alternates: one harder and
one easier, chosen from the same pool and reserved so nothing else takes them. When the reader
moves on, shiftFor reads the score just earned. At 0.8 or above the next question is
swapped for the harder alternate, and below 0.45 for the easier one. Those two thresholds live in
lib/adaptiveThresholds.ts, which the DSA placement shares, so raising and lowering
mean the same thing in both. It only happens within a
stage, and never in a retry round.
persona.ts gives each of the 12 stages an interviewer with a name, a role and a tone
(warm, sharp or calm). Each tone has three opening lines and its own way of saying the next
question is harder or more basic. An Amazon-style loop replaces the behavioural interviewer with
a Bar Raiser.
Scoring
Coding is scored by the tests: the fraction passing, minus 0.05 per hint up to 0.15, and capped at 0.3 if the reader opened the solution. Spoken answers are scored by the reader against a rubric built for that question, each criterion marked 0, a half or 1.
| Criterion | Weight | Included when |
|---|---|---|
| Answered what they were really testing | 2 | The question says what it tests |
| Had the substance of the model answer | 3 | Always |
| Stayed clear of the trap | 2 | The question names a trap |
| Held up when pushed | 2 | A follow-up was asked |
| Structured and concise | 1 | Always |
A score maps to a verdict: strong hire from 0.8, hire from 0.65, lean no from 0.45, and no hire
below that. decideLoop then acts as the hiring committee. It weights core stages
twice as heavily as the others, then applies its rules in order:
- A lean-no in a stage the style vetoes is a no hire. This is the Bar Raiser rule.
- A no-hire in any core stage is a no hire, whatever the average says.
- Two or more lean-nos anywhere make a lean no.
- Otherwise the weighted average decides.
A hire can still come with a level call. Above junior, a design round under 0.65 turns the offer into one at the level below. Every round at strong hire, plus a design score of at least 0.8 above junior, makes the case for the level above.
Pacing
pacing.ts reads the timestamps. A spoken answer is marked rushed if it took less
than 45 seconds or a fifth of its slot, whichever is shorter. Two or more rushed answers earn a
note in the debrief, and so does any answer that ran past the clock. The debrief can also be
copied as plain text with the verdict, each round, the reasons, the pacing and the five weakest
questions.
The design-round whiteboard
The system design stage has a small whiteboard of its own in app/mock/Whiteboard.tsx,
not the full board from /whiteboard. Its model in lib/mock/board.ts has
four shape kinds (box, database, queue, text), six stencils (client, load balancer, service,
database, cache, queue), arrows between shape ids, and a fixed 800 × 420 canvas that shapes
are clamped inside. The board is stored on the question itself through a board
action, so it is saved and resumed with the session.
Storage and readiness
When a session finishes, mockStore.finish runs the committee once more and appends
a summary to groundwork:mock:history, which keeps the last 50. Questions scoring
under 0.6 join a retry queue at groundwork:mock:retry, which holds up to 60, and
questions scoring 0.8 or more leave it. The store exposes subscribe and a cached
snapshot for useSyncExternalStore, with an empty snapshot on the server.
The readiness dashboard is computed from that history. Each competency's score is an average in which a session's weight halves every 14 days, so a session four weeks old counts a quarter as much as one from today. The overall score falls in one of four bands (not yet, getting there, interview-ready, strong) at the same cut-offs as the verdicts. The dashboard also draws trends, a heatmap of the last 10 sessions, and a streak, and suggests what to do next: the weakest competency's weakest stage, or a coding or design round never tried.
Nothing here sends an answer to a model or a server. The rubric puts the model answer, what the question tests and the trap next to the reader's notes, and asks for an honest mark. That keeps the feature free and private and makes it work offline. The cost is that the score is only as honest as the person marking.
Finished reading?
0 of 26 chapters read in this series