Build a Content-Moderation Rule Engine With Boolean Chains and Nested Groups
StripeSoftware Engineer · Technical ScreenLast updated
Grow a small text-driven policy engine, in stages, from one comparison per post into a full boolean expression language with left-to-right AND/OR and nested parentheses, while treating an AI pair-programmer as a junior you're delegating narrow, well-specified tasks to.
You're prototyping the policy layer for a community forum: every new post needs to land in one of three buckets — visible immediately, held for a moderator to look at, or taken down outright — based on a set of editable rules that non-engineers will eventually tweak without redeploying code.
Model a post as a plain mapping of field name to value, for example {"author_strikes": 2, "region": "EU", "word_count": 40, "hours_since_signup": 3} — treat this as illustrative, not a schema you should hardcode against. Each rule pairs a human-editable condition string with one of SHOW, HOLD, TAKEDOWN. Given an ordered rule list and a batch of posts, your engine returns one bucket per post, preserving input order.
A condition string starts as a bare comparison — a field, one of ==, !=, >, >=, <, <=, and a literal, where the literal is either a bare number or a quoted string. The scope of what a condition string can express grows across three stages below; treat each stage as additive, not a replacement for the last, and lean toward a design where stage four (whatever it turns out to be) would slot in without touching stages one through three.
You'll be pairing with an AI coding assistant for this exercise. Use it the way you'd use a capable but unsupervised junior engineer: you own the module boundaries and the contracts between them, you hand it one clearly-bounded piece at a time, and you read what it hands back before wiring it in — the goal is a codebase you could explain line by line, not one big generated dump you're hoping works.
Clarifying Questions
- If two rules both match the same post, does list order decide, does
TAKEDOWNalways win regardless of position, or something else — and what bucket applies when nothing matches at all? - Will rule strings always come in pre-written, or does the engine itself need to accept and validate newly typed-in rule text?
- If a condition references a field the post doesn't have, or compares a string field with
>against a number, what should that comparison decide — silently false, or a hard error? - Does a rule with a typo get rejected the moment it's loaded, or only discovered the first time a post tries to match against it?
- Can numeric fields hold fractional values, or are they always whole numbers?
Stage 1 — One comparison, decide the batch
Support exactly one comparison per rule and return a bucket for every post in the batch. word_count > 50 should match a post with word_count 75 but not one at exactly 50.
Even for a single comparison, you have four distinct jobs: turning text into tokens, building a representation of one comparison, checking that comparison against one post, and picking a bucket once you know which rules matched. Keep these as four separable pieces now so stages 2 and 3 only ever touch two of them.
Stage 2 — Chained AND/OR, no precedence at all
Let a condition chain multiple comparisons with AND and OR, for example region == "EU" OR region == "UK" AND hours_since_signup < 1. Critically, there is no precedence between AND and OR here — everything groups strictly left to right, so that example means (region == "EU" OR region == "UK") AND hours_since_signup < 1, full stop.
A precedence-climbing parser is the wrong instinct here, since the grammar you're implementing explicitly has no precedence. Instead, carry a single running result forward: evaluate the first comparison, then for each AND/OR that follows, combine the operator with whatever has accumulated so far — never with just the term immediately before it.
Stage 3 — Parentheses change the grouping
Let parentheses override the default left-to-right grouping, as in (region == "EU" OR region == "UK") AND author_strikes == 0, nested as deep as needed.
Don't give parenthesized groups their own parsing logic — a '(' should hand control straight back to whatever function parses a full condition from scratch, then expect a matching ')' when that call returns. Nesting then falls out without any extra code.
Worked Example
rules = [
("author_strikes > 3", "TAKEDOWN"),
("(region == \"EU\" OR region == \"UK\") AND hours_since_signup < 1", "HOLD"),
]
post = {"id": "q7", "author_strikes": 1, "region": "UK", "hours_since_signup": 0}
# -> "HOLD"
Hints
Hint 1
Split the engine into four independent pieces from the start: a tokenizer (text to tokens), a parser (tokens to a small tree of comparison/AND/OR nodes), an evaluator (tree + one post -> true/false), and a rule-matcher (ordered rule list + one post -> action). Each later part only touches the parser and evaluator.
Hint 2
Represent AND/OR chains without precedence as a flat left-to-right fold, not a precedence-climbing parser — precedence climbing is the wrong tool when the grammar explicitly has no precedence.
Hint 3
For nested parentheses, write the parser so that encountering '(' recursively calls the same top-level 'parse a condition' function and expects a matching ')' — this handles arbitrary nesting without extra cases.
Hint 4
When directing an AI assistant, hand it one function at a time (e.g. 'write the tokenizer for this token grammar') with the surrounding interfaces already fixed by you, rather than asking it to produce the whole engine in one shot.
Follow-up questions
- How would you add a `NOT` operator cleanly into the grammar and parser without restructuring the AND/OR handling you already built?
- How would you change the design to support a rule referencing a field on a *related* object (e.g. the author's account age, not the post's), without the evaluator needing to know about every possible related-object type?
- If rule text came from untrusted users rather than internal config, what would you change about parsing and evaluation to avoid a crafted rule causing excessive CPU time or a crash?
- How would you add unit tests for the parser alone, independent of the evaluator, to pin down exactly how left-to-right grouping and nested parentheses are parsed?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.