When should a family of near-copy policy rules become one parameterised rule?
answer
- same logic, different value
- one id, one fix, one trend
- does the data differ or the message?
- different remediation means different rule
- beware a parameter that picks a branch
basics
~20 sCollapse near-copies when the logic is identical and only a data value differs - the list of allowed licences, say. Keep them separate when the denial message or the remediation genuinely differ, because that is different logic wearing the same shape.
solid answer
~50 sThe classic case is nine licence rules, one per business unit, differing only in which licences the unit accepts. That is one rule with a parameter - an allowed-licence list - and a parameter set chosen by the consuming unit, evaluated against the artifact's SBOM at promotion. Collapsing gets you one place to fix a bug, one id for waivers and trends, and one review instead of nine slowly diverging copies. The test for whether to collapse: if the only difference is a **value the rule reads**, parameterise it; if the difference is **what you tell the blocked person and what they must change**, they are different rules and deserve different ids and different remediation. The smell that you overshot is a parameter that switches which branch of the rule runs - a boolean or a mode enum. That is two rules hiding inside one.
code
yaml · 6 lines# rule id: licence-class - evaluated against an artifact's SBOM at promotion
parameterSets:
platform: { allowedLicences: [Apache-2.0, MIT, BSD-3-Clause] }
mobile: { allowedLicences: [Apache-2.0, MIT] }
data-science: { allowedLicences: [Apache-2.0, MIT, BSD-3-Clause, MPL-2.0] }
# ...six more units, all reading the same rule under the same idgo deeper
Be ready to explain why nine near-identical rules are worse than one rule with a list of accepted values, and to name the risk: copies drift and bugs get fixed in only some of them.
Explain the collapse test out loud - data differs means parameterise, message and remediation differ means keep them apart - and name the mode-switch smell where a parameter starts choosing branches.
Demonstrate the migration: run old and new side by side, diff the decisions on real inputs, re-point every waiver and suppression, then retire the old ids permanently rather than deleting them.
Own where variation is allowed to live. Decide whether difference between units belongs in parameters, in scope selectors or in separate rules, and set the limit on how many parameter sets the library will carry.
## The problem: a family of near-copies Rule libraries grow near-copies the way codebases grow copy-pasted functions. A licence rule is written for one business unit, a second unit needs the same check with a slightly different accept-list, and by the ninth copy you have nine ids, nine sets of tests, nine review threads and one bug that was fixed in four of them. Every one of the nine is subtly free to drift, and drift in a guardrail is invisible: the copies all pass their own tests, so nothing tells you that three units are enforcing a rule the others quietly stopped enforcing. The fix is to notice that the nine differ in **data**, not in **logic**, and to make the data an input. ## What parameterising actually buys - **One place to fix the logic.** A bug in how the rule reads a licence expression is fixed once and every unit gets it. - **One id.** Waivers, suppressions, control maps and dashboards all key on a single stable name, so "how often did this guardrail fire across the estate" is one query rather than nine unioned ones. - **One review.** Changing the check is a code review of a rule; changing what a unit accepts is a data review of a parameter set. Those are genuinely different decisions with different reviewers, and separating them is a feature. - **A visible difference.** With nine copies, the difference between units is buried in nine files. With one rule, the whole variation across the organisation is a single readable data file. ## The test for whether a family should collapse Ask what actually differs between two candidates: | What differs | Verdict | |---|---| | A value the rule compares against - a list, a threshold, a set of allowed names | Parameterise | | The field the rule reads on the same document | Usually parameterise, if the meaning is the same | | The message shown on denial, and the fix the person must make | **Separate rules** | | Which branch of the logic runs at all | **Separate rules** | The third row is the one people get wrong, and it is the whole reason this is a judgment question rather than a refactoring exercise. A rule that denies a copyleft licence in a distributed binary and a rule that denies an unrecognised licence expression may compare against the same list, but the person who trips them needs entirely different advice: one has to swap a dependency or get a legal exception, the other has to fix the metadata their build produced. Merging them yields one denial message that is vague enough to cover both, which means it helps neither. **A parameter must not absorb near-copies whose denial message and remediation genuinely differ.** ## The mode-switch smell The failure mode of enthusiastic parameterisation is a parameter that stops being data and becomes control flow: a boolean like `strict`, an enum like `mode`, a flag that says which of two shapes the rule should check. The giveaway is that the two paths are never exercised together - no consumer runs both - so the tests for one path do not protect the other, and a change made for one unit ships to a unit that never asked for it and never runs that branch. When you see it, split the rule and give each half its own id, its own message and its own tests. A second smell is a parameter with a default that most consumers never set. A default is a policy decision made silently for everyone who did not read the file, and when it is later tightened, every unconfigured consumer changes behaviour at once. ## Collapsing an existing family without losing exemptions The migration is where this gets sharp, because the nine old ids are already referenced from outside the library. A safe collapse looks like: publish the new parameterised rule alongside the old nine; run both and compare decisions on real inputs until they agree; inventory every waiver and suppression keyed on the nine old ids and re-issue them against the new id **before** the old rules stop being emitted; mark the nine ids retired rather than deleting them; and never mint a new rule under any of the nine names. Skip the inventory step and you get the quiet version of the failure - exemptions that still sit in a register, still look active, and exempt nothing.
- What is the giveaway that a parameter has turned into a mode switch?It selects which branch of the rule executes rather than which value the rule compares against. The two paths are then never exercised by the same consumer, so one unit's tests never cover the other's behaviour, and a change made for one silently ships to the other. Split it into two rules with two ids.
- Nine rules collapse into one - what happens to the nine old ids?They are retired, not recycled. Before the old rules stop being emitted, every waiver, suppression, control-map row and dashboard keyed on those ids has to be inventoried and re-pointed at the new id. Retire the old names permanently so a future rule cannot inherit references written for something else.
- Should a parameter ever have a default value?Only where the default is the safe end of the range, because a default is a policy decision made on behalf of everyone who never read the file. If the default is permissive, unconfigured consumers are silently exempt; if you later tighten it, every unconfigured consumer changes behaviour in one release.
saying these in an interview costs you the question
- Parameterises anything that looks similar, including different remediations
- Adds boolean flags until one rule has four branches
- Copy-pastes a rule per team and fixes bugs in only one copy
- Assumes one parameterised rule forces one shared value on everyone
- Deletes the old rule ids on collapse without re-pointing exemptions