Open the source of any static analyzer and you will eventually find the list. It has different names in different tools, a rules directory, a query pack, a set of taint specifications, but it is always the same object: an enumeration of the functions, patterns, and sinks the tool knows are dangerous. The list is the product. Everything else is machinery for applying it.
Acutis does not have the list. Not a short one, not a hidden one, not a sensible-defaults one. The verifier is name-blind: it does not know that eval is dangerous, that executeQuery is a SQL sink, or that encodeForHTML is a sanitizer. It cannot be told these things in its own code, because we hold ourselves to a rule that forbids it. This post is about why we tie our hands that way, what it costs, and the one thing it demands from the outside in exchange.
The listWhat an enumeration actually buys, and what it silently costs
The enumeration approach works, for what it covers. If your rule pack lists eval() as a code-execution sink, the tool flags eval(user_input), and it is right to. The trouble is not the entries that are present. It is the shape of the thing.
An enumeration has four properties its authors cannot escape. Its recall is bounded: anything not on the list is invisible, so a sanitizer from a framework nobody enumerated is treated as if it does nothing, and a sink nobody thought of passes. Its precision is naming-fragile: eval() is caught, but getattr(__builtins__, "e" + "val")(payload) may not be, because the check was against a name and the name changed. It is miscalibrated on new code: these lists were tuned over years against the patterns humans get wrong, and AI-generated code is wrong in different places, so the same rules that were quiet on human code fire constantly on machine code. And it is non-compositional: adding a new vulnerability class means adding a new list, because there is no underlying model that a new class falls out of. Five classes, five enumerations, maintained forever.
None of these are bugs in a particular tool. They are the enumeration itself, showing through. You cannot patch your way out of the shape.
An enumeration is a bet that the authors thought of everything dangerous. That bet has never once paid out.
The inversionMove the knowledge out of the tool
Here is the move. The reason the tool needs a list is that the tool is trying to know what the code means. It sees foo(bar) and has to decide, from the outside, whether foo is dangerous and whether bar is tainted. The list is how it guesses.
But in the setting Acutis is built for, there is something present that already knows what the code means: the AI that just wrote it. The assistant that generated db.query(sql) knows that line runs a database query and knows where sql came from. That knowledge exists at the moment of generation and is thrown away everywhere else. Acutis picks it up.
So we invert the arrangement. The AI declares the security-relevant semantics of the code it produced, as a contract: these calls are sources of user input, these are dangerous sinks and what kind, these are transforms and what they neutralize. Acutis verifies that contract against the actual code, formally. The knowledge lives with the party that has it. The verifier stays ignorant of names on purpose, and checks structure instead.
This is the whole reason the verifier can be name-blind. It never has to recognize encodeForHTML, because the contract told it that call neutralizes HTML metacharacters, and its only job is to check that the neutralized value is what actually reaches the sink. Swap in a sanitizer from a framework released tomorrow and nothing in Acutis needs to change: the AI declares it, Acutis verifies it, the same day.
The coreA lattice is what replaces the list
If there is no list, what is there? A property lattice. Every value in the code carries a set of security properties drawn from a small fixed vocabulary: may be user-controlled, may contain HTML metacharacters, may contain SQL metacharacters, may reach an internal host, and so on. The bottom of the lattice is the empty set, a value with no dangerous properties, provably safe. The top is every property at once, a value about which nothing safe can be assumed.
Three rules move properties through the code, and they are the entire engine. A literal starts at the bottom: a constant string carries nothing. A function parameter starts at the top: absent a declaration, an incoming value could be anything, so it is treated as maximally dangerous. And concatenation takes the union: joining two values yields a value with the properties of both. From those, taint flows through the program by pure lattice operations, no names consulted. A declared transform is the only thing that removes properties, and only the specific ones it claims to remove, verified against its own body.
Notice what this vocabulary is made of. Not function names, but kinds of danger. "May contain SQL metacharacters" is a property of a value, true or false regardless of which library produced it. That is what makes the model compositional: a new vulnerability class is usually a new property and a constraint on where values carrying it may flow, not a new list of the world's functions. The core stays small while the domain it covers stays open.
The disciplineHolding the line inside our own code
A principle like this survives only if it is enforced against the people who could quietly break it, which is us. It is genuinely tempting, mid-bugfix, to write if name in KNOWN_SANITIZERS and move on. It would work on the test in front of you. It would also reintroduce every limitation above through the back door, and it would do so invisibly, because one small list looks harmless.
So the rule is absolute and it is checked: no set of API-name strings, no branching on hardcoded function names, no regex over user identifiers, no stdlib defaults, no silent fallback to "probably safe." An edit to the verifier that pattern-matches to any of those shapes is treated the same as a failing test. The discipline is the moat. The moment we allow ourselves one exception, we are a scanner with extra steps.
Zero enumeration is not free, and pretending it is would be exactly the kind of edge-sanding this blog exists to avoid. The cost is that Acutis is only as good as the contract it is handed. Hand it a wrong declaration and it faithfully verifies the wrong thing. We do not treat that as a footnote; it is why the failure mode is deliberately set to over-block on missing or suspicious information, and why the contract itself is the thing we test hardest. A verifier with no list has moved the hard problem, from "did the authors enumerate everything" to "did the declarer declare honestly." We think that is a far better problem to have, because the declarer is the one party that actually knows.
The payoffFinite core, unbounded domain
What you get for the discipline is a verifier that is small and fixed on the inside and open-ended on the outside. The lattice, the three transfer rules, and the boundary checks are a bounded thing we can reason about completely. The set of codebases they apply to is not bounded, because it is not gated by an enumeration we had to write in advance. A framework Acutis has never seen works on day one, provided the AI can describe what its calls do, which is exactly the knowledge the AI had when it wrote them.
That is the sentence to remember: finite spec, unbounded domain. Conventional scanners are the opposite, an unbounded spec (the ever-growing list) chasing a domain it can never quite cover. We would rather own a small thing that is correct than a large thing that is merely current.