polix.residual

Residual data model for policy unification.

Residuals represent the result of unifying a policy with a document:

  • {} (empty map) — satisfied, no constraints remain
  • {:key [constraints]} — partial, constraints remain on keys

Constraints come in two forms:

  • Open constraints like [:< 10] — awaiting evaluation
  • Conflict constraints like [:conflict [:< 10] 11] — evaluated and failed

A conflict [:conflict C w] records that constraint C was evaluated against witness value w and failed. This preserves diagnostic information about what was required and what was actually provided.

Residual Structure

A residual is a map from paths to constraint vectors:

{[:role] [[:= "admin"]]
 [:level] [[:> 5] [:< 100]]}

With conflicts:

{[:mfa-age-minutes] [[:conflict [:< 10] 11]]}

Special keys: - ::cross-key — constraints comparing two document paths - ::complex — non-simplifiable expressions (quantifiers, etc.)

add-constraint

(add-constraint r path constraint)

Adds a constraint to a residual at the given path.

If the path already has constraints, the new constraint is appended. Sets ::conflict marker if adding a conflict constraint.

all-conflicts?

(all-conflicts? r)

Returns true if every constraint in the residual is a conflict.

Used to determine if NOT of a fully-conflicted residual should be satisfied. A residual where all paths have only conflict constraints represents a complete contradiction.

(all-conflicts? {[:x] [[:conflict [:< 10] 15]]})           ;=> true
(all-conflicts? {[:x] [[:conflict [:< 10] 15] [:> 5]]})   ;=> false
(all-conflicts? {[:x] [[:< 10]]})                          ;=> false

combine-residuals

(combine-residuals r1 r2)

Combines residuals with OR semantics.

Returns: - {} if either residual is satisfied (short-circuit) - ::complex marker if residuals have different constraint structures

OR combinations typically produce complex results because we cannot merge disjunctive constraints into a simple residual structure.

In the conflict model: - If both branches have conflicts, both are preserved in the complex marker - This enables showing ‘fix either A or B’ in UIs

Note: For backward compatibility, nil inputs are handled but new code should not produce nil residuals.

conflict

(conflict inner-constraint witness)

Creates a conflict constraint tuple.

A conflict records that inner-constraint was evaluated against witness and failed. The inner constraint is preserved for diagnostic and recovery purposes.

(conflict [:< 10] 11)
;; => [:conflict [:< 10] 11]

The conflict structure enables: - Diagnostic messages: ‘required < 10, got 11’ - Recovery guidance: the inner constraint tells you what would satisfy - Uniform handling: no special nil case

conflict-constraint

(conflict-constraint c)

Extracts the inner constraint from a conflict.

(conflict-constraint [:conflict [:< 10] 11])
;; => [:< 10]

Returns nil if not a valid conflict.

conflict-residual

(conflict-residual path inner-constraint witness)

Creates a residual with a single conflict constraint.

Convenience function combining residual and conflict:

(conflict-residual [:x] [:< 10] 15)
;; => {::conflict true, [:x] [[:conflict [:< 10] 15]]}

The ::conflict marker enables O(1) conflict detection.

conflict-witness

(conflict-witness c)

Extracts the witness value from a conflict.

(conflict-witness [:conflict [:< 10] 11])
;; => 11

Returns nil if not a valid conflict.

conflict?

(conflict? x)

Returns true if x is a conflict constraint tuple.

(conflict? [:conflict :< 10 11]) ;=> true (conflict? :< 10) ;=> false (conflict? nil) ;=> false

constraints->residual

(constraints->residual constraints)

Converts a sequence of constraint maps back to a residual.

constraints-for

(constraints-for r path)

Returns the constraints for a given path in the residual, or nil.

has-complex?

(has-complex? r)

Returns true if the residual contains complex (non-simplifiable) constraints.

has-conflicts?

(has-conflicts? r)

Returns true if the residual contains any conflict constraints.

A residual with conflicts indicates the policy was evaluated against concrete data that violated constraints. This replaces checking for nil in the old contradiction model.

Detects (in order for performance): 1. ::conflict marker (O(1) - set by conflict-residual) 2. Cross-key conflicts {:conflict true ...} 3. Collection conflicts {::complex {:type :collection-conflict}} 4. Tuple-form conflicts [:conflict C w] (O(n) fallback)

(has-conflicts? {[:x] [[:conflict [:< 10] 15]]})  ;=> true
(has-conflicts? {[:x] [[:< 10]]})                  ;=> false
(has-conflicts? {})                                ;=> false

has-cross-key?

(has-cross-key? r)

Returns true if the residual contains cross-key constraints.

map-constraints

(map-constraints r f)

Applies f to each constraint vector in the residual.

f receives [path constraints] and should return [path new-constraints] or nil to remove the path.

merge-constraint-vectors

(merge-constraint-vectors v1 v2)

Merges two constraint vectors for the same key.

Combines constraints with AND semantics. Both constraint sets must be satisfied for the merged result to be satisfied.

merge-residuals

(merge-residuals r1 r2)

Merges two residuals with AND semantics.

Returns: - {} if both residuals are satisfied - Combined residual otherwise (may contain both open and conflict constraints)

Constraints on the same key are merged into a single constraint vector. Conflicts are preserved and merged alongside open constraints. The ::conflict marker is propagated if either residual has conflicts.

Note: For backward compatibility, nil inputs are propagated as nil. New code should not produce nil residuals.

open-residual?

(open-residual? r)

Returns true if r is a residual with only open (non-conflict) constraints.

An open residual indicates the policy couldn’t be fully evaluated due to missing data, but no contradictions were found in the data that was present.

(open-residual? {[:x] [[:< 10]]})                  ;=> true
(open-residual? {[:x] [[:conflict [:< 10] 15]]})  ;=> false
(open-residual? {})                                ;=> false

remove-path

(remove-path r path)

Removes all constraints for a path from the residual.

residual

(residual path constraints)

Creates a residual with constraints on a single key.

path is a vector of keys representing the document path. constraints is a vector of constraint tuples like [[:= "admin"]].

For conflicts, use conflict to create the constraint:

(residual [:x] [(conflict [:< 10] 15)])

residual->constraints

(residual->constraints r)

Converts a residual to a flat sequence of constraint maps.

Each constraint map has :path, :op, and :value keys.

residual-keys

(residual-keys r)

Returns the set of keys with constraints in the residual.

Excludes special keys like ::cross-key, ::complex, and ::conflict.

residual?

(residual? x)

Returns true if x is a residual (partial satisfaction).

A residual is a map with vector keys holding constraint vectors: {[:level] [[:> 5]]}

This distinguishes residuals from plain data maps like {:level 5}. Documents and residuals share the same structure - vector keys indicate constraints at that path.

result-type

(result-type x)

Returns the type of a unification result.

  • :satisfied — empty residual, policy fully satisfied
  • :conflict — residual contains conflicts, policy violated
  • :open — residual with only open constraints, needs more data
  • :unknown — unrecognized result type

satisfied

(satisfied)

Returns an empty residual indicating satisfaction.

satisfied?

(satisfied? x)

Returns true if x represents a satisfied policy (empty residual).

A policy is satisfied when no constraints remain after unification.