<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alessandro Bahgat</title>
    <description>The latest articles on DEV Community by Alessandro Bahgat (@abahgat).</description>
    <link>https://dev.to/abahgat</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F230210%2F4c09c9e5-9977-4879-97cf-b8f78e0efc5c.jpeg</url>
      <title>DEV Community: Alessandro Bahgat</title>
      <link>https://dev.to/abahgat</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/abahgat"/>
    <language>en</language>
    <item>
      <title>Visualizing Ukkonen's Suffix Tree Algorithm</title>
      <dc:creator>Alessandro Bahgat</dc:creator>
      <pubDate>Wed, 11 Mar 2026 12:05:00 +0000</pubDate>
      <link>https://dev.to/abahgat/visualizing-ukkonens-suffix-tree-algorithm-13mo</link>
      <guid>https://dev.to/abahgat/visualizing-ukkonens-suffix-tree-algorithm-13mo</guid>
      <description>&lt;h2&gt;
  
  
  Learning algorithms from books
&lt;/h2&gt;

&lt;p&gt;I learned most of what I know about algorithms by poring over a copy of &lt;em&gt;Introduction to Algorithms&lt;/em&gt; I got while in university. The book is very well known, especially among folks who got a formal education in computer science.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7kdozf2wzwt4d6ityp4z.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7kdozf2wzwt4d6ityp4z.jpg" alt="If you have studied it, you know the book: it is over a thousand pages long and it weighs enough to double as a doorstop." width="800" height="419"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;If you have studied it, you know the book: it is over a thousand pages long and it weighs enough to double as a doorstop.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I worked through large sections of it, pen in hand, trying to trace through increasingly complex algorithms, building intuition for their behavior and tradeoffs. The book covers the theory in great depth: correctness proofs, recurrence relations, asymptotic analysis.&lt;/p&gt;

&lt;p&gt;But there was often a gap between reading an algorithm and truly understanding it. The book would present pseudocode, sometimes a few diagrams showing state at key moments and theorems about performance characteristics. The work of tracing what actually happens was left as an exercise to the reader. I did that work with pen and paper, drawing trees, crossing out nodes, scribbling indices in the margins. It worked, eventually. But it was slow, error-prone, and the understanding felt fragile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementing from a paper
&lt;/h2&gt;

&lt;p&gt;Years later, I ran into this gap again. I was working on a &lt;a href="https://dev.to/blog/the-programming-puzzle-that-got-me-my-job/"&gt;programming puzzle&lt;/a&gt; that required near-instant substring search over a large dataset. After some research, I settled on a &lt;a href="https://dev.to/project/suffix-tree/"&gt;Generalized Suffix Tree&lt;/a&gt;: a data structure that indexes all suffixes of a set of strings, enabling &lt;em&gt;O(m)&lt;/em&gt; lookups where &lt;em&gt;m&lt;/em&gt; is the length of the search pattern, even over an extremely large corpus.&lt;/p&gt;

&lt;p&gt;The algorithm I chose for building the tree was Ukkonen’s, described in a &lt;a href="https://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf" rel="noopener noreferrer"&gt;1995 paper&lt;/a&gt;. The paper is well written and includes the full algorithm in pseudocode:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyhepn1yvc6dk9ozu5h9s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fyhepn1yvc6dk9ozu5h9s.png" alt="One of several pseudocode snippets from Ukkonen's paper, describing the update function. Clear on paper, but its translation to working code is much more verbose than this." width="800" height="462"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;One of several pseudocode snippets from Ukkonen's paper, describing the update function. Clear on paper, but its translation to working code is much more verbose than this.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It took me a few hours to get right. Not because the pseudocode was wrong: it was precise and correct. The difficulty was that the algorithm manipulates a tree in non-obvious ways. There is an “active point” that walks around the tree. Suffix links connect internal nodes as shortcuts. Three different extension rules fire depending on what is already in the tree and what is being added. The pseudocode tells you &lt;em&gt;what&lt;/em&gt; to do, but building an intuition for &lt;em&gt;why&lt;/em&gt; it works requires watching it happen.&lt;/p&gt;

&lt;p&gt;I did what I always did: I sketched trees by hand. I traced the algorithm on the string &lt;code&gt;cacao&lt;/code&gt;, then on &lt;code&gt;banana&lt;/code&gt;, drawing and redrawing nodes and edges as each character was processed. When my &lt;a href="https://github.com/abahgat/suffixtree" rel="noopener noreferrer"&gt;Java implementation&lt;/a&gt; finally produced correct results, I was relieved, but my understanding of the algorithm still felt like it had been assembled from fragments.&lt;/p&gt;

&lt;p&gt;The biggest frustration was that I had no way to inspect what my code was actually building. I relied on the usual bag of tricks: print statements, breakpoints, inspecting memory structures one by one in a debugger. But that is like understanding a forest by looking at one tree at a time. What I wanted was to &lt;em&gt;see&lt;/em&gt; the whole data structure after each operation — to watch the algorithm work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visualization I wish I had
&lt;/h2&gt;

&lt;p&gt;That idea stuck with me: build the algorithm in a language where rendering the data structure is easy, then step through the construction visually. JavaScript and &lt;a href="https://d3js.org/" rel="noopener noreferrer"&gt;D3.js&lt;/a&gt; are a natural fit: the algorithm produces a tree, and D3 is very good at drawing trees.&lt;/p&gt;

&lt;p&gt;So here it is. The visualization below builds a suffix tree for the string &lt;code&gt;banana&lt;/code&gt; using Ukkonen’s algorithm, step by step. Use the playback controls to move through the construction. The gold-highlighted node is the active point. Dashed arcs are suffix links.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5vbfvht15aopbnsd8919.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F5vbfvht15aopbnsd8919.gif" alt="Suffix tree for " width="760" height="744"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This post was originally published on &lt;a href="https://www.abahgat.com/blog/visualizing-ukkonens-algorithm/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=syndication&amp;amp;utm_content=gif"&gt;abahgat.com&lt;/a&gt;, where the visualizations are fully interactive — you can step through the algorithm, zoom, and pan. The GIFs here show a condensed version of the construction.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The paper describes the core logic across Sections 2–4. Here is &lt;code&gt;test_and_split&lt;/code&gt;, the procedure that decides whether the tree needs to grow, which is a companion to the &lt;code&gt;update&lt;/code&gt; function we showed earlier:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpctvwin3sohp0uryvvq1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fpctvwin3sohp0uryvvq1.png" alt="Procedure test_and_split from Ukkonen's paper. It returns true when the next character is already in the tree (the end point), and false after splitting an edge to make room for a new branch." width="800" height="468"&gt;&lt;/a&gt; &lt;br&gt;
&lt;em&gt;Procedure test_and_split from Ukkonen's paper. It returns true when the next character is already in the tree (the end point), and false after splitting an edge to make room for a new branch.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A few things to watch for in the visualization — each one corresponds to something in this procedure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Branching in &lt;code&gt;update&lt;/code&gt;:&lt;/strong&gt; when &lt;code&gt;test_and_split&lt;/code&gt; finds no existing transition for the next character, it splits the edge if needed and &lt;code&gt;update&lt;/code&gt; creates a new leaf. These are the moments where the tree visibly grows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reaching the end point:&lt;/strong&gt; when &lt;code&gt;test_and_split&lt;/code&gt; finds that a transition for the next character already exists, the algorithm has reached what the paper calls the &lt;em&gt;end point&lt;/em&gt; of the current phase. All remaining suffixes are already represented implicitly, so the phase stops. This is the key to the algorithm’s &lt;em&gt;O(n)&lt;/em&gt; time: the end point can only move forward through the string across phases, bounding the total work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suffix links&lt;/strong&gt; (the paper’s &lt;em&gt;suffix function&lt;/em&gt; f): if an internal node has path-label xα, its suffix link points to the node with path-label α. The &lt;code&gt;update&lt;/code&gt; procedure follows these links to jump to the next insertion point instead of walking from the root every time.&lt;/li&gt;
&lt;li&gt;Finally, &lt;strong&gt;the ”$” terminator&lt;/strong&gt; converts an &lt;em&gt;implicit&lt;/em&gt; suffix tree, where some suffixes may end mid-edge, into an &lt;em&gt;explicit&lt;/em&gt; one where every suffix terminates at a distinct leaf.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Adding more strings
&lt;/h2&gt;

&lt;p&gt;A generalized suffix tree indexes multiple strings. Each string is added with its own terminator, and the tree grows incrementally. Below, &lt;code&gt;panama&lt;/code&gt; is added after &lt;code&gt;banana&lt;/code&gt;. Step through and notice how much of the tree structure already exists from the first string.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fomneqziitiabe9ekor9g.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fomneqziitiabe9ekor9g.gif" alt="Tree for " width="720" height="750"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching
&lt;/h2&gt;

&lt;p&gt;Once the tree is built, searching for a pattern means matching characters along edges from the root. The visualization below has both strings pre-loaded. Try searching for &lt;code&gt;ana&lt;/code&gt;, then try &lt;code&gt;pan&lt;/code&gt;, &lt;code&gt;ban&lt;/code&gt;, &lt;code&gt;xyz&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fblex9n06hzof3r1x9f3m.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fblex9n06hzof3r1x9f3m.gif" alt="Searching for a suffix" width="800" height="834"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;An empty tree, yours to experiment with. Add strings, watch the construction, search for patterns. Use the scroll wheel to zoom and click-drag to pan if the tree gets large.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Head over to the &lt;a href="https://www.abahgat.com/blog/visualizing-ukkonens-algorithm/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=syndication&amp;amp;utm_content=try"&gt;original post&lt;/a&gt; to experiment with an empty tree: add your own strings, watch the construction step by step, and search for patterns. Use the scroll wheel to zoom and click-drag to pan if the tree gets large.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Beyond suffix trees
&lt;/h2&gt;

&lt;p&gt;What excites me most is how well this generalizes. The gap between an algorithm on paper and an algorithm in memory has always been one of the hardest parts of learning computer science. Textbooks give you static diagrams. Debuggers give you one node at a time. Neither shows you the whole picture in motion.&lt;/p&gt;

&lt;p&gt;Browser-based rendering, interactive SVGs, and JavaScript engines fast enough to run non-trivial algorithms client-side make it possible to close that gap for almost any data structure. Red-black trees, B-trees, tries, skip lists, hash tables with open addressing: all of them would benefit from this kind of treatment. Not as a replacement for the theory, but as a companion to it. Read the algorithm, then &lt;em&gt;watch&lt;/em&gt; it work.&lt;/p&gt;

&lt;p&gt;There is an obvious question lurking here: why bother learning algorithms at all when you can ask an LLM to write one for you? I think the question misses the more interesting possibility. LLMs are not just code generators; they are learning accelerators. You can ask one to explain a single step of an algorithm, to walk through an edge case, or to generate a diagram of how components interact. When I started working in a new codebase recently, the fastest way for me to build a mental model was not reading code or documentation. It was asking an LLM to produce component and sequence diagrams: a much higher-bandwidth channel for understanding, at least for the way I think.&lt;/p&gt;

