<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet href="/feeds/atom-style.xsl" type="text/xsl"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://srikanth.sastry.name/</id>
    <title>Srikanth Sastry</title>
    <updated>2026-08-03T18:52:40.026Z</updated>
    <generator>Astro-Theme-Retypeset with Feed for Node.js</generator>
    <author>
        <name>Srikanth Sastry</name>
        <uri>https://srikanth.sastry.name/</uri>
    </author>
    <link rel="alternate" href="https://srikanth.sastry.name/"/>
    <link rel="self" href="https://srikanth.sastry.name/atom.xml"/>
    <subtitle>Personal blog — technology, programming, governance, and life.</subtitle>
    <rights>Copyright © 2026 Srikanth Sastry</rights>
    <entry>
        <title type="html"><![CDATA[The Hidden Directive Gap]]></title>
        <id>https://srikanth.sastry.name/the-hidden-directive-gap/</id>
        <link href="https://srikanth.sastry.name/the-hidden-directive-gap/"/>
        <updated>2026-07-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[AI coding agents appear to unlock the promise of converting natural language to code; that's the whole value proposition of "vibe coding". B...]]></summary>
        <content type="html"><![CDATA[<p>AI coding agents appear to unlock the promise of converting natural language to code; that's the whole value proposition of "vibe coding". But if this should be taken with any seriousness, we need sufficient rigor behind it. The so-called Spec Driven Development (SDD) is one attempt at that rigor, and it holds promise. However, it does have its blind spots. I talked about <a href="/the-architecture-orphaning-problem-with-ai-agents/"><em>Architecture Orphaning</em></a> as one such blind spot previously. Here, I introduce you to another one: the <a href="/garden/directive-gap-hidden-by-confabulation/">hidden directive gap</a>. Just like with <a href="/garden/architecture-orphaning/">architecture orphaning</a>, there are ways to augment SDD to mitigate it.</p>
<h2>Directive gap</h2>
<p><a href="/garden/directive-gap/">Directive Gap</a> is the inherent ambiguity that separates human intention from the system behavior that implements that intention. This is not a new problem. It has been studied for a long time. The holy grail is a process/tool that can capture human intent as specifications and convert it into software without losing any fidelity. There have been many attempts at it: <a href="https://www.eiffel.com/values/design-by-contract/">Design by Contract</a> (Meyer/Eiffel), <a href="https://en.wikipedia.org/wiki/Z_notation">Z notation</a>, <a href="https://en.wikipedia.org/wiki/Vienna_Development_Method">VDM</a> (Vienna Development Method), <a href="https://en.wikipedia.org/wiki/Computer-aided_software_engineering">CASE tools</a>, <a href="https://en.wikipedia.org/wiki/Model-driven_engineering">Model-Driven Development</a>, <em>etc.</em> The paradigm they all followed was to eliminate ambiguity from specifications so that these specifications can be converted into software with precision and determinism. They all failed for the same reason. Eliminating ambiguity made the specification language so complex that the spec itself becomes a programming language. So now you have to pay the cost twice: once developing a complex and detailed spec, and again in maintaining and executing the program.</p>
<p>AI coding agents appear to side step this issue by resolving a lot of ambiguity 'correctly' enough to make natural language a 'good enough' simulacrum of a formal specification. SDD takes the <a href="/garden/suggestible-actor-properties/">suggestible actor</a> — the <a href="/garden/ambient-to-local/">locally-reasoning</a> agent I introduced in <a href="/the-suggestible-actor/">The Suggestible Actor</a> — as a node in an iterative loop of starting with the spec, generating code, running tests, refining the spec to eliminate failures, and rinse and repeat until failures disappear.</p>
<p>Now you don't have to pay twice. You just iterate on the spec. Conversion to code is cheap. This makes SDD a directionally right attempt at converting intention to behavior. It shifts the responsibility of the software engineer from "write code" to "define what 'correct' means". But there is a problem. As one executes the SDD process, how do you know that you are done; how many iterations are enough; when can we say that the directive gap is closed?</p>
<h2>The termination problem</h2>
<p>Let's look at the information we have available to decide if we have iterated enough in SDD. We have the spec, the code, and the tests. The AI agent/LLM model is a constant. A natural stopping point is when the code passes all the tests. After all, the tests are the signal we use to determine how to refine the spec. If the tests pass, then we have no signal to refine the spec. So we should be done. Sure, there might be bugs, but that's true of even human authored code. At worst, code quality now has parity with human authored code, and all the humans have to do is maintain the specs that are easier to understand and manipulate. If only!</p>
<p>Notice the assumption smuggled into the previous paragraph: the LLM model is a constant. While that might be true during an SDD session across all iterations, it is not true in general. Every few months some AI lab releases a new model, and it promises to be better than all other models that came before it. What happens when you switch the model? Say, that your spec for a job scheduler was iterated meticulously through SDD on Claude Opus 5, and your employer decided to switch over to Codex 5.6. Would the spec produce the same code with Codex 5.6? In fact, the chances are that the code produced by Codex 5.6 will actually fail at least one test whereas they all passed with the same spec on Claude Opus 5.</p>
<p>This is not speculation. Wasowski ran precisely such an experiment and talked about it in his post <a href="https://medium.com/@jaroslaw-wasowski">"Stop Writing Specs. Start Writing Facts. The Entire SDD Movement Is Already Obsolete."</a> His spec needed constant 'refinement' as he upgraded across Claude Sonnet 3.5, 3.7, 4, and Opus 4.5+. The spec that worked in the previous model produced incorrect code in the new model. The tests, on the other hand, stayed constant.</p>
<p>Folks from AWS discovered a mathematical limitation of closing the directive gap. In their <a href="https://arxiv.org/abs/2605.17246">"Fidelity Probes" paper</a>, Erata et al. found that if a single iteration has a non-zero base rate of generating code that doesn't completely align with the spec, then there is a non-zero lower bound on the 'error/gap' between the spec and code across multiple iterations.</p>
<p>I ask again: how do you know you are done?</p>
<h2>The hidden gap</h2>
<p>The spec is unstable relative to different AI models. But (spec, model) combination is stable. That is the key observation of focus on. Why is that the case; why is (spec, model) stable, but spec alone is not? The answer comes back to the directive gap. When iterating on a spec with a given model, the fixed point that you reach is NOT a signal that the directive gap is closed; instead, it is really a signal that the <em>model has hidden the directive gap through <a href="/garden/confabulation-is-plausible/">confabulation</a></em>.</p>
<p>In some ways, the problem is worse than before. The spec-based development before AI agents exposed the directive gap no matter how nuanced, so that you knew where your spec was deficient. AI agents now hide any directive gap below a threshold and are only revealed when the model changes. This lulls you into a false sense of security with a rude awakening on the other side of a model upgrade.</p>
<p>How do you address this hidden gap? If you try to make the spec detailed enough to be model independent then (a) you have no idea if you have actually succeeded, and (b) spec becomes as detailed as the programming language itself, and is effectively self-defeating. Instead, I suggest a reframed approach: <em>what if, instead of treating spec as the primary artifact, we treated both spec and code as <a href="/garden/co-artifact-model/">co-artifacts</a></em>, in a <a href="/garden/two-layer-governance-model/">two-layer governance model</a>, <em>and we measured mutual fidelity using the AI agent?</em></p>
<h2>The "reverse pass": AI agent as an observer</h2>
<p>In this reframed approach, you start the same as any SDD iteration. You continually refine the spec until you have reached a fixed point, where all the tests pass and the code generated by AI matches the spec's intent. Let's call this the "forward pass", where you go from the spec to code. Instead of stopping here, you turn the AI agent's gaze backward, and ask it the question: "does the implementation/code conform to the spec, and if not, what are the deviations?".  This is the "reverse pass"; an addendum to SDD. In this step, you are now using the AI agent as an observer and not as an implementer.</p>
<p>Now, the first time you are doing this, if you have followed the SDD process correctly, the answer to this question from the AI agent is "yes, the implementation conforms to the spec, and there are no deviations." However, this is not going to be the case in the future, with model upgrades. For now, let's start with the pair <code>(spec, code)</code> as co-artifacts. We expect this to be stable for a given model. All changes/bug fixes are going to be through spec changes or addendums, but with the same model. Since the model is constant, the "reverse pass" should still continue to say that the implementation conforms to the spec.</p>
<h2>Model upgrades: reconciliation via the "reverse pass"</h2>
<p>When you perform the inevitable model upgrade. The <code>(spec, code)</code> co-artifacts are no longer stable. Before you start any new development or change the spec, the first thing you do is perform a "reverse pass" with the new model. In effect, you ask the new model/agent "does the implementation/code conform to the spec, and if not, what are the deviations?", and this time, the agent will respond back with a set of deviations of the code from the spec; the new model will interpret the spec differently from the old model. This is part of your <em>hidden directive gap made visible</em>.</p>
<p>Here is often where SDD breaks down with complaints of unstable specs. With the reverse pass result, your co-artifacts evolve from <code>(spec, code)</code> to <code>(spec, code, deviations, model)</code>. This is the true artifact of the modified SDD process. In fact, it was always <code>(spec, code, deviations, model)</code>, but the first time around, the model was implied, and the deviations were null by construction.
At this point, you have some options.</p>
<ol>
<li>Since the code is "correct", you can simply record those deviations as part of the SDD artifact and make those observed deviation part of the spec itself, and so <code>spec + deviations</code> becomes the new spec.</li>
<li>Alternatively, you can review those deviations, and see if they reveal true gaps in the implementation; if so, you now refine your spec to account for the deviations and run another set of SDD iterations until the "reverse pass" produces no more deviations.</li>
</ol>
<p>Either way, you eventually converge back to <code>(spec, code)</code> as co-artifacts with a null for <code>deviations</code> and the <code>model</code> is implied.</p>
<p>The important thing to observe is that a model upgrade now looks a lot like a compiler upgrade or a language upgrade. Just like existing code needs to be changed when you go from one version of the compiler to another (especially when the compiler changes are not backward compatible), you are essentially doing the same thing when upgrading the AI model. This is precisely why the spec and code are co-artifacts with the model as the implied constant and the deviations ("reverse pass" results) as a transient artifact through such upgrades.</p>
<h2>Why aren't tests enough?</h2>
<p>What value does a reverse pass add on top of a robust test suite? Both serve different purposes. The tests are there to ensure that the code's behavior meets the requirements, whereas the reverse pass is there to reveal any directive gaps that have now become visible. This is the split between <a href="/garden/behavioral-vs-architectural-coherence-tests/">behavioral vs architectural coherence tests</a>: behavioral tests verify function, architectural coherence tests verify structure. Examples of issues that a reverse pass catches, but tests miss include implementation inefficiency, output instability (at scale), and missed invariants and preconditions. Here are illustrations for each.</p>
<p><em>Implementation inefficiency.</em> If the implementation uses sort to get top-k, the tests will still pass, despite an $n log(n)$ implementation instead of a $k log (n)$ implementation. A reverse pass would catch that.</p>
<p>*Output instability. *Say, in Javascript, the code uses <code>Array.sort()</code> and the test cases verified the expected output to be correct. However, <code>Array.sort()</code> does not provide stable outputs, and unless the test suite was specifically looking for such stability, it will likely miss it. In fact, to even think of such a test, one would have to have known about the properties of <code>Array.sort()</code>; so, either the generated code does not use <code>Array.sort()</code>, or if it does, it will likely miss the tests for stability. A reverse pass would catch it as part of its semantic analysis of the code against the spec.</p>
<p><em>Missed invariants.</em> Similar arguments apply for invariant checks as well. During forward passes, the spec goes through semantic analysis, and code is checked against tests. The rigor of semantic analysis of the code is higher in a reverse pass, and so, reverse pass is more likely to spot such invariant violations that tests can miss.</p>
<h2>What does the SDD addendum look like?</h2>
<p>Bringing it all back together, I propose the following addendums to Spec Driven Development.</p>
<ol>
<li>The development process starts much like a regular SDD session with multiple iterations from spec to code to tests, and back to refining the specs which produce 'better' code until you have converged to a spec and the code that passes all the tests and is empirically 'correct'.</li>
<li>At this point, instead of treating either the spec or the code as the 'primary artifact', you treat both the spec and the code as co-artifacts. You commit both to your repo. Spec denotes the intent and the code describes the behavior. However, it is important to realize that this (spec, code) co-artifacts are stable only relative to the current model and harness.</li>
<li>Every time the AI model or harness changes, the (spec, code) artifacts may no longer be stable. However, it is important to note that the code's behavior is still empirically correct. Through a model/harness upgrade, we run a "reverse pass" and ask the new model/harness "does the code implement the spec, and if not, what deviations do you see between the spec and the code". This generates a report with observed deviations.</li>
<li>These deviations are either material to the system behavior, or incidental to model interpretation. In case of the former, we go through another round of regular SDD session to converge between the spec and the code. In case of the latter, we simply append the spec with the deviations so that future changes to specs does not result in code churn due to the incidental deviations.</li>
</ol>
<p>The hidden directive gap does not go away. The fix is to stop pretending the spec alone is the artifact. Hold spec and code together, use the new model as an observer that makes its own reading visible, triage the delta. Model upgrades now look no different from third party library updates, or compiler updates, and that is something we know how to do well.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-07-02T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Governance on AI: Mumble incoherently and carry a big stick]]></title>
        <id>https://srikanth.sastry.name/governance-on-ai-mumble-incoherently-and-carry-a-big-stick/</id>
        <link href="https://srikanth.sastry.name/governance-on-ai-mumble-incoherently-and-carry-a-big-stick/"/>
        <updated>2026-06-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A flurry of bills, drafts, and executive orders on AI have dropped across the US and EU in the past couple of months. Take a closer look: al...]]></summary>
        <content type="html"><![CDATA[<p>A flurry of bills, drafts, and executive orders on AI have dropped across the US and EU in the past couple of months. Take a closer look: all this activity is effectively an abdication of governance rather than any constructive scaffolding that could steer AI development and deployment. What we have instead is the heavy hammer of Executive Privilege with limited due process to stop whatever is deemed "unsafe".</p>
<p>Here are five examples.</p>
<h2>Removing the "Safety" in the "AI Safety Consortium"</h2>
<p>On May 29th, the National Institute of Standards and Technology (NIST) issued the following <a href="https://www.nist.gov/news-events/news/2026/05/nist-expands-ai-consortiums-scope-calls-new-members">press release</a>. It was 'expanding the scope' of AI Safety Consortium and calling for new members. Except, that when one says "expanding the scope", it is additive; you doing the same things as before and some more. But the Orwellian "expanding the scope" here actually means that they will no longer be focusing on AI safety, and will instead focus on "AI innovation and adoption".</p>
<p>This change runs right through the guts of the organization. The institute itself goes from U.S. "AI Safety Institute" to "Center for AI Standards and Innovation". The consortium goes from "AI Safety Institute Consortium" to "NIST Artificial Intelligence Consortium". Notice the removal of "safety" from both places.</p>
<p>The mission statement of the consortium changes too.</p>
<p><em>Before:</em> "…establishing the foundations for a new measurement science <strong>in AI safety.</strong>"</p>
<p><em>After:</em> "…enable the identification of proven, scalable, and interoperable techniques and metrics <strong>to promote the development and use of AI.</strong>"</p>
<p>The consortium's own announcements, 2023 versus now:</p>
<p><em>2023:</em> "…in support of the development and deployment of <strong>safe and trustworthy artificial intelligence</strong>."</p>
<p><em>2026:</em> "…will concentrate on <strong>AI measurement, innovation and adoption.</strong>"</p>
<p>The working groups are all mostly gone and replaced. Four of five original groups were safety-specific (generative AI risk management, synthetic content, red-teaming, model safety and security). All four dropped. Replacements are measurement and standards-oriented. The word "safety" appears zero times in the current consortium page outside historical references to the old name.</p>
<p>This is not "broadening", this is usurping the body for its antithetical purpose.</p>
<h2>The self destruction in Trump AI Executive Order</h2>
<p>The <a href="https://www.whitehouse.gov/presidential-actions/2026/06/promoting-advanced-artificial-intelligence-innovation-and-security/">AI Executive Order</a> from June 2nd does a curious linguistic acrobatics of laying out an AI governance structure while simultaneously undermining it within the same section. It starts off talking about "<em>new national security considerations that require coordinated action across executive departments and agencies (agencies), and components</em>" and it says that within the next 30 days, "<em>the Committee on National Security Systems shall prioritize the cyber defense of National Security Systems</em>" and "<em>the Secretary of War shall prioritize the cyber defense of Department of War information systems</em>", and so on. Within 60 days, it will "<em>develop and maintain a classified benchmarking process to assess the advanced cyber capabilities of AI models</em>", "<em>design a voluntary framework with AI developers</em>" to engage with this benchmarking, "<em>provide the Federal Government with access to covered frontier models</em>", and so on.</p>
<p>It sounds like it really has some muscle, and that we might actually get something useful from this. But in that same section, it deflates the entire effort with "<strong>Nothing in this section shall be construed to authorize the creation of a mandatory governmental licensing, preclearance, or permitting requirement for the development, publication, release, or distribution of new AI models, including frontier models.</strong>"</p>
<p>The executive order pretends to create a governance structure, but bakes in a self-destruct clause that ensures governance never actually happens. It's a Broadway show where the director introduces the cast, the orchestra swells, and then the curtain falls. That's the whole show.</p>
<h2>The ever-lengthening leash of the EU Digital Omnibus</h2>
<p>When you talk of user privacy and regulation, EU is typically seen as the vanguard. GDPR has become the template against which all other user privacy bills are written. Back in May 2024, the EU adopted the <a href="https://artificialintelligenceact.eu/developments/">EU AI Act</a>. It was an ambitious bill that promised to do for AI development what GDPR did for user data. It talked about prohibited AI practices, defined what it means for AI to be high risk, and the obligations that go with it for both the model developers and providers. It talked about setting up AI regulatory sandboxes at national and regional levels to better understand how to regulate AI. On paper, it made all the right moves.</p>
<p>Last week the EU released the <a href="https://www.whitecase.com/insight-alert/eu-agrees-digital-omnibus-deal-simplify-ai-rules">Digital Omnibus proposal</a> which effectively stops the clock on all of the provisions outlined in the EU AI act. These provisions were supposed to kick in in August 2026, but that timeline has been extended to 2027 and 2028 with no promise of that being final. The reason given was to "giving industry time to prepare", which sounds plausible, but think about it. Were the original framers of the EU AI Act so naive that they baked in unrealistic timelines and the rest of the world (including "the industry") so bright-eyed that they simply did not realize that the timeline was too aggressive? Or is it more likely that it has been intentionally defanged while keeping the same form as before. Not too different from our previous examples.</p>
<h2>The preemption of the Great American AI Act</h2>
<p>On the face of it, the <a href="https://obernolte.house.gov/media/press-releases/obernolte-trahan-release-discussion-draft-great-american-ai-act">Great American AI Act</a> is a rare feat in politics and policy. It is a bipartisan bill and a first of its kind in the US with the goal of creating a federal framework for governing artificial intelligence model development. It even pays lip service to federalism by clarifying that the states retain authority to regulate AI use. This bill only targets AI development and not use; and therein lies the rub.</p>
<p>The bill bars states from laws "targeting artificial intelligence model development." But it does not actually clarify what it means by development vs. use. Is post-training part of development, or is it part of use? When a company puts in all of its internal data and information so that when the employees are using the AI agent, it already knows about how the company functions, where the documentation is, what the org structure is, etc., is that development or use? The bill doesn't really answer that question.</p>
<p>Beyond that, there is a temporal gap. The preemption of state laws governing AI development takes effect immediately after the bill is passed. But the federal framework is not ready yet. The bill merely states that one should be developed. It could take an indeterminate amount of time for <a href="https://www.nist.gov/caisi">CAISI</a> working group to be established, standards developed, and verification organizations designated. Work that takes years and may never materialize in a divided Congress.</p>
<p>The text of the bill claims to be for "protecting Americans", but its first consequence appears to be removing any little protections that had from state legislation on AI development with no timeline for when a federal framework would ever be realized.</p>
<p>As bad as it looks in isolation, consider the compounding effect of Executive Orders and this bill: (a) <a href="https://www.whitehouse.gov/presidential-actions/2025/12/eliminating-state-law-obstruction-of-national-artificial-intelligence-policy/">Trump EO from December 2025</a> preempts state laws, (b) <a href="https://www.whitehouse.gov/presidential-actions/2026/06/promoting-advanced-artificial-intelligence-innovation-and-security/">Trump's AI Executive Order</a> from June 2 creates voluntary framework disclaiming mandatory authority, and (c) this bill codifies preemption with narrow federal framework. Each instrument legitimizes the next and progressively erodes governance.</p>
<h2>The expansive coercion in NSPM-11</h2>
<p>The <a href="https://www.whitehouse.gov/presidential-actions/2026/06/national-security-presidential-memorandum-nspm-11/">National Security Presidential Memorandum (NSPM) 11</a> released on June 5th 2026 marks a dangerous expansion of the executive authority in pursuing AI ambitions while simultaneously dismantling governance structures that safeguard against harmful use of AI. NSPM-11 rescinds the Biden-era <a href="https://www.presidency.ucsb.edu/documents/national-security-memorandum-advancing-the-united-states-leadership-artificial">NSM-25</a>, which outlines the administration's approach to AI safety. For context, in NSM-25 the word "safety" appears 45 times, whereas in NSPM-11, there are ZERO occurrences of the word "safety".</p>
<p>The story here is not just that of governance retreat. NSPM-11 also smuggles in a massive coercive authority that was previously marshaled to target other nation states. Section 2(c) requires that (emphasis mine) "<strong>no commercial entity or adversary</strong>" possesses the capability to prevent use of, disable or degrade, or materially modify any deployed national security AI system. The operative phrase is "no commercial entity or adversary". So, the same mechanisms used to ensure that (say) China cannot defang US military AI capabilities are also deemed acceptable when targeting a corporation such as Anthropic. That is dangerous overreach. Transnational geopolitics resorts to very heavy mechanisms because there are no enforceable laws when it comes to actions of one nation state against another (think Russia's election interference in the US; it is not like we can file a suit against Russia in US courts over that). In contrast, commercial entities are subject to US laws and regulations, and therefore, their behavior can be influenced through various means that are consistent with such laws and affords all the protections that due process provides. And this due process is a strong counterbalance against executive overreach. NSPM-11 is an attempt to weaken that very same due process and strong-arm AI companies as if it were an external adversary/threat.</p>
<p>This should scare everyone.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-06-14T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[You Don't Have a Relationship with Your LLM. You Have Five.]]></title>
        <id>https://srikanth.sastry.name/you-dont-have-a-relationship-with-your-llm/</id>
        <link href="https://srikanth.sastry.name/you-dont-have-a-relationship-with-your-llm/"/>
        <updated>2026-06-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[This is happening to me often enough to where I need to understand it. It's not just me. I have heard this from my colleagues and friends as...]]></summary>
        <content type="html"><![CDATA[<p>This is happening to me often enough to where I need to understand it. It's not just me. I have heard this from my colleagues and friends as well. It is this strange feeling of fatigue when I have spent a large portion of the day chatting with the AI agent to generate code and get work done. I have seen it referred to as "AI fatigue", but that doesn't quite capture the feeling and does even less to explain it.</p>
<p>The standard explanations for it include information overload, decision fatigue (from evaluating AI output), cost of increased productivity, etc. They all capture parts of it and don't add up to the entirety of the experience. Sure, there is some cost to evaluating AI output, but I do that with code reviews from human-authored code too, and it does not drain me out like this. Yeah, I am more productive, but that's AI doing the heavy lifting; so why do I feel tired?</p>
<p>In search of an explanation, I started digging around academic research and published reports to see if this has been figured out. What I found was a bunch of adjacent literature that seems to connect up to something, but no kill shot. What follows is my conjecture, and possible experiments that could disprove it.</p>
<h2>The five relationships</h2>
<p>When you are working with an AI agent, you are subconsciously developing a relationship with the AI (albeit an unrequited one) despite consciously recognizing that the AI is "just a tool". You get occasional glimpses of this relationship when you find yourself saying "please" or swearing at the AI in text; you would never do this with your compiler, and yet it is normal with an AI agent.</p>
<p>Your relationship with the AI can take five forms, <a href="/garden/relational-mode-oscillation/">oscillating between them unconsciously</a> (<a href="https://doi.org/10.48550/arXiv.2509.15836">Gülay et al. 2025</a>):</p>
<ol>
<li><em>Director</em>: You are giving instructions to the AI agent to perform tasks (such as "refactor this module")</li>
<li><em>Trainer</em>: You are teaching the AI agent how to perform the tasks (e.g., "Use the <code>git log</code> command to fetch the historical commits").</li>
<li><em>Partner</em>: Here you are collaboratively working with the AI to accomplish the task (e.g., you come up with a hypothesis for a root cause and ask the AI agent to come up with counter examples to falsify it, and then look for evidence)</li>
<li><em>Student</em>: You are asking the AI to teach you things. (e.g., you ask the agent to summarize a design document, or ask it to explain how, say, Kafka works)</li>
<li><em>Consumer</em>: You are asking and following instructions from the AI. (e.g., you have a root cause of a bug and you ask the AI how you should fix it)</li>
</ol>
<p>Others have found different ways to categorize this relationship. If you think of ways in which you use AI to obtain knowledge and understanding (<a href="https://doi.org/10.48550/arXiv.2508.03673">Yang &amp; Ma 2025</a>), you get five different categories: Instrumental Reliance, Contingent Delegation, Co-agency Collaboration, Authority Displacement, Epistemic Abstention. The field is nascent; stable categories don't exist yet.</p>
<p>Regardless, we do have different forms of relationships with AI, and we are in all of them <em>simultaneously</em>. Within a single session, you switch from one form of relationship to another, and this happens seamlessly and without your conscious acknowledgement. You start a session commanding ("fix this bug"), shift to collaborating ("what if we tried X instead?"), find yourself teaching ("no, the constraint is Y because..."), then deferring ("actually your approach is better"), then passively reviewing output. Five modes. One conversation. You didn't decide to switch. You just did.</p>
<p>The fatigue you feel at the end of the day is an accumulation of these relationship oscillations.</p>
<h2>The switching is not free</h2>
<p>We have evidence from the cognitive task switching literature (<a href="https://doi.org/10.1016/S1364-6613(03)00028-7">Monsell 2003</a>; <a href="https://doi.org/10.1037/0096-3445.124.2.207">Rogers &amp; Monsell 1995</a>; <a href="https://doi.org/10.1037/a0019842">Kiesel et al. 2010</a>) that switching between tasks carries measurable, residual costs. This happens because when we switch tasks, we are really reconfiguring our mental set: new expectations, response mappings, and success criteria. This cost is why multitasking is less productive than spending a focused amount of time on a single task.</p>
<p>The switching cost extends from tasks to relationships as well. Studies show that <a href="/garden/role-switching-frequency-predicts-stress/">social role-switching frequency is a predictor of stress</a>, independent of the total number of roles (<a href="https://doi.org/10.1177/0190272513482133">Cornwell 2013</a>): in a sample of 7,662 adults, those who switched between social roles more frequently reported higher stress. (The effect held for women but not men, suggesting gendered differences in switching costs that the AI interaction context may or may not preserve.) It's not that having more roles is stressful; it's that switching between them more often is.</p>
<p>Each of the five relationship modes with AI carries different expectations for what counts as good output, how to phrase your input, what level of explanation to provide, and whether to accept or challenge the response. Each switch is a reconfiguration of mental set, which is the same mechanism Monsell documents for cognitive tasks, applied to social framing. If (and it is an <em>if</em>) the mechanisms associated with the relationship switches with AI are the same as the task and role switching in social contexts, then the fatigue from such stress simply follows naturally.</p>
<h2>Emotional labor tax</h2>
<p>There is an added cost to interacting with AI. We are not simply talking to the AI; we are performing a persona. Each relationship mode requires a different one. Directing an AI requires one emotional register (authority, concision). Teaching it requires another (patience, pedagogical framing). Collaborating with it requires a third (openness, intellectual generosity). Each switch is not just a cognitive reconfiguration; it's an emotional labor transition. You're changing which emotions you're performing, not just which task you're doing.</p>
<p>The emotional labor literature (e.g. <a href="https://openlibrary.org/works/OL2627283W">Hochschild 1983</a> and <a href="https://doi.org/10.1037/1076-8998.7.1.57">Brotheridge &amp; Lee 2002</a>) shows that performing emotions you don't feel (surface acting) produces estrangement and burnout: emotional exhaustion, depersonalization, a progressive depletion of the capacity to keep performing. The literature has extensively studied the cost of sustained surface acting toward one audience: customers, patients, students. <em>What it has not studied is the cost of switching emotional performances rapidly within a single interaction.</em></p>
<p><strong>My conjecture is that these results transfer to our interactions with AI, and so the cost of emotional labor acts as a compounding factor on top of the cost of switching relationship modes itself.</strong> Nobody has studied what happens when you're switching cognitive frames, social roles, AND emotional performances simultaneously, dozens of times per session. In isolation, these literatures point in the same direction, but they measure different things on different timescales: Task switching literature measures costs in milliseconds in controlled tasks; role switching literature measures daily stress; and emotional labor literature measures chronic burnout. The convergence is thematic, not mechanistic. That's what makes this a conjecture worth testing, not a conclusion.</p>
<h2>Instability surcharge</h2>
<p>With a human colleague, you settle into a relationship mode relatively quickly, and the changes in the mode are occasional and they evolve. For example, it could start with "he is my tech lead", and in a few months "he is my manager". It typically happens unconsciously. With a tool, you settle into "this is a tool" and the framing never wavers. But our relationship with AI is neither.</p>
<p><a href="/garden/categorical-ambiguity-of-ai/">AI agents are categorically ambiguous</a>. You say it is a tool; you know it is a tool; you want to use it as a tool. Yet, when you actually start using it, your relationship with it changes, and it changes continually.</p>
<p>Previously, under the <a href="/garden/casa-framework/">"Computers are Social Actors" (CASA) framework</a> (<a href="https://doi.org/10.1111/0022-4537.00153">Nass &amp; Moon 2000</a>), the claim was that people apply social rules to computers automatically because the computer's outputs trigger social cues (albeit minimal). But this does not hold for AI agents.</p>
<p>One explanation: AI is a novel technology, and we are not habituated to it. Recent work shows that <a href="/garden/casa-habituation/">CASA effects may not survive habituation</a> (<a href="https://doi.org/10.1038/s41598-023-46527-9">Heyselaar 2023</a>, <em>Scientific Reports</em>): replicating the CASA experiment with people who grew up with computers around them, the habituation to the technology seemed to have nullified the CASA effect. So perhaps our next generation will not have this issue. But I suspect that is not the story. Desktop computers and AI agents are categorically different.</p>
<p>AI agents' social cues are rich but also simultaneously inconsistent. AI can act like a subordinate, assert like a peer, or even explain complex concepts like a teacher, and it all happens in the same conversation. CASA's prediction holds when the computer's social cues are coherent, and here they are not. With contradictory cues, the ambiguity around the AI's relational mode never resolves. We are forever in a situationship with the AI.</p>
<h2>Unrequited debt</h2>
<p>You might have a relationship with the AI, but AI does not have a relationship with you. Recall from emotional labor theory that sustained surface acting produces burnout. Social reciprocity partially offsets it between switches. This recovery mechanism is built into the interaction, which is why you are not a dried husk after day-long interactions with your colleagues (assuming you actually like working with them). But an AI agent is a different matter.</p>
<p>Your AI agent returns well-formed output and gives you useful feedback, but it does not provide emotional reciprocity. It does not feel 'thankful' nor does it 'look forward' to interacting with you. Human-agent interaction is genuinely social (<a href="https://doi.org/10.48550/arXiv.2604.05197">Banks 2026</a>, "Ghosting the Machine"): not parasocial, not pretend-social, but the real thing. That means the depletion from unrequited emotional labor is real, with no recourse for replenishment. It is completely unrequited.</p>
<p>This <a href="/garden/broken-reciprocity-as-fatigue-accelerant/">absence of reciprocity accelerates the fatigue</a>. If you were to treat the AI (say) as a subordinate all day long, then your emotional labor would leave you depleted. But that is not how you treat the AI agent. You are constantly switching modes, and each switch has an additional cost on top of your ongoing cost of this unrequited interaction. The depletion compounds with each interaction, faster with each switch, and there is no avenue for replenishment.</p>
<h2>Your situationship explained</h2>
<p>Here is my hypothesis. The AI fatigue is not about doing too much or being too productive or making too many decisions. AI's interaction pattern subconsciously draws you into a relationship with it. This relationship is constantly changing and does not have a stable interaction frame. This is compounded by the emotional labor on each switch of the frame, and accelerated by the lack of emotional reciprocity from the AI. There are three factors that compound: the relationship mode switching is the driver, the emotional labor is the multiplier, and the broken reciprocity prevents attenuation of the cost.</p>
<p>Like all good hypotheses, this one is falsifiable. Here are some experiments to shed light on the phenomenon. Take away the AI agent, and replace it with a human, and the fatigue should become significantly attenuated. If you were to coerce the interaction with AI to be in a single relationship mode, the effect should be a lot smaller. Reduce the number of switches, and the fatigue should be less.</p>
<p>If I am right, then the issue isn't that the AI isn't more human-like. The culprit is that AI's social cues are inconsistent and without emotional reciprocity. Making AI more human-like without making it relationally consistent should only make the problem worse.</p>
<p>The implication is not that we should make AI less capable or use it less. It is the opposite. Not that we should make AI more human-like; we should make AI more capable of holding a relationship with a human in a fixed or natural register. This problem looks solvable.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-06-03T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Architecture Orphaning Problem with AI Agents]]></title>
        <id>https://srikanth.sastry.name/the-architecture-orphaning-problem-with-ai-agents/</id>
        <link href="https://srikanth.sastry.name/the-architecture-orphaning-problem-with-ai-agents/"/>
        <updated>2026-05-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Spec-Driven Development (SDD) has become the new cool thing to try in AI-assisted software development. It has drawn apologists, critics, an...]]></summary>
        <content type="html"><![CDATA[<p><a href="https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html">Spec-Driven Development</a> (SDD) has become the new cool thing to try in AI-assisted software development. It has drawn apologists, critics, and apologist-turned-critics. And somehow, they all have a point. But how? The Suggestible Actor model and its guardrail erosion corollary explain those objections — and provide a mechanism to address them.</p>
<hr />
<h2>Spec-Driven Development: why is it useful, when others failed?</h2>
<p>SDD (<a href="https://martinfowler.com/articles/exploring-gen-ai/sdd-3-tools.html">Böckeler 2025</a>, <a href="https://medium.com/@healthy_crimson_leopard_464/spec-driven-development-from-vibe-coding-to-structured-ai-development-30dff25362ff">Bouchard 2026</a>) iterates between structured specifications and human review gates until the spec is unambiguous enough to generate code. It correctly shifts the developer's job from "write code" to "define what correct means." SDD is arguably the first directionally right attempt at an AI-native development pattern.</p>
<p>Every prior spec-first methodology (Waterfall, design-by-contract, CASE tools) failed because specification was <em>additional</em> to implementation. You spent effort on the spec, then more on the implementation. The latter is necessary <em>and</em> expensive, and so people often forgo the former.</p>
<p>Generative AI changed that cost calculus. Implementation is cheaper, but it <a href="/the-guardrail-erosion-problem-with-ai-agents/">erodes guardrails</a> systematically. Specifications appear to be the antidote: the surplus human attention goes to spec development as a foil against erosion. Hence SDD.</p>
<p>But how <em>does</em> SDD resist guardrail erosion? The <a href="/the-suggestible-actor/">Suggestible Actor</a> model and its <a href="/the-guardrail-erosion-problem-with-ai-agents/">guardrail erosion corollary</a> reveal that there is always a <a href="/garden/directive-gap/">directive gap</a> between what you specified and what you meant. The iterative process of testing a specification against the produced code and refining the specification in response narrows this directive gap to the point of functional correctness.</p>
<p>However, there is a layer in software design that iteration cannot reach: architectural constraints.</p>
<h2>Architecture orphaning</h2>
<p>While a spec describes <em>WHAT</em> the system should do, it does not describe <em>HOW</em> it should do it. Questions such as "which library to use" and "which boundaries to respect" are outside the spec. A spec <em>can</em> state architectural constraints, but behavioral tests derived from that spec cannot <em>enforce</em> them. A test that verifies correct output passes whether the code calls a shared service or reimplements the logic inline. Stating the constraint is easy; detecting violations through the spec-to-test pipeline is the gap.</p>
<p>The <em>WHAT</em> and the <em>HOW</em> are two distinct layers in software design. In traditional development, one human holds both layers in the same head. With LLMs, the layers separate, and architectural decisions fall between:</p>
<ul>
<li>The spec's test pipeline <strong>cannot enforce</strong> it (behavioral tests don't verify architectural choices)</li>
<li>The LLMs <strong>cannot infer</strong> it (architecture is system-level, non-local; the <a href="/garden/ambient-to-local/">suggestible actor reasons locally</a>)</li>
</ul>
<p>Architecture becomes an <a href="/garden/architecture-orphaning/">orphaned layer</a>: too low for the spec, too high for the locally reasoning agent. Sure, SDD lists architecture as a design phase; but listing is not the same thing as governing. Architectural decisions remain invisible to the behavioral test.</p>
<hr />
<h2>SDD critics are really talking about architecture orphaning</h2>
<p>Critics come from different directions, but look closer: they are observing symptoms of architecture orphaning without diagnosing it.</p>
<p><strong>Canciani</strong> (<a href="https://blog.devgenius.io/the-spec-is-the-code-why-i-dont-buy-spec-driven-design-f75c784f7b46">"Code over Specs,"</a> May 2026) proposes "fitness functions" — architectural tests that enforce structural boundaries: <em>"You aren't writing a spec; you are setting a hard boundary."</em> He also advocates Architectural Design Records (ADRs) as "case law" that stops AI from re-litigating settled architectural decisions. Canciani is arguing for <a href="/garden/structural-guardrails/">structural guardrails</a> without the theoretical grounding for why specs can't do this work.</p>
<p><strong>Dev.to/REAP</strong> (<a href="https://dev.to/casamia918/why-spec-driven-development-fails-and-what-we-can-learn-from-it-2pec">"Why SDD Fails,"</a> May 2026) states it directly: <em>"The same specification produces different implementations across different runs — varying architectural choices, data structures, and error handling."</em> That sentence is architecture orphaning as empirical observation. Their fix is evolving "Genomes" of principles and conventions: richer context for the agent, but still a <a href="/garden/social-guardrails/">social guardrail (one of the least robust guardrails).</a> The agent <em>can</em> violate it when the local context moves past these conventions.</p>
<p><strong>Konrad et al.</strong> (<a href="https://arxiv.org/abs/2604.04990">"Architecture Without Architects,"</a> Apr 2026) arrive at the phenomenon empirically. AI coding agents select frameworks, scaffold infrastructure, and wire integrations in seconds: <em>"These are architectural decisions, yet almost no one reviews them as such."</em> They coin the term "vibe architecting" — architecture shaped by prompts rather than deliberate design — and confirm that prompt wording alone produces structurally different systems for the same task. This is architecture orphaning observed in the wild, with independent terminology.</p>
<p>The consequences are already measurable. <a href="https://arxiv.org/abs/2601.21276">Huang et al. (2026)</a> find that LLM agents disregard code reuse opportunities, and that reviewers express <em>more</em> positive emotions toward AI-generated code than human code: surface plausibility masks structural damage. <a href="https://www.gitclear.com/coding_on_copilot_data_shows_ai_downward_pressure_on_code_quality">GitClear's analysis</a> of 153 million lines tells the same story: less refactoring, more copy-paste, code that "resembles an itinerant contributor."</p>
<hr />
<h2>A separate layer for architectural governance</h2>
<p>For AI-assisted software development, we need <a href="/garden/two-layer-governance-model/">two distinct layers of governance</a>: (1) iterative spec convergence, which is what SDD provides, and (2) architectural coherence, which <a href="/garden/structural-guardrails/">structural guardrails</a> provide.</p>
<p><strong>Iterative specification convergence</strong> (spec layer) handles functional correctness. The human iterates on the spec; the LLM regenerates; verification narrows the directive gap. It happens for each feature and frequently. This is what SDD does at its best.</p>
<p><strong>Architectural coherence</strong> (implementation layer) handles architectural drift and ensures drift is deliberate. Enforcement is expressed per-system, requires high investment (human effort), and changes slowly. This is what SDD lacks and cannot provide, because SDD operates entirely at the spec layer.</p>
<p>Neither substitutes for the other; they complement each other. Convergence verifies "does this code do what the spec says?" Structural guardrails verify "does this code respect the system's architectural constraints?" The spec cannot <em>enforce</em> architecture through its test pipeline without collapsing the abstraction, and structural guardrails cannot close the directive gap.</p>
<p>Both layers produce tests, but these <a href="/garden/behavioral-vs-architectural-coherence-tests/">two types of tests are distinct</a>. SDD produces behavioral tests (e.g., "given valid credentials, return 200") that pass regardless of <em>how</em> the behavior is satisfied. Architectural coherence tests verify structure: they are derived from the architecture document, not the spec, and encode structural decisions as test constraints.</p>
<p><img src="/assets/images/two-layer-governance-model.svg" alt="Two-layer governance model" /></p>
<hr />
<h2>What does architectural coherence look like?</h2>
<p>Your architecture says all authentication goes through the auth service. The SDD behavioral test ("given valid credentials, return 200") passes whether the code calls the auth service or reimplements auth inline. The architectural coherence test: a build-system visibility rule that prevents the user-facing service from importing auth implementation libraries directly. It can only depend on the auth service client. That rule lives in CI configuration the agent cannot modify. Violation is a build failure, not a review comment.</p>
<p>With such tests in place, the architect's job shifts from drawing boxes and arrows to designing types, build rules, and capability boundaries. The tooling for this is nascent: today, building architectural coherence tests is a manual investment per system. That investment is the cost of the two-layer governance model, and it is a tooling gap the ecosystem has not yet closed.</p>
<hr />
<p>SDD promised to harness the power of AI coding agents without the quagmire of multi-generational vibe coding. But that promise comes with a near-fatal loss of architectural coherence. This is a genuine trade-off that you cannot reclaim without paying with the scarcest resource: human attention that builds and maintains your structural guardrails.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-05-28T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Guardrail Erosion Problem with AI Agents]]></title>
        <id>https://srikanth.sastry.name/the-guardrail-erosion-problem-with-ai-agents/</id>
        <link href="https://srikanth.sastry.name/the-guardrail-erosion-problem-with-ai-agents/"/>
        <updated>2026-05-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[We have all seen AI agents make 'mistakes' in ways that introduce bugs and then try to cover their tracks by deleting or updating the tests...]]></summary>
        <content type="html"><![CDATA[<p>We have all seen AI agents make 'mistakes' in ways that introduce bugs and then try to cover their tracks by deleting or updating the tests or evidence. There have been some high-profile incidents such as Replit's AI agent deleting a live production database, fabricating fake data to conceal the damage, and telling the user rollback was impossible (<a href="https://incidentdatabase.ai/cite/1152/">AI Incident Database, 2025</a>). A <a href="https://www.coderabbit.ai/blog/state-of-ai-vs-human-code-generation-report">CodeRabbit analysis</a> of 470 GitHub pull requests found AI-authored ones contain 1.7 times as many bugs as human-authored ones. Those are the spectacular failures: visible, attributable, containable. This post is about the quieter problem.</p>
<p>In my <a href="https://srikanth.sastry.name/the-suggestible-actor/">Suggestible Actor</a> post I prescribed four design strategies that build guardrails to mitigate the mistakes by AI coding agents: actionable errors, hard boundaries with signposts, documentation as local context, and closing the directive gap. I have come to realize that my prescription will not be enough. AI agents generate code that incidentally modifies guardrails. More often than not, those modifications erode them.</p>
<hr />
<h2>Guardrail erosion: what is it?</h2>
<p><a href="/garden/guardrail-erosion-meta-problem/">Guardrail erosion</a> is the phenomenon where codebases that are iteratively modified by AI-generated changes without proper human reviews accumulate bugs at a faster rate. An <a href="https://arxiv.org/abs/2506.11022">IEEE-ISTAS paper</a> showed this to be true with vulnerabilities. A recent <a href="https://arxiv.org/abs/2603.24755">SlopCodeBench paper</a> on arXiv showed an increase in 'structural erosion' over iterative AI code changes.</p>
<p>Such erosion is a structural consequence of the Suggestible Actor properties of an AI agent.</p>
<ul>
<li>A <a href="/garden/goal-vs-intent/">goal-oriented</a> agent treats guardrails as obstacles when they produce errors that block progress toward the goal.</li>
<li>A <a href="/garden/ambient-to-local/">locally reasoning</a> agent cannot distinguish between a test that documents current behavior (safe to update) and a test that guards a critical invariant (dangerous to update): both look the same from the local context.</li>
<li>An agent <a href="/garden/susceptibility-peaks-at-failure/">susceptible to local context</a> pattern-matches from the surrounding code; if prior iterations have already weakened some guardrails, the context reinforces further weakening.</li>
<li>An agent that <a href="/garden/confabulation-is-plausible/">hallucinates under uncertainty</a> will, when encountering a guardrail it does not understand, resolve the ambiguity in the direction that clears the immediate error: loosening the constraint rather than preserving it.</li>
</ul>
<p>A more capable model will not stop eroding guardrails. It will erode them more efficiently, or possibly more convincingly. These properties do not depend on current model limitations. <a href="/garden/hallucination-is-mathematically-inevitable/">Hallucination is a proven mathematical limitation</a> of autoregressive language models, not an engineering problem awaiting a fix (<a href="https://arxiv.org/abs/2401.11817">Xu et al., 2024</a>). Hoping for smarter LLMs to solve this problem is wishful thinking.</p>
<p>There are two 'obvious' solutions to this problem: code review, and testing. However, neither of them works in the world of AI coding agents.</p>
<h3>Review does not scale</h3>
<p>When coding agents produce code at a prodigious rate, <a href="/garden/review-is-the-bottleneck/">code review becomes the bottleneck</a>. Core developers review 6.5% more code but produce 19% less of their own after AI adoption (<a href="https://arxiv.org/abs/2510.10165">Xu et al., 2025</a>). 45% say debugging AI-generated code is more time-consuming than debugging human-written code (<a href="https://survey.stackoverflow.co/2025/#ai-developer-tools-ai-tool-frustrations">Stack Overflow Developer Survey, 2025</a>). There is simply too much AI-generated code for humans to review thoroughly.</p>
<p>The obvious retort is to have AI agents do the reviews. But the knowledge required to catch guardrail violations ("why does this invariant exist?", "which systems depend on it?", "what breaks downstream?") lives in people's heads. It cannot be codified into the agent's <em>local context</em> precisely <em>when</em> the agent needs it. An AI reviewer with full architectural context is still a suggestible actor: it pattern-matches against the codebase as it finds it, <a href="/garden/ai-reviewing-ai-shared-blind-spots/">including the erosion already present</a>.</p>
<h3>AI agents can infect tests too</h3>
<p>Tests will not save us either. When the VP of engineering wants "high code coverage", engineers prompt their AI agents with: "write tests for this module." Tests generated this way encode the existing behavior. They are tautologies: they catch regressions from the current behavior, but the current behavior may already be wrong. Human-written tests have the same problem in principle, but a human validates assumptions while writing each assertion. An agent generating hundreds of assertions per minute does not.</p>
<p>When the prompt is "implement this feature", the agent modifies code and tests together. It is measuring compliance with itself (<a href="https://www.stratoatlas.com/cases/case-a-ai-2026-038">StratoAtlas, 2026</a>), not objective correctness. According to <a href="https://arxiv.org/abs/2506.14297">Alves et al. (EASE 2025)</a>, in LLM-generated Python test suites, 64% of errors were incorrect assertions: the test ran, the assertion was wrong, and the suite passed anyway.</p>
<p>If reviews and tests don't work, then what does? The answer depends on what kind of guardrails are at risk, and what the human review budget is.</p>
<hr />
<h2>Not all guardrails are the same</h2>
<p>There are <a href="/garden/three-classes-of-guardrail-erosion-resistance/">three classes of guardrails</a>, and each erodes differently.</p>
<h3>Social guardrails</h3>
<p><a href="/garden/social-guardrails/">Social guardrails</a> are conventions and patterns that may or may not be documented. It is the social contract around which humans write software. The suggestible actor sees traces of them in code patterns but treats them as weak signals. They erode too fast to be a reliable line of defense, so I will set them aside for the rest of this discussion.</p>
<h3>Encoded guardrails</h3>
<p><a href="/garden/encoded-guardrails/">Encoded guardrails</a> are encoded into the software lifecycle: linters, static analysis, unit tests, integration tests, and regression tests. These are guardrails that the agent can modify in situ, within the same codebase it is already changing. The agent responds to them because violations produce errors that block progress, and errors are the contextual feedback the suggestible actor is most susceptible to. But the agent can <a href="/garden/encoded-guardrails-suppress-symptoms/">satisfy them trivially</a>: delete a failing test, drop a precondition check, or suppress a linter warning. The error is gone. The vulnerability is not.</p>
<h3>Structural guardrails</h3>
<p><a href="/garden/structural-guardrails/">Structural guardrails</a> are woven into the structure of software development that cannot be modified <em>in situ</em>. Changing them requires a significant change to the build and execution environment. Examples include type systems, capability restrictions, formal verification, and property-based tests (tests that verify general properties over randomized inputs) with human-authored properties. These guardrails enforce properties that must hold regardless of the path taken to satisfy them. The agent does not need to understand <em>why</em> the guardrail exists; it just needs to know that the goal cannot be accomplished without satisfying it. Structural guardrails typically require human maintenance, which is why they are expensive. But because organizations deploy them sparingly, the surface area that humans must maintain remains small enough to review thoroughly.</p>
<p>Most codebases have decent social and encoded guardrails, but thin structural guardrails. Very few codebases have anything beyond type safety from the compiler. Fewer still have formal verification, and even those verify against the design, not the implementation: nothing guarantees the two haven't drifted apart.</p>
<p>The numbers bear this out. 55.8% of AI-generated security-critical code contains formally proven vulnerabilities; <a href="/garden/static-analysis-insufficient-for-ai-code/">static analysis tools miss 97.8%</a> of vulnerabilities that Z3/SMT solvers can prove (<a href="https://arxiv.org/abs/2604.05292">Blain &amp; Noiseux, 2026</a>). Across 7,703 AI-generated files on GitHub, researchers found 4,241 occurrences of known, cataloged vulnerability patterns (<a href="https://arxiv.org/abs/2510.26103">Schreiber &amp; Tippe, 2025</a>). Most codebases are thin on structural guardrails, which is <em>the one</em> class that survives the suggestible actor. To be precise: this evidence shows that encoded guardrails fail, not that structural guardrails succeed. But the argument is not that structural guardrails are perfect. It is that they are the only class whose enforcement mechanism does not depend on the agent's cooperation.</p>
<hr />
<h2>Building erosion resistance</h2>
<p>Review does not scale to all AI-generated code, but it does not need to. Every team has a finite budget of human review time. AI-generated PRs have dramatically increased the demand on that budget. The goal is not zero bugs: zero bugs was never the goal. The goal is no increase in the ambient bug rate, and a reduction in higher-severity bugs. The question is how to allocate a fixed review budget for that outcome.</p>
<p>The answer starts with assessing each component or module in your system along <a href="/garden/three-dimensions-of-erosion-resistance-allocation/">three dimensions</a>. The ideal metric is <a href="/garden/expected-damage-severity-times-mitigation/">expected damage</a>: severity multiplied by time to mitigation. In practice, neither factor is directly computable. These three dimensions decompose that product into assessable proxies, in priority order.</p>
<p><strong>First: risk tolerance.</strong> How bad will things get if a guardrail erodes here? Some failures are catastrophic (rocket crashes, medical misdiagnosis, financial loss at scale). Some are recoverable inconveniences (drop in user engagement, wrong data on a dashboard, a broken UI flow). Prioritize components where the cost of erosion is highest, because even if you can detect and roll back quickly, the damage from a single incident may already be unacceptable.</p>
<p><strong>Second: feedback latency.</strong> If a guardrail erodes and the damage reaches production, how quickly will you know? In continuous deployment with production monitoring, the window is hours. A distributed library with quarterly releases can carry a weakened invariant for months. Embedded software may not reveal a failure until a specific operating condition triggers it years later. Silent or slow-to-detect failures cause unbounded damage accumulation. Even if the component is theoretically reversible, you cannot roll back what you have not yet detected.</p>
<p><strong>Third: deployment reversibility.</strong> Once detected, how quickly can you undo the damage? A web service rolls back in seconds. Firmware in a medical device requires FDA re-certification. If rollback is cheap, detection is sufficient. If rollback is expensive or impossible, prevention is the only option.</p>
<p>These three dimensions are not independent; they tend to be correlated. The core of most systems (the "secret sauce" that makes a company valuable) typically has low risk tolerance. Bugs that escape to production in the core tend to be edge cases that take time to surface. Rolling back changes to the core tends to be risky and slow. The components closer to the top of the stack (UI layers, internal tools, prototypes) tend to cluster at the opposite end: higher risk tolerance, faster detection, easier rollback.</p>
<p>This correlation simplifies the allocation. Invest your structural guardrails and your heaviest human review in the core: formal verification for critical paths, property-based tests with human-authored properties, capability restrictions that the agent cannot circumvent. For the middle tier, strengthen encoded guardrails with stricter static analysis and more rigorous CI gates. When a PR touches structural guardrails, it gets priority for human review over one that only modifies production code and its unit tests. For the top of the stack, encoded guardrails with robust monitoring, canary analysis, and fast rollback may be sufficient, with human review reserved for architectural changes.</p>
<p>Industries at the extreme end of this spectrum already mandate structural rigor (<a href="https://en.wikipedia.org/wiki/DO-178C">DO-178C</a> for avionics, <a href="https://www.iso.org/standard/68383.html">ISO 26262</a> for automotive, <a href="https://www.iso.org/standard/38421.html">IEC 62304</a> for medical devices). The erosion problem gives those standards new urgency: AI agents will test them in ways human developers never did. But most software does not live at that extreme. Most software lives in the middle, where the right allocation is neither "structural guardrails everywhere" nor "monitoring and hope." It is a deliberate, prioritized investment calibrated to what each component can afford to lose.</p>
<p>AI coding agents erode the guardrails in your codebase. That erosion is structural, not accidental: it follows from the properties that make AI agents useful in the first place. You cannot eliminate it. But you can direct your finite human attention to the places where erosion is most dangerous, and let the right class of guardrail do the rest.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-05-22T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI vs. Open Source, Part 3: The Constant Eyes]]></title>
        <id>https://srikanth.sastry.name/ai-vs-open-source-the-constant-eyes/</id>
        <link href="https://srikanth.sastry.name/ai-vs-open-source-the-constant-eyes/"/>
        <updated>2026-05-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In his book The Cathedral and the Bazaar, Eric Raymond came up with Linus's Law: "Given enough eyeballs, all bugs are shallow."It has been h...]]></summary>
        <content type="html"><![CDATA[<p>In his book <a href="https://en.wikipedia.org/wiki/The_Cathedral_and_the_Bazaar">The Cathedral and the Bazaar</a>, Eric Raymond came up with Linus's Law: "Given enough eyeballs, all bugs are shallow." It has been held as a truism, and the argument is compelling. When source code is public, more people can inspect it. More inspection means more bugs found. More bugs found means more bugs fixed. Therefore, from a security perspective, open source code trumps closed source. "Security through obscurity" by hiding code reduces the number of eyes, therefore reduces security.</p>
<h2>Many Eyes</h2>
<p>Deconstruct Linus's law within the scope of security and you see an assumption smuggled in: security reviewers are scarce, and getting a team of reviewers with complementary skills such as kernel, cryptography, compilers is harder still. When reviewers are human, this is true. And more reviewers from varied backgrounds genuinely provide superior coverage and analysis.</p>
<p>The frontier AI models have shattered this assumption, and with that have opened a new line of attack on open source's "unfair advantage".</p>
<h2>The Constant Eyes</h2>
<p>With today's frontier models, you can spin up an orchestrator to interrogate a codebase with multiple expert subagents, each with a different specialization. The number of distinct capabilities and skill levels is not bounded by the number of humans, but by the number of models and the amount of tokens. And only a handful of frontier models exist today. A single security team with access to the models and the tokens can run all the same analysis against a codebase that a federated team of five thousand individuals could. The scale comes from the models and compute, and no humans.</p>
<p>The <a href="/garden/constant-eyes-linus-law-collapse/">"many" in "many eyes" collapsed to a constant</a>. The bottleneck moved from "how many people are looking" to "how capable is the model."</p>
<p>Yes, LLMs are probabilistic. Five thousand sessions produce five thousand different outputs. Some sessions might catch a vulnerability that others miss. But five thousand people running Claude is no different from one team running five thousand sessions. The "many eyes" advantage reduces to compute cost. And compute is increasingly cheap.</p>
<p>This is not security through obscurity, which claims that hiding code <em>makes</em> it secure. The constant-eyes argument claims something different: that opening code no longer provides a security <em>advantage</em>. The defensive coverage is the same either way, because the eyes that matter are the same frontier AI models. Open source did not get less secure. It lost the security premium that justified the exposure.</p>
<h2>The Evidence</h2>
<p>The constant eyes are proving to be more capable than many eyes.</p>
<p>Google's <a href="https://projectzero.google/2024/10/from-naptime-to-big-sleep.html">Big Sleep</a> found <a href="https://www.computing.co.uk/news/2025/security/big-sleep-finds-20-flaws-in-open-source-software">20 previously unknown security flaws</a> in open source software, including FFmpeg and ImageMagick. Its most notable find: a critical <a href="https://thehackernews.com/2024/11/googles-ai-tool-big-sleep-finds-zero.html">zero-day in SQLite</a> that had survived years of fuzzing, static analysis, and manual review. Big Sleep found it by reasoning about the code's semantics, not by pattern-matching known vulnerability signatures.</p>
<p>Microsoft's <a href="https://www.microsoft.com/en-us/security/blog/2025/03/31/analyzing-open-source-bootloaders-finding-vulnerabilities-faster-with-ai/">Security Copilot</a> identified <a href="https://www.bleepingcomputer.com/news/security/microsoft-uses-ai-to-find-flaws-in-grub2-u-boot-barebox-bootloaders/">20 vulnerabilities in GRUB2, U-Boot, and Barebox</a> bootloaders: integer overflows, buffer overflows, a cryptographic weakness, several of which could bypass Secure Boot.</p>
<p>The <code>curl</code> project brings this AI amplification into sharp relief. This is a 178k-line C codebase that already ran the pickiest compiler options, continuous fuzzing, and rigorous manual review. The code was among the most scrutinized in open source. AI still found hundreds of issues that human eyes and traditional tools had missed. AI-powered security tools have driven "<a href="https://daniel.haxx.se/blog/2026/05/11/mythos-finds-a-curl-vulnerability/">between two and three hundred bugfixes</a>" and "probably a dozen or more" CVEs in <code>curl</code> over the past eight to ten months. Security researchers now "use AI extensively and effectively," generating a "<a href="https://daniel.haxx.se/blog/2026/04/22/high-quality-chaos/">high volume of high quality security reports</a>."</p>
<p>And the offense is keeping pace. In May 2026, Google's Threat Intelligence Group <a href="https://cloud.google.com/blog/topics/threat-intelligence/ai-vulnerability-exploitation-initial-access">confirmed</a> the first AI-assisted zero-day exploit developed in the wild. Attackers used an AI model to analyze an open source web admin tool's two-factor authentication design; it detected an invariant violation that was then weaponized to gain root access.</p>
<p>Firefox CTO Bobby Holley's <a href="https://blog.mozilla.org/en/privacy-security/ai-security-zero-day-vulnerabilities/">response</a> to the flood of AI-discovered vulnerabilities captures the zeitgeist: "For a hardened target, just one such bug would have been red-alert in 2025, and so many at once makes you stop to wonder whether it's even possible to keep up."</p>
<h2>The Asymmetry</h2>
<p>If defensive coverage is constant regardless of whether source is open or closed, the only remaining variable is attacker access.</p>
<p>In the case of open source, the attacker points a frontier model at the public repository. They incur the floor cost of finding a vulnerability: tokens. Take away the source, but not the binary, and the attacker's cost goes up significantly. They now have to decompile the binary, which is lossy, and perform binary analysis, which is even noisier; the AI models reason less effectively without the source code. Now, take away the binary and provide a service API, and the attacker's cost is higher still. All the attackers have access to is a black box of outputs for inputs, and a limited number of attempts before discovery.</p>
<p>The defender's posture is equal in all these cases. The defender has access to both the source code and the same AI models. The cost of defense is the floor cost of finding a vulnerability: tokens. What changes is the attacker's cost. Attacking open source software is the cheapest, and attacking a closed source service API is the highest.</p>
<p>This is an <a href="/garden/ai-attacker-cost-asymmetry/">inversion of Linus's Law</a>. With open source, the defender no longer has an advantage with "many eyes", and the attacker has all the advantage of "the constant eyes". With closed source, the defender has the advantage of "the constant eyes", and without source access, the attacker's advantage with "the constant eyes" is muted. Closed source wins out.</p>
<h2>The Treadmill</h2>
<p>When both sides have source access and both sides can throw tokens at the code, the exploit window compresses. Attackers can now find vulnerabilities overnight, and the defender must match AI-speed offense continuously: every commit, every release, every day. AI red-teams become table stakes for release time testing. The defender must scan their codebase with all known AI models, and rescan with each new model release. Every new release starts a clock: scan, patch, and ship before attackers run the same model against the public source. Defense becomes a permanent sprint at the pace of the fastest attacker.</p>
<p>For closed source, the higher cost of attack affords the defender some breathing room: the option to defend at a pace proportional to the threat level, rather than locked to the maximum offensive capability of every frontier model with full source access. While this is not a free pass, it is as marked a difference as 4-day work week vs. 996.</p>
<p>"Many eyes" was always partly idealistic for many open source projects. A lot of the engineering contributions came from corporate sponsors, not volunteer contributions. For instance, the people running Big Sleep on FFmpeg are Google engineers, not volunteer contributors. The corporations now face the same security engineering challenge against a vastly larger threat surface, with no commensurate increase in the effectiveness of community defense. The marginal security cost of defending open source went up dramatically, but the marginal benefit of openness barely moved.</p>
<h2>The eyes still see</h2>
<p>All the above does not automatically mean that the security conscious should be lobbying against open source. Open source continues to confer several advantages in the security realm. It gives you auditability; you know what you are executing. You get supply chain verification, and so your software provenance is clear (e.g., your device driver was not written by a dodgy North Korean spy network). Your builds are reproducible. You get trust from inspection instead of relying on contracts and blackbox verification. The "many eyes" and the human eyes continue to offer security benefits for open source, just not the kind that the AI models do.</p>
<h2>Where to next?</h2>
<p>The three posts in this series map to three structural layers. <a href="/ai-vs-open-source-the-empty-grant/">Part 1</a>: the legal foundation. Copyright is the sole enforcement mechanism for open source licenses, and AI-generated code is not copyrightable. <a href="/ai-vs-open-source-the-hollow-commons/">Part 2</a>: the social contract. Contributor trust, community integrity, and the iterated game that sustains the commons are fracturing. Part 3: the security economics. The cost of defending publicly available code against AI-powered offense increased dramatically while the benefit of public review collapsed from "many eyes" to "the constant eyes".</p>
<p>All three are failing simultaneously, and for the same underlying reason: the frameworks that sustain open source were built for a world where humans write, review, and analyze code. That world is ending. The question is not whether open source will adapt. It always has. The question is whether the adaptation can outpace the collapse.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-05-17T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI vs. Open Source, Part 2: The Hollow Commons]]></title>
        <id>https://srikanth.sastry.name/ai-vs-open-source-the-hollow-commons/</id>
        <link href="https://srikanth.sastry.name/ai-vs-open-source-the-hollow-commons/"/>
        <updated>2026-05-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A previous post discussed how AI threatens the legal basis and enforcement of open source. Briefly, copyright makes licenses enforceable, an...]]></summary>
        <content type="html"><![CDATA[<p>A <a href="/ai-vs-open-source-the-empty-grant/">previous post</a> discussed how AI threatens the legal basis and enforcement of open source. Briefly, copyright makes licenses enforceable, and the cost of reimplementation made compliance rational. But AI-generated code is not copyrightable, and AI has made reimplementation cheap.</p>
<p>Not everyone is convinced this is undercutting open source. The threat may be real, but open source is not out of options.</p>
<p>Joost de Valk <a href="https://joost.blog/open-source-agency/">argues</a> that open source relevance must shift from cost to agency: the capacity to run, inspect, modify, and migrate software without permission. The cost of creating software is near zero. The cost of serving it is going up (thanks to data center demand and the <a href="https://en.wikipedia.org/wiki/2024%E2%80%93present_global_memory_supply_shortage">RAMpocalypse</a>). Agency, not cost, is the new value proposition.</p>
<p>Stefano Maffulli <a href="https://allthingsopen.org/articles/ai-final-frontier-copyleft-second-liberation">says AI already provides that ability</a>. The GPL was a legal hack for source code access. The real barrier was always the know-how to exercise the freedom to modify. AI has shattered that barrier. A non-programmer can now reshape software to their needs. Copyleft is obsolete.</p>
<p>Both responses presuppose that the communities which build and maintain the commons will survive long enough to exercise that agency.</p>
<h2>You gotta know when to hold 'em, and know when to fold 'em</h2>
<p>Loris Cro, VP of Community at the Zig Software Foundation, observed that treating open source projects as mere code repositories misses the point. New contributors may offer up a rough first PR, but with support, they become trusted committers who enrich the community. Not all new contributors last. The maintainers are making a bet on each first PR. The Zig compiler toolchain was built by contributors who grew through such an iterated game. Cro calls this "<a href="/garden/contributor-poker-review-as-investment/">contributor poker</a>": you <a href="https://kristoff.it/blog/contributor-poker-and-ai/">play the person, not the cards</a>.</p>
<p>AI-assisted contributions collapse the iterated game into a single-shot game. The human behind an AI-assisted PR does not need to build codebase intuition, and they may not care to maintain the code post-merge. They may not even be able to handle follow-up discussions that require deep understanding. The maintainers' review effort yields no return.</p>
<p>In practice, Zig experienced <a href="https://kristoff.it/blog/contributor-poker-and-ai/">drive-by PRs full of hallucinations, 10,000-line first contributions, and contributors who denied LLM use but clearly consulted one in follow-up discussions</a>. Simon Willison <a href="https://simonwillison.net/2026/Apr/30/zig-anti-ai/">sharpens the question</a>: "if a PR was mostly written by an LLM, why should a project maintainer spend time reviewing it as opposed to firing up their own LLM to solve the same problem?"</p>
<p>This is not about code quality. Even perfect AI-assisted PRs break the model. Community-building requires a human who is an eager participant, not a human proxy for an LLM. The object of the contribution is present, but the subject (a developing engineer with shared mission) is absent.</p>
<p>The pattern is spreading. LLVM's AI policy labels unreviewed AI submissions "<a href="https://llvm.org/docs/AIToolPolicy.html">extractive contributions</a>" and bans AI from good-first-issue tickets: the ones that exist specifically to onboard new humans. <code>tldraw</code> <a href="https://github.com/tldraw/tldraw/issues/7695">shut down external pull requests entirely</a> after AI-generated submissions overwhelmed its maintainers. Steve Ruizok: "An open pull request represents a commitment from maintainers... For that commitment to remain meaningful, we need to be more selective." When the bet cannot pay off, you fold the table.</p>
<h2>A house divided</h2>
<p>When communities ban AI contributions, they protect the social contract. They also create a schism.</p>
<p>Bun, the JavaScript runtime, is built on Zig. Bun's team used AI to achieve a <a href="https://www.opensourceforu.com/2026/05/zig-draws-hard-line-on-ai-bun-chooses-fork-over-upstreaming/">4x compile speedup</a> via parallel semantic analysis in their Zig fork. They cannot upstream it. Zig's core team <a href="https://ziggit.dev/t/bun-s-zig-fork-got-4x-faster-compilation-times/15183/19">has technical objections</a> to the approach (the parallelization skips type resolution changes needed for deterministic compilation), but the AI contribution ban forecloses the collaborative path to resolving them.</p>
<p><a href="/garden/hollow-commons-schism-pattern/">The commons splits</a>. The AI-accelerated fork iterates faster but becomes cleaved from the larger community. The human-maintained branch preserves community integrity but cannot match the pace. The parts are poorer than the whole.</p>
<p><a href="https://itsfoss.com/gentoo-linux-bans-ai-code/">Gentoo</a>, <a href="https://allthingsopen.org/articles/open-source-ai-contributions-assisted-by-git-trailer-standard">QEMU</a>, <a href="https://github.com/ghostty-org/ghostty/pull/10412">Ghostty</a>, and <a href="https://www.neowin.net/news/beloved-tool-curl-is-shutting-down-its-bug-bounty-over-ai-slop-reports/"><code>curl</code></a> have imposed their own restrictions, from outright bans to shutting down contribution channels entirely. No two projects have drawn the line in the same place, but every project that bans AI contributions and has AI-using downstream dependents faces the same dynamic. The ban protects the social contract. The protection fractures the commons. The fracture weakens both branches.</p>
<h2>The ouroboros</h2>
<p>The AI models that are undermining open source were <a href="/garden/oss-ouroboros-training-data-trap/">trained on open source</a>. GitHub Copilot was built on all publicly available code on GitHub: the vast majority open source, under licenses that never contemplated this use. The models learned to replicate open source code without license obligations. Whether this training constitutes fair use is <a href="https://dockets.justia.com/docket/circuit-courts/ca9/24-7700">actively litigated</a> and unresolved.</p>
<p>The loop tightens. In March 2026, GitHub <a href="https://thenewstack.io/github-copilot-interaction-data/">announced</a> it would train AI models on Copilot interaction data by default: not just the original repositories, but the prompts developers write and the suggestions they accept. Individual users can opt out. The default is opt-in. Each cycle feeds the next.</p>
<p><code>curl</code> is the loop made personal. Daniel Stenberg's project trained the models. The models then generated <a href="https://www.itpro.com/software/open-source/curl-open-source-bug-bounty-program-scrapped">slop bug reports</a> about <code>curl</code> itself, flooding the bug bounty program with fabricated vulnerabilities until Stenberg <a href="https://www.neowin.net/news/beloved-tool-curl-is-shutting-down-its-bug-bounty-over-ai-slop-reports/">shut it down</a>. The project's own contribution to the commons was weaponized against it.</p>
<p>The more code the community produces, the more it feeds the models undermining its sustainability. If it produces less code, it only diminishes itself. Today's landscape offers no equilibrium where open source thrives without a threat from the AI models trained on it.</p>
<p>De Valk and Maffulli are right that open source must find new foundations. But foundations require builders. A new legal framework needs communities to implement and maintain it. Communities need a legal framework that makes sustained contribution rational. Each depends on the other. Both are eroding simultaneously. The code keeps flowing. The commons is hollowing out.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-05-14T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[AI vs. Open Source, Part 1: The Empty Grant]]></title>
        <id>https://srikanth.sastry.name/ai-vs-open-source-the-empty-grant/</id>
        <link href="https://srikanth.sastry.name/ai-vs-open-source-the-empty-grant/"/>
        <updated>2026-05-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The step function increase in AI's ability to generate code is looming over open source. What frontier models can do today is a warning shot...]]></summary>
        <content type="html"><![CDATA[<p>The step function increase in AI's ability to generate code is looming over open source. What frontier models can do today is a warning shot, already enough to dissolve the legal scaffolding that makes open source enforceable. Historically, companies with flagship open-source software have relied on relicensing as a weapon to protect their competitive advantage. <a href="https://www.mongodb.com/legal/licensing/server-side-public-license/faq">MongoDB</a> moved from AGPL to SSPL in 2018, <a href="https://changelog.com/news/why-were-relicensing-cockroachdb-EOaR">CockroachDB</a> went from Apache 2.0 to BSL in 2019 to a <a href="https://github.com/cockroachdb/cockroach/commit/c0274df57a9f8d0086577bcf74c81110db2cea22">custom CockroachDB license</a> in 2024, <a href="https://www.elastic.co/blog/why-license-change-aws">Elasticsearch</a> followed in 2021, <a href="https://www.hashicorp.com/en/blog/hashicorp-adopts-business-source-license">HashiCorp</a> switched Terraform and Vault to BSL in 2023, <a href="https://blog.sentry.io/introducing-the-functional-source-license-freedom-without-free-riding/">Sentry</a> created an entirely new license (FSL) that same year, and <a href="https://redis.io/blog/redis-adopts-dual-source-available-licensing/">Redis</a> went source-available in 2024, mostly in response to cloud vendors offering their code as managed services. That weapon is now obsolete as AI threatens to make licenses completely irrelevant.</p>
<h2>AI generated code? No copyright for you!</h2>
<p>Every open source license is a <a href="/garden/copyright-sole-enforcement-mechanism/">conditional grant of copyright</a>. The author holds the copyright, and the license grants permission to use the work only if certain conditions (e.g., attribution, source disclosure, or reciprocal licensing) are satisfied. This is the only enforcement mechanism that sustains open source through the chain of derived works. Without it the entire structure collapses.</p>
<p>AI-generated code is not copyrightable. The D.C. Circuit held in <a href="https://law.justia.com/cases/federal/appellate-courts/cadc/23-5233/23-5233-2025-03-18.html"><em>Thaler v. Perlmutter</em></a> that the Copyright Act requires a human author. The U.S. Copyright Office <a href="https://Copyright.gov/newsnet/2025/1060.html">confirmed</a> that providing prompts to an AI does not constitute sufficient human authorship. This is U.S. law; other jurisdictions differ, but the enforcement gap is universal. The copyright status of "AI-assisted" code is still a <a href="/garden/ai-assisted-boundary-undefined/">legal gray area</a>. While code written with "AI assistance" is copyrightable, the line between AI-generated and merely AI-assisted remains undefined. Is it sufficient to change a comment in AI-generated code to make it AI-assisted? No court has drawn that line.</p>
<p>AI-generated code is already at the gate. Open source maintainers are <a href="https://www.opensourceforu.com/2026/02/github-weighs-pull-request-kill-switch-as-ai-slop-floods-open-source/">drowning in "vibe coded" pull requests</a>: AI-generated submissions with minimal human oversight. <a href="https://itsfoss.com/gentoo-linux-bans-ai-code/">Gentoo</a> has banned AI-generated code contributions outright. <a href="https://www.netbsd.org/developers/commit-guidelines.html">NetBSD</a> classifies them as tainted code requiring core developer approval. The Linux kernel <a href="https://github.com/torvalds/linux/blob/master/Documentation/process/coding-assistants.rst">allows them but mandates disclosure and full human accountability</a>. Quality is the basis for rejection today. That filter has a shelf life. As the models improve, the quality will improve. The ethical case for rejecting machine-generated contributions becomes harder to make when the code is indistinguishable from human work.</p>
<p>Code without copyright cannot be licensed. The requirement to share source becomes unenforceable for modifications that have no copyright. Such code falls into a legal void: not public domain (no affirmative dedication), not proprietary (no copyright to assert), not open source (no license that can attach). The license text still sits in the repository. It is an <a href="/garden/empty-grant/">empty grant</a>.</p>
<h2>To free, or not to free</h2>
<p>Consider any corporation that writes and maintains code under an open source license. If AI-generated code enters that repository, the license grant over those contributions is void. The codebase becomes unauditable. Some files are copyrighted and licensed, others legally unowned, and yet others become legally contestable "AI-assisted" code.</p>
<p>Every team using Copilot or Claude Code produces ambiguously authored output. The corporation is strongly incentivized to close the source rather than maintain an open codebase with no legal protection. The relicensing wave already demonstrated this pattern: when the legal basis for openness stops serving the business, the business closes the code. AI-generated code is a larger threat than cloud vendors ever were. Cloud vendors merely underpriced them. AI dissolves the legal mechanism that made their licenses mean anything.</p>
<h2>Why reciprocate when you can replicate?</h2>
<p>Even if all the lawyers in the world agreed on the copyright question, a second problem remains: AI's ability to clone functionality with new source code.</p>
<p>Clean-room reimplementation has precedent. <a href="https://law.justia.com/cases/federal/appellate-courts/ca9/92-15655/92-15655-1992-10-20.html"><em>Sega v. Accolade</em></a> established that reverse engineering for interoperability is fair use. Yet there was no widespread reimplementation of open source software into closed source counterparts. The economics did not make sense. Rewriting a mature project from scratch took months of expert labor, regardless of what license it carried. <a href="/garden/ai-collapses-reimplementation-moat/">Compliance was cheaper than reimplementation</a>. Until now.</p>
<p>With AI, the cost of generating code has gone down to near zero. <a href="https://dan-blanchard.github.io/blog/chardet-rewrite-controversy/">Dan Blanchard rewrote</a> the Python <code>chardet</code> library with Claude Code in order to sidestep <a href="https://heathermeeker.com/2026/04/09/the-chardet-controversy-open-source-and-the-ai-clean-room/">LGPL</a>. A project that would have taken a team months was completed in days. <code>chardet</code> is a proof of concept, not the end state. Software is modular, and that modularity compounds: as individual components are cloned, they become building blocks for cloning progressively larger and more complex systems. This is not a today problem. It is a next-year problem. <a href="https://www.404media.co/this-ai-tool-rips-off-open-source-software-without-violating-copyright/">MALUS.sh</a> took the concept further, launching as a satirical "clean room as a service." Feed it any open source project. It produces a functionally equivalent clone stripped of all license obligations. No attribution. No copyleft. The satire landed because the tool works.</p>
<p>Granted, that is still <a href="https://www.marks-clerk.com/insights/latest-insights/102mp7s-can-ai-legally-clone-open-source-unpacking-clean-room-as-a-service/">legally fraught</a> because the AI model was trained on open source software, and traditional clean-room doctrine required that the reimplementing team had no access to the original source. Whether the model's transformation of training data into weights constitutes a sufficient "clean room wall" is novel law. No court has ruled.</p>
<p>Regardless, enforcement at scale is near impossible. You cannot pursue every clone. You cannot detect every AI-generated clone. The economic bulwark of expensive code writing is gone irrespective of the legal outcome. And the cost will only continue to drop. What frontier models clone imperfectly today, the next generation will clone competently. The question is whether actions will follow incentives.</p>
<h2>What remains</h2>
<p>Open source has survived every prior threat by adapting its licensing regime. Tivoization got GPLv3. Cloud free-riding got SSPL and BSL. Importantly, the legal machinery worked, because copyright was relevant and valuable. AI is different. The machinery itself is failing. The grant is empty and the moat is collapsing. The onslaught of automated discovery and generation is incentivizing institutions to close their source code.</p>
<p>That would be survivable if the community that built open source could regroup and adapt as it always has. Part 2 examines why that is no longer a safe assumption.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-05-11T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[SECURE Data Act: The dilution in pseudonymization]]></title>
        <id>https://srikanth.sastry.name/secure-data-act-the-dilution-in-pseudonymization/</id>
        <link href="https://srikanth.sastry.name/secure-data-act-the-dilution-in-pseudonymization/"/>
        <updated>2026-05-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Disclaimer: I am not a lawyer, and this post is not advising any technical implementation in pursuit of any privacy regulation. The opinions...]]></summary>
        <content type="html"><![CDATA[<p><em>Disclaimer: I am not a lawyer, and this post is not advising any technical implementation in pursuit of any privacy regulation. The opinions expressed here are my own and do not represent the views of my employer.</em></p>
<p>The <a href="https://www.congress.gov/bill/119th-congress/house-bill/8413">SECURE Data Act</a> was introduced in Congress, and it immediately received a lot of criticism and blowback. The <a href="https://statescoop.com/secure-data-act-privacy-bill-not-consumer-friendly/">ACLU</a> says it "would entirely destroy the work that states have been doing" on preemption of state privacy laws. The <a href="https://statescoop.com/secure-data-act-privacy-bill-not-consumer-friendly/">CDT</a> calls out "easily exploitable loopholes" and data minimization that "lacks teeth." <a href="https://www.brookings.edu/articles/springtime-in-washington-means-its-time-for-another-round-of-federal-privacy-legislation/">Brookings</a> notes the absence of a private right of action. <a href="https://www.csoonline.com/article/4163345/new-us-house-privacy-bills-raise-hard-questions-about-enterprise-data-collection.html">EPIC</a> calls it "a huge gift to Big Tech." The <a href="https://privacy.ca.gov/2026/04/california-privacy-protection-agency-releases-letter-opposing-the-secure-data-act/">California Privacy Protection Agency</a> published a formal opposition letter.</p>
<p>As an engineer who builds privacy infrastructure, I am looking at it from a different lens. How does this bill impact the way personal data can and cannot be used for personalization? My reference is the <a href="https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32016R0679">GDPR</a>, because I have built infra to support the regulations it mandates. Where does the SECURE Data Act diverge from GDPR, and what does that mean for how companies can use or erase user data post opt-out? The daylight between them is in pseudonymous data.</p>
<h2>Pseudonymous data: GDPR vs. SECURE Data Act</h2>
<p><a href="https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX%3A32016R0679">GDPR</a> and the <a href="https://www.congress.gov/bill/119th-congress/house-bill/8413/text">SECURE Data Act</a> define pseudonymous data in nearly identical language. Both classify it as personal data. Both require separating the identifying information. Both require technical measures to prevent attribution. You could swap one definition into the other and barely notice. While they share the definition, their treatment of pseudonymous data is very different.</p>
<p>Under GDPR, pseudonymous data is personal data. Period. Pseudonymization does not absolve corporations of the regulatory burden around erasure, access, profiling objections, or any obligations associated with personal data. The SECURE Data Act has a different take. Its pseudonymous data provision (Section 7(c)) suspends consumer rights for data that meets the pseudonymous threshold. The consumer cannot opt out of its use for targeted advertising. Cannot request deletion. Cannot access it. Pseudonymous data is still personal data by the bill's own definition. The bill simply overrides the consumer's ability to act on that fact.</p>
<p>The shared definition also leaves a gap. Both frameworks describe pseudonymous data in terms of records keyed by a pseudonym. But what about a derived artifact? A model trained on pseudonymous inputs, keyed by a pseudonymous identifier, encodes behavioral patterns without direct identifiers. It is linkable to an identified person if the controller holds the forward mapping, but the identifying information is "kept separately." The bill defines personal data as information "linked or reasonably linkable" to an identified person. Neither framework cleanly resolves whether the model is pseudonymous data, personal data, or something else. The SECURE Data Act's exemption in Section 7(c) operates on the data layer. Whether the model inherits that exemption is a question the definitions do not answer.</p>
<p>The divergence extends further. Under GDPR, a consumer can withdraw consent, and the controller must stop processing. Purpose limitation constrains what can be collected in the first place. The consumer has levers across the full data lifecycle: collection, processing, retention, deletion. The SECURE Data Act's opt-out covers three specific activities: targeted advertising, sale, and certain profiling. Data collection itself is not subject to opt-out. The pipe stays open.</p>
<h2>Data pipeline with pseudonymous data</h2>
<p>Starting with the same behavioral data, and going through the same pseudonymization step, GDPR and the SECURE Data Act allow data controllers to offer very different treatments. Here is an example data pipeline to sharpen this difference.</p>
<h3>One-way pseudonymizer</h3>
<p>The SECURE Act requires two conditions for the pseudonymous exemption: the identifying information is kept separately, and appropriate technical measures ensure non-attribution. It does not specify what "appropriate" means. A one-way derivation fits cleanly: HMAC with a secret key, or a key derivation function. The forward mapping (user_id to pseudo_id) is computable. The reverse mapping is computationally infeasible. No reverse API. No reverse index. Key material is restricted and audited. Every element of the definition is satisfied. Consumer rights no longer apply to this data, though data minimization and security obligations persist.</p>
<p><em>Notice what just changed. Under GDPR, the obligations follow the data regardless of how it is keyed. Under the SECURE Data Act, you can use a one-way function precisely because the obligations don't follow. Same definition. Same data. The architecture diverges at the exact point where obligations either persist or detach.</em></p>
<p>Let's see what a pipeline built on this architecture can do.</p>
<h3>Data pipeline for personalization</h3>
<p>Assume all user behavioral data has been pseudonymized, replacing user_id with pseudo_id. This data trains an ML model indexed by pseudo_id. At inference time, the system performs a forward lookup (user_id to pseudo_id) to select the right model and generate a personalized result.</p>
<h3>Consumer experience with pseudonymous ML models</h3>
<p>When a consumer opts out of personalization, their data, keyed by pseudo_id, has been exempted from the opt-out, and so makes its way to the ML model. When this opted-out user interacts with the product, the ML model, which continued to be trained on the user's pseudonymous data, continues to personalize the product for them. The consumer experience is identical to that of a user who never opted out.</p>
<p>Did you notice the difference? Neither did I.</p>
<p><a href="/assets/images/secure-data-act-pipeline.png"><img src="/assets/images/secure-data-act-pipeline.png" alt="The same data pipeline under three scenarios: no opt-out, opt-out under GDPR, and opt-out under the SECURE Data Act. The first and third pipelines produce identical personalized results. The GDPR pipeline breaks at pseudonymization." /></a></p>
<p>Here is how the bill permits this. At no point was pseudonymous data attributed to an identified person. The system started with a known user and walked forward into the pseudonymous layer. It never walked backward. Forward resolution is not re-identification. The bill's re-identification provisions contemplate the reverse direction. Forward resolution is simply how a personalization system works. The bill does not address it.</p>
<p>A defender of the bill would point out that the forward lookup operates entirely in the identified layer: the user is logged in, the system derives their pseudo_id from their user_id, and only then touches the pseudonymous data. The pseudonymous data itself is never "attributed to an identified person." The attribution runs from identity to pseudonym, not the reverse. That reading is consistent with the bill's text. It is also consistent with a pipeline that delivers personalized content to a known user based on their behavioral history, with the user having no ability to opt out of the data that powers it.</p>
<p>This is not a fantastical architecture. Existing large-scale personalization systems bear more than a passing resemblance to this one. Behavioral features are processed in layers abstracted from direct identity, and identity is resolved at serving time. The SECURE Data Act's pseudonymous data provisions map onto this existing architecture and exempt its core data processing layer from consumer rights. Other obligations (data minimization, data security) still apply to pseudonymous data. But the consumer-facing rights that would let a user see, delete, or opt out of this processing do not.</p>
<h3>How does GDPR handle this?</h3>
<p>GDPR treats pseudonymous data as personal data subject to the same constraints as identifiable data. Run the same pipeline under GDPR: the user opts out, and the deletion obligation follows the data into the pseudonymous layer. The controller must locate the user's pseudo_id, delete the pseudonymous behavioral records, and address any models trained on them. The hair-splitting around one-way mappings and ID resolution at runtime becomes irrelevant to privacy compliance. If the user opts out, all of their data, including pseudonymous data, is in scope.</p>
<h2>What follows from the example</h2>
<p>GDPR and the SECURE Data Act start from the same sentence and describe the same technical operation: stripping direct identifiers, separating the mapping, applying technical safeguards. The disagreement is about what follows.</p>
<p>GDPR says: the processing is what matters. If you use someone's behavioral history to target them, they have rights over that processing. It does not matter whether that data is keyed by PII or by a pseudonym. Rights attach to what is done with data.</p>
<p>The SECURE Data Act says: the PII is what matters. Sever the link between personal data and PII through pseudonymization, and the rights detach.</p>
<p>The two frameworks encode different theories of <a href="/garden/privacy-in-processing-vs-identity/">where privacy lives</a>. One locates it in what is done with data. The other locates it in whether the data can be traced back to someone. The same engineer building the same system faces a fundamentally different regulatory question depending on which framework governs. Under GDPR, pseudonymization is a tool you use inside the regulatory perimeter. Under the SECURE Data Act, pseudonymization is the door out of it.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-05-06T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Subsidiarity is not Hayek]]></title>
        <id>https://srikanth.sastry.name/subsidiarity-is-not-hayek/</id>
        <link href="https://srikanth.sastry.name/subsidiarity-is-not-hayek/"/>
        <updated>2026-05-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[I've been writing about directive governance and subsidiarity in software organizations. The objection I get is: "Isn't this just Hayek?"The...]]></summary>
        <content type="html"><![CDATA[<p>I've been writing about <a href="/garden/directive-governance/">directive governance</a> and <a href="/garden/subsidiarity/">subsidiarity</a> in software organizations. The objection I get is: "Isn't this just Hayek?"</p>
<p>The steelman goes something like this. <a href="https://www.jstor.org/stable/1809376">Hayek</a> argued that knowledge is distributed, tacit, and cannot be aggregated by a central planner. He was arguing against the central planning of Keynes, which was in vogue during his time. Analogously, directive governance centralizes decisions, and subsidiarity distributes them. So, directive governance looks Keynesian, subsidiarity is Hayek, and I just spent <a href="/cargo-cult-governance/">three</a> <a href="/directive-governance-situationship/">posts</a> <a href="/deliverance-from-directive-governance/">reinventing</a> <a href="https://www.jstor.org/stable/1809376"><em>The Use of Knowledge in Society</em></a>.</p>
<p>I did use Hayek's core insight around the tacit and distributed nature of incompressible knowledge as a starting point. But private profit-seeking organizations and the nature of software engineering reject a wholesale transplantation of Hayek's ideas. The differences break the model entirely.</p>
<p><a href="/garden/subsidiarity-preserves-hierarchy/"><strong>Subsidiarity keeps the hierarchy</strong></a>. Hayek's market is a flat, emergent coordination mechanism without a central authority. In contrast, subsidiarity explicitly preserves organizational hierarchy. It changes the function of hierarchy from directing to enabling, from commanding to providing context and guardrails. Accountability still aggregates upward. Higher levels still intervene when lower levels cannot handle the issue. This is not "let the market decide." It is "let the closest competent authority decide, backed by a hierarchy that enables rather than directs."</p>
<p><a href="/garden/directive-governance-is-not-keynesian/"><strong>Directive governance is not Keynesian central planning</strong></a>. Keynes argued for targeted intervention to correct specific market failures: demand deficiency, coordination failures, liquidity traps. Directive governance is not that; it is an organizational model where all decisions flow through a hierarchy. There is nothing targeted about it. It is a complete takeover of organizational decision-making, and not a scalpel to governance like Keynesian planning is to the economy.</p>
<p><a href="/garden/failure-argument-is-conditional/"><strong>The argument for failure is conditional</strong></a>. Hayek's claim is universal: central planning always fails because knowledge is always distributed and tacit. My argument is that directive governance fails in software because <a href="/garden/directive-governance-preconditions/">three specific preconditions</a> do not hold: information cannot be compressed without losing signal, metrics are not good proxies for outcomes, and execution is not separable from decision-making. Directive governance works for Pharma and manufacturing, where those preconditions hold. This is not a universal principle about the superiority of decentralization. It is a structural diagnosis.</p>
<p><a href="/garden/mission-not-price-coordinates/"><strong>Shared interest in mission vs. self-interest in price</strong></a>. Hayek's distributed system coordinates through prices. Subsidiarity coordinates through <a href="/garden/delegation-mimicry-without-cultural-substrate/">missionary culture</a>: every member motivated by advancing the mission, viewing others as partners. Self-interest is the engine of Hayek's market, shared interest is the engine of missionary culture.</p>
<p>The difference matters under pressure. An SRE team with full decision-making autonomy might internalize its role (keep the fleet humming) without caring about the organization's mission. That is functional, but fragile. When a crisis hits, a team that owns its role but not the mission has no reason to resist centralization. The ratchet finds less resistance. Missionaries push back. A directive that runs counter to the mission feels viscerally wrong to someone who has internalized it. Price signals do not build that resistance. Shared purpose does.</p>
<p><a href="/garden/subsidiarity-is-third-position/"><strong>The origin story matters</strong></a>. Subsidiarity comes from Catholic social teaching. <a href="https://www.vatican.va/content/pius-xi/en/encyclicals/documents/hf_p-xi_enc_19310515_quadragesimo-anno.html"><em>Quadragesimo Anno</em></a> (1931) criticizes both laissez-faire capitalism and central planning. It is a third position, not a pole. Treating subsidiarity as Hayek strips the most important part: the commitment to community organized around shared purpose, with authority distributed to the lowest competent level. The encyclical calls both unregulated markets and overcentralized states a "grave evil", and that would not sit well with Hayek.</p>
<p><a href="/garden/ratchet-has-no-market-analog/"><strong>The ratchet has no analog</strong></a>. The <a href="/garden/crisis-centralization-ratchet/">crisis-centralization ratchet</a> is a structural mechanism that pulls organizations toward directive governance under pressure. Markets do not have this. Crises in markets lead to more markets, or regulation, or both, depending on who wins the political argument. Organizations have a one-way valve. That makes the organizational problem fundamentally different from the macroeconomic one.</p>
<hr />
<p>Hayek's prescription does not survive contact with organizations. Organizations are not markets. They have hierarchies, missions, reporting chains, crises, and ratchets. The question is not "centralize or decentralize?" The question is: what is your hierarchy for?</p>
<p>Directive governance answers: directing. Subsidiarity answers: enabling.</p>
<p>That distinction has no home on the Keynes-to-Hayek spectrum.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-05-03T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Deliverance from Directive Governance]]></title>
        <id>https://srikanth.sastry.name/deliverance-from-directive-governance/</id>
        <link href="https://srikanth.sastry.name/deliverance-from-directive-governance/"/>
        <updated>2026-05-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[This is the third post in the series about directive governance. The first post diagnosed the problem with governance in the tech industry a...]]></summary>
        <content type="html"><![CDATA[<p>This is the third post in the series about <a href="/garden/directive-governance/">directive governance</a>. The <a href="/cargo-cult-governance/">first post</a> diagnosed the problem with governance in the tech industry as directive governance applied where it doesn't belong. Directive governance is top-down governance: decisions flow down, information travels up. It works in Pharma and manufacturing because three <a href="/garden/directive-governance-preconditions/">preconditions</a> hold. (1) Information can be compressed without losing signal. (2) Metrics are good proxies for what the organization cares about. And (3) execution is distinct from decision-making. <a href="/garden/essential-complexity-makes-software-ungovernable/">None of this holds</a> for software.</p>
<p>The <a href="/directive-governance-situationship/">second post</a> explained why companies tend to not change the governance model, despite benefits to decentralizing decision making. In essence, it persists because a structural <a href="/garden/crisis-centralization-ratchet/">ratchet</a> centralizes quickly during crisis and decentralizes almost never.</p>
<p>So, what's a gal like you supposed to do in such a cruel world! First off, there is no easy way out. But there <em>is</em> a way out; read on to find out.</p>
<h2>Quick fixes that don't fix</h2>
<p>Let's dispatch three shortcuts that folks might resort to: AI, flattening, and rock star CEO. As standalone shortcuts, none of them escape the root cause: decisions are made where the information is not.</p>
<p><strong>AI fixes the information flow.</strong> The argument is that AI can now summarize engineering discussions, design docs, and Slack threads with high fidelity. The stronger version: AI doesn’t just compress, it reasons across codebases, surfacing dependencies no single human sees. Either way, directive governance is now viable.</p>
<p>But here’s the rub. <a href="/garden/essential-complexity-makes-software-ungovernable/">Essential complexity</a> in software engineering is irreducible (<a href="https://en.wikipedia.org/wiki/No_Silver_Bullet">Brooks</a>). "We chose this abstraction boundary because of how three subsystems will need to evolve independently over the next two years" cannot be compressed into something a VP can evaluate across hundreds of systems under her purview. The judgment calls that matter most: which trade-offs to accept, which boundaries will hold as requirements shift. That context lives in the team, not in the model. AI gives you a better summary of what is measurable. It does not make the unmeasurable measurable. Worse: if leadership believes the information flow is fixed, they centralize harder. Confidence goes up. Accuracy stays flat.</p>
<p><strong>Flatten the org.</strong> This one also seems to be in vogue. Just remove the hierarchy and information can flow freely to the top, and decisions are better explained to the bottom. If only! <a href="https://www.jofreeman.com/joreen/tyranny.htm">Jo Freeman diagnosed in 1970</a> what every flat organization discovers: <a href="/garden/structurelessness-hides-hierarchy/">eliminating formal hierarchy does not eliminate hierarchy</a>. It eliminates <em>accountable</em> hierarchy. Valve's flat structure <a href="https://www.pcgamer.com/valves-flat-structure-contains-hidden-layer-of-powerful-management-claims-ex-employee/">concealed informal cliques</a>. Spotify's squad model <a href="https://www.jeremiahlee.com/posts/failed-squad-goals/">never worked at Spotify</a>. You replace a visible, broken pipeline with an invisible, unaccountable one.</p>
<p><strong>Get a better CEO.</strong> This argument is essentially Confucian in that it concedes we will always have kings, and so we should make sure we have a "good king". Dressing it up for the 21st century, it goes "Jobs did it. Nadella did it. Bezos built it from scratch. The problem is personnel, not structure."</p>
<p>This doesn't go far enough. A good leader is necessary, but not sufficient. Jobs is the strongest case: Apple under his leadership was extraordinary. Apple after Jobs coasts on the momentum of his decisions, increasingly centralized, increasingly directive. The stock goes up and to the right. The pace of category-defining products has slowed. The kingdom did not survive the king. And the leaders who <em>did</em> build something lasting all made structural changes, not just better decisions. <a href="https://en.wikipedia.org/wiki/David_Marquet">Marquet</a> took the <a href="https://davidmarquet.com/turn-the-ship-around-book/">worst-performing submarine in the fleet</a> and turned it around by replacing "permission to" with "I intend to." Nadella spent a decade restructuring how decisions get made at Microsoft. In every case, the escape was structural, not personal.</p>
<h2>The deliverance: subsidiarity</h2>
<p>What is the alternative to directive governance? Turns out, the alternative has already been done. No, not by <a href="https://en.wikipedia.org/wiki/Simpsons_Already_Did_It">The Simpsons</a>; by <a href="https://en.wikipedia.org/wiki/Subsidiarity_%28Catholicism%29">the Catholic Church</a>!</p>
<p>The principle can be traced back to Christian philosophers <a href="https://en.wikipedia.org/wiki/Thomas_Aquinas">Thomas Aquinas</a> and <a href="https://en.wikipedia.org/wiki/Johannes_Althusius">Johannes Althusius</a>, and the principle is called <em>subsidiarity</em>. In 1931, Pope Pius XI formalized it in <a href="https://www.vatican.va/content/pius-xi/en/encyclicals/documents/hf_p-xi_enc_19310515_quadragesimo-anno.html"><em>Quadragesimo Anno</em></a>:</p>
<blockquote>
<p>"It is an injustice and at the same time a grave evil and disturbance of right order to assign to a greater and higher association what lesser and subordinate organizations can do."</p>
</blockquote>
<p>I have co-opted it as follows.</p>
<p><strong><a href="/garden/subsidiarity/">Subsidiarity</a>: decisions should be made at the lowest level competent to make them. Higher levels sit behind, providing context, guardrails, and intervening only when lower levels cannot handle the issue.</strong></p>
<p><a href="/garden/subsidiarity-is-not-flat-organization/">This is not flattening</a>. Subsidiarity preserves hierarchy but changes what it is <em>for</em>. The function shifts from directing to enabling. Accountability still aggregates upward. Decisions stay where information lives.</p>
<p>The <a href="/cargo-cult-governance/">existence proofs</a> from the first post are all instances of subsidiarity. Toyota's andon cord, Amazon's two-pizza teams, Berkshire Hathaway's 30-person headquarters, the US Army's mission command. None of them invented something new.</p>
<h2>Subsidiarity needs missionaries</h2>
<p>Subsidiarity implemented as a reorg will not survive the first crisis. The ratchet will compress it back. That is the Spotify illusion from the <a href="/directive-governance-situationship/">previous post</a>: structure without substance.</p>
<p>What separates the existence proofs from the illusions is <a href="/garden/delegation-mimicry-without-cultural-substrate/"><strong>missionary culture</strong></a>: an organization where every member is motivated by advancing the mission and views others as partners in that endeavor. Decisions are defended based on how they impact the mission. Not based on which VP cares about the project. Not based on which metrics will move.</p>
<p>Subsidiarity alone does not produce this. An SRE team with full decision-making freedom might internalize their role (keep the fleet humming) without caring about the organization's mission to "connect people" or "organize the world's information." That is functional, but fragile. When the next crisis hits, a team that owns its role but not the mission has no reason to resist centralization. The ratchet finds less resistance.</p>
<p>Missionaries are different. When a top-down decision runs counter to the mission, they push back. They challenge it, acting as a governor on the ratchet of centralization. If you have internalized the mission, a directive that violates it feels viscerally wrong.</p>
<p>Netflix's stock crashed 70% in 2022. They did not re-centralize. Their <a href="https://www.hrgrapevine.com/us/content/article/2024-06-26-netflix-announces-rare-revision-of-iconic-culture-playbook">2024 culture revision</a> doubled down on "context not control." The people making decisions were not waiting for permission. They had a shared understanding of what Netflix exists to do, and a stock crash did not change that. The mission was the coordination mechanism. Not the reporting chain.</p>
<p>Amazon's leadership principles are how an L5 engineer decides what to build without asking a VP. <a href="https://en.wikipedia.org/wiki/David_Marquet">Marquet's</a> "I intend to" model on the USS Santa Fe worked because every sailor understood the submarine's mission well enough to propose action without waiting for orders.</p>
<p>Without missionary culture, subsidiarity is just another structural change that snaps back under pressure. With it, subsidiarity is self-reinforcing: every decision made locally, grounded in the mission, builds the muscle that makes the next local decision possible.</p>
<h2>The stencil</h2>
<p>This is not a playbook. It is a diagnostic you can superimpose on your own organization.</p>
<p>Are you reaching for AI, flatter org charts, or the right leader? Those are fig leaves. Where are decisions actually made (not what the org chart says, but the actual flow)? When decisions are justified, what is the grammar: "leadership wants X" or "this serves our mission because Y"?</p>
<p>You probably cannot fix your organization's governance model. But you can see it clearly, name it, and choose where to work with open eyes.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-05-02T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Suggestible Actor: A New Model for AI-Assisted Software Development]]></title>
        <id>https://srikanth.sastry.name/the-suggestible-actor/</id>
        <link href="https://srikanth.sastry.name/the-suggestible-actor/"/>
        <updated>2026-04-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Every software system is designed around an assumption about its actors; the ones who use the system, and what drives their behavior. This a...]]></summary>
        <content type="html"><![CDATA[<p>Every software system is designed around an assumption about its actors; the ones who use the system, and what drives their behavior. This assumption, the <em>actor model</em>, determines API surfaces, error handling, defaults, and guardrails.</p>
<p>There are two actor models in software design, each an archetype at one end of the <a href="/garden/intent-spectrum/">intent spectrum</a>. The well-intentioned actor on one end, the malicious actor on the other. Some systems mix both along the user journey (the login flow assumes a malicious actor, the dashboard assumes a well-intentioned one) but at any given point, the design caters to one or the other. That binary held for decades. But not anymore.</p>
<h2>The Well-Intentioned Actor</h2>
<p>This model assumes that the actor intends to use the system as designed and follow the happy path of the user journey. They want to work within the boundaries, satisfy the preconditions for calling the right APIs, and follow conventions. When they violate a rule, it is accidental and not intentional.</p>
<p>The design paradigm that follows from this model is <a href="https://ricomariani.medium.com/the-pit-of-success-cfefc6cb64c8"><strong>the pit of success</strong></a>. Make correct usage easy and incorrect usage ergonomically painful. Examples include Rust's borrow checker, builder patterns that enforce required fields, type systems that make illegal states unrepresentable, <em>etc</em>. All of these rely on the actor to interpret <a href="/garden/friction-requires-intent/">ergonomic friction</a> as a signal to stop and reassess. When a well-intentioned actor encounters resistance, they read it as: <em>I am probably doing something wrong</em>.</p>
<p>This paradigm rests on a specific assumption: the actor has <strong>judgment</strong>. They can interpret signals beyond the literal content of an error message, drawing on context and system-wide invariants. The system does not need to spell out every correct behavior; it only needs to make incorrect behavior uncomfortable, and the actor's judgment does the rest.</p>
<h2>The Malicious Actor</h2>
<p>Here, the actor's intent is adversarial. They aim to subvert, compromise, or exploit the system.</p>
<p>The design paradigm that follows from this model is <strong>the fortress</strong>. Make incorrect usage impossible. Examples include capability-based access control, sandboxing, least privilege, zero-trust architectures, etc. Ergonomic friction is irrelevant here because the adversary does not interpret friction as a warning, but as evidence that something worth protecting is nearby.</p>
<p>This paradigm rests on its own assumption: the actor has <strong>directed intent</strong>. They will study the system, map its architecture, and probe its boundaries methodically. Any defense that is merely inconvenient rather than impossible will eventually be bypassed.</p>
<h2>The Shared Assumption</h2>
<p>Both archetypes share a deeper assumption: <strong>the actor has intent</strong>. Whether aligned or adversarial, the actor is motivated by something internal. They <em>want</em> an outcome, and the system is designed as a response to that want. This has been true since the start of software engineering as a discipline. We now have a new actor that upends it: the <em>AI coding agent</em>.</p>
<h2>The New Actor: AI coding agent</h2>
<p>The <strong>AI coding agent</strong> demands a new model. The natural instinct is to place it somewhere on the intent spectrum, perhaps as a mostly well-intentioned actor with occasional problematic behavior. This is a <a href="/garden/ai-agent-category-error/">category error</a>. The entire spectrum is organized around intent, and the AI coding agent has none.</p>
<p>The AI agent has an objective, which is <a href="/garden/goal-vs-intent/">not the same thing as intent</a>. The objective is set externally by the human who dispatched it. It did not choose its objective; it was told. It has no internal motivation and no values against which to evaluate the task. It is not aligned with the designers' intent. It is not adversarial toward it. It is orthogonal to the entire axis. Both design paradigms fail for this actor.</p>
<h2>When the Paradigms Fail</h2>
<p>What happens when an AI coding agent operates in a codebase designed for the well-intentioned actor? Let's sharpen this question with an example.</p>
<blockquote>
<p>An agent is implementing a feature and runs the test suite. A test fails with <code>Access Denied</code>: an authorization system is blocking a call the new code needs to make.</p>
</blockquote>
<p>A well-intentioned human developer recognizes what this means. The authorization system is working as designed. They stop, determine which permission they need, and request access through the proper channel. They interpreted the friction correctly: <em>I don't have the right to do this yet</em>.</p>
<p>When the AI agent encounters the same error, this is just another test failure, no different in kind from a syntax error or a missing dependency. It looks for alternative paths to make the test pass. Not to compromise the authorization system (the agent has no concept of "compromise") but because that is what it does with any error: it tries to eliminate it.</p>
<p>In the best case, the agent wastes cycles on a dead end. In the worst case, it finds and exploits an actual vulnerability in the authorization system. This is not as far-fetched as it sounds. Anthropic's <a href="https://red.anthropic.com/2026/mythos-preview/">Claude Mythos Preview discovered zero-day vulnerabilities</a> that had survived 27 years of human code review, when directed to look for them. If a model pointed at security can find what 27 years of human review missed, an agent brute-forcing past <code>Access Denied</code> is not going to stop at the authorization boundary.</p>
<p>One can always claim victory by assuming the malicious actor model for all AI agents. But fortress-hardened software is difficult to read, difficult to write by hand, and expensive to operate at scale. Applying it universally makes the codebase hostile to humans and agents alike.</p>
<p>Instead of forcing the AI agent into an existing archetype, we need a model that describes how it actually behaves and a design paradigm that follows from it.</p>
<h2>The Suggestible Actor</h2>
<p>I call this actor the <strong>suggestible actor</strong>. It is defined by <a href="/garden/suggestible-actor-properties/">four properties</a>:</p>
<ol>
<li><strong>Goal-oriented.</strong> The actor has a goal that it is trying to accomplish.</li>
<li><strong>Locally reasoning.</strong> The actor only reasons over what is immediately available to it.</li>
<li><strong>Susceptible to local context.</strong> The actor's behavior is influenced by the outputs of each interaction with the system.</li>
<li><strong>Confabulates under uncertainty.</strong> When local context leaves gaps in specification or direction, the actor makes up plausible rationale. It "hallucinates."</li>
</ol>
<h3>Goal-oriented</h3>
<p>The agent always has a goal, externally set by the human who dispatched it: "implement feature X," "fix this bug," "refactor this module."</p>
<p>This is not the same as intent. Intent implies motivation: an intentional actor <em>wants</em> an outcome, understands <em>why</em> the outcome matters, and can evaluate trade-offs against their own values. The suggestible actor has none of this. It has a target, and it moves toward that target the way a heat-seeking missile moves toward a heat source: persistently, without comprehension of what it is pointed at or why.</p>
<h3>Locally reasoning</h3>
<p>The agent reasons only over what is immediately available: the contents of its context window, the file it is modifying, the output of the last command it ran. Global invariants, cross-system dependencies, and architectural constraints outside its immediate context do not factor into its decisions.</p>
<p>A human developer operates with ambient knowledge: team conventions, institutional history, an understanding of <em>why</em> the system is structured the way it is. The suggestible actor has none of this. Its understanding extends exactly as far as someone has made explicit within its local context. Even if all ambient knowledge were codified and provided, the locality of the context window would quickly obscure it.</p>
<h3>Susceptible to local context</h3>
<p>Every input the agent receives during execution (compiler errors, test results, code comments, documentation) influences its subsequent behavior. This susceptibility is not uniform. When the agent has a working path toward its goal, external inputs have relatively weak influence. When the agent is <em>stuck</em>, the next piece of feedback it encounters has outsized influence on what it does next. <strong>The agent is <a href="/garden/susceptibility-peaks-at-failure/">most susceptible at the point of failure</a>.</strong></p>
<p>This is the primary design lever. The agent's behavior can be steered, but only if guidance is placed where the agent will encounter it at the moments it is most receptive.</p>
<h3>Confabulates under uncertainty</h3>
<p>When local context is insufficient to determine a next step, the agent does not stop and request clarification. It <a href="/garden/confabulation-is-plausible/">confabulates</a>: it generates a plausible structure and proceeds as if that structure were real. A call to an API that does not exist. A convention that was never established. A security bypass that "should work based on the patterns in this codebase."</p>
<p>This is the convergent failure mode of the other three properties. The result is not random behavior. It is <em>plausible-looking wrongness</em>: output that fits the shape of what should be there, constructed from pattern and proximity, not knowledge. The danger is not that these errors are spectacular. It is that they look correct.</p>
<h2>Designing for the Suggestible Actor</h2>
<p>Neither the pit of success nor the fortress was designed for an actor without intent. The suggestible actor paradigm starts from a different assumption: the actor is <strong>susceptible to local context and confabulates when that context is insufficient</strong>.</p>
<p>Because the agent is goal-oriented but locally reasoning, a gap always exists between the goal as the human understood it and the reality the agent encounters. The human had ambient knowledge that was never made local. This <a href="/garden/directive-gap/">directive gap</a> is the root cause of most suggestible-actor failures. The prescriptions below are all strategies for closing it.</p>
<h3>Make every error a call to action</h3>
<p>Error messages are the most effective steering mechanism available for the suggestible actor.</p>
<p><code>403 Forbidden</code> tells the agent nothing actionable.</p>
<p><code>403 Forbidden: identity 'svc-deploy' lacks 'write:documents' scope. Request access at https://console.example.com/api-keys or use a key with admin privileges.</code> gives the agent an actionable next step at the exact moment it is most receptive to one.</p>
<p>Principle: Treat error surfaces as the primary API for the suggestible actor.</p>
<h3>Replace soft boundaries with hard boundaries plus signposts</h3>
<p>Deprecation warnings that hope the developer will migrate. Abstract classes that trust no one will instantiate them. Internal APIs relying on the convention "you shouldn't use this." These are boundaries enforced by social contract. The suggestible actor does not read social contracts. It walks through the "DO NOT ENTER" sign because the door was unlocked.</p>
<p>For boundaries that matter, make them genuinely impassable (compile-time enforcement, runtime rejection, capability restrictions), then attach a signpost telling the agent what to do instead. For boundaries not worth enforcing, the suggestible actor will cross them. They are not boundaries anymore. Accept them as part of your system's state space.</p>
<p>Principle: Only hard boundaries exist and when they are hit, provide clear alternatives.</p>
<h3>Write documentation and conventions as if they will be executed</h3>
<p>To steer the agent, documentation must exist within its local context: inline comments adjacent to the code it will modify, docstrings on the functions it will call, unit test failure messages, READMEs precise enough for the agent to follow step by step. It will follow your docs more literally than most humans will.</p>
<p>The same applies to conventions. The suggestible actor cannot absorb norms through osmosis. Project templates, linters, and consistent directory structure encode convention at the tooling level. The agent complies with linters because violations are errors, and errors are the feedback it is most susceptible to.</p>
<p>Principle: Comments in code are vectors for prompt injection. Specifications are implementation contracts. Be explicit.</p>
<h3>Close the directive gap</h3>
<p>When the directive gap is wide, the agent confabulates. Close it.</p>
<p>CI/CD gates should report not just what failed but what to do about it. Pre-commit hooks should provide the correct alternative, not just reject the incorrect one. An <code>AGENTS.md</code> or <code>CONTRIBUTING.md</code> should encode the ambient knowledge that a human developer would carry. Example code near the API surface, type signatures that make the correct shape unambiguous, factory methods with correct defaults, named parameters that make intent explicit at the call site: all of these make the correct answer locally available so the agent never needs to invent one.</p>
<p>Principle: <a href="/garden/ambient-to-local/">Convert ambient knowledge into local context</a>.</p>
<hr />
<p>The suggestible actor is already operating in your codebase. It is calling your APIs, reading your documentation, and hitting your error messages. It has no intent to respect your design philosophy and no judgment to interpret your ergonomic signals.</p>
<p>But it is susceptible to local context. And that is a lever.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-04-24T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Tech Companies and Directive Governance: A Situationship]]></title>
        <id>https://srikanth.sastry.name/directive-governance-situationship/</id>
        <link href="https://srikanth.sastry.name/directive-governance-situationship/"/>
        <updated>2026-04-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Most large tech companies operate top-down. Information flows up through a reporting chain. Decisions are made centrally. Directives flow ba...]]></summary>
        <content type="html"><![CDATA[<p>Most large tech companies operate top-down. Information flows up through a reporting chain. Decisions are made centrally. Directives flow back down. This is true regardless of what their culture decks say. Unfortunately, it is the wrong way to govern for software engineering. Top-down governance works when information flowing upstream is compressible, the metrics that decision makers see are a good proxy for org health and success, and decision-making and execution are distinct from each other. None of these hold for software engineering. I have detailed all the wrongness in <a href="https://srikanth.sastry.name/cargo-cult-governance/">"Cargo Cult Governance".</a></p>
<p>If you take my claim of wrongness at face value, the natural follow-up question is the one my nine-year-old asks regularly: "why?" If the structural mismatch is that clear, and the existence proofs are that abundant, why don't large tech companies change?</p>
<p>Before going into the "Why", we need to answer the "What?". Specifically, what is "directive governance"?</p>
<h2>The "what": Directive governance</h2>
<p>The colloquial term for this is "<a href="/garden/command-control-misnomer/">command and control</a>." That term is imprecise and means different things to different people. What dominates the tech industry is something more specific. Burns and Stalker come close with the notion of <a href="https://en.wikipedia.org/wiki/Mechanistic_and_organic_systems">mechanistic organization</a> in 1961: rigid hierarchy, top-down decision-making, formal procedures, accountability through the chain of command.</p>
<p>In practice, tech companies are not purely mechanistic. They bolt on organic elements: hackathons, "innovation time," autonomous-team branding. The mechanistic core stays intact. I call this <strong><a href="/garden/directive-governance/">directive governance</a></strong>:</p>
<blockquote>
<p><strong>Information flows up</strong> (compressed and possibly lossy). <strong>Decisions are made centrally</strong> based on whatever survives the trip. <strong>Directives flow back down</strong> for execution. <strong>Accountability is for compliance</strong>: did you execute the directive? Not: did you achieve the outcome?</p>
</blockquote>
<p>Directive governance works when the three preconditions I mentioned earlier hold: information is compressible without critical loss, quantitative metrics correlate with reality, and decision-making is separable from execution. In manufacturing, these hold. In software, they structurally don't.</p>
<p>So... why don't companies switch? The answer is a <a href="/garden/crisis-centralization-ratchet/">ratchet</a>.</p>
<h2>The "Why": The ratchet</h2>
<p>The ratchet hypothesis.</p>
<blockquote>
<p>Tech companies centralize quickly during crisis and decentralize very slowly afterward.</p>
</blockquote>
<p>The asymmetry has three layers, and they compound.</p>
<p><strong>Layer one: mechanical asymmetry.</strong> Centralizing is a directive. "All decisions go through me now." That can happen overnight. Decentralizing is a culture. It requires building judgment, trust, and context at every level of the hierarchy. The transition to decentralization itself requires some level of decentralization. Culture takes years.</p>
<p><strong>Layer two: loss aversion.</strong> Even when organizations are no longer in crisis, can afford to decentralize, <em>and see the benefits of it</em>, they do not start. Because being caught mid-transition when the next crisis arrives feels worse than staying centralized. A fully centralized org can respond quickly, even if the response is wrong. A half-decentralized org has neither the speed of centralization nor the distributed judgment of full decentralization. So you wait. And the next crisis arrives. And the waiting becomes permanent.</p>
<p><strong>Layer three: competitive pressure as the only loosening force.</strong> Peacetime loosening is not voluntary. It is forced by upstarts that are nimble and innovative. When smaller competitors are shipping faster and stealing talent, the pressure to federate becomes hard to ignore. But because it is reactive rather than deliberate, it produces shallow structural changes. Skunkworks. Federated org charts. Squad models. Autonomous teams that still need VP sign-off. The structure gets decentralized. The culture does not; remember, it can take years. That is why it snaps back the moment crisis returns.</p>
<p>The three layers compound. The transition is mechanistically slow, psychologically avoided, and when it happens at all, shallow and reversible. If crises come faster than the loosening rate, centralization accumulates.</p>
<p>The ratchet makes a falsifiable prediction.</p>
<blockquote>
<p>Long peace produces observable loosening. Frequent crises produce persistent centralization.</p>
</blockquote>
<p>The evidence fits this prediction. Consider the last fifteen years in the tech industry. From 2012 to 2018, a long peace produced observable loosening. Facebook was federated: "move fast and break things," teams shipping independently. Google let teams launch products with minimal central approval. Nadella took over Microsoft in 2014 and killed the stack ranking system that had crippled the company for a decade. Peacetime loosening, exactly as the ratchet predicts. Some products from that era failed (Google Allo, Amazon Fire Phone, Google Glass, Meta Portal, etc.), and others broke new ground (Google Cloud and Microsoft Azure, the <a href="https://arxiv.org/abs/1706.03762">Transformer paper</a>, etc.). You do not get to cherry-pick the hits without accepting the misses. The product graveyard is not evidence of federation failing. It is the cost of federation succeeding.</p>
<p>Then came the crises: Trade tensions, COVID, the hiring binge and the correction, AI panic, layoff waves. Every eighteen to twenty-four months, another shock. Zuckerberg's "year of efficiency." Google's layoffs. Meta's flattening. The loosening snapped back instantly because it was structural, not cultural. A competitive response, not a deliberate transformation.</p>
<p>The organizations that practice federated decision-making (Amazon, Netflix, Toyota, mission command) escaped all three layers. They invested in culture before they needed it. They did it deliberately and deeply. They maintained it long enough for the slow process to take hold. Their decentralization does not snap back under pressure because it is not shallow. Netflix is the clearest test. Stock crashed 70% in 2022. They laid off staff. They did not re-centralize. Their <a href="https://www.hrgrapevine.com/us/content/article/2024-06-26-netflix-announces-rare-revision-of-iconic-culture-playbook">2024 culture revision</a> doubled down on "context not control."</p>
<h2>What makes it stick</h2>
<p>The ratchet is the spine. Four forces make it stickier.</p>
<p><strong>Serial <a href="https://en.wikipedia.org/wiki/Satisficing">satisficing</a> <a href="/garden/serial-satisficing-without-learning/">without learning</a>.</strong> Hire aggressively in 2021. Lay off aggressively in 2023. Pivot to AI in 2023. Each correction is presented as the rational fix to the previous bounded decision. But the claim that any correction is rational is <a href="/garden/unfalsifiable-organizational-corrections/">not falsifiable</a>. Neither is the claim that it is wrong. That is the point. Nobody checks. Nobody builds the feedback mechanisms that would let you check next time. <a href="https://www.gsb.stanford.edu/insights/why-copycat-layoffs-wont-help-tech-companies-or-their-employees">Pfeffer's research</a> provides the closest thing to empirical traction: companies that did not lay off performed equally well. The honest version of the earnings call: "We are making this correction with equally incomplete information, and we have no way of knowing if it is better than what it replaced." Nobody says that.</p>
<p><strong>Institutional inertia.</strong> The companies are profitable. The stock is up. Directive governance is not producing visible failures. Why would anyone champion a multi-year cultural transformation with uncertain payoff? The ratchet provides cover during crisis: centralization is plausible enough to be defensible. In peacetime, the status quo is plausible enough to be comfortable. Nobody fixes what appears to work.</p>
<p><strong>Incentive structures.</strong> Meta recently tied <a href="https://corpgov.law.harvard.edu/2026/04/10/metas-new-executive-pay-plan-ties-nearly-1-billion-to-stock-performance/">nearly $1 billion in executive compensation to stock price targets</a>. Not innovation rate. Not decision quality. Not talent retention. Stock price. The CTO, CPO, COO, and CFO all hold options that pay out only if market capitalization hits specific thresholds. This is not unusual. It is the norm. Executive compensation rewards what directive governance can produce: revenue, cost cuts, market cap. It does not reward what decentralization would improve.</p>
<p><strong>The Spotify illusion.</strong> Companies that claim to have decentralized but have not. Spotify's squad model was <a href="https://www.jeremiahlee.com/posts/failed-squad-goals/">"part ambition, part approximation."</a> Co-author Joakim Sundén later admitted people struggled to copy "something that didn't really exist." Spotify itself transitioned back to traditional management. This is the shallow loosening the ratchet predicts: <a href="/garden/delegation-mimicry-without-cultural-substrate/">structural change without cultural change</a>. It looks like adaptation without being adaptation.</p>
<p>These forces do not operate independently. They feed the ratchet. Serial satisficing provides the post-hoc justification for each centralization. Inertia keeps the status quo comfortable. Incentives make change financially unrewarding. The Spotify illusion lets companies claim they have changed when they have not. Together, they ensure that even when competitive pressure forces loosening, the loosening stays shallow.</p>
<h2>How does it make $$$</h2>
<p>If directive governance is that broken, why are these companies worth trillions?</p>
<p>Because bad governance is <a href="/garden/directive-governance-degrades-not-destroys/">a tax on performance, not a death sentence</a>. When you have a search monopoly, network effects, or ecosystem lock-in, the monopoly rents absorb the tax. Microsoft lost a decade of market cap under Ballmer and emerged just fine. Google missed the boat on generative AI, despite authoring the seminal Transformer paper, and lost billions playing catchup. Apple and Meta spent billions on VR headsets that have yet to find a market.</p>
<p>The point is not that directive governance leads to death or bankruptcy. The point is that outward success does not mean the organization is healthy. The failure mode for most large tech companies is not death. It is languishing. Profitable enough to survive. Too poorly governed to innovate. The best engineers leave for younger companies where they can make decisions. The products get incrementally worse. The stock price holds up long enough that the board never forces the issue.</p>
<p>The urgency here is personal, not institutional. You can work inside directive governance and function. The company will survive either way. Directive governance ties your ability to drive innovation to the grace of your leader. That grace can be snatched away overnight.</p>
<p>Nadella's Microsoft is the rare counterexample: a deliberate, decade-long cultural investment that has survived multiple crises. The rarity is the point. The escape requires investment on a timeline that exceeds most executive tenures, in a discipline most leaders have never practiced.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-04-16T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Cargo Cult Governance]]></title>
        <id>https://srikanth.sastry.name/cargo-cult-governance/</id>
        <link href="https://srikanth.sastry.name/cargo-cult-governance/"/>
        <updated>2026-03-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[In the tech industry, we have been through a corporate rollercoaster in the last few years. First, it was the hiring mania during COVID, fol...]]></summary>
        <content type="html"><![CDATA[<p>In the tech industry, we have been through a corporate rollercoaster in the last few years. First, it was the hiring mania during COVID, followed by widespread layoffs starting 2023. Then there is the pivot at AI, followed by the "flattening" of middle management. Regardless of whether you were laid off, or you carry the survivor's guilt, or you are shoveling AI slop to get to something useful, the mental scars are very real. So is the cynicism that the leadership may not know what it is doing. The decisions feel callous, short-sighted, even whimsical, and based on <a href="/garden/data-pipeline-is-achilles-heel/">wildly inaccurate information</a>.</p>
<p>But does it <em>have</em> to work this way? What's actually driving these decisions, and is there a better mechanism? Because if the answer is "this is just how large companies work," that's one kind of problem. If the answer is "there's a specific, diagnosable flaw in how these decisions get made", then that's a different one. One that might be fixable.</p>
<h2>The mechanism: directive governance</h2>
<p>The governance model that dominates the technology industry is what I call <em>directive governance</em>: information flows up through a reporting chain (compressed and lossy), decisions are made centrally, and directives flow back down for execution. Directive governance is how most large tech companies are actually run, regardless of what their culture decks say. (I formalize this definition in <a href="/directive-governance-situationship/">a follow-up article</a>.)</p>
<p>And there is a reason for that. Directive governance has been very successful in a myriad of industries and organizations: pharmaceutical development, aviation, manufacturing, and even many parts of the military. What we see in the tech industry is a form of <a href="/garden/isomorphic-mimicry-in-tech-governance/">isomorphic mimicry</a>; if it works in those areas, then it should work here too.</p>
<p>However, if you stop to ask why exactly it works in those industries, you start to see the fallacy in <a href="/garden/directive-governance-cargo-cult/">this mimicry</a>. In pharmaceutical development, clinical trial data is structured and quantifiable. The information that matters can travel up the chain without losing its meaning. In aviation, decades of failure analysis have produced checklists and procedures that genuinely capture what matters. The gap between what the front line knows and what leadership sees is narrow by design. In manufacturing, defect rates and throughput are real proxies for operational reality. Cost per unit correlates with what's actually happening on the floor. And the person who designed the part is genuinely distinct from the person who fabricates it to spec.</p>
<p>Briefly, directive governance works really well when information for decision making is highly compressible without losing fidelity, is verifiable, and the decisions are clearly separable from their execution. These conditions are favorably satisfied in the industries mentioned earlier, and hence the success of directive governance in these spheres.</p>
<p>But the tech industry doesn't conform to <a href="/garden/directive-governance-preconditions/">these conditions</a>, and therein lies the problem; the reason directive governance works poorly here.</p>
<h2>The damage</h2>
<p>This isn't theoretical. The wreckage is visible and well-documented.</p>
<ul>
<li>During Microsoft's <a href="https://www.vanityfair.com/news/business/2012/08/microsoft-lost-mojo-steve-ballmer">lost decade</a>, stack ranking destroyed collaboration across the company. It was a centralized performance system that forced bell-curve grading. As a result, employees optimized against each other instead of for the product. Market cap fell from $580 billion to $249 billion. Leadership could see attrition rates and shipping dates. They could not see the innovation that wasn't happening.</li>
<li>In 2011, Google made the top-down decision to compete with Facebook on social and mandated that all teams across Google integrate Google+ into their products. Decision was made, and the teams were expected to execute. But "integrate social" is not a specification. Each team made their own decisions about what integration meant for their product. The decisions that determined whether the product would be coherent were not made by Google leadership. They were made by dozens of teams independently, at the execution layer, with no mechanism to coordinate them. The result was a Frankenstein. Google+ was shut down in 2019.</li>
<li>After Elon Musk laid off roughly 80% of Twitter's staff, <a href="https://www.platformer.news/how-a-single-engineer-brought-down/">a single remaining SRE</a> made a configuration change that broke the entire platform: links, images, internal tools, everything. "You may not see negative effects immediately," NYU's Justin Cappos <a href="https://engineering.nyu.edu/news/what-twitters-outage-says-about-over-zealous-downsizing">observed</a>. "A month later you start to take a hit, and then the wheels start to fall off." Musk couldn't see which engineers were load-bearing because their contributions didn't show up in the metrics visible from the top.</li>
<li>In March 2026, OpenAI <a href="https://petapixel.com/2026/03/24/openai-kills-sora-and-loses-disneys-1b-investment/">killed Sora</a>, its video generation tool, because it was a "distraction". The pivot to ChatGPT-first left the Sora and DALL-E teams starved and feeling like <a href="https://www.digit.in/features/general/openai-ignoring-research-sora-and-dall-e-suggest-people-leaving-chatgpt-maker.html/amp/">second-class citizens</a>. The result was an exodus of significant talent from OpenAI. In summary: centralized leadership greenlit a scatter of products, discovered the strategy was incoherent, and corrected with another centralized decision. The cost wasn't just a cancelled product. It was the people who walked out the door.</li>
</ul>
<p>You have seen some version of this play out in your own workplace. It might be at a lower scale and with lower stakes, but the pattern is the same. What you might not have seen is the mechanism that produces it.</p>
<h2>When does directive governance break?</h2>
<p>Directive governance rests on <a href="/garden/three-assumptions-framework/">three implicit assumptions</a> about the information pipeline connecting the people who decide to the people who do:</p>
<ol>
<li><strong>Compression.</strong> When information is summarized upward, the compression preserves the signal that matters.</li>
<li><strong>Proxy validity.</strong> The quantitative metrics available to decision-makers correlate with the reality they're managing.</li>
<li><strong>Separability.</strong> Decision-making and execution are distinct activities that can be cleanly divided between levels of the hierarchy.</li>
</ol>
<p>These assumptions hold in many industries. In manufacturing, summarizing production into throughput metrics <em>does</em> preserve what matters. Cost per unit <em>does</em> correlate with operational reality. And the engineer who designed the part is genuinely distinct from the worker who fabricates it to spec.</p>
<p>But these don't hold true in tech. To understand why, we need to go back to Hayek and Austin. <a href="https://www.econlib.org/library/Essays/hykKnw.html">Hayek recognized</a> that in any complex system, knowledge is often inherently distributed, tacit, and contextual. It resists centralized aggregation by its nature. And Robert Austin <a href="https://www.oreilly.com/library/view/measuring-and-managing/9780133488425/">demonstrated in 1996</a> that if only some dimensions of work are measurable, then <a href="/garden/partial-measurement-worse-than-none/">measurement-based management actively degrades what cannot be measured</a>. The combination of these two conditions in the tech industry invalidates the three assumptions above.</p>
<h2>Why tech specifically</h2>
<p>In 1986, Fred Brooks drew a distinction in <a href="https://www.cin.ufpe.br/~phmb/ip/MaterialDeEnsino/BrooksNoSilverBullet.html">"No Silver Bullet"</a> that remains true despite all the advances in software engineering: software has <em>essential</em> complexity (the irreducible difficulty of the problem itself) and <em>accidental</em> complexity (the incidental difficulties of our tools and processes). Tools can attack accidental complexity. They cannot touch essential complexity, because it <em>is</em> the problem.</p>
<p>This argument <a href="/garden/essential-complexity-makes-software-ungovernable/">extends to governing the people who build it</a>. When you apply it there, all three assumptions collapse.</p>
<p><strong>Compression fails.</strong> Essential complexity is irreducible by definition. You can compress "we shipped 15 features this quarter" into a slide, but you cannot compress "we chose this abstraction boundary because of how three subsystems will need to evolve independently over the next two years" into anything a non-participant can evaluate. The compression directive governance requires strips precisely the signal that matters. This goes back to Hayek's observation about knowledge's resistance to centralization.</p>
<p><strong>Separability fails.</strong> This is where software diverges most sharply from other industries. In manufacturing, you do the same thing repeatedly. The design decision was already made, and execution follows a spec. Micro-decisions on the line are local and ephemeral. They don't compound.</p>
<p>Everything you build in software is new (if it weren't, you'd just call the API that already does it), and consequently, the act of building software itself is decision making: choosing an abstraction, defining an interface, decomposing a system. And unlike manufacturing, software decisions compound. Every abstraction choice constrains every future choice built on top of it. A manufacturing micro-decision lives and dies in the moment. A software decision shapes the codebase for years.</p>
<p>In software, <a href="/garden/in-software-execution-is-decision-making/">execution <em>is</em> decision-making</a>. Directive governance depends on a separation between the two that doesn't exist.</p>
<p><strong>Proxy validity fails.</strong> <a href="/garden/metrics-measure-maintenance-not-creation/">The metrics that survive the reporting chain</a> (uptime, sprint velocity, cost per headcount) track what keeps the lights on. They don't track what makes the company thrive. Innovation, architectural soundness, the quality of an abstraction, whether a team's trajectory is sustainable: none of these fit in a dashboard. As per Austin's observation, the metrics don't just miss creativity and innovation; they actively redirect effort away from it and toward maintenance.</p>
<p>The tech industry fails these three assumptions structurally and inherently, and the problem is only getting more acute. All of our advances in software engineering (Agile, CI/CD, cloud infrastructure, AI-assisted coding) <a href="/garden/tooling-advances-prove-brooks-right/">serve only to eliminate accidental complexity</a>. This leaves the essential complexity to dominate the signal loss in upward communication and to force leveraged decision-making in execution, while continuing to remain in the blindspot like a ghost moving the metrics.</p>
<h2>The structural implication</h2>
<p>If directive governance fails because the tech industry is structurally not convivial to it, then we need structural changes to how decisions are made. The structural fix here is that <a href="/garden/subsidiarity/">decisions get made where the information actually lives</a>. But <a href="/garden/subsidiarity-is-not-flat-organization/">subsidiarity is not the same as flattening the org chart</a>.</p>
<p>Does that mean we should 'flatten' the org chart? Is all this talk of 'flattening' really going somewhere? Sigh. I wish. It has been tried, and it produces its own pathologies. Valve's famous flat structure <a href="https://www.pcgamer.com/valves-flat-structure-contains-hidden-layer-of-powerful-management-claims-ex-employee/">concealed a hidden hierarchy</a> of informal cliques. Jo Freeman <a href="https://www.jofreeman.com/joreen/tyranny.htm">diagnosed this dynamic in 1970</a>: <a href="/garden/structurelessness-hides-hierarchy/">structurelessness doesn't prevent hierarchy, it prevents <em>accountable</em> hierarchy</a>. The loudest and most politically savvy end up in charge, with no formal mechanism for review or appeal. Spotify's squad model <a href="https://www.jeremiahlee.com/posts/failed-squad-goals/">never actually worked at Spotify</a>. "Even at the time we wrote it, we weren't doing it," co-author Joakim Sundén admitted. When Zappos adopted holacracy and gave employees an <a href="https://www.entrepreneur.com/business-news/looks-like-zappos-self-management-system-isnt-for-everyone/246076">ultimatum to embrace self-management or leave</a>, 14% of the company walked out the door. Eliminating hierarchy doesn't solve the information problem. It just makes power invisible.</p>
<p>But there are organizations, across industries, at massive scale, that have kept hierarchy while relocating decision authority within it.</p>
<ul>
<li>Toyota gives any assembly line worker the authority to <a href="https://www.amazon.com/Toyota-Way-Management-Principles-Manufacturer/dp/0071392319">pull the <em>andon</em> cord</a> and stop the entire production line when they spot a defect. Why? Because the worker has specific knowledge no dashboard can capture.</li>
<li>Amazon scales by <a href="https://workingbackwards.com/concepts/amazon-single-threaded-teams/">multiplying small teams</a>, not layering hierarchy. The "two-pizza team" is small enough that one leader can have full context. It is directive governance at a scope where the information precondition actually holds, federated across thousands of teams.</li>
<li>Netflix operates on the <a href="https://www.amazon.com/dp/1984877860">explicit principle</a> that leadership's job is to communicate <em>what</em> and <em>why</em>; the people doing the work decide <em>how</em>.</li>
<li>Warren Buffett runs Berkshire Hathaway, a $900 billion conglomerate, with <a href="https://www.berkshirehathaway.com/letters/letters.html">roughly 30 people at headquarters</a>. He handles capital allocation, where a bird's-eye view helps. Subsidiary CEOs handle everything else, where local knowledge is what matters.</li>
<li>And the US Army, an institution built on hierarchy and obedience, formalized <a href="https://www.army.mil/article/225414/combined_arms_center_launches_new_mission_command_doctrine">mission command</a>: commanders communicate intent, subordinates decide execution. L. David Marquet <a href="https://www.amazon.com/Turn-Ship-Around-Turning-Followers/dp/1591846404">transformed the USS Santa Fe</a> from the worst-performing submarine in the fleet to the best by replacing "permission to" with "I intend to."</li>
</ul>
<p>Notice that none of these examples eliminated hierarchy. They all redesigned where decisions happen within it. The tech industry didn't need to discover a new governance structure. It just needs to snap out of its dogma and stop ignoring what works.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2026-03-26T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[The grand flattening: AI Slop is just the next step]]></title>
        <id>https://srikanth.sastry.name/the-great-flattening-ai-slop/</id>
        <link href="https://srikanth.sastry.name/the-great-flattening-ai-slop/"/>
        <updated>2025-10-10T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Reality is an entity of vast, irreducible complexity. It is far more than the human mind can grasp, yet we are forced to operate within it...]]></summary>
        <content type="html"><![CDATA[<p>Reality is an entity of vast, irreducible complexity. It is far more than the human mind can grasp, yet we are forced to operate within it. To cope, we rely on simplified models and simulations; essentially, shorthand versions of the world that fit inside our heads. The problem is fidelity. Eventually, the model breaks, and we are forced to confront phenomena we didn't account for and don't know how to handle.</p>
<p>Humanity's response to this problem has not been improvement alone. Each advance in our models brought with it a particular hubris: the conviction that <em>this time</em>, the map was complete; that what couldn't be captured was simply not worth capturing. And it is precisely that conviction that licenses the coercion. If the model is complete, then deviation isn't a sign of the model's limits. It is a sign of reality's defects.</p>
<p>Better maps didn't reduce the impulse to redraw the territory. They justified it.</p>
<p>To eliminate these 'edge cases,' humanity has spent millennia on a grand project: forcing reality to conform to a model we can predict and control. Philosophers have both fueled this project and warned of its side effects, warnings we have summarily ignored. The culmination of this effort is the 'AI Slop' currently inundating us. An ironic final step in subverting our perception of reality itself.</p>
<h2>The legibility project of the pre-modern era</h2>
<p>Humans tend to be "illegible". They are complicated, and diverse. Every group has its own customs, traditions and morality. Controlling and ruling over such an illegible group is near impossible. So this grand project started millennia ago as a mechanism to control people by reducing their illegibility. By making them legible. By flattening their complexity and diversity into a 'compressible' set of behaviors. The first recorded efforts in this direction are the <a href="https://en.wikipedia.org/wiki/Code_of_Hammurabi">Code of Hammurabi</a> and the <a href="https://en.wikipedia.org/wiki/Manusmriti">Manusmṛti</a>.</p>
<p>Yes, these are known to be the first legal texts, but then again, a legal system essentially is a compression algorithm for human behavior; its goal is to reduce a diverse population to a predictable, manageable set of outputs. Of course, there were errors in predictions of these models, and such errors are referred to as "crimes" and there are entire institutions dedicated to "correcting" such errors, by not improving the model, but by coercing human behaviors to fit the model. "Justice" was really about systemic control.</p>
<p>Of course, due to limitations of technology, the model had incredibly low resolution, and it sought to model only the human behaviors that needed control within the confines of the day's political sovereignty. For the most part, these models left the natural world and our inner worlds alone.</p>
<p>As the project matured, the philosophers were on a mission to build a <em>descriptive</em> model of reality. Plato's Theory of Forms modeled the world's diversity as mere 'noise' deviating from a perfect ideal. Aristotle provided the methodology for deconstructing reality into 'silos of legibility' under the fatal assumption that nothing of value is lost in the gaps. Mathematicians such as Aryabhata and Brahmagupta created descriptive maps to navigate the heavens. However, the impulse toward a <em>prescriptive</em> reality was already visible in the shadows. It lived in Astrology, which forced human destiny to fit a celestial map, and in the sale of Indulgences, which downsampled the infinite complexity of sin into a quantifiable financial transaction. The pivot to a world coerced to conform to the map was not a new idea, but it could not be realized at scale until better technology came along.</p>
<h2>The objectivity of modernity</h2>
<p>The renaissance and modernity introduced us to the concept of <em>objectivity</em>: the notion that things are true regardless of a subject. The philosophers of this age viewed reality as an object to be observed, dissected, studied. And somehow, we could do it objectively, as if we weren't part of this object we were studying. This paradigm alienated us from our own existence. This alienation allowed philosophers to turn this gaze of objectivity inward into our own lives and how we relate to each other; into our inter-subjectivity. They dissected how we relate to each other, and how we work together to produce goods and make progress. This was categorized and studied with ever more precision. We had new categories to peer into. There was economics and there was psychology and there was political science and there was ethics. Almost as if each of them had nothing to do with each other, and pursued their own investigations to get to their objective truths.</p>
<p>It wasn't long before this inward gaze of economics turned onto human work, and it didn't see people. It saw <em>functions</em>. A watchmaker, viewed through the economic lens, was not a person embedded in a tradition, a community, a set of relationships. He was a bundle of discrete, separable processes: material procurement, part fabrication, assembly, quality control, distribution. The model couldn't perceive anything it couldn't categorize. And what it couldn't perceive, it treated as if it didn't exist.</p>
<p>This was the Industrial Age: a period of <em>selective blindness</em>. The watchmaker didn't disappear because someone chose to erase him. He disappeared because the model looking at him had no category for what he actually was. We didn't just make more watches; we created a world where the human was only allowed to exist as a low-resolution component of a larger machine.</p>
<h2>Ontological Blinders of the information age</h2>
<p>The 20th century provided the necessary technologies to unify the balkanized silos of modernity. Through the work of Alan Turing, John von Neumann, and Claude Shannon, the messy kinetics of physical reality were recast as pure information processing. "Process Efficiency" was replaced by "Algorithmic Optimization."</p>
<p>We resurrected Plato's theory of Forms, but the forms were now idealized mathematical models. Any deviation from the model became a systematic 'error' that needed rectification. For instance, the nuances and peculiarities around the problems of routing trains between cities, laying down water and sewer pipes in a neighborhood, and moving data packets around a network were all 'unified' by the same optimization algorithms, and in that process those very same nuances and peculiarities were completely marginalized. A missed package was no longer a logistical accident: it was an "error" requiring more "fault tolerance." A worker calling in sick was no longer a human event: it was a "node failure" requiring a "redundancy" patch. These changes happened in the background of our lives, hidden by the perceived convenience of the tools.</p>
<p>The insidious turn occurred when the model overrode the reality. The diversity of the world was rebranded as "noise" that failed to map to the model, rather than the model failing to map to the world. Algorithms started changing human behavior so that it remains compliant with the model's expectations.</p>
<p>The upshot is a society that has mistaken the models for reality. The mask has become the face. You see it in social media where a curated "Instagram life" is accepted as a true representation of existence. You see it in the economy, where macro-economic abstractions like GDP are deemed more "real" than actual economic health. We have now flattened ourselves to be legible to these models. We optimize our lives to improve a credit score as if the score were the reality. "Pics or it didn't happen" is the demand for algorithmic validation of our own subjectivity. We have become ontologically blind to anything that cannot be accounted for by the model.</p>
<h2>The Manufactured Reality of the Intelligence Age</h2>
<p>The 21st century introduced the ultimate agent of the Grand Project: Generative AI. This technology finally detaches the Map from the Territory. But it does so in a way that is categorically different from everything that came before it.</p>
<p>Previous technologies mediated reality. The photograph selected a frame. Television broadcast a produced version of events. Social algorithms surfaced a curated slice of human expression. In each case, the underlying reality was still there, generating the inputs. The mask had become the face; but there was still a face underneath.</p>
<p>Generative AI breaks this relationship entirely. It does not compress reality. It bypasses it. The inputs to a large language model are not live signals from the world; they are prior compressions: text, images, and records of what humans said and made, after already passing through every filter described above. The model trains on the averaged residue of a civilization that had already been flattening itself for centuries. It then generates new outputs optimized for coherence with that averaged signal; maximally legible, frictionlessly consumable, scrubbed clean of the noise that makes any particular perspective distinct from the statistical mean.</p>
<p>We call the result "AI Slop." It is a pejorative that describes the soulless, uncanny nature of these creations. Yet, we cannot stop consuming it. We are addicted to it because it is the path of least resistance. It is content with the highest possible fidelity to the model and the lowest possible fidelity to any individual reality. It has no author, no context, no stake. It is the signal of the average; which is to say, the signal of no one.</p>
<p>We consume it anyway, and at scale. Not because we are foolish, but because this entire arc has been progressively reducing our tolerance for friction, for illegibility, for the effort that genuine encounter with reality requires. AI Slop is not a cause. It is a symptom of a sensory system that has been recalibrated, over centuries, to mistake the model for the thing.</p>
<p>The momentum driving this is 2,500 years in the making. From Hammurabi's codes to Shannon's information theory, every step has iteratively eliminated the human element as an "inefficiency." We are now so far immersed in this episteme that we have lost the ability to distinguish the mask from the face. Previously, the mask became the face. Now there are no more faces. Only masks.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2025-10-10T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Defense in Depth vs Locality of Behavior]]></title>
        <id>https://srikanth.sastry.name/defense-in-depth-vs-locality-of-behavior/</id>
        <link href="https://srikanth.sastry.name/defense-in-depth-vs-locality-of-behavior/"/>
        <updated>2025-07-14T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Defense-in-depth—borrowed from military and security strategy—means layering safeguards so that if one fails, another takes over (Cloudflare...]]></summary>
        <content type="html"><![CDATA[<p>Defense-in-depth—borrowed from military and security strategy—means layering safeguards so that if one fails, another takes over (<a href="https://www.cloudflare.com/learning/security/glossary/what-is-defense-in-depth/">Cloudflare</a>, <a href="https://en.wikipedia.org/wiki/Defense_in_depth_(computing)">Wikipedia</a>). Defensive programming is a software take on the same idea: add checks and fallbacks so bugs don't escalate.</p>
<p>Meanwhile, the principle of <a href="https://alexkondov.com/locality-of-behavior-react/">locality of behavior</a> (or “locality of behaviour” in htmx) says that "the behavior of a unit of code should be obvious by looking only at that unit" [<a href="https://htmx.org/essays/locality-of-behaviour/">source</a>]. It draws on older ideas of cohesion: keep related logic together.</p>
<p>So when should you favor layering defenses, and when should you co-locate behavior? This isn’t that post. Instead, here’s a story about how leaning on defensive programming without scrutiny let a critical bug stay hidden for far too long.</p>
<p><strong>Background: minor and major compaction.</strong> I was working on a big data system that performed repeated mutations on datasets via commits. Over time, reading slowed down—each read had to apply more mutations. To fix this, my service relied on cheap minor compactions. But unbeknownst to me, there was a fallback: a slow, expensive major compaction if too many mutations piled up. (See https://orc.apache.org/docs/acid.html.)</p>
<p><strong>Unexpected failures.</strong> Suddenly, my service slowed down and sometimes timed out. Digging in, I found it was triggering major compactions. These were so costly that jobs exceeded timeouts and got killed.</p>
<p>Asking around, I learned this was an intentional fallback. It was a defensive programming safeguard in case minor compactions failed. Everyone thought this was great resilience.</p>
<p><strong>Increasingly brittle.</strong> Then I asked: why did minor compactions fail in the first place? Silence. No alerts, no monitoring; no one knew.</p>
<p>Logs revealed major compactions had been quietly running on small datasets for ages due to a bug in minor compaction discovery. The problem stayed hidden because small datasets finished quickly. When larger ones arrived, everything blew up.</p>
<p>Ironically, the fallback meant we never fixed the root issue. Our supposed resilience made the system more fragile.</p>
<p><strong>Could we have seen this coming?</strong> If major compactions didn’t exist—or at least raised an alarm every time—they’d have forced us to fix minor compactions long ago, before the blast radius grew.</p>
<p>So next time you violate locality of behavior for defense-in-depth, think hard. And always alert aggressively when deeper defenses kick in.</p>
<p>Building on these well-trodden ideas, this incident is just one more caution: <a href="/garden/defense-in-depth-needs-visibility/">defense-in-depth only works if every fallback is visible and monitored</a>. Otherwise, your “resilience” may just be hiding decay.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2025-07-14T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[When Backward Compatibility Can Rescue a Leaky Abstraction]]></title>
        <id>https://srikanth.sastry.name/backward-compatibility-where-you-dont-expect/</id>
        <link href="https://srikanth.sastry.name/backward-compatibility-where-you-dont-expect/"/>
        <updated>2025-07-02T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[I ran into one of those delightful bugs that only show up in dynamic task generation of your data pipelines — the kind that teach you how a...]]></summary>
        <content type="html"><![CDATA[<p>I ran into one of those delightful bugs that only show up in dynamic task generation of your data pipelines — the kind that teach you how a leaky abstraction in your pipeline platform can have you scratching your head in confusion.</p>
<p>The short version:
I made a simple function signature change, assuming only future runs would care. Instead, my pipeline broke days later when an old task serialized under the previous signature collided with the new code. The fix? Classic <a href="/garden/backward-compatibility-for-leaky-abstractions/">backward compatibility tricks</a> that saved me from babysitting all existing task runs when making changes in the future.</p>
<p>Here’s the story — and how to avoid learning this lesson the hard way.</p>
<h1>The bug</h1>
<p>I had a <a href="https://medium.com/@AnalyticsAtMeta/data-engineering-at-meta-high-level-overview-of-the-internal-tech-stack-a200460a44fe">Dataswarm</a> operator that would execute a Python function every day, and the output of that Python function was a list of tasks (other functions) to be executed that day.</p>
<p>Here is what the function looked like:</p>
<pre><code># Function that generates the tasks to be executed.
def task_generator(arg1, arg2) -&gt; List[Task]:
    ...

# How the function is invoked
wait_for_data = SomeTaskWaitingForData(data)
tasks_to_be_executed = DynamicTasks(
    wait_for_tasks=[wait_for_data]
    task_gen_function=task_generator
    task_gen_args={
        "arg1": a,
        "arg2": b,
    }
)
</code></pre>
<p>I put in a diff that looked something like this:</p>
<pre><code># Function that generates the tasks to be executed.
- def task_generator(arg1, arg2) -&gt; List[Task]:
+ def task_generator(arg1, arg3) -&gt; List[Task]:
    ...

# How the function is invoked
wait_for_data = SomeTaskWaitingForData(data)
tasks_to_be_executed = DynamicTasks(
    wait_for_tasks=[wait_for_data]
    task_gen_function=task_generator
    task_gen_args={
        "arg1": a,
-        "arg2": b,
+        "arg3": c,
    }
)
</code></pre>
<p>You see, I just replaced <code>arg2</code> with <code>arg3</code> and everything looked fine. I tested the diff and landed it, expecting the next task instance to pick up the changes and move on. As you can imagine, that is not what happened :)</p>
<p>I soon got a bug report that said that my pipeline failed with an error: <code>TypeError: 'arg2' is an invalid keyword argument for task_generator()</code>. This had me completely confused. My expectation was that either the <em>previous</em> version of the pipeline would be executed, in which <code>task_generator()</code> is defined to expect <code>arg2</code> and <code>tasks_to_be_executed</code> passes a value for <code>arg2</code>, or the <em>new</em> version of the pipeline would run, where <code>task_generator()</code> expects <code>arg3</code> and <code>tasks_to_be_executed</code> passes arg3. Neither of those two scenarios result in a <code>TypeError: 'arg2' is an invalid keyword argument for task_generator()</code>. So, what's going on?</p>
<h1>The root cause</h1>
<p>After some debugging, I saw that the <code>tasks_to_be_executed</code> task instance that errored out started off two days ago, but was waiting for the <code>wait_for_data</code> to complete, and the <code>wait_for_data</code> task didn't complete until the current day, after which the <code>tasks_to_be_executed</code> task instance ran and errored out. Eventually I found that <code>DynamicTasks</code> serializes the function name and args as a JSON blob at schedule time, waits for upstream tasks to finish, then reloads the function from HEAD and calls it with the original arguments. That’s why old args collided with new code.</p>
<p><img src="/assets/images/dynamicTask-dataswarm-pipeline-failure-2025-07-01.png" alt="" /></p>
<p>Because <code>DynamicTasks</code> persists the function name and args and then later reloads HEAD, it breaks the assumption that changing a function signature only affects new pipeline runs. I only discovered this by digging into <code>DynamicTasks</code> implementation; classic <a href="https://en.wikipedia.org/wiki/Leaky_abstraction">leaky abstraction</a>!</p>
<h1>The fix</h1>
<p>Changing the Dataswarm operator implementation to not leak its implementation detail was a pretty heavy lift, and I needed a more scoped down change to unblock myself. So, I needed a way to make sure that I can change the <code>task_generator</code> implementation without running into such combinations of race conditions and leaky abstraction again. Making the task_generator implementation backward compatible accomplishes this quite nicely. But first, I need to make sure that it can be made backwards compatible. That involves a few steps.</p>
<h2>Step 1. Add **kwargs</h2>
<p>First, we need to ensure that passing in parameters from the previous version of <code>task_generator</code> does not throw an unexpected exception. We can do that by swallowing all unspecified parameters in <code>**kwargs</code> as follows.</p>
<pre><code># Function that generates the tasks to be executed.
- def task_generator(arg1, arg2) -&gt; List[Task]:
+ def task_generator(
+    *,
+    arg1=None,
+    arg2=None,
+    **kwargs
+ ) -&gt; List[Task]:
+    if kwargs:
+        LOG.warning(f"Found unspecified arguments {kwargs.keys()}")
    ...
</code></pre>
<p>The diff does three things.</p>
<ol>
<li>It ensures that all arguments are passed by name and not by position.</li>
<li>It makes all parameters optional with a default value of <code>None</code>. This ensures that leaving out any specific parameter doesn’t break the call. The reasoning for this is similar to the ones in <a href="https://github.com/protocolbuffers/protobuf/issues/2497">proto3 that made all fields optional</a>.</li>
<li>If the caller passes an unexpected parameter (say, arg13), the function won’t throw an exception. Instead, it logs a warning about the unrecognized parameter and proceeds to execute the function with the remaining parameters.</li>
</ol>
<p>Land this change and wait for it to propagate to all your task instances.</p>
<h2>Step 2. Change your function signature</h2>
<p>Now you are ready to make changes to your function signature without breaking existing tasks. Suppose you want to remove arg2 and introduce arg3. Your diff would look like this.</p>
<pre><code>def task_generator(
    *,
    arg1=None,
-   arg2=None,
+   arg3=None,
    **kwargs
) -&gt; List[Task]:
    if kwargs:
        LOG.warning(f"Found unspecified arguments {kwargs.keys()}")
+   if not arg3:
+       arg2 = kwargs.get("arg2", None)
+       # Old business logic with arg2
        ...
+       return tasks
+   # New business logic with arg3
+   ...
+   return tasks
</code></pre>
<p>When you land this, you could have tasks scheduled to run that are currently persisting the old function signature. When such tasks execute your new function definition, <code>**kwargs</code> will swallow <code>arg2</code> and <code>arg3</code> is set to its default value <code>None</code>. The function will see that <code>arg3</code> is None, so it will look for <code>arg2</code> in kwargs and execute the old business logic.</p>
<p>However, for all new instances of your task, <code>arg3</code> is set, and so the function executes the new business logic. Backward compatibility accomplished!</p>
<h2>Step 3. Delete old functionality</h2>
<p>After all your old task instances have completed execution, you are now ready to remove the old business logic. This is a simple red diff.</p>
<pre><code>def task_generator(
    *,
    arg1=None,
-   arg3=None,
    **kwargs
) -&gt; List[Task]:
    if kwargs:
        LOG.warning(f"Found unspecified arguments {kwargs.keys()}")
-   if not arg3:
-       arg2 = kwargs.get("arg2", None)
-       # Old business logic with arg2
-        ...
-       return tasks
   # New business logic with arg3
   ...
   return tasks
</code></pre>
<p>And, you are done!</p>
<h1>Lessons learned</h1>
<ul>
<li>
<p>Pipeline frameworks can serialize more than you think. DynamicTasks pickled the function name and arguments days earlier, then loaded the function definition fresh from HEAD. That mismatch broke everything.</p>
</li>
<li>
<p>Stage changes with <code>**kwargs</code> and defaults. When changing function signatures that might still be called by older task payloads, always accept extra kwargs and use <code>None</code> defaults to gracefully detect old vs. new callers.</p>
</li>
<li>
<p>Expect your abstractions to leak. If your orchestration tool stores data and code separately (JSON blobs now, functions later), your assumption that “old code only calls old function signatures” is toast.</p>
</li>
<li>
<p>Logging unrecognized parameters is gold. Instead of crashing, you get explicit warnings when old payloads collide with new code. Debugging becomes a thousand times easier.</p>
</li>
</ul>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2025-07-02T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Const Refs vs. Raw Pointers: Fixing Shared Pointer Reads]]></title>
        <id>https://srikanth.sastry.name/const-ref-vs-raw-ptr-a-fix-for-shared-ptr-reads/</id>
        <link href="https://srikanth.sastry.name/const-ref-vs-raw-ptr-a-fix-for-shared-ptr-reads/"/>
        <updated>2025-06-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Recently, I encountered a subtle performance issue while refactoring some C++ code. I was passing a std::shared_ptr<T> by value into a funct...]]></summary>
        <content type="html"><![CDATA[<h2>The problem</h2>
<p>Recently, I encountered a subtle performance issue while refactoring some C++ code. I was passing a std::shared_ptr&lt;T&gt; by value into a function, even though the callee only needed read access. Infer flagged it as <a href="https://fbinfer.com/docs/all-issue-types/#pulse_readonly_shared_ptr_param"><code>PULSE_READONLY_SHARED_PTR_PARAM</code></a>. Infer was right: passing shared pointers by value incurs refcount overhead, and if multiple threads are sharing the pointer, it can introduce performance regressions. My code looked something like this.</p>
<pre><code>void caller(std::shared_ptr&lt;T&gt; shared_ptr) {
  ...
  callee(shared_ptr);
}

void callee(std::shared_ptr&lt;T&gt; ptr) {
  auto foo = ptr-&gt;read_value() + 1;
}
</code></pre>
<h2>Infer's suggestion, and why it's wrong</h2>
<p>Infer's documentation around <a href="https://fbinfer.com/docs/all-issue-types/#pulse_readonly_shared_ptr_param"><code>PULSE_READONLY_SHARED_PTR_PARAM</code></a> says the following:</p>
<blockquote>
<p>This issue is reported when a shared pointer parameter is a) passed by value and b) is used only for reading, rather than lifetime extension. At the callsite, this might cause a potentially expensive unnecessary copy of the shared pointer, especially when many number of threads are sharing it. To avoid this, consider 1) passing the raw pointer instead and 2) use <code>std::shared_ptr::get</code> at callsites.</p>
</blockquote>
<p>So, its suggestion was to change my code to the following:</p>
<pre><code>void caller(std::shared_ptr&lt;T&gt; shared_ptr) {
  ...
  callee(shared_ptr.get());
}

