Interview Kit que s'executa en un portàtil durant una entrevista tècnica

He construït una app que el comitè de contractació no obrirà mai

El que veu el comitè, i el que no veu

Un PDF de 7 pàgines adjunt a un correu. Aquesta és tota la superfície que el comitè de contractació arriba a tocar. Ni l’assistent, ni els temporitzadors, ni el desat automàtic, ni les dreceres de teclat, ni les puntuacions amb codi de colors. L’app existeix per a una sola persona: l’entrevistador, durant la trucada.

El PDF reuneix tota la feina: puntuacions, notes, fortaleses i àrees de millora, la decisió de contractar o no, i quatre apèndixs de proves. Tot el que el comitè necessita per fer una oferta o passar al següent.

La majoria de comitès no es posaran a treballar amb una eina de revisió de codi, i seria estrany demanar-los-ho. La seva feina és la decisió, no la introducció de dades. La feina de l’app és que les dades que arriben a la pàgina mereixin la seva atenció.

Arribar fins aquí va costar dos formats, una idea descartada abans de començar, i un bug que hauria deixat passar una puntuació equivocada si una ronda de prova no l’hagués caçat.

El problema del format

Vaig dissenyar els scorecards per al nostre procés de contractació en React Native a principis d’aquest any. Tres avaluacions: una revisió de codi de 100 checks, una entrevista de walkthrough puntuada d’1 a 5, i una entrevista conductual mapejada als nostres cinc valors. La puntuació funcionava. El format que estava utilitzant per capturar aquestes puntuacions durant una trucada en directe no.

L’entrevista de walkthrough és la part més difícil. Un candidat comparteix la seva pantalla, et mostra la prova tècnica que ha construït, explica les seves decisions. Jo he de llegir una pregunta del guió, escoltar, puntuar-la d’1 a 5, escriure notes, mirar el rellotge i passar a la següent. Tot això mantenint el contacte visual i fent que la conversa flueixi amb naturalitat.

Prova-ho en una taula de markdown dins de VS Code. El cursor cau a la cel·la equivocada. La posició de scroll es desplaça. El candidat sent que escrius i va més a poc a poc.

Notion, markdown i una app de React

Notion va ser la meva primera idea. L’utilitzo per a totes les coses personals. Però no és una eina que fem servir a la feina, i construir sobre una plataforma que seria només per a mi semblava un carreró sense sortida. Vaig descartar la idea abans de començar.

Després van venir els fitxers markdown. Un .md per scorecard, taules per a les puntuacions, espai per a notes. La revisió de codi funcionava bé així. 100 checks de sí o no que completes després de l’entrevista al teu propi ritme. Els scorecards de walkthrough i conductual havien de funcionar durant la trucada, i el markdown s’hi posava pel mig. Trobar la fila correcta, escriure un número, fer scroll a la següent secció. Precís, però lent. Prestava més atenció al document que a la persona.

Després de l’entrevista era pitjor. Tres fitxers markdown separats, amb tres formats diferents, cosits a mà en un únic document coherent per a l’equip de selecció. Cada vegada trigava més del que volia.

Al final va arribar una app de React en localhost. Sense backend, sense base de dades, sense desplegament. Només npm run dev i una pestanya del navegador. Tot queda desat a localStorage. L’app mor quan tanco la pestanya i torna quan l’obro de nou.

Estar present durant la trucada

L’objectiu era deixar de barallar-me amb l’eina durant l’entrevista. Tres coses van marcar la diferència.

Una pregunta per pantalla. El walkthrough és un assistent pas a pas. Cada pas mostra el guió per llegir en veu alta (en un blockquote blau perquè el trobi a l’instant), les preguntes amb botons grans d’1 a 5, i un camp de notes. Sense scroll. Sense buscar la secció correcta. Premo “Següent” i apareix el següent grup. Per a candidats sèniors, l’assistent s’amplia de 4 passos a 8 amb una Part B addicional sobre disseny de sistemes.

