Lesson 08 - Decision¶
Objective¶
Turn a measured drift into an explained, audited recommendation driven by a versioned policy - and understand why the platform refuses to guess why the fleet is degrading.
Prerequisites¶
- Lesson 06 - KPIs and Lesson 07 - Analytics. This lesson continues their story: FL-NORTH is at 1,150 t/h against a 1,250 target, declining ~12.4 t/h per shift.
Concepts covered¶
| Concept | Why it exists |
|---|---|
Policy |
A versioned, governed rule set - not an if buried in a script. |
Rule (PredicateSpecification) |
A named, testable predicate over the evidence. |
Threshold |
Declarative limits that say by how much something breached. |
DecisionPipeline + stages |
RuleEngineStage → ModelStage → ExplanationStage → ranking, composed. |
Recommendation |
Carries triggered_rules, summary, evidence, explanation. |
DecisionAuditTrail |
Append-only accountability record. |
Interface-only RootCauseAnalyzer / WhatIfEngine |
Zero implementations, on purpose (ADR-0007). |
Complete runnable example¶
examples/fundamentals/08_decision/decision.py
The policy is a governed object, versioned like any other:
from mineproductivity.core import PredicateSpecification
from mineproductivity.decision import Policy, Rule, Threshold
rules: dict[str, Rule] = {
"tph_below_target": PredicateSpecification(
lambda ctx: any(r.value is not None and r.value < TPH_TARGET for r in ctx.kpi_results)
),
"tph_trend_declining": PredicateSpecification(
lambda ctx: any(
isinstance(r, TrendResult) and r.direction == "decreasing"
for r in ctx.analytics_results
)
),
}
policy = Policy(
code="PROD.NorthFleetUnderTarget",
rules=rules,
thresholds={
"tph_below_target": Threshold(field="value", comparator="<", limit=TPH_TARGET),
"tph_trend_declining": Threshold(field="trend.slope", comparator="<", limit=0.0),
},
strategy_code="STRATEGY.Threshold",
)
Expected output¶
--- 1. Evidence, gathered -- never recomputed here ---
KPI fact : PROD.TPH = 1,150.0 t/h (target 1,250)
Analytics : TREND.Linear direction='decreasing' r^2=0.974
(the decision layer will consume these two facts and derive neither)
--- 2. A versioned, governed Policy -- not an `if` buried in a script ---
policy 'PROD.NorthFleetUnderTarget' v1.0.0
rules : ['tph_below_target', 'tph_trend_declining']
(versioned: an auditor can ask which policy version produced a call)
--- 3. One audited pipeline run: evaluate rules, then recommend ---
triggered: ('tph_below_target', 'tph_trend_declining')
summary : Policy 'PROD.NorthFleetUnderTarget' triggered by rule(s): tph_below_target, tph_trend_declining
evidence : ('PROD.TPH', 'TREND.Linear')
--- 4. Declarative thresholds confirm the breach independently ---
value < 1250.0 (observed 1,150.0000)
trend.slope < 0.0 (observed -0.0003)
(the rule said 'this fired'; the threshold says 'by how much')
--- 5. Explain and rank -- an unexplained recommendation is not usable ---
rank=1 score=0.755
premise: Rule 'tph_below_target' triggered under policy 'PROD.NorthFleetUnderTarget'
premise: Rule 'tph_trend_declining' triggered under policy 'PROD.NorthFleetUnderTarget'
premise: PROD.TPH = 1150.0
premise: TREND.Linear observed
--- 6. The audit trail is the accountability record ---
1 audited entry for shift A-2026-06-28
(append-only: six months later an incident review can reconstruct
exactly which policy version fired, on what evidence, and why)
--- 7. What this package deliberately does NOT decide ---
decision.RootCauseAnalyzer and decision.WhatIfEngine are interfaces
with ZERO implementations (ADR-0007). The platform will tell you the
fleet is under target and declining -- it will not guess WHY without
a plugin that encodes your site's causal model. That refusal is the
feature: a fabricated root cause is worse than none.
Explanation¶
Why a Policy object instead of an if statement¶
if tph < 1250: alert() works - until you need to answer these questions:
- Which threshold was in force last June? (The target changed in April.)
- Who approved it, and when?
- The alert fired at 03:40; on what evidence?
- Why did the same conditions not fire an alert the week before?
A Policy is versioned (v1.0.0) and governed, so every one of those has an answer. An if in a cron script has none.
Rules and thresholds do different jobs¶
This trips people up. Section 3's rule answers a boolean: did this fire? Section 4's threshold answers a magnitude: by how much? - value < 1250.0 (observed 1,150.0).
You need both. "The fleet breached target" prompts a shrug; "the fleet is 100 t/h under a 1,250 target and losing 12.4 t/h per shift" prompts a decision. The rule triggers the workflow; the threshold gives the human the size of the problem.
An unexplained recommendation is unusable¶
Section 5 is the part most alerting systems get wrong. The ExplanationStage attaches premises: which rules fired, under which policy, on which evidence, with what values. A recommendation that says only "fleet underperforming" gets ignored by shift three. One that says "rule X triggered under policy Y because PROD.TPH = 1150.0 and TREND.Linear was observed decreasing" can be argued with - and being arguable is what makes it trustworthy.
The WeightedScoreRanking then puts it in priority order (rank=1, score=0.755), because a superintendent with fourteen alerts needs to know which one to read first.
The audit trail is the point¶
DecisionAuditTrail is append-only. Six months later, an incident review can reconstruct exactly which policy version fired, on what evidence, and why. This is the same discipline as the event log in Lesson 04, applied to decisions rather than facts.
The refusal (ADR-0007)¶
RootCauseAnalyzer and WhatIfEngine are interfaces with zero implementations.
The platform will tell you FL-NORTH is under target and declining. It will not tell you why. Why not? Because the honest answer depends on your site: is it haul distance growth as the pit deepens? A shovel with a failing swing motor? Wet-season road conditions? Operator rotation? A generic framework guessing among those would be inventing a causal claim it has no basis for - and a fabricated root cause is worse than none, because people act on it.
Encode your site's causal model in a plugin; the contract is waiting.
Best practices¶
- Version your policies. The target will change; you need to know what was in force when.
- Name rules for what they mean (
tph_below_target), notrule_1. The name lands in the audit trail and the explanation. - Always attach thresholds, not just rules - magnitude drives triage.
- Always run the
ExplanationStage. An unexplained recommendation gets ignored. - Consume evidence; never re-derive it. The decision layer reads
KPIResultandTrendResult. - Audit every operationally-actionable run.
Common mistakes¶
| Mistake | What happens | Do this instead |
|---|---|---|
Business rules as inline ifs |
No version, no audit, no explanation | A governed Policy |
| Re-deriving the KPI inside a rule | A second definition of the metric appears | Read ctx.kpi_results |
| Rules without thresholds | "It breached" - but nobody knows if it is 1 t/h or 100 | Add Threshold |
| Skipping explanation | Alert fatigue; the recommendation is ignored | ExplanationStage |
| Expecting a built-in root-cause engine | RootCauseAnalyzer has zero implementations |
Write a site-specific plugin |
| Not auditing | Unreconstructable six months later | DecisionAuditTrail |
Exercises¶
- Change the target. Set
TPH_TARGET = 1_100.0and re-run. Which rules fire now? What does that tell you about why the threshold must be versioned with the policy? - Add a third rule. Fire only when
r_squared > 0.9- i.e. only escalate when the decline is convincing, not noisy. Why is this a better alerting policy than trend direction alone? - Read the audit. Print the full audit entry, including
recorded_at. What could an incident review reconstruct from it? What could it not? - Design the refusal away - then reconsider. Sketch a
RootCauseAnalyzerfor your site. What data would it need that this framework does not have? Does that change your view of ADR-0007? - Rank several. Build three recommendations at different severities and rank them. Does the order match your operational intuition?
Suggested next lesson¶
Lesson 09 - Digital Twin - you have measured, characterised, and decided against history. Now: what is true right now?
See also: decision API Reference · decision package guide · Decision Intelligence design specification · ADR-0007