Seven Functions AI Coding Agents Suppress and What I Built to Get Them Back
✓ Human-authored analysis; AI used for formatting and proofreading. Stave is a cloud security reasoning engine. It evaluates infrastructure snapshots against system invariants with static analysis, air-gapped, determin
✓ Human-authored analysis; AI used for formatting and proofreading.
Stave is a cloud security reasoning engine. It evaluates infrastructure snapshots against system invariants with static analysis, air-gapped, deterministic. I deleted 25,000 lines of working code, reverted an entire subsystem I'd spent a full day building, wrote a 246-line architecture document instead of a single line of code, and built a 595-line tool whose only job is classifying test diffs into five categories.
It was not planned. The problems forced it.
Only afterward did I find seven computer science papers spanning 1972 to 1994 that describe the functions I was rediscovering under pressure. These papers predate AI coding agents by decades, but they diagnose the same gaps that AI-assisted development creates today. The difference is that when you're building by hand, these functions degrade slowly. When an AI agent is writing most of your code, they disappear overnight.
This article is about those seven functions and what happens when they're absent.
The setup: a poor man's RAG I didn't know I was building
Before the seven functions, one pattern needs explaining because it runs through everything that follows.
Throughout Stave's development, I wrote detailed git commit messages and maintained a constellation of markdown files: DOCTRINE.md, MEMORY.md, ARCHITECTURE.md, design notes, decision memos. I treated these as a thinking tool. A place to record not just what changed but why, what was tried and rejected, and what constraints future work has to respect.
What I didn't realize until later is that this is a retrieval system. When I start a new session with an AI coding agent, the agent has no memory. It can read the codebase, but the codebase doesn't contain the reasoning behind its own shape. The git history and the markdown notes are the layer that lets the agent or me, three weeks later to reconstruct the theory without re-deriving it from source.
This is not the same as RAG over a codebase. Vector-similarity retrieval finds text chunks that look related. Git log preserves temporal ordering. You get causality: this was tried, then reverted, then replaced with X, and here's why. A vector database loses that narrative arc. What I'd built is closer to a decision journal with full diffs attached than a retrieval index.
There's a deeper mismatch too. RAG as it's used today is passive: ask a question, get an answer. But development isn't question-answering. It's task execution. When I'm about to modify the chain engine, I don't need "what is the chain engine?" I need "what constraints exist on this module, what was tried and reverted, and what will break if I move these types?" That's task-oriented retrieval. The context needed to do something safely, not to know something. RAG never seemed like a good fit for development because it optimizes for the wrong access pattern. Git history with detailed commit messages and design notes optimizes for the right one: "I'm about to change X, what do I need to know first?"
Peter Naur wrote in 1985 that the program isn't the source code, it's the theory in the programmer's head about how the problem maps to the solution. My git messages and markdown notes are that theory, externalized into a form that survives the one thing AI sessions guarantee: context death.
That's Function 2 in the table below. Here are all seven.
The seven functions
Each function names something a developer does often unconsciously that AI-assisted development either hides, skips, or actively destroys. For each one, I'll show the paper that named it, then the specific decision that made me understand it wasn't optional.
1. Comprehends module boundaries — Parnas, 1972
The paper says: Information hiding. A developer only needs the interface, not the implementation. Modules should hide design decisions behind stable boundaries.
What happened in Stave: Three times, the hexagonal architecture boundary was violated by otherwise-correct feature work. The pattern was always the same: a new feature in the core engine imported something from an adapter layer because it was convenient and the types matched.
The first major fix is a "refactor: airgap core from cobra via GlobalSettings + depguard" told the whole story. The core engine had acquired a transitive dependency on Cobra (the CLI framework) through an intermediate adapter package. Banning Cobra in the linter wasn't enough. The dependency re-entered through a different path. The fix required introducing a pure-data struct at the boundary and rewriting the depguard rules from "ban this library" to "enforce this architectural layer."
The second was a performance optimization that placed a cache in internal/adapters/assessmentcache/ and an app-layer workflow imported it, violating the inward dependency rule. Caught by TestHexagonalDependencyDirection, an architecture test that runs on every build.
Why this matters for AI development: An AI agent will cross a module boundary every time it's the shortest path to making the code compile. It doesn't know the boundary exists unless the boundary is machine-enforced. Stave's history shows that a human team violates undocumented boundaries within weeks. An AI agent violates them within minutes.
Resolution: Boundaries that exist only in documentation or team convention are not boundaries. depguard rules and TestHexagonalDependencyDirection enforce Stave's boundaries at build time. The test has caught three violations so far. They all introduced by correct feature work that happened to take the wrong import path.
2. Builds mental model of the domain — Naur, 1985
The paper says: Theory building. Programming is the activity of building a theory of how the problem domain maps to the software. The theory lives in the programmer's head, not in the code.
What happened in Stave: Over nine commits on a single day, I built a catalog conflict analyzer with roughly 4,800 lines of code that would detect contradictions between controls in Stave's security catalog. It compiled. Tests passed. Documentation was thorough.
It produced 590 contradictions on a 630-control catalog. 546 of them were keyed on sub-asset routing fields with false positives caused by the analyzer not understanding that controls operating on different sub-assets don't conflict.
The fix deleted the entire subsystem: +152 / -5,726 lines across 21 files. A decision memo (catalog-authoring-as-service.md) was committed alongside, explaining that catalog authoring is a service offering, not in-product tooling. A generic analyzer cannot produce a signal clean enough to act on without indefinite catalog-shape-specific filter work.
Why this matters for AI development: The conflict-detection code was correct at every level an AI agent can verify: it compiled, it passed type checks, it passed tests, it had documentation. An AI agent following the spec would have kept fixing the 590 false positives indefinitely, because the code was working. The problem was that the question was wrong. Only the person holding the domain theory could recognize that the approach was architecturally misplaced.
The 25,350-line deletion six weeks later removing 10 out-of-scope commands was the same pattern at a larger scale. Working features, intentionally removed, with drift guards (features/scope.yaml with forbids entries) added so they cannot be reintroduced. The deletions stick because the theory "Stave is snapshots in, invariant evaluation, findings out, nothing else" is documented and enforced.
Resolution: The poor man's RAG earns its keep here. The decision memo, the commit message explaining why the deletion happened, and the drift guards are the externalized theory. Six months from now, neither I nor an AI agent will remember the 590-contradiction dead end. But catalog-authoring-as-service.md remembers it, and the forbids entry in scope.yaml mechanically prevents re-entering the dead end even if the reasoning is forgotten.
3. Encounters essential complexity — Brooks, 1986
The paper says: Essential complexity is the irreducible logic of the business problem. Accidental complexity is everything else. No tool eliminates essential complexity. It can only be confronted or hidden.
What happened in Stave: The iam-explain tool was initially built with CEL (Common Expression Language) as the rule evaluation DSL. CEL is Stave's predicate language for controls, so it seemed natural. The CEL version worked, tests passed, Z3 cross-verification passed.
But iam-explain has 8 rules. Each rule is a 4-6 line Go closure. The CEL version imposed DSL parsing, environment setup, and type adaptation overhead on a problem that didn't need it. The refactor replaced the CEL implementation with plain Go, preserving the CEL version under alt-cel/ as a pedagogical comparison artifact. The essential complexity was 8 simple rules; the CEL infrastructure was accidental complexity.
The opposite case of essential complexity that couldn't be avoided showed up during the chain-engine untangle (part 5 of a 9-commit sequence). Moving types from risk/ to report/ hit a real import cycle: risk → report → evaluation → risk. No amount of aliasing or interface extraction could fix it. The solution required creating a new leaf package (internal/core/findings/) that both sides could import without importing each other. The import cycle was essential complexity in the dependency graph. It reflected a conceptual dependency that had to be resolved.
Why this matters for AI development: An AI agent defaults to the most direct solution. If CEL is the project's DSL, it will use CEL for everything. It can't distinguish "this tool should use CEL because it's the project standard" from "this tool shouldn't use CEL because 8 closures are simpler than a DSL runtime for 8 rules." That distinction requires encountering the complexity, feeling the weight of the accidental overhead against the simplicity of the essential problem which is short-circuited by AI agents.
4. Records intent as narrative — Knuth, 1984
The paper says: Literate programming. Programs should be literature that explains why to humans, not just what to machines.
What happened in Stave: Three audit findings arrived with properties that operators commonly assume Stave checks but that fall outside its architectural scope. The response was a 246-line architecture document (docs/architecture/boundaries.md) explaining where the trust boundary sits. Zero lines of code. The document's core statement: "Three properties operators commonly assume are checked by Stave fall outside that function. They are not bugs. They are decisions the architecture forces."
DOCTRINE.md codified nine "X over Y" principles, each citing an existing artifact in the repo. Each one references a concrete file (chains/, controls/, cmd/forge/, schemas/) that already embodies it.
TERMINOLOGY.md locks down the vocabulary: one word per concept, used consistently across the CLI, docs, MCP tool descriptions, and external articles.
Why this matters for AI development: An AI agent can generate code that does the right thing. It cannot generate a document that explains why the system doesn't do something because not-doing is invisible in the codebase. The 246-line boundaries document has prevented more bugs than most feature commits, because every new contributor reads it and understands not just what the system does, but what it deliberately refuses to do.
This is also where the decision journal pattern pays its highest dividend. When I deleted the conflict-detection subsystem, two durable design notes survived the 5,726-line deletion. Six months later, those notes are the only artifact that explains why a seemingly-obvious feature doesn't exist. Without them, the next AI session or the next engineer would rebuild it.
5. Simplifies logic for verification — Dijkstra, 1972
The paper says: The humble programmer writes code simple enough to be obviously correct, rather than complex enough to hide errors.
What happened in Stave: Stave's core invariant is determinism: same inputs + same --eval-time flag produces byte-identical output. Every run. This is the condition that lets the verdict be evidence instead of a vendor's claim.
Three commits tell the story of what determinism costs:
A 2-line fix. Go's map iteration is deliberately randomized, causing CompoundFinding.AttackStages to appear in different orders across runs with identical inputs. Two lines added: a sort before serialization.
A 1-file logic fix that rippled through 325 golden files. The --eval-time flag was silently ignored; referenceTime always used the last snapshot's CapturedAt. Correcting the clock changed the output of every single fixture.
A 595-line tool (regenerate-goldens) that regenerates all golden test fixtures and classifies every diff into five categories: CLEAN, FINGERPRINT-ONLY, METADATA-ONLY, BEHAVIORAL, MIXED. The tool exists because a 325-file golden update is meaningless without a way to distinguish catalog-growth noise from real behavioral changes.
Why this matters for AI development: An AI agent will write code that produces correct output. It will not spontaneously add a sort to a map iteration, because the output is correct, just non-deterministic. It will not build a 595-line diff classifier, because the tests pass. They just pass differently each time. Determinism is a property of the system, not of any individual function, and maintaining it requires tooling whose sole purpose is making verification tractable.
6. Embeds behavioral promises — Liskov, 1994
The paper says: Behavioral subtyping. Components must honor contracts, not just type shapes, but the behavioral promises the downstream consumer depends on.
What happened in Stave: The most dangerous gap in the system was a silent false negative. When an extractor forgot to populate a field, the CEL predicate field: properties.foo op: eq value: true evaluated to false (not unsafe) and the control silently never fired. The system reported "no violations found" when the real answer was "the extractor didn't even populate the fields the controls check."
The fix added a per-asset-type schema registry with Required/Optional field declarations and an ingest-side validator that emits structured warnings when required fields are absent. This is Liskov's point: a schema that validates shape (JSON Schema additionalProperties: false) but not behavioral completeness gives false confidence. Any producer claiming to implement the observation contract must populate the fields the downstream predicate engine depends on.
A separate fix addressed the output side: 22 fields the FindingDTO emitted on the wire were missing from the JSON Schema contract, and field names had drifted (first_seen_unsafe in schema vs first_unsafe_at in DTO). The fix aligned the Draft 2020-12 schemas with the real wire format and flipped validation from opt-in to default-on.
Why this matters for AI development: An AI agent checking "does this conform to the schema?" will get a passing answer from a structurally-valid but behaviorally-incomplete observation. The schema says the JSON is well-formed. The system says no violations. The reality is that the controls couldn't evaluate because the fields were empty. The OpenAPI ecosystem solved this for HTTP APIs. You can't claim to implement an endpoint if your response doesn't match the schema. Stave's observation contract applies the same principle to security observations: conformance means behavioral completeness, not just type correctness.
7. Maintains structural integrity — Lehman, 1980
The paper says: Increasing complexity. Unless active work is done to reduce it, a system's complexity grows with every change until the cost of further change exceeds the value.
What happened in Stave: The canonical example is the index.yaml refactor. A 1,892-line monolithic file was the central pack registry. Every control addition required editing it in 3-4 places. Two commits first deleted the 970-line hand-maintained controls: section by deriving metadata from the embedded filesystem at load time, then split the remaining packs into 44 per-pack YAML files. The manifest went from 1,892 lines to 90.
The 25,350-line scope-narrowing deletion is the more aggressive form: 10 working commands removed, not because they were broken, but because they pulled the architecture in too many directions. Drift guards were added simultaneously so the removals stick permanently.
Why this matters for AI development: An AI agent adds code. It does not remove working features because they compromise architectural coherence. It does not refactor a 1,892-line manifest into 90 lines plus derived metadata, because the manifest works fine. It's just an entropy magnet where every change risks desynchronization. Lehman's law says entropy grows unless you actively invest in reducing it. AI agents are entropy accelerators: they add capability faster than any human, but they cannot recognize when the system needs less capability, not more.
The bridge: from survival tactics to Reasoning Specs
Everything above was bottom-up. I built solutions to problems I hit while building Stave. Only afterward did I recognize that seven papers, spanning 22 years of computer science research, had already named every one of those problems.
The top-down view of reading the papers and mapping them to the solutions I'd already built revealed that the solutions weren't isolated fixes. They're instances of a pattern: externalize the function into a machine-readable, machine-enforceable specification so that it survives context death.
That pattern has a precedent at scale: OpenAPI. Before OpenAPI, HTTP API contracts lived in documentation, team knowledge, and hope. OpenAPI externalized the contract into a spec that tools could validate, generate from, and enforce. The entire API ecosystem of code generators, mock servers, contract testing, SDK generation exists because the spec became the source of truth rather than the implementation.
Reasoning Specs are the same move applied to the verification and intent layer that OpenAPI doesn't cover. OpenAPI tells you the shape of an HTTP response. It doesn't tell you the safety invariants the system must maintain, the behavioral promises the producer must honor, or the architectural constraints future changes must respect.
What I arrived at through building Stave with contracts as boundaries (obs.v0.1), mechanical enforcement (depguard, golden tests, schema validation), rationale preservation (design notes, DOCTRINE.md, decision memos), and drift guards (scope.yaml forbids entries) — maps onto Reasoning Specs as the formalization that connects "here's what I built to survive" with "here's what the theory says you need."
The connection isn't that Reasoning Specs solve the seven-function problem directly. They provide the specification layer that makes the solutions portable. Without a spec, every team rediscovers these functions independently, the way I did by getting burned. With a spec, the functions are encoded in a form that tools, agents, and new team members can read without re-deriving the theory from the codebase.
How this played out: the reasoning YAML timeline
The abstract argument above of "externalize the function into a spec" is easier to see in a concrete sequence. Here's how Stave's control format went from a single-engine YAML predicate to a universal fact source consumed by eight reasoning engines, and where the seven functions show up in the timeline.
Day 1: The unsafe_predicate YAML format shipped as the control definition language. A declarative predicate tree of any/all nesting over field comparisons with a closed operator set (eq, ne, in, missing, present, contains, gt, lt, and a few more). One engine evaluated these predicates: in-process Google CEL.
unsafe_predicate:
all:
- field: properties.storage.access.external_account_ids
op: not_subset_of_field
value_from_param: allowed_accounts
At this point, the format existed because it was the simplest way to express security invariants. No grand plan.
April 2026: The compound chain engine shipped, and the identity shifted from scanner to reasoning engine. The compiler analogy: controls are programs, the engine is the compiler, observations are input data. This is Function 3 (Brooks), encountering the essential complexity of the domain and naming it, rather than hiding it behind a tool abstraction.
May 5, 2026 — the SIR morning: Three commits in a single morning created the Structured Intermediate Representation: ControlFact, AssetFact, IdentityFact, TemporalFacts, plus forbidden_state and intent_rationale fields added to the control schema. 941 lines. The SIR projected the YAML predicates plus observation data into vendor-neutral triples.
May 6, 2026 — the pivot: The entire Python Z3 solver backend (728 lines) was deleted. The architectural decision, stated in the commit message: "Stave's evaluator is in-process Google CEL. There is no alternate backend. Z3 reasoning is illustrated in a Go example, not as a swappable solver backend."
Multiple functions intersect simultaneously here. It's Function 7 (Lehman). Deleting working code to maintain structural integrity. It's Function 2 (Naur). The domain model holder recognizing that owning the solver is the wrong abstraction. It's Function 3 (Brooks), distinguishing the essential function (producing facts) from the accidental complexity (running solvers). And the commit message is Function 4 (Knuth), recording the intent so no future contributor rebuilds the solver backend.
The design principle that emerged: "Facts only, never rules. The exporter writes what is true about the configuration; external programs decide what it MEANS."
May 7, 2026 — five engines in five hours: With --format jsonl and --format smt2 added to the export command, five external engines shipped in a single afternoon: Z3/cvc5 (satisfiability), Clingo (answer set enumeration), PySAT (boolean compound analysis), Soufflé (Datalog blast-radius counts), and Prolog (proof trees). Three more followed the next day (PRISM, TLA+, Game Theory).
Each engine answers a structurally different question from the same fact base. Z3 asks "does an unsafe path exist?" Soufflé asks "how wide is the blast radius?" Prolog asks "why is this path reachable?" The YAML predicate format made this possible because any/all nesting over field comparisons is isomorphic to propositional logic. It translates mechanically into SMT-LIB assertions, Datalog facts, ASP atoms, and Prolog clauses.
Why this matters for the seven-function argument: None of this was planned on Day 1. The unsafe_predicate format was chosen because it was the simplest way to write security controls. The SIR emerged four months later because the compound chain engine needed a richer fact representation. The solver deletion happened because the domain model was wrong, owning the solver fought the architecture instead of serving it. And eight engines shipped in two days because the format, arrived at bottom-up, turned out to be the right abstraction: a declarative predicate tree that any formal system can consume.
An AI agent could have written any individual piece. The YAML parser, the SIR builder, the SMT-LIB serializer. What it could not have done is deleted the solver backend. That deletion required Function 2 (the domain theory that says "Stave is a fact exporter, not a reasoning engine"), Function 3 (the recognition that solver ownership is accidental complexity), Function 4 (the commit message that prevents the solver from being rebuilt), and Function 7 (the willingness to delete working code). The seven functions aren't independent. They converge at the architectural decisions that define the system.
The Ralph loop test
There's a pattern in AI-assisted development called the Ralph loop: a single agent works autonomously against a specification in an infinite loop, each iteration starting with a fresh context window. The agent selects a task, implements it, the context resets, the loop restarts.
The Ralph loop is the most aggressive possible test of whether the seven functions have been successfully externalized. A memoryless agent is guaranteed to lose every function that isn't encoded in a machine-readable artifact. If the loop converges, if the codebase moves toward the spec over repeated iterations. That's evidence the spec carries enough of the theory. If the loop drifts, some essential function still lives in a human head that the artifacts failed to capture.
Stave is well-positioned for this test. The boundaries are machine-enforced (Function 1). The domain theory is externalized in design notes and decision memos (Function 2). The essential/accidental complexity distinction is documented (Function 3). Intent is recorded in DOCTRINE.md and architecture docs (Function 4). Verification is mechanized through golden tests and determinism invariants (Function 5). Behavioral promises are schema-enforced with completeness checking (Function 6). Structural integrity is maintained through drift guards and scope enforcement (Function 7).
Whether a Ralph loop would converge on Stave is an empirical question I haven't tested. But the architecture is designed so that a memoryless agent has everything it needs to make correct local decisions because every function that would otherwise require memory has been externalized into an artifact the agent can read.
The model is a subsystem, not the system
There's a misconception in the current market that a better AI model will solve the problems described in this article. It won't because these aren't model problems. They're system problems.
An AI model is a subsystem in a larger system that includes the developer, the codebase, the specification artifacts, the enforcement tooling, the retrieval layer, and the feedback loops between all of them. The seven functions I've described are properties of that larger system, not of the model. No amount of model improvement gives you machine-enforced boundaries (Function 1), externalized domain theory (Function 2), or drift guards that prevent deleted features from returning (Function 7). Those are system-level properties that have to be designed and built. They don't emerge from a smarter model.
The framing the industry uses reveals the blind spot. When a frontier model is deployed inside a broader system, the surrounding infrastructure gets called a harness. That word choice reduces the boundary enforcement, the specification layer, the retrieval system, the verification tooling, and the feedback mechanisms to a container that holds the model. But those components aren't scaffolding. They're the subsystems that determine whether the model's output converges toward correctness or drifts toward entropy. Calling them a harness is like calling the brakes, steering, and navigation system a harness for the engine.
The problems that arise when humans and machines collaborate on a codebase are context death, boundary violations, theory loss, silent false negatives, entropy accumulation are interaction problems between subsystems. They require systems thinking, not model thinking. Stave's solver deletion is the clearest example: the model can write the solver, write the tests for the solver, and write documentation for the solver. What it cannot do is recognize that the solver's existence fights the architecture because that recognition requires a system-level view that no individual subsystem holds.
The seven functions are the system-level properties that make AI-assisted development work. They're not waiting for a better model. They're waiting for engineers to build the system around the model that the model cannot build for itself.
What this means for you
If you're building with AI coding agents:
Your git messages are your theory's persistence layer. Write them like they're the only artifact that will survive between sessions, because they are.
Your architecture documents are the specs that keep a memoryless agent from re-making decisions you already made and rejected. The 246-line document that explains what your system doesn't do will prevent more bugs than your next feature.
Your linter rules and architecture tests are the only boundaries that exist. If a boundary isn't machine-enforced, an AI agent will cross it the first time it's the shortest path to a passing test.
Your drift guards are the forbids entries, the scope files, the depguard rules that prevent deleted features from returning make deletions permanent. Without them, the next AI session rebuilds what you deliberately removed.
If you're building tools for the AI development era, the contract is the product. Not the CLI, API or SDK. The contract is the machine-readable specification that encodes what the system promises, what it refuses, and what it requires from its inputs is the thing that survives when everything else resets.
Stave is open source at stave. The seven papers referenced: Parnas (1972), Naur (1985), Brooks (1986), Knuth (1984), Dijkstra (1972), Liskov (1994), Lehman (1980).
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.