&lt;p&gt;That is the real shift. Not that machines can write algorithms so we don’t have to learn them, but that they can teach us in ways that adapt to how each of us actually learns. Through visualizations, through diagrams, through conversation, through whatever representation makes the concept click. This post is one example. The next one might look completely different, tailored to a different person and a different way of thinking.&lt;/p&gt;

&lt;p&gt;We write fewer algorithms from scratch in our day-to-day work than we used to. But we still benefit from understanding them, whether it’s to choose the right data structure, to debug performance issues, or to evaluate tradeoffs. And for those of us who enjoy algorithms for their own sake, the tools for learning them have never been better.&lt;/p&gt;

&lt;p&gt;The original Java suffix tree implementation is &lt;a href="https://github.com/abahgat/suffixtree" rel="noopener noreferrer"&gt;open source on GitHub&lt;/a&gt;. For the full backstory, see the &lt;a href="https://www.abahgat.com/project/suffix-tree/" rel="noopener noreferrer"&gt;project page&lt;/a&gt; and the &lt;a href="https://www.abahgat.com/blog/the-programming-puzzle-that-got-me-my-job/" rel="noopener noreferrer"&gt;story of the programming puzzle&lt;/a&gt; that started it all. Ukkonen’s &lt;a href="https://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf" rel="noopener noreferrer"&gt;original paper&lt;/a&gt; remains the definitive reference for the algorithm.&lt;/p&gt;




&lt;p&gt;This post was originally published on &lt;a href="https://www.abahgat.com/blog/visualizing-ukkonens-algorithm/?utm_source=dev.to&amp;amp;utm_medium=referral&amp;amp;utm_campaign=syndication&amp;amp;utm_content=footer"&gt;abahgat.com&lt;/a&gt; on March 9, 2026. If you enjoyed this piece, you can &lt;a href="//www.linkedin.com/comm/mynetwork/discovery-see-all?usecase=PEOPLE_FOLLOWS&amp;amp;followMember=abahgat"&gt;follow me on LinkedIn&lt;/a&gt; for more thoughts on engineering leadership and software.&lt;/p&gt;

</description>
      <category>algorithms</category>
      <category>datastructures</category>
      <category>computerscience</category>
    </item>
    <item>
      <title>The Velocity Paradox</title>
      <dc:creator>Alessandro Bahgat</dc:creator>
      <pubDate>Tue, 03 Mar 2026 13:47:00 +0000</pubDate>
      <link>https://dev.to/abahgat/the-velocity-paradox-4182</link>
      <guid>https://dev.to/abahgat/the-velocity-paradox-4182</guid>
      <description>&lt;p&gt;We've all been there. You sit down with an AI agent on a Saturday morning to hack on a side project and it feels like magic. Ten minutes in, you are blown away by how quickly the agent can turn even poorly organized thoughts into working prototypes. You feel like you could do this all day.&lt;/p&gt;

&lt;p&gt;And clearly, many of us do: we're rediscovering our passion for side projects, and every day a thousand bespoke ToDo apps are born, perfectly tailored to the unique needs of their creators.&lt;/p&gt;

&lt;p&gt;At the same time, if you're in an engineering leadership role, you're also seeing your stakeholders dabble with agentic coding. They are shipping side-hustles on the weekend, and respectable work applications in an afternoon. Some of them might even look at you with ill-concealed suspicion. They want to know why their "pet feature" is stuck in a two-week cycle when they just whipped up a functional prototype over coffee.&lt;/p&gt;

&lt;p&gt;And they aren't entirely wrong. AI agents have been writing 100% of my code for several months now. Informed by the wins on my side-projects, I wanted to see how much faster we could build at work. During the holiday break, I spent a few hours having Claude write a non-trivial feature that touched our database, cloud infra, mobile app, and the embedded application that runs on our hardware devices at Quilt. What would have taken me a week to write took an afternoon to generate.&lt;/p&gt;

&lt;p&gt;Yet it still took weeks to get it tested and merged.&lt;/p&gt;

&lt;p&gt;It felt like strapping a rocket engine to a tricycle. Exhilarating, sure, but the road ahead is still full of potholes, and there's a canyon where the bridge used to be. So why isn't the 100x improvement in how fast AI can &lt;em&gt;generate&lt;/em&gt; code moving the needle on how fast we can &lt;em&gt;ship&lt;/em&gt; features and improvements?&lt;/p&gt;

&lt;p&gt;Coding was never 100% of the job. But for those of us managing legacy debt, AI doesn't just fail to solve our problems; it collides with them.&lt;/p&gt;

&lt;p&gt;I've been at several conferences recently where I met leaders from "AI-native" companies, organizations founded in an age where agentic coding is the baseline. One founder told me they don't do code reviews at all; their CI pipeline is the reviewer. Another gives agents full control of their production infrastructure. For those of us anchored to a culture that is older than even just two years, these practices feel reckless. Yet even more measured companies are rethinking the fundamentals. OpenAI recently pulled back the curtain with their &lt;a href="https://openai.com/index/harness-engineering/" rel="noopener noreferrer"&gt;Harness Engineering&lt;/a&gt; article, showing engineering re-architected around AI from the ground up.&lt;/p&gt;

&lt;p&gt;For the rest of us, the gap between "generating code" and "shipping value" is becoming a chasm. We are stuck in the Unhappy Middle, where the cost of code is diminishing rapidly, but the cost of review and verification is skyrocketing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Unhappy Middle
&lt;/h2&gt;

&lt;p&gt;To understand why the promise of 100x faster progress thanks to AI still feels like an illusion, we have to look at the two forces we're being squeezed by.&lt;/p&gt;

&lt;p&gt;On one side, we have the AI-Natives. These are companies and teams founded in the AI era. They have zero legacy debt, they can approach the craft of engineering with an open mind, and they use the same exact "boring" tech stacks the models were trained on. They don't have to go out of their way to "integrate" AI; they are born out of it. They don't have to refactor their code to support automated verification, they never knew a world without it.&lt;/p&gt;

&lt;p&gt;On the other side, you have the companies with the slack to reinvent themselves. Shopify's CEO made headlines when he declared that &lt;a href="https://x.com/tobi/status/1909251946235437514" rel="noopener noreferrer"&gt;AI proficiency is now a baseline expectation&lt;/a&gt; and that teams must justify why a job can't be done by AI before requesting headcount. Companies like that (or Google, I bet) can dedicate teams to rearchitect their codebase, tooling and processes and build the scaffolding that is required to make AI work at scale.&lt;/p&gt;

&lt;p&gt;Then, there's the rest of us. I call it the Unhappy Middle.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We support live products and services, with customers trusting us and depending on us daily. The cost of failure is higher than a toy prototype. Unlike your ToDo app, you can't just throw an agent at a problem and hope it doesn't break your production environment.&lt;/li&gt;
&lt;li&gt;We have accumulated technical debt as we were racing towards product/market fit, and yet never had the resources to pay it back. We have to balance work on infrastructure and developer experience with business priorities like opening new product lines. Most of these target ambitious schedules which (you guessed right) require taking on additional technical debt.&lt;/li&gt;
&lt;li&gt;With the age of Zero Interest-Rate Policies well behind us, but not quite with the coffers of a larger company, we always have to be mindful of our runway, are constantly short-staffed and always "do more with less".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In short, we have to balance the technical complexity of an established company with the reality of a startup. Our survival depends on crossing the chasm as quickly as possible. Not every team is here. If your stack is standard and your tests are green, you may already be seeing the gains. But if any of this sounds familiar, the path forward is harder. Here are some examples from my reality.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bespoke Frameworks: from Asset to Dead Weight
&lt;/h3&gt;

&lt;p&gt;Before AI, we may have optimized for human speed by building bespoke frameworks, custom boilerplate generators or domain-specific languages and abstractions. For many teams, these were their "secret sauce": internal abstractions that helped teams move fast in 2022. They came at a price (typically, new engineers have to take some time getting comfortable with them), but they often paid off.&lt;/p&gt;

&lt;p&gt;Today, those clever optimizations are anchors holding us back. AI agents are brilliant at standard React and Python because they've seen it a billion times. And, at the same time, they are completely illiterate in our proprietary and opinionated internals. Every time I ask an agent to work in our bespoke code, I'm paying an invisible tax: I spend a third of my time fixing hallucinations because our "clever" code isn't in anyone's training set. (I wrote more about why this happens in &lt;a href="https://dev.to/blog/the-ghost-in-the-training-set"&gt;The Ghost in the Training Set&lt;/a&gt;.)&lt;/p&gt;

&lt;p&gt;And you know what's funny? That's often why some of the best engineers I know are unimpressed by AI agents: they focus on the last time they saw Claude trip on a gotcha that's specific to their codebase and ignore the fact that it can build flawless React in the blink of an eye.&lt;/p&gt;

&lt;h3&gt;
  
  
  Zero Slack
&lt;/h3&gt;

&lt;p&gt;We know technical debt is there, we always wanted to increase test coverage, we defer refactoring for testability because we need to fit one more feature before the release cut. We know that frameworks need to be standardized to become "AI-hospitable." But in the Unhappy Middle, you have zero slack. You're always racing, either to hit product-market fit or to extend your runway, and "cleaning up" feels like a luxury you can't afford.&lt;/p&gt;

&lt;p&gt;This creates a painful tradeoff. In a side project, or a non-critical business app, failure is cheap. For a company with a legacy codebase, complex release processes and addressing user-critical needs, the stakes are considerably higher. Without the slack to build automated guardrails, we're left with manual human review and auditing.&lt;/p&gt;

&lt;p&gt;And that's where the 100x speed gain from AI goes to die.&lt;/p&gt;

&lt;h2&gt;
  
  
  When Generation Outruns Verification
&lt;/h2&gt;

&lt;p&gt;We often think of the craft of software engineering as composed of several loops, each covering a different stage of the lifecycle, from idea to product. A good visual to illustrate this is the slide below, from a &lt;a href="https://youtu.be/qi89lhRI8zc?t=207" rel="noopener noreferrer"&gt;talk Addy Osmani gave at LeadDev New York 2025&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fklxahn7p3ktdl449f532.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fklxahn7p3ktdl449f532.png" alt="Development Loop" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;At the center is the &lt;em&gt;Inner Loop&lt;/em&gt;: the tight cycle of thinking, coding, building and testing. This is where "flow" happens. Surrounding that is the &lt;em&gt;Submit Loop&lt;/em&gt;, where your code goes through linting and code review, and the &lt;em&gt;Outer Loop&lt;/em&gt;, where it finally gets deployed and gets tested in the real world.&lt;/p&gt;

&lt;p&gt;The promise of AI-assisted engineering is to effectively collapse the Inner Loop. When an agent can "Think" and "Code" a cross-stack feature in a single morning, that center circle feels like it's spinning at the speed of light.&lt;/p&gt;

&lt;p&gt;But for those of us who are still in the Unhappy Middle, that loop is often broken before it even starts.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Broken Inner Loop
&lt;/h3&gt;

&lt;p&gt;The first problem teams are likely to encounter is a broken Inner Loop. Before AI, back in the day when code was expensive to write, tests were the first aspect of a healthy architecture to be sacrificed (or, in the best case scenario, deferred). When we skip writing tests, it's common to end up with code for which it's hard to write tests in the long run.&lt;/p&gt;

