[{"data":1,"prerenderedAt":779},["ShallowReactive",2],{"case-study:\u002Fcase-studies\u002Fcareerforge":3},{"id":4,"title":5,"body":6,"date":768,"description":769,"extension":770,"meta":771,"navigation":772,"path":773,"provenance":774,"sensitivityReviewed":775,"seo":776,"sources":775,"stem":777,"__hash__":778},"caseStudies\u002Fcase-studies\u002Fcareerforge.md","Building CareerForge with a Two-Seat Agentic Workflow",{"type":7,"value":8,"toc":744},"minimark",[9,25,30,33,36,39,42,45,48,58,62,102,106,109,112,235,238,241,262,267,270,276,282,288,294,300,306,309,313,316,328,331,334,337,340,434,438,441,444,447,458,584,588,592,595,599,602,606,609,613,616,620,623,627,630,634,637,640,643,647,650,656,662,668,674,677,681,684,691,701,707,710,714,720,726,732,738,741],[10,11,12,16,17,24],"p",{},[13,14,15],"strong",{},"Live demo:"," ",[18,19,23],"a",{"href":20,"rel":21},"https:\u002F\u002Fdemo.carlosgutz.com",[22],"nofollow","demo.carlosgutz.com"," runs the real product on fictional example data (AI drafting is disabled in the demo, so those results are pre-generated) and resets nightly. Everything below is how it was built.",[26,27,29],"h2",{"id":28},"problem","Problem",[10,31,32],{},"CareerForge began as a practical response to my own search for my next senior\nengineering role.",[10,34,35],{},"I did not need another job board. I needed a system that could take a real job\nposting, compare it with my actual experience, show the evidence behind that\ncomparison, identify where I genuinely fell short, and turn those gaps into a\nconcrete improvement plan. Existing job platforms help people find openings, but\nthey do not complete the full loop: find a role, assess the evidence, identify\ngaps, strengthen the candidate, and apply strategically.",[10,37,38],{},"CareerForge also addressed a second problem: much of my strongest engineering work\nbelongs to former employers and cannot be published. I can describe the outcomes,\nbut I cannot show the underlying proprietary code. That work includes improving\ncomplex sales-data visualization performance by more than 30%, reducing targeted\nAPI latency from more than two seconds to roughly 40 milliseconds, building backend\nservices for a feature that processes more than $150k in daily transactions, and\ncreating automation that reduced QA costs by roughly $161k per quarter.",[10,40,41],{},"I therefore needed a current, public artifact where employers could inspect more\nthan resume bullets. CareerForge gave me a place to demonstrate how I define\nboundaries, structure a system, evaluate tradeoffs, test claims, and decide when\nwork is actually complete.",[10,43,44],{},"As the project evolved, it became an additional engineering experiment: could I use\nAI to accelerate implementation without outsourcing judgment? I did not want a pile\nof plausible-looking code that I could not fully explain or defend. I wanted to\nfind out whether a disciplined, multi-agent process, combined with explicit\nboundaries, independent review, planted failures, and a merge gate controlled by\nme, could produce software I would confidently place in a public repository.\nCareerForge became both the product and the proof.",[10,46,47],{},"CareerForge is under active development. The Job Intelligence MVP is complete and\nthis portfolio is live, while later milestones (the skill accelerator and the\nintegrations that close the loop) are still ongoing. I describe it here honestly as\nwork in progress.",[10,49,50,53,54,57],{},[13,51,52],{},"Primary user:"," me, during an active senior-engineering job search.\n",[13,55,56],{},"Secondary audience:"," hiring teams and engineers who want to evaluate both the\nfinished platform and the engineering process behind it.",[26,59,61],{"id":60},"constraints","Constraints",[63,64,65,72,78,84,90,96],"ul",{},[66,67,68,71],"li",{},[13,69,70],{},"Public repository, private career data."," The monorepo is public, but real\nresume data, salary information, job postings, and application activity remain\nlocal and gitignored. Public tests and fixtures use a fictional profile only.",[66,73,74,77],{},[13,75,76],{},"Never fabricate evidence."," CareerForge cannot invent experience, resume\ncontent, accomplishments, or metrics. Fit analysis must connect a conclusion to\nreal evidence or clearly state that evidence is missing.",[66,79,80,83],{},[13,81,82],{},"No automated scraping in the MVP."," Although the original idea included\nscraping jobs, the implemented MVP accepts pasted job descriptions. Automated\ncollection was deliberately excluded because of terms-of-service, legal, privacy,\nand maintenance concerns.",[66,85,86,89],{},[13,87,88],{},"Local-first platform."," The career platform holding real data stays local.\nWhat is deployed is the portfolio and a demo instance carrying fictional example\ndata only.",[66,91,92,95],{},[13,93,94],{},"Team-grade controls on a solo project."," Changes to the main branch require\npull requests, green checks, and merge-only integration, with no personal\nbypass. I wanted the repository to demonstrate how I work under real engineering\ncontrols, not merely what I can make run locally.",[66,97,98,101],{},[13,99,100],{},"The tool cannot replace the search."," I began using the platform alongside\nreal applications rather than postponing the job search until the product felt\ncomplete.",[26,103,105],{"id":104},"architecture","Architecture",[10,107,108],{},"CareerForge uses a modular-monolith architecture with TypeScript across the whole\nsystem: Nuxt and Vue 3 for the frontend, Fastify for the backend, and PostgreSQL\nwith Drizzle for persistence.",[10,110,111],{},"The architectural feature I value most is the enforcement of explicit module\nboundaries. The scoring package contains pure, deterministic logic and cannot\nimport the LLM package; the LLM package is the only one that touches provider SDKs\nand owns the versioned prompt registry; the database package is the only module\nthat contains SQL; application flow runs from routes to services to repositories;\nand the portfolio cannot import platform packages or private career data. This\nseparation prevents probabilistic model output from quietly becoming deterministic\nbusiness logic. The system can use an LLM where interpretation is valuable without\nletting the model become the untraceable source of every decision.",[113,114,122,126,130,182,213],"svg",{"viewBox":115,"width":116,"role":117,"ariaLabelledBy":118,"style":121},"0 0 720 260","100%","img",[119,120],"diagA-t","diagA-d","font-family: ui-monospace, SFMono-Regular, Menlo, monospace",[123,124,125],"title",{"id":119},"Enforced module boundaries",[127,128,129],"desc",{"id":120},"Request flow runs from routes to services to repositories. The scoring package is pure and deterministic and is forbidden from importing the llm package; the db package holds the only SQL.",[131,132,136,143,146,149,153,156,159,162,167,170,173,178],"g",{"fill":133,"stroke":134,"style":135},"none","currentColor","stroke-width: 1.5",[137,138],"rect",{"x":139,"y":140,"width":141,"height":142},"30","34","180","46",[137,144],{"x":145,"y":140,"width":141,"height":142},"270",[137,147],{"x":148,"y":140,"width":141,"height":142},"510",[150,151],"path",{"d":152},"M210 57 h60",[150,154],{"d":155},"M262 52 l8 5 l-8 5",[150,157],{"d":158},"M450 57 h60",[150,160],{"d":161},"M502 52 l8 5 l-8 5",[137,163],{"x":139,"y":164,"width":165,"height":166},"150","200","66",[137,168],{"x":169,"y":164,"width":165,"height":166},"290",[137,171],{"x":172,"y":164,"width":164,"height":166},"540",[150,174],{"d":175,"strokeDashArray":176},"M230 183 h60",[177,177],"4",[150,179],{"d":180,"style":181},"M251 170 l28 26 M279 170 l-28 26","stroke-width: 2",[131,183,185,192,196,200,205,209],{"fill":134,"stroke":133,"fontSize":184},"15",[186,187,191],"text",{"x":188,"y":189,"style":190},"120","62","text-anchor: middle","routes",[186,193,195],{"x":194,"y":189,"style":190},"360","services",[186,197,199],{"x":198,"y":189,"style":190},"600","repositories",[186,201,204],{"x":202,"y":203,"style":190},"130","182","scoring",[186,206,208],{"x":207,"y":203,"style":190},"390","llm",[186,210,212],{"x":211,"y":203,"style":190},"615","db",[131,214,216,220,224,227,230],{"fill":134,"stroke":133,"fontSize":215},"11",[186,217,219],{"x":139,"y":218},"24","request flow",[186,221,223],{"x":202,"y":222,"style":190},"202","pure, deterministic",[186,225,226],{"x":207,"y":222,"style":190},"provider SDKs + prompts",[186,228,229],{"x":211,"y":222,"style":190},"the only SQL",[186,231,234],{"x":232,"y":233,"style":190},"260","164","never",[10,236,237],{},"Every external boundary is validated with Zod: API input, environment\nconfiguration, structured LLM output, and persisted data entering application\nworkflows. Structured logging uses request IDs and excludes personally\nidentifiable information.",[10,239,240],{},"The development process is also part of the architecture. I used two distinct\nagent seats: an execution seat that wrote and committed changes, and a read-only\nreview seat that independently checked claims and implementation details against\nprimary sources. I remained responsible for defining the work, resolving\ndisagreements, deciding whether the evidence was sufficient, and authorizing every\nmerge. The agents increased implementation and review capacity; they did not own\nthe definition of correctness.",[10,242,243,244,249,250,255,256,261],{},"The public monorepo is itself the living evidence for this study. Its architecture\ndecision records, its continuous-integration workflows, and the code are all open\nto inspection: the ",[18,245,248],{"href":246,"rel":247},"https:\u002F\u002Fgithub.com\u002Fcarlos-gutz-25\u002Fcareerforge",[22],"repository",",\nthe ",[18,251,254],{"href":252,"rel":253},"https:\u002F\u002Fgithub.com\u002Fcarlos-gutz-25\u002Fcareerforge\u002Ftree\u002Fmain\u002Fdocs\u002FDECISIONS",[22],"architecture decision records",",\nand the ",[18,257,260],{"href":258,"rel":259},"https:\u002F\u002Fgithub.com\u002Fcarlos-gutz-25\u002Fcareerforge\u002Ftree\u002Fmain\u002F.github\u002Fworkflows",[22],"CI workflows",".",[263,264,266],"h3",{"id":265},"resume-integrity-a-claim-that-cannot-cite-its-evidence-does-not-ship","Resume integrity: a claim that cannot cite its evidence does not ship",[10,268,269],{},"Resume Studio composes a tailored resume out of individual claims rather than out\nof free prose. Each claim carries its text, the section it belongs to, the\nexperience or project it describes, and the evidence references it rests on. That\nstructure exists for one reason: a machine can check a structured claim against its\nsources before a human ever reads it, and cannot check a paragraph.",[10,271,272,275],{},[13,273,274],{},"Everything the model drafts stays a draft until I review it."," The system never\nsends anything resembling an application, and no generated sentence reaches a\ndocument on its own authority.",[10,277,278,281],{},[13,279,280],{},"One gate decides, and it is deterministic."," The claim-provenance check is a pure\nfunction in the scoring package: no input or output, no clock, no randomness, and\nno access to the LLM package. The compose route calls it before any insert, and on\nany violation it writes nothing and marks the run flagged. A model that produces a\nfluent, well-formed, unsupported sentence gets a flagged run, not a resume.",[10,283,284,287],{},[13,285,286],{},"Six laws, each separately testable, each covering a different way a claim can\noutrun its evidence."," Citation membership requires every cited reference to have\nactually been sent as evidence, and forbids a claim citing the same reference\ntwice. The numeric law requires every number in the claim to appear in a cited\nsource as written, and a unit-marked number such as 40% or $50 additionally needs a\ncompatible marker in that source. The vocabulary law requires any skill phrase the\nclaim asserts to be backed by a cited source. The provenance-class law holds two\nindependent structural locks: an experience or project claim may cite only its own\nentity's evidence, and personal or AI-assisted evidence can never back a claim in\nthe experience section. The external-pointer law keeps URLs, emails, and domains\nout of resume body prose, because links belong to the deterministic contact header\nrather than to model-drafted text. The shape law carries the cross-field and\naggregate caps: a summary claim holds no entity reference, a non-summary claim must\nhold one, and claim length, claim count, and per-entity totals stay inside fixed\nlimits.",[10,289,290,293],{},[13,291,292],{},"Where a deterministic comparison is ambiguous, the gate flags."," Over-flagging\nroutes work to human review, which is merely inconvenient. Under-flagging publishes\nan unsupported claim about my own career, which is the failure the entire mechanism\nexists to prevent. The tie-break is written down as a design law rather than left\nto whichever branch happened to be written first.",[10,295,296,299],{},[13,297,298],{},"A correct gate can still be a dishonest one, and mine was."," The verdicts were\nright from the beginning, but a flagged run did not record which law it had\nviolated. I was told that something had failed and not what. Surfacing the violated\nlaw identifiers changed nothing about what the gate decides; it changed only\nwhether the decision could be read. The recorded violation is built to carry law\nidentifiers, and for a shape violation the specific sub-rule, while dropping the\noffending token and the evidence references by construction, so teaching the gate\nto speak did not turn it into a leak.",[10,301,302,305],{},[13,303,304],{},"The interface was worse than silent, because it guessed."," The Resume Studio\nbanner enumerated three of the six laws and omitted the two that had actually\nfired, so a run that breached a summary length cap was reported to me as possible\nfabrication. Four of the shape law's sub-rules are aggregate caps, where no\nindividual claim is defective and the set is simply too large; describing that as\ninvented content is not a wording problem but a false accusation against work that\nwas accurate. The banner now has three display states and all three are honest.\nWhen the violated laws are recorded it names them. When they are not recorded it\nsays exactly that and enumerates nothing, on the principle that a system with\nincomplete information should say less rather than guess.",[10,307,308],{},"That sequence is the part of this project I would most want a reviewer to look at.\nBuilding a correct gate was the ordinary engineering. Noticing that a correct gate\nwas communicating dishonestly, and treating that as a defect worth its own story\nrather than as cosmetic copy, is the part I had to be taught by using the thing.",[263,310,312],{"id":311},"design-system-two-identities-one-grammar","Design system: two identities, one grammar",[10,314,315],{},"The v2 redesign gives the two frontends distinct visual identities that share one\nenforceable grammar. The public portfolio adopts a \"Provenance Ledger\" identity\n(hairline rules, a self-hosted display face, monospace provenance stamps); the\nprivate platform UI adopts a separate \"Dusk Console\" identity. They look nothing\nalike, and that is deliberate: one is a public document a hiring team reads, the\nother is a dark-first operator console I use during a search.",[10,317,318,319,323,324,327],{},"What the two apps share is not a stylesheet but a contract. Each app owns its own\n",[320,321,322],"code",{},"tokens.css"," in which every color custom property is a bare hex value or a strict\n",[320,325,326],{},"light-dark(#hex, #hex)"," pair, and nothing else parses. Each app carries its own\ncopy of a text-parsing contrast gate: it reads the tokens file as text, computes\nWCAG relative luminance inline, and asserts an explicit manifest of\nforeground\u002Fbackground\u002Fthreshold pairs in both light and dark mode. A forgotten\ndark-mode value is unrepresentable by construction, and a token that participates\nin no contrast pair fails the build. The thresholds are the WCAG floors applied as\ntests rather than aspirations: 4.5:1 for text, 3:1 for structural hairlines and\nindicators, with no decorative exemption tier. When the drafted hairline color\nfailed 3:1 it was re-chosen, not exempted; the adopted value measures 3.03:1 at its\nworst case across both surfaces and both modes.",[10,329,330],{},"The display typeface is a self-hosted Fraunces subset rather than a font-CDN\nrequest, and it is now two static instances rather than one. Both are cut from the\nsame upstream variable font over the same 225-codepoint latin subset, each with a\nmetric-adjusted local fallback so the swap does not shift layout. Body headings use\nan optical-size 30 cut of 34424 bytes. The hero uses a separate optical-size 144\ninstance of 17308 bytes, the \"Big\" display cut, where hairline serifs and high\nstroke contrast are the entire point at a 68px clamp. Keeping the optical-size axis\nvariable instead measured 66.5KB, over the 40KB budget, which is why these are\npinned static cuts rather than one flexible file.",[10,332,333],{},"Splitting them was a correction, not an embellishment. A single display cut applied\nto everything went spidery and ran words together at heading sizes, which a reader\nnoticed on the live site before any gate did: automated checks measure contrast and\nbyte budgets, and neither of those is legibility. The two instances are two distinct\nCSS families, so neither cascades into the other and the fix to body headings cannot\nbe undone by a later change to the hero.",[10,335,336],{},"The typeface is a want and the performance budget is a law: an\nabort-to-system-stack ramp drops Fraunces if the Lighthouse median performance\nscore falls below 96, one point above the never-lowered 0.95 CI floor, so the font\nis sacrificed before the budget is ever at risk.",[10,338,339],{},"The quality budgets are floors rather than a boast, and they are deliberately not\nall the same height. Accessibility is enforced at a strict 100, so a single lost\npoint fails the build, and a full axe-core pass runs alongside it because\nLighthouse's accessibility category does not execute every axe rule. Performance,\nbest practices and SEO are enforced at 95, and the audited pages currently measure\nabove those floors. The asymmetry is the argument: an accessibility regression\nexcludes a reader outright, so it blocks, while a performance point spent on a\ndeliberate tradeoff is a cost to be weighed and disclosed, so it is given headroom.\nClaiming a perfect score in all four categories would read better than this\nparagraph does. It would also not be true, and a number in this study that a reader\ncannot reproduce is worth less than no number at all.",[113,341,346,349,352,390,414],{"viewBox":342,"width":116,"role":117,"ariaLabelledBy":343,"style":121},"0 0 720 290",[344,345],"diagB-t","diagB-d",[123,347,348],{"id":344},"Two identities, one grammar",[127,350,351],{"id":345},"The portfolio (Provenance Ledger) and the platform web UI (Dusk Console) each own a tokens.css file and a contrast gate, and both build on one shared token grammar checked in light and dark mode. No shared package is created.",[131,353,354,358,361,366,368,371,374,378,381,384,387],{"fill":133,"stroke":134,"style":135},[137,355],{"x":356,"y":356,"width":145,"height":357},"40","118",[137,359],{"x":360,"y":356,"width":145,"height":357},"410",[137,362],{"x":363,"y":364,"width":365,"height":142},"58","92","110",[137,367],{"x":203,"y":364,"width":365,"height":142},[137,369],{"x":370,"y":364,"width":365,"height":142},"428",[137,372],{"x":373,"y":364,"width":365,"height":142},"552",[137,375],{"x":356,"y":376,"width":377,"height":363},"212","640",[150,379],{"d":380},"M175 158 V212",[150,382],{"d":383},"M170 200 l5 8 l5 -8",[150,385],{"d":386},"M545 158 V212",[150,388],{"d":389},"M540 200 l5 8 l5 -8",[131,391,393,397,401,404,408,411],{"fill":134,"stroke":133,"fontSize":392},"14",[186,394,396],{"x":395,"y":166,"style":190},"175","portfolio",[186,398,400],{"x":399,"y":166,"style":190},"545","apps\u002Fweb",[186,402,322],{"x":403,"y":188,"style":190},"113",[186,405,407],{"x":406,"y":188,"style":190},"237","contrast gate",[186,409,322],{"x":410,"y":188,"style":190},"483",[186,412,407],{"x":413,"y":188,"style":190},"607",[131,415,416,420,423,426,430],{"fill":134,"stroke":133,"fontSize":215},[186,417,419],{"x":395,"y":418,"style":190},"82","Provenance Ledger",[186,421,422],{"x":399,"y":418,"style":190},"Dusk Console",[186,424,425],{"x":194,"y":218,"style":190},"no shared package (v2.1 trigger)",[186,427,429],{"x":194,"y":428,"style":190},"238","one grammar: color = #hex or light-dark(#hex, #hex)",[186,431,433],{"x":194,"y":432,"style":190},"256","contrast gate asserts every pair in both modes",[263,435,437],{"id":436},"deployment-topology","Deployment topology",[10,439,440],{},"Three things deploy differently, and the difference is the whole privacy design.",[10,442,443],{},"The portfolio you are reading builds to static files and ships to GitHub Pages\nthrough an OIDC-based workflow with no long-lived deployment secret, at a custom\napex domain.",[10,445,446],{},"The public demo is a separate deployment that carries fictional example data only.\nIt runs as a single container task on AWS Fargate behind an API Gateway HTTP API,\nbacked by Neon serverless Postgres, provisioned with Terraform and deployed through\na GitHub OIDC federated role that stores no long-lived cloud secret. A nightly\nscheduled job re-seeds it, which makes the reset and the backup the same mechanism.\nIt is keyless by decision rather than by omission: the environment layer fails\nclosed, so if demo mode is set while a live API key is present, the process refuses\nto boot rather than starting in a state nobody intended. That was chosen over a\ncapped live key, which would have put real spend and a prompt-injection surface on a\npublic box, and over a mocked provider, which would have displayed fabricated output\nas though it were real. The demo shows pre-generated real artifacts instead, and the\nendpoints that would cost money answer honestly that they are disabled rather than\npretending to work.",[10,448,449,450,453,454,457],{},"The platform itself, meaning the Fastify API, the platform web UI, and PostgreSQL,\nstill runs local-first via ",[320,451,452],{},"docker compose"," on my own machine, and it stays there on\npurpose: it holds real, private career data (resume detail, salary targets,\napplication history), and a local-only database is an invariant rather than a\npreference. Every table already carries a ",[320,455,456],{},"user_id"," for a future multi-user move,\nbut until a real second user or a concrete remote-access need appears, hosting that\nprivate store on someone else's disk is a permanent exposure surface the project\ndeliberately declines. The demo does not soften that line. It exists precisely\nbecause the real data stays home: what is hosted is the example profile, so a\nvisitor can exercise the product without any real career detail leaving my machine.",[113,459,463,465,468,516,546],{"viewBox":342,"width":116,"role":117,"ariaLabelledBy":460,"style":121},[461,462],"diagC-t","diagC-d",[123,464,437],{"id":461},[127,466,467],{"id":462},"Two things are deployed publicly and one is not. The portfolio ships as static files to GitHub Pages via an OIDC workflow with no long-lived secret. The public demo is a separate deployment on AWS Fargate with Neon Postgres, carrying fictional example data only; it is keyless by decision and refuses to boot if an API key is present, and it re-seeds nightly. The platform API, web UI, and PostgreSQL run local-first under docker compose and hold the real private career data, which is never hosted.",[131,469,470,474,477,480,483,486,490,493,496,501,505,510,513],{"fill":133,"stroke":134,"style":135},[137,471],{"x":140,"y":472,"width":202,"height":473},"54","52",[137,475],{"x":476,"y":472,"width":188,"height":473},"214",[150,478],{"d":479},"M164 80 h50",[150,481],{"d":482},"M206 75 l8 5 l-8 5",[137,484],{"x":140,"y":485,"width":202,"height":473},"152",[137,487],{"x":488,"y":485,"width":489,"height":473},"204","140",[150,491],{"d":492},"M164 178 h30",[150,494],{"d":495},"M196 173 l8 5 l-8 5",[150,497],{"d":498,"strokeDashArray":499},"M360 26 V276",[500,500],"5",[137,502],{"x":503,"y":472,"width":504,"height":164},"398","288",[137,506],{"x":507,"y":508,"width":509,"height":140},"416","70","252",[137,511],{"x":507,"y":512,"width":509,"height":140},"112",[137,514],{"x":507,"y":515,"width":509,"height":140},"154",[131,517,519,523,528,532,535,539,543],{"fill":134,"stroke":133,"fontSize":518},"13",[186,520,396],{"x":521,"y":522,"style":190},"99","78",[186,524,527],{"x":525,"y":526,"style":190},"274","84","GitHub Pages",[186,529,531],{"x":521,"y":530,"style":190},"176","demo",[186,533,534],{"x":525,"y":530,"style":190},"AWS Fargate",[186,536,538],{"x":537,"y":364,"style":190},"542","apps\u002Fapi (Fastify)",[186,540,542],{"x":537,"y":541,"style":190},"134","apps\u002Fweb (Dusk Console)",[186,544,545],{"x":537,"y":530,"style":190},"PostgreSQL (pgdata)",[131,547,548,552,556,559,563,566,570,574,577,580],{"fill":134,"stroke":133,"fontSize":215},[186,549,551],{"x":140,"y":550},"26","public internet",[186,553,555],{"x":521,"y":554,"style":190},"94","static SSG",[186,557,558],{"x":140,"y":202},"OIDC deploy, no long-lived secret",[186,560,562],{"x":521,"y":561,"style":190},"192","example data",[186,564,565],{"x":525,"y":561,"style":190},"Neon Postgres",[186,567,569],{"x":140,"y":568},"228","keyless by decision",[186,571,573],{"x":140,"y":572},"244","a present key blocks boot",[186,575,576],{"x":140,"y":232},"nightly re-seed = backup",[186,578,579],{"x":503,"y":550},"local (docker compose)",[186,581,583],{"x":503,"y":582},"224","real career data, never hosted",[26,585,587],{"id":586},"tradeoffs","Tradeoffs",[263,589,591],{"id":590},"pasted-job-descriptions-instead-of-automated-scraping","Pasted job descriptions instead of automated scraping",[10,593,594],{},"The earliest concept included scraping jobs to find opportunities automatically. I\ndeliberately narrowed the MVP to pasted job descriptions. That choice made\ningestion less convenient, but it removed avoidable terms-of-service, legal,\nprivacy, and maintenance risk. It also kept the first milestone focused on the more\nimportant problem: whether CareerForge could evaluate a role honestly and produce a\nuseful action plan once a posting entered the system.",[263,596,598],{"id":597},"extending-the-existing-required-ci-check-instead-of-adding-another","Extending the existing required CI check instead of adding another",[10,600,601],{},"New gates were folded into the existing required check rather than advertised as a\nseparate blocking status. Adding a new required check would have meant another\nbranch-protection change and risked configuring protection around a status that was\nnot yet reliably emitted. The cost is that each pull request now pays a few extra\nminutes for browser-based validation. I accepted that cost in exchange for an\nenforceable and truthful gate.",[263,603,605],{"id":604},"full-axe-core-analysis-instead-of-lighthouses-accessibility-subset","Full axe-core analysis instead of Lighthouse's accessibility subset",[10,607,608],{},"Lighthouse's accessibility audit does not execute every axe rule, so CareerForge\nruns the full axe-core engine. That makes the accessibility gate slower, but it\nmeans the claim of zero detected violations refers to the complete configured\nengine rather than a convenient subset.",[263,610,612],{"id":611},"pinned-browser-instead-of-a-rolling-version","Pinned browser instead of a rolling version",[10,614,615],{},"The browser is pinned in CI so Lighthouse measurements and performance budgets are\nreproducible. A rolling version could introduce unexplained score changes unrelated\nto application code.",[263,617,619],{"id":618},"github-pages-with-an-oidc-deployment","GitHub Pages with an OIDC deployment",[10,621,622],{},"The portfolio uses GitHub Pages and an OIDC-based deployment with no long-lived\ndeployment secret. This introduces limitations around configurable HTTP response\nheaders. I accepted that limitation only after verifying that the affected\nLighthouse audits carried no score weight under the configured quality budget.",[263,624,626],{"id":625},"a-duplicated-grammar-instead-of-a-shared-design-package","A duplicated grammar instead of a shared design package",[10,628,629],{},"A shared design package was rejected for v2. The portfolio's module wall lets it\nimport only shared configuration and no platform packages, so a shared design\npackage would either breach that wall or complicate it with a second carve-out,\ncoupling the public zero-backend site to the platform's release cadence for the sake\nof one small CSS file and one self-contained test. At two consumers whose token\nvalues differ per identity anyway, duplication sits below the abstraction bar, and\neach copy is independently verified by its own CI so drift cannot silently break\neither app. The cost is two files kept in sync by hand; I accepted it and recorded\nan explicit reopening trigger (a third frontend, or measured drift pain traced to a\nreal defect) rather than pretending the duplication is free.",[263,631,633],{"id":632},"the-real-career-store-stays-local-and-the-demo-is-the-arm-of-that-fork-i-took","The real career store stays local, and the demo is the arm of that fork I took",[10,635,636],{},"Keeping the platform on a local machine trades away remote access for the strongest\nprivacy posture. I costed the hosted alternatives honestly: an Azure App Service\nplus managed PostgreSQL floor around 25 to 40 dollars a month, a Fly or Render class\nPaaS around 10 to 20, and the cheaper Azure Container Apps consumption tier paired\nwith a burstable database around 12 to 15. The decision was not cost-decisive,\nthough; it was privacy-decisive. Every hosted option forced the same fork: either\nput the real private career store on a third party's disk behind three first-ever\nplatform secrets, or stand up an instance seeded only with the fictional example\nprofile.",[10,638,639],{},"That fork has since been resolved, and only one arm of it was taken. The demo now\nexists: it runs the real product on the example profile, it is keyless, and it\nresets nightly, so a hiring team can exercise the system without an account and\nwithout any real career data being hosted anywhere. The private store still sits on\nmy own machine behind no platform secrets, because the privacy argument against\nhosting it never rested on cost and nothing since has weakened it.",[10,641,642],{},"Writing the fork down before choosing is what later made the choice cheap. The\nconditions under which a demo would be worth standing up were already on record, so\nwhen they were met the work was a deployment rather than a reversal. I would rather\nshow that than an unused always-on service, and the deployment competency now on\ndisplay is two secretless OIDC pipelines rather than one.",[26,644,646],{"id":645},"testing","Testing",[10,648,649],{},"CareerForge treats testing as evidence, not ceremony.",[10,651,652,655],{},[13,653,654],{},"Every gate must be observed failing."," When I add or narrow a quality gate, I\nintentionally introduce a controlled defect and capture the resulting failure. A\ncheck that has only ever passed has not yet demonstrated that it protects anything.\nThis planted-failure discipline makes each gate prove that it can detect the\ncondition it claims to prevent.",[10,657,658,661],{},[13,659,660],{},"New checks must demonstrate unique coverage."," A gate must also prove that it\ncatches something the existing suite cannot. An accessibility defect in a\nscrollable region made the full axe check fail while the Lighthouse accessibility\nscore still reported a perfect result; an incorrect base-path prefix made the\ninternal-link check fail while the structural HTML check stayed green, the same\nclass of defect that had previously allowed an unstyled deployment to look\nsuccessful.",[10,663,664,667],{},[13,665,666],{},"Merge blocking is proven in CI, not just locally."," Planted regressions were\ncommitted and allowed to reach CI so the repository could prove that the required\ncheck actually turned red and prevented merging. The evidence is tied to specific\ncommits rather than to local terminal output alone.",[10,669,670,673],{},[13,671,672],{},"Review is adversarial, including toward the tests."," The two-seat process found\nfailures that conventional coverage would not necessarily expose: a planted test\nthat exercised a rule Lighthouse already covered, so it did not prove the new\ngate's unique value; a dependency published that same day that was automatically\nintroduced as a supply-chain exception, then removed and replaced with a vetted,\npinned release; and residue from a planted regression that remained after the\nintended repair and was found by comparing the resulting tree against a known-clean\nbaseline. The process did not assume that a passing test, a generated explanation,\nor a review agent was automatically correct.",[10,675,676],{},"The platform also uses conventional automated coverage: Vitest unit and integration\ntests, a dockerized PostgreSQL for integration testing, Playwright end-to-end\ntests, mocked LLM providers with recorded fixtures, and a prompt-injection corpus\nthat must remain green.",[26,678,680],{"id":679},"results","Results",[10,682,683],{},"The Job Intelligence MVP completes the workflow it set out to: extract\nrequirements, evaluate fit, classify gaps, and generate an improvement plan for\nreview.",[10,685,686,687],{},"During the M1 dogfood gate I used CareerForge to evaluate 6 real job postings from\nmy active search, and its fit reports informed applications that were already in\nflight. ",[688,689,690],"span",{},"M1-13; docs\u002Fprofile\u002Fprojects.md",[10,692,693,694,697,698],{},"The portfolio is deployed at a custom apex domain with no long-lived deployment\nsecret. Three CI quality gates protect the deployed artifact: Lighthouse\nperformance and quality budgets, full axe-core accessibility analysis, and\ninternal-link and asset-path validation. Each gate is proven to block merges rather\nthan merely to run: planted regressions turned the required check red at specific\ncommits, and the evidence is tied to those commits rather than to local output\nalone. ",[688,695,696],{},"ec37ecf; b7492b6"," The Lighthouse budget was demonstrated with a local\nplanted failure, and in CI it blocks through the same fail-on-error wiring the\nother two gates use. ",[688,699,700],{},"M2-03",[10,702,703,704],{},"The v2 design system and the platform-hosting decision are each recorded as an\narchitecture decision record with measured rationale rather than taste. The\nProvenance Ledger enforces its accessibility floor mechanically, 4.5:1 for text and\n3:1 for indicators in both modes with a worst-case hairline of 3.03:1, and the\ntwo self-hosted Fraunces cuts are 34424 and 17308 bytes, each against a 40KB\nper-file budget; the\nplatform stays local-first with a costed hosting trade recorded down to the roughly\n12-to-15-dollar-per-month cheapest hosted option, rejected on privacy grounds rather\nthan cost. ",[688,705,706],{},"docs\u002FDECISIONS\u002F0016-design-system.md; docs\u002FDECISIONS\u002F0015-platform-deployment.md",[10,708,709],{},"The public repository is itself one of the project's primary deliverables. It\nprovides inspectable evidence of senior full-stack TypeScript development; backend\nand data-boundary design; deterministic and LLM-assisted logic kept under separate\ncontrols; runtime validation and privacy boundaries; automated testing and\nadversarial quality gates; CI\u002FCD and branch-protection discipline; architectural\ntradeoff documentation; and responsible use of AI-assisted development. That\nmatters because my target roles emphasize backend-leaning full-stack ownership,\nperformance, reliability, modernization, maintainable systems, and practical\nengineering judgment.",[26,711,713],{"id":712},"what-id-change-and-what-i-learned","What I'd Change and What I Learned",[10,715,716,719],{},[13,717,718],{},"Do not build protection for an unobserved failure."," I deliberately left a\nproposed CI paint-flake guard unimplemented until the failure actually occurs. A\nspeculative retry could hide a real regression and create confidence without\nevidence. The better decision was to document the risk, leave the item visibly\nopen, and wait for observable behavior before designing the protection.",[10,721,722,725],{},[13,723,724],{},"Park work honestly."," Some improvements remain open, including broader\nmulti-page gate coverage and a path-scoped CI skip. I record these as named parked\nitems rather than describing the related area as complete. \"Not required for this\nmilestone\" and \"finished\" are not the same statement.",[10,727,728,731],{},[13,729,730],{},"Review systems are fallible."," The review agent made identifiable mistakes during\ndevelopment. Those errors were caught, corrected, and preserved as part of the\nproject record. That did not invalidate the review workflow; it demonstrated why no\nmodel, tool, test, or reviewer should become a single source of truth.",[10,733,734,737],{},[13,735,736],{},"AI accelerated execution, not accountability."," The largest lesson was that\ndirecting AI did not reduce the need for engineering judgment. It concentrated it.\nThe agents could implement, investigate, and review quickly, but that speed made\nweak assumptions and unjustified confidence more dangerous, not less. My\nresponsibility was to define the boundaries, require evidence, compare conclusions\nacross independent viewpoints, and reject work I could not explain or defend.",[10,739,740],{},"I learned to trust AI with bounded execution: generating an implementation from\nexplicit requirements, performing repetitive analysis, exploring alternatives, and\nchallenging a proposed solution. I did not delegate truth, architecture, security,\nprivacy, or the definition of done. The most important failures were rarely syntax\nerrors; they were plausible claims that exceeded the evidence, dependencies that\nhad not earned trust, tests that appeared stronger than they were, and cleanup that\nlooked complete until it was compared against a known-good state.",[10,742,743],{},"Next time, I would establish the evidence contract, the review rubric, the\ndependency-admission policy, and the planted-failure requirement even earlier. The\nmost effective part of the workflow was not using more AI. It was giving each agent\na bounded role, keeping their claims independently testable, and retaining a human\nmerge gate with enough technical understanding to say no.",{"title":745,"searchDepth":746,"depth":746,"links":747},"",2,[748,749,750,756,765,766,767],{"id":28,"depth":746,"text":29},{"id":60,"depth":746,"text":61},{"id":104,"depth":746,"text":105,"children":751},[752,754,755],{"id":265,"depth":753,"text":266},3,{"id":311,"depth":753,"text":312},{"id":436,"depth":753,"text":437},{"id":586,"depth":746,"text":587,"children":757},[758,759,760,761,762,763,764],{"id":590,"depth":753,"text":591},{"id":597,"depth":753,"text":598},{"id":604,"depth":753,"text":605},{"id":611,"depth":753,"text":612},{"id":618,"depth":753,"text":619},{"id":625,"depth":753,"text":626},{"id":632,"depth":753,"text":633},{"id":645,"depth":746,"text":646},{"id":679,"depth":746,"text":680},{"id":712,"depth":746,"text":713},"2026-07-22","A career-development platform built as both product and proof, a modular-monolith TypeScript system developed through a disciplined, independently-reviewed, multi-agent process with a human merge gate.","md",{},true,"\u002Fcase-studies\u002Fcareerforge","personal_ai_assisted",null,{"title":5,"description":769},"case-studies\u002Fcareerforge","b6IqvPiHrkIZu7WKvRGJ2BCpFEuv81o9TGUCtP-WGB4",1788403975817]