Puntuació amb teclat. Premo d’1 a 5 i la puntuació es registra a l’instant. Sense clics, sense menús desplegables, sense diàlegs de confirmació. Els meus ulls no es mouen de la videotrucada. La puntuació la faig de cua d’ull.

Un temporitzador de secció a la cantonada. No és un compte enrere, només una visualització discreta del temps transcorregut. El vaig mirar durant el primer walkthrough i em vaig adonar que havia passat 8 minuts en una secció que hauria de durar-ne 4. Sense ell m’hauria passat de temps i hauria hagut de retallar l’última secció. El candidat hauria perdut l’oportunitat de respondre preguntes que podrien haver-li pujat la nota.

El bug que puntuava tot igual

L’stack era React 19, TypeScript, Vite i Tailwind v4. La primera versió no tenia biblioteca de gestió d’estat: un hook personalitzat useLocalStorage i React Router.

Durant les proves, vaig puntuar el walkthrough d’un candidat de principi a fi. Cada secció, cada pregunta, notes completes. Vaig prémer “Següent” per arribar a la pantalla de resum i vaig veure que totes les seccions tenien la mateixa puntuació: la que havia introduït a l’últim pas.

Un bug de stale closure. El useCallback de cada pas de l’assistent capturava les dades del walkthrough del render anterior. Quan el pas 3 desava, sobreescrivia els passos 1 i 2 perquè continuava utilitzant l’estat antic.

La solució va ser deixar de confiar, a l’hora d’escriure, en la visió que React té de les dades. Cada mutació llegeix l’estat actual del candidat directament des de localStorage en lloc d’un closure capturat. Un helper freshCandidate() que crida localStorage.getItem a cada desat. No és elegant. Funciona sempre.

function freshCandidate(id: string): Candidate | undefined {
  const raw = localStorage.getItem('ik-candidates');
  if (!raw) return undefined;
  return JSON.parse(raw).find((c: Candidate) => c.id === id);
}

El mateix patró es repeteix en tres hooks: useWalkthrough, useBehavioural i useCodeReview. Cadascun llegeix l’estat al moment, l’escriu al moment, i dispara un esdeveniment personalitzat (ls-sync) perquè les altres instàncies del hook detectin el canvi. Vint línies de codi de persistència. Sense Redux, sense context providers, sense middleware. Aquelles vint línies van fer anar les primeres entrevistes reals; l’store es va formalitzar més tard amb Redux Toolkit a mesura que l’app creixia.

El PDF que ningú no em veu construir

Després de l’entrevista, premo “Imprimir / PDF” i el navegador genera un Candidate Assessment Report. Sense biblioteca de PDF. Només CSS d’impressió.

La pàgina 1 és el resum: una taula de puntuacions, el nivell recomanat, la decisió de contractar o no, i el nivell de l’oferta. Les pàgines 2 i 3 mostren fortaleses i àrees de millora extretes de les tres avaluacions, agrupades per origen. Després, quatre apèndixs: desglossament de la revisió de codi, puntuacions del walkthrough amb cada pregunta i nota, puntuacions conductuals per valor, i una taula de referència dels nivells amb el del candidat ressaltat en blau marí.

Aquesta taula de nivells mapeja la puntuació combinada a un dels 12 graons: des de Graduate 1 fins a Senior 2+. (Els quatre nivells del post de l’scorecard es van subdividir en tres graons cadascun quan els candidats reals van començar a caure entremig.) El graó 2+ és deliberadament difícil d’assolir. Marca algú al cim de la seva categoria, i l’empeny cap a la següent. Quan un membre del comitè veu “Associate 2+” al PDF, la lectura és immediata: Associate fort, no del tot SE. Aquesta sola etiqueta diu més que un paràgraf de justificació.

