Deterministic rules

A share of the standard needs no model. A ticket reference is present or it is not — and asking an LLM invites a false positive on a fact, which is the worst kind.

Why split the standard at all

Sending a decidable rule to a model costs tokens and buys uncertainty. It also produces the one kind of finding an author cannot argue with productively: you cannot debate a model about whether the word TODO appears on a line. These rules are evaluated directly instead, in the gate's policy job, with no model call.

What is classified today

RuleSeverityWhat the checker decides
core/untracked-todoHIGHFlag `TODO`/`FIXME`/placeholder code without a ticket reference.
core/type-checker-suppressionBLOCKERNo type-checker or linter suppression without an inline comment AND a ticket reference. Recognised in whichever dialect the stack uses: `@ts-ignore`, `@ts-expect-error`, `@ts-nocheck`, `eslint-disable`, `# type: ignore`, `# pyright: ignore`, `# noqa`, `# pylint: disable`, `@SuppressWarnings("unchecked")`, `@Suppress("UNCHECKED_CAST")`, `// swiftlint:disable`, `//nolint`, `#pragma warning disable`.
core/unsafe-assertionBLOCKERNo unsafe assertions (`as unknown as X`, force casts) used to silence an error. Where one is genuinely required, the justification is a `SAFETY:` comment on the same line or the line above, stating why the assertion holds (`// SAFETY: payload parsed by the schema above`). Any other comment does not count.
javascript/var-in-new-codeHIGH`var` in new code — `const` by default, `let` when reassigned.
javascript/unsafe-numeric-coercionHIGH`parseInt` without radix; `Number()`/`+` coercion of user input without `Number.isFinite`.
typescript/module-mockingHIGHModule mocking in tests (`vi.mock`, `vi.doMock`, `jest.mock`, `jest.doMock`, `unstable_mockModule`). The test replaces the import graph, so it keeps passing when the real module's contract changes or its wiring breaks. Inject the dependency through a real interface and pass a faithful test implementation; `vi.spyOn` on an injected object is fine.
typescript/unknown-returnHIGHAn explicit return type of `unknown`, `Promise<unknown>` or `PromiseLike<unknown>`. Every caller now has to parse or assert the result; parse it once inside the function and return the domain type.
typescript/unknown-type-aliasHIGHA type alias that is `unknown` or a union containing it (`type Payload = unknown`). The name hides the top type, so readers and reviewers treat it as a contract it is not.
typescript/object-parameterHIGHA parameter typed `object`. It accepts arrays, functions and class instances, and allows no property access without an assertion. Accept a named type, or a generic `<T extends object>`.
typescript/reflect-dynamic-accessHIGH`Reflect.apply` or `Reflect.get` in application code. Both take untyped arguments and return `any`, so the call and its result escape type checking. Use a typed call or property access; dynamic dispatch goes behind a named interface.
python/blanket-suppressionHIGHA `# type: ignore`, `# pyright: ignore` or `# noqa` with no error code. It silences every error on the line, including ones introduced later — an `arg-type` bug added next month is hidden by an ignore meant for an untyped import. Name the code (`# type: ignore[import-untyped]`, `# noqa: F401`). In addition to `core/type-checker-suppression`: a ticket does not excuse a missing code.
python/broad-exception-assertionHIGH`pytest.raises(Exception)` / `assertRaises(Exception)` (or `BaseException`) with no `match=`. The test passes on any error — a `TypeError` from a typo included — so it proves nothing about the behaviour it names. Assert the specific type, or add a non-empty `match=`.
python/module-patchingHIGHPatching by dotted import path in tests — `patch("app.billing.client.post")`, `mocker.patch("…")`, `monkeypatch.setattr("pkg.mod.attr", …)`. The test replaces the import graph, so it stays green when the real wiring breaks and goes red on a harmless move. Inject the dependency and pass a faithful test implementation. Not `patch.object` on an injected collaborator, `patch.dict(os.environ)` or `monkeypatch.setenv`.
python/quadratic-accumulationHIGHBuilding a sequence by copying it each step — `sum(lists, [])`, `reduce(lambda a, b: a + b, lists)`, `acc = acc + [x]` or `acc = {acc, k: v}` in a loop. Each step allocates a new container, so the work grows quadratically. Use `itertools.chain.from_iterable`, a comprehension, or mutate a local accumulator.
java/reference-equalityBLOCKER`==`/`!=` on `String`, boxed numbers or other value objects. It compares identity, so it passes in tests on interned literals and cached small boxes and fails in production — `status == "ACTIVE"` is false for a deserialised value, `Long` 128 `==` 128 is false. Use `equals`/`Objects.equals`, or unbox. Not for enums or a deliberate identity check against a sentinel.
java/bigdecimal-value-semanticsBLOCKER`new BigDecimal(double)` or `BigDecimal.equals` for amounts. The double constructor captures binary rounding error (`new BigDecimal(0.1)` is `0.1000000000000000055…`), and `equals` compares scale, so `10.0` is not `10.00`. Use `new BigDecimal("0.1")` or `BigDecimal.valueOf(d)`, and `compareTo(…) == 0`.
java/dropped-exception-causeHIGHA new exception rethrown without the caught one as its cause. `throw new ServiceException(e.getMessage())` cuts the trace at the catch, so a pool timeout and a network fault look identical in the log. Pass the original: `throw new ServiceException("context", e)`. Not when the parameter is named `unused`/`ignored` and the drop is deliberate.
java/print-stack-traceHIGH`e.printStackTrace()` in a catch. It writes to stderr outside the logging pipeline — no level, correlation id or alert — and almost always marks a swallowed failure, so the request returns 200 with partial data. Log through the logger with context, then rethrow or fail the operation.
java/ignored-pure-resultHIGHThe result of a method on an immutable value discarded — `s.trim();`, `s.toLowerCase();`, `bd.add(x);`, `date.plusDays(1);`, `stream.filter(…);`. The call does nothing and the unchanged value is used: `email.toLowerCase();` then `findByEmail(email)` misses `User@X.com`. Assign or return the result.
kotlin/todo-stubBLOCKER`TODO()` / `throw NotImplementedError()` in production code. The IDE's placeholder compiles and ships, and `NotImplementedError` is an `Error`, so `catch (e: Exception)` does not stop it — the first refund request crashes the app or the request thread. Implement it, or throw `UnsupportedOperationException("why")` for a deliberately unsupported operation. Not in test fakes.
kotlin/dropped-exception-causeHIGHA new exception thrown with only `e.message`. `throw RepositoryException(e.message)` loses the original type and trace, so a certificate-expiry outage is diagnosed as an app bug. Pass the cause: `throw RepositoryException("context", e)`.
kotlin/downcast-readonly-collectionHIGHA read-only `List`/`Set`/`Map` cast to `Mutable*` to mutate it. It throws `UnsupportedOperationException` on `listOf`/`emptyList`, or silently mutates a collection the caller thinks is immutable — mutating a `StateFlow` value in place so `distinctUntilChanged` never emits. Copy with `toMutableList()`, or expose a mutable type honestly.
kotlin/print-stack-traceHIGH`e.printStackTrace()` in a catch. It goes to stderr or logcat, past the logger and crash reporting, so a production-wide failure never reaches Crashlytics. Log it with context through the logger or crash reporter, then rethrow or surface the failure.
swift/concurrency-checking-opt-outHIGHConcurrency checking switched off to silence the compiler — `@unchecked Sendable`, `nonisolated(unsafe)`, `@preconcurrency import`, `MainActor.assumeIsolated`. Each removes data-race checking, so `final class Cache: @unchecked Sendable { var items = [Key: Value]() }` corrupts its dictionary under concurrent writes and crashes intermittently. Name the lock, queue or invariant that makes it safe, inline, with a ticket. Not when every mutable member really is guarded — say by what.
swift/unowned-captureHIGH`unowned` in a closure capture list. `[unowned self]` in an escaping closure crashes when the closure runs after its owner is deallocated ("Attempted to read an unowned reference…"), and `unowned(unsafe)` is undefined behaviour. Use `[weak self]` with `guard let self else { return }`. Not in a non-escaping closure or a `lazy var` initialiser.
swift/implicitly-unwrapped-declarationHIGHAn implicitly unwrapped optional outside `@IBOutlet`. `var viewModel: ProfileViewModel!` force-unwraps on every read with no `!` at the use site, so a navigation path that forgets to set it crashes far from the cause. Inject a non-optional through `init`, or use a real optional and handle it. Not for `@IBOutlet` or XCTest fixtures set in `setUp`.
swift/copying-reduce-accumulatorHIGH`reduce` with a copy-on-write accumulator — `reduce([]) { $0 + [$1] }`, `reduce("") { $0 + … }`, `reduce([K: V]())`. Each element copies the whole accumulator, so the loop is quadratic: hex-encoding a 1 MB payload stalls the main thread. Use `reduce(into:)`, `flatMap`, `joined()` or `Dictionary(grouping:)`.
csharp/rethrow-loses-stackHIGH`throw ex;` inside a catch. Rethrowing the variable restarts the stack trace at this method, so the frames between the fault and the catch are gone from the incident log. Use bare `throw;`, or `ExceptionDispatchInfo.Capture(ex).Throw()` outside the handler.
csharp/reserved-exception-typeHIGHThrowing `Exception`, `ApplicationException`, `SystemException` or a runtime-reserved type (`NullReferenceException`, `IndexOutOfRangeException`, …). A caller can only handle it with `catch (Exception)`, which also swallows real bugs — mapping "not found" to 404 that way turns a null dereference into a 404 too. Throw a specific framework or domain exception. Not in test doubles simulating an arbitrary failure.
csharp/culture-implicit-parse-formatHIGHMachine-readable values parsed or formatted with the server's culture — `decimal/double/DateTime.Parse(s)`, `ToString()` written to storage or the wire, `ToLower()` building a key. On a `de_DE` host `decimal.Parse("1.50")` is `150`. Pass `CultureInfo.InvariantCulture` (or an invariant format, `ToLowerInvariant()`); current culture only for text shown to users.
csharp/ef-inmemory-test-doubleHIGHQuery tests against `UseInMemoryDatabase` or a mocked/list-backed `DbSet`. The fake has no transactions, raw SQL, constraints or provider translation, so the suite is green on queries that throw or behave differently in production — Microsoft's own testing guidance discourages it. Test against the real provider (Testcontainers, LocalDB) or stub a repository boundary.
go/error-wrap-verbHIGHAn error formatted with `%v`/`%s` instead of `%w`. `fmt.Errorf("find: %v", err)` flattens the cause to text, so `errors.Is(err, sql.ErrNoRows)` upstream is false and a 404 becomes a 500. Use `%w`. Not when deliberately hiding an internal error from the API contract — say so on the line.
go/missing-client-server-timeoutHIGH`http.Client`/`http.Server` without timeouts (`Timeout`, `ReadTimeout`, `WriteTimeout`) — zero values mean infinite.
terraform/empty-list-equalityHIGHComparing a collection to `[]`. `var.x == []` is always false and `var.x != []` always true — `[]` is an empty tuple and `==` needs identical types — so `count = var.subnets == [] ? 0 : 1` creates the resource whether or not the list is empty. Use `length(var.x) == 0`.
terraform/ignore-changes-allHIGH`lifecycle { ignore_changes = all }`. Terraform stops proposing any update to the resource, so a later change to its arguments merges, plans as "No changes" and is never applied. List only the attributes something outside Terraform genuinely owns, with a comment naming that owner.
react/set-state-in-renderBLOCKERA state setter called during render — unconditionally in the component body, or called instead of passed in a handler prop (`onClick={setOpen(true)}`). It schedules a render from inside render and loops until React throws "Too many re-renders". Derive the value during render, or pass a function (`onClick={() => setOpen(true)}`). Not the documented guarded pattern (`if (items !== prevItems) { setPrevItems(items); … }`).
react/javascript-urlBLOCKERA `javascript:` URL, or an unvalidated user-supplied URL, in `href`/`src`/`action`/`formAction`. Before React 19 a stored `javascript:` profile link runs in the viewer's session; React 19 blocks it and a literal `javascript:void(0)` throws on click. Use `<button type="button" onClick>` for actions and allow-list `http:`/`https:`/relative URLs for user links.
react/async-effect-callbackHIGHAn `async` function passed straight to `useEffect`/`useLayoutEffect`. The effect returns a Promise where React expects a cleanup, so nothing can cancel it: the slower "hell" response lands after "hello" and overwrites its results. Declare the async function inside, call it, and return a cleanup that sets an `ignore` flag or aborts.
react/impure-renderHIGHA non-deterministic value or global write during render — `Math.random()`, `Date.now()`/`new Date()`, `crypto.randomUUID()` read in the body, or a module/`window` variable assigned. Output differs between renders and between server and client; a random `key` remounts the subtree every render, dropping focus and state. Use `useId` or a lazy `useState(() => …)`, and move clock reads and writes into effects or handlers.
react-native/deep-importHIGHAn import from `react-native/Libraries/…` or another private deep path. These modules have no stability contract and move across upgrades, so the bundle stops resolving on every platform. Import the public symbol from `'react-native'`. Not the documented secondary entry points (`react-native/asset-registry`, `setup-env`) or jest preset paths.
react-native/cached-window-dimensionsHIGH`Dimensions.get(...)` captured at module scope or in `StyleSheet.create`. It is read once at import, so after rotation, split-screen or unfolding the layout uses the old size. Use `useWindowDimensions()` in the component.
vue/async-computedHIGHA `computed` with an `async` getter, or returning a Promise chain. Its value is the Promise, so `v-if="allowed"` is always true and the result never re-resolves. Use VueUse `computedAsync`, or a `ref` filled by a watcher with cleanup.
vue/shared-mutable-defaultHIGHAn `Array`/`Object` prop default, or the `data` option, given as a literal instead of a factory. One literal is shared by every instance, so adding a tag in one `<TagList>` shows it in all. Use `default: () => []` and `data() { return {…} }`.
angular/async-lifecycle-hookHIGH`async ngOnInit` / `ngOnDestroy` / another lifecycle hook. Angular never awaits it, so the template renders the unloaded state (`Cannot read properties of undefined`), a rejection floats, and awaited teardown runs after the view is gone. Load through the `async` pipe, a resolver or `resource()`, or call a handled async method from a synchronous hook.
angular/output-native-event-nameHIGHAn `@Output()` / `output()` named after a bubbling DOM event — `click`, `change`, `input`, `submit`. The parent's `(change)` binds both the output and the native event bubbling out of the component, so the handler fires twice, once with an `Event`. Name it for the domain: `itemSelected`, `valueChange`.
angular/banana-out-of-boxHIGHTwo-way binding written `([x])="v"` instead of `[(x)]="v"`. The inverted form parses as an event binding named `[x]`, so the field never syncs from what the user types. Use `[(ngModel)]`.
angular/manual-lifecycle-callHIGH`this.ngOnInit()` or another hook called by hand to "refresh". Each call re-runs every subscription the hook sets up, so polling multiplies per click, and an early `ngOnDestroy` tears down a live view. Extract the shared work into a method. `super.ngOnInit()` is fine.
angular/impure-pipeHIGHA pipe declared `pure: false`. It re-runs on every change-detection cycle for every binding using it — hundreds of calls per keystroke in a 100-row table. Use a pure pipe over immutable inputs, or a `computed` signal.
svelte/async-store-startBLOCKERAn `async` start function for `readable`/`writable`, or an `async` `derived` callback. Svelte calls the start function's return value as the stop function, and a Promise is not callable, so the last unsubscribe throws `stop is not a function`; a one-argument async `derived` holds a Promise as its value. Do the async work in a synchronous callback and `set` on resolve.
svelte/load-in-page-componentHIGH`load` (or Sapper's `preload`) exported from `+page.svelte` / `+layout.svelte`. SvelteKit only calls `load` from `+page(.server).js` and `+layout(.server).js`, so the page renders with `data` undefined and no error. Move it to the sibling file.
svelte/double-brace-mustacheHIGHVue/Angular-style `{{ value }}` in Svelte markup. Svelte reads it as an object literal, so the page shows `Hello [object Object]`. Use single braces: `{value}`.
dom/remove-listener-fresh-functionHIGH`removeEventListener` given an inline arrow, `function` expression or fresh `.bind()` result. That function was never added, so nothing is removed and the listener keeps firing against torn-down state. Keep the bound reference in a field, or pass `{ signal }` from an `AbortController`.
dom/on-property-clobbers-handlerHIGH`window.onload =`, `document.onkeydown =` or another `on*` property assigned on a global target. Assignment replaces the host page's or another script's handler — an embed setting `window.onerror` disables the page's error tracking. Use `addEventListener`.

Everything not listed is a judgement rule and stays with the model. The classification is complete by construction — listed means deterministic, absent means judgement.

The classification lives in the manifest, not the markdown

This is the decision that removes most of the risk from splitting the source of truth. standards/*.md is what a reviewer reads, and deleting a rule from it because a checker also covers it would narrow what the model considers. So the list lives in standards/manifest.json, no rule text changed, and not one rule id moved — every historical telemetry record is keyed on them.

Where a check can only decide part of a rule it decides that part and the model still sees the whole rule. javascript/unsafe-numeric-coercion is checked for a missing parseInt radix; whether a Number() coercion is applied to user input is judgement and stays where judgement belongs.

Only added lines

A checker that read whole files would flag every var in a legacy file the author merely renamed — which the standard's own “what NOT to flag” section forbids. An author who is right to ignore one finding learns to ignore the next one too.

Running it yourself

terminal
$ git diff main...HEAD > change.diff
$ npx redlinegate policy --diff-file change.diff

The floor is BLOCKER, not HIGH, on purpose: a deterministic tier that failed merges over a missing ticket reference on day one would be switched off by week two, and then nothing it decides is enforced at all.

What is deliberately not in the tier

core/hardcoded-secrets. A regex over added lines is how a secret scanner earns a reputation for false positives; the gate already runs a real one against verified secrets, and the model keeps the rule for what a scanner misses — a credential in a comment, for instance.

A rule classified as machine-checked with no implementation is a build failure. That state — everyone believes it is covered, and it is enforced by nobody — is worse than leaving it to the model, which would at least have looked.