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)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)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.