Methodology
August 8, 2026 · View on GitHub
How Predictive-ML-Core turns a declared system inventory into architecture
risk scores, and why the method is deterministic, explainable rules rather than
a trained model.
Why not a trained model
The repository originally scaffolded a Microsoft.ML.FastTree regressor that
predicted latency from low-level OS telemetry (context switches, GC
collections, IOPS throttling). None of that data exists in this service's
contract: the request carries a declared inventory — name, type, criticality,
public exposure, data sensitivity, auth method — and optionally a few latency
numbers. There is also no labelled corpus of past assessments to fit a model
against.
A supervised model needs labelled examples of "this architecture failed" to learn from. Without them, wrapping heuristics in ML vocabulary produces numbers that look learned but are not — and that is precisely the kind of claim that collapses under scrutiny. So the engine scores with explicit, weighted rules where every point is traceable to an input field, and keeps the same HTTP contract a real model would use, so a model can replace the rules later without consumers changing.
The three risk dimensions
Each system is scored 0–100 on three dimensions. Weights live in
RiskRules.cs.
singlePointOfFailure — base weight by criticality (LOW 0, MEDIUM 15,
HIGH 35, CRITICAL 55) plus a type modifier (DATABASE +15: state is costly to
replicate; THIRD_PARTY_SERVICE +20: outside your control, no declared fallback;
APPLICATION/API +5). The schema has no redundancy field, so the worst case —
single instance — is assumed and stated in the finding.
excessiveCoupling — a proxy for exposure and dependency, since the request
carries no dependency graph. publicFacing without authMethod scores +40;
with an authMethod, +15; internal systems get a +5 base. THIRD_PARTY_SERVICE
adds +25. A fan-out term adds points when the whole inventory declares more than
two third-party dependencies — an organisation-level coupling signal, capped.
scalabilityGap — prefers real data: when historicalMetrics is present,
p99LatencyMs above 1000 ms and a p99/avg ratio above 3 (heavy tail) each add
weight. Without metrics it falls back to a weaker, explicitly conditional
signal — a sensitive database, or a critical system with no observed load.
The overallArchitectureScore is a criticality-weighted average across
systems, not a flat mean, so one CRITICAL system with a severe single point of
failure cannot dilute into a healthy score among many LOW-criticality systems.
Explainability as a first-class output
The differentiator over a black-box score is that every number is auditable.
Each dimension is computed as a list of RuleContribution { rule, points, rationale }, and the response returns that scoreBreakdown alongside the
scores. The identity
riskScore == min( sum(breakdown.points), 100 )
holds for every dimension of every system, and ScoreBreakdownTests enforces
it — the explanation cannot silently diverge from the score. Example:
singlePointOfFailure = 60
├─ criticality=CRITICAL +55 "impact of a failure grows with declared criticality"
└─ type=API +5 "application/API adds moderate single-instance risk"
Every response also carries engineVersion and method
(deterministic-rule-scoring) as provenance, so a consumer persisting findings
can record exactly what produced them.
Honest limitations
The scores are a structured reading of declared inventory, not a measurement
of a running system. They are only as good as the inventory, and where a signal
is inferred rather than observed (everything except the historicalMetrics
path) the finding says so. This honesty is deliberate: the value is a
transparent, reproducible risk model, not a prediction dressed up as one.