Policy Evaluation Flow
Overview
The Policy Evaluation Flow describes how Stella Ops applies the 8-state reachability lattice to vulnerability findings, incorporating VEX statements, reachability analysis, and confidence scoring to produce deterministic verdicts. This is the core decision-making flow that determines whether a container image meets security requirements.
The Policy Engine evaluates a compiled policy (authored in the stella-dsl@1 DSL) deterministically against advisory/VEX/SBOM inputs. Each finding receives a status (affected, not_affected, suppressed, ignored, deferred, warned, or blocked), an optional 0.0–1.0 confidence score with a five-factor breakdown, and — when the Evidence-Weighted Score enricher is enabled — a 0–100 EWS score and triage bucket (ActNow, ScheduleNext, Investigate, Watchlist).
Business Value: Consistent, explainable security verdicts with full audit trail for compliance and governance.
Source:
src/Policy/StellaOps.Policy.Engine/Evaluation/PolicyEvaluator.cs,src/Policy/__Libraries/StellaOps.Policy/Confidence/Services/ConfidenceCalculator.cs,src/__Libraries/StellaOps.Reachability.Core/LatticeState.cs.
Actors
| Actor | Type | Role |
|---|---|---|
| Caller (CI/CD, Release Console, Scanner) | Client | Submits advisory/VEX/SBOM tuples or snapshots for evaluation |
| Policy Engine | Service | Compiles and evaluates the active policy pack |
| VexLens | Service | Produces VEX consensus/issuer trust (joined into the evaluation context upstream) |
| Signals / ReachGraph | Service | Produces reachability facts (joined into the evaluation context upstream) |
| Policy pack store | Component | Stores compiled stella-dsl@1 policy packs |
The Policy Engine evaluator does not call VexLens or ReachGraph synchronously during evaluation. VEX statements and reachability facts are joined into the
PolicyEvaluationContextbefore the evaluator runs (seeReachabilityFacts/*andVex/*inStellaOps.Policy.Engine). The sequence diagram below is a conceptual, not wire-accurate, view of those dependencies.
Prerequisites
- Policy pack compiled and active for the tenant (
stella-dsl@1) - Caller bears the
policy:readscope (all evaluation/decision endpoints require it) - Scan findings generated with SBOM
- VEX statements loaded (optional)
- Reachability analysis completed (optional)
Reachability Lattice Model
Stella Ops uses an 8-state reachability lattice for vulnerability reachability (LatticeState in src/__Libraries/StellaOps.Reachability.Core/LatticeState.cs).
Note: This reachability lattice is distinct from the “K4” four-valued (Belnap) trust lattice used by the Trust Algebra engine (
src/Policy/__Libraries/StellaOps.Policy/TrustLattice/K4Lattice.cs), whose values areUnknown/True/False/Conflict. Earlier drafts of this flow conflated the two; the reachability progression below is the relevant model for policy verdicts.
States are ordered by evidence strength, progressing from Unknown toward confirmed states as static and runtime evidence accumulate. A conflict between static and runtime evidence escalates to Contested, which requires manual review.
X (Contested)
/ \
CR (Confirmed CU (Confirmed
Reachable) Unreachable)
| \ / |
| \ / |
RO (Runtime RU (Runtime
Observed) Unobserved)
| |
| |
SR (Static SU (Static
Reachable) Unreachable)
\ /
\ /
U (Unknown)
State Definitions
The codes below match the LatticeState enum and the v1 lattice-state constants used by the policy gate evaluator (src/Policy/StellaOps.Policy.Engine/Gates/PolicyGateEvaluator.cs).
| State | Code | Description |
|---|---|---|
| Unknown | U | No analysis performed; initial state for all symbols |
| StaticReachable | SR | Static call-graph analysis shows a path from an entrypoint |
| StaticUnreachable | SU | Static call-graph analysis proves no path exists |
| RuntimeObserved | RO | Symbol execution observed at runtime within the observation window |
| RuntimeUnobserved | RU | Observation window passed with no execution detected |
| ConfirmedReachable | CR | Static + runtime both confirm an execution path exists |
| ConfirmedUnreachable | CU | Static proves no path AND runtime observed no execution |
| Contested | X | Conflicting static/runtime evidence (requires manual review) |
Flow Diagram
┌─────────────────────────────────────────────────────────────────────────────────┐
│ Policy Evaluation Flow │
└─────────────────────────────────────────────────────────────────────────────────┘
┌─────────┐ ┌─────────┐ ┌─────────┐ ┌───────────┐ ┌─────────────┐
│ Scanner │ │ Policy │ │ VexLens │ │ ReachGraph│ │Policy Store │
└────┬────┘ └────┬────┘ └────┬────┘ └─────┬─────┘ └──────┬──────┘
│ │ │ │ │
│ Evaluate │ │ │ │
│ request │ │ │ │
│────────────>│ │ │ │
│ │ │ │ │
│ │ Load policy │ │ │
│ │ set │ │ │
│ │─────────────────────────────────────────────>
│ │ │ │ │
│ │ Policy │ │ │
│ │ rules │ │ │
│ │<─────────────────────────────────────────────
│ │ │ │ │
│ │ Get VEX │ │ │
│ │ consensus │ │ │
│ │────────────>│ │ │
│ │ │ │ │
│ │ │ Query issuers│ │
│ │ │──────┐ │ │
│ │ │ │ │ │
│ │ │<─────┘ │ │
│ │ │ │ │
│ │ VEX status │ │ │
│ │ per CVE │ │ │
│ │<────────────│ │ │
│ │ │ │ │
│ │ Get reach │ │ │
│ │ states │ │ │
│ │─────────────────────────────> │
│ │ │ │ │
│ │ │ │ Query call │
│ │ │ │ graph │
│ │ │ │───────┐ │
│ │ │ │ │ │
│ │ │ │<──────┘ │
│ │ │ │ │
│ │ Reach facts │ │ │
│ │<───────────────────────────── │
│ │ │ │ │
│ │ Apply rules │ │ │
│ │──────┐ │ │ │
│ │ │ │ │ │
│ │<─────┘ │ │ │
│ │ │ │ │
│ │ Compute │ │ │
│ │ confidence │ │ │
│ │──────┐ │ │ │
│ │ │ │ │ │
│ │<─────┘ │ │ │
│ │ │ │ │
│ Verdict + │ │ │ │
│ explain │ │ │ │
│<────────────│ │ │ │
│ │ │ │ │
Step-by-Step
1. Evaluation Request
Callers (CI/CD pipelines, the release console, or the Scanner) submit advisory/VEX/SBOM tuples for policy evaluation. The Policy Engine exposes the following authenticated HTTP surfaces (all require the policy:read scope). The /policy/decisions endpoints additionally enforce a resolved tenant context (RequireTenantContext(), overriding any request tenantId with the middleware-resolved tenant); /policy/eval/batch is scope-gated and reads tenantId from the request body:
| Endpoint | Purpose | Source |
|---|---|---|
POST /policy/decisions | Evaluate and retrieve decisions for component snapshots with source-evidence summaries | Endpoints/PolicyDecisionEndpoint.cs |
GET /policy/decisions/{snapshotId} | Replay recorded decisions for a snapshot (filterable by PURL/advisory) | Endpoints/PolicyDecisionEndpoint.cs |
POST /policy/eval/batch | Batch-evaluate a page of advisory/VEX/SBOM tuples against active packs (cursor pagination, optional budgetMs) | Endpoints/BatchEvaluationEndpoint.cs |
The exact wire schema is owned by the DTOs in
src/Policy/StellaOps.Policy.Engine(BatchEvaluationRequestDto,PolicyDecisionRequest). The JSON below is an illustrative request shape for the conceptual evaluation flow and is not a verbatim copy of any single endpoint contract. In the real batch contract each item carriespackId,version,subjectPurl,advisoryId, and structuredseverity/advisory/vex/sbom/exceptions/reachabilityobjects, and must setevaluationTimestamp(the validator rejects items without it to keep evaluation deterministic). Page size defaults to 100 and is clamped to 1–500.
// Illustrative — see DTOs for authoritative field names
{
"tenantId": "acme-corp",
"items": [
{
"advisory": { "cve": "CVE-2024-1234", "severity": "critical", "cvss": 9.8 },
"component": { "purl": "pkg:npm/lodash@4.17.20", "fixedVersion": "4.17.21" }
},
{
"advisory": { "cve": "CVE-2024-5678", "severity": "high", "cvss": 7.5 },
"component": { "purl": "pkg:npm/express@4.18.0", "fixedVersion": "4.18.2" }
}
]
}
2. Policy Loading
Policy Engine loads and compiles the active policy pack. Policies are authored in the stella-dsl@1 DSL (parsed by src/Policy/StellaOps.PolicyDsl/PolicyParser.cs), not YAML. A policy is a policy { ... } block of priority-ordered rule blocks; each rule has a when condition, a then action sequence, an optional else branch, and a because rationale. Rules are evaluated in ascending priority order, and the first matching rule wins; if no rule matches, the default action applies.
// syntax: stella-dsl@1
policy "production" syntax "stella-dsl@1" {
metadata {
version = "1.0.0"
description = "Production deployment policy"
author = "StellaOps"
}
settings {
default_action = "warn"
fail_on_critical = true
}
profile standard {
trust_score = 0.85
}
// Critical vulnerabilities with confirmed reachability
rule critical_reachable priority 100 {
when finding.severity == "critical" and reachability.state == "reachable"
then
severity := "critical"
annotate finding.priority := "immediate"
escalate to "security-team" when reachability.confidence > 0.9
because "Critical vulnerabilities with confirmed reachability require immediate action"
}
// Low severity unreachable can be ignored
rule low_unreachable priority 50 {
when finding.severity == "low" and reachability.state == "unreachable"
then
ignore until "next-scan" because "Low severity unreachable code"
because "Low severity unreachable vulnerabilities can be safely deferred"
}
}
The example above is the
default.dslfixture (src/Policy/__Tests/StellaOps.PolicyDsl.Tests/TestData/default.dsl). Reference-surface gotcha: thefinding.*references are valid in the standaloneStellaOps.PolicyDslengine (PolicyEngineFactory.cs), which binds an arbitraryfindingobject into itsSignalContext. The runtime evaluator that backs the HTTP endpoints (PolicyExpressionEvaluatorinStellaOps.Policy.Engine) does not expose afindingroot — its condition roots areseverity,vex,advisory,sbom,reachability,entropy,env,score, andnow. Under the runtime evaluator, finding severity is read asseverity.normalized(orseverity.score), notfinding.severity. Reachability members (reachability.state,reachability.confidence,reachability.has_runtime_evidence) are identical across both engines.
Rule actions map to evaluation outcomes rather than literal PASS/WARN/FAIL keywords. The supported actions are: status/severity assignment (status := ..., severity := ...), annotate, warn, escalate, require vex, ignore, and defer. The resulting finding status is one of affected, not_affected, suppressed, ignored, deferred, warned, or blocked (see Evaluation/PolicyEvaluator.cs). Whether a given status fails a release is decided downstream by the CI/CD gate (see CI/CD Gate Flow).
3. VEX Consensus Query
VEX statements reach the evaluator as part of the evaluation context (PolicyEvaluationVexEvidence / PolicyEvaluationVexStatement in Evaluation/PolicyEvaluationContext.cs); the engine does not fetch them via the synchronous call shown below. VexLens consensus and issuer trust are produced upstream and joined into the context before evaluation. Inside the evaluator, each VEX statement is mapped to a status (affected, not_affected, fixed, under_investigation) and assigned a trust score derived from the issuer (vendor/distro issuers default to 0.85, others to 0.70, with bonuses for justification and statement-id presence) — see BuildVexEvidence / MapVexStatus / ComputeVexTrustScore in Evaluation/PolicyEvaluator.cs.
The request/response below is an illustrative consensus shape;
/internal/vex/consensusis not a Policy Engine endpoint. Treat it as a conceptual view of the VexLens output that feeds the evaluation context.
// Illustrative consensus shape (produced upstream by VexLens)
{
"statements": [
{
"vulnerability": "CVE-2024-1234",
"status": "affected",
"issuers": [
{"name": "vendor-psirt", "trust": 0.95, "status": "affected"},
{"name": "osv", "trust": 0.7, "status": "affected"}
],
"consensus": "affected",
"confidence": 0.92
},
{
"vulnerability": "CVE-2024-5678",
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path",
"issuers": [
{"name": "vendor-psirt", "trust": 0.95, "status": "not_affected"}
],
"consensus": "not_affected",
"confidence": 0.95
}
]
}
4. Reachability State Query
Reachability facts are likewise joined into the evaluation context rather than fetched synchronously by the evaluator. The Policy Engine consumes reachability facts from Signals/ReachGraph through ReachabilityFacts/* (e.g. SignalsBackedReachabilityFactsStore, ReachabilityCoreBridge) and exposes them to rules as PolicyEvaluationReachability (IsReachable / IsUnreachable / IsUnknown / HasRuntimeEvidence / Confidence / EvidenceRef). In DSL conditions the state surfaces as reachability.state ("reachable" / "unreachable") plus reachability.confidence and reachability.has_runtime_evidence.
The shape below is illustrative;
/internal/reachability/statesis not a Policy Engine endpoint. The latticestatevalues map to the codes in the State Definitions table.
// Illustrative reachability facts (produced upstream by Signals/ReachGraph)
{
"states": [
{
"package": "pkg:npm/lodash@4.17.20",
"state": "StaticReachable",
"evidence": {
"static": {"call_paths": 3, "entry_points": ["src/api/handler.js:45"]},
"runtime": null
}
},
{
"package": "pkg:npm/express@4.18.0",
"state": "RuntimeObserved",
"evidence": {
"static": {"call_paths": 12},
"runtime": {"invocations": 1547, "last_seen": "2024-12-29T09:00:00Z"}
}
}
]
}
5. Rule Evaluation
Policy Engine evaluates rules in ascending priority order; the first rule whose when condition holds applies its then actions and stops further matching. If no rule matches, the default outcome applies.
Finding: CVE-2024-1234 in pkg:npm/lodash@4.17.20
- finding.severity: critical
- reachability.state: reachable (lattice: StaticReachable / SR)
- reachability.confidence: 0.70
- vex status: affected
Rule: critical_reachable (priority 100)
- when: finding.severity == "critical" and reachability.state == "reachable"
- evaluation: "critical" == "critical" ✓ AND "reachable" == "reachable" ✓
- then: severity := "critical"; annotate finding.priority := "immediate"
- result: MATCH → status "affected", severity "critical"
The evaluator’s status is the policy verdict for the finding. It does not emit a literal PASS/WARN/FAIL; release pass/fail is decided by the CI/CD gate, which also applies lattice-state, VEX-trust, uncertainty-tier, and confidence gates with allow / warn / block outcomes (Gates/PolicyGateEvaluator.cs).
6. Confidence Scoring
Policy Engine computes a 0.0–1.0 confidence score as a weighted sum of five factors. Default weights come from ConfidenceWeightOptions (src/Policy/__Libraries/StellaOps.Policy/Confidence/Configuration/ConfidenceWeightOptions.cs) and must sum to 1.0; if a configured override does not, the calculator normalizes them.
| Factor | Weight | Description |
|---|---|---|
| Reachability | 0.30 | Lattice-state certainty × analysis confidence |
| Runtime | 0.20 | Runtime corroboration (with 24h recency bonus) |
| VEX | 0.25 | Best VEX statement status × issuer trust |
| Provenance | 0.15 | Provenance/SLSA level + SBOM completeness |
| Policy | 0.10 | Rule match strength |
Each factor produces a raw value in [0,1] (see ConfidenceCalculator). Notable mappings: reachability base values are ConfirmedUnreachable 1.0, StaticUnreachable 0.85, Unknown 0.5, StaticReachable 0.3, ConfirmedReachable 0.1 — multiplied by analysis confidence. VEX raw value is NotAffected → trust, Fixed → trust × 0.9, UnderInvestigation → 0.4, Affected → 0.1. A missing factor degrades to a neutral default (0.5 for reachability/runtime/VEX/policy, 0.3 for provenance) rather than 0.
Confidence = Σ(weight × raw_value), then clamped to [0,1]
For CVE-2024-1234 (StaticReachable @ analysis confidence 0.70, no runtime,
best VEX = affected, unsigned SBOM, rule matched):
- Reachability: 0.30 × (0.3 × 0.70 = 0.21) = 0.063
- Runtime: 0.20 × 0.50 (no observations → neutral) = 0.100
- VEX: 0.25 × 0.10 (affected) = 0.025
- Provenance: 0.15 × 0.30 (unsigned) = 0.045
- Policy: 0.10 × 0.90 (matched rule) = 0.090
Total Confidence ≈ 0.32 → Tier: Low
The score carries a Tier (VeryLow < 0.3, Low < 0.5, Medium < 0.7, High < 0.9, VeryHigh ≥ 0.9), a per-factor breakdown with human-readable reasons, and a ranked list of Improvements (e.g. “Deploy runtime sensor”, “Obtain VEX statement from vendor”). The exact arithmetic above is illustrative; consult ConfidenceCalculator.Calculate for the authoritative per-factor logic.
Evidence-Weighted Score (EWS)
When the EWS enricher is enabled (IFindingScoreEnricher), evaluation additionally attaches a separate 0–100 Evidence-Weighted Score and a triage bucket (ScoreBucket enum — see src/Signals/StellaOps.Signals/EvidenceWeightedScore/EvidenceWeightedScoreCalculator.cs). The default bucket thresholds are ActNow (90–100), ScheduleNext (70–89), Investigate (40–69), and Watchlist (0–39); the cut-points are configurable via BucketThresholds (ActNowMin, ScheduleNextMin, InvestigateMin).
The EWS is exposed to DSL rules through the score condition root (see the ScoreScope in Evaluation/PolicyExpressionEvaluator.cs): score >= 80, score.value, score.bucket == "ActNow", score.is_act_now, dimension accessors (score.rch / score.rts / score.bkp / score.xpl / score.src / score.mit), and score.has_flag(...) / score.flags. The pre-computed score is also written to the result annotations as ews.score (formatted F2) and ews.bucket (ApplyEvidenceWeightedScore in PolicyEvaluator.cs) — those annotation keys are outputs on the result, distinct from the score DSL accessor used inside when conditions. EWS is distinct from the 0.0–1.0 confidence score and is computed by the Signals module, not by the confidence calculator.
7. Verdict Assembly
The batch endpoint (POST /policy/eval/batch) returns one result per input item. Each result carries the per-finding status (not a PASS/WARN/FAIL verdict), plus rule attribution, confidence, annotations, applied exception, and cache indicators. The authoritative per-item shape is BatchEvaluationResultDto (src/Policy/StellaOps.Policy.Engine/BatchEvaluation):
// Per-item result (BatchEvaluationResultDto) — field names are authoritative
{
"results": [
{
"packId": "production",
"version": 7,
"policyDigest": "sha256:...",
"status": "affected", // affected | not_affected | suppressed | ignored | deferred | warned | blocked
"severity": "critical",
"ruleName": "critical_reachable",
"priority": 100,
"annotations": {
"priority": "immediate",
"ews.score": "92.00",
"ews.bucket": "ActNow"
},
"warnings": [],
"appliedException": null,
"confidence": {
"value": 0.32,
"tier": "Low",
"explanation": "Confidence is low (32%), primarily driven by runtime and policy."
},
"correlationId": "...",
"cached": false,
"cacheSource": "None", // CacheSource enum: None | InMemory | Redis (non-nullable)
"evaluationDurationMs": 3
},
{
"packId": "production",
"version": 7,
"status": "not_affected",
"severity": "high",
"ruleName": "vex_not_affected",
"appliedException": null,
"confidence": { "value": 0.81, "tier": "High" },
"cached": true,
"cacheSource": "InMemory"
}
],
"nextPageToken": null,
"total": 2,
"returned": 2,
"cacheHits": 1,
"cacheMisses": 1,
"durationMs": 5,
"budgetRemainingMs": null
}
The aggregate fields (
total,returned,cacheHits,cacheMisses,durationMs,budgetRemainingMs,nextPageToken) are page-level batch metadata, not a release verdict. Release pass/fail (allow/warn/block) is computed by the CI/CD gate; see CI/CD Gate Flow.
Data Contracts
Policy Rule (compiled IR) Schema
Rules are authored in stella-dsl@1 and compiled to PolicyIrRule (src/Policy/StellaOps.PolicyDsl/PolicyIr.cs). A rule has a name, a numeric priority, a when condition expression, ordered then actions, optional else actions, and a because rationale (the rationale is mandatory at parse time).
// Shape mirrors PolicyIrRule
interface PolicyIrRule {
name: string;
priority: number; // lower runs first; first match wins
when: PolicyExpression; // boolean condition
thenActions: PolicyIrAction[];
elseActions: PolicyIrAction[];
because: string; // required rationale
}
// PolicyIrAction is one of:
// assignment (status:= / severity:= / annotation), annotate, warn,
// escalate, require vex, ignore, defer
Exceptions are not embedded in the rule; they are applied during evaluation from the context (PolicyEvaluationExceptions) with effect types Suppress, Defer, Downgrade, and RequireControl (Evaluation/PolicyEvaluator.cs, Domain/ExceptionContracts.cs).
Per-Finding Result Schema
There is no single PolicyVerdict envelope with PASS/WARN/FAIL. Each evaluated finding yields a result (BatchEvaluationResultDto) whose status is the verdict:
// Shape mirrors BatchEvaluationResultDto
interface PolicyEvaluationResultDto {
packId: string;
version: number;
policyDigest: string;
status: 'affected' | 'not_affected' | 'suppressed' | 'ignored'
| 'deferred' | 'warned' | 'blocked';
severity?: string;
ruleName?: string;
priority?: number;
annotations: Record<string, string>; // includes ews.score / ews.bucket when EWS enabled
warnings: string[];
appliedException?: PolicyExceptionApplication;
confidence?: { value: number; tier: ConfidenceTier; explanation?: string };
correlationId: string;
cached: boolean;
cacheSource: 'None' | 'InMemory' | 'Redis'; // CacheSource enum (non-nullable)
evaluationDurationMs: number;
}
// ConfidenceTier = 'VeryLow' | 'Low' | 'Medium' | 'High' | 'VeryHigh'
// AppliedException carries the PolicyExceptionApplication record
// (exceptionId, effectId, effectType, original/applied status+severity, metadata).
Error Handling
| Error | Recovery |
|---|---|
| Policy pack not found / not active | 404 (KeyNotFoundException) from the decision endpoint |
| Invalid request body | 400 (ArgumentException); batch validation returns a ProblemDetails |
Batch item missing evaluationTimestamp | 400 ProblemDetails — every batch item must supply evaluationTimestamp to keep evaluation deterministic (BatchEvaluationValidator) |
Batch pageSize out of range / bad pageToken | 400 ProblemDetails — pageSize must be 1–500; pageToken must be a non-negative integer offset |
| Missing VEX evidence | Confidence VEX factor degrades to the neutral default (0.5); evaluation continues |
| Missing reachability facts | Reachability treated as Unknown; reachability factor uses the neutral default |
| EWS enrichment failure | Caught and swallowed; result returned without an EWS score (never fails evaluation) |
| Unhandled DSL action | Recorded as a warning on the result; evaluation continues |
| Multiple matching rules | First match in (priority asc, source order) wins; remaining rules are skipped |
| Time budget exhausted (batch) | Stops early; returns processed page with nextPageToken for continuation |
Observability
Metrics
Emitted by PolicyEngineTelemetry (meter StellaOps.Policy.Engine). Names and labels below match src/Policy/StellaOps.Policy.Engine/Telemetry/PolicyEngineTelemetry.cs.
| Metric | Type | Labels |
|---|---|---|
policy_evaluations_total | Counter | tenant, policy, mode |
policy_evaluation_latency_seconds | Histogram | tenant, policy |
policy_run_seconds | Histogram | mode, tenant, policy |
policy_rules_fired_total | Counter | policy, rule |
policy_vex_overrides_total | Counter | policy, vendor |
policy_findings_materialized_total | Counter | tenant, policy |
policy_evaluation_failures_total | Counter | tenant, policy, reason |
policy_errors_total | Counter | type, tenant |
policy_concurrent_evaluations | Gauge | tenant |
(Additional compilation, simulation, entropy, and API metrics exist in the same file.)
Trace Context
Traces are emitted under the StellaOps.Policy.Engine activity source (PolicyEngineTelemetry.ActivitySource). The actual span names are dotted, e.g. policy.evaluate, policy.evaluate_batch, policy.select, policy.materialize, policy.compile, and policy.simulate (see Telemetry/PolicyEngineTelemetry.cs and Services/PolicyRuntimeEvaluationService.cs).
The phase tree below is a conceptual view of what happens inside a single policy.evaluate span — the rule/exception/budget/confidence/EWS steps run inline in PolicyEvaluator.Evaluate and are not emitted as separate child spans today:
policy.evaluate (actual span)
├── policy load (compile / cache lookup) — conceptual phase
├── rule evaluation (priority asc; first match) — conceptual phase
├── exception application — conceptual phase
├── unknown budget — conceptual phase
├── confidence scoring — conceptual phase
└── evidence-weighted-score (when EWS enabled) — conceptual phase
Related Flows
- Scan Submission Flow - Parent flow
- CI/CD Gate Flow - Pipeline integration
- Exception Approval Workflow - Policy exceptions
- Multi-Tenant Policy Rollout Flow - Policy distribution
