# Gateway Rules

First-stage filters/samplers. This is the area with the largest **intentional** semantic
divergence between the two systems — worth understanding clearly since it changes what a
"gateway rule" actually means in each.

## Jube

Source: `Jube.Engine/.../Context/Extensions/GatewayRulesExtensions.cs:48-104`,
`EntityModelGatewayRule.cs`, pipeline order in `EntityAnalysisModelInvoke.cs:118-187`.

- Each gateway rule has a `GatewaySample` (a sampling probability) and a `MaxResponseElevation`
  (a cap on how much downstream response elevation this transaction can reach).
- Evaluation draws a random sample and **iterates rules until one matches, then `break`s** — first
  match wins, short-circuiting the loop (`GatewayRulesExtensions.cs:96`).
- A match sets `MatchedGatewayRule = true` and `ResponseElevationLimit = gatewayRule.MaxResponseElevation`.
- **There is no `IsBypass` boolean.** The bypass/continue semantics are entirely implicit in one
  check at the call site: `EntityAnalysisModelInvoke.cs:147` — **if no gateway rule matched at
  all, the entire rest of the pipeline (TTL counters, sanctions, dictionary lookups, abstraction
  rules, activation rules) is skipped outright.** A transaction that doesn't match *any* gateway
  rule effectively gets no further scoring.
- No approval-workflow (`ReviewStatus`) gate exists for gateway rules at all — only an `Active`
  flag, checked at sync/load time.

## mw-core

Source: `apps/mw_risk/lib/mw_risk/gateway_rule_engine.ex`, `apps/mw_risk/lib/mw_risk/scoring_pipeline.ex`.

- Each gateway rule has `gateway_sample` (sampling %), `priority` (evaluation order), and an
  explicit **`is_bypass` boolean**.
- Evaluation iterates **all** rules in priority order. `is_bypass=false` rules that match **flag
  and continue** (their label is added to `fired_rules`, evaluation keeps going to the next rule
  and then on to full scoring). `is_bypass=true` rules **halt evaluation and short-circuit
  straight to `:approve`** (`{:halt, {:bypass, label}}` in `GatewayRuleEngine.check/1`).
- **If zero gateway rules match at all, the pipeline still runs in full** — TTL counters,
  abstraction rules, and activation rules all execute regardless. This is the **opposite of
  Jube's "no match = skip everything"** semantics.
- A special case added this engagement: a matched rule whose expression contains a `"sanction"`
  op **always declines**, regardless of its `is_bypass` value — found because a rule named
  `"Bypass - Sanctions Exact Match"` had `is_bypass=true`, meaning a sanctions hit was
  short-circuiting straight to **approve** (`is_bypass=true` → "trusted fast-path, skip scoring
  and approve"). Fixed by adding a `declines?/1` check ahead of the `is_bypass` branch in
  `GatewayRuleEngine.check/1`, returning a new `{:decline, label}` result that `ScoringPipeline`
  now also handles.
- Approval workflow (`review_status`): see [08-FIXED-GAPS-CHANGELOG.md](08-FIXED-GAPS-CHANGELOG.md)
  — was completely unenforced (DB/UI had the concept, application layer didn't even declare the
  Ecto field) until fixed; now filtered at query time in `GatewayRuleEngine.load_rules/2`.

## Gap table

| Aspect | Jube | mw-core | Tag |
|---|---|---|---|
| No-match behavior | Skip entire downstream pipeline | Continue to full scoring regardless | 🟡 Intentional divergence — confirmed not a bug, but a real behavioral difference worth knowing |
| First-match-wins vs. accumulate | First match short-circuits the loop entirely | `is_bypass=false` matches accumulate (multiple can fire); only `is_bypass=true` (or a sanction-op match) short-circuits | 🟡 Intentional divergence |
| Bypass semantics | Implicit (absence of any match) | Explicit per-rule boolean (`is_bypass`) | 🟡 Intentional divergence — mw-core's model supports per-rule fast-path approval, which Jube's binary match/no-match model doesn't |
| Sanction-match short-circuit polarity | N/A — sanctions aren't gateway-rule-expression concepts in Jube (see [02](02-RULE-EXPRESSION-ENGINE.md)) | Originally could approve via `is_bypass=true` on a sanction-hit rule — backwards | 🔴 Fixed this engagement |
| Approval workflow enforcement | Enforced at load time (`ReviewStatusId`) | Was never enforced anywhere | 🔴 Fixed this engagement |
| Sampling | `GatewaySample` probability | `gateway_sample` %, same concept | 🟢 Parity |
| Cross-model isolation | Structural (per-model `Collections`) | Was broken (tenant-only scoping); fixed this engagement | 🔴 Fixed — see [07](07-MULTI-MODEL-SCOPING.md) |

## Open items

- mw-core has no equivalent of Jube's "no gateway match = skip everything" mode. If product intent
  is for gateway rules to act as a true admission gate (not just a sampler/flagger), this would be
  a deliberate, larger behavioral change — not done in this engagement since it wasn't requested
  and changes scoring outcomes for every existing model.
