Constraint Composition and Policy Precedence
Pattern classification: Canonical Pattern
Difficulty: Intermediate
Prerequisites: Policy Context and Explicit Decision Outcomes
The foundational policy-context tutorial establishes two important ideas:
Policy context contains facts.
Constraints interpret those facts.
That separation remains correct when a system grows beyond one compact policy method.
The next architectural question is what happens when several independent constraints interpret the same context and do not all return the same result.
This article focuses on that middle part of the governance spine:
Intent / request
↓
Policy context
↓
Constraint evaluation
↓
Constraint-result composition
↓
Base governance decision
↓
Optional decision policy
↓
Final governance decision
↓
Host-owned continuation or execution
The central lesson is:
Constraint evaluation and constraint composition are different responsibilities.
A constraint answers a narrow rule question.
A composer answers how multiple rule results become one base decision.
An optional decision policy can then apply broader host or domain rules without taking ownership of the protected side effect.
Why This Separation Matters
A single policy method can hide several responsibilities inside one sequence of if statements:
if (!actor.IsAdministrator)
{
return Denied;
}
if (resource.IsProtected)
{
return EscalationRecommended;
}
if (environment.MaintenanceHoldActive)
{
return Deferred;
}
return Allowed;
For a small operation, that may be entirely reasonable.
But once rules become independently authored, registered, tested, versioned, or reused, the ordering of those statements can accidentally become the policy.
For example:
Rule A returns Warning
Rule B returns Deny
Rule C returns NotApplicable
The system still needs to answer:
What is the final decision?
Which reasons survive?
Do all rules run?
Does order matter?
What happens if no rules exist?
What happens if one rule throws?
Can a later host policy introduce acknowledgment or escalation?
Those are composition questions, not individual constraint questions.
Keep the Pipeline Vocabulary Explicit
A useful policy pipeline separates five responsibilities.
| Stage | Responsibility | Should not own |
|---|---|---|
| Policy context | Capture the facts available for this decision | Hidden rule logic or execution |
| Constraint evaluation | Let each rule interpret the same decision context | Global precedence or side effects |
| Constraint composition | Combine individual constraint results into one base governance decision | Protected execution |
| Decision policy | Optionally reshape or raise the composed decision according to host/domain policy | Protected execution |
| Host execution | Enforce the final decision and perform or refuse the operation | Reinterpreting policy implicitly |
The distinction can be summarized as:
Policy context
= facts available for the decision
Constraint evaluation
= individual rules interpret those facts
Constraint composition
= constraint results become a base governance decision
Decision policy
= optional host policy may raise or reshape the composed decision
Host execution
= remains outside policy evaluation
Keeping these stages visible makes policy behavior easier to test, explain, and review.
Constraint Results Are Local Findings
An individual constraint should normally return a result about its own rule, not a conclusion about the entire workflow.
A small framework-neutral result vocabulary is:
Allow
Warning
Deny
NotApplicable
These results are intentionally narrower than the final governance-decision vocabulary.
Allow
The constraint evaluated the context and found no reason to block or warn.
Constraint: actor must be authenticated
Result: Allow
Allow does not mean the entire operation is allowed.
It means only:
This constraint did not block the operation.
Another constraint may still deny it.
Warning
The constraint found a condition that should remain visible but does not, by itself, block continuation.
Constraint: export size is unusually large
Result: Warning
Reason: export.large-volume
A warning should not become a denial merely because it is evaluated before a stricter rule.
Deny
The constraint intentionally blocks the proposed operation.
Constraint: actor and resource must share a tenant
Result: Deny
Reason: export.cross-tenant
A returned denial is an expected policy outcome.
It should be represented as data, not as an exception.
NotApplicable
The constraint does not apply to this context.
Constraint: contractor export-hours restriction
Context: actor is an employee
Result: NotApplicable
This is different from Allow.
Allow means the rule applied and passed.
NotApplicable means the rule had nothing to decide.
That distinction helps diagnostics and policy review, even when both are neutral during composition.
Composition Is a Policy of Its Own
Suppose three constraints return:
Allow
Warning
Deny
A naive implementation may accidentally use registration order:
foreach (var constraint in constraints)
{
var result = await constraint.EvaluateAsync(context);
if (result.IsTerminal)
{
return result;
}
}
If "terminal" is not rigorously defined, the first result that happens to look final can win.
That means changing dependency-injection registration order can change governance behavior.
This is usually a design smell.
A composition policy should instead state the precedence rule directly.
For example:
Deny > Warning > Allow
NotApplicable = neutral
The symbols here describe composition precedence, not numeric enum values.
The important point is that the ordering is documented and tested.
A Conservative Base Composition
The current AsiBackbone default evaluator uses an intentionally conservative base composition model:
- Any denial produces a denied base decision.
- If there is no denial but at least one warning, the base decision is warning.
- If constraints exist and none deny or warn, the base decision is allowed.
- Not-applicable results do not block.
- With the default configuration, an empty constraint collection denies rather than silently allowing.
In simplified pseudocode:
GovernanceDecision Compose(
IReadOnlyList<ConstraintResult> results)
{
if (results.Count == 0)
{
return GovernanceDecision.Deny(
"policy.no-constraints",
"No active constraints were available.");
}
ConstraintResult[] denials =
[.. results.Where(result => result.IsDenied)];
if (denials.Length > 0)
{
return GovernanceDecision.Deny(
reasons: denials.SelectMany(result => result.Reasons));
}
ConstraintResult[] warnings =
[.. results.Where(result => result.IsWarning)];
if (warnings.Length > 0)
{
return GovernanceDecision.Warning(
reasons: warnings.SelectMany(result => result.Reasons));
}
return GovernanceDecision.Allow();
}
This is a teaching sketch, not a reproduction of the implementation source.
Its purpose is to make the precedence rule visible.
Denial Precedence
Denial precedence means:
If any active constraint intentionally blocks the request, the base decision cannot become allowed merely because other constraints passed.
For example:
| Constraint | Result | Reason |
|---|---|---|
| Actor authenticated | Allow | — |
| Same tenant | Deny | export.cross-tenant |
| Export-size check | Warning | export.large-volume |
A denial-wins policy produces:
Base decision: Denied
The successful authentication result does not cancel the cross-tenant denial.
The warning does not outrank the denial.
This sounds obvious when written down.
It becomes less obvious when rules are scattered across handlers, filters, middleware, feature flags, and service registrations.
That is why precedence belongs in one explicit composition boundary.
Warning-Only Behavior
If no constraint blocks but one or more constraints warn, a warning-only composition can preserve advisory context while still allowing the host to continue.
Example:
| Constraint | Result |
|---|---|
| Actor authenticated | Allow |
| Same tenant | Allow |
| Export-size check | Warning |
| Contractor-hours rule | NotApplicable |
The base result becomes:
Warning
The host can then decide whether warning is a proceedable outcome.
In the Learning model and current AsiBackbone decision model, warning is proceedable, but its reason should remain visible for review or audit.
NotApplicable Is Neutral, Not Approval
A common mistake is to treat NotApplicable as equivalent to global approval.
Consider:
Constraint A: NotApplicable
Constraint B: NotApplicable
Constraint C: Deny
The final base decision is still denied.
NotApplicable does not vote yes.
It simply contributes no blocking or warning result.
Another edge case is:
Constraint A: NotApplicable
Constraint B: NotApplicable
The current AsiBackbone default evaluator treats a non-empty constraint set with no denials or warnings as allowed.
That is a distinct case from having zero constraints, which denies by default.
A host that wants all-not-applicable to fail closed can adopt a stricter documented composition policy, but that behavior should be intentional and tested.
Worked Example: Governed Data Export
Consider a host that evaluates a proposed data export.
The context snapshot contains facts only:
public sealed record ExportPolicyContext(
string ActorId,
string ActorType,
string ActorTenant,
string ResourceTenant,
string Region,
string Classification,
int RecordCount,
bool MaintenanceHoldActive,
string CorrelationId,
string PolicyVersion);
For one request:
ActorId: ops-agent-17
ActorType: Employee
ActorTenant: tenant-a
ResourceTenant: tenant-a
Region: US-LA
Classification: Restricted
RecordCount: 250000
MaintenanceHold: false
PolicyVersion: export-policy/12
Five independent constraints evaluate the same snapshot.
Constraint 1 — Authenticated Actor
Question:
Is the actor identity acceptable for this operation?
Result:
Allow
Constraint 2 — Tenant Boundary
Question:
Does the actor tenant match the resource tenant?
Result:
Allow
Constraint 3 — Large Export Advisory
Question:
Does the record count exceed the review threshold?
Result:
Warning
Reason:
export.large-volume
Constraint 4 — Restricted Regional Export
Question:
May Restricted data be exported under the active regional rule?
Result:
Deny
Reason:
export.region.restricted
Constraint 5 — Maintenance Hold
Question:
Does a maintenance hold affect this operation?
Result:
NotApplicable
The individual results are therefore:
| Constraint | Result | Reason |
|---|---|---|
| Authenticated actor | Allow | — |
| Tenant boundary | Allow | — |
| Large export advisory | Warning | export.large-volume |
| Restricted regional export | Deny | export.region.restricted |
| Maintenance hold | NotApplicable | — |
Compose the Base Decision
Under a deny-wins full-evaluation policy:
Allow
Allow
Warning
Deny
NotApplicable
↓
Denied
The base decision is denied because one active constraint blocks the request.
Under the current AsiBackbone default full-evaluation behavior, a denied composed decision focuses on blocking rationale rather than copying warning-only reasons into the final denial.
So the final blocking reason would include:
export.region.restricted
The warning still existed as an evaluated constraint result, but it is not elevated into the blocking rationale by the default composer.
Compare Composition Policies Deliberately
The same constraint results can be combined in different documented ways.
| Composition policy | Result for the worked example | Tradeoff |
|---|---|---|
| Deny wins, full evaluation | Denied | Strong audit visibility; all blocking constraints can contribute reasons |
| Deny wins, short-circuit on first denial | Denied | Lower latency; later constraints do not run |
| First terminal result wins | Depends on rule order | Simple but highly order-sensitive unless ordering itself is the intended policy |
| Preserve warnings with denials | Denied with warning + denial context | Richer receipt, but can make blocking rationale noisier |
| Custom severity ladder | Depends on documented ladder | Can fit a domain, but must avoid arbitrary enum-number ordering |
The lesson is not that one table row is universally correct.
The lesson is:
Choose the policy, document it, and test it. Do not inherit it accidentally from iteration order.
Full Evaluation Versus Short-Circuiting
Two legitimate evaluator strategies are common.
Full Evaluation
Every active constraint runs.
Constraint A
Constraint B
Constraint C
Constraint D
↓
Compose all results
Benefits:
- More complete denial-reason visibility.
- Better policy diagnostics.
- Better reviewer understanding.
- Easier comparison of active rules.
- Useful for audit-heavy paths.
Costs:
- More work per decision.
- Potentially higher latency.
- More downstream dependency calls if constraints are not purely local.
- More telemetry and diagnostic volume.
The current AsiBackbone default favors full evaluation.
Short-Circuit on First Denial
Evaluation stops when the first blocking constraint is found.
Constraint A: Warning
Constraint B: Deny
↓
Stop
Constraint C: not evaluated
Constraint D: not evaluated
Benefits:
- Lower latency when denials are common.
- Less unnecessary work after a block is known.
- Useful for high-throughput or expensive-policy paths.
Costs:
- Later denial reasons are unavailable.
- Later warnings are unavailable.
- Diagnostics now depend more visibly on constraint order.
- Reordering constraints can change the reason set even when the final outcome remains denied.
In the current AsiBackbone evaluator, short-circuit behavior is an explicit option rather than an incidental side effect.
That distinction matters.
If the host chooses fast-abort behavior, it should treat the reduced evidence set as part of the architectural tradeoff.
Preserve Useful Reasons Without Hiding the Block
Reason codes are part of policy explainability.
A useful denial should answer:
Why was this blocked?
Which constraint produced the reason?
Which policy version was active?
Which correlation ID links the evaluation to surrounding evidence?
For full evaluation, a composer may aggregate multiple denial reasons:
export.cross-tenant
export.region.restricted
export.actor.insufficient-assurance
A composer should avoid replacing those with a vague message such as:
Policy failed.
At the same time, more information is not always better.
The current AsiBackbone default full-evaluation path keeps a denied decision focused on denial reasons rather than mixing advisory warnings into the final blocking rationale.
A host that needs warnings preserved alongside denials can implement that deliberately in a decision policy.
The key is that the choice is visible.
Empty Policy Is an Architectural State
An empty constraint collection is not always equivalent to:
No restrictions exist.
It may instead mean:
Dependency injection failed
Configuration did not load
Policy discovery returned nothing
A feature flag removed every rule
A database-backed policy source is unavailable
The wrong policy set was selected
For governed surfaces, silently treating that state as allow can turn a configuration failure into an authorization or governance bypass.
That is why the current AsiBackbone 3.x default is fail closed:
Zero constraints
↓
Denied
with the implementation reason code:
asibackbone.policy.no_constraints
Hosts can explicitly choose permissive empty-policy behavior for controlled local scenarios, but the opt-out should be deliberate and observable.
Expected Denial Is Not an Exception
Constraints should return explicit results for expected policy outcomes.
Prefer:
return ConstraintResult.Deny(
"export.region.restricted",
"Restricted exports are not permitted by the active regional policy.");
Avoid:
throw new InvalidOperationException(
"Restricted exports are not permitted.");
The first form means:
The rule evaluated successfully
and intentionally denied the request.
The second means:
Constraint evaluation faulted unexpectedly.
Those are different operational states and should remain distinguishable.
Unexpected Constraint Exceptions
A governed evaluator still needs a posture for unexpected constraint failures.
Possible choices include:
Propagate exception
Fail closed as denial
Defer for retry
Escalate for review
The current AsiBackbone 3.x default converts eligible non-cancellation, non-critical constraint exceptions into a denied decision using:
asibackbone.policy.constraint_exception
This is a safety default, not a policy-authoring shortcut.
Expected denial logic should still return a denial result normally.
Cancellation remains cancellation rather than being rewritten as a policy denial, and critical host/runtime failures should not be disguised as ordinary policy outcomes.
Production systems should also keep public reason messages free of exception text, stack traces, secrets, raw payloads, connection strings, and unnecessary user data.
Composition Should Not Depend on Incidental Registration Order
Suppose these constraints are registered:
services.AddSingleton<IConstraint, LargeExportConstraint>();
services.AddSingleton<IConstraint, RegionalExportConstraint>();
services.AddSingleton<IConstraint, TenantBoundaryConstraint>();
If the intended rule is:
Any denial wins
then moving TenantBoundaryConstraint to the first registration position should not change an allowed result into a denied result or vice versa.
With full evaluation and explicit deny precedence, the outcome remains stable.
Registration order may still affect presentation details such as reason ordering, and it intentionally affects which reasons are observed when short-circuiting is enabled.
Those are narrower consequences than making the entire governance outcome depend on whichever rule ran first.
Do Not Encode Precedence in Enum Numbers
A tempting shortcut is:
public enum Outcome
{
Allow = 0,
Warning = 1,
AcknowledgmentRequired = 2,
Deferred = 3,
EscalationRecommended = 4,
Denied = 5
}
followed by:
final = results.Max(result => result.Outcome);
This looks elegant but hides important semantics.
Deferred, AcknowledgmentRequired, and EscalationRecommended are not simply points on one universal severity scale.
They describe different workflow states.
For example:
- Deferred may mean "try again after maintenance."
- Acknowledgment required may mean "a qualified actor can continue after an explicit checkpoint."
- Escalation recommended may mean "route to a different authority."
Treating them as ordinal severity values can create accidental policy.
A better separation is:
Constraint results
↓
Base composition with narrow precedence
↓
Optional decision policy with explicit workflow rules
The Decision Policy Comes After Base Composition
Constraint composition should answer the narrow question:
What do the individual constraints collectively say?
A decision policy can then answer a broader host or domain question:
Given that composed result and this context, what governance state should the host observe next?
For example:
Base decision: Warning
Context risk: High
↓
Decision policy
↓
AcknowledgmentRequired
Or:
Base decision: Allowed
Regional overlay: Unsupported jurisdiction
↓
Decision policy
↓
Denied
Or:
Base decision: Allowed
Gateway readiness evidence: Incomplete
↓
Decision policy
↓
EscalationRecommended
This gives the system a place to model outcomes such as:
Deferred
AcknowledgmentRequired
EscalationRecommended
without forcing every individual constraint to understand the entire workflow.
A Decision Policy Should Not Become an Execution Engine
The decision-policy boundary is still policy evaluation.
Avoid:
public async Task<GovernanceDecision> ApplyAsync(...)
{
if (composedDecision.CanProceed)
{
await externalService.ExecuteAsync(); // wrong boundary
}
return composedDecision;
}
The policy has now acquired side-effect authority.
Prefer:
Context
↓
Constraints
↓
Base decision
↓
Decision policy
↓
Final decision
↓
Host enforces final decision
↓
Host performs or refuses execution
The decision describes the allowed next state.
The host owns the state transition that has real-world effect.
Never Silently Weaken a Blocking Decision
A useful default rule for post-composition policy is:
Broader policy may narrow or reshape a proceedable decision, but it should not silently turn an existing block into permission.
For example:
if (!composedDecision.CanProceed)
{
return composedDecision;
}
before applying regional or readiness overlays.
There may be specialized architectures where an explicit higher-authority override exists.
If so, that override should be modeled as an explicit, auditable authority path rather than hidden inside an ordinary decision policy.
Determinism Expectations
A useful governance property is:
Same relevant context
+
Same active constraints
+
Same policy version
+
Same deterministic inputs
↓
Same governance result
This does not mean every real-world system is perfectly deterministic.
A constraint may depend on changing facts such as:
- Current account state.
- Current risk score.
- Current region configuration.
- Current maintenance state.
- Current threat intelligence.
The architectural response is to make those facts explicit when practical.
Prefer:
Host gathers current facts
↓
Creates policy-context snapshot
↓
Evaluator consumes snapshot
rather than allowing constraints to discover changing state invisibly throughout evaluation.
If a constraint must consult a live dependency, treat that dependency as part of the policy's operational assumptions and test the failure behavior explicitly.
Policy Version and Hash Matter More as Composition Grows
When several constraints and a decision policy participate, the question:
Which policy produced this decision?
becomes more important.
A useful decision or audit record should preserve policy identity such as:
Policy version
Policy hash
Correlation ID
Reason codes
That does not magically make the result reproducible.
It does make the active policy structure more explainable.
For example:
Decision: Denied
PolicyVersion: export-policy/12
PolicyHash: sha256:...
CorrelationId: 4d7f...
Reasons:
- export.region.restricted
Policy identity becomes especially important when:
- Constraint sets change over time.
- Regional overlays differ.
- Rules are loaded from configuration.
- Multiple tenants use different policy bundles.
- A later audit reviewer must understand why a historical decision differed from today's behavior.
Separate Policy Denial from Infrastructure Failure
A denial is a governance result.
A broken policy loader, unavailable database, malformed configuration, or crashed dependency is an operational failure.
A fail-closed evaluator may deliberately map some failures into a denied governance decision to prevent unsafe continuation.
That does not make the underlying operational failure equivalent to an ordinary policy denial.
Preserve the distinction through:
- Stable reason codes.
- Operational logging.
- Metrics or alerts where useful.
- Audit residue.
- Exception telemetry that does not leak sensitive data.
A reviewer should be able to tell the difference between:
Denied because the regional rule prohibited the operation
and:
Denied because a constraint unexpectedly failed and the evaluator failed closed
Host-Owned Execution Remains the Final Boundary
Constraint composition does not perform the protected operation.
Decision policy does not perform the protected operation.
A final decision should return control to the host:
GovernanceDecision decision =
await evaluator.EvaluateAsync(
context,
cancellationToken);
if (!decision.CanProceed)
{
return DenyOrRoute(decision);
}
await exportService.ExecuteAsync(
context,
cancellationToken);
A fuller governed flow may insert acknowledgment, audit, or capability validation before the host executes.
The important invariant remains:
Denied final decision
↓
Protected executor invocation count = 0
Testing the Composition Boundary
Composition deserves direct tests because it defines policy behavior across rules.
Useful invariant tests include:
Any Denial Wins
Allow + Deny
↓
Denied
Warning Wins Only When No Denial Exists
Allow + Warning + NotApplicable
↓
Warning
NotApplicable Does Not Block
Allow + NotApplicable
↓
Allowed
Full Evaluation Aggregates Blocking Reasons
Deny(reason-a) + Deny(reason-b)
↓
Denied
Reasons include reason-a and reason-b
Short-Circuit Stops Later Constraints
Warning
Deny
LaterConstraint
↓
Denied
LaterConstraint invocation count = 0
Empty Policy Fails Closed by Default
No constraints
↓
Denied
Expected Denial Does Not Throw
Known policy violation
↓
Constraint returns Deny
Unexpected Exception Uses the Configured Failure Posture
Constraint throws unexpectedly
↓
Configured fail-closed denial
or
Exception propagation
Policy Does Not Execute
Final decision produced
↓
No protected side effect until host explicitly continues
These tests make the composition rules executable rather than merely descriptive.
When a Simpler Pattern Is Better
Do not introduce a composition pipeline simply because multiple conditions exist.
A direct guard clause may be better when:
- The rule belongs to one local operation.
- The conditions are few and stable.
- The code is already easy to read and test.
- There is no need for reusable rule registration.
- No richer governance lifecycle exists.
ASP.NET Core authorization may be better when:
- The main question is access control.
- Success/failure is sufficient.
- The actor and resource are the primary inputs.
- Execution follows immediately in the same request.
- Built-in policies, requirements, and handlers express the need cleanly.
A richer constraint-composition pipeline becomes more useful when:
- Rules are independently authored or configured.
- Multiple constraints may contribute reasons.
- Policy versions or regional overlays matter.
- Audit reviewers need to understand the active rule set.
- The system needs explicit failure posture for empty or faulting policy.
- A post-composition decision policy must introduce acknowledgment, deferment, or escalation.
- Approval and execution are deliberately separate architectural stages.
Use the smallest model that preserves the boundaries the problem actually needs.
Relationship to the Foundational Tutorial
Policy Context and Explicit Decision Outcomes intentionally uses a compact policy class so the beginner lesson remains visible:
Explicit facts
↓
Explicit rules
↓
Explicit outcome
This article expands the middle stage without replacing that tutorial:
Explicit facts
↓
Independent constraints
↓
Explicit composition
↓
Optional broader decision policy
↓
Explicit outcome
The beginner model is not wrong.
It is a smaller teaching model.
The composition model becomes useful when the rule set itself needs architecture.
Working Implementation Map
The AsiBackbone/AsiBackbone repository provides a fuller implementation of these ideas.
| Learning concept | Working implementation reference | What to inspect |
|---|---|---|
| Core policy vocabulary | Core Domain Language | Context, constraints, active policy structure, decisions, and host boundary |
| Constraint evaluation and base composition | Policy Evaluator Pipeline | Deny/warning/allow composition, empty-policy behavior, short-circuiting, exception posture, and reason handling |
| Post-composition policy | Custom Decision Policy Examples | Warning preservation, regional overlays, acknowledgment, escalation, and host-owned execution |
| Concrete evaluator | DefaultAsiBackbonePolicyEvaluator |
Source-level evaluation and composition behavior |
| Decision-policy contract | IAsiBackboneDecisionPolicy |
Boundary between base composition and host/domain decision transformation |
| End-to-end behavior | PolicyEvaluatorEndToEndTests |
Executable policy-evaluator invariants |
The Learning article remains framework-neutral on purpose.
The implementation repository is the better place to inspect package APIs, constructor overloads, options, and exact source behavior.
Review Questions
When reviewing a composed policy pipeline, ask:
- Does the policy context contain facts rather than hidden rule logic?
- Does each constraint answer one understandable rule question?
- Are
Allow,Warning,Deny, andNotApplicablemeanings explicit? - Is denial precedence documented?
- Is warning-only behavior documented?
- Is all-not-applicable behavior understood?
- Is empty-policy behavior deliberate?
- Does the system run all constraints or short-circuit?
- If it short-circuits, is the lost reason visibility acceptable?
- Are reason codes stable and useful?
- Are expected denials returned rather than thrown?
- Is unexpected exception behavior explicit?
- Can a decision policy narrow or reshape the base decision without silently weakening a block?
- Are policy version and policy hash preserved where decision lineage matters?
- Can the same deterministic context and policy version produce a stable outcome?
- Does host execution remain outside policy evaluation?
- Would ordinary ASP.NET Core authorization or a direct guard clause solve the problem with less machinery?
If several answers are unclear, the system may have policy logic, but it does not yet have a well-defined composition architecture.
Related Content
- Policy Context and Explicit Decision Outcomes — begin with explicit decision-time facts and structured outcomes.
- Decision Before Execution — revisit the boundary between a governance decision and the protected side effect.
- Acknowledgment and Audit Residue — continue from final decisions into acknowledgment and governance evidence.
- When ASP.NET Core Authorization Is Enough — compare the richer governance model with a simpler built-in authorization approach.
- Governed AI Tool Gateway — see composition participate in an end-to-end AI-assisted workflow while the host retains execution authority.
Make precedence a policy, not an accident of rule order.