PageSourceSearch

https://ljefferson-menoko-site.netlify.app/assets/m3n0ko0g-website-Dw3ceUDW.js

js ljefferson-menoko-site.netlify.app collected 2026-10-03 11:00:47 UTC 13,417 bytes, 1 lines download raw bytes

1const e="m3n0ko0g-website",t="M3n0ko0g Website (Agent Console + Digital Twin)",a="My portfolio rebuilt as an agent console, where a digital twin answers questions about my work by actually searching a build-time corpus in front of you, and the trace you watch is the retrieval that happened, not an animation of one.",n="Production (live on Netlify)",s="production",i="primary",o=["React 18","TypeScript 5","Vite 6","Tailwind CSS 3","React Router 7","Netlify Functions","OpenAI API (server-side)","Vitest"],r=["retrieval design without a vector database","BM25-style keyword and tag scoring","build-time corpus indexing","streaming LLM responses","graceful degradation to a deterministic search-only mode","serverless functions","secure server-side key handling","accessibility (reduced-motion, focus management, WCAG AA in two themes)","design-token discipline"],d="/case-studies/art/m3n0ko0g-website.webp",l=[{label:"Live site",href:"https://ljefferson-menoko-site.netlify.app/"}],c=JSON.parse('[{"heading":"Problem","blocks":[{"kind":"p","spans":[{"text":"A portfolio has to answer a question a static page cannot: the follow-up. A hiring manager reads \\"built an ETL pipeline\\" and wants to know whether it was idempotent, how big the feed was, and what happened when it failed. A founder wants to know whether I would tell them not to use agents. A resume cannot answer either, and neither can a case study, because you cannot ask a case study anything."}]},{"kind":"p","spans":[{"text":"The obvious move is bolting a chatbot onto the site. That fails in two directions at once. An unrestricted chatbot on a professional property is a liability, it will eventually invent a client, a metric, or a claim about my family, and I own whatever it says. A bounded one that answers only from a paragraph of approved copy is a FAQ with a typing animation, and everyone can tell."}]},{"kind":"p","spans":[{"text":"There is a third problem that only shows up when you demo it. Most \\"AI-powered\\" interfaces show a spinner and then text. The interesting part (what the system actually looked at) is hidden, and hiding it is exactly what makes the output untrustworthy. Meanwhile most products that "},{"text":"do","bold":true},{"text":" show a trace show a scripted one: an animation designed to look like work. Once you know that, every trace becomes worthless, including honest ones."}]},{"kind":"p","spans":[{"text":"So the requirement I set was uncomfortable: "},{"text":"the site should demonstrate agentic engineering by being agentic engineering.","bold":true},{"text":" Real retrieval, over a real corpus, with the real tool calls rendered, and it must not break in front of an audie
1nce when the model API is unavailable."}]}]},{"heading":"What I Built","blocks":[{"kind":"p","spans":[{"text":"A React and TypeScript single-page app whose primary surface is a working agent console, backed by a Netlify function, over a corpus built at build time from the same Markdown files my coding agents read."}]},{"kind":"p","spans":[{"text":"A build-time indexer instead of a vector database.","bold":true},{"text":" `scripts/build-corpus.mjs` parses `agent-memory/**` (the use-case files, the profile, resume and certifications) plus the writing corpus and the free-course library, chunks by Markdown heading, tags each chunk with project, stack, skills, status and source file, and emits four artifact sets: a full-text server corpus bundled into the chat function, a sanitized client corpus carrying excerpts for local search, one structured JSON document per case study for the detail routes, and the sitemap and robots.txt. Nothing about the corpus is typed a second time in a component. Adding a case study is adding a Markdown file."}]},{"kind":"p","spans":[{"text":"The decision not to use embeddings was deliberate and is recorded as an ADR. At this corpus size a hand-rolled BM25 over heading-chunked Markdown retrieves better than a small embedding index, has no cold-start cost, no external service, no key, no index-drift-versus-content failure mode, and (the part that mattered most) it is "},{"text":"explainable","bold":true},{"text":", so the trace can name the query terms and the matched headings rather than reporting an opaque similarity number."}]},{"kind":"p","spans":[{"text":"Retrieval as one pure module, running in two places.","bold":true},{"text":" `src/lib/corpus/scoring.ts` is a dependency-free BM25 implementation (k1 = 1.4, b = 0.75) with field boosts: title terms weighted 3, tag terms 2.5, body 1, plus a flat bonus when a query token exactly hits a stack, skill or status tag. The same module runs inside the Netlify function for server search and inside the browser for the offline fallback, so the two paths cannot silently diverge, the fallback is not a reimplementation, it is the same code."}]},{"kind":"p","spans":[{"text":"Results are diversified before they reach the model: at most a fixed number of chunks per project, so one heavily-documented project cannot flood the context window and crowd out a better answer."}]},{"kind":"p","spans":[{"text":"Traces that are the work, not a picture of it.","bold":true},{"text":" The function streams NDJSON and "},{"text":"the event order is the real order of work","bold":true},{"text":", search steps stream before the model call, because retrieval genuinely happens first. The TraceLine component renders the actual tool calls from that payload: the query that ran, how many chunks were searched, which headings matched and at what score. Empty results are shown honestly as \\"0 matches\\" rather than hidden. The trace is `aria-live=\\"polite\\"` only while running, and auto-collapses about a second after the answer settles; mobile defaults to a summary line."}]},{"kind":"p","spans":[{"text":"Degradation as a designed mode, not an error.","bold":true},{"text":" No API key, an upstream failure, a timeout, or a stream that dies mid-answer all produce the same thing: a deterministic "},{"text":"search-only mode","bold":true},{"text":" over the same event protocol, still running real local retrieval and still returning matching corpus chunks and source tiles. It is presented as an intentional mode with its own status pill and a system note, not as a red error state, and the function never returns a 5xx for a model failure. The console cannot break in front of a room."}]},{"kind":"p","spans":[{"text":"Guardrails that hold.","bold":true},{"text":" The OpenAI key is server-side only; the docs explicitly forbid a `VITE_`-prefixed key that would land in the client bundle. Answers are grounded in retrieved chunks and cite their sources as tiles linking to the matching pages. Boundary topics route to the same deterministic path rather than to an improvised answer, and there is a standing rule that family health details never appear, the phrase is \\"accessibility-first design.\\""}]},{"kind":"p","spans":[{"text":"A build gate against my own filesystem.","bold":true},{"text":" `use-case-index.jsonl` carries a `path` field pointing at local Windows and WSL locations. The indexer structurally drops it at parse time, and then a leak scan runs over every serialized artifact looking for a literal `path` key, WSL UNC paths, Windows drive paths and WSL mount paths, and "},{"text":"fails the build","bold":true},{"text":" if any survive. A convention that says \\"don\'t seri
1alize that field\\" degrades the moment someone adds a passthrough. A build that refuses to complete does not."}]},{"kind":"p","spans":[{"text":"Accessibility treated as a constraint, not a pass.","bold":true},{"text":" The streaming answer is deliberately "},{"text":"not","bold":true},{"text":" an ARIA live region, a live region on streaming text reads character by character and is worse than nothing; instead completion is announced politely and the finished region can be focused on demand. Every animation (trace reveal, underline sweep, dot cycle, caret pulse, tile stagger, collapse) has a `prefers-reduced-motion` variant that preserves the information rather than removing it. Status is carried by text and shape, never colour alone. The layout reflows at 320px and at 200% zoom."}]},{"kind":"p","spans":[{"text":"A visual system with one source of truth.","bold":true},{"text":" The Retro Aventura Anos 70 palette lives once as CSS custom properties under `:root, .light` and `.dark`; Tailwind only exposes them, and nothing else defines a colour. Because semantic tokens swap with the theme, restyled components need no `dark:` variants. The accent tokens are split by role for contrast reasons that are load-bearing: filled controls use the `-fill`/`-on-` pairs, text on a surface uses the `-deep` variants, and status glyphs use a dedicated glyph token because the display mustard measures 1.4:1 on white. Four AA failures in the supplied palette were found and corrected during implementation, and that is written down in an ADR rather than fixed silently."}]},{"kind":"p","spans":[{"text":"Tests on the parts that can rot quietly.","bold":true},{"text":" Vitest covers the indexer, the scoring function, the chat handler against a mocked OpenAI, the fallback mode, the case-study filters, the console reducer and the certifications parser."}]}]},{"heading":"Skills Demonstrated","blocks":[{"kind":"p","spans":[{"text":"Retrieval design without a vector database, hand-rolled BM25 with field boosting and result diversification, build-time corpus indexing and artifact generation, isomorphic pure modules shared between serverless and browser, streaming NDJSON protocols, LLM integration with server-side key handling, graceful degradation designed as a first-class mode, build-time data-leak gating, React 18, TypeScript, Vite, Tailwind CSS, React Router, Netlify Functions, design-token architecture and theme swapping without variant classes, WCAG AA contrast verification in two themes, screen-reader-correct handling of streaming text, reduced-motion variants, responsive reflow at 320px and 200% zoom, per-route SEO metadata and sitemap generation, Vitest, architecture decision records"}]}]},{"heading":"Outcome / Evidence","blocks":[{"kind":"p","spans":[{"text":"Live on Netlify at "},{"text":"ljefferson-menoko-site.netlify.app","href":"https://ljefferson-menoko-site.netlify.app/"},{"text":"."}]},{"kind":"p","spans":[{"text":"Verifiable in the repository: a build-time indexer emitting a server corpus, a sanitized client corpus, per-case-study JSON and the sitemap; a 455-line streaming chat function whose event order mirrors the real order of work; a dependency-free BM25 module shared by the function and the browser; a leak scan that fails the build on any surviving filesystem path; and a Vitest suite across the indexer, scoring, chat handler, fallback mode, filters, reducer and credentials parser. The corpus claim is 8 case studies plus 10 pieces of writing, and it is generated from the file count rather than typed into the copy."}]},{"kind":"p","spans":[{"text":"The design system is documented as a binding handoff with the token table, component specs and interaction rules, and the palette corrections are recorded in an ADR rather than absorbed into a commit."}]}]},{"heading":"Interview Talking Points","blocks":[{"kind":"list","items":[[{"text":"The trace is the whole point and it is only worth anything because it is real. Search events stream before the model call because retrieval genuinely happens first, and the component renders the actual queries, chunk counts and matched headings from that payload. Zero matches display as zero. A trace you can catch lying is worse than no trace."}],[{"text":"Keyword scoring over embeddings was the right call at this corpus size and it is written up as an ADR. No cold start, no external service, no index drift, and it is explainable, which is what lets the trace name the terms and headings instead of reporting an opaque similarity score."}],[{"text":"One pure scoring module runs in both the serverless function and the browser, so the offline fallback is the same code rather than a second implementation that drifts. That is 
1what makes search-only mode trustworthy instead of a degraded guess."}],[{"text":"Degradation is a designed mode with its own status treatment, not an error state, and the function never returns 5xx for a model failure. The demo requirement was that it cannot break in front of a room, and the way you meet that is by deciding in advance what the broken state looks like."}],[{"text":"The path-leak scan is my favourite piece of defensive engineering here. The index carries local filesystem paths, the parser drops the field structurally, and then the build re-checks every serialized artifact and refuses to complete if one survived. Conventions rot; a failing build does not."}],[{"text":"Not making the streaming answer an ARIA live region is the accessibility decision I would defend hardest. The naive implementation announces text character by character, which is technically \\"accessible\\" and practically unusable. Announcing on completion and letting the user move focus to the finished answer is the version that actually works."}]]}]}]'),h={slug:e,title:t,oneLiner:a,status:n,statusFamily:s,tier:i,stack:o,skills:r,image:d,links:l,sections:c};export{h as default,d as image,l as links,a as oneLiner,c as sections,r as skills,e as slug,o as stack,n as status,s as statusFamily,i as tier,t as title};

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.