Your Rego deny rule skips its message for waived repositories — what does that silence cost you?
answer
- waived output equals clean output
- three states, one empty result
- you can no longer count exceptions
- relabel instead of suppress
- not over undefined is true
basics
~20 sA waived repository then produces output identical to a compliant one: no messages, exit zero. You lose the ability to tell an exception from a pass, to count how many waivers are live, and to notice when the rule stopped firing at all.
solid answer
~50 sGuarding the body with something like `not data.waivers[input.repo]` makes the deny message disappear for waived repositories, which is the same output a compliant pipeline produces. Three things go with it: you can no longer count active exceptions from the gate's results, you cannot distinguish "excepted" from "the rule never matched", and the gate's own output stops being evidence that the control was applied. The cheap fix is to stop suppressing and start relabelling — keep the deny for the unwaived case, and route the waived case to a `warn` message that names the repository and the waiver, so it still appears in the report without failing the build. If the consumer needs it machine-readable, an allow-shaped decision object carrying the reasons and a waived flag preserves the distinction properly. One thing the guard does get right: `not` over an absent waivers document is true, so a missing waiver list makes the deny fire rather than vanish.
go deeper
Understand that a rule which skips its message produces exactly the same output as one that found nothing wrong, so the two cases cannot be told apart afterwards.
Explain the mechanics: how the guard suppresses the set element, and why negation over an absent data document evaluates to true and therefore still denies.
Show the operational consequence — exceptions become uncountable and a third state hides in the empty result — and propose relabelling to warn rather than inventing a new subsystem.
Argue about where exception visibility should live at all: in the rule's output, in the data the rule reads, or in the system that consumes the decision.
## The shape in question The rule enforces a floor on how long build logs and job artifacts are retained. Someone asks for an exception, so a guard goes into the body: ``` deny contains msg if { some job_name, job in input.jobs job.artifacts.retention_days < min_days not data.waivers[input.repo] msg := sprintf(...) } ``` It works. The waived repository ships. And the gate's output for that repository is now byte-identical to the output for a repository that is fully compliant: no messages, exit zero, green check. ## What you gave up **1. Exceptions become uncountable from the outside.** The set of live waivers now exists only in the waiver data, never in the gate's results. Nobody reading a month of pipeline runs can tell how many builds shipped under an exception. The question "how many teams are currently excepted from the retention floor?" can no longer be answered by looking at what the control produced — only by reading the input the control consumed. **2. Excepted and never-evaluated collapse into one output.** A deny set already conflates "compliant" with "nothing matched". Adding a suppression branch adds a third state to the same empty result. When someone later asks why a repository never failed the retention gate, the honest answer is that you cannot tell from the results whether it was compliant, waived, or whether the rule silently stopped matching after a schema change. **3. The gate's output stops being evidence.** A control's own output is the cheapest artefact you have showing that it ran and what it decided. Suppression turns the exception into a non-event, so the record of it lives elsewhere or nowhere. ## What to do instead **Relabel rather than suppress.** Keep one rule that fires on the underlying condition, and let the waiver decide *which name it lands under* rather than whether it exists: - unwaived and non-compliant, the message goes into `deny` and the build fails; - waived and non-compliant, the message goes into `warn`, naming the repository and stating that a waiver is being applied. Both appear in the report. Because the runner reads `warn` messages as warnings rather than failures, the waived team still ships, and the exception is now visible in every run instead of invisible in all of them. The cost is a second rule body — usually a shared helper called from both — which is a fair price for the distinction. **Or change shape.** If a downstream system needs the outcome machine-readable rather than printed, the deny set is the wrong container. A complete rule producing a decision object — the outcome, the reasons, and a flag saying an exception was applied — carries all three states explicitly, at the cost of writing the query and the harness yourself instead of relying on name lookup. ## The one thing the guard gets right `not data.waivers[input.repo]` behaves correctly when the waiver document is missing entirely. In Rego, `not expr` is true when `expr` is false *or undefined*. If `data.waivers` was never loaded, the inner reference is undefined, the negation is true, and the deny fires. A broken waiver-data path therefore over-blocks rather than under-blocks — the safe direction, and worth saying out loud, because the instinctive worry is the opposite. Writing the guard the other way round is where it goes wrong. A positive condition over the same missing document — asserting that the repository is *not equal* to something under `data.waivers` — leaves the whole body undefined when the document is absent, and an undefined body contributes nothing to the set. Same intent, opposite failure direction. ## What an interviewer is listening for Not "suppression is bad". They want you to notice that the output of a waived run is indistinguishable from a clean run, to say which questions that makes unanswerable later, and to propose the relabel — keep the message, change its severity name — rather than reaching for a new subsystem. Naming the `not`-over-undefined behaviour, in the correct direction, is what separates someone who has written these rules from someone who has read about them.
- Why does routing the waived case to warn rather than deny preserve what you need?The message still exists in the report, so a waived run looks different from a clean run and the exception is visible on every build. Because the runner treats warn messages as warnings rather than failures, the waived team is not blocked while the record keeps being produced.
- The waivers document fails to load one morning. What happens to the guarded rule?The deny fires everywhere it otherwise would. `not` over an undefined reference evaluates to true, so every repository is treated as unwaived and the gate over-blocks. Noisy, but the safe direction — the opposite of what a positively phrased guard would do.
- When is a decision object the right answer instead of relabelling?When something downstream has to consume the outcome rather than a human reading it — a dashboard counting exceptions, or a system that must branch on waived versus compliant. Strings in a set are for people; a structured result with an explicit outcome and reasons is for programs.
saying these in an interview costs you the question
- Sees no difference between a waived run and a clean run
- Suppresses the message and calls the exception documented
- Assumes a missing waiver document makes the deny vanish
- Adds a second copy of the rule instead of a shared helper
- Treats a green gate as proof no exception was applied