El filtre conductual afegeix un segon control. Un candidat que puntuï per sota de 10/25 en valors no avança, independentment de la seva puntuació tècnica. Entre 10 i 14 obliga a una discussió del comitè. 15 o més passa el filtre. Les habilitats tècniques es poden ensenyar. Els desajustos de valors creen problemes que creixen amb el temps.

El CSS d’impressió és una disciplina a part

Vaig escriure més CSS per a @media print que per a pantalla. És la part que més em va sorprendre.

El fons blau marí del quadre de puntuació combinada? No s’imprimeix. Els navegadors eliminen els colors de fons per defecte (print-color-adjust: exact ho anul·la per element, tot i que cada motor fa la seva i el WebKit antic demana el prefix -webkit-). El vaig convertir, en canvi, en un quadre blanc amb una vora negra gruixuda utilitzant selectors [style*="background: #002147"] al full d’estils d’impressió, perquè la llegibilitat de l’informe no depengui mai d’un ajust d’impressió. Les classes utilitàries de Tailwind com bg-white necessiten selectors d’atribut ([class*="bg-white"]) per sobreescriure el padding, les vores i els marges en la impressió.

page-break-inside: avoid és un suggeriment, no una ordre. El navegador trencarà dins d’un element si l’alternativa és una pàgina gairebé buida. Vaig passar una hora depurant per què una secció de fortaleses es partia en dues pàgines abans d’adonar-me que el contingut era simplement massa alt per a l’espai restant.

Els estils dels encapçalaments necessitaven un border-bottom inline explícit perquè el reset d’impressió sobreescriu les declaracions de les utilitats de Tailwind. Les mides de font canvien de rem a pt. Els elements interactius (textareas, checkboxes, dropdowns) s’amaguen. Tot el layout d’impressió viu en un component CandidatePrintReport separat que es renderitza dins de hidden print:block. Separació neta. La pantalla no veu mai el layout d’impressió, la impressió no veu mai els botons.

Si ho construís una altra vegada, dissenyaria primer el layout d’impressió i després el de pantalla. El PDF és l’artefacte que importa. La pantalla és el formulari d’entrada.

El que canviaria

Tests abans que la lògica de puntuació. Les deduccions per red flags, els bonus per stretch goals, els lookups dels nivells, el llindar del filtre conductual. Ara són funcions pures, extretes a utils/scoring.ts, i són el tipus de codi que es trenca en silenci quan toques un límit. Les vaig escriure al final. Haurien d’haver vingut primer.

El parser d’importació de markdown és fràgil. Utilitza regex per llegir els valors Y/N dels fitxers de revisió de codi puntuats. Funciona per al format concret que vaig dissenyar, però una disposició de taula diferent o una columna extra i es trenca. Un parser de debò amb recuperació d’errors aguantaria millor.

L’accessibilitat va arribar tard. La feina de WCAG AA (títols de pàgina per ruta, jerarquia d’encapçalaments, ràtios de contrast de color, roving tabindex als selectors de puntuació, aria-live als indicadors de desat) es va afegir després en lloc d’estar incorporada des del principi. Hauria estat més net construir-la accessible des del primer dia. Les eines internes mereixen els mateixos estàndards que les públiques.

La prova real

Vaig utilitzar aquesta app per primera vegada en una entrevista real la setmana passada. El candidat no sabia que estava fent servir res d’inusual. Va presentar la seva entrega del take-home, jo vaig puntuar, vam parlar. Sense scroll, sense escriure en taules de markdown, sense perdre el fil. Després de la trucada, una sola premuda de botó i el PDF estava llest.

Aquest és l’objectiu. La puntuació, els temporitzadors, els càlculs de nivell i la generació del PDF queden al marge: el comitè llegeix el PDF, el candidat té una conversa, i l’eina es manté en silenci entre tots dos.

Warren de Leon
Warren de Leon

Software Engineering Manager. Recentment he liderat l'equip de Mobile Platform a Hargreaves Lansdown. Escric sobre lideratge tècnic, React Native i com construir bons equips.

Veure perfil