Skip to content

investigate: drop the M generic from Policy / PolicyRule / Action #277

Description

@martsokha

Background

After #276 landed, `Policy` is generic over modality purely so `Action::Redact { operator: M::Redaction }` is statically typed. Everything else in `Policy` (name, version, description, retention, selector, conditions) is modality-agnostic.

The `M` generic forces:

  • An `AnyPolicy` sum-type wrapper at every public surface (`DetectionInput::policies`, request payloads, etc.)
  • A `PolicyStore` with `TypeMap`-keyed per-modality buckets and a `from_any_policies` dispatch step
  • Per-modality match arms scattered through the dispatch sites (`pipeline/detection/pipeline.rs`, `core/policy_store.rs`)
  • The audit's `Decision` enum carries `operator: M::Redaction` and stays modality-typed

`AnyRedaction` already exists in `crates/nvisy-engine/src/policy/redaction/any.rs` as a tagged sum over `TextRedaction` / `TabularRedaction` / `ImageRedaction` / `AudioRedaction`. If `Action` carried `AnyRedaction` directly the whole `` generic could go away.

Proposal sketch

```rust
struct Policy {
name: HipStr<'static>,
version: Version,
description: Option,
rules: Vec,
default_action: Option,
retention: Vec,
}

struct PolicyRule {
name: HipStr<'static>,
selector: EntitySelector,
action: Action,
conditions: Vec,
enabled: bool,
}

enum Action {
Redact { operator: AnyRedaction },
Suppress,
}
```

`DetectionInput::policies: Vec`. `AnyPolicy` deleted. `PolicyStore`'s typed buckets reduce to a single `Vec<Arc>`. The resolver walks rules and skips ones whose `operator.modality()` doesn't match the entity's modality.

Trade-offs to investigate

Property Today (typed `Policy`) Proposed (untyped + `AnyRedaction`)
Author writes a text operator on an image rule Deserialise fails Deserialises; runtime silently skips
Surface area `AnyPolicy` sum + per-modality match arms everywhere One `Policy` type, one `Vec<Arc>`
Adding a new modality `DocumentModality::Redaction` assoc + `AnyPolicy` arm + dispatch arm `AnyRedaction` arm + matching check
Compile-time modality mismatch detection Yes No

The compile-time guarantee is the only real win. Everything else is overhead the current shape forces on consumers.

Mitigating the loss of compile-time check

Two ways to recover early validation:

  1. Add a `modality` field on `PolicyRule`. Authors declare it explicitly; `validate_policy_namespace` (or a sibling pass) walks rules and rejects ones whose `operator.modality()` disagrees with the rule's declared modality. Re-introduces a per-rule tag at the data layer, not the type layer.
  2. Reject silently at runtime; trust the author. Smallest code; relies on the author to keep the operator and the selector in sync. The audit's `PolicyDecisionRef` already names the rule, so misconfigurations are debuggable.

(1) feels right for a redaction engine where silent skips would be a compliance gap.

What to look at

  • Every call site that holds `Policy` / `PolicyRule` / `Action` — currently dozens of generic functions threaded through `phases/redaction/mod.rs`, `pipeline/redaction/applicator.rs`, `pipeline/detection/pipeline.rs`, `core/policy_store.rs`, the audit `Decision` enum.
  • How `Decision::Redact { operator: M::Redaction }` reads after the change. Probably becomes `Decision::Redact { operator: AnyRedaction }` and the apply phase pattern-matches.
  • Whether `RedactionRegistry` can stay typed if the policy layer drops typing. The toolkit's `Anonymizer` chain is the boundary where typing genuinely matters; the policy layer might be where it stops.
  • Does dropping the generic regress TOML deserialise quality? Today a typo in `operator.kind` for an `image` policy fails at parse-time with a clear message. Under the new shape it would parse fine and skip silently — unless we add option (1) above.

Out of scope

References

  • refactor(engine): inline policies on DetectionInput; drop /policies resource #276 (the inline-policies refactor this would build on)
  • `crates/nvisy-engine/src/policy/mod.rs` — `Policy` definition
  • `crates/nvisy-engine/src/policy/rule.rs` — `PolicyRule` and `Action`
  • `crates/nvisy-engine/src/policy/redaction/any.rs` — existing `AnyRedaction`
  • `crates/nvisy-engine/src/core/policy_store.rs` — typed dispatch the `M` generic forces

Metadata

Metadata

Assignees

No one assigned

    Labels

    architecturearchitectural decision records and cross-cutting design issuesengineredaction engine, pipeline runtime, orchestration, configurationrefactorcode restructuring without behavior change

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions