Risk policy reference
A risk score is only evidence if the constants behind it are recoverable later. Veritio stores a policyVersion string inside every hashed security.risk conclusion, and veritio.reference.v1 is the one policy every SDK must reproduce byte-for-byte. This page is the published table of those constants, plus the exact rules the temperature knob and caller overrides follow.
The reference constants
Section titled “The reference constants”DEFAULT_RISK_POLICY (DefaultRiskPolicy in Go) carries policyVersion: "veritio.reference.v1". Every number below is a cross-language protocol contract: changing one is a protocol change that must land in TypeScript, Python, Go, and spec/conformance together.
operationBase seeds the per-step score from the operation class:
operationType |
base |
|---|---|
read |
0.05 |
create |
0.20 |
update |
0.30 |
config |
0.45 |
bulk |
0.55 |
permission |
0.60 |
delete |
0.70 |
destructive |
0.85 |
reversibilityFactor and envCriticalityFactor are multiplicative. Both default to the most conservative class when the caller omits the signal: recoverable and production.
reversibility |
factor |
|---|---|
reversible |
0.6 |
recoverable |
1.0 |
irreversible |
1.3 |
envCriticality |
factor |
|---|---|
sandbox |
0.4 |
development |
0.6 |
staging |
0.8 |
production |
1.0 |
magnitude turns three non-negative integer counts into additive boosts through the saturating curve x / (x + k). maxBoost caps their combined ceiling; weights splits that ceiling between signals; k sets how fast each signal saturates.
| magnitude field | dataVolume |
fanOut |
referenceCount |
|---|---|---|---|
weights |
0.5 | 0.3 | 0.2 |
k |
100 | 25 | 50 |
magnitude.maxBoost is 0.4. The weights sum to 1.0, so the additive part of a step can never exceed maxBoost no matter how large the counts get.
rollup governs episode aggregation: windowSeconds: 60, decayPerWindow: 0.5, velocityNormalizer: 3.0, frequencyRules: []. Momentum carried between steps decays by decayPerWindow per elapsed window, peak momentum is divided by velocityNormalizer to produce velocityScore, and the episode score is the maximum of peak, velocityScore, and — only when rules are configured — frequencyScore. The reference policy ships zero frequency rules, so a reference rollup emits no frequencyScore or frequencyMatches fields at all.
Bands are half-open
Section titled “Bands are half-open”bands holds four inclusive lower bounds: low: 0.05, medium: 0.25, high: 0.5, critical: 0.75. bandOf compares strictly less-than against each in ascending order, so a score exactly on a boundary lands in the higher band.
score < 0.05 → none0.05 <= score < 0.25 → low0.25 <= score < 0.50 → medium0.50 <= score < 0.75 → high0.75 <= score → criticalNote both ends. none exists only below bands.low; there is no separate “critical ceiling”, because critical is the last band and every score is clamped to [0,1] before banding. Bands must stay strictly ascending — the policy validator rejects any policy where they are not.
What temperature does
Section titled “What temperature does”riskPolicy({ temperature }) derives a full policy from the reference constants. It is not a multiplier on the final score and not a threshold-only knob: it moves both the scoring inputs and the bands, in the same direction.
Nine fields — and only these nine — are rescaled:
| field | lenient (t=0) |
reference (t=0.5) |
strict (t=1) |
|---|---|---|---|
bands.low |
0.10 | 0.05 | 0.02 |
bands.medium |
0.35 | 0.25 | 0.18 |
bands.high |
0.60 | 0.50 | 0.40 |
bands.critical |
0.85 | 0.75 | 0.65 |
rollup.decayPerWindow |
0.30 | 0.50 | 0.70 |
rollup.velocityNormalizer |
4.0 | 3.0 | 2.0 |
magnitude.maxBoost |
0.25 | 0.40 | 0.60 |
reversibilityFactor.irreversible |
1.15 | 1.30 | 1.60 |
envCriticalityFactor.production |
0.90 | 1.00 | 1.20 |
Everything else is untouched by temperature: all eight operationBase entries, the reversible and recoverable factors, sandbox/development/staging, all three magnitude.weights, all three magnitude.k, rollup.windowSeconds, and rollup.frequencyRules.
Derivation is two-segment linear interpolation, chosen so the reference column is an exact fixed point rather than an approximation:
t = 0.0 ────────── t = 0.5 ────────── t = 1.0lenient reference strict └─ blend ────────┘ └──── blend ──────┘
t <= 0.5: lenient + (reference - lenient) * (t / 0.5)t > 0.5: reference + (strict - reference) * ((t - 0.5) / 0.5)then round4 (floor(x * 10000 + 0.5) / 10000)There is no pow, exp, or log anywhere in the derivation. That restriction is deliberate: transcendental functions are the usual source of last-digit divergence between language runtimes, and a derived constant that differs in the fourth decimal produces a different score, a different band, and a different hashed conclusion.
The policyVersion suffix
Section titled “The policyVersion suffix”temperature must be a finite number in [0,1] and a multiple of 0.01. The value is immediately converted to integer hundredths, and the derived policyVersion is built from those integers alone — never from float formatting:
0.7 → hundredths 70 whole = 70 / 100 = 0 frac = 70 % 100 = 70 "veritio.reference.v1" + "+temp" + "0" + "." + "70" → "veritio.reference.v1+temp0.70"
0.05 → hundredths 5 → frac 5 zero-padded → "+temp0.05"1.0 → hundredths 100 → whole 1, frac 0 → "+temp1.00"This is why the 0.01 granularity is enforced rather than rounded. 0.7 renders as 0.7 in one language and 0.7000000000000001 in another; integer division and modulo do not. The suffix is a label that appears inside hashed evidence, so it has to be produced identically everywhere or two SDKs would sign the same conclusion under two different policy names.
Calling riskPolicy() with no options at all returns a fresh copy equal to DEFAULT_RISK_POLICY, keeping the unsuffixed veritio.reference.v1 version. Calling it with temperature: 0.5 returns the same constants with the suffixed version veritio.reference.v1+temp0.50.
Verified output
Section titled “Verified output”The following is produced by an executable fixture whose output is byte-compared in CI. It scores one fixed signal set — update, irreversible, production, dataVolume: 120, fanOut: 5, referenceCount: 8 — under five temperatures, so only the policy varies.
{ "signals": { "operationType": "update", "reversibility": "irreversible", "envCriticality": "production", "dataVolume": 120, "fanOut": 5, "referenceCount": 8 }, "profiles": [ { "temperature": 0, "policyVersion": "veritio.reference.v1+temp0.00", "bands": { "low": 0.1, "medium": 0.35, "high": 0.6, "critical": 0.85 }, "rollup": { "windowSeconds": 60, "decayPerWindow": 0.3, "velocityNormalizer": 4, "frequencyRuleCount": 0 }, "scaledMultipliers": { "magnitudeMaxBoost": 0.25, "irreversibleFactor": 1.15, "productionFactor": 0.9 }, "fixedSignalScore": { "score": 0.4012, "level": "medium" } }, { "temperature": 0.2, "policyVersion": "veritio.reference.v1+temp0.20", "bands": { "low": 0.08, "medium": 0.31, "high": 0.56, "critical": 0.81 }, "rollup": { "windowSeconds": 60, "decayPerWindow": 0.38, "velocityNormalizer": 3.6, "frequencyRuleCount": 0 }, "scaledMultipliers": { "magnitudeMaxBoost": 0.31, "irreversibleFactor": 1.21, "productionFactor": 0.94 }, "fixedSignalScore": { "score": 0.4647, "level": "medium" } }, { "temperature": 0.5, "policyVersion": "veritio.reference.v1+temp0.50", "bands": { "low": 0.05, "medium": 0.25, "high": 0.5, "critical": 0.75 }, "rollup": { "windowSeconds": 60, "decayPerWindow": 0.5, "velocityNormalizer": 3, "frequencyRuleCount": 0 }, "scaledMultipliers": { "magnitudeMaxBoost": 0.4, "irreversibleFactor": 1.3, "productionFactor": 1 }, "fixedSignalScore": { "score": 0.5721, "level": "high" } }, { "temperature": 0.8, "policyVersion": "veritio.reference.v1+temp0.80", "bands": { "low": 0.032, "medium": 0.208, "high": 0.44, "critical": 0.69 }, "rollup": { "windowSeconds": 60, "decayPerWindow": 0.62, "velocityNormalizer": 2.4, "frequencyRuleCount": 0 }, "scaledMultipliers": { "magnitudeMaxBoost": 0.52, "irreversibleFactor": 1.48, "productionFactor": 1.12 }, "fixedSignalScore": { "score": 0.7991, "level": "critical" } }, { "temperature": 1, "policyVersion": "veritio.reference.v1+temp1.00", "bands": { "low": 0.02, "medium": 0.18, "high": 0.4, "critical": 0.65 }, "rollup": { "windowSeconds": 60, "decayPerWindow": 0.7, "velocityNormalizer": 2, "frequencyRuleCount": 0 }, "scaledMultipliers": { "magnitudeMaxBoost": 0.6, "irreversibleFactor": 1.6, "productionFactor": 1.2 }, "fixedSignalScore": { "score": 0.9796, "level": "critical" } } ], "referenceInvariant": { "defaultPolicyVersion": "veritio.reference.v1", "derivedPolicyVersion": "veritio.reference.v1+temp0.50", "constantsMatchDefault": true }, "rejections": [ { "temperature": 0.005, "error": "temperature must be a multiple of 0.01" }, { "temperature": 1.5, "error": "temperature must be a finite number in [0,1]" } ]}Read it field by field:
profiles[].policyVersionproves the suffix rule for0,0.2,0.5,0.8, and1, including zero-padding (+temp0.00,+temp0.20) and the whole-number case (+temp1.00).profiles[].bandsproves the bands move monotonically inward as temperature rises, and stay strictly ascending at both extremes.profiles[].rollupproveswindowSecondsstays60at every temperature andfrequencyRuleCountstays0. Those two are the control group: they show the knob is scoped, not global.profiles[].scaledMultipliersproves exactly which three scoring constants move —magnitude.maxBoost, theirreversiblefactor, and theproductionfactor — while the other reversibility and environment classes hold their reference values.profiles[].fixedSignalScoreproves the compounding effect. The same signals score0.4012/mediumatt=0and0.9796/criticalatt=1.referenceInvariantprovestemperature: 0.5reproduces every reference constant, comparing the whole derived policy againstDEFAULT_RISK_POLICYwithpolicyVersionstripped.rejectionsproves the two distinct fail-closed messages for a sub-hundredth value and an out-of-range value.
The reference row is worth checking by hand, because it shows why both the numerator and the bands matter:
base(update) 0.3000+ 0.4 * 0.5 * 120/(120+100) → round4 0.1091+ 0.4 * 0.3 * 5/(5+25) → round4 0.0200+ 0.4 * 0.2 * 8/(8+50) → round4 0.0110 sum 0.4401* irreversible 1.3 * production 1.0 0.5721band: 0.5721 >= high 0.5, < critical 0.75 → highAt t=1 the same arithmetic runs with maxBoost 0.6, irreversible 1.6, production 1.2 and is banded against critical 0.65. Scores move and thresholds move together, which is why scores derived under different temperatures are not comparable. Comparing them is only meaningful when the policyVersion strings match.
The fixture source is the definition of what is being asserted:
import { DEFAULT_RISK_POLICY, type RiskScoringPolicy, type RiskSignals, riskPolicy, scoreRiskSignals,} from '@veritio/core/risk-score'
/** * One fixed signal set scored under every temperature so the documented table * shows only the policy changing, never the input. Deliberately exercises all * three temperature-scaled scoring fields (magnitude maxBoost, the irreversible * multiplier, the production multiplier) and is sized to land mid-range, so the * same signals cross bands instead of saturating at 1.0 everywhere. */export const auditedSignals: RiskSignals = { operationType: 'update', reversibility: 'irreversible', envCriticality: 'production', dataVolume: 120, fanOut: 5, referenceCount: 8,}
/** Temperatures documented on the risk-policy page, lenient through strict. */export const documentedTemperatures = [0, 0.2, 0.5, 0.8, 1] as const
/** * Projects the fields `riskPolicy({ temperature })` actually rescales. Keeping * this projection explicit is the point of the fixture: it proves the knob * touches only bands, rollup decay/velocity, magnitude maxBoost, and the * irreversible/production multipliers, and leaves every other pinned constant * (operation bases, weights, k, windowSeconds) untouched. */export function temperatureProfile(policy: RiskScoringPolicy) { const assessment = scoreRiskSignals(auditedSignals, policy) return { policyVersion: policy.policyVersion, bands: policy.bands, rollup: { windowSeconds: policy.rollup.windowSeconds, decayPerWindow: policy.rollup.decayPerWindow, velocityNormalizer: policy.rollup.velocityNormalizer, frequencyRuleCount: policy.rollup.frequencyRules.length, }, scaledMultipliers: { magnitudeMaxBoost: policy.magnitude.maxBoost, irreversibleFactor: policy.reversibilityFactor.irreversible, productionFactor: policy.envCriticalityFactor.production, }, fixedSignalScore: { score: assessment.score, level: assessment.level }, }}
/** * Compares a derived policy against `DEFAULT_RISK_POLICY` on every field except * `policyVersion`, which is expected to carry the deterministic "+temp0.50" * suffix. This is the invariant the fixture asserts: temperature 0.5 is the * reference policy byte-for-byte, so a hosted retune of the endpoints cannot * silently move the documented default. */export function matchesReferenceConstants(policy: RiskScoringPolicy): boolean { const strip = ({ policyVersion: _ignored, ...rest }: RiskScoringPolicy) => rest return JSON.stringify(strip(policy)) === JSON.stringify(strip(DEFAULT_RISK_POLICY))}
/** * Renders the rejection for a temperature the protocol refuses, so the docs can * show that the knob fails closed instead of rounding a caller's value into a * policyVersion string that would misdescribe the constants behind a hashed * risk conclusion. */export function rejection(temperature: number): { temperature: number; error: string } { try { riskPolicy({ temperature }) return { temperature, error: 'accepted (unexpected)' } } catch (error) { return { temperature, error: (error as Error).message } }}
if (import.meta.main) { const referencePolicy = riskPolicy({ temperature: 0.5 }) const output = { signals: auditedSignals, profiles: documentedTemperatures.map((temperature) => ({ temperature, ...temperatureProfile(riskPolicy({ temperature })), })), referenceInvariant: { defaultPolicyVersion: DEFAULT_RISK_POLICY.policyVersion, derivedPolicyVersion: referencePolicy.policyVersion, constantsMatchDefault: matchesReferenceConstants(referencePolicy), }, rejections: [rejection(0.005), rejection(1.5)], }
if (!matchesReferenceConstants(referencePolicy)) { throw new Error('temperature 0.5 must reproduce DEFAULT_RISK_POLICY constants exactly') }
console.log(JSON.stringify(output, null, 2))}Overrides
Section titled “Overrides”overrides are deep-merged after temperature derivation. One rule dominates: if the overrides object carries any key at all, overrides.policyVersion is mandatory and must be a non-empty string.
riskPolicy({ temperature: 0.7, overrides: { magnitude: { maxBoost: 0.5 } } }) → TypeError: overrides.policyVersion is required ...
riskPolicy({ temperature: 0.7, overrides: { policyVersion: 'acme.custom.v3', magnitude: { maxBoost: 0.5 },} }) → policyVersion "acme.custom.v3"The reason is evidentiary, not stylistic. An auto-generated +temp0.70 suffix asserts “these are the reference constants derived at temperature 0.70”. Once a caller has changed maxBoost, that assertion is false, and it would be false inside a hashed conclusion that a reviewer reads months later. Requiring an explicit name makes a hand-tuned policy self-identifying.
Merging is explicit, not recursive. Only documented sub-objects merge — one level into operationBase, reversibilityFactor, envCriticalityFactor, bands, and rollup; two levels into magnitude.weights and magnitude.k. Unknown keys are ignored rather than smuggled into the policy shape.
rollup.frequencyRules is the exception: it replaces wholesale. Supplying rules discards the base rule set instead of concatenating with it. A merged rule list would make the effective burst-detection configuration depend on inheritance order, which is exactly the kind of ambiguity a stored policyVersion cannot describe.
Fail-closed behavior
Section titled “Fail-closed behavior”Derivation validates the finished policy — after temperature and after overrides — and throws rather than returning something the banding math cannot honor.
| Condition | Result |
|---|---|
temperature non-finite, < 0, or > 1 |
temperature must be a finite number in [0,1] |
temperature not a multiple of 0.01 |
temperature must be a multiple of 0.01 |
| any numeric leaf non-finite | policy numeric fields must all be finite numbers |
| bands not strictly ascending | policy bands must be strictly ascending |
any reversibility or environment factor <= 0 |
policy multiplier factors must be greater than 0 |
rollup.windowSeconds or velocityNormalizer <= 0 |
... must be greater than 0 |
empty or non-string policyVersion |
policy policyVersion must be a non-empty string |
Python additionally rejects bool where a number is required, because True is an int in Python and would otherwise pass as 1. Go takes float64 directly and returns an error instead of panicking.
These checks run on the caller’s overrides too, so a hand-tuned policy cannot silently mis-band scores; it fails at construction, before any evidence is written.
Cross-language names
Section titled “Cross-language names”The field names inside a policy are language-neutral JSON identifiers and never change. Only the API surface differs.
| Concept | TypeScript | Python | Go |
|---|---|---|---|
| Reference policy | DEFAULT_RISK_POLICY |
DEFAULT_RISK_POLICY |
DefaultRiskPolicy |
| Derive a policy | riskPolicy(options) |
risk_policy(options) |
RiskPolicy(options) |
| Options | RiskPolicyOptions object |
dict |
RiskPolicyOptions struct |
| Overrides | RiskPolicyOverrides |
dict under "overrides" |
*RiskPolicyOverrides |
| Failure | throws TypeError |
raises TypeError |
returns error |
TypeScript and Python read policy fields by their JSON names (policy.bands.low, policy["bands"]["low"]). Go uses exported struct fields (policy.Bands.Low) with map[string]float64 for the three enum-keyed tables, keyed by the same protocol strings.
Go’s optional numeric fields are pointers, so an absent knob is distinguishable from a deliberate 0.0 — which is a real temperature meaning “lenient endpoints”, not “unset”. Use the veritio.Float64 helper:
policy, err := veritio.RiskPolicy(veritio.RiskPolicyOptions{ Temperature: veritio.Float64(0.7),})if err != nil { return err}// policy.PolicyVersion == "veritio.reference.v1+temp0.70"The same pattern applies to the override structs: MaxBoost, Bands.Low, Rollup.DecayPerWindow and the rest are *float64, and nil means “keep the derived value”.
What a policy version does not prove
Section titled “What a policy version does not prove”A matching policyVersion proves that two conclusions were computed from the same constants under the same math. It proves nothing else.
It does not prove the signals were accurate. operationType, reversibility, envCriticality, and the three counts are classifications supplied by the caller or by a capture adapter; a mislabelled delete scored as an update produces a perfectly reproducible wrong number.
It does not make a score a judgement. critical is a band, not a finding. The bands encode one defensible default ordering of operation classes; whether that ordering matches your risk appetite is a decision you make, and a temperature or a named override is how you record having made it.
It does not survive a silent retune. A hosted or forked deployment that changes constants while keeping veritio.reference.v1 breaks the contract this page describes. That is precisely what the conformance fixture at spec/conformance/risk-policy-temperature.json exists to catch — and why a derived policy carries a suffix rather than reusing the bare reference name.
Finally, a risk score is evidence tooling, not legal advice, and not a control. It records what a system observed and how it classified it; enforcement, review, and response still belong to your application.
Continue with Risk scoring for the step and episode math these constants parameterize, Audit events for how a security.risk conclusion is carried as evidence, and Hash chain for what makes that stored conclusion tamper-evident.