7 min read
Gate Engine
The gate engine is StateAnchor's breaking change detection and policy enforcement system. On every push, it diffs the incoming spec against the previous canonical IR, classifies each change into a categorical lane, and produces a gate decision: block or proceed. The lane drives the decision.
How StateAnchor identifies drift
StateAnchor runs four independent diff operationson every sync. Each produces a separate syndrome — a structured view of what changed since a different reference point. The gate evaluates all four syndromes together.
- Parent diff— your spec vs. the direct parent commit's spec. Catches: changes introduced in this specific commit.
- Merge-base diff— your spec vs. the common ancestor with
main. Catches: changes introduced in your branch as a whole, not inmain. - LKG (last known good) diff— your spec vs. the last sync that passed the gate. Catches: accumulated drift even across many passing syncs.
- Deployed diff— your spec vs. the spec at the last production deployment. Catches: drift between what is in Git and what is actually running.
Why 4 syndromes?
A single diff would miss accumulated drift. If your spec changes gradually across 20 small PRs, each one might look fine against its parent while the LKG and deployed snapshots reveal that the API has drifted significantly from what consumers have integrated against.
ERR is raised if any syndrome produces a breaking change. This makes the gate conservative — it catches drift that single-diff tools miss. The four syndromes are evaluated independently; their findings are combined into a single verdict, but a clean parent diff does not mask an ERR against the LKG or deployed baseline.
Worked example
Say a teammate merged the removal of DELETE /users/:id into main two weeks ago, but the deploy was rolled back and the endpoint is still live in production. You branch off main and ship an unrelated spec change. Your parent diff is clean— your branch did not remove anything. A single-baseline diff tool returns PASS.
StateAnchor runs all four syndromes. The deployed diff (your spec vs. the spec of the last deployed version) shows that DELETE /users/:idis present in production but absent from your spec — an ERR on endpoint_removed. The gate blocks. You see a specific conflict — your spec drops an endpoint that is still serving requests — instead of a green check that hides the landmine.
That is the core difference between detection and enforcement. A diff tool can tell you what changed against one baseline. A gate has to reason across every baseline a consumer might be depending on — the branch’s parent, the merge target, the last gate-approved build, and the code actually running.
Categorical lanes
Every detected change is classified into one of three lanes based on its kind. The highest-severity lane present determines the gate decision. A fourth advisory lane, PREDICTIVE_WARN, overlays drift velocity projections without blocking.
ERR lane
Changes that are unambiguously breaking regardless of context. ERR lane changes always block. No score threshold applies. The only path through is an explicit exception in the exception ledger with a named approver.
| Operation | Score | Description |
|---|---|---|
endpoint_removed | 40 | An existing endpoint was deleted |
auth_changed | 35 | Authentication scheme was changed |
field_removed | 30 | A response or request field was removed |
oneOf_anyOf_modified | 30 | Union type structure was changed |
type_changed | 25 | Field type was changed (e.g., string → number) |
enum_value_removed | 25 | An enum value was removed |
WARN lane
Changes that are potentially breaking depending on consumer behavior. WARN lane changes block if the count of warnings meets or exceeds the configured threshold. The default warn_count_threshold is 0, which means WARN never blocks; set a positive integer per project to block once the WARN count reaches that value.
| Operation | Score | Description |
|---|---|---|
required_added | 20 | A new required parameter was added |
field_renamed | 15 | A field was renamed |
INFO lane
Changes that are always safe. INFO lane changes always pass. They are logged but never block or warn.
| Operation | Score | Description |
|---|---|---|
endpoint_added | 0 | A new endpoint was added |
field_added_optional | 0 | A new optional field (or parameter) was added |
description_changed | 0 | Only the description text changed |
endpoint_key_collision | 0 | Two endpoints normalize to the same key (advisory) |
Evaluator confidence scoring
The gate engine runs multiple Stage-C evaluators on every sync and uses a closure-phase scorer (feature flag CLOSURE_PHASE_SCORER, on by default in production) to measure their agreement. The scorer inspects each evaluator’s output along several dimensions and computes a per-dimension spread.
When evaluators strongly agree, the gate result is high-confidence and no advisory signal fires. When the spread is large — meaning the evaluators diverged significantly on whether a change is safe — the evaluator_disagreement_high finding (WARN lane, advisory) is appended and the ADVISORY signal shows in the PredictiveWarnBanneron the dashboard with a “judges significantly split on this change — human review recommended” message.
Evaluator disagreement does not block by itself (it’s WARN, and WARN defaults to non-blocking at warn_count_threshold = 0). It is an uncertainty signal, not a gate decision. The gate still produces a normal ERR/WARN/INFO/PASS lane; the advisory tells you the IR generation stage had reduced confidence in that lane.
PREDICTIVE_WARN lane
PREDICTIVE_WARN is a non-blocking advisory lane. It fires when the drift velocity -- the linear trend of breaking scores across your recent syncs -- projects a threshold crossing within a configurable window (default: 3 commits).
When it fires: requires at least 3 prior completed syncs. The current lane must not be ERR (if your sync is already blocked, the predictive warning is redundant).
What it means: your breaking score trend suggests you will cross the gate threshold in approximately N commits. Consider reviewing recent API changes now, before you hit a hard block.
What it does NOT do: it does not block the sync. It is informational only -- a yellow warning banner alongside a proceed gate action. It does not escalate ERR, WARN, or INFO decisions. It is an overlay, not a lane override.
How to respond:
- Review recent spec changes -- identify which operations are trending toward ERR/WARN.
- Check if the drift is intentional (e.g., planned deprecation).
- If the trend is expected, open a drift exception to suppress the projected block.
- If the trend is unintentional, fix the spec before the next push.
How to tune: predictive_horizon (default: 3) controls how many commits ahead to project. predictive_threshold (default: 60) sets the breaking score ceiling used for projection. Both are configurable per project in policy settings.
PASS lane
When no changes are detected at all, the gate returns PASS with zero findings. This is distinct from INFO (which has additive changes).
How the decision works
The gate engine classifies all changes, then applies the highest-severity lane present:
- If any ERR lane item exists → block. No threshold applies.
- If any WARN lane item exists and count meets threshold → block.
- If any WARN lane item exists below threshold → proceed.
- If only INFO lane items exist → proceed.
- If no items at all → proceed (PASS).
After the lane decision, PREDICTIVE_WARN is evaluated as an overlay. If drift velocity projects a threshold crossing and the lane is not ERR, a PREDICTIVE_WARN finding is appended to the findings array. The action (block or proceed) is not changed.
Gate reasons
These are the exact reason strings emitted by the gate engine on the sync-run record.
| Action | Lane | Reason |
|---|---|---|
| block | ERR | BLOCK -- breaking removal detected |
| block | WARN | BLOCK -- degradation above threshold |
| proceed | WARN | WARN -- degradation below threshold |
| proceed | INFO | PASS -- additive change only |
| proceed | PASS | PASS -- no changes detected |
| proceed* | PREDICTIVE_WARN | ... | PREDICTIVE_WARN -- threshold crossing projected in N commit(s) |
* PREDICTIVE_WARN appends to the reason of whatever lane was already present. It does not change the action.
Thresholds
| Parameter | Default | Description |
|---|---|---|
warn_count_threshold | 0 | WARN blocks when warning count >= this value. Default 0 means WARN never blocks. Set to a positive integer per project to block above that many warnings. |
predictive_horizon | 3 | Commits ahead to project for PREDICTIVE_WARN. |
predictive_threshold | 60 | Breaking score ceiling for drift velocity projection. |
All thresholds are configurable per project in the project policy settings.
Gate score calculation
The composite breaking score is the sum of individual scores from all active findings. Each change kind has a fixed score weight:
endpoint_removed: 40 pointsauth_changed: 35 pointsfield_removed,oneOf_anyOf_modified: 30 pointstype_changed,enum_value_removed: 25 pointsrequired_added: 20 pointsfield_renamed: 15 points- INFO items: 0 points
The composite score is stored on the sync run for analytics and display but does not drive the gate decision. The decision is purely lane-based: ERR always blocks, WARN checks count against threshold, INFO always passes.
Example: A push removes one endpoint (40 points) and adds a required parameter (20 points). The composite score is 60. But the gate decision is block because an ERR item is present -- the score is irrelevant to the decision.
GitHub Action integration
The gate result is returned to the GitHub Action as structured output:
| Output | Description |
|---|---|
gate-action | proceed or block |
gate-lane | ERR, WARN, INFO, or PASS |
gate-threshold-applied | true if WARN threshold was evaluated |
predictive-warn | true if drift velocity projects threshold crossing |
sync-run-id | UUID of the sync run created |
The PR comment displays the lane badge prominently: [ERR], [WARN], [INFO], or [PASS], followed by the decision reason and a line-item breakdown of findings. If PREDICTIVE_WARN is active, a yellow advisory note appears below the gate decision.
Handling a blocked push
When the gate blocks a push, you have three options:
- Fix the breaking change -- modify the spec to avoid the breaking operation.
- Create a drift exception -- in project settings, add an exception for the specific endpoint+operation. Exceptions have a maximum 90-day TTL and require a named approver.
- Adjust thresholds -- raise the
warn_count_thresholdif your team accepts higher risk for WARN lane items. ERR lane items always block regardless of thresholds.
Reviewed Exceptions
Drift exceptions suppress specific breaking operations from the gate evaluation. Each exception is scoped to a project, endpoint, and change kind. Exceptions expire automatically (max 90 days). When an exception expires, the item re-activates and the gate may block again on the next push.
Exception fields:
- Endpoint: the specific endpoint the exception applies to (e.g.,
GET /users) - Kind: the change kind being suppressed (e.g.,
endpoint_removed) - Approver: the person who approved the exception (required, cannot be blank)
- Expires: automatic expiration date (maximum 90 days from creation)
Exceptions do not disable the gate. They suppress specific findings. If a push triggers both an excepted finding and a new, non-excepted ERR finding, the gate still blocks. Only the excepted item is removed from the evaluation.
Outage policy
If StateAnchor is unavailable (network error, service outage, timeout), the GitHub Action behavior is controlled by the outage-policy input. The default is fail-closed: the Action exits non-zero and the push is blocked. Set outage-policy: fail-open in your workflow to allow pushes to proceed during outages (the Action returns gate-action: allow with a soft-pass result).
Full methodology
The complete gate engine methodology -- corpus construction, change classification rationale, ICE validation, Council Mode scoring, verdict decomposition, and the RFC 6962 Merkle log -- is published as a standalone reference at docs/engineering/gate-engine-methodology-public.md (approximately 3,400 words, technical reviewer-ready). Every verdict StateAnchor emits ships with a corpus citation pointing at the exact classification rule that produced it.
The classification reference covers the full 16 ERR + 9 WARN + 8 INFO kinds -- see Gate Classification for the full table.
Gate-check vs full sync
StateAnchor has two distinct operations with different credit semantics:
| Operation | Triggered by | Credits | Stops when credits are exhausted? |
|---|---|---|---|
| gate-check | GitHub Action on every push (synchronous, seconds) | Free -- always | No. Gate verdicts never stop. |
| full sync | Webhook push, manual sync button, Trigger.dev pipeline | 1 credit per run | Yes. Artifact generation, SDK rebuilds, and drift scanning stop. |
Gate-check is the synchronous endpoint called by the GitHub Action on every push. It diffs the incoming spec against the previous IR, runs the categorical lane engine, and returns a block-or-proceed verdict within seconds. It is always available regardless of credit balance. StateAnchor never blocks your CI pipeline on billing grounds.
Full sync runs the complete pipeline: Stage A IR generation, Stage B ICE validation, Stage C LLM artifact generation (TypeScript/Python/Go SDK wrappers, MCP server), drift scanning, and Merkle log publication. Each full sync consumes one credit. When credits are exhausted, full syncs are blocked and a Credits exhausted notice appears on the dashboard.
The result: a team that runs out of credits will still see accurate gate verdicts on every push -- their CI stays green or blocked based on actual spec changes. They lose artifact regeneration and drift scan updates until they upgrade.