&lt;p&gt;When you can't give an agent a deterministic way to verify its own work, the feedback cycle doesn't feed back into the AI, it feeds back into &lt;strong&gt;you&lt;/strong&gt;. The agent isn't looping, it's just throwing code over the wall and waiting for you to tell it what happened.&lt;/p&gt;

&lt;p&gt;In the best scenario you can imagine, the loop is closed by automation. The agent writes code, runs a test, sees the failure and iterates until it's green. The feedback is a tight, self-correcting circuit.&lt;/p&gt;

&lt;p&gt;Without a way to automate verification, you're just making a mountain of work for yourself, or accepting to take an enormous amount of risk by shipping code that hasn't been properly tested.&lt;/p&gt;

&lt;p&gt;You were promised AI agents working for you to help you be more effective; instead, you are working for your agents. Not only is it not fun, it's also a huge waste of your time because you are 100x slower than a software agent.&lt;/p&gt;

&lt;p&gt;In my world, this isn't just a metaphor. I feel it physically. At Quilt, we make hardware devices, and you can't throw prompt engineering at the physical world. If a test requires me to get up, walk to a test bench and manually press a button, the inner loop isn't just broken; it's wide open.&lt;/p&gt;

&lt;p&gt;And there are even worse consequences downstream.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Slowing Submit Loop
&lt;/h3&gt;

&lt;p&gt;Before AI agents were this capable, the high cost of &lt;em&gt;writing&lt;/em&gt; code carried a hidden benefit. If an engineer spent two days wrestling with a complex feature, they effectively distilled a lot of context information into their brain. By the time they put a change up for review, the author was the deepest expert on those 200 lines of code.&lt;/p&gt;

&lt;p&gt;That's not how it works today.&lt;/p&gt;

&lt;p&gt;As wonderful as the democratizing effect of AI agents is (they enable engineers to contribute well beyond their historical area of expertise), it comes with downsides.&lt;/p&gt;

&lt;p&gt;If an agent can't automatically verify its changes, and the author is not the most experienced engineer in the area affected by a change, the bulk of the burden of audit and review will shift to the reviewer.&lt;/p&gt;

&lt;p&gt;On the average team, code reviews are assigned to the most experienced engineers in a given area or domain. In this new world, these folks are getting overloaded with more code to review. Worse, they can no longer assume that the author has the same depth of knowledge about the code that reviewers historically could take for granted.&lt;/p&gt;

&lt;p&gt;At the extreme, this has multiple effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Because the agent did the heavy lifting, the human author may have a shallower understanding of the "why" behind specific implementation choices.&lt;/li&gt;
&lt;li&gt;The reviewer is now receiving 10x more code, but with 10x less intent provided by the author. If the reviewer didn't (or couldn't) do a thorough review themselves, it's 10x more code reviews of a higher intensity. Think more of a forensic audit than a style check.&lt;/li&gt;
&lt;li&gt;In a legacy codebase with bespoke frameworks, this can be extremely challenging. If neither the author nor the reviewer fully understands the "clever" choices the AI made, they can't distinguish between valuable additions and hallucinations, and therefore are taking a high risk shipping this to production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical consequences are tangible. Code ends up spending more time waiting for review than in development (this is what happened to my proof of concept I mentioned earlier). Your most experienced engineers struggle to be productive themselves because they are drowning in code reviews.&lt;/p&gt;

&lt;p&gt;But the most worrisome part is what this does at an emotional level.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Craftspeople to Janitors
&lt;/h2&gt;

&lt;p&gt;If we take the patterns above to the extreme and let them fester without fixing them, then we are taking on a huge organizational risk by turning our most senior engineers into Janitors.&lt;/p&gt;

&lt;p&gt;Instead of going to a challenging workday where, at the end, we experience the joy of having created something new, we now have to pore over someone (or, rather, something) else's code to spot issues and problems. Some engineers feel like they are being paid to clean up AI hallucinations.&lt;/p&gt;

&lt;p&gt;This can be deeply demotivating. No one likes being a linear bottleneck downstream of a stage that is accelerating at exponential speed. This is even more difficult at the speed this shift is happening, as many people are mourning the loss of the craft, made worse by simplistic takes about how the world of tomorrow needs fewer engineers.&lt;/p&gt;

&lt;p&gt;I still deeply enjoy coding but I recognize that, even in the best of days, a lot of the code I wrote was boilerplate needed to wire together different application components. A very common micro-kitchen joke from my time at Google was that we were all just highly-compensated Protocol Buffer translators.&lt;/p&gt;

&lt;p&gt;We miss the 20% of the code we used to write that was high-leverage and intellectually interesting, and forget the other 80% that was toilsome and repetitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Janitors to Gardeners
&lt;/h2&gt;

&lt;p&gt;If you treat every AI-generated PR like a chore to be cleaned up, you are a Janitor. To move fast in a legacy codebase, we need a considerable change in mindset. If you allow me another metaphor, we need to start treating our codebase less like a perfect jewel to polish and more like a plot of land to tend to.&lt;/p&gt;

&lt;p&gt;I've been thinking about this metaphor for a while. As you scale an organization, you can't afford to micromanage; you provide structure and support so that decisions happen organically, aligned to what the business needs. The same applies to codebases.&lt;/p&gt;

&lt;p&gt;Playing into the metaphor, a gardener may focus their attention on a few things:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tending the Soil&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hospitable Ground&lt;/strong&gt; -- Transforming AI-Hostile codebases into an AI-Hospitable playing field requires investing in reducing technical debt, so that AI can't hide behind it. It may mean moving away from bespoke patterns that routinely trip up agents, or making them work reliably. It means standardizing on a well-defined and documented set of abstractions, instead of having 3 different ways to set up an API server because we never finish migrations every time we deprecate an old pattern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nutrient-Rich Soil&lt;/strong&gt; -- Agents are great at brute-forcing their way to a workable solution, but very often they struggle because the codebase lacks information beyond the code itself. Code written in haste often lacks documentation about "Intent" and the "Why" we made decisions. If we don't expose context about tradeoffs and historical decisions, our agents are operating with limited information. Well structured &lt;code&gt;agents.md&lt;/code&gt; files are a good start. Checking in architectural guidelines and making them discoverable is increasingly paying off. Ironically, if you keep your design docs locked in Google Docs, your agent is blind to them (hey Google, when can we have MCP access to Google Docs?)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Scaffolding and Direction&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Scaffolding&lt;/strong&gt; -- You don't tell plants how to grow and expect them to listen; you provide scaffolding and support. In software, this can be types, interfaces and architectural boundaries. Well crafted designs that reduce coupling and abstract complexity behind well-defined interfaces are how you give agents a way to grow that is aligned to what you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resilience&lt;/strong&gt; -- Automated tests, lint checks and verifications are much more helpful for AI agents than they are to humans, as they enable both faster iteration speed and more confidence in the review stage of the submit loop. In the gardening metaphor, this is akin to the sturdy fencing that protects your plants from critters.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I find it ironic that many of the principles above are ones that practitioners have been advocating for under the banner of clean code, test-driven development and many others. We might callously shrug at the idea that we struggled to adopt them for the sake of our human co-workers and are now prioritizing them for the sake of our AI-agents. But the truth is that in the last decade, writing effective tests and good documentation cost us time: the time to think about them, and the time to type them. With AI agents being this capable, the typing cost is approaching zero. What remains is the thinking, and that was always the valuable part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Building the Dark Factory
&lt;/h2&gt;

&lt;p&gt;Our job is no longer to write the code. It's to build the factory that builds the code.&lt;/p&gt;

&lt;p&gt;By now, it should be obvious that if we use AI only to automate the "Coding" stage of the development loop, we may not only struggle to make our team more effective, we may even hurt their effectiveness.&lt;/p&gt;

&lt;p&gt;In the same talk by Addy Osmani I referenced earlier, he goes on to show several areas where AI can be effectively adopted to improve developer experience. In my day-to-day work, I've had considerable success using AI agents to troubleshoot bug reports and infrastructure alerts from our production fleet. The gains are real.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feyz3j0vq1ki7inm48nwk.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feyz3j0vq1ki7inm48nwk.png" alt="Annotated development loop" width="800" height="444"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There is a growing conversation in engineering circles about "&lt;a href="https://www.danshapiro.com/blog/2026/01/the-five-levels-from-spicy-autocomplete-to-the-software-factory/" rel="noopener noreferrer"&gt;Dark Factories&lt;/a&gt;": fully automated systems that run without human intervention. In the age of AI, our job is no longer to write the code; it's to &lt;strong&gt;build the factory that builds the code.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some high-leverage areas to start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Verification Machine&lt;/strong&gt; -- Good test infrastructure should be the top priority. Well-written tests enable AI-agents to have much faster inner loops, but they also greatly help with faster code reviews. With good test scaffolding, you don't just ask "Will this code work in this scenario?" You can ask an agent to demonstrate the expected behavior via a unit test.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Address common tripping hazards for agents&lt;/strong&gt; -- You likely have a few areas where agents routinely struggle. Don't just scoff when that happens, and use it to say "AI isn't quite there yet". Ask yourself &lt;em&gt;why&lt;/em&gt; agents are struggling. Is it because of inconsistent patterns? Lack of context or documentation? Because your bespoke framework requires 1 year of experience &lt;em&gt;in your own codebase&lt;/em&gt; to master? Making sure agents don't make the same mistake twice should be part of our responsibilities.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reducing human dependencies for mechanical tasks&lt;/strong&gt; -- Invest in building reliable automated end to end tests that rely on production-like observability to spot issues and regressions. Wherever manual testing is required, ask yourself "what would it take for this test to happen automatically?" In a hardware company like Quilt, this means augmenting our ability to perform more tests in software.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Lights-Out Goal&lt;/strong&gt; -- Aim to have a "Submit Loop" so robust that if tests pass and the architectural boundaries are respected, the code is "shippable" by default. Even if that goal feels unrealistic (e.g. for code that is security-critical or that runs on devices that are hard to recover), ask yourself "What would it take for me to be 100% confident in a change without needing to review it?"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A word of warning: don't confuse building the factory with building more features. If you ship 10x more features without correspondingly improving your infrastructure, you're taking on a compounding liability. If AI agents today are enabling you to move even just a bit faster than yesterday, aim to put some of those velocity gains towards your scaffolding, instead of putting everything on more features.&lt;/p&gt;

&lt;h2&gt;
  
  
  Crossing the Chasm
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9j060nn8lbm082nn07zy.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9j060nn8lbm082nn07zy.png" alt="A rocket-powered bicycle approaching a broken bridge over a canyon" width="800" height="339"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If the smartest AI in the world can't understand your code, it might not be the AI's fault.&lt;/p&gt;

&lt;p&gt;The Unhappy Middle is a trap, but it's also an opportunity to rethink what engineering leadership looks like.&lt;/p&gt;

