1.0.0-beta.1.1 — the risk floor
Pre-release
Pre-release
Fixed
- A Standing Order written to the documented contract could never auto-approve.
StandingOrderBasedefaulted itsRiskThresholdtoRiskLevel.Low(1) and auto-approved only
when the computed score was at or below it, while the risk formulaAddAffiantPolicies()
registered for every host returnedMedium(2) orHigh(3) on every path — over-50Value
field → High, any otherValue→ Medium, noValuefield → Medium. Nothing scoredLow, so a
subclass that implementedMatchesAsyncand changed nothing else always fell through to reviewer
confirmation. - New semantics.
RiskThresholdis nowint?and defaults tonull, meaning no risk
ceiling: matching the conditions is the whole test, and such a Standing Order needs no risk
calculator at all. Declaring a threshold opts into scoring — the framework still owns the
score <= thresholdcomparison, the host owns the score. - Fail closed on misconfiguration. A Standing Order that declares a
RiskThresholdwith no
RiskScoreCalculatorBaseregistered now throwsInvalidOperationExceptionnaming
SetRiskScoreCalculator<T>(). It fails on the policy's first evaluation, before any write is
auto-approved, never silently — rather than deferring every write it was written to approve.
Added
AffiantPolicies.ValidateStandingOrders(IServiceProvider)— an optional boot-time check. It
resolves every registeredIApprovalPolicyin a throwaway scope and runs each Standing Order's
risk-configuration check, turning a misconfiguration into a startup failure rather than a
first-request one. It evaluates no Affidavit and approves nothing.
Changed
RiskScoreCalculatorBase.ComputeAsyncis abstract. There is no framework scoring formula:
what counts as risk is a property of the host's domain.ClassifyScoreand theRiskLevelenum
are unchanged.AddAffiantPolicies()registers an internal placeholderRiskScoreCalculatorBasewhen the host
registers none. It carries no formula and no risk floor — every call to it throws, naming
SetRiskScoreCalculator<T>(). It exists so that a Standing Order whose constructor takes
RiskScoreCalculatorBaseas a required dependency — the shape every1.0.0-beta.1order that
declared aRiskThresholdwas forced into — still resolves, and so sees the actionable message
rather than the container's own "Unable to resolve service for type 'RiskScoreCalculatorBase'".
It is registered withTryAdd, so a calculator the host registers always wins.StandingOrderBase's risk calculator is an optional constructor dependency
(RiskScoreCalculatorBase? riskScorer = null), and the protectedRiskScorerfield is nullable.StandingOrderBase.RiskThresholdisint?(wasint).
Removed
DefaultRiskScoreCalculator, and its automatic registration insideAddAffiantPolicies().
AddAffiantPolicies()no longer registers any scoring formula — only the throwing placeholder
described above.
Upgrade note
- A host that relied on the stock formula — over-50
Valuefield → High, otherwise Medium —
registers its own calculator: subclassRiskScoreCalculatorBase, implementComputeAsync, and
pass it toSetRiskScoreCalculator<T>()insideAddAffiantPolicies(...). - A host with a Standing Order that overrides
RiskThresholdmust register a calculator, or that
policy throws on its first evaluation, before any write is auto-approved, naming
SetRiskScoreCalculator<T>(). CallAffiantPolicies.ValidateStandingOrders(app.Services)after
Build()to hit the same failure at startup instead. Changing the override's type fromintto
int?is required to compile — and it is required at runtime too, not merely convenient:
a subclass compiled against1.0.0-beta.1and dropped in as a binary without recompiling still
overrides a property whose signature the base class no longer declares, so the CLR fails at type
load with aTypeLoadException(or aMissingMethodExceptionat the call site), not a graceful
fallback to the old behaviour. - The configuration check — is a calculator registered wherever a
RiskThresholdis declared —
runs beforeMatchesAsync, on every evaluation. A misconfigured Standing Order therefore halts
every evaluation of that policy, not only the writes it would actually have matched: intended,
since the point is to fail loudly and closed rather than silently approve or refuse on an
unscored guess. - An order that took the calculator as a required constructor parameter — the shape beta.1's base
constructor forced — keeps working unchanged: it resolves against the placeholder and, if it
declares aRiskThreshold, reports the missing registration itself. Widening the parameter to
RiskScoreCalculatorBase? scorer = nullis optional. - A host whose Standing Orders never overrode
RiskThresholdneeds no calculator and no code
change — but note the behaviour change: those orders now auto-approve on the match, which is what
they were always written to do. - These are declared breaking changes against
1.0.0-beta.1, permitted by the prerelease-stability
policy, and recorded insrc/Affiant.Policies/CompatibilitySuppressions.xml. StandingOrderBase.RiskScoreris now nullable (RiskScoreCalculatorBase?). Binary-compatible; source-breaking for a subclass that dereferences it under nullable reference types with warnings as errors — add a null check or declare no threshold.