A small but stable mini-language is embedded as strings in code, parsed and evaluated by ad-hoc functions that grow operator-by-operator. Operator precedence is implicit in eval order; new operators require editing the parser AND the evaluator; static analysis of rules is impossible because they're untyped strings.
Ad-hoc string parsing the agent must read sequentially to understand evaluation semantics. Operator precedence is implicit; bugs in the parser cascade silently across every rule. Static analysis of 'what variables does this rule depend on' requires re-implementing the parser in the analysis tool.
Each grammar rule becomes a class with an interpret(context) method. The AST is the program; building it is explicit (constructors), evaluating it is recursive descent through interpret(). New operators are new classes; the parser (if needed) and the evaluator stay structurally separate.
One class per grammar rule, each with interpret(env). The agent reads the AST shape directly from the construction expression; static traversal returns complete answers; per-rule edits scope to one class file.
Before the pattern
function evaluateRule(ruleString, context) {if (ruleString.includes(' AND ')) {const [left, right] = ruleString.split(' AND ');return evaluateRule(left, context) && evaluateRule(right, context);}if (ruleString.includes(' OR ')) {const [left, right] = ruleString.split(' OR ');return evaluateRule(left, context) || evaluateRule(right, context);}if (ruleString.startsWith('NOT ')) {return !evaluateRule(ruleString.slice(4), context);}return Boolean(context[ruleString]);}const pass = evaluateRule('verified AND NOT banned', user);// Strings as DSL, ad-hoc parser, operator precedence implicit in eval order,// no caching, no static analysis, every consumer re-implements its own dialect.
After the pattern
class Variable {constructor(name) {this.name = name;}interpret(env) {return Boolean(env[this.name]);}}class And {constructor(left, right) {this.left = left;this.right = right;}interpret(env) {return this.left.interpret(env) && this.right.interpret(env);}}class Or {constructor(left, right) {this.left = left;this.right = right;}interpret(env) {return this.left.interpret(env) || this.right.interpret(env);}}class Not {constructor(operand) {this.operand = operand;}interpret(env) {return !this.operand.interpret(env);}}const rule = new And(new Variable('verified'), new Not(new Variable('banned')));const pass = rule.interpret(user);
String-DSL rules are Primitive Obsession at the worst level — the type system sees a string; the runtime expects a domain language. Every consumer either re-implements the parser or imports it; bugs in the embedded parser silently corrupt evaluation across every consumer in lockstep.
String DSLs are opaque to the type system. The agent reasoning about rule correctness must trace through the parser's logic at every consumer; partial verification produces misleading confidence. New operators add cascading edits the agent must verify across every parsing path.
Building the AST in client code is verbose: new And(new Variable('a'), new Or(...)) is harder to read than 'a AND (b OR c)'. The pattern shines when the system controls rule construction (rules come from a config file or builder); it strains when human-readable input is required and a parser is needed anyway.
AST construction in code is verbose and harder to read than the string DSL. The agent's reading cost on the construction site is higher; tracing a runtime error in an AND.interpret requires walking the recursive call chain through the tree, which can be deep.
Operator semantics live in one class per operator; new operators are isolated changes; AST is statically typed and traversable. Static analysis (which variables does this rule depend on?) becomes a single recursive walk of the AST.
Per-rule edits are one-file; static traversal of 'rules using variable X' is a complete enumeration; type system enforces interpret-method existence on every rule class. The agent's verification budget on grammar changes is bounded and structurally provable.
Grammars that grow beyond a handful of operators outgrow the pattern fast — every new operator adds a class, every binary operator with precedence needs explicit AST shape, and traversal logic accumulates. At that point reach for a real parser generator or a proper AST-walker library; don't keep adding interpret() methods to a sprawling class hierarchy.
Grammars growing past ~10 operators turn into class proliferation the agent navigates as a graph rather than a hierarchy. Visitor or AST-walker tooling becomes mandatory at that scale; without it, every traversal-style analysis requires editing every rule class — the cross-cutting problem the pattern was supposed to avoid.