Staff-engineer audit. Audit only — no code was modified. Date: 2026-07-01. Branch: main, clean (only untracked .claude/), up to date with origin/main.
- What it is: a client-side React single-page app — a static marketing/reservation site, not a library or service. Deployed to GitHub Pages under base
/Capstone-react/usingHashRouter. Confirmed fromApp.js,vite.config.mjs,package.json(gh-pages -d dist). - Toolchain (from lockfile/manifest): React 18.3.1, react-router-dom 6.25.1, Vite 5.4.21, Vitest 2.1.9, Testing Library. JavaScript (not TypeScript). JSX kept in
.jsfiles (CRA legacy), parsed via an esbuild loader override invite.config.mjs. - Build: ✅ passes —
vite build, 66 modules, ~470ms, assets emitted under/Capstone-react/. Verified the builtdist/index.htmlcorrectly rewrites favicon/manifest to the base path (no 404 there). - Tests: ✅ 8/8 pass across 3 files (
App.test.js1,Booking.test.js3,BookingForm.test.js4). - Lint / typecheck / CI: ❌ none. No ESLint/Prettier config, no lint script, no
.github/workflows. - Security:
npm audit→ 5 vulns (1 critical, 1 high, 3 moderate), all in the dev toolchain (esbuild/vite/vitest). Verified vianpm audit --json: none are in code shipped to the static build.
This is a competent, working L1 app with L0 scaffolding gaps and a coursework residue that reads at L2/L3. The architecture is sound for its size — clean component decomposition, a real useReducer + mock-API pattern that matches the Meta spec, an accessible booking form (aria-invalid/aria-describedby/role="alert"), a proper focus-managed modal, and the correct HashRouter choice for GitHub Pages project pages. Nothing is structurally broken; the build and tests are green. The gap to a top-tier showcase is not correctness — it's maturity and finish: there is no lint/CI gate (the single biggest deep gap for "something others depend on"), the one real domain feature (booking) has a correctness hole (past dates accepted) and tests that don't actually exercise submit, and the repo still carries visible tutorial scaffolding (dead reportWebVitals, CRA-default manifest.json, // Add this line comments, README pointing at the wrong port and the wrong test runner). Deep-first: close the L0 tooling gap and the L1 date bug before polishing.
FOLLOW-UP [L0 · Works→Senior] (Observed) — No CI pipeline. ls .github/workflows → absent.
For a repo positioned as depended-on, there is no automated gate that build/tests stay green.
→ Add a GitHub Actions workflow running npm ci && lint && test && build on PR/push.
FIX-BEFORE-MERGE [L0 · Works→Senior] (Observed) — No linting/formatting layer at all. The CRA→Vite migration (commit 6b215fe) dropped the built-in ESLint and nothing replaced it. A React portfolio with zero linting reads as unfinished.
→ Add ESLint (eslint-plugin-react, react-hooks, jsx-a11y) + Prettier; wire a lint script.
Update (fix in progress — ESLint 9 flat config landed): Adopting the gate corrected and surfaced several things:
- The two
eslint-disable-next-line react-hooks/exhaustive-depsdirectives atBookingForm.js:63/Nav.js:30are not dead —reportUnusedDisableDirectivesconfirms they still actively suppress a realexhaustive-depswarning. The earlier "dead directives" note was an inference and is withdrawn.react-in-jsx-scopefired onApp.test.js(the one file not importing React); fixed with a one-line import to match the repo's convention.- Prettier is split into its own finding (see ranked list): a
--writesweep rewrites every file's indentation and would bury the gate setup in an unreviewable diff.- New violations surfaced and demoted to warnings for a green baseline — see Lint-surfaced debt below.
RESOLVED [L0] (Observed) — JSX used to live in .js files, needing an esbuild loader hack in vite.config.mjs. This was fine on esbuild-based Vite (≤7) but Vite 8 replaces esbuild with Rolldown/oxc, which ignores the hack (JSX syntax is disabled). Fixed (see #11): the 14 JSX-bearing files were renamed to .jsx and the loader hack removed, so Vite handles them natively — unblocking the Vite 8 upgrade in #8. (TypeScript remains a further, optional modernization.)
FIX-BEFORE-MERGE [L1 · Works→Correct] (Observed) — Booking form accepts past dates. src/components/BookingForm.js — the date InputField has no min, and validateField('date') only checks non-empty (:84). You can reserve a table for yesterday. This is the core domain rule of a reservation app.
→ Set min={todayISO} on the date input and reject date < today in validateField.
FOLLOW-UP [L1 · Works→Correct] (Observed) — Time can submit empty and is never validated. Corrected: I enumerated the seeded fetchAPI for every day-of-month (1–31); the minimum is 4 slots and it is never empty, and time always defaults to a valid slot. So an empty-time submission is not reachable via the current mock — the earlier claim is withdrawn.
→ Still worth a defensive guard (the mock explicitly stands in for a real backend): time-required validation + a disabled "no availability" placeholder were added, but they are defensive, not fixing a live bug.
FOLLOW-UP [L1 · Works→Senior] (Observed) — Dev-toolchain vulnerabilities: 1 critical (Vitest UI arbitrary file read/exec — UI server not used here), 1 high + 3 moderate (Vite/esbuild dev-server path traversal & request bypass). Verified all are dev-only; none ship in the static dist/ build, so live-site risk is ~nil. But a depended-on repo should stay current.
→ Bump Vite 5→7 and Vitest 2→3 (npm audit fix --force equivalent), then re-run the suite.
FIX-BEFORE-MERGE [L2 · Works→Correct] (Observed) — README inaccuracies. README.md:56 claims the dev server runs at http://localhost:3000 (Vite serves 5173, and under /Capstone-react/). README.md:68 says tests use "React Testing Library + Jest" — the project runs Vitest. Anyone following the README hits friction immediately.
→ Fix the port/path and the test-runner name.
FIX-BEFORE-MERGE [L2 · Works→Showcase] (Observed) — Tutorial residue in source. src/components/BookingForm.js carries // Add this line ×4 (:147,:159,:171,:192); src/App.test.js still has // Replace "learn react" with actual text…. These read as pasted-from-coursework rather than authored. Cheap, high-signal cleanup.
→ Delete the instructional comments.
FOLLOW-UP [L2 · Works→Showcase] (Observed) — public/manifest.json is still Create React App boilerplate: "name": "Create React App Sample", "short_name": "React App". PWA identity is wrong for a "Little Lemon" showcase.
→ Set real name/short_name/theme.
FOLLOW-UP [L2 · Works→Senior] (Observed) — Dead code. src/reportWebVitals.js is defined and exported but never imported (verified: index.js doesn't reference it; grep finds no caller). Its only consumer of the web-vitals dependency is itself, so web-vitals is effectively an unused prod dependency — and it's pinned to v2 using the deprecated getFID/getCLS API.
→ Remove reportWebVitals.js + the web-vitals dep, or actually wire it to an analytics sink.
FOLLOW-UP [L1/L2 · Works→Senior] (Observed) — Tests are broad but shallow; they don't cover the actual booking behavior. BookingForm.test.js's "fills out the form correctly" only asserts inputs hold values — it never asserts submitForm is called on a valid submit, never asserts an invalid submit is blocked, and there's no component-level test of the date→times reducer wiring, ContactModal, or Nav. Coverage breadth on the one real feature (submit/validation happy + sad path) is effectively zero.
→ Add: valid submit calls submitForm with form data; invalid submit does not; changing the date dispatches UPDATE_TIMES.
These were surfaced by the new gate and set to warn so the baseline is green; each is promoted back to error when fixed.
RESOLVED [L1 · Works→Senior] (Observed) — . The "keep selected time valid" effect called react-hooks/set-state-in-effect at BookingForm.js:61setFormData synchronously in an effect body (cascading-render smell). Fixed: the effect is gone; the selected time is now derived during render (selectedTime, clamped to availableTimes) and used for the field value, validation, and the submit payload. Verified in-browser: the shown time is always a valid slot across date changes. Rule promoted to error once Nav's violation is also cleared.
RESOLVED [L3 · Works→Senior] (Observed) — . The deferred-scroll effect set react-hooks/set-state-in-effect at Nav.js:28pendingScroll(false) inside the effect. Fixed: pendingScroll is now a useRef (flipping it shouldn't re-render), and the scroll helper is memoized so the effect deps are honest (the exhaustive-deps disable is gone too). With BookingForm's fix, react-hooks/set-state-in-effect is promoted back to error.
RESOLVED [L3 · Works→Senior] (Observed) — . The overlay jsx-a11y/click-events-have-key-events + jsx-a11y/no-static-element-interactions at ContactModal.js:29<div onClick=…> had no keyboard equivalent / role. Fixed: the backdrop-click handler is now attached imperatively in an effect (not a JSX prop), since backdrop click is a mouse-only affordance and keyboard users dismiss via Escape / the Close button / the focus trap. Both rules are promoted back to error; the lint gate is now fully clean at recommended severity.
RESOLVED [L3] (Observed, verified in browser) — Nav "About"/"Contact" used <a href="#about"> / #contact under HashRouter, mutating the routing hash as a side effect. Fixed: both are now <button type="button"> (styled to match the links — computed styles verified identical), so no stray hash mutation; and a catch-all <Route path="*"> now redirects unknown paths home (verified: a bogus hash lands back on home).
RESOLVED [L3] (Observed, verified in browser) — ContactModal moved focus in and closed on Escape/overlay, but Tab focus was not trapped, focus wasn't restored to the trigger on close, and it didn't close on route change. Fixed: added a Tab focus trap (verified — Tab stays in the dialog) and focus restore to the trigger on close (standard save-activeElement/restore pattern; correct by construction but not verifiable in the headless preview, which can't own OS focus). Close-on-route-change deliberately skipped: the full-screen overlay already blocks background nav, so it would only matter for browser back/forward, and a clean implementation would reintroduce the exact set-state-in-effect just removed — not worth it for that edge.
RESOLVED [L3] (Observed, verified) — Testimonials rendered ratings as bare ★ glyphs with no aria-label. Fixed: the rating wrapper is now role="img" with an aria-label like "4 out of 5 stars" (verified in-browser), so screen readers get meaning, not symbols.
RESOLVED [L3] (Observed, verified) — Minor taste: SelectField used key={index} (fixed in #4 → key={option}); Footer hardcoded © 2024 (now © {current year}, verified showing 2026).
- Stand up ESLint (react/react-hooks/jsx-a11y) + a
lintscript. [L0] ✅ done — ESLint 9 flat config, green baseline. - Add Prettier + format sweep (config,
format/format:checkscripts,eslint-config-prettier, one--writepass). [L0] ✅ done — 4-space/single-quote config matching existing style; JSON kept at 2-space; JS bundle hash unchanged. - Add a GitHub Actions CI workflow running
lint → test → buildon push/PR. [L0] ✅ done —.github/workflows/ci.ymlon Node 22; also runsformat:check(enforces #2). Full sequence verified locally incl. cleannpm ci. - Fix the past-date booking bug (
min+ validation) and reject emptytime. [L1] ✅ done — nativemin+ React past-date validation (verified in browser: past date blocked, valid future booking still confirms). Empty-time guard added as defense; the mock never actually returns zero slots. - Deepen the tests: assert valid submit calls
submitForm, invalid submit is blocked, date-change updates times. [L1/L2] ✅ done — refactored to a per-test render helper; added 4 behavior tests (valid submit payload, invalid submit blocked, past-date blocked,UPDATE_TIMESdispatched). Suite 8 → 12. - Fix README (port 5173/base path, Vitest not Jest). [L2] ✅ done — dev URL now
http://localhost:5173/Capstone-react/; test runner named as Vitest. - Remove tutorial residue:
reportWebVitals.js+web-vitalsdep,// Add this linecomments,App.test.jsplaceholder comment; customizemanifest.json. [L2] ✅ done — deleted the dead file + unused dep (bundle hash unchanged, proving it was dead), stripped the stray comments, and set the manifest + theme-color to the Little Lemon identity/brand green. - Bump Vite / Vitest to clear the dev-toolchain advisories; re-run the suite. [L1] ✅ done — went to the latest: Vite 8.1 + plugin-react 6 + Vitest 4 (enabled by the
.jsxrename in #11).npm auditnow reports 0 vulnerabilities; 12/12 tests + build + browser smoke all green. - Fix the lint-surfaced debt and promote the 3 demoted rules back to
error: derive time without an effect inBookingForm[L1]; reworkNavpendingScroll + modal overlay a11y alongside polish. [L3] ✅ done — all 3 rules resolved and promoted; lint gate is now fully clean at recommended severity, no warnings, no disables. - Polish: modal focus trap + focus restore + close-on-route-change; rating
aria-label; convert#about/#contactanchors to buttons + add catch-all route; dynamic footer year. [L3] ✅ done across the nav, modal, and this final polish commit (focus trap/restore, rating labels, buttons + catch-all, current-year footer). Close-on-route-change intentionally skipped (rationale above). Also removed the now-stale.jspath-header comments left by the #11 rename. - Rename JSX-bearing
.jsfiles to.jsx(drop the esbuild loader hack) to unblock Vite 8+. [L0] ✅ done — 14 JSX files renamed (imports are extensionless, so no import edits);vite.config.mjsloader hack removed; verified 12/12 tests + identical build on Vite 5. Unblocks the Vite 8 bump in #8.
Introduce ESLint + a GitHub Actions CI gate (lint / test / build). It's the deepest gap (L0), it converts the existing green tests into an actual guarantee, it makes the dead eslint-disable directives meaningful again, and — via jsx-a11y and react-hooks/exhaustive-deps — it will automatically flag several of the polish items instead of you hunting them. On a portfolio, "no linter, no CI" is the single loudest signal that a React project is unfinished; closing it is what most moves this from "works" to "senior."
Reads as portfolio-grade already:
- Clean component decomposition and file layout; styles colocated per component.
- Real state pattern:
useReducerfor available times + mockfetchAPI/submitAPImatching the Meta capstone spec. - Accessible booking form:
aria-invalid,aria-describedby,role="alert"error messaging, labeled inputs, blur-then-change validation. - Focus-managed, Escape/overlay-dismissible contact modal; semantic HTML throughout.
- Correct
HashRouterfor GitHub Pages project pages; Vite base wired correctly (verified in built HTML). - Good engineering hygiene in history: image optimization (35MB→1.3MB), MIT license, structured README.
Top 3 gaps blocking showcase-grade:
- No lint/CI — the clearest "unfinished" tell for a front-end piece.
- Tutorial residue — dead
reportWebVitals, CRA-defaultmanifest.json,// Add this linecomments, README pointing at CRA's port and "Jest." Signals coursework, not craft. - The core feature is under-proven and slightly wrong — booking accepts past dates, and the tests never actually exercise submit. The one thing a reviewer will click is the weakest-covered path.