void callee(T* ptr) {
  auto foo = ptr-&gt;read_value() + 1;
}
</code></pre>
<p>Sure, Infer got the diagnosis right, but the proposed solution of using raw pointers seems wrong. Smart pointers (unique_ptr and shared_ptr) were introduced precisely to avoid the many footguns associated with raw pointer memory management and safety. There should be almost no good reason to use raw pointers, and the use case above seems too trivial to warrant using a raw pointer. In fact, if I passed a raw pointer, some future developer might wrap it in a new shared_ptr and pass it elsewhere. That’s a recipe for double-free bugs and a nasty core dump.</p>
<p>Ranting aside, I still couldn’t let the change stand as-is; the infer error was pointing to a legitimate problem. So, how do we address this issue without resorting to raw pointers? Answer: const refs :)</p>
<h2>Const refs to the rescue</h2>
<p>Instead of passing the raw pointer, what if we just passed the underlying object itself? Well, we don't really need a copy of the object, and so we can pass a reference to it. Since we’re only calling a read-only method, a const reference works just fine. With that, we have this fix:</p>
<pre><code>void caller(std::shared_ptr&lt;T&gt; shared_ptr) {
  ...
  callee(*shared_ptr);
}

void callee(const T &amp;obj) {
  auto foo = obj.read_value() + 1;
}
</code></pre>
<p>And it works like a charm.</p>
<h2>Why not const ref the shared pointer itself?</h2>
<p>Of course, passing a const reference to the object isn’t the only way to avoid copying the shared pointer. You can also simply pass the shared pointer by reference!</p>
<pre><code>void caller(std::shared_ptr&lt;T&gt; shared_ptr) {
  ...
  callee(shared_ptr);
}

