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:
parent
3c56d50e05
commit
de097110d2
7 changed files with 329 additions and 38 deletions
51
CHANGELOG.md
51
CHANGELOG.md
|
|
@ -7,7 +7,56 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
Nothing yet.
|
||||
### Added — the axis separation: assessment (`Risk`) vs action (`Disposition`)
|
||||
|
||||
> **Additive, and measured to be so.** Every disposition 0.4.0 rendered is
|
||||
> rendered identically: the full suite went 703 → 715 with no test changed, the
|
||||
> coverage matrix holds at 128/128 recall with 6/6 documented gaps, and the
|
||||
> `PRESET_USER_UPLOAD` grading table locked in 0.3.1 was re-measured row by row
|
||||
> and is unchanged. A caller that never reads the new field sees no difference.
|
||||
|
||||
`decide` and `guard` 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 the library
|
||||
returned an action it cannot enforce, while discarding 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`** — the new assessment axis: `NONE` / `LOW` / `ELEVATED` / `SEVERE`.
|
||||
It answers *how dangerous is this artifact given its source context*, and is
|
||||
trust-aware exactly as BRIEF §4.7 describes the domain: the same finding
|
||||
genuinely is a different judgement in authored prose than in a code fence.
|
||||
- **`DispositionResult.assessment`** — carries that judgement alongside the
|
||||
action. The field is **required, with no default**: `Risk.NONE` would be the
|
||||
natural-looking default and is the wrong one, since a construction site that
|
||||
forgot it would report *clean* and the axis would fail open.
|
||||
- **`Policy.action_map`** — an optional `Risk -> Disposition` override, so
|
||||
"hold for review where you would block" is a policy statement rather than a
|
||||
reason to pin our grading. Defaults to `None`, which means
|
||||
`DEFAULT_ACTION_MAP` and keeps an untouched `Policy` hashable as before. A
|
||||
partial map falls back per-level instead of raising.
|
||||
- Both overlays — compound escalation and the quarantine floor — now move the
|
||||
**assessment**, so a custom action map cannot silently drop them.
|
||||
- The fail-closed path in `guard` pins both axes to their most severe value and
|
||||
deliberately does **not** route through the action map: a policy that
|
||||
downgrades `SEVERE` means "I accept this class of finding", never "I accept a
|
||||
scanner that crashed on crafted input" (§4.6).
|
||||
|
||||
`NONE` and `LOW` both map to `WARN`, which is the point rather than an
|
||||
oversight: a clean document and one carrying only low-severity findings were a
|
||||
single indistinguishable value through 0.4.0.
|
||||
|
||||
### Known limitation recorded (32, was 31)
|
||||
|
||||
`Severity` still carries disposition intent on the *detection* side — the
|
||||
separation above is caller-side only. Two places say so outright:
|
||||
`ACTIVE_CONTENT_ORDINARY_SEVERITY = LOW` exists because grading an ordinary
|
||||
external image `HIGH` fail-secured ordinary uploads, and the quarantine floor
|
||||
was raised to `MEDIUM+` to repair the same regression from the other end.
|
||||
Closing it changes the grading and so fires a consumer-notification promise;
|
||||
deferred deliberately. See `docs/LIMITATIONS.md`.
|
||||
|
||||
|
||||
## [0.4.0] — 2026-08-10
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue