About this course
<p>In <a href="https://www.freecodecamp.org/news/measure-conformance-operational-specification-execution/">my previous article</a>, I explored operational conformance: how to evaluate whether observed evidence satisfies an operational specification.</p>
<p>That gives us a useful point-in-time question:</p>
<blockquote>
<p>Does the system satisfy the specification right now?</p>
</blockquote>
<p>But production systems don't stay still.</p>
<p>Traffic changes. Dependencies degrade. Certificates expire. Configuration is modified. Feature flags move. Data grows. People make manual changes. Agents take actions. Infrastructure is replaced. And so on.</p>
<p>A system can be conformant immediately after a deployment and stop being conformant hours or days later.</p>
<p>That's the problem I want to explore here.</p>
<p>Operational drift is the divergence between the operational specification and the state we continue to observe over time.</p>
<p>This is broader than configuration drift. Configuration drift asks whether infrastructure or configuration differs from a declared state.</p>
<p>Operational drift asks whether the system still behaves within the boundaries we intended, even if the configuration has not changed at all.</p>
<p>A service can have exactly the same configuration it had yesterday and still drift operationally because latency increased, error rate degraded, replica availability fell, a dependency became unhealthy, a policy is no longer being enforced, or evidence disappeared.</p>
<p>In this article, I’ll show you how to model operational drift, establish a conformance baseline, reevaluate a system over time, distinguish temporary noise from meaningful divergence, preserve drift evidence, classify severity, and decide when drift should trigger an alert, an investigation, or an automated response.</p>
<p>The central idea is this: conformance tells you whether reality matches intent at a point in time. Drift tells you whether that relationship is changing.</p>
<h2 id="heading-prerequisites">Prerequisites</h2>
<p>You should be comfortable with:</p>
<ul>
<li><p>software architecture</p>
</li>
<li><p>TypeScript or a similar language</p>
</li>
<li><p>observability</p>
</li>
<li><p>operational specifications</p>
</li>
<li><p>evidence-based evaluation</p>
</li>
<li><p>conformance</p>
</li>
<li><p>basic monitoring concepts</p>
</li>
</ul>
<p>You don't need a specific infrastructure platform.</p>
<p>The examples use a deployment scenario, but the same model applies to infrastructure, data pipelines, backup systems, access controls, AI agents, and other operational workflows.</p>
<h2 id="heading-table-of-contents">Table of Contents</h2>
<ul>
<li><p><a href="#heading-prerequisites">Prerequisites</a></p>
</li>
<li><p><a href="#heading-what-operational-drift-means">What Operational Drift Means</a></p>
</li>
<li><p><a href="#heading-operational-drift-isnt-the-same-as-configuration-drift">Operational Drift Isn't the Same as Configuration Drift</a></p>
</li>
<li><p><a href="#heading-start-with-a-conformance-baseline">Start with a Conformance Baseline</a></p>
</li>
<li><p><a href="#heading-observe-the-same-constraints-over-time">Observe the Same Constraints Over Time</a></p>
</li>
<li><p><a href="#heading-model-drift-explicitly">Model Drift Explicitly</a></p>
</li>
<li><p><a href="#heading-temporary-variation-isnt-always-drift">Temporary Variation Isn't Always Drift</a></p>
</li>
<li><p><a href="#heading-use-windows-instead-of-single-samples">Use Windows Instead of Single Samples</a></p>
</li>
<li><p><a href="#heading-distinguish-drift-from-missing-evidence">Distinguish Drift from Missing Evidence</a></p>
</li>
<li><p><a href="#heading-add-severity-and-persistence">Add Severity and Persistence</a></p>
</li>
<li><p><a href="#heading-detect-directional-drift-before-a-threshold-fails">Detect Directional Drift Before a Threshold Fails</a></p>
</li>
<li><p><a href="#heading-preserve-the-evidence-behind-drift">Preserve the Evidence Behind Drift</a></p>
</li>
<li><p><a href="#heading-a-reusable-drift-evaluator">A Reusable Drift Evaluator</a></p>
</li>
<li><p><a href="#heading-a-small-end-to-end-example">A Small End-to-End Example</a></p>
</li>
<li><p><a href="#heading-how-drift-detection-applies-to-multiple-executors">How Drift Detection Applies to Multiple Executors</a></p>
</li>
<li><p><a href="#heading-how-drift-detection-applies-to-ai-agents">How Drift Detection Applies to AI Agents</a></p>
</li>
<li><p><a href="#heading-when-drift-should-trigger-action">When Drift Should Trigger Action</a></p>
</li>
<li><p><a href="#heading-what-operational-drift-cant-tell-you">What Operational Drift Can't Tell You</a></p>
</li>
<li><p><a href="#heading-a-practical-drift-detection-workflow">A Practical Drift Detection Workflow</a></p>
</li>
<li><p><a href="#heading-from-point-in-time-conformance-to-continuous-verification">From Point-in-Time Conformance to Continuous Verification</a></p>
</li>
<li><p><a href="#heading-conclusion">Conclusion</a></p>
</li>
</ul>
<h2 id="heading-what-operational-drift-means">What Operational Drift Means</h2>
<p>Suppose a deployment finishes at 10:00.</p>
<p>The operational specification says:</p>
<pre><code class="language-text">version = v42
available replicas >= 3
error rate <= 1%
p95 latency <= 400 ms
</code></pre>
<p>Immediately after deployment, the evidence says:</p>
<pre><code class="language-text">version = v42
available replicas = 3
error rate = 0.4%
p95 latency = 280 ms
</code></pre>
<p>The system is conformant.</p>
<p>Now suppose that at 14:00 the evidence says:</p>
<pre><code class="language-text">version = v42
available replicas = 3
error rate = 0.8%
p95 latency = 370 ms
</code></pre>
<p>The system is still conformant, but something has changed.</p>
<p>The error rate and latency are moving toward their limits.</p>
<p>At 16:00:</p>
<pre><code class="language-text">version = v42
available replicas = 3
error rate = 1.4%
p95 latency = 460 ms
</code></pre>
<p>Now the system is no longer conformant.</p>
<p>The operational specification didn't change. The executor did not necessarily run again. But reality moved.</p>
<p>That's operational drift.</p>
<p>A useful way to think about it is:</p>
<pre><code class="language-text">Specification
↓
Expected operational boundaries
↓
Observed state at T0
↓
Observed state at T1
↓
Observed state at T2
↓
Drift evaluation
</code></pre>
<p>The key idea is temporal: conformance is a snapshot, while drift is a relationship across snapshots.</p>
<h2 id="heading-operational-drift-isnt-the-same-as-configuration-drift">Operational Drift Isn't the Same as Configuration Drift</h2>
<p>Configuration drift is likely a familiar infrastructure concept to you.</p>
<p>By <strong>configuration drift</strong>, I mean a difference between the configuration or infrastructure state you declared and the state that actually exists in the environment.</p>
<p>For example:</p>
<pre><code class="language-text">desired replicas = 3
actual replicas = 2
</code></pre>
<p>Here, the declared state says the service should have three replicas, but the running environment only has two. The configuration and the actual state no longer match.</p>
<p>Or:</p>
<pre><code class="language-text">Terraform state != deployed infrastructure
</code></pre>
<p>In that case, infrastructure has changed outside the expected Terraform-managed state, or Terraform's record no longer matches what's actually deployed.</p>
<p>Those are both configuration drift because the mismatch is between a declared configuration and the environment that should reflect it.</p>
<p>That's useful, but it's narrower.</p>
<p>Operational drift can exist even when configuration is unchanged.</p>
<p>Consider this specification:</p>
<pre><code class="language-text">error rate <= 1%
</code></pre>
<p>The deployment configuration may still be identical to yesterday.</p>
<p>Yet traffic mix may have changed, a downstream API may have slowed down, a database index may have degraded, a certificate may be approaching expiration, or a queue backlog may have grown.</p>
<p>So:</p>
<pre><code class="language-text">configuration drift
=
declared configuration != actual configuration
</code></pre>
<p>while:</p>
<pre><code class="language-text">operational drift
=
observed operational state != intended operational boundaries
</code></pre>
<p>The two can overlap, but they're not the same thing.</p>
<p>That distinction matters because production failures are often behavioral before they are configurational.</p>
<h2 id="heading-start-with-a-conformance-baseline">Start with a Conformance Baseline</h2>
<p>Drift needs a reference point. The easiest reference is a known conformant state.</p>
<p>For example:</p>
<pre><code class="language-typescript">type DeploymentEvidence = {
version?: string;
replicas?: number;
errorRate?: number;
p95LatencyMs?: number;
};
</code></pre>
<p>Suppose the system was evaluated after deployment:</p>
<pre><code class="language-typescript">const baseline: DeploymentEvidence = {
version: "v42",
replicas: 3,
errorRate: 0.004,
p95LatencyMs: 280,
};
</code></pre>
<p>and all required constraints passed.</p>
<p>That baseline gives you two useful things.</p>
<p>First, you know the system satisfied the specification at that point. And second, you have a concrete evidence snapshot against which later observations can be compared.</p>
<p>This doesn't mean later evidence must match the baseline exactly. That would be too strict.</p>
<p>It means later evidence can be evaluated in two ways:</p>
<pre><code class="language-text">Does it still satisfy the specification?
How is it changing relative to the baseline?
</code></pre>
<p>Those are different questions. Both are useful.</p>
<h2 id="heading-observe-the-same-constraints-over-time">Observe the Same Constraints Over Time</h2>
<p>Suppose you collect evidence every hour:</p>
<pre><code class="language-typescript">type TimedEvidence = {
timestamp: string;
evidence: DeploymentEvidence;
};
</code></pre>
<p>For example:</p>
<pre><code class="language-typescript">const observations: TimedEvidence[] = [
{
timestamp: "10:00",
evidence: {
version: "v42",
replicas: 3,
errorRate: 0.004,
p95LatencyMs: 280,
},
},
{
timestamp: "12:00",
evidence: {
version: "v42",
replicas: 3,
errorRate: 0.006,
p95LatencyMs: 320,
},
},
{
timestamp: "14:00",
evidence: {
version: "v42",
replicas: 3,
errorRate: 0.008,
p95LatencyMs: 370,
},
},
];
</code></pre>
<p>Each observation can be evaluated against the same specification.</p>
<p>This gives you a conformance sequence:</p>
<pre><code class="language-text">10:00 PASS
12:00 PASS
14:00 PASS
</code></pre>
<p>At first glance, everything looks healthy. But a closer look reveals that the evidence is moving in one direction.</p>
<p>That's where drift detection becomes more interesting than ordinary threshold monitoring.</p>
<h2 id="heading-model-drift-explicitly">Model Drift Explicitly</h2>
<p>A minimal drift result might look like this:</p>
<pre><code class="language-typescript">type DriftStatus =
| "STABLE"
| "DEGRADING"
| "VIOLATED"
| "UNKNOWN";
</code></pre>
<p>This gives the evaluator four explicit states. <code>STABLE</code> means the observed behavior remains within the expected range without meaningful deterioration. <code>DEGRADING</code> means the system is still within its allowed boundary but is moving in an unfavorable direction. <code>VIOLATED</code> means the current evidence has crossed the specification's limit. <code>UNKNOWN</code> means there's not enough evidence to classify the state safely.</p>
<p>Then:</p>
<pre><code class="language-typescript">type DriftResult = {
constraint: string;
status: DriftStatus;
baseline?: number;
current?: number;
limit?: number;
};
</code></pre>
<p><code>DriftResult</code> keeps the classification together with the values that explain it. <code>baseline</code> is the known-good reference, <code>current</code> is the latest observation, and <code>limit</code> is the boundary defined by the specification. Those fields are optional because some constraints may not be numeric or some evidence may be missing.</p>
<p>For latency:</p>
<pre><code class="language-text">baseline = 280
current = 370
limit = 400
</code></pre>
<p>This example says latency started from a known-good value of 280 ms, is now 370 ms, and must remain at or below 400 ms. The constraint hasn't failed, but the current value is much closer to the boundary.</p>
<p>That may be worth labeling:</p>
<pre><code class="language-text">DEGRADING
</code></pre>
<p>A drift model therefore doesn't replace conformance. It adds another dimension.</p>
<p>You can have:</p>
<pre><code class="language-text">conformance = PASS
drift = DEGRADING
</code></pre>
<p>That combination is often more actionable than either signal alone.</p>
<h2 id="heading-temporary-variation-isnt-always-drift">Temporary Variation Isn't Always Drift</h2>
<p>Production systems are noisy.</p>
<p>One slow request doesn't mean the system is drifting. One missing sample doesn't mean the service is unhealthy. And one temporary spike doesn't necessarily justify intervention.</p>
<p>Suppose latency samples are:</p>
<pre><code class="language-text">280
290
420
285
295
</code></pre>
<p>The <code>420</code> sample crosses the limit. But the system immediately returns to normal.</p>
<p>Compare that with:</p>
<pre><code class="language-text">280
310
340
370
395
</code></pre>
<p>The second sequence may never cross the limit. But it shows persistent degradation.</p>
<p>That's why drift shouldn't be defined as:</p>
<pre><code class="language-text">current != baseline
</code></pre>
<p>or even:</p>
<pre><code class="language-text">one sample crossed threshold
</code></pre>
<p>Drift is more useful when it accounts for persistence and direction.</p>
<h2 id="heading-use-windows-instead-of-single-samples">Use Windows Instead of Single Samples</h2>
<p>A better approach is to evaluate observations over a window.</p>
<p>For example:</p>
<pre><code class="language-typescript">function average(
values: number[]
): number {
return (
values.reduce(
(sum, value) => sum + value,
0
) / values.length
);
}
</code></pre>
<p>Now suppose you evaluate the last five latency observations:</p>
<pre><code class="language-typescript">const latencyWindow = [
300,
320,
340,
360,
380,
];
const currentAverage =
average(latencyWindow);
</code></pre>
<p>This smooths temporary noise.</p>
<p>You can compare that window with an earlier window:</p>
<pre><code class="language-typescript">const baselineWindow = [
270,
275,
280,
285,
290,
];
const baselineAverage =
average(baselineWindow);
</code></pre>
<p>The earlier window gives you a short historical reference for typical behavior. The later window represents the more recent operating state. Comparing the two averages helps you see whether the whole signal has shifted, rather than reacting to one unusual sample.</p>
<p>Then calculate the change:</p>
<pre><code class="language-typescript">const delta =
currentAverage -
baselineAverage;
</code></pre>
<p>If <code>baselineAverage</code> is 280 ms and <code>currentAverage</code> is 340 ms, the delta is <code>+60 ms</code>. That doesn't mean the latency constraint has failed, but it does show that recent latency is materially worse than the earlier window.</p>
<p>This doesn't prove a problem exists, but it gives you a more stable signal.</p>
<p>The exact window size should come from the operational context.</p>
<p>A five-minute window may make sense for latency. A one-day window may make more sense for storage growth.</p>
<h2 id="heading-distinguish-drift-from-missing-evidence">Distinguish Drift from Missing Evidence</h2>
<p>This follows the same principle as conformance.</p>
<p>Suppose latency evidence disappears.</p>
<p>That's not the same as:</p>
<pre><code class="language-text">latency is stable
</code></pre>
<p>and it's not the same as:</p>
<pre><code class="language-text">latency violated the threshold
</code></pre>
<p>It is:</p>
<pre><code class="language-text">UNKNOWN
</code></pre>
<p>For example:</p>
<pre><code class="language-typescript">function evaluateLatencyDrift(
baseline: number | undefined,
current: number | undefined,
limit: number
): DriftStatus {
if (
baseline === undefined ||
current === undefined
) {
return "UNKNOWN";
}
if (current > limit) {
return "VIOLATED";
}
if (current > baseline) {
return "DEGRADING";
}
return "STABLE";
}
</code></pre>
<p>The function evaluates the cases in a deliberate order. First, if either the baseline or the current observation is missing, it returns <code>UNKNOWN</code> because there's not enough evidence to make a safe comparison. If evidence exists and the current value is already above the allowed limit, it returns <code>VIOLATED</code>. If the value is still below the limit but worse than the baseline, it returns <code>DEGRADING</code>. Only when none of those conditions apply does it return <code>STABLE</code>.</p>
<p>This is still simplistic because any increase over the baseline becomes <code>DEGRADING</code>, even if the change is tiny or temporary. In a real system, you would usually combine this with tolerances, windows, persistence, or trend rules. But it preserves an important rule: absence of evidence isn't evidence of stability.</p>
<p>That becomes especially important when the observability system itself is degraded.</p>
<h2 id="heading-add-severity-and-persistence">Add Severity and Persistence</h2>
<p>Drift becomes more useful when you know how important the constraint is and how long the drift has persisted.</p>
<p>For example:</p>
<pre><code class="language-typescript">type DriftSeverity =
| "INFO"
| "WARNING"
| "CRITICAL";
type DriftObservation = {
constraint: string;
status: DriftStatus;
severity: DriftSeverity;
consecutiveWindows: number;
};
</code></pre>
<p>Here, <code>severity</code> describes the operational impact of the affected constraint, while <code>consecutiveWindows</code> records how many observation windows have produced the same drift condition in a row. Keeping both matters because one brief warning is different from a warning that has persisted for eight consecutive windows, and a critical replica violation may deserve action even if it has only appeared twice.</p>
<p>Now you can distinguish:</p>
<pre><code class="language-text">latency
DEGRADING
WARNING
1 window
</code></pre>
<p>from:</p>
<pre><code class="language-text">latency
DEGRADING
WARNING
8 windows
</code></pre>
<p>and:</p>
<pre><code class="language-text">replicas
VIOLATED
CRITICAL
2 windows
</code></pre>
<p>The second signal is more persistent, but the third is more severe.</p>
<p>That helps determine what response is appropriate.</p>
<h2 id="heading-detect-directional-drift-before-a-threshold-fails">Detect Directional Drift Before a Threshold Fails</h2>
<p>One of the most useful aspects of drift detection is recognizing directional movement.</p>
<p>Suppose the error-rate limit is:</p>
<pre><code class="language-text">1%
</code></pre>
<p>and observations are:</p>
<pre><code class="language-text">0.2%
0.4%
0.6%
0.8%
</code></pre>
<p>Nothing has failed yet, but the trend is clear.</p>
<p>A simple slope can capture that direction.</p>
<p>For example:</p>
<pre><code class="language-typescript">function slope(
values: number[]
): number {
if (values.length < 2) {
return 0;
}
return (
values[values.length - 1] -
values[0]
) / (values.length - 1);
}
</code></pre>
<p>This helper looks at the first and last values in the series and divides their difference by the number of intervals between them. It's a deliberately simple trend indicator: a positive result means the metric increased across the window, a negative result means it decreased, and a value near zero means it stayed roughly flat.</p>
<p>Then:</p>
<pre><code class="language-typescript">const errorRates = [
0.002,
0.004,
0.006,
0.008,
];
const trend =
slope(errorRates);
</code></pre>
<p>Here, the error rate rises from <code>0.002</code> to <code>0.008</code>, so <code>trend</code> is positive. The code isn't trying to predict the future or perform full statistical analysis. It's only making the direction of change explicit so the system can notice sustained movement toward the <code>1%</code> boundary before the boundary is actually crossed.</p>
<p>A positive trend doesn't automatically mean failure, but it tells you the system is moving toward the boundary.</p>
<p>This is where drift detection differs from basic alerting.</p>
<p>Basic threshold alert:</p>
<pre><code class="language-text">tell me when error rate > 1%
</code></pre>
<p>Drift detection:</p>
<pre><code class="language-text">tell me when error rate is persistently moving toward 1%
</code></pre>
<p>The second can give you time to investigate before the system becomes non-conformant.</p>
<h2 id="heading-preserve-the-evidence-behind-drift">Preserve the Evidence Behind Drift</h2>
<p>A drift result should be explainable.</p>
<p>For example:</p>
<pre><code class="language-json">{
"constraint": "error-rate",
"status": "DEGRADING",
"baseline": 0.004,
"current": 0.008,
"limit": 0.01,
"window": "10m",
"consecutiveWindows": 4,
"observedAt": "2026-10-13T18:00:00Z"
}
</code></pre>
<p>This is better than:</p>
<pre><code class="language-text">drift detected
</code></pre>
<p>because someone can answer:</p>
<pre><code class="language-text">what changed?
compared with what?
how close are we to the limit?
how long has this been happening?
when was it observed?
</code></pre>
<p>That matters for debugging, as well as for auditability.</p>
<h2 id="heading-a-reusable-drift-evaluator">A Reusable Drift Evaluator</h2>
<p>We can generalize the model.</p>
<p>First define a drift rule:</p>
<pre><code class="language-typescript">type DriftRule<T> = {
name: string;
severity: DriftSeverity;
evaluate(
baseline: T,
current: T
): DriftStatus;
};
</code></pre>
<p>Then define a drift specification:</p>
<pre><code class="language-typescript">type DriftSpec<T> = {
name: string;
rules: DriftRule<T>[];
};
</code></pre>
<p>And an evaluator:</p>
<pre><code class="language-typescript">type DriftRuleResult = {
name: string;
status: DriftStatus;
severity: DriftSeverity;
};
function evaluateDrift<T>(
spec: DriftSpec<T>,
baseline: T,
current: T
): DriftRuleResult[] {
return spec.rules.map(
(rule) => ({
name: rule.name,
status:
rule.evaluate(
baseline,
current
),
severity:
rule.severity,
})
);
}
</code></pre>
<p>The evaluator doesn't need to understand latency, error rate, replicas, or certificates.</p>
<p>Each rule owns its own comparison logic. That makes the engine reusable.</p>
<h2 id="heading-a-small-end-to-end-example">A Small End-to-End Example</h2>
<p>Let’s create a drift specification for a deployment.</p>
<p>Start with evidence:</p>
<pre><code class="language-typescript">type DeploymentEvidence = {
replicas?: number;
errorRate?: number;
p95LatencyMs?: number;
};
</code></pre>
<p>Now define rules:</p>
<pre><code class="language-typescript">const driftRules:
DriftRule<DeploymentEvidence>[] = [
{
name: "replicas",
severity: "CRITICAL",
evaluate: (
baseline,
current
) => {
if (
baseline.replicas === undefined ||
current.replicas === undefined
) {
return "UNKNOWN";
}
if (current.replicas < 3) {
return "VIOLATED";
}
if (
current.replicas <
baseline.replicas
) {
return "DEGRADING";
}
return "STABLE";
},
},
{
name: "error-rate",
severity: "CRITICAL",
evaluate: (
baseline,
current
) => {
if (
baseline.errorRate === undefined ||
current.errorRate === undefined
) {
return "UNKNOWN";
}
if (current.errorRate > 0.01) {
return "VIOLATED";
}
if (
current.errorRate >
baseline.errorRate * 1.5
) {
return "DEGRADING";
}
return "STABLE";
},
},
{
name: "latency",
severity: "WARNING",
evaluate: (
baseline,
current
) => {
if (
baseline.p95LatencyMs === undefined ||
current.p95LatencyMs === undefined
) {
return "UNKNOWN";
}
if (
current.p95LatencyMs > 400
) {
return "VIOLATED";
}
if (
current.p95LatencyMs >
baseline.p95LatencyMs * 1.25
) {
return "DEGRADING";
}
return "STABLE";
},
},
];
</code></pre>
<p>Create the drift specification:</p>
<pre><code class="language-typescript">const driftSpec:
DriftSpec<DeploymentEvidence> = {
name: "orders-runtime-drift",
rules: driftRules,
};
</code></pre>
<p>Now define baseline and current evidence:</p>
<pre><code class="language-typescript">const baseline:
DeploymentEvidence = {
replicas: 3,
errorRate: 0.004,
p95LatencyMs: 280,
};
const current:
DeploymentEvidence = {
replicas: 3,
errorRate: 0.007,
p95LatencyMs: 360,
};
</code></pre>
<p>Evaluate:</p>
<pre><code class="language-typescript">const drift =
evaluateDrift(
driftSpec,
baseline,
current
);
</code></pre>
<p>Possible output:</p>
<pre><code class="language-text">replicas STABLE CRITICAL
error-rate DEGRADING CRITICAL
latency DEGRADING WARNING
</code></pre>
<p>The system may still be conformant, but it's no longer operationally stable relative to its baseline.</p>
<p>That's valuable information.</p>
<h2 id="heading-how-drift-detection-applies-to-multiple-executors">How Drift Detection Applies to Multiple Executors</h2>
<p>Suppose two executors both successfully deployed the same service.</p>
<p>Immediately after deployment:</p>
<pre><code class="language-text">Executor A → conformant
Executor B → conformant
</code></pre>
<p>That doesn't mean their long-term operational behavior is identical.</p>
<p>Over time:</p>
<pre><code class="language-text">Executor A
error rate stable
latency stable
replicas stable
Executor B
error rate rising
latency rising
replicas stable
</code></pre>
<p>Both executors satisfied the initial specification, but their downstream operational behavior is different.</p>
<p>Drift gives you another comparison dimension.</p>
<p>You can ask:</p>
<pre><code class="language-text">Which executor produces more stable outcomes?
Which one drifts more often?
Which one produces more unknown evidence?
Which one needs recovery more frequently?
</code></pre>
<p>This is especially useful when executor choice is dynamic.</p>
<h2 id="heading-how-drift-detection-applies-to-ai-agents">How Drift Detection Applies to AI Agents</h2>
<p>AI agents introduce another kind of variability.</p>
<p>An agent may produce different plans for the same objective.</p>
<p>For example:</p>
<pre><code class="language-text">Run 1:
scale
deploy
observe
Run 2:
deploy canary
observe
expand
Run 3:
create parallel environment
switch traffic
</code></pre>
<p>Each plan may be valid, and each may initially conform.</p>
<p>But the long-term outcomes can differ.</p>
<p>One plan may produce a stable system, while another may create a system that slowly moves toward a boundary.</p>
<p>This gives us a useful separation:</p>
<pre><code class="language-text">agent chooses the execution path
specification defines acceptable boundaries
evidence describes the observed state
conformance evaluates the current state
drift evaluates how that state changes over time
</code></pre>
<p>That allows agent autonomy without making the agent the only judge of its own outcomes.</p>
<h2 id="heading-when-drift-should-trigger-action">When Drift Should Trigger Action</h2>
<p>Not every drift signal should trigger automation.</p>
<p>A useful response model might be:</p>
<pre><code class="language-text">STABLE
→ no action
DEGRADING + low severity
→ observe
DEGRADING + persistent
→ investigate
VIOLATED + warning
→ alert
VIOLATED + critical
→ rollback / remediate / escalate
UNKNOWN
→ collect evidence / block risky automation
</code></pre>
<p>The exact policy depends on the operation.</p>
<p>For example, automatically restarting a stateless service may be acceptable. But automatically rolling back a database migration may not be.</p>
<p>Drift detection should therefore produce evidence and classification. Policy should decide what action is permitted. That keeps observation, evaluation, decision, and execution separate.</p>
<h2 id="heading-what-operational-drift-cant-tell-you">What Operational Drift Can't Tell You</h2>
<p>Operational drift doesn't automatically tell you why the system changed.</p>
<p>Suppose latency rises from:</p>
<pre><code class="language-text">280 ms
</code></pre>
<p>to:</p>
<pre><code class="language-text">390 ms
</code></pre>
<p>Drift detection can tell you:</p>
<pre><code class="language-text">latency is degrading
</code></pre>
<p>It can't automatically prove the cause.</p>
<p>The cause might be traffic growth, database contention, a downstream dependency, network degradation, a code regression, or resource exhaustion.</p>
<p>Drift is therefore a detection mechanism. It's not root-cause analysis.</p>
<p>The baseline can also be wrong.</p>
<p>If the baseline was captured during an unusually good period, normal behavior may look like degradation.</p>
<p>So drift quality depends on good specifications, good evidence, good baselines, appropriate windows, and correct rules.</p>
<h2 id="heading-a-practical-drift-detection-workflow">A Practical Drift Detection Workflow</h2>
<p>If you want to introduce operational drift detection into an existing system, I would use this sequence.</p>
<h3 id="heading-1-start-with-a-conformant-operation">1. Start with a Conformant Operation</h3>
<p>Pick an operation where you already know the system satisfies the specification.</p>
<h3 id="heading-2-preserve-the-baseline-evidence">2. Preserve the Baseline Evidence</h3>
<p>Store the evidence associated with that known-good state.</p>
<h3 id="heading-3-identify-drift-sensitive-constraints">3. Identify Drift-Sensitive Constraints</h3>
<p>Not every constraint needs trend analysis.</p>
<p>Start with things like:</p>
<pre><code class="language-text">latency
error rate
capacity
queue depth
availability
certificate age
backup freshness
</code></pre>
<h3 id="heading-4-define-observation-windows">4. Define Observation Windows</h3>
<p>Choose windows based on operational behavior.</p>
<h3 id="heading-5-define-stable-degrading-violated-and-unknown">5. Define Stable, Degrading, Violated, and Unknown</h3>
<p>Make the states explicit.</p>
<h3 id="heading-6-add-severity">6. Add Severity</h3>
<p>Separate informational drift from critical drift.</p>
<h3 id="heading-7-add-persistence">7. Add Persistence</h3>
<p>Avoid reacting to one noisy sample.</p>
<h3 id="heading-8-preserve-evidence">8. Preserve Evidence</h3>
<p>Store baseline, current state, thresholds, windows, and timestamps.</p>
<h3 id="heading-9-separate-detection-from-response">9. Separate Detection from Response</h3>
<p>Don't make every drift signal trigger an automatic action.</p>
<h3 id="heading-10-revisit-the-model">10. Revisit the Model</h3>
<p>If the system produces constant false positives, improve the rule. If it misses real degradation, improve the evidence or the window.</p>
<p>Don't simply widen thresholds until everything looks stable.</p>
<h2 id="heading-from-point-in-time-conformance-to-continuous-verification">From Point-in-Time Conformance to Continuous Verification</h2>
<p>Conformance gives us this:</p>
<pre><code class="language-text">Specification
+
Evidence
↓
Evaluation
↓
PASS / FAIL / UNKNOWN
</code></pre>
<p>Drift adds time:</p>
<pre><code class="language-text">Specification
+
Evidence at T0
+
Evidence at T1
+
Evidence at T2
↓
Drift Evaluation
↓
STABLE / DEGRADING / VIOLATED / UNKNOWN
</code></pre>
<p>That changes the operational model.</p>
<p>We're no longer asking only:</p>
<blockquote>
<p>Did the operation succeed?</p>
</blockquote>
<p>or:</p>
<blockquote>
<p>Is the system conformant now?</p>
</blockquote>
<p>We can also ask:</p>
<blockquote>
<p>Is the system moving away from the intended state?</p>
</blockquote>
<p>That's a much stronger foundation for operational control, especially when systems become more autonomous.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Operational conformance tells us whether observed evidence satisfies the specification at a point in time.</p>
<p>Operational drift tells us whether that relationship is changing.</p>
<p>A useful drift model should preserve:</p>
<pre><code class="language-text">baseline evidence
current evidence
constraint boundaries
observation windows
direction of change
severity
persistence
unknown states
timestamps
</code></pre>
<p>That gives us a way to distinguish temporary noise, persistent degradation, actual violation, and missing evidence.</p>
<p>The executor doesn't need to know any of this. The executor performs the work. The specification defines the intended boundaries.</p>
<p>Evidence tells us what's happening. Conformance tells us whether the current state is acceptable. Drift tells us whether the system is moving away from that acceptable state.</p>
<p>At this point, the pieces are starting to form a larger operational model:</p>
<pre><code class="language-text">Intent
↓
Specification
↓
Executor
↓
Evidence
↓
Conformance
↓
Drift
↓
Governed response
</code></pre>
<p>The next step is to put those pieces together explicitly.</p>
<p>That's where I want to define <strong>Specification-Driven Operations</strong>, or SDOps, as a complete operating model.</p>