1
0
Fork 0

feat(disposition): separate the assessment axis from the action

`decide` returned a `Disposition` — WARN / QUARANTINE_REVIEW / FAIL_SECURE —
which names an ACTION. But BRIEF design principle 4 says the library reports
and the pipeline decides, and disposition.py admitted the gap in its own
docstring: "It imposes no blocking of its own." So we returned an action we
cannot enforce, having discarded the judgement that produced it. A consumer
wanting different behaviour had to reinterpret the action itself — which is
why a consumer ends up pinning our GRADING: the action was all they got.

`Risk` (NONE/LOW/ELEVATED/SEVERE) now carries that judgement, and
`Policy.action_map` lets a caller map it to their own action. Both overlays
move the assessment rather than the action, so a custom map cannot silently
drop the compound escalation or the quarantine floor. `guard`'s fail-closed
path pins both axes and deliberately bypasses the map: downgrading SEVERE
means "I accept this class of finding", never "I accept a crashed scanner".

`DispositionResult.assessment` is required with no default. `Risk.NONE` is the
natural-looking default and the wrong one — a site that forgot the field would
report clean, and the axis would fail open.

MEASURED ADDITIVE, not assumed:
  - 703 -> 715 tests, no existing test changed
  - coverage matrix 128/128 recall, 6/6 documented gaps still hold
  - the PRESET_USER_UPLOAD grading table locked in 0.3.1 re-measured row by
    row: ordinary link/image/autolink/refdef -> warn on BOTH doors, unchanged

Both locked consumer promises in docs/PLAN-v1.md were checked against that
measurement and neither fires: the grading is untouched (linkedin-studio), and
the relative-target asymmetry is untouched (llm-ingestion-okf).

Scope held to disposition, per PLAN-v1.md:380. Version stays 0.4.0; the 0.5.0
bump lands in its release commit with all five version surfaces at once —
that is the fix for the defect where the v0.4.0 tag carried a 0.3.4 README.

Records limitation 32: `Severity` still carries disposition intent on the
DETECTION side, which this change does not address and cannot without moving
the grading.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-10 20:55:46 +02:00
commit de097110d2
7 changed files with 329 additions and 38 deletions

View file

@ -90,6 +90,34 @@ DISPOSITION_RANK = {
"fail_secure": 2,
}
# --- assessment: the risk axis (0.5.0 axis separation) ----------------------
# Rank of each risk level, same value-keyed convention as DISPOSITION_RANK.
# ``Risk`` answers *how dangerous is this artifact given its source context*;
# ``Disposition`` answers *what should the pipeline do*. They were one enum
# through 0.4.0, which meant a consumer wanting a different action had to
# re-derive it from the action itself — the assessment was already discarded.
RISK_RANK = {
"none": 0,
"low": 1,
"elevated": 2,
"severe": 3,
}
# The default risk -> disposition mapping, keyed by both enum *value* strings.
# This map is what keeps the separation additive: it reproduces every
# disposition 0.4.0 rendered, so a caller that ignores the new axis sees no
# change at all. A ``Policy`` may override it; ``None`` means "use this".
#
# NONE and LOW both map to ``warn`` deliberately — that collapse is precisely
# the information 0.4.0 could not express, since a clean document and one
# carrying only low-severity findings were the same single value.
DEFAULT_ACTION_MAP = {
"none": "warn",
"low": "warn",
"elevated": "quarantine_review",
"severe": "fail_secure",
}
# --- active_content: per-construct severities -------------------------------
# Zero-click auto-fetch / auto-execute constructs are HIGH; click-required ones
# are MEDIUM. Mirrors ``neutralize``'s defang classes. These are the severities