Skip to content

feat(masking): bind masking decisions to classification stance — the missing seam - #581

Merged
mdheller merged 1 commit into
mainfrom
feat/masking-decision-classification-stance
Aug 4, 2026
Merged

feat(masking): bind masking decisions to classification stance — the missing seam#581
mdheller merged 1 commit into
mainfrom
feat/masking-decision-classification-stance

Conversation

@mdheller

@mdheller mdheller commented Aug 4, 2026

Copy link
Copy Markdown
Member

The masking PDP decides what to do with a field. The six-layer fingerprint classifier (SP-FPRINT-STACK-001, contracted in SourceOS-Linux/sourceos-spec#284) decides what a field is.

Nothing joined them. masking-decision.v1 carried no data_class_ref, no stance, no confidence of any kind — so the PDP could not distinguish "this field is confidently public" from "we have no idea what this field is." Both arrive as the absence of a matching policy, and the unknown field gets served.

What's added

applied_transforms[].classificationdata_class_ref, stance (POS/NEG/ZERO/INADMISSIBLE), stance_ref, zero_disposition, zero_attestation_ref.

An absent classification block reads as ZERO, never as safe.

DR-4 enforced at this layer

Invariant Rule
INADMISSIBLE_CLASSIFICATION_ALLOWED A glut can never be resolved by allowing. If any field's stance is INADMISSIBLE, the verdict must not be allow
ZERO_CLASSIFICATION_FAIL_OPEN_UNATTESTED Serving data whose classification is unknown requires an attestation

INADMISSIBLE means two fingerprint layers disagree about what the field is, and one reading may well be "personal data". Serving it lets the contradiction settle in favour of whoever asked — the one resolution nobody chose. review_required is where a glut belongs.

fail-open on ZERO is legitimate for a public reference table and a breach for a customer-master column. That asymmetry is exactly why it's decided on the record rather than defaulted.

Structural, not validator-only

Both invariants are draft 2020-12 if/then in the schema, with negative fixtures that must be rejected and a positive fixture showing the same shapes done right (glut → review_required; ZERO fail-open with an attestation). 6 rejected, 5 accepted.

The conformance suite earned its keep

The first version of the ZERO conditional was nested one level too deep — it sat inside the classification subschema, so its if looked for classification.classification and could never match. The schema accepted the under-masked fixture.

The validator reported ZERO_CLASSIFICATION_FAIL_OPEN_UNATTESTED has no teeth instead of passing green. A conformance suite that only checked acceptance would have shipped a privacy invariant that enforced nothing.

…missing seam

The masking PDP decides what to DO with a field. The six-layer fingerprint classifier
(SP-FPRINT-STACK-001) decides what a field IS. Nothing joined them, so masking-decision.v1
carried no data_class_ref, no stance, no confidence of any kind — and the PDP could not tell
"this field is confidently public" from "we have no idea what this field is". Both arrived as
the absence of a matching policy, and the unknown field was served.

Adds applied_transforms[].classification { data_class_ref, stance, stance_ref,
zero_disposition, zero_attestation_ref } and enforces DR-4 at this layer:

  * INADMISSIBLE_CLASSIFICATION_ALLOWED — a glut can never be resolved by allowing. If any
    field's stance is INADMISSIBLE the verdict must not be `allow`. INADMISSIBLE means two
    fingerprint layers disagree about what the field IS and one reading may well be "personal
    data"; serving it lets the contradiction settle in favour of whoever asked, which is the
    one resolution nobody chose. review_required is where a glut belongs.
  * ZERO_CLASSIFICATION_FAIL_OPEN_UNATTESTED — serving data whose classification is unknown
    requires an attestation. fail-open on ZERO is legitimate for a public reference table and
    a breach for a customer-master column, which is precisely why it is decided on the record
    rather than defaulted.

An absent classification block reads as ZERO, never as safe.

Both invariants are structural (draft 2020-12 if/then), not validator-only, with negative
fixtures that must be rejected and a positive fixture showing the same shapes done right
(glut -> review_required; ZERO fail-open with an attestation). 6 rejected, 5 accepted.

The negative-fixture validator earned its keep here: the first version of the ZERO
conditional was nested one level too deep — it sat inside the `classification` subschema, so
its `if` looked for classification.classification and could never match. The schema accepted
the under-masked fixture and the validator reported "ZERO_CLASSIFICATION_FAIL_OPEN_UNATTESTED
has no teeth" rather than passing green. A conformance suite that only checks acceptance would
have shipped it.
@mdheller
mdheller merged commit 9214b22 into main Aug 4, 2026
35 checks passed
@mdheller
mdheller deleted the feat/masking-decision-classification-stance branch August 4, 2026 07:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant