Outcome
By the end of this lesson, you will be able to explain what cuts a new rule version, predict how existing recommendations are routed when one does, and distinguish a catalog change from a customer outcome.
| Tier | Engineer |
| JTBD | ”Understand why a batch of my recommendations changed status when nobody in my org touched them.” |
| Personas | Platform Engineer · FinOps Lead |
| Prerequisites | M2.13.L2 · M2.13.L4 |
| Time | 9 minutes |
| Bloom verb | Explain (Understand), Predict (Analyze), Distinguish (Analyze) |
1. Concept
Rules change. When one does, every open recommendation it raised was raised under a definition that no longer exists. Versioning is how that is made visible rather than silent.
What cuts a version, and what does not
A BEHAVIOUR CHANGE CUTS A VERSION a threshold moved a lookback window changed a condition added or removed
The author implements ContractRevisioned and increments:
// RC-1704: expiry threshold 30 -> 60 days. func (*acmExpiringRule) ContractRevision() int { return 1 }
A DECLARATION CHANGE DOES NOT renaming a rule changing its severity recategorising it
Those reach the customer through the registry at render time. Nothing about what the rule DOES has moved.Not implementing the interface is revision 0, so a rule that never changes behaviour never versions.
The asymmetry this fixed
The declaration used to be what got hashed, and it had the logic exactly backwards:
BEFORE a copy-edit to a TITLE -> withdrew and re-raised every open recommendation for that rule a THRESHOLD change -> no version at all
AFTER title edit -> nothing threshold change -> a version, if the author bumpsThe hash is over (rule_id, revision) separated by a NUL byte. Rule ids are free-form and several end in digits, so plain concatenation would let ("RC-1", 2) collide with ("RC-12", 0).
A reverted contract returns to its original number, because the hash is the key rather than a counter. An accidental edit and its undo do not relabel a customer’s work twice.
The gap, named rather than hidden
WHAT IS STILL NOT DETECTED The body of Evaluate, when the author does not bump. A threshold moving 5% -> 10% behind an unchanged declaration produces the same hash and no new version.
It is MANUAL and nothing verifies it was bumped.The alternatives were hashing the compiled function, which is unavailable at runtime and unstable across builds, or a constant every author remembers. The gap is documented rather than papered over with the word “version”.
Routing: by prior status and verdict, not by the edit
When a version is cut, existing recommendations are routed by what they were and what we measured, never by the rule edit alone.
PRIOR STATUS VERDICT OUTCOME─────────────────────────────────────────────────────────────open any rule_retired (reason: rule_version_ changed), then re-raised under the new version in the same pass
dismissed any stays dismissed, re-stamped to the new version
applied verified_adopted UNTOUCHED. Keeps applied AND its original version.
applied verified_unchanged / rule_retired, verified_diverged / re-raised as open verified_revertedAn applied recommendation with no verdict (NULL, pending or abstained) is left alone. Those mean we never looked, cannot yet tell, or never will. None is evidence against the customer, so withdrawing on them would retract an adoption claim on the strength of our own instruments being quiet.
That sentence is the whole ethic of this module in one line.
Why rule_retired rather than optimised
optimised is served in the customer's own tab row and reads as an outcome THEY achieved
rule_retired is hidden from that surface entirely and stays visible internally, rendered as "Withdrawn"Writing optimised for a withdrawal would credit the customer with a saving that came from us editing our own catalogue. The withdrawn row is not frozen by the upsert either, so it comes back open under the new version in the same pass. The audit trail keeps both halves: a close, then a reopen.
Ordering matters here: the routing runs before the engine’s upsert. The upsert re-stamps every non-frozen row to the current version, so running the routing afterwards would find no stale open recommendations at all and the open arm would silently no-op.
Supersede is off by default, for a sharper reason
The applied transition is gated behind a flag, default off, because it overwrites a status the customer set.
WHEN ENABLED, THE SUPERSEDE DOES status applied -> optimised closure_reason_code rule_superseded (its own reason) updated_at PINNED. Nothing about the resource was checked; our catalog moved. resolution_state UNTOUCHED. Carried, never written.The candidate count is measured and logged per org whether or not the flag is set. With it unset the log line is a warning, because those recommendations are attributed to a definition that no longer exists and nothing will correct them until someone turns it on.
“Verified” is carried, not written. A recommendation that was already verified stays verified through the transition; one that was never verified does not acquire a verification from a rule edit. Stamping one would manufacture a saving out of a catalog change, which is precisely the over-claim the two-column model exists to prevent.
rule_superseded is its own closure reason rather than a borrowed one. Borrowing rule_not_applicable put superseded rows inside the auto-dismiss scope, where the next pass would freeze them as zop_dismissed stamped rule_not_fired: a customer’s applied claim laundered into an auto-dismissal over a rule they had nothing to do with.
2. Demo
A threshold change, and what each population did:
CHANGE: RC-1704 certificate-expiry threshold 30 -> 60 days Author bumps ContractRevision to 1. New version cut.
POPULATION BEFORE THE PASS: 214 recommendations
open, 96 -> rule_retired (rule_version_changed), re-raised under v1 in the same pass. Net effect for the customer: the finding is still there, and now reflects the 60-day threshold. Audit trail shows closed + reopened.
dismissed, 44 -> stay dismissed, re-stamped to v1. Nobody's dismissal is undone by our threshold moving. NOT gated by the flag: it changes one metadata column on rows whose status nobody is touching.
applied + verified_adopted, 31 -> UNTOUCHED. Still applied, still stamped v0. They did the work under the old definition and it held. Relabelling it would erase that.
applied + verified_unchanged, 12 -> rule_retired, re-raised open. They claimed it and the estate never agreed, so under a new definition it is simply an open finding again.
applied + no verdict, 24 -> LEFT ALONE. We never looked. Withdrawing would retract their claim on the strength of our own silence.
optimised / other, 7 -> unaffected.
WHAT THE CUSTOMER SEES 96 findings refreshed. 44 dismissals intact. 31 adoptions intact. 12 findings back on the board. No mention of "version" anywhere: that is internal.
WHAT WOULD HAVE HAPPENED UNDER THE OLD SCHEME Nothing, because a threshold change cut no version at all. All 214 rows would still be attributed to a definition that no longer existed, and the 12 false applied claims would have stayed hidden.3. Hands-on (6 min)
1. On the internal console, find a recommendation showing "Raised under rule vN" with N > 0. Rule: ____________ version: ______ Is that string visible on the customer surface? Y / N
2. Find a recommendation with status `rule_retired`. What reason code? ______________________ Would a customer see this row at all? Y / N
3. Look at your `applied` recommendations. How many have NO verdict (NULL/pending/abstained)? ______ Those are the population a version change deliberately leaves alone. Explain in one line why: ______________________________________________
4. Is the supersede flag enabled in your environment? Y / N If N, find the per-org warning log line. It names the candidate count. What is it? ______
5. Write the difference, in your own words, between a recommendation closing because the customer fixed something and one closing because we edited a rule: ______________________________________________4. Knowledge check
Q1
A rule author renames a rule and changes its severity from medium to high. This:
A. Cuts a new version and re-raises every open recommendation
B. Cuts no version
C. Cuts a version only if the severity increases
D. Retires the rule
Show answer
Correct: B. Renaming, re-severity and recategorising are declaration changes that reach the customer through the registry at render time; only a behaviour change (a threshold, a lookback window, an added or removed condition) implements ContractRevisioned and increments. This fixed an asymmetry that ran exactly backwards: a copy-edit to a title used to withdraw and re-raise every open recommendation, while a threshold change produced no version at all.
Q2
A version is cut. A recommendation with status = applied and no verdict (NULL, pending or abstained) is:
A. Retired and re-raised as open
B. Superseded to optimised
C. Re-stamped to the new version and kept applied
D. Left alone
Show answer
Correct: D. NULL, pending and abstained mean we never looked, cannot yet tell, or never will, and none of those is evidence against the customer. Withdrawing would retract an adoption claim on the strength of our own instruments being quiet. Contrast with applied + verified_unchanged, which is retired and re-raised: there we have positive evidence that the estate never agreed with the claim.
Q3
When a withdrawal happens, the row is written as rule_retired rather than optimised because:
A. optimised is reserved for auto-remediation
B. optimised is served in the customer’s own tab row and reads as an outcome they achieved
C. rule_retired rows are simply deleted once 30 days have passed since the withdrawal
D. optimised cannot be reopened
Show answer
Correct: B. Crediting a customer with a saving that came from us editing our own catalogue would be an over-claim, so rule_retired is hidden from that surface and stays visible internally as “Withdrawn”. The same reasoning gives rule_superseded its own closure reason. Borrowing an existing one would put superseded rows inside the auto-dismiss scope. A customer’s applied claim would then be quietly turned into an auto-dismissal, over a rule change they had nothing to do with.
5. Apply
When a batch of recommendations changes state and nobody in your org acted, check whether a rule version was cut before opening a support ticket. The audit trail will show a close with rule_version_changed followed by a reopen in the same pass.
If you author rules, treat ContractRevision as a required part of any behaviour change. Nothing verifies it was bumped, which makes it a review-time habit rather than a compiler guarantee.
Related lessons
- L4: The seven verification buckets
- L2: Status is a claim, evidence is a fact
- L1: The recommendation timeline
- T2.M2.1.L3: How the recommender thinks
Glossary terms touched
ContractRevision · rule_retired · rule_superseded · Rule version