&lt;p&gt;This requires a fundamental shift in our ego as developers. Instead of 'pwning' the agent every time it trips on our proprietary abstractions, we need to 'own' our codebase and make it more AI-hospitable. If the smartest AI in the world can't understand your code, it might not be the AI's fault, but it might be a sign that our "cleverness" has become our biggest liability.&lt;/p&gt;

&lt;p&gt;If we don't cross the chasm quickly and change our mindset about how we write software, we risk being buried under our own AI-generated slop. The first step is to stop prioritizing just features as our primary output and start prioritizing the speed and accuracy of the factory.&lt;/p&gt;

&lt;p&gt;It is notoriously hard to get organizational buy-in to address technical debt. The key is to reframe: this isn't about "cleaning up" to pay off debt, it's about investing in tooling to accomplish 10x velocity.&lt;/p&gt;

&lt;p&gt;And even then, there are harder questions ahead. If you actually succeed in building the "factory," you'll quickly find that the technical bottleneck has evaporated, only to leave you with an organizational one. A 10x software factory is effectively useless if it's embedded in a 1x decision-making process. And it is possible that we are approaching a &lt;a href="https://en.wikipedia.org/wiki/Great_Filter" rel="noopener noreferrer"&gt;Great Filter&lt;/a&gt;-like event for companies in the business of software — one that separates those who adapt from those who drown. But those are topics for another day.&lt;/p&gt;

&lt;p&gt;For now, the goal is clear: stop just auditing lines of code and start building the systems that define the future of our industry.&lt;/p&gt;

&lt;p&gt;Let us begin.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This article was originally published on &lt;a href="https://www.abahgat.com/blog/the-velocity-paradox/?utm_source=platform&amp;amp;utm_medium=repost&amp;amp;utm_campaign=the-velocity-paradox&amp;amp;utm_content=footer" rel="noopener noreferrer"&gt;abahgat.com&lt;/a&gt; on February 23, 2026. If you enjoyed this piece, feel free to &lt;a href="https://www.linkedin.com/in/abahgat" rel="noopener noreferrer"&gt;follow me on LinkedIn&lt;/a&gt; for more thoughts on engineering leadership and software.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>devex</category>
      <category>programming</category>
      <category>testing</category>
    </item>
    <item>
      <title>The Ghost in the Training Set</title>
      <dc:creator>Alessandro Bahgat</dc:creator>
      <pubDate>Mon, 16 Feb 2026 20:10:18 +0000</pubDate>
      <link>https://dev.to/abahgat/the-ghost-in-the-training-set-496n</link>
      <guid>https://dev.to/abahgat/the-ghost-in-the-training-set-496n</guid>
      <description>&lt;p&gt;During the last several weeks, I've run into setting up MCP servers a few times and noticed something surprising. MCP has been gaining popularity and, as things mature, it's running into paradigm shifts. In early 2025, the recommended way to build MCP servers over HTTP switched from SSE (Server-Sent Events) to &lt;a href="https://modelcontextprotocol.io/specification/2025-03-26/basic/transports#streamable-http" rel="noopener noreferrer"&gt;&lt;strong&gt;Streamable HTTP&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;To my surprise, the agents I use most (Gemini and Claude) kept reverting to SSE. It wasn't until I started digging that I realized what was happening: the models were haunted by the &lt;strong&gt;statistical momentum&lt;/strong&gt; of their own training data.&lt;/p&gt;

&lt;p&gt;Even when LLMs are aware that Streamable HTTP is the standard now—and can competently answer questions about it when asked—the "statistical momentum" in their training data pulls them back to the old standard. Because most of the examples they have seen were written using the old approach, they default to it when generating code.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: Astutely, Claude ships with an &lt;code&gt;/mcp-builder&lt;/code&gt; skill, which serves as a specialized instruction package. Try building an MCP server with Gemini now and you'll be surprised to get a perfectly functional implementation built on a deprecated pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Happening: The Invisible Weight of Training Bias
&lt;/h2&gt;

&lt;p&gt;LLMs don't just "read" instructions in a traditional sense; they weigh them against their internal probability map. If the majority of MCP implementations in their training set used SSE, that creates a massive bias in that direction.&lt;/p&gt;

&lt;p&gt;This is a sneaky pattern. We don't naturally think about how old (or new) a model's training set is. If you're working on a bleeding-edge domain, you may find yourself with an agent offering a beautiful implementation that is actually a frozen snapshot of &lt;em&gt;last year's&lt;/em&gt; best practices. &lt;/p&gt;

&lt;p&gt;Agents thrive on &lt;em&gt;Common Knowledge&lt;/em&gt;, but they struggle with &lt;em&gt;Private Context&lt;/em&gt;. When we use bespoke patterns or fast-moving standards, we are essentially moving the agent into a zero-shot environment without even realizing it.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Instructions to Infrastructure
&lt;/h2&gt;

&lt;p&gt;You may be tempted to overcome this through prompting (&lt;code&gt;ALWAYS use Streamable HTTP&lt;/code&gt;), but over time, you should move these guardrails into your &lt;code&gt;agents.md&lt;/code&gt; files. We need to shift technical standards out of lossy prompts and into the tooling infrastructure.&lt;/p&gt;

&lt;p&gt;Well-written skills and tools help a lot here. Anthropic's &lt;code&gt;/mcp-builder&lt;/code&gt; is extremely effective in ensuring you land with a well-functioning implementation that overcomes the inherent bias in the models.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Trap of "Contextual Debt"
&lt;/h2&gt;

&lt;p&gt;Just like code accumulates technical debt, continuously adding to &lt;code&gt;agents.md&lt;/code&gt; without cleaning up leads to &lt;strong&gt;Contextual Debt&lt;/strong&gt;. Files become bloated with a mountain of "Don't do X" or "Remember Y." &lt;/p&gt;