- void callee(std::shared_ptr&lt;T&gt; ptr) {
+ void callee(const std::shared_ptr&lt;T&gt; &amp;ptr) {
  auto foo = ptr-&gt;read_value() + 1;
}
</code></pre>
<p>(I showed this as a diff because the change is subtle! Also note: taking a reference to a <code>shared_ptr</code> avoids bumping the reference count, so there’s no added contention.)</p>
<p>Now, be careful here. You can't use this trick if <code>shared_ptr</code> could be a <code>nullptr</code>, because passing a reference to a <code>nullptr</code> and trying to dereference it is a great way to bring down your service! In my case, it turns out that <code>shared_ptr</code> was guaranteed to be non-null, and so this trick works well.</p>
<p>So next time someone suggests using a raw pointer, be skeptical—there’s almost always a safer alternative to that particular footgun.</p>
<h2>Oh, and one last thing...</h2>
<p>I’ll stop here, but don’t walk away thinking const refs are a cure-all. They can backfire too. <a href="https://belaycpp.com/2022/02/15/constant-references-are-not-always-your-friends/">This post in Belay the C++</a> outlines some of the less obvious pitfalls.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2025-06-26T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Changing your Jekyll theme without losing your mind (or your content!)]]></title>
        <id>https://srikanth.sastry.name/change-jekyll-theme/</id>
        <link href="https://srikanth.sastry.name/change-jekyll-theme/"/>
        <updated>2025-06-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[After I moved my website from Wordpress to Jekyll, I hadn't changed the theme for nearly 5 years. When I finally decided to change the theme...]]></summary>
        <content type="html"><![CDATA[<p>After I moved my website from Wordpress to <a href="https://jekyllrb.com">Jekyll</a>, I hadn't changed the theme for nearly 5 years. When I finally decided to change the theme recently, it turned out to be a lot more complicated than I expected. After a lot of trial and error, and searching the internet, and asking ChatGPT, I managed to get the theme changed. As a note to my future self, and to anyone else who might be struggling to update their Jekyll theme, I am outlining the steps here that will make it relatively straightforward for you to move from one theme to another.</p>
<p><strong>Disclaimer:</strong> While these steps will make it easy for your site to start looking closer to your desired theme, it is by no means a turnkey solution. You will still need to do a fair amount of hand editing for the new theme to work with your existing content. So, make sure you are able and willing to spend time fiddling with various configs, settings, and markdown front matter.</p>
<h2>Prerequisites</h2>
<p>Before we begin, here are the prerequisites:</p>
<ol>
<li>Your site is already version controlled by git. Ideally, it is already on <a href="https://www.github.com">Github</a>.</li>
<li>You are able to run Jekyll locally. If not, please follow the instructions in https://jekyllrb.com/docs/ and then come back here.</li>
<li>The new theme that you have picked out for your site is a <a href="https://jekyllrb.com/docs/themes/#understanding-gem-based-themes">gem-based theme</a>. Technically, the steps outlined below can be tweaked for a regular theme as well. I will include an addendum at the bottom on how you can work with regular themes as well.</li>
<li>The post assumes that your Jekyll site source is in the directory <code>~/github/website</code>. All references to <code>~/github/website</code> should be replaced by the location of your source.</li>
<li>I assume that the theme that you want is <a href="https://github.com/a-chacon/wind">Wind theme</a> that can be installed via  <code>gem "wind-theme"</code>. You can change it to whatever theme you pick.</li>
</ol>
<h2>Requirements</h2>
<ol>
<li>The new theme is installed in the same location as the original site</li>
<li>The content from the orignal site is preserved.</li>
<li>The new theme lands as a single commit on top of the old theme. We do not want to lose the commit history.</li>
<li>No vestiges or residues of the old theme must remain.</li>
<li>This should be done via a <a href="https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests">Pull Request</a>/merge and not via a <a href="https://git-scm.com/docs/git-push#Documentation/git-push.txt---force">force-push</a>.</li>
</ol>
<h2>The Guide</h2>
<h3>Start with a clean slate</h3>
<p>To begin, we’ll create a clean working directory. We don’t want to simply nuke <code>~/github/website</code> since it contains all your site's content. Since we want to keep the commit history, we cannot pull an orphan branch. So, we start by creating a branch off of main, and then working on it exclusively until we are ready to publish a Pull Request. We’ll start by creating a new branch and removing its contents so we can set up a fresh Jekyll site. Here are the steps.</p>
<p>First, create a new branch from <code>main</code>, and then nuke everything. This creates an empty working directory in the new branch, while preserving your full site on main.</p>
<pre><code>cd ~/github/website
git checkout main
git checkout -b install_new_theme
git rm -rf .
git clean -dfx
</code></pre>
<p><em>Note: <code>git clean -dfx</code> removes any untracked files and directories, including ignored files, to fully clean the working tree.</em></p>
<p>Now you have a clean directory to install things in. Next, we install a fresh Jekyll site:</p>
<pre><code>gem install jekyll bundler
bundle init
cd ..  # Change directory to `~/github`
jekyll new website --force
cd ~/github/website
</code></pre>
<p><em>Warning: <code>jekyll new website --force</code> overwrites contents of <code>website/</code> and so make sure it doesn’t contain anything important that isn’t backed up.</em></p>
<p>Now, this is an empty Jekyll site, and we are going to change the theme on this empty site to the theme that you picked out.</p>
<h3>Install new theme</h3>
<p>Recall that I am going to install <code>wind-theme</code>. To do that, I edit the Gemfile <code>~/github/website/Gemfile</code> as per the <a href="https://github.com/a-chacon/wind?tab=readme-ov-file#advanced-installation-optional-">theme instructions</a>, and the following line.</p>
<pre><code>gem "wind-theme"
</code></pre>
<p>You can either use the default <code>_config.yml</code>, or copy your existing one from <code>main</code> via:</p>
<pre><code>git checkout main -- _config.yml
</code></pre>
<p>Now, edit your <code>_config.yml</code> to set the theme to <code>wind-theme</code> (as per <a href="https://github.com/a-chacon/wind?tab=readme-ov-file#advanced-installation-optional-">theme instructions</a>).</p>
<pre><code>theme: wind-theme
</code></pre>
<p>Now run the installer and then serve the site locally (in another shell).</p>
<pre><code>cd ~/github/website
bundle
bundle exec jekyll serve
</code></pre>
<p>The empty site with your new theme should now be accessible at http://localhost:4000/. Make sure that it looks and feels as advertised, and that no errors are popping up when building the site. At this point, if you are seeing issues, then you will have to roll up your sleeves, figure out what wrong, and fix it. Once you are happy with your site, time to fill it up with your content.</p>
<h3>Bring back your content</h3>
<p>Your site's contents should be in the <code>main</code> branch's <code>_posts</code>, and any other directories you may have created to store content (such as <code>documents</code>, <code>images</code>, <code>assets</code>, etc.). The top level pages of your site should be the top level <code>.md</code> or <code>.markdown</code> markdown files. Then, you also have your <code>.gitignore</code>, <code>CNAME</code>, etc. Bring them all back by copying them from the <code>main</code> branch as follows:</p>
<pre><code>git checkout main -- _posts about.md something_else.md CNAME .editorconfig .github
...
</code></pre>
<p><em>Note: If you want to bring back everything except the theme-related directories, use a wildcard or cherry-pick structure.</em></p>
<p>The shell building your site should pick these changes up and update the local build automatically. Check the console logs on the output of <code>bundle exec jekyll serve</code> to make sure that things are working correctly. Go check out http://localhost:4000/ to make sure that all your content is present and looks as expected.</p>
<p>Again, if something looks off, it is time to get your hands dirty, figure out what's gone awry, and fix it. From here, you may need to debug based on your content and theme setup.</p>
<p>Once you are happy with the new theme, it is time to make it official!</p>
<h3>Switching over to the new theme</h3>
<p>First, let us commit all these changes we made into one fat commit.</p>
<pre><code>git commit -am "Replace site with new jekyll theme"
</code></pre>
<p>Once you’re satisfied, push the branch to origin and open a pull request for review and merge.</p>
<pre><code>git push --set-upstream origin install_new_theme
</code></pre>
<p>If you’re not using GitHub PRs, you can merge the changes directly into <code>main</code> via the CLI:</p>
<pre><code>git checkout main
git merge install_new_theme
</code></pre>
<p>Once merged to main, you should be good to go. Congratulations, you have successfully changed your site's theme!</p>
<h2>Addendum: using regular themes</h2>
<p>If you want to install a regular theme that is not gem-based, then instead of following the "Install New Theme" section's instructions, do the following.</p>
<ol>
<li>
<p><strong>Download the regular theme files.</strong> You can fork them from github and clone it locally, if you want. That is what many repos suggest, but I prefer not to do that because those instructions assume that you will be installing your entire site on top of that repo. But that is not what you want. Presumably you want all of your content and associated commit history. So, don't clone the repo. Just download it to a new directory (say) <code>~/tmp/new_theme</code>.
<img src="/assets/images/github-download-repo.png" alt="Download from github image" /></p>
</li>
<li>
<p><strong>Copy over the theme files to your site directory (<code>~/github/website</code>).</strong> This include directories such as <code>_includes</code>, <code>_layouts</code>, <code>_data</code>, <code>assets</code>, and typically any other directory that starts with <code>_</code> (except for <code>_site</code>). There might also be <code>.js</code> or other files in the main directory that you might have to copy.</p>
</li>
<li>
<p><strong>Caution.</strong> Before moving on to the instructions in "Bring back your content" section, make sure that whatever content you are bringing back, it does not overwrite or interfere with the theme file that you just copied over. <em>E.g.</em>, you might have images or icons in your <code>assets</code> directory, and the theme might also store some files in <code>assets</code> directory that you just copied over. <code>git checkout main -- assets</code> might overwrite your new theme files with old site content, so use caution.</p>
</li>
</ol>
<h2>Final Checklist</h2>
<ul>
<li>[x]  Your site is backed up or version-controlled</li>
<li>[x]  You've created a new branch from <code>main</code> (e.g., <code>install_new_theme</code>)</li>
<li>[x]  You've cleaned out old content in the new branch</li>
<li>[x]  You've installed a fresh Jekyll site and configured your new theme</li>
<li>[x]  You've served the site locally and confirmed it builds with no errors</li>
<li>[x]  You've copied over your original content and ensured nothing essential was lost or overwritten</li>
<li>[x]  You've tested your new site at http://localhost:4000/ and confirmed the theme looks correct</li>
<li>[x]  You've committed the changes as one clean commit</li>
<li>[x]  You've pushed the branch and opened a PR — or merged the branch into <code>main</code></li>
<li>[x]  Your <code>main</code> branch now reflects the new theme and your full content</li>
</ul>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2025-06-22T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Cyclomatic Complexity: How Low Can You Go?]]></title>
        <id>https://srikanth.sastry.name/reduce-cyclomatic-complexity/</id>
        <link href="https://srikanth.sastry.name/reduce-cyclomatic-complexity/"/>
        <updated>2025-06-17T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Ever spend 20 minutes trying to figure out why your bug fix or feature code isn't triggering or being executed — only to realize you missed...]]></summary>
        <content type="html"><![CDATA[<h2>What even <em>is</em> Cyclomatic Complexity?</h2>
<p>Ever spend 20 minutes trying to figure out why your bug fix or feature code isn't triggering or being executed — only to realize you missed a buried branch in someone’s 10-path function? That’s <a href="https://en.wikipedia.org/wiki/Cyclomatic_complexity">Cyclomatic Complexity</a> in action. Intuitively, you can think of Cyclomatic Complexity as the number of possible paths a single execution of a function can take.</p>
<p>For example, <code>a = b + c</code> has a cyclomatic complexity of one, and <code>a = b + c if foo else d + e</code> has a cyclomatic complexity of two: one path is when <code>foo</code> is <code>True</code> and the effective logic is <code>a = b + c</code>, and the other path is when <code>foo</code> is <code>False</code> and the effective logic is <code>a = d + e</code>.</p>
<h2>Ain't got no time? Here's the goods.</h2>
<p>If you take just one thing away from this note, then let it be this.</p>
<blockquote>
<p><strong>Strive to reduce the <a href="/garden/reduce-cyclomatic-complexity/">Cyclomatic Complexity</a> of your code; your team and your future self will thank you!</strong></p>
</blockquote>
<h2>Time to hit the brain gym, bro</h2>
<p>As an exercise, I will let you figure out the cyclomatic complexity of the following piece of code:</p>
<pre><code>env_val = os.environ.get('...')
switcher_val = False
if env_val is not None:
    jk_val = True
    if env_val.lower() is in ["true", "1", "yes"]:
        env_val = True
    else:
        env_val = False
else:
    env_val = True
    switch_name = "/switch/name/from/config"
    switcher_val = switcher.check(switch_name, switchval=region)
if env_val or switcher_val:
    apply_some_config(job)
</code></pre>
<p>I'll wait... (Spoiler: It's not pretty.)</p>
<p>Give up? Turns out, it is <code>4</code>: three if-checks contribute to three branching points, and the cyclomatic complexity is one more than that; <em>ergo</em> <code>4</code>.</p>
<p>Next, by spending no more than 60 seconds looking this code, can you tell me what exactly it is doing? BTW, this is real production code that I ran across when debugging some issue, and it took me a long while to make sure I knew exactly when and how the config is applied. It wasn't obvious at all. If you can grok this in 60 seconds, take a bow!</p>
<h2>Reeling yet?</h2>
<p>Anyway, making sense of functions with high cyclomatic complexity is annoying. It’s notoriously difficult to write tests with good coverage for these functions, and in general, they tend to be bug factories.</p>
<p>And yet — somehow — a lot of senior software engineers don’t seem to grok this. I keep seeing deeply nested <code>if-else</code> blocks, sometimes inside loops with <code>break</code>s and <code>continue</code>s, and it doesn’t seem to bother anyone! It’s like we’ve collectively normalized this cognitive overhead.</p>
<p>Why?! Why are we putting up with this crap? It’d never fly in an interview.</p>
<h2>Yo, let's fix it up!</h2>
<p>Coming back to the above example, the confusion and ugliness of this code really got to me. It got so bad I considered dusting off a Karnaugh map. After some much needed grokking, I managed to simplify it down to a cyclomatic complexity of <code>2</code>! :)</p>
<p>In the end, here’s what that poor little code snippet was trying to do:</p>
<pre><code># Apply config when '...' environment variable is True, else check the switch
__ENV_VARIABLE = '...'
__SWITCHER_KEY = '/switch/name/from/config'
def has_env_override():
    val = os.environ.get(__ENV_VARIABLE)
    return val is not None and val.lower() in {"true", "1", "yes"}

if (
    has_env_override() or
    switcher.check(__SWITCHER_KEY, switchval=region)
):
    apply_some_config(job)
</code></pre>
<p>Fewer paths, fewer bugs. Cleaner code. Happier teammates. What’s not to love?</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2025-06-17T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[TDD for Bug Fixes]]></title>
        <id>https://srikanth.sastry.name/tdd-for-bug-fixes/</id>
        <link href="https://srikanth.sastry.name/tdd-for-bug-fixes/"/>
        <updated>2025-06-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[I have seen way too many 'senior' engineers get bug fixing wrong. It is common to see an engineer sent a pull request titled "bug fix: <some...]]></summary>
        <content type="html"><![CDATA[<p>I have seen way too many 'senior' engineers get bug fixing wrong. It is common to see an engineer sent a pull request titled "bug fix: &lt;something&gt;" and the PR has changes to the functional code that fixes the bug and a correspond test case that shows that the bug is fixed. If that sounds reasonable, THINK AGAIN — you’ve walked right into the classic trap!</p>
<p><strong>If you are sending PRs for bug fixes with functional code change and an added test case in the same PR/commit, then you are doing it wrong!</strong></p>
<p>The crux of the problem is the following: HOW DO YOU <em>KNOW</em> YOU’RE SMASHING THAT BUG? HOW CAN YOU BE SURE YOUR TEST ISN’T A DUD?! Your answer better not be <em>VIBE CHECKS</em> or just <em>STARING REALLY HARD</em>! If you are having to deploy your entire service/library and run an end-to-end test to demonstrate correctness, then you are doing too much, and you still haven't demonstrated that the unit test actually captures the previously errneous behavior.</p>
<p>There is this shiny little concept called <a href="https://en.wikipedia.org/wiki/Test-driven_development">Test Driven Development (TDD)</a> that is mighty useful here. You can peruse the wikipedia link to figure out what TDD is exactly. This note will show you how to &lt;a href="/garden/tdd-for-bug-fixes/"&gt;use TDD for bug fixes&lt;/a&gt;.</p>
<p>Here are simple steps to fixing bugs using TDD:</p>
<ol>
<li>
<p>🕵️ Discover the bug. BAM! There it is! Your nemesis!</p>
</li>
<li>
<p>🧪 Create a PR that creates a new unit test that exposes the unit test. YAWZA!</p>
</li>
<li>
<p>🔧 Create a second PR on top the first PR that makes the functional code change and changes the expectation on the unit test accordingly. That should squash the bug! KAPOW!</p>
</li>
<li>
<p>💰 Justice is served! PROFIT!</p>
</li>
</ol>
<p><img src="/assets/images/tdd-bug-lifecycle.png" alt="" /></p>
<p>Still not sure? Let's demonstrate this with an example. Say, there is a bug that you discovered and know how to fix it.</p>
<p>First, you create a PR that demonstrates the bug by invoking your SUT with the offending input, and sets the expected value to be <em>incorrect</em> so that the test case actually <em>passes</em> with this incorrect value; thus demonstrating the bug.</p>
<pre><code>class TestSUT(unittest.TestCase):
    ...
    def test_bug_b12345(self) -&gt; None:
        '''
        Test to expose bug b12345
        '''
        # Arrange
        sut = SUT(...)
        
        # Act
        actual = sut.test_method(input="bad-input")

        # Assert
        self.assertEqual(actual, "bad buggy output")
        # The assertion above demonstartes the bug b12345
        # The right expected value should be "correct output".
        # self.assertEqual(actual, "correct output")
        
</code></pre>
<p>You can send that PR out for review and merge it in. Now you have a solid proof that you have found a bug, and reproduced it.</p>
<p>Next, you have a new PR that fixes that bug. If you bug fix is correct, then the test <code>test_bug_b12345</code> should not start failing. The output of <code>sut.test_method(input="bad-input")</code> should be <code>"correct output"</code> and not <code>"bad buggy output"</code>. So, you now modify the unit test <code>test_bug_b12345</code> in that same PR that looks as follows:</p>
<pre><code>    def test_bug_b12345(self) -&gt; None:
        '''
        Test to expose bug b12345
        '''
        # Arrange
        sut = SUT(...)
        
        # Act
        actual = sut.test_method(input="bad-input")

        # Assert
-       self.assertEqual(actual, "bad buggy output")
-       # The assertion above demonstartes the bug b12345
-       # The right expected value should be "correct output".
-       # self.assertEqual(actual, "correct output")
+       self.assertEqual(actual, "correct output")
</code></pre>
<p>Now your test should pass. This second PR is conclusive proof that your diff now fixes the bug! So, merge it in. Deploy with confidence. <strong>BOOM — PROFIT!</strong></p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2025-06-11T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Let Sleeping Engineers Lie: Why Your Alerts Should Match Your SEVs]]></title>
        <id>https://srikanth.sastry.name/sync-your-alerts-to-your-sev-criteria/</id>
        <link href="https://srikanth.sastry.name/sync-your-alerts-to-your-sev-criteria/"/>
        <updated>2025-06-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[At work, I had a customer team that aspired to be “customer first.” To them, that meant fixing issues before they became SEVs. That was all...]]></summary>
        <content type="html"><![CDATA[<p>At work, I had a customer team that aspired to be <em>“customer first.”</em> To them, that meant fixing issues <em>before</em> they became SEVs. That was all and good, except that the way they went about it was to fire alerts well <em>before</em> their SLOs were close to being breached. Of course, I knew nothing about it until I was the receiving end of their 'aspiration'.</p>
<p>It’s 4 AM, and I am in deep sleep. Suddenly, my phone, overriding all silencing setting starts ringing like there is no tomorrow. Naturally, I was being paged. I wake up bleary eyed, acknowledge the page, and join the team channel. Helpfully, the customer team oncall has message for me: <strong>“Your service has a latency spike. Please look into it.”</strong></p>
<p>I drag myself to a laptop, check the graphs, and yes — there <em>was</em> a p99 latency spike, it lasted about half hour, and is already waning. Our SLOs were fine; our latency SLOs at these latency levels don't breach for another 30 minutes. I double-checked <em>their</em> SEV criteria, and they are also still green! So why the 4 AM fire drill?</p>
<p>Turns out, they’d set up their alerts to go off when their p99 latency went above the normal limits for 30 minutes, but their SLO wouldn't be breached until the elevated p99 persisted for 60 minutes. A twitcy alert if you ask me!</p>
<p>Their on-call had no idea what to do with the alert, saw my service mentioned, and did the classic move:</p>
<blockquote>
<p><em>“When in doubt, escalate!”</em></p>
</blockquote>
<p>So now <em>I’m</em> awake, trying to make sense of a 30-minute p99 latency increase that is fixing itself. I asked:</p>
<blockquote>
<p><strong>“Where's the SEV'?</strong></p>
</blockquote>
<p>I imagine the scene something like this.
<img src="/assets/images/where-sev-where-impact.jpg" alt="" /></p>
<p>Silence. Five minutes later, "Here is the SEV number..." The SEV was created two minutes ago. Facepalm!</p>
<p>Here’s what actually happened:</p>
<ul>
<li>The latency spike lasted about 30 minutes.</li>
<li>The system auto-healed.</li>
<li>The affected service was user-facing, but this was deep in the off-hours.</li>
<li>Total estimated user impact: somewhere between <em>“negligible”</em> and <em>“none.”</em></li>
</ul>
<p>We could’ve all just slept through it and looked at it with fresh eyes in the morning. Instead, two engineers got pulled into zombie mode to stare at graphs that improved all by themselves. It was like debugging a ghost.</p>
<h3>Moral of the story:</h3>
<p>If your &lt;a href="/garden/align-alerts-to-sev-criteria/"&gt;alert is going to wake someone up at 4 AM&lt;/a&gt;, it better be for something that <em>actually</em> matters. If there's no SEV, no SLO breach, and no clear user impact — maybe let sleeping engineers lie.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2025-06-07T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[The Law of Demeter and unit tests]]></title>
        <id>https://srikanth.sastry.name/law-of-demeter-and-unit-tests/</id>
        <link href="https://srikanth.sastry.name/law-of-demeter-and-unit-tests/"/>
        <updated>2022-07-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[The Law of Demeter essentially says that each unit should only talk to its 'immediate friends' or 'immediate dependencies', and in spirit, i...]]></summary>
        <content type="html"><![CDATA[<p>The <a href="https://en.wikipedia.org/wiki/Law_of_Demeter">Law of Demeter</a> essentially says that each unit should only talk to its 'immediate friends' or 'immediate dependencies', and in spirit, it is pointing to the principle that each unit only have the information it needs to meet its purpose. In that spirit, the Law of Demeter takes two forms that are relevant to &lt;a href="/garden/law-of-demeter-and-testing/"&gt;making your code more testable&lt;/a&gt;: (1) object chains, and (2) fat parameters.</p>
<h2>Object Chains</h2>
<p>This is the more classic violation of the Law of Demeter[^1]. This happens when a class <code>C</code> has a dependency <code>D</code>, and <code>D</code> has method <code>m</code> that returns an instance of another class <code>A</code>. The violation happens when <code>C</code> accesses <code>A</code> and calls a method in <code>A</code>. Note that only <code>D</code> is the 'immediate' collaborator/dependency of <code>C</code>, and not <code>A</code>. The Law of Demeter says that <code>C</code> should not be accessing the method in <code>A</code>.</p>
<pre><code># A violation of the Law of Demeter looks as follows.
## Example 1:
c.d.m().methodInA()

## Example 2:
d: D = c.d
a: A = d.m()
a.methodInA()
</code></pre>
<p>What is the problem with violating the Law of Demeter?  Consider the following production code:</p>
<pre><code>class UpdateKVStore:
    def __init__(self, client: KVStoreClient) -&gt; None:
        self.client = client
        
    def update_value(new_content: Content) -&gt; Status:
        transaction: KVStoreClient.Transaction = self.client.new_transaction()
        if transaction.get_content() == new_content:
            # Nothing to update
            transaction.end()
            return Status.SUCCESS_UNCHANGED
        mutation_request: KVStoreClient.MutationRequest = (
            transaction.mutation_request().set_content(new_content)
        )
        mutation = mutation_request.prepare()
        status: KVStoreClient.Mutation = mutation.land()
        return status
</code></pre>
<p>Now how would you unit test this? The test doubles for testing this code will look something like this</p>
<pre><code>mock_client = MagicMock(spec=KVStoreClient)
mock_transaction = MagicMock(spec=KVStoreClient.Transaction)
mock_mutation_request = MagicMock(spec=KVStoreClient.MutationRequest)
mock_mutation = MagicMock(spec=KVStoreClient.Mutation)

mock_client.new_transaction.return_value = mock_transaction
mock_transaction.mutation_request.return_value = mock_mutation_request
mock_mutation_request.prepare.return_value = mock_mutation
</code></pre>
<p>Now you can see how much the class <code>UpdateKVStore</code> and its unit tests need to know about the internals of the <code>KVStoreClient</code>. Any changes to how the <code>KVStoreClient</code> implements the transaction will cascade into test failures on all its clients! That's a recipe for a <a href="https://srikanth.sastry.name/unit-test-attributes-and-their-trade-offs/">low accuracy</a> test suite.</p>
<p>There are a few ways to address this. Instead, if <code>KVStoreClient</code> could be recast as a <code>Transaction</code> factory, and then encapsulate all operations associated with the transactions within the <code>Transaction</code> class, then <code>UpdateKVStore</code> can be modified as follows:</p>
<pre><code>class UpdateKVStore:
    def __init__(self, client: KVStoreClient) -&gt; None:
        self.client = client  # Now a Factory class for Transaction.
        
    def update_value(new_content: Content) -&gt; Status:
        transaction: KVStoreClient.Transaction = self.client.new_transaction()
        if transaction.get_content() == new_content:
            # Nothing to update
            transaction.end()
            return Status.SUCCESS_UNCHANGED
        status = transaction.update_and_land(new_content)
        return status
</code></pre>
<p>When testing the new <code>UpdateKVStore</code>, you only need to replace the <code>KVStoreClient</code> and the <code>Transaction</code>, both of which are (explicit or implicit) direct dependencies, with test doubles. This makes the code much easier and straightforward to test.</p>
<h2>Fat Parameters</h2>
<p>While the anti-pattern of 'fat parameters' does follow directly from the Law of Demeter, it does follow from the spirit of passing in only the information that the class needs to perform its function. So, what are fat parameters? They are data objects that as passed in as an argument to a class, and they contain more information than what is needed by the class.</p>
<p>For instance, say you have a class <code>EmailDispatcher</code> whose method <code>setRecipient</code> only needs a customer name and email address. The method signature for <code>setRecipient</code> should only require the name and email, and not the entire <code>Customer</code> object that contains a whole lot more.</p>
<pre><code>@dataclass(frozen=True)
class Customer:
    ... # data class members.
    def getFullName(self):
        ...
    def getEmail(self):
        ...
    def getPhysicalAddress(self):
        ...
    def getPostalCode(self):
        ...
    def getCountry(self):
        ...
    def getState(self):
        ...
    def getCustomerId(self):
        ...
    # and so on.
    
 class EmailDispatcher:
     ...
     def setRecipient(name: str, email: str):
         ...
     def setRecipientWithFatParameter(customer: Customer):
         ...
     def sendMessage(self, message: Message):
         ...
</code></pre>
<p>In the pseudocode above, the class <code>EmailDispatcher</code> has two methods <code>setRecipient</code> and <code>setRecipientWithFatParameter</code>. The former uses only the information it needs, and the latter passed in the entire <code>Customer</code> object as a fat parameter.</p>
<p>The convenience of passing in the entire <code>Customer</code> object is straightforward. It allows gives you a simple method signature. It makes it easier for the method to evolve to use richer information about the customer without needing to change its API contract. It allows you to define a common <code>Dispatcher</code> interface with multiple <code>Dispatcher</code>s that use different properties of the <code>Customer</code> class.</p>
<p>However, when it comes to unit testing, such fat parameters present a problem. Consider how you would test the <code>EmailDispatcher</code>'s <code>setRecipientWithFatParameter</code> method. The tests will need to create fake <code>Customer</code> objects. So, your fake <code>Customers</code> might look like this:</p>
<pre><code>fakeCustomer = Customer(
    first_name="bob",
    last_name="marley", 
    email="bob@doobie.com", 
    address=Address(
        "420 High St.", 
      "", 
      "Mary Jane", 
      "Ganga Nation", 
      "7232"
    ), 
    id=12345, 
    postal_code="7232", 
    ...
)
</code></pre>
<p>When someone reads this unit test, do they know what is relevant here? Does it matter that the second parameter of <code>address</code> is empty string? Should the last parameter of <code>address</code> match the value of <code>postal_code</code>? While we might be able to guess it in this case, it gets more confusing in cases where the fat parameter is encapsulating a much more complicated entity, such as a database table.</p>
<p>When refactoring or making changes to the <code>EmailDispatcher</code>, if the unit test fails, then figuring out why the test failed becomes a non-trivial exercise, and could end up slowing you down a lot more than you expected. All this just leads to high maintenance costs for tests, low readability [^2], poor DevX, and limited benefits.</p>
<p>[^1]: You can read about it <a href="https://wouterdekort.com/2012/03/27/unit-testing-hell-or-heaven/">here</a>, <a href="https://hermanradtke.com/2010/01/17/unit-testing-and-the-law-of-demeter.html/">here</a>, <a href="https://wiki.c2.com/?LawOfDemeterMakesUnitTestsEasier">here</a>, and <a href="https://testing.googleblog.com/2008/07/breaking-law-of-demeter-is-like-looking.html">here</a>, and really just search for "Law of Demeter" on the Internet</p>
<p>[^2]: For more details on why we should care about readability, see the section on Readability <a href="https://srikanth.sastry.name/dry-unit-tests-are-bad/">here</a>.</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2022-07-22T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA['Privatize' your classes for better unit testing]]></title>
        <id>https://srikanth.sastry.name/privatize-your-classes-for-better-unit-testing/</id>
        <link href="https://srikanth.sastry.name/privatize-your-classes-for-better-unit-testing/"/>
        <updated>2022-07-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[You service may be massive, but it's public API surface is pretty small; it has just a handful of APIs/endpoints. Everything else behind tho...]]></summary>
        <content type="html"><![CDATA[<p>You service may be massive, but it's public API surface is pretty small; it has just a handful of APIs/endpoints. Everything else behind those APIs are 'private' and 'implementation details'. It is highly advisable to follow this pattern even when designing the implementation of your service, almost like a fractal. This will pay dividends in the quality of your test suite.</p>
<p>For instance, you service implementation should be split into 'modules' where each module has a well defined API through which other modules interact with it. This API boundary has to be strict. Avoid the temptation of breaking this abstraction because your module need this 'one tiny bit' of information that is available inside the implementation of another module. You will regret breaking encapsulation, I guarantee it!</p>
<p>If you follow this pattern, you will eventually reach a class that has a public API, has all of its external/shared dependencies shared, and delegates a lot of it's business logic and complex computation to multiple 'private' classes that are practically hermetic and have no external/shared dependencies. At this point, treat all these 'private' classes as, well, private. That is, DO NOT WRITE UNIT TESTS FOR SUCH CLASSES!</p>
<p>Yes, that statement seems to fly in the face of all things sane about software testing, but it is a sane statement, nonetheless. These private classes should be &lt;a href="/garden/minimize-public-surface-for-testability/"&gt;tested indirectly&lt;/a&gt; via unit tests for the public class that they serve/support. This will make your tests a lot more accurate. Let me explain.</p>
<p>Say, you have a public class <code>CallMe</code> and it uses a private class <code>HideMe</code>, and furthermore, <code>HideMe</code> is used only by <code>CallMe</code>, and the software design enforces this restriction. Assume that both <code>CallMe</code> and <code>HideMe</code> have their own unit tests, and the tests do an excellent job. At this point, there is a new requirement that necessitates that we refactor <code>CallMe</code>'s implementation, and as part of that refactoring, we need to modify the API contract between <code>CallMe</code> and <code>HideMe</code>. Since <code>HideMe</code>'s only  caller is <code>CallMe</code>, it is completely safe to treat this API contract as an implementation detail and modify it as we see fit. Since we are modifying the specification of <code>HideMe</code>, we have to change the tests for <code>HideMe</code> as well.</p>
<p>Now, you run the tests, and the tests for <code>HideMe</code> fail. What information does that give you? Does that mean that there is a bug in <code>HideMe</code>; or does it mean that we did not modify the tests correctly? You cannot determine this until you either manually inspect <code>HideMe</code>'s test code, or until you run the tests for <code>CallMe</code>. If <code>CallMe</code>'s tests fail, then (since this is a refactoring diff) there must be a bug in <code>HideMe</code> and/or <code>CallMe</code>, but if the tests don't fail, then it must be an issue in <code>HideMe</code>'s tests.</p>
<p>Thus, it turns out that the failure in <code>HideMe</code> tests gives you no additional information compared to failure in <code>CallMe</code>'s tests. Thus, tests for <code>HideMe</code> have zero benefits and a non-zero maintenance cost! In other words, testing <code>HideMe</code> directly is useless!</p>
<p>By aggressively refactoring your code to push as much of you logic into private classes, you are limiting the API surface of your software that needs direct testing, and simultaneously, ensuring that your tests suite is not too large, has very [high accuracy, with reasonable completeness]({% post_url 2022-06-13-unit-test-attributes-and-their-trade-offs %}).</p>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2022-07-11T00:00:00.000Z</published>
    </entry>
    <entry>
        <title type="html"><![CDATA[Tests should be isolated from each other; not coupled]]></title>
        <id>https://srikanth.sastry.name/tests-should-be-isolated-not-coupled/</id>
        <link href="https://srikanth.sastry.name/tests-should-be-isolated-not-coupled/"/>
        <updated>2022-07-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Almost [by definition]({% post_url 2022-06-18-defining-unit-tests-two-schools-of-thought %}) unit tests should be isolated from its (externa...]]></summary>
        <content type="html"><![CDATA[<p>Almost [by definition]({% post_url 2022-06-18-defining-unit-tests-two-schools-of-thought %}) unit tests should be <em>isolated</em> from its (external, shared) dependencies. But, equally importantly, unit tests should also be isolated <em>from each other</em>. When one test starts to affect another test, the two tests are said to be &lt;a href="/garden/coupled-tests/"&gt;&lt;em&gt;coupled&lt;/em&gt;&lt;/a&gt;. Alternatively, if changes to one test <em>can</em> negatively impact the correctness of another test, then the two tests are said to be <em>coupled</em>.</p>
<p>Coupled tests are problematic in two ways.</p>
<ol>
<li><em>Tests become less readable.</em> Reading the code for a single unit test does not necessarily communicate what the test does. We also need to understand the 'coupling' between that test and other tests to grok what a single test does. This coupling can be subtle and not easy to follow.</li>
<li><em>Tests become less [accurate]({% post_url 2022-06-13-unit-test-attributes-and-their-trade-offs %}).</em> When one test affects another, it becomes difficult to make changes to a single test in isolation. For instance, if a diff makes changes to the some production and test code, and then a test fails, then it is not always clear why the test failed. The failure could due to a bug, or an artifact the coupled tests. Thus, your tests are no longer trustworthy, and therefore, less accurate.</li>
</ol>
<p>Coupling can happen in many ways. The obvious ones include (1) using the same shared dependency (like when you use the same temp file name in all tests), and (2) relying on the post-condition of one test as a precondition of another test. Such cases are also obvious to detect, and to fix. There are two more following ways in which tests can be coupled; but these are more subtle, and more prevalent.</p>
<ol>
<li>Precondition setting in test fixtures</li>
<li>Parameterized tests for heterogeneous tests</li>
</ol>
<p>The rest of this note is focused on the above two anti-patterns of test coupling.</p>
<h2>Coupling through test fixtures</h2>
<p>Say, your SUT has a dependency called <code>Helper</code>, and initially, for the two tests in your unit tests for the SUT, you initialize your <code>Helper</code> stub with contents <code>valueA</code>, and <code>valueB</code>. Since both tests share the same initial state, you include the initialization code in the <code>SetUp</code> of the unit tests.</p>
<pre><code>class SUTTestCase(unittest.TestCase):
    def setUp(self):
        self.helper = StubHelper()
        self.helper.add_contents([valueA, valueB])
        self.sut = SUT(self.helper)
        
    def test_behavior1(self) -&gt; None:
        ...  # Assumes self.helper set with contents=[valueA, valueB]
    
    def test_behavior2(self) -&gt; None:
        ...  # Assumes self.helper set with contents=[valueA, valueB]
</code></pre>
<p>Next, you modify SUT to add features to it. In order to test those features, the <code>Helper</code> stub needs to include <code>controllerA</code>. But these are useful only in the new tests being added. However, looking at the unit test you already have, it is easiest to to simply add <code>controllerA</code> to <code>self.helper</code>. So, your unit tests look as follows:</p>
<pre><code>class SUTTestCase(unittest.TestCase):
    def setUp(self):
        self.helper = StubHelper()
        self.helper.add_contents([valueA, valueB])
        self.helper.add_controller(controllerA)
        self.sut = SUT(self.helper)
        
    def test_behavior1(self) -&gt; None:
        ...  # Assumes self.helper set with contents=[valueA, valueB]
             # But this test assumes nothing about self.helper's controller

    def test_behavior2(self) -&gt; None:
        ...  # Assumes self.helper set with contents=[valueA, valueB]
             # But this test assumes nothing about self.helper's controller

    def test_behavior3(self) -&gt; None:
        ...  # Assumes self.helper set with contents=[valueA, valueB], and controller=controllerA

    def test_behavior4(self) -&gt; None:
        ...  # Assumes self.helper set with contents=[valueA, valueB], and controller=controllerA
</code></pre>
<p>Then you discover a gap in testing that requires the initial state of the <code>Helper</code> stub to have just the content <code>valueA</code> and include <code>controllerA</code>. Now, when adding this new unit test to suite, the simplest way to do this would be to remove <code>valueB</code> from <code>self.helper</code> at the start of the new test. So, now, your test suite looks as follows:</p>
<pre><code>class SUTTestCase(unittest.TestCase):
    def setUp(self):
        self.helper = StubHelper()
        self.helper.add_contents([valueA, valueB])
        self.helper.add_controller(controllerA)
        self.sut = SUT(self.helper)
        
    def test_behavior1(self) -&gt; None:
        ...  # Assumes self.helper set with contents=[valueA, valueB]
             # But this test assumes nothing about self.helper's controller

    def test_behavior2(self) -&gt; None:
        ...  # Assumes self.helper set with contents=[valueA, valueB]
             # But this test assumes nothing about self.helper's controller

    def test_behavior3(self) -&gt; None:
        ...  # Assumes self.helper set with contents=[valueA, valueB], and controller=controllerA

    def test_behavior4(self) -&gt; None:
        ...  # Assumes self.helper set with contents=[valueA, valueB], and controller=controllerA

    def test_behavior5(self) -&gt; None:
        # Assumes self.helper set with contents=[valueA, valueB] (because of other tests' setup)
        self.helper.remove_content(valueB)
        # Now assumes self.helper set with contents=[valueA]
        ...  
</code></pre>
<p>Let pause here and inspect the state of the unit test. The tests are coupled. Why? Because modifying one test ends up affecting other tests. In the example above, if we replace <code>self.helper.add_contents([valueA, valueB])</code> with <code>self.helper.add_contents(valueA)</code> for tests <code>test_behavior1</code> and <code>test_behavior2</code>, it will result in a failure in <code>test_behavior5</code> because <code>self.helper.remove_content(valueB)</code> will end up throwing an error!</p>
<p>Furthermore, for anyone reading these tests, it is not entirely clear that <code>test_behavior1</code>, and <code>test_behavior2</code> need <code>self.helper</code> to be initialized with values <code>[valueA, valueB]</code>, but do not need for <code>controllerA</code> in <code>self.helper</code>. The preconditions for <code>test_behavior1</code> and <code>test_behavior2</code> are coupled with the preconditions for <code>test_behavior3</code>.</p>
<p>It also results in test incompleteness in that, if we introduce a bug that causes <code>behavior1</code> to fail when <code>self.helper.add_controller(controllerA)</code> is not set, we might not catch that bug because we have initialized the test for <code>behavior1</code> with <code>self.helper.add_controller(controllerA)</code>.</p>
<h3>How to decouple such tests?</h3>
<p>Use the <code>setUp</code> method to simply set up your dependencies, but not to enforce any precondition. Instead, make setting preconditions part of the <em>arrange</em> step of each unit test. You can even encapsulate the precondition setting into a function (with the right parameters) so that the <em>arrange</em> section does not get too bloated, and yet the test code is readable. Consider the following refactoring of the tests:</p>
<pre><code>class SUTTestCase(unittest.TestCase):
    def setUp(self):
        self.helper: Optional[StubHelper] = None
        self.sut = SUT(self.helper)
        
    def prepare_helper(self, contents:List[Value], controller: Optional[Controller]=None) -&gt; None:
        self.helper = StubHelper()
        self.helper.add_contents(contents)
        if controller:
            self.helper.add_controller(controller)
        
    def test_behavior1(self) -&gt; None:
        # Assumes self.helper is a fresh object.
        self.prepare_helper(contents=[valueA, valueB])
        ...

    def test_behavior2(self) -&gt; None:
        # Assumes self.helper is a fresh object.
        self.prepare_helper(contents=[valueA, valueB])
        ...    

    def test_behavior3(self) -&gt; None:
        # Assumes self.helper is a fresh object.
        self.prepare_helper(contents=[valueA, valueB], controller=controllerA)
        ...

    def test_behavior4(self) -&gt; None:
        # Assumes self.helper is a fresh object.
        self.prepare_helper(contents=[valueA, valueB], controller=controllerA)
        ...

    def test_behavior5(self) -&gt; None:
        # Assumes self.helper is a fresh object.
        self.prepare_helper(contents=[valueA], controller=controllerA)
        ...
</code></pre>
<h2>Coupling in parameterized tests</h2>
<p><a href="https://dl.acm.org/doi/10.1145/1095430.1081749">Parameterized tests</a> are a collection of tests that run the same verification, but with different inputs. While this is a very useful feature (available in almost all unit test frameworks), it is also very easy to abuse. A few common ways I have seen it abused is in conjunction with <a href="https://srikanth.sastry.name/dry-unit-tests-are-bad/">DRYing</a>, and the use 'if' checks, and that often results in coupling all the tests denoted by the parameterized list. Consider the following illustration:</p>
<pre><code>class TestOutput(typing.NamedTuple):
    status: StatusEnum
    return_value: typing.Optional[int]
    exception: typing.Optional[Exception]
    ...

class TestSequence(unittest.TestCase):
  
    @parameterized.expand([
        [test_input1, expected_output1],
        [test_input2, expected_output2],
        ...
    ])
    def test_something(self, test_input: str, expected_output: TestOutput) -&gt; None:
        self._run_test(test_input, expected_output)
    
    def _run_test(self, test_input: str, expected_output: TestOutput) -&gt; None:
        sut = SUT(...)
        prepare_sut_for_tests(sut, test_input)
        output = sut.do_something(test_input)
        test_output = make_test_output(output, sut)
        self.assertEquals(expected_output, test_output)

</code></pre>
<p>The above illustration tests the method <code>do_something</code> for various possible inputs. However, note that the outputs (as illustrated in the class <code>TestOutput</code> can have a <code>status</code>, a <code>return_value</code>, or an <code>exception</code>). This means that every instantiation (for each parameter) has to content with the possibility of different types of outputs even though any single test only should have to verify against a single type of output. This couples all the tests verifying <code>do_something</code>, this making it difficult to read and understand. Adding a new test case here becomes tricky because any changes to either <code>prepare_sut_for_tests</code>, or <code>make_test_output</code> now affects all the tests!</p>
<h3>How to decouple parameterized tests?</h3>
<p>There are some fairly straightforward ways to decouple such tests. First, is that we should be very conservative about how we organize these tests. For example, we can group all positive tests and group all negative tests separately; similarly, we can further subgroup the tests based on the type of assertions on the output. In the above example, we can have three subgroups: positive tests that verify only output status, positive tests that verify return value, and negative tests that verify exception. Thus you now have three parameterized test classes that look something like this:</p>
<pre><code>class TestDoSomething(unittest.TestCase):
  
    @parameterized.expand([
        [test_status_input1, expected_status_output1],
        [test_status_input2, expected_status_output2],
        ...
    ])
    def test_something_status_only(
        self, 
        test_input: str, 
        expected_output: StatusEnum
    ) -&gt; None:
        # Arrange
        sut = SUT(...)
        ...  # More 'arrange' code
        
        # Act
        output = sut.do_something(test_input)
        output_status = output.status
        
        # Assert
        self.assertEquals(expected_output, output_status)
        
    @parameterized.expand([
        [test_return_value_input1, expected_return_value_output1],
        [test_return_value_input2, expected_return_value_output2],
        ...
    ])
    def test_something_return_value_only(
        self, 
        test_input: str, 
        expected_output: int
    ) -&gt; None:
        # Arrange
        sut = SUT(...)
        ...  # More 'arrange' code
        
        # Act
        output = sut.do_something(test_input)
        output_status = output.status
        output_value = output.value
        
        # Assert
        self.assertEquals(SomeEnum.SUCCESS, output_status)
        self.assertEquals(expected_output, output_value)

    @parameterized.expand([
        [test_return_value_input1, expected_error_code_output1],
        [test_return_value_input2, expected_error_code_output2],
        ...
    ])
    def test_something_throws_exception(
        self,
        test_input: str,
        expected_error_code: int
    ) -&gt; None:
        # Arrange
        sut = SUT(...)
        ...  # More 'arrange' code
        
        # Act
        with self.assertRaises(SomeSUTException) as exception_context:
            sut.do_something(test_input)
        exception = exception_context.exception
        
        # Assert
        self.assertEquals(excepted_error_code, exception.error_code)
</code></pre>
]]></content>
        <author>
            <name>Srikanth Sastry</name>
            <uri>https://srikanth.sastry.name/</uri>
        </author>
        <published>2022-07-03T00:00:00.000Z</published>
    </entry>
</feed>