&lt;p&gt;We are getting to a point where our "Instruction Budget" is as important as our compute budget. If you have clashing instructions across multiple files, you're not just wasting tokens; you're creating "hallucination traps" that are far more expensive to debug than a standard syntax error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Strategies for Garbage Collecting &lt;code&gt;agents.md&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Here are a few things that seem to work for me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Progressive Disclosure:&lt;/strong&gt; Borrow from the &lt;a href="https://resources.anthropic.com/hubfs/The-Complete-Guide-to-Building-Skill-for-Claude.pdf" rel="noopener noreferrer"&gt;Claude skills playbook&lt;/a&gt;. Instead of one giant instruction file, use a modular approach (e.g., a &lt;code&gt;docs/MCP_STANDARDS.md&lt;/code&gt; file linked from your root &lt;code&gt;agents.md&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The "Zero-Prompt Test":&lt;/strong&gt; Periodically run your project with a blank instruction file, especially after model updates. If it works well, your instructions have become cruft. Delete them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Project-Level Ground Truth:&lt;/strong&gt; Get your team to own maintaining agent configs as much as they would maintain their editor configurations. Up-to-date documentation is now more precious than ever.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion: Managing the Agent's AI "Memory"
&lt;/h2&gt;

&lt;p&gt;Regardless of the tropes around "software engineering being dead," more of our job is moving &lt;em&gt;up&lt;/em&gt; the stack from writing code. &lt;/p&gt;

&lt;p&gt;We are increasingly managing the attention and memory of our agents. The most sustainable systems will be the ones where instructions and scaffolding are pruned as ruthlessly as—if not more than—the code itself.&lt;/p&gt;




&lt;h3&gt;
  
  
  Enjoyed this?
&lt;/h3&gt;

&lt;p&gt;I write about the intersection of engineering leadership and the "agentic" era. If you're navigating similar paradigm shifts in your own team, let's connect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn:&lt;/strong&gt; &lt;a href="https://www.linkedin.com/in/abahgat/" rel="noopener noreferrer"&gt;Follow me for more leadership insights&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal Blog:&lt;/strong&gt; &lt;a href="https://www.abahgat.com" rel="noopener noreferrer"&gt;abahgat.com&lt;/a&gt; — where I dive deeper into the code and the strategy.&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>ai</category>
      <category>mcp</category>
      <category>programming</category>
      <category>leadership</category>
    </item>
    <item>
      <title>Receiving Feedback Is A Skill</title>
      <dc:creator>Alessandro Bahgat</dc:creator>
      <pubDate>Wed, 02 Sep 2020 15:12:59 +0000</pubDate>
      <link>https://dev.to/abahgat/receiving-feedback-is-a-skill-1dog</link>
      <guid>https://dev.to/abahgat/receiving-feedback-is-a-skill-1dog</guid>
      <description>&lt;p&gt;Delivering feedback is a critical part of my day job as a manager at Google. However, it took me a while to realize that &lt;em&gt;receiving&lt;/em&gt; feedback is one of the skills that helped me grow the most in my career.&lt;/p&gt;

&lt;p&gt;For many of us, our job is the first setting where we receive developmental feedback from people other than our parents or teachers. That experience may be quite shocking.&lt;/p&gt;

&lt;p&gt;I still remember the first time I got professional feedback early in my career. I remember almost every single word that my manager chose to use.&lt;/p&gt;

&lt;p&gt;What I remember even more vividly though is the strong reaction that feedback caused in me. Within seconds, I got defensive, I felt like I was being criticized, attacked, unappreciated. I heard what they were trying to tell me, but something inside me kept translating that into a personal criticism. A statement about how I, personally, fell short of expectations.&lt;/p&gt;

&lt;p&gt;Good feedback sounds like  "here's one thing you can do better next time". Better feedback sounds like "here's one thing that you could do differently to achieve a greater result". &lt;/p&gt;

&lt;p&gt;Embracing that mindset allowed me to accept, process and build on feedback. While I can't say I &lt;em&gt;prefer&lt;/em&gt; criticism over praise, constructive feedback no longer makes me uncomfortable. Instead, I actively seek it.&lt;/p&gt;

&lt;p&gt;Changing my mindset around feedback required me to make two key changes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stop doing things that hurt my ability to improve&lt;/li&gt;
&lt;li&gt;start doing things that help build on what I hear&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Things I Stopped Doing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Taking It Personally
&lt;/h3&gt;

&lt;p&gt;The main reason I had a difficult time processing feedback is the fact that I often took it personally.&lt;/p&gt;

&lt;p&gt;When receiving feedback about something I did, I often read it as feedback about &lt;em&gt;me&lt;/em&gt;. Oftentimes, that was not the intention. &lt;/p&gt;

&lt;p&gt;Instead of hearing "this email was hard to understand", I heard "you do not communicate effectively". When the other party was saying "this piece of code is brittle", I was hearing "you are a lousy programmer".&lt;/p&gt;

&lt;p&gt;I often ended up reacting defensively. I was unable to hear and processing the actual message I needed to receive.&lt;/p&gt;

&lt;p&gt;Most developmental feedback will naturally trigger a defensive attitude. That prevents us from getting the full value of what the other person is trying to tell us. &lt;em&gt;We need to make a conscious effort to not jump to defensive mode, and rather engage in active listening&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Arguing With Feedback
&lt;/h3&gt;

&lt;p&gt;Even worse than taking feedback personally, I sometimes found myself wanting to argue with the person delivering it. I wanted to explain why I disagreed with what they were seeing or try to convince them that they were wrong.&lt;/p&gt;

&lt;p&gt;In most cases, arguing with feedback is pointless. Take an example from many years ago. &lt;/p&gt;

&lt;p&gt;A colleague approached me and told me "I think the comments you left in this review were too harsh".&lt;/p&gt;

&lt;p&gt;Now, if they cared enough to bring up this feedback, perhaps they were not the only ones. Or maybe my communication style could have had an unintended effect on some people, some time.&lt;/p&gt;

&lt;p&gt;Yes, I could have argued with my colleague, perhaps even convince them that my tone was not that bad. Winning the argument might even have felt better.&lt;/p&gt;

&lt;p&gt;That would not have changed the my comments did trigger a negative reaction for them. Quite likely, others might have had the same reaction. Knowing that, having that awareness, made me more thoughtful when writing review comments. I can tell they were better received from that moment on.&lt;/p&gt;

&lt;p&gt;Arguing with people who are trying to give us feedback, does not help us. Eventually, people will shy away from telling us where we can improve. It leads us to us working with less information about what we can do to get better. In the long run, we miss out on a significant opportunity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I Learned To Do Instead
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Being Thankful
&lt;/h3&gt;

&lt;p&gt;A friend of mine once shared a quote that sounded like "feedback is a gift"&lt;/p&gt;

&lt;p&gt;Good feedback is thoughtful and timely. Often, it is as difficult to deliver as it is to receive. It is especially difficult for people we are not very close with.&lt;/p&gt;

&lt;p&gt;Any yet, some people choose to take a risk. They let us know where we can do better. They do that knowing well that we may feel hurt by what they say.&lt;/p&gt;

&lt;p&gt;Because of this, the first thing I do when receiving feedback is thank whoever is giving it. I thank them because they took a risk and did something uncomfortable. I also thank them because what they are telling me has the potential of making me much better.&lt;/p&gt;

&lt;p&gt;Good feedback allows us to identify growth areas. Areas where we could invest more to get better at something we have been trying to do. Even those of us that have good self-awareness often need to work hard to find where they need to improve the most.&lt;/p&gt;

&lt;p&gt;If someone is coming to us with feedback, they may be sparing us a lot of hard work required to identify areas of improvement.&lt;/p&gt;

&lt;p&gt;The least we can do is thank them profusely for the gift they just gave us and get to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Following Up
&lt;/h3&gt;

&lt;p&gt;Whenever I receive feedback about something I can improve and want to work on, I note it down. Over time, this list becomes my feedback log.&lt;/p&gt;

&lt;p&gt;Keeping a list of the items I am trying to get better at is a way to hold myself accountable. I go through this feedback log every few weeks and reflect on the progress (or lack of progress) I have seen so far.&lt;/p&gt;

&lt;p&gt;This helps me making sure I make the most of the feedback I was generously given and use it to gradually get better. I try to spend some time every week to work on some of the most important items on the feedback log.&lt;/p&gt;

&lt;p&gt;Doing this helps me well beyond the result of addressing feedback. It also helps me ground my identity as someone who can accept feedback gracefully and use it as a tool to keep growing every day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;A few simple changes in perspective helped me change my view on feedback. I went from seeing it as a threat to my own self-worth to a stepping stone to become a better version of myself.&lt;/p&gt;

&lt;p&gt;The results of this attitude compound over time as I keep focusing my energy towards addressing the most critical feedback items.&lt;/p&gt;




&lt;p&gt;I originally published this post on my personal website (&lt;a href="https://abahgat.com" rel="noopener noreferrer"&gt;abahgat.com&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.twitter.com/abahgat" rel="noopener noreferrer"&gt;&lt;strong&gt;Follow me on Twitter&lt;/strong&gt;&lt;/a&gt; for more content like this.&lt;/p&gt;

</description>
      <category>career</category>
    </item>
    <item>
      <title>The Programming Puzzle That Landed Me a Job at Google</title>
      <dc:creator>Alessandro Bahgat</dc:creator>
      <pubDate>Mon, 27 Apr 2020 19:27:49 +0000</pubDate>
      <link>https://dev.to/abahgat/the-programming-puzzle-that-landed-me-a-job-at-google-1pc0</link>
      <guid>https://dev.to/abahgat/the-programming-puzzle-that-landed-me-a-job-at-google-1pc0</guid>
      <description>&lt;p&gt;Back in 2011, as I was getting a bored with my job and I started looking for new options. During my search, my friend Daniele (with whom I had built &lt;a href="https://www.abahgat.com/project/novlet" rel="noopener noreferrer"&gt;Novlet&lt;/a&gt; and &lt;a href="https://www.abahgat.com/project/bitlet" rel="noopener noreferrer"&gt;Bitlet&lt;/a&gt; years before) forwarded me a link to the careers page of the company he was working for at the time, &lt;a href="https://en.wikipedia.org/wiki/ITA_Software" rel="noopener noreferrer"&gt;ITA Software&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;While Google was in the process of acquiring ITA Software, ITA still had a number of open positions they were looking to hire for. Unlike Google, however, they required candidates to &lt;a href="https://web.archive.org/web/20111012115624/http://itasoftware.com/careers/work-at-ita/hiring-puzzles.html" rel="noopener noreferrer"&gt;solve a programming challenge&lt;/a&gt; before applying to engineering roles.&lt;/p&gt;

&lt;p&gt;The problems to solve were surprisingly varied, ranging from purely algorithmic challenges to more broadly scoped problems that still required some deep technical insight. As I browsed through the options, I ended up settling on a problem that intrigued me because I thought it resembled a problem I might one day wanted to solve in the real world and seemed to try to test both the breadth of my knowledge (it required good full stack skills) as well as my understanding of deep technical details.&lt;/p&gt;

&lt;p&gt;I have good memories of the time I spent investigating this problem and coming up with a solution. When I was done, I had learned about a new class of data structures (suffix trees), gained a deeper understanding of Java's internals. A year later, I got a job offer due in part to this puzzle.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ft81w2dd7gdhzwn7332xs.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Ft81w2dd7gdhzwn7332xs.png" alt="Puzzle text" width="800" height="651"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  The Problem Statement
&lt;/h4&gt;

&lt;p&gt;The brief for the challenge was the following:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Instant Search&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Write a Java web application which provides "instant search" over properties listed in the National Register of Historic Places. Rather than waiting for the user to press a submit button, your application will dynamically update search results as input is typed. We provide the file &lt;code&gt;nrhp.xml.gz&lt;/code&gt;, which contains selected information from the register's database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Database&lt;/strong&gt; The key component of your server-side application is an efficient, in-memory data structure for looking up properties (written in pure Java). A good solution may take several minutes to load, but can answer a query in well under 0.1 ms on a modern PC. (Note that a sequential search of all properties is probably too slow!) An input matches a property if it is found at any position within that property's names, address, or city+state. Matches are case-insensitive, and consider only the characters A-Z and 0-9, e.g. the input "mainst" matches "200 S Main St" and "red" matches "Lakeshore Dr." Note that the server's JVM will be configured with 1024M maximum heap space. Please conform to the interfaces specified in &lt;code&gt;nrhp.jar&lt;/code&gt; when creating your database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Servlet&lt;/strong&gt; Your servlet should accept an input string as the request parameter to a GET request. Results should include the information for a pre-configured number of properties (e.g. 10), the total number of matches which exist in the database, and the time taken by your search algorithm. Your servlet should be stateless, ie. not depend on any per-user session information. Paginate your additional results as a bonus!&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Client&lt;/strong&gt; Your web page should access the servlet using JavaScript's XMLHttpRequest object. As the user types, your interface should repeatedly refine the list of search results without refreshing the page. Your GUI does not have to be complicated, but should be polished and look good.&lt;/p&gt;

&lt;p&gt;Please submit a WAR file, configuration instructions, your source code, and any comments on your approach. Your application will be tested with Tomcat on Sun's 64-bit J2SE and a recent version of Firefox.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fm659xwdwi0a598y8hx3y.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fm659xwdwi0a598y8hx3y.png" alt="UI screenshot from puzzle submission" width="411" height="694"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Client
&lt;/h4&gt;

&lt;p&gt;I started building this from the UI down.&lt;br&gt;
The puzzle brief mentioned using &lt;code&gt;XMLHttpRequest&lt;/code&gt;, so I avoided using any client-side libraries (the functionality I was asked to build on the client was, after all, quite simple).&lt;br&gt;
The screenshot included with the puzzle brief included just a text field for the search query and a list of results.&lt;/p&gt;

&lt;p&gt;I wrote a function to listen for key presses, dispatch an asynchronous call to the server and render the response as soon as it came back. By 2011, I had been coding web applications for a while and I was able to implement that functionality in less than an hour of work.&lt;/p&gt;

&lt;h4&gt;
  
  
  Web application and Servlet code
&lt;/h4&gt;

&lt;p&gt;The Servlet layer was also quite simple, since all it had to was handle an incoming XML request and dispatch it to what the brief called a &lt;em&gt;database&lt;/em&gt;. Again, less than an hour of work here.&lt;/p&gt;

&lt;p&gt;At this level, I also wrote code to parse the database of strings to index from an XML file containing data from the National Register of Historic Places. The Tomcat server would run this code when loading my web application and use the resulting data to construct a data structure to use as an index for power the fast search functionality I needed to build. I needed to figure that out next.&lt;/p&gt;

&lt;h4&gt;
  
  
  Finding a suitable data structure
&lt;/h4&gt;

&lt;p&gt;This is, unsurprisingly, the most challenging part of the puzzle and where I focused my efforts the most. As pointed out in the problem description, looping sequentially through the list of landmarks would not work (it would take much longer than the target 0.1ms threshold). I needed to find data structure with good runtime complexity associated with lookup operations.&lt;/p&gt;

&lt;p&gt;I spent some time thinking about how I would implement a data structure allowing the fast lookup times required in this case. The most common fast-lookup option I was familiar with, the &lt;em&gt;hash table&lt;/em&gt;, would not work straight away with this problem because it would expect the search operation to have the full key string.&lt;br&gt;
In this problem, however, I wanted to be able to look up entries in my index even when given an incomplete substring, which would have required me to store all possible substrings as keys in the table.&lt;/p&gt;

&lt;p&gt;After doing some sketching on paper, it seemed reasonable to expect that &lt;a href="https://xlinux.nist.gov/dads/HTML/trie.html" rel="noopener noreferrer"&gt;tries&lt;/a&gt; would work better here.&lt;/p&gt;

&lt;h4&gt;
  
  
  Suffix trees
&lt;/h4&gt;

&lt;p&gt;As I was researching data structures providing fast lookup operations given partial strings, I stumbled upon a number of papers referencing suffix trees, commonly used in computational biology and text processing, offering lookup operations with linear runtime with respect to the length of the string to search &lt;em&gt;for&lt;/em&gt; (as opposed to the length of the string to search &lt;em&gt;within&lt;/em&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F3sspdkyyk8z9poe97x8s.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F3sspdkyyk8z9poe97x8s.png" alt="Suffix Tree for the word cacao" width="386" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Plain suffix trees, however, are designed to find matches of a given candidate string sequence within a &lt;em&gt;single&lt;/em&gt;, longer, string, while this puzzle revolved around a slightly different use case: instead of having a single long string to look up matches in, I needed to be able to find matches in multiple strings. Thankfully, I read some more and found a good number of papers documenting data structures called &lt;a href="https://www.abahgat.com/project/suffix-tree" rel="noopener noreferrer"&gt;&lt;em&gt;generalized&lt;/em&gt; suffix trees&lt;/a&gt; that do exactly that.&lt;/p&gt;

&lt;p&gt;Based on what I had learned so far, I was convinced this type of tree could fit my requirements but I had two likely challenges to overcome:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Suffix trees tend to occupy much more space than the strings they are indexing and, based on the problem statement, "the server's JVM will be configured with 1024M maximum heap space" and that needed to accommodate the Tomcat server, my whole web application and the tree I was looking to build.&lt;/li&gt;
&lt;li&gt;Much of the complexity of working with suffix tree lies in &lt;em&gt;constructing&lt;/em&gt; the trees themselves. While the puzzle brief was explicitly saying my solution could take "several minutes to load", I did not want the reviewer of my solution to have to wait several hours before they could test my submission.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Ukkonen's algorithm for linear runtime tree construction
&lt;/h4&gt;

&lt;p&gt;Thankfully, had I found a popular algorithm for generating Suffix Trees in linear time (linear in the total length of the strings to be indexed), described by Ukkonen in a paper published in 1995 (&lt;a href="https://www.cs.helsinki.fi/u/ukkonen/SuffixT1withFigs.pdf" rel="noopener noreferrer"&gt;On–line construction of suffix trees&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;It took me a couple days of intermittent work (remember: I was working on this during nights and weekends -- I had another day job back then) to get my suffix tree to work as expected.&lt;/p&gt;

&lt;p&gt;Interestingly, some of the challenges with this stage were revolving around a completely unexpected theme: Ukkonen's paper includes the full algorithm written in pseudo-code and good prose detailing the core steps. However, that same pseudo-code is written at such a high level of abstraction that it did take some work to reconduct it to fast and efficient Java code.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F68s4ogwf8xbuu6jky9hx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2F68s4ogwf8xbuu6jky9hx.png" alt="Pseudocode from Ukkonen's paper" width="800" height="462"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Also, the pseudo-code algorithm is written assuming we are working with a single string represented as a character array, so many of the operations outlined there deal with &lt;em&gt;indices&lt;/em&gt; within that large array (e.g. &lt;em&gt;k&lt;/em&gt; and &lt;em&gt;i&lt;/em&gt; in the procedure above).&lt;/p&gt;

&lt;p&gt;In my Java implementation, instead, I wanted to work with &lt;code&gt;String&lt;/code&gt; objects as much as possible. I was driven by a few different reasons:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Java implements &lt;a href="https://en.wikipedia.org/wiki/String_interning" rel="noopener noreferrer"&gt;string interning&lt;/a&gt; by default -- there is no memory benefit in representing substrings by manually manipulating indices within an array of characters representing the containing string: the JVM &lt;em&gt;already does that&lt;/em&gt; transparently for us.&lt;/li&gt;
&lt;li&gt;Working with &lt;code&gt;String&lt;/code&gt; references led to code that was much more legible to me.&lt;/li&gt;
&lt;li&gt;I knew my next step would be to generalize the algorithm to handle building an index on &lt;em&gt;multiple&lt;/em&gt; strings and that was going to be much more difficult if I had to deal with low level specifics about which array of character represented which input string.&lt;/li&gt;
&lt;/ol&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Generalized&lt;/em&gt; Suffix Trees
&lt;/h4&gt;

&lt;p&gt;This last consideration proved to be critical: generalizing the suffix tree I had up to this point to work with multiple input strings was fairly straightforward. All I had to do was to make sure the nodes in my tree could carry some &lt;em&gt;payload&lt;/em&gt; denoting which of the strings in the index would match a given query string. This stage amounted to a couple hours of work, but only because I had good unit tests.&lt;/p&gt;

&lt;p&gt;At this point, things were looking great. I had spent maybe a couple days reading papers about suffix trees and another couple days writing all the code I had so far. I was ready to try out running my application with the input data provided with the puzzle brief: the entire National Register of Historic Places, an XML feed totaling a few hundred megabytes.&lt;/p&gt;

&lt;h4&gt;
  
  
  Trial by fire: &lt;code&gt;OutOfMemoryError&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;The first run of my application was disappointing. I started up Tomcat and deployed my web application archive, which triggered parsing the XML database provided as input and started to build the generalized suffix tree to use as an index for fast search. Not even two minutes into the suffix tree construction, the server crashed with an &lt;code&gt;OutOfMemoryError&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The 1024 megabytes I had were not enough.&lt;/p&gt;

&lt;p&gt;Thankfully, a couple years earlier I had worked with a client that had a difficult time keeping their e-commerce site up during peak holiday shopping season. Their servers kept crashing because they were running out of memory. That in turn led me to learn how to read and make sense of JVM memory dumps.&lt;/p&gt;

&lt;p&gt;I never thought I would make use of that skill for my own personal projects but this puzzle proved me wrong. I fired up &lt;a href="https://visualvm.github.io" rel="noopener noreferrer"&gt;visualvm&lt;/a&gt; and started looking for the largest contributors to memory consumption.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fn0x7hq7nof8pph84l775.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fi%2Fn0x7hq7nof8pph84l775.png" alt="A screenshot of the VisualVM UI" width="600" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It did not take long to find that there were a few memory allocation patterns that were not efficient. Many of these items would hardly be an issue for an average application, but they all ended up making a difference in this case because of the sheer size of the tree data structure being constructed.&lt;/p&gt;

&lt;h4&gt;
  
  
  Memory micro-optimizations
&lt;/h4&gt;

&lt;p&gt;Analyzing a few heap dumps suggested me a series of possible changes that would lead to savings in memory, usually at the cost of additional complexity or switching from a general purpose data structure implementation (e.g. maps) to special purpose equivalent tailored to this use case and its constraints.&lt;/p&gt;

&lt;p&gt;I ranked possible optimizations by their expected return on investment (i.e. comparing value of the memory savings to the additional implementation complexity, slower runtime and other factors) and implemented a few items at the top of the list.&lt;/p&gt;

&lt;p&gt;The most impactful changes involved optimizing the memory footprint of the suffix tree &lt;em&gt;nodes&lt;/em&gt;: considering my application required constructing a very large graph (featuring tens of thousands of nodes), any marginal savings coming from a more efficient node representation would end up making a meaningful difference.&lt;/p&gt;

&lt;p&gt;A property of suffix tree nodes is that no outgoing edges can be labeled with strings sharing a prefix. In practice, this means that the data structure implementing a node must hold a reference to a set of outgoing edges keyed by the first character on the label.&lt;/p&gt;

&lt;p&gt;The first version of my solution was using a &lt;code&gt;HashMap&amp;lt;Character,Edge&amp;gt;&lt;/code&gt; to represent this. As soon as I looked at the heap dump, I noticed this representation was extremely inefficient for my use case.&lt;/p&gt;

&lt;p&gt;Hash Maps in Java are initialized with a &lt;a href="https://en.wikipedia.org/wiki/Hash_table#Key_statistics" rel="noopener noreferrer"&gt;load factor&lt;/a&gt; of 0.75 (meaning they generally reserve memory for at least 25% more key/value pairs than they hold at any given point) and, more importantly, with enough initial capacity to hold 16 elements.&lt;/p&gt;

&lt;p&gt;The latter item was a particularly poor fit for my use case: since I was indexing strings using the English alphabet (26 distinct characters) a map of size 16 would be large enough to accommodate more than half the possible characters and would often be wasteful.&lt;/p&gt;

&lt;p&gt;I could have mitigated this problem by tuning the sizing and load factor parameters but I thought I could save even more memory by switching to a specialized collection type.&lt;br&gt;
The default map implementations included in the standard library require the key and value types to be reference types rather than native types (i.e. the map is keyed by &lt;code&gt;Character&lt;/code&gt; instead of &lt;code&gt;char&lt;/code&gt;) and reference types tend to be much less memory efficient (since their representation is more complex).&lt;/p&gt;

&lt;p&gt;I wrote a special-purpose map implementation, called &lt;code&gt;EdgeBag&lt;/code&gt;, which featured a few tweaks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;stored keys and values and two parallel arrays,&lt;/li&gt;
&lt;li&gt;the arrays would start small gradually grew if more space if necessary,&lt;/li&gt;
&lt;li&gt;relied on a linear scan for lookup operation if the bag contained a small number of elements and switched to using binary search on a sorted key set if the bag had grown to contain more than a few units,&lt;/li&gt;
&lt;li&gt;used &lt;code&gt;byte[]&lt;/code&gt; (instead of &lt;code&gt;char[]&lt;/code&gt;) to represent the characters in the keys. Java's 16-bit &lt;code&gt;char&lt;/code&gt; type takes twice as much space as a &lt;code&gt;byte&lt;/code&gt;. I knew all my keys were ASCII characters, so I could forgo Unicode support here and could squeeze some more savings by casting to a more narrow value range.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some more specific details on this and other changes to reduce the memory footprint of my suffix tree implementation are in the &lt;a href="https://www.abahgat.com/project/suffix-tree#problem-specific-optimizations" rel="noopener noreferrer"&gt;Problem-specific optimizations&lt;/a&gt; section of the Suffix Tree project page.&lt;/p&gt;

&lt;h4&gt;
  
  
  Conclusion
&lt;/h4&gt;

&lt;p&gt;When I tested out my program after the memory optimizations, I was delighted to see it met the problem requirements: lookups were lightning fast, well under 0.1ms using the machine I had back then (based on an Intel Q6600 2.4GHz CPU) and the unit tests I had written gave me good confidence that the program behaved as required.&lt;/p&gt;

&lt;p&gt;I packaged up the solution as a WAR archive, wrote a brief README file outlining design considerations and instructions on how to run it (just deploy on a bare Tomcat 6 server) and sent it over email. Almost a year later, I was packing my bags and moving to Amsterdam to join Google (which had by then acquired ITA Software).&lt;/p&gt;

&lt;p&gt;I owe it in no small part to the fun I had with this coding puzzle.&lt;/p&gt;

&lt;p&gt;When I think of how much I enjoyed the time I spent building Instant Search, I think it must be because it required both breadth (to design a full stack application, albeit a simple one) and depth (to research the best data structure for the job and follow up with optimizations as required). It allowed me to combine my background as a generalist with my interest with the theoretical foundations of Computer Science.&lt;/p&gt;

&lt;p&gt;The careful choice of specifying both memory and runtime constraints as part of the problem requirements made the challenge much more fun. When the first version I coded did not work, I was able to reuse my experience with memory profiling tools to identify which optimizations to follow up with. At the same time, I built a stronger understanding of Java's internals and learned a lot more about implementation details I had, until then, just given for granted.&lt;/p&gt;

&lt;p&gt;When ITA retired Instant Search (and other programming puzzles&lt;sup id="fnref1"&gt;1&lt;/sup&gt;), I decided to &lt;a href="https://www.abahgat.com/project/suffix-tree" rel="noopener noreferrer"&gt;release the Java Generalized Suffix Tree as open source&lt;/a&gt; for others to use. Despite the many problem-specific optimizations I ended up making, it is generic enough that has been used in a few other applications since I built it, which gives me one more thing to be thankful for.&lt;/p&gt;

&lt;p&gt;I write about programming, software engineering and technical leadership. You can &lt;a href="https://www.twitter.com/abahgat" rel="noopener noreferrer"&gt;follow me on twitter&lt;/a&gt; for more posts like this.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This post was originally published on &lt;a href="https://www.abahgat.com/blog/the-programming-puzzle-that-got-me-my-job/" rel="noopener noreferrer"&gt;abahgat.com&lt;/a&gt; on Sep 30, 2019&lt;/em&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;While the original page is no longer online, the Wayback Machine still has a snapshot of the original page with the original selection of &lt;a href="https://web.archive.org/web/20111012115624/http://itasoftware.com/careers/work-at-ita/hiring-puzzles.html" rel="noopener noreferrer"&gt;past programming puzzles&lt;/a&gt;. They are still a great way to test your programming skills. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>java</category>
      <category>programming</category>
      <category>career</category>
    </item>
    <item>
      <title>Visual and HTML Testing for Static Sites</title>
      <dc:creator>Alessandro Bahgat</dc:creator>
      <pubDate>Tue, 01 Oct 2019 18:18:53 +0000</pubDate>
      <link>https://dev.to/abahgat/visual-and-html-testing-for-static-sites-343d</link>
      <guid>https://dev.to/abahgat/visual-and-html-testing-for-static-sites-343d</guid>
      <description>&lt;p&gt;Over a year ago I switched from having my site hosted on a CMS to having it &lt;a href="https://www.abahgat.com/blog/migrating-from-wordpress-to-hugo/" rel="noopener noreferrer"&gt;built statically&lt;/a&gt; and served as a collection of static pages. I have been extremely happy with the end result for all these months -- the site is very easy to update and effortless to maintain -- but I just made a few changes that made my experience even better.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why test Static Sites
&lt;/h2&gt;

&lt;p&gt;Even for sites as simple as this, it is surprisingly easy to make breaking changes without realizing. Over the time I have been maintaining &lt;a href="https://www.abahgat.com/" rel="noopener noreferrer"&gt;abahgat.com&lt;/a&gt;, I ended up accidentally introduction bugs more than a few times. Here a few examples of things I ran into:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;broken links&lt;/em&gt; -- by default, Hugo does not validate any of the links in the content I am editing, which means that I have to be careful and make sure all URLs and paths are valid&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;incorrect theme configuration&lt;/em&gt; -- the more complex the theme I am using is, the more configuration options it will offer. The more options I have to configure, the more likely I am to make mistakes.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;bugs in theme customizations&lt;/em&gt; -- Hugo is great at allowing to override and customize theme templates. However, this is another source of potential issues.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;bugs in the theme code itself&lt;/em&gt; -- No software is perfect, and any theme I might be using can have its own bugs and edge cases. This might be especially true for you if you are actively developing your own theme or you frequently update it to the most recent version available.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most of the issues above still affected me when I was hosting my site on Wordpress (I did break links and styling every now and then) but one advantage of working with a statically generated site is that we can leverage many of the tools that are available to web developers to catch issues early (and potentially block deploys if any issues are detected). So I set out to find what kind of options I had to improve my workflow so that I could make changes with more confidence that I wouldn't accidentally break my site.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can be tested
&lt;/h2&gt;

&lt;p&gt;Based on the list above, I knew I was looking to set up tests to detect, in order of priority, problems such as:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;broken internal links&lt;/li&gt;
&lt;li&gt;invalid or malformed HTML&lt;/li&gt;
&lt;li&gt;issues with layout or presentation&lt;/li&gt;
&lt;li&gt;invalid RSS feed entries&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Thankfully, I was able to find a way to cover most of these.&lt;/p&gt;

&lt;h3&gt;
  
  
  Testing HTML with &lt;code&gt;html-proofer&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Covering the first items on the list has been fairly straightforward with &lt;a href="https://github.com/gjtorikian/html-proofer" rel="noopener noreferrer"&gt;&lt;code&gt;html-proofer&lt;/code&gt;&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Provided you have Ruby installed, you can get &lt;code&gt;html-proofer&lt;/code&gt; as a gem via the command below&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gem &lt;span class="nb"&gt;install &lt;/span&gt;html-proofer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and then run it via&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;htmlproofer &lt;span class="nt"&gt;--extension&lt;/span&gt; .html ./public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will scan the &lt;code&gt;./public&lt;/code&gt; directory for any files with &lt;code&gt;html&lt;/code&gt; extension and output a report listing any issues with the markup in those files.&lt;/p&gt;

&lt;p&gt;When I first ran it on my site, I got a pretty good list of actionable warnings. The messages are fairly specific and easy to understand, as you can tell by looking at the snippet below:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- ./public/author/abahgat/index.html
  *  356:11: ERROR: Opening and ending tag mismatch: section and div (line 356)
- ./public/author/index.html
  *  356:11: ERROR: Opening and ending tag mismatch: section and div (line 356)
- ./public/blog/index.html
  *  829:2157: ERROR: Unexpected end tag : p (line 829)
- ./public/blog/maps-for-public-transport-users/index.html
  *  internally linking to uploads/2009/01/p-480-320-0e6ac38d-252e-47fa-be79-0ae974dad8d2.jpeg, which does not exist (line 476)
     &amp;lt;a href="uploads/2009/01/p-480-320-0e6ac38d-252e-47fa-be79-0ae974dad8d2.jpeg"&amp;gt;&amp;lt;img class="size-full wp-image-364 aligncenter" src="/img/wp-uploads/2009/01/p-480-320-0e6ac38d-252e-47fa-be79-0ae974dad8d2.jpeg" alt="" width="200" height="300"&amp;gt;&amp;lt;/a&amp;gt;
- ./public/blog/page/2/index.html
  *  linking to internal hash #broken-priorites that does not exist (line 1456)
     &amp;lt;a href="#broken-priorites"&amp;gt;The way priorities are managed is broken&amp;lt;/a&amp;gt;
  *  linking to internal hash #duplicates that does not exist (line 1453)
     &amp;lt;a href="#duplicates"&amp;gt;Lots of issues are duplicates&amp;lt;/a&amp;gt;
  *  linking to internal hash #missing-info that does not exist (line 1455)
     &amp;lt;a href="#missing-info"&amp;gt;Bug reports do not include enough information&amp;lt;/a&amp;gt;
  *  linking to internal hash #processes that does not exist (line 1454)
     &amp;lt;a href="#processes"&amp;gt;The system imposes over-engineered processes&amp;lt;/a&amp;gt;
  *  linking to internal hash #tracker-misuse that does not exist (line 1452)
     &amp;lt;a href="#tracker-misuse"&amp;gt;The issue tracking system is misused&amp;lt;/a&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with default settings, &lt;code&gt;html-proofer&lt;/code&gt; is able to catch most of the issues I was interested in detecting: the list above features a good mix of problems caused by invalid links in my Markdown sources, errors due to how I was misusing my template and bugs in the template I was using.&lt;/p&gt;

&lt;p&gt;Fixing the issues required a combination of updating a few broken links, cleaning up the Markdown sources for my site, submitting a few bugs and Pull Requests against the theme I am using.&lt;/p&gt;

&lt;p&gt;Overall, all the issues flagged made sense and worth fixing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Visual Testing with Percy
&lt;/h3&gt;

&lt;p&gt;As useful as &lt;code&gt;html-proofer&lt;/code&gt; is, it does not help catching layout and presentational issues that are not due to invalid markup. I have had good experiences with visual testing and review at work and I was interested in using screenshots to detect layout issues and catch any unintended presentational changes on my own site too.&lt;/p&gt;

&lt;p&gt;I cared about this because upgrading my Hugo theme sometimes involves non-trivial changes that could go wrong (despite George, the author, keeping &lt;a href="https://sourcethemes.com/academic/updates/v4.5.0/" rel="noopener noreferrer"&gt;really good change logs&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Also, I wanted to make customizations to the theme and having testing in place is the only way I know to make sure I don't inadvertently break anything (since I will not review every single page manually every time I make layout changes, having a way to be warned about any differences is very valuable).&lt;/p&gt;

&lt;p&gt;I ended up settling on &lt;a href="https://www.percy.io" rel="noopener noreferrer"&gt;Percy&lt;/a&gt;, a tool that was clearly designed first and foremost for testing dynamic web applications but also offered an option to &lt;a href="https://docs.percy.io/docs/static-sites" rel="noopener noreferrer"&gt;test static sites&lt;/a&gt; via a command line program.&lt;/p&gt;

&lt;p&gt;The main idea behind a snapshot testing system is to keep a set of approved snapshots ("goldens"), capture a new set of snapshots upon change and flag any differences for review. Changes can be either intended (in which case the screenshot is approved and becomes the new golden) or accidental (in which case they are flagged as regressions and expected to be fixed before pushing a new version).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewvv3ptvvc4lv580qp2l.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fewvv3ptvvc4lv580qp2l.png" alt="Example screenshot highlighting differences introduced by a specific commit." width="800" height="265"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Percy offers a nice interface to highlight any difference between snapshots and can be easily integrated with GitHub and other source control systems to make approving any updated snapshots part of the code review process.&lt;/p&gt;

&lt;p&gt;Percy runs as a service, so you will need to create an account with them before being able to use it. Once you have done that you can try it by following the instructions on &lt;a href="https://docs.percy.io/docs/command-line-client" rel="noopener noreferrer"&gt;their documentation page&lt;/a&gt; and running the following command on your site (where &lt;code&gt;./public&lt;/code&gt; is a directory containing your static pages):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx percy snapshot ./public
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running tests on every change via CI services
&lt;/h2&gt;

&lt;p&gt;Unlike the HTML tests, which test a specific version of your site in isolation, the value of snapshot testing lies in comparing your site against a previously approved set of snapshots, which need to be kept up to date.&lt;/p&gt;

&lt;p&gt;I then configured a simple workflow with &lt;a href="https://circleci.com/" rel="noopener noreferrer"&gt;CircleCI&lt;/a&gt;, having it build my site with Hugo, run &lt;code&gt;html-proofer&lt;/code&gt; on the generated sources, grab a fresh set of screenshots on every change and flag any differences for review.&lt;/p&gt;

&lt;p&gt;From what I could tell, many other CI services can be configured to do the same; I ended up choosing CircleCI because I thought its Docker-based setup worked better for what I was trying to do and I had little trouble finding Docker images suitable for running the steps in my workflow.&lt;/p&gt;

&lt;p&gt;Below the resulting configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2.1&lt;/span&gt;

&lt;span class="na"&gt;orbs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;hugo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;circleci/hugo@0.3&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;snapshot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;docker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;buildkite/puppeteer:v1.15.0&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;attach_workspace&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;at&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install percy&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PERCY_TOKEN=$PERCY_TOKEN npx percy snapshot ./public&lt;/span&gt;

&lt;span class="na"&gt;workflows&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;hugo/build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.55.6"&lt;/span&gt;
          &lt;span class="na"&gt;html-proofer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;snapshot&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;requires&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
            &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;hugo/build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first section sets up build with Hugo via an &lt;em&gt;Orb&lt;/em&gt; (&lt;a href="https://circleci.com/orbs/" rel="noopener noreferrer"&gt;Orbs&lt;/a&gt; are CircleCI's packages of functionality that can be packaged and reused) that also runs &lt;code&gt;html-proofer&lt;/code&gt; tests on the resulting build.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;snapshot&lt;/code&gt; task installs &lt;code&gt;percy&lt;/code&gt; via npm and then invokes it on the directory containing the sources generated in the previous step. It runs on the &lt;a href="https://hub.docker.com/r/buildkite/puppeteer" rel="noopener noreferrer"&gt;Docker Puppeteer&lt;/a&gt; image, which comes with most of Percy's package dependencies already installed.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Note:&lt;/em&gt;&lt;br&gt;
There seems to be a &lt;a href="https://hub.docker.com/r/percyio/agent" rel="noopener noreferrer"&gt;Docker image maintained by Percy&lt;/a&gt; but I could not get it to work. I suspect it is because it ships with an old version of the &lt;code&gt;percy&lt;/code&gt; command, I did not investigate this further.&lt;/p&gt;

&lt;p&gt;With this configuration, every commit and Pull Request will trigger a Hugo build, run your site through &lt;code&gt;html-proofer&lt;/code&gt; and capture a new set of snapshots. If any visual differences are detected, they can be inspected and approved via Percy's web interface.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffm0gad9yafv0ajkztkw8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffm0gad9yafv0ajkztkw8.png" alt="GitHub will show the latest status of your tests on every commit and Pull Request." width="800" height="414"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note that there is no &lt;em&gt;deploy&lt;/em&gt; workflow since I configured Netlify to automatically publish a new version of my site whenever I push to the &lt;em&gt;master&lt;/em&gt; branch.&lt;/p&gt;
&lt;h2&gt;
  
  
  Tweaking the setup
&lt;/h2&gt;

&lt;p&gt;If you got to this point, your configuration will feature sensible defaults and help you capture a number of issues caused by your own mistakes or any issues introduced by the theme upstream.&lt;/p&gt;

&lt;p&gt;There are a few opportunities to make the setup more efficient, but they require making changes with the CircleCI configuration above since the Orb we used before does not expose a good way to pass flags to tweak neither the build nor test test. (This &lt;a href="https://github.com/CircleCI-Public/hugo-orb/issues?q=author%3Aabahgat" rel="noopener noreferrer"&gt;might be fixed&lt;/a&gt; by the time you read this).&lt;/p&gt;

&lt;p&gt;You can &lt;a href="https://gist.github.com/abahgat/e7fc5b3023692610c4760fedcd8e3b43" rel="noopener noreferrer"&gt;click here&lt;/a&gt; to see a CircleCI configuration file that you can further customize based on the sections below.&lt;/p&gt;

&lt;p&gt;Here some of the tweaks you might consider implementing.&lt;/p&gt;
&lt;h3&gt;
  
  
  Test pages with a future publish date and drafts
&lt;/h3&gt;

&lt;p&gt;Hugo allows you mark pages as drafts or to set a publish date to a future time (for scheduled content). Neither of these pages will be built by default in your deploy workflow, but you might want to do that when running your tests so that you ensure that content passes validation even as it is being edited (as opposed to being surprised by unexpected errors just when you thought you were ready to publish).&lt;/p&gt;

&lt;p&gt;You can do this by passing the &lt;code&gt;-D&lt;/code&gt; and &lt;code&gt;-F&lt;/code&gt; flags to the &lt;code&gt;hugo&lt;/code&gt; command during the build step.&lt;/p&gt;
&lt;h3&gt;
  
  
  Consider enabling minification
&lt;/h3&gt;

&lt;p&gt;If you are building your site with minification enabled when you are deploying, you might have to make a decision:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;if you enable minification only on the deploy workflow (and leave it disabled for development), the version of the site you will be testing will not be identical to the version you are publishing. This might hide subtle bugs that you would not be able to track down easily (such as &lt;a href="https://github.com/gcushen/hugo-academic/issues/1219" rel="noopener noreferrer"&gt;this one&lt;/a&gt;).&lt;/li&gt;
&lt;li&gt;on the contrary, if you do enable minification, debugging issues flagged by &lt;code&gt;html-proofer&lt;/code&gt; and &lt;code&gt;percy&lt;/code&gt; might be slightly more difficult, since the resulting source code will be more difficult to read.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not have a firm recommendation here, I am currently working with the latter setup and it has been working fine so far but isolating the cause of an issue is slightly harder this way.&lt;/p&gt;

&lt;p&gt;If you want try this, you need to pass &lt;code&gt;--minify&lt;/code&gt; to the &lt;code&gt;hugo&lt;/code&gt; command during the build step.&lt;/p&gt;
&lt;h3&gt;
  
  
  Skip redundant screenshots
&lt;/h3&gt;

&lt;p&gt;Just like, when writing unit tests, we don't want to have multiple redundant tests that cover the same behavior, in most cases it is not necessary to take screenshots of pages that use the same template and have very similar content.&lt;/p&gt;

&lt;p&gt;For example, if part of your site is a blog that features tags and categories (in Hugo, this would apply to any &lt;a href="https://gohugo.io/content-management/taxonomies/" rel="noopener noreferrer"&gt;&lt;em&gt;taxonomy&lt;/em&gt;&lt;/a&gt;), you will not need to take screenshot of every individual tag page as you won't get much value out of them, since they all look the same. They will rather be a burden to maintain (should your theme ever change, you'd have many more -- very similar -- screenshots to approve).&lt;/p&gt;

&lt;p&gt;You can probably make a similar case for directory pages (say, if you have 40 pages of articles, the screenshots for the second to thirty-ninth pages are likely going to be the same.&lt;br&gt;
There could be value in testing the first and last page separately since you'd imagine they would have a different configuration for the next/previous navigation elements, but that is up to you.&lt;/p&gt;

&lt;p&gt;Thankfully, the &lt;code&gt;percy&lt;/code&gt; command offers a way to manually exclude certain paths from being considered when grabbing screenshots. The syntax for that argument expects &lt;a href="https://en.wikipedia.org/wiki/Glob_(programming)" rel="noopener noreferrer"&gt;globs&lt;/a&gt;, which can take some trial and error to get right.&lt;/p&gt;

&lt;p&gt;In case it helps, here a configuration that worked reasonably well for me so far:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx percy snapshot ./public &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'categories/!(coding|coding/**)/*.html'&lt;/span&gt;,&lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'tags/!(amsterdam|amsterdam/**)/*.html'&lt;/span&gt;,&lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s1"&gt;'blog/page/!(1|2)/*.html'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What the above is doing is excluding all categories but one (&lt;a href="https://www.abahgat.com/categories/coding" rel="noopener noreferrer"&gt;Coding&lt;/a&gt;) and all tags excluding one (&lt;a href="https://www.abahgat.com/tags/amsterdam" rel="noopener noreferrer"&gt;Amsterdam&lt;/a&gt;). It is also ignoring any page beyond the second in the &lt;code&gt;/blog&lt;/code&gt; directory.&lt;/p&gt;

&lt;h3&gt;
  
  
  Capture screenshots less frequently
&lt;/h3&gt;

&lt;p&gt;I have yet to run into this limitation but I could see how, if your site is very large and/or if you commit very frequently, you may be concerned about exceeding Percy's free quota (5000 screenshots/month).&lt;/p&gt;

&lt;p&gt;I have not had to handle this in any special way so far, but here a few options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Percy grabs screenshots of each page on your site in both Chrome and Firefox to ensure your site behaves well across browsers. You may decide you are comfortable with taking the risk of having smaller issues undetected and grab screenshots only on one of the two. This will mean you will consume half as many snapshots every time you run visual tests.&lt;/li&gt;
&lt;li&gt;Percy will also test your site on a couple different viewport sizes. This is helpful to ensure your site works well on desktop and mobile devices. Again, you may be comfortable with just running tests on one configuration in order to reduce resource consumption by half.&lt;/li&gt;
&lt;li&gt;You may configure your CircleCI workflow to &lt;a href="https://circleci.com/docs/2.0/workflows/#holding-a-workflow-for-a-manual-approval" rel="noopener noreferrer"&gt;toggle the snapshot step manually&lt;/a&gt; and run it only when you have meaningful changes to test (e.g. if you are adding new content or upgrading your theme). If you do this, you still want to make sure you refresh your screenshots based on &lt;em&gt;master&lt;/em&gt; fairly often, otherwise you might find yourself with visual diffs that cover so many changes together that are no longer informative. And if you run this very infrequently, you might as well just choose to run the &lt;code&gt;percy&lt;/code&gt; command locally.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Realistically, for most personal sites, you can likely go a long way with the free quota. If you are considering this for a large corporate site, I would rather consider paying for a higher tier and get more snapshots rather than trying too hard to capture fewer and have a less informative workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests are even more valuable if you are a theme developer
&lt;/h2&gt;

&lt;p&gt;If you are developing a theme that others are going to use, testing this way is likely to be even more impactful: you can save yourself quite a bit of time by having a way to catch issues before you ship a new version instead of relying on your users to report problems they run into after they upgrade.&lt;/p&gt;

&lt;p&gt;You can apply most of the suggestions above by making sure that you have an example site (the &lt;a href="https://sourcethemes.com/academic/" rel="noopener noreferrer"&gt;Academic&lt;/a&gt; theme I use is great for this) that exercises most of the features in your theme, &lt;em&gt;especially the ones that are not enabled by default&lt;/em&gt;. This would also likely reduce the time you spend manually inspecting your pages to make sure they still render as expected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;This has been a great opportunity to learn about great tools that are available out there (I will definitely consider Percy for the next app I will build in my own time) and how they can help greatly even with sites that are statically generated.&lt;/p&gt;

&lt;p&gt;I have accomplished most of the goals I had in mind when I started playing with this. There is one item left open for future investigation (mainly, a way to ensure the RSS for my site is valid and well-formed) but the CircleCI workflow I set up gave me a good foundation I can extend to cover more tests.&lt;/p&gt;




&lt;p&gt;This post was &lt;a href="https://www.abahgat.com/blog/testing-static-sites/" rel="noopener noreferrer"&gt;originally posted&lt;/a&gt; on my personal website, &lt;a href="https://www.abahgat.com/" rel="noopener noreferrer"&gt;abahgat.com&lt;/a&gt;, where I write about software, design and human factors.&lt;/p&gt;

&lt;p&gt;If you enjoyed this post, you can be notified of more by signing up for this &lt;a href="https://buttondown.email/abahgat" rel="noopener noreferrer"&gt;newsletter&lt;/a&gt; or &lt;a href="https://twitter.com/abahgat" rel="noopener noreferrer"&gt;following me on twitter&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>testing</category>
      <category>staticgen</category>
      <category>jamstack</category>
    </item>
  </channel>
</rss>
