{"rules":[{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/user.test.ts","source":"test('accepts a', () => { const result = parse('a'); expect(result.ok).toBe(true); expect(result.value).toBe('a'); });\ntest('accepts b', () => { const result = parse('b'); expect(result.ok).toBe(true); expect(result.value).toBe('b'); });"}],"fixedFiles":[],"focusPath":"src/user.test.ts","id":"copied-sibling-tests","outcome":"reject","title":"Sibling tests repeat the same body"},{"expectedCount":0,"files":[{"path":"src/user.test.ts","source":"test.each(['a', 'b'])('parses %s', (value) => { const x = parse(value); expect(x.ok).toBe(true); expect(x.value).toBe(value); });"}],"fixedFiles":[],"focusPath":"src/user.test.ts","id":"parameterized-cases","outcome":"accept","title":"A case table shares one test body"}],"filePatterns":[],"id":"duplicate-test-body","key":"eslint:duplicate-test-body","languages":["typescript"],"limitations":["The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments."],"messageIds":["duplicateTestBody"],"optionsSchema":null,"rationale":"Copy-pasted test bodies hide the cases that differ and allow equivalent assertions to drift independently.","references":[],"remediation":"Move the varying inputs and expected values into a case table consumed by `test.each(...)` or `it.each(...)`.","since":null,"source":"packages/typescript/src/rules/duplicate-test-body.ts","status":"active","summary":"Disallow substantial sibling tests with the same body shape; express their differing inputs as a parameterized case table.","test":"packages/typescript/tests/rules/duplicate-test-body.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/component.ts","source":"export const x = 1;\nimport { z } from 'zod';"}],"fixedFiles":[],"focusPath":"src/component.ts","id":"import-after-declaration","outcome":"reject","title":"An import follows a module declaration"},{"expectedCount":0,"files":[{"path":"src/component.ts","source":"import { z } from 'zod';\nexport const schema = z.string();"}],"fixedFiles":[],"focusPath":"src/component.ts","id":"imports-first","outcome":"accept","title":"Imports precede module declarations"}],"filePatterns":[],"id":"enforce-file-structure","key":"eslint:enforce-file-structure","languages":["typescript"],"limitations":["The rule skips tests and generated files, treats re-exports as neutral, and does not order body declarations."],"messageIds":["importsFirst","useServerDirective"],"optionsSchema":null,"rationale":"Interleaved imports obscure module dependencies, while a displaced `use server` string is not an active directive.","references":[],"remediation":"Move `use server` to the first statement when present, then place imports before declarations and executable statements.","since":null,"source":"packages/typescript/src/rules/enforce-file-structure.ts","status":"active","summary":"Require imports before body statements and require `use server` to be the first statement.","test":"packages/typescript/tests/rules/enforce-file-structure.test.ts"},{"aliases":["no-async-callback-in-waitfor"],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/component.test.ts","source":"it('fails', async () => { await waitFor(async () => expect(foo).toBe(true)); });"}],"fixedFiles":[],"focusPath":"src/component.test.ts","id":"async-wait-for-callback","outcome":"reject","title":"waitFor receives an async callback"},{"expectedCount":0,"files":[{"path":"src/component.test.ts","source":"it('works', async () => { await waitFor(() => expect(foo).toBe(true)); });"}],"fixedFiles":[],"focusPath":"src/component.test.ts","id":"synchronous-wait-for-callback","outcome":"accept","title":"waitFor retries a synchronous assertion"}],"filePatterns":[],"id":"no-async-callback-in-wait-for","key":"eslint:no-async-callback-in-wait-for","languages":["typescript"],"limitations":["The rule checks inline first-argument callbacks to bare or non-computed `.waitFor` calls in test files."],"messageIds":["noAsyncCallbackInWaitFor"],"optionsSchema":null,"rationale":"`waitFor` retries synchronous assertions; an async callback changes that contract and can hide a rejected assertion promise.","references":[],"remediation":"Remove `async` and keep the assertions inside `waitFor` synchronous.","since":null,"source":"packages/typescript/src/rules/no-async-callback-in-wait-for.ts","status":"active","summary":"Disallow async callbacks in `waitFor` to prevent swallowed promise rejections.","test":"packages/typescript/tests/rules/no-async-callback-in-wait-for.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/users.tsx","source":"import { useEffect } from 'react'; useEffect(() => { console.log('mounted'); }, []);"}],"fixedFiles":[],"focusPath":"src/users.tsx","id":"effect-without-fetch","outcome":"accept","title":"An effect performs no data request"},{"expectedCount":1,"files":[{"path":"src/users.tsx","source":"useEffect(() => { fetch('/api/users'); }, []);"}],"fixedFiles":[],"focusPath":"src/users.tsx","id":"fetch-inside-effect","outcome":"reject","title":"An effect starts a data request"}],"filePatterns":[],"id":"no-client-side-data-fetching","key":"eslint:no-client-side-data-fetching","languages":["typescript"],"limitations":["The rule recognizes common fetch clients syntactically and exempts analytics endpoints and non-GET `fetch` calls."],"messageIds":["noClientFetch"],"optionsSchema":null,"rationale":"Effect-driven reads begin after rendering and can create request waterfalls, duplicate fetches, and loading-state layout shifts.","references":[],"remediation":"Fetch in a React Server Component or Server Action, or use a client cache such as SWR or React Query.","since":null,"source":"packages/typescript/src/rules/no-client-side-data-fetching.ts","status":"active","summary":"Disallow direct data fetching inside `useEffect` or `useLayoutEffect`.","test":"packages/typescript/tests/rules/no-client-side-data-fetching.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/retry.ts","source":"// retry because the upstream API is flaky\nconst x = retry();"}],"fixedFiles":[],"focusPath":"src/retry.ts","id":"rationale-comment","outcome":"accept","title":"A comment explains why retry is required"},{"expectedCount":1,"files":[{"path":"src/helpers.ts","source":"const x = 1;\n// region helpers\nconst y = 2;"}],"fixedFiles":[],"focusPath":"src/helpers.ts","id":"region-banner","outcome":"reject","title":"A region comment decorates a code boundary"}],"filePatterns":[],"id":"no-comment-cruft","key":"eslint:no-comment-cruft","languages":["typescript"],"limitations":["The rule skips generated files and conservatively preserves prose, issue references, licenses, examples, and tool directives."],"messageIds":["commentWall","commentedOutCode","fileHeaderPreamble","placeholderImplementation","redundantNarration","sectionBanner","untrackedTodo"],"optionsSchema":null,"rationale":"Decorative, narrated, or dead-code comments obscure the constraints and rationale that comments should preserve.","references":[],"remediation":"Delete dead code and narration; express boundaries with named code and retain only comments that explain constraints or intent.","since":null,"source":"packages/typescript/src/rules/no-comment-cruft.ts","status":"active","summary":"Flag commented-out code, section-banner comments, and leading file-header comment preambles.","test":"packages/typescript/tests/rules/no-comment-cruft.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/component.test.ts","source":"it('fails with if', () => { if (ready) { expect(value).toBe(1); } });"}],"fixedFiles":[],"focusPath":"src/component.test.ts","id":"conditional-assertion","outcome":"reject","title":"A branch can skip the assertion"},{"expectedCount":0,"files":[{"path":"src/component.test.ts","source":"it('works', () => { expect(1).toBe(1); });"}],"fixedFiles":[],"focusPath":"src/component.test.ts","id":"unconditional-assertion","outcome":"accept","title":"A test always executes its assertion"}],"filePatterns":[],"id":"no-conditional-in-test","key":"eslint:no-conditional-in-test","languages":["typescript"],"limitations":["The rule exempts lifecycle hooks, nested helpers, and narrow guards whose outcome is pinned by a preceding assertion."],"messageIds":["noConditionalInTest"],"optionsSchema":null,"rationale":"A branch can skip the assertion that gives a test its meaning, allowing unexpected inputs to pass silently.","references":[],"remediation":"Split each path into a separate test or use a parameterized case table with unconditional assertions.","since":null,"source":"packages/typescript/src/rules/no-conditional-in-test.ts","status":"active","summary":"Disallow test conditionals that can skip a runtime assertion or exit the test before one runs.","test":"packages/typescript/tests/rules/no-conditional-in-test.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/server.ts","source":"app.use(cors({ origin: 'https://app.example.com', credentials: true }));"}],"fixedFiles":[],"focusPath":"src/server.ts","id":"trusted-origin-with-credentials","outcome":"accept","title":"Credentials are limited to a trusted origin"},{"expectedCount":1,"files":[{"path":"src/server.ts","source":"app.use(cors({ origin: '*', credentials: true }));"}],"fixedFiles":[],"focusPath":"src/server.ts","id":"wildcard-origin-with-credentials","outcome":"reject","title":"Credentials are enabled for every origin"}],"filePatterns":[],"id":"no-cors-wildcard-with-credentials","key":"eslint:no-cors-wildcard-with-credentials","languages":["typescript"],"limitations":["The rule detects literal CORS option and header combinations within the same syntactic scope; it does not resolve runtime configuration."],"messageIds":["corsWildcardWithCredentials"],"optionsSchema":null,"rationale":"Reflecting every origin while allowing credentials can let an untrusted site read authenticated cross-origin responses.","references":[],"remediation":"Enumerate the trusted origins that may receive credentialed responses.","since":null,"source":"packages/typescript/src/rules/no-cors-wildcard-with-credentials.ts","status":"active","summary":"Disallow wildcard CORS origins when credentials are enabled.","test":"packages/typescript/tests/rules/no-cors-wildcard-with-credentials.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/status.ts","source":"enum Status {\n  /** The pending status. */\n  Pending = 'pending',\n  /** The finished status. */\n  Finished = 'finished',\n  /** The failed status. */\n  Failed = 'failed',\n}"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"restated-enum-members","outcome":"reject","title":"Do not restate every enum member"},{"expectedCount":0,"files":[{"path":"src/status.ts","source":"enum Status { Pending = 'pending', Done = 'done', Failed = 'failed' }"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"uncommented-members","outcome":"accept","title":"Let clear member names stand alone"}],"filePatterns":[],"id":"no-declaration-comment-wall","key":"eslint:no-declaration-comment-wall","languages":["typescript"],"limitations":["Only enum and class bodies meeting the configured comment-count and restatement-ratio thresholds are reported."],"messageIds":["commentWall"],"optionsSchema":{"additionalProperties":false,"properties":{"maxNovelWords":{"description":"Most content words a comment may add beyond its member's own source and still count as a restatement.","minimum":0,"type":"integer"},"minCommentedMembers":{"description":"Fewest commented members that can count as a wall.","minimum":2,"type":"integer"},"minCommentedRatio":{"description":"Least share of the members that must be commented; below it the comments are group labels.","maximum":1,"minimum":0,"type":"number"},"minRestatedRatio":{"description":"Least share of the member comments that must be restatements.","maximum":1,"minimum":0,"type":"number"}},"type":"object"},"rationale":"A dense block of repetitive member comments obscures the few comments that add information and drifts with renamed members.","references":[],"remediation":"Delete comments that restate member names and retain comments that explain constraints, lifecycle, or behavior.","since":null,"source":"packages/typescript/src/rules/no-declaration-comment-wall.ts","status":"active","summary":"Flag an enum body or class body whose member comments mostly re-spell the members' own names.","test":"packages/typescript/tests/rules/no-declaration-comment-wall.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/users.ts","source":"db.prepare('select * from users where id = ?').bind(userId);"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"bound-sql-parameter","outcome":"accept","title":"A runtime value is bound separately"},{"expectedCount":1,"files":[{"path":"src/users.ts","source":"db.prepare(`select * from users where id = '${userId}'`);"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"interpolated-sql-value","outcome":"reject","title":"A runtime value is interpolated into SQL"}],"filePatterns":[],"id":"no-dynamic-sql","key":"eslint:no-dynamic-sql","languages":["typescript"],"limitations":["The rule recognizes SQL by syntax and configured method names; static fragments and parameterizing tagged templates are exempt."],"messageIds":["dynamicSql"],"optionsSchema":{"additionalProperties":false,"properties":{"methods":{"description":"Statement-taking method names to inspect. Replaces the defaults.","items":{"type":"string"},"type":"array"}},"type":"object"},"rationale":"Embedding runtime values in SQL bypasses driver parameterization and can introduce injection defects or unstable query plans.","references":[],"remediation":"Use SQL placeholders and pass runtime values through the driver's binding API.","since":null,"source":"packages/typescript/src/rules/no-dynamic-sql.ts","status":"active","summary":"Disallow runtime interpolation or concatenation in SQL passed to statement-execution methods.","test":"packages/typescript/tests/rules/no-dynamic-sql.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/status.ts","source":"enum Status { Active, Inactive }"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"numeric-enum","outcome":"reject","title":"A numeric enum emits a mutable runtime object"},{"expectedCount":0,"files":[{"path":"src/status.ts","source":"type Status = \"active\" | \"inactive\";"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"string-literal-union","outcome":"accept","title":"A string-literal union has no emitted runtime enum"}],"filePatterns":[],"id":"no-enum","key":"eslint:no-enum","languages":["typescript"],"limitations":[],"messageIds":["noEnum"],"optionsSchema":{"additionalProperties":false,"properties":{"ignoreFiles":{"description":"Additional generated-file globs to ignore; shared generated paths and header markers are always ignored.","items":{"type":"string"},"type":"array"}},"type":"object"},"rationale":"TypeScript enums emit runtime objects and numeric enums accept values outside their declared members, adding behavior where a type-only model is sufficient.","references":[],"remediation":"Replace the enum with a string-literal union or an `as const` object and derive its value type from that object.","since":null,"source":"packages/typescript/src/rules/no-enum.ts","status":"active","summary":"Disallow TypeScript `enum`; use string-literal unions or `as const` objects instead.","test":"packages/typescript/tests/rules/no-enum.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/load.ts","source":"function f() { try { const a = one(); const b = two(); const c = three(); const d = four(); } catch (error) { handle(error); } finish(); }"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"broad-try-block","outcome":"reject","title":"A try block contains four throwing operations"},{"expectedCount":0,"files":[{"path":"src/load.ts","source":"function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } finish(); }"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"focused-try-block","outcome":"accept","title":"A try block contains three throwing operations"}],"filePatterns":[],"id":"no-fat-try-blocks","key":"eslint:no-fat-try-blocks","languages":["typescript"],"limitations":["The rule uses syntax to identify throwing operations and exempts generated files, finally blocks, rethrows, and terminal error boundaries."],"messageIds":["fatTryBlock"],"optionsSchema":null,"rationale":"A broad `try` block obscures which operation failed and encourages one catch clause to recover from unrelated errors.","references":[],"remediation":"Keep only the operations that share one recovery policy inside the `try` block and move other work outside it.","since":null,"source":"packages/typescript/src/rules/no-fat-try-blocks.ts","status":"active","summary":"Disallow `try` blocks containing more than three top-level operations that can throw.","test":"packages/typescript/tests/rules/no-fat-try-blocks.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/utils.ts","source":"export function parseOrder() { return {}; }"}],"fixedFiles":[],"focusPath":"src/utils.ts","id":"generic-module-name","outcome":"reject","title":"Do not hide one export in a generic module"},{"expectedCount":0,"files":[{"path":"src/order-parser.ts","source":"export function parseOrder() { return {}; }"}],"fixedFiles":[],"focusPath":"src/order-parser.ts","id":"responsibility-named-module","outcome":"accept","title":"Name the module after its export"}],"filePatterns":[],"id":"no-generic-single-export-module","key":"eslint:no-generic-single-export-module","languages":["typescript"],"limitations":["Only configured generic stems with exactly one public runtime export are reported."],"messageIds":["genericSingleExport"],"optionsSchema":null,"rationale":"A generic filename hides the sole exported responsibility and makes navigation less descriptive.","references":[],"remediation":"Rename the module after its single runtime export.","since":null,"source":"packages/typescript/src/rules/no-generic-single-export-module.ts","status":"active","summary":"Disallow generic module stems when one runtime export already names the responsibility.","test":"packages/typescript/tests/rules/no-generic-single-export-module.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/lib/queue.ts","source":"import { setTimeout as sleep } from \"node:timers/promises\";\nawait sleep(500, undefined, { signal });"}],"fixedFiles":[],"focusPath":"src/lib/queue.ts","id":"cancellable-node-timer","outcome":"accept","title":"A standard-library timer accepts an abort signal"},{"expectedCount":1,"files":[{"path":"src/lib/queue.ts","source":"await new Promise((resolve) => setTimeout(resolve, 500));"}],"fixedFiles":[],"focusPath":"src/lib/queue.ts","id":"uncancellable-sleep","outcome":"reject","title":"A Promise wraps a timer without cancellation"}],"filePatterns":[],"id":"no-hand-rolled-sleep","key":"eslint:no-hand-rolled-sleep","languages":["typescript"],"limitations":["The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions."],"messageIds":["handRolledSleep","handRolledTimeoutRace"],"optionsSchema":{"additionalProperties":false,"properties":{"allowIn":{"description":"Glob patterns for modules exempt from the rule (e.g. a single sanctioned `sleep` utility). Matched against the ABSOLUTE file path, so anchor with a `**/` prefix (e.g. `**/lib/sleep.ts`).","items":{"type":"string"},"type":"array"},"checkClientModules":{"description":"Also report the sleep form in browser/React Native modules. Off by default: those bundles cannot import `node:timers/promises` and the web platform has no equivalent, so the fix is impossible to follow. Turn on only where every file can resolve `node:` builtins.","type":"boolean"}},"type":"object"},"rationale":"A timer that outlives an aborted operation or a lost promise race retains work and can keep the process alive until it fires.","references":[],"remediation":"Use `node:timers/promises` with an abort signal for delays, or pass `AbortSignal.timeout(...)` to the timed operation.","since":null,"source":"packages/typescript/src/rules/no-hand-rolled-sleep.ts","status":"active","summary":"Disallow uncancellable promisified timers and timeout arms.","test":"packages/typescript/tests/rules/no-hand-rolled-sleep.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/loading-state.tsx","source":"<div className=\"size-4 animate-spin rounded-full border-2 border-t-transparent\" />"}],"fixedFiles":[],"focusPath":"src/loading-state.tsx","id":"border-ring-spinner","outcome":"reject","title":"Do not rebuild a spinner"},{"expectedCount":0,"files":[{"path":"src/loading-state.tsx","source":"<Spinner className=\"size-4\" />"}],"fixedFiles":[],"focusPath":"src/loading-state.tsx","id":"design-system-spinner","outcome":"accept","title":"Use the shared spinner"}],"filePatterns":[],"id":"no-hand-rolled-spinner","key":"eslint:no-hand-rolled-spinner","languages":["typescript"],"limitations":["Only static className values on div and span elements are inspected."],"messageIds":["handRolledSpinner"],"optionsSchema":null,"rationale":"One-off loading indicators duplicate a shared primitive and let accessibility and styling diverge.","references":[],"remediation":"Render the design-system Spinner component instead.","since":null,"source":"packages/typescript/src/rules/no-hand-rolled-spinner.ts","status":"active","summary":"Disallow intrinsic elements styled as Tailwind border-ring spinners outside the design-system implementation.","test":"packages/typescript/tests/rules/no-hand-rolled-spinner.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/schema.ts","source":"import { z } from \"zod\"; const S = z.number().gte(3).lte(3);"}],"fixedFiles":[],"focusPath":"src/schema.ts","id":"compatible-number-bounds","outcome":"accept","title":"Allow a number admitted by both bounds"},{"expectedCount":1,"files":[{"path":"src/schema.ts","source":"import { z } from \"zod\"; const S = z.number().min(5).max(4);"}],"fixedFiles":[],"focusPath":"src/schema.ts","id":"contradictory-number-bounds","outcome":"reject","title":"Reject an empty numeric interval"}],"filePatterns":[],"id":"no-impossible-zod-literal-bounds","key":"eslint:no-impossible-zod-literal-bounds","languages":["typescript"],"limitations":["Only finite numeric literals in a single number, string, or array schema chain are compared.","Chains with dynamic bounds, non-bound validators, transforms, pipes, or preprocessors are skipped.","Test and generated files are excluded."],"messageIds":["impossibleBounds"],"optionsSchema":null,"rationale":"A schema with contradictory literal bounds rejects every input, turning validation into an unreachable contract that usually reflects a typo.","references":[],"remediation":"Choose compatible lower and upper bounds, or remove the constraint that does not express the intended domain.","since":null,"source":"packages/typescript/src/rules/no-impossible-zod-literal-bounds.ts","status":"active","summary":"Disallow same-chain literal Zod bounds whose accepted set is mathematically empty.","test":"packages/typescript/tests/rules/no-impossible-zod-literal-bounds.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/session.ts","source":"const sessionToken = crypto.randomUUID();"}],"fixedFiles":[],"focusPath":"src/session.ts","id":"cryptographic-id","outcome":"accept","title":"Use the Web Crypto API"},{"expectedCount":1,"files":[{"path":"src/session.ts","source":"const sessionToken = Math.random();"}],"fixedFiles":[],"focusPath":"src/session.ts","id":"predictable-token","outcome":"reject","title":"Do not derive a token from Math.random"}],"filePatterns":[],"id":"no-insecure-random-id","key":"eslint:no-insecure-random-id","languages":["typescript"],"limitations":["Ambiguous identifiers and test files are excluded to avoid flagging sampling and fixture data."],"messageIds":["insecureRandomId"],"optionsSchema":null,"rationale":"Math.random is predictable and lacks the entropy required for security-sensitive values.","references":[],"remediation":"Generate the value with crypto.randomUUID or crypto.getRandomValues.","since":null,"source":"packages/typescript/src/rules/no-insecure-random-id.ts","status":"active","summary":"Disallow using `Math.random()` to generate identifiers, tokens, or secrets; use `crypto.randomUUID()` or `crypto.getRandomValues(...)` instead.","test":"packages/typescript/tests/rules/no-insecure-random-id.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/report.ts","source":"try { f(); } catch (err) { JSON.stringify({ error: err.message }); }"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"explicit-error-message","outcome":"accept","title":"Serialize an enumerable error field"},{"expectedCount":1,"files":[{"path":"src/report.ts","source":"try { f(); } catch (err) { JSON.stringify({ error: err }); }"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"stringified-error","outcome":"reject","title":"Do not stringify an Error object"}],"filePatterns":[],"id":"no-json-stringify-error","key":"eslint:no-json-stringify-error","languages":["typescript"],"limitations":["The rule uses local syntax and naming evidence rather than type information."],"messageIds":["noJsonStringifyError"],"optionsSchema":null,"rationale":"Native Error details are non-enumerable, so generic JSON serialization discards diagnostic information.","references":[],"remediation":"Serialize explicit error fields or use an error-aware serializer.","since":null,"source":"packages/typescript/src/rules/no-json-stringify-error.ts","status":"active","summary":"Disallow `JSON.stringify` on an Error value; it yields `{}` because `message`/`stack` are non-enumerable.","test":"packages/typescript/tests/rules/no-json-stringify-error.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/task.ts","source":"try { run(); } catch (error) { console.error(error); }"}],"fixedFiles":[],"focusPath":"src/task.ts","id":"log-and-swallow","outcome":"reject","title":"Do not only log a failure"},{"expectedCount":0,"files":[{"path":"src/task.ts","source":"try { run(); } catch (error) { console.error(error); throw error; }"}],"fixedFiles":[],"focusPath":"src/task.ts","id":"rethrow-after-log","outcome":"accept","title":"Preserve failure after logging"}],"filePatterns":[],"id":"no-log-only-catch","key":"eslint:no-log-only-catch","languages":["typescript"],"limitations":["Documented intentional ignores, tests, and catches with observable recovery are excluded."],"messageIds":["emptyCatch","noLogOnlyCatch"],"optionsSchema":{"additionalProperties":false,"properties":{"logFunctions":{"items":{"type":"string"},"type":"array"},"loggerNames":{"items":{"type":"string"},"type":"array"}},"type":"object"},"rationale":"Swallowing an exception after logging lets execution continue as if the operation succeeded.","references":[],"remediation":"Rethrow the error, return an explicit fallback, or perform concrete recovery.","since":null,"source":"packages/typescript/src/rules/no-log-only-catch.ts","status":"active","summary":"Disallow `catch` clauses that only log (or silently do nothing) and then swallow the error; rethrow or handle it instead.","test":"packages/typescript/tests/rules/no-log-only-catch.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/cache.ts","source":"// The cache is process local.\nconst cache = new Map();"}],"fixedFiles":[],"focusPath":"src/cache.ts","id":"local-fact","outcome":"accept","title":"Keep a concise local fact"},{"expectedCount":1,"files":[{"path":"src/chart.ts","source":"/** One. Two. Three. Four. Five. Six. Seven. Eight. */\nconst chart = createChart();"}],"fixedFiles":[],"focusPath":"src/chart.ts","id":"prose-wall","outcome":"reject","title":"Avoid an unstructured prose wall"}],"filePatterns":[],"id":"no-long-comment","key":"eslint:no-long-comment","languages":["typescript"],"limitations":["Only JSDoc blocks are inspected; structured API docs, tests, scripts, generated files, and versioned dependencies are excluded."],"messageIds":["tooLong"],"optionsSchema":null,"rationale":"Large narrative comments become stale and obscure the local facts that belong beside the code.","references":[],"remediation":"Keep only durable local constraints and express the remaining behavior in code.","since":null,"source":"packages/typescript/src/rules/no-long-comment.ts","status":"active","summary":"Flag unusually large unstructured JSDoc blocks in implementation code.","test":"packages/typescript/tests/rules/no-long-comment.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/runs.ts","source":"db.prepare(`SELECT id FROM runs WHERE id > ? ORDER BY id LIMIT ?`).all();"}],"fixedFiles":[],"focusPath":"src/runs.ts","id":"keyset-pagination","outcome":"accept","title":"Page from a stable cursor"},{"expectedCount":1,"files":[{"path":"src/runs.ts","source":"db.query(`SELECT id FROM runs ORDER BY id LIMIT ? OFFSET ?`);"}],"fixedFiles":[],"focusPath":"src/runs.ts","id":"offset-pagination","outcome":"reject","title":"Do not page by offset"}],"filePatterns":[],"id":"no-offset-pagination","key":"eslint:no-offset-pagination","languages":["typescript"],"limitations":["Only embedded SQL is inspected; test files and non-pagination OFFSET syntax are excluded."],"messageIds":["noOffsetPagination"],"optionsSchema":null,"rationale":"Offset pagination scans skipped rows and shifts page boundaries under concurrent writes.","references":[],"remediation":"Page with a stable ordered key and a cursor predicate.","since":null,"source":"packages/typescript/src/rules/no-offset-pagination.ts","status":"active","summary":"Disallow OFFSET pagination in embedded SQL; it is O(N) per page and drops or repeats rows under concurrent writes. Use a keyset cursor.","test":"packages/typescript/tests/rules/no-offset-pagination.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/download.ts","source":"export function download(): { body: string; status: number } { return impl(); }"}],"fixedFiles":[],"focusPath":"src/download.ts","id":"named-object-return","outcome":"accept","title":"Return named fields"},{"expectedCount":1,"files":[{"path":"src/download.ts","source":"export function download(): [string, number] { return impl(); }"}],"fixedFiles":[],"focusPath":"src/download.ts","id":"tuple-return","outcome":"reject","title":"Do not expose positional fields"}],"filePatterns":[],"id":"no-positional-tuple-return","key":"eslint:no-positional-tuple-return","languages":["typescript"],"limitations":["Only declared multi-field tuple returns on public TypeScript surfaces are inspected."],"messageIds":["noPositionalTupleReturn"],"optionsSchema":null,"rationale":"Public tuple fields are identified only by position, so reordering can preserve types while changing meaning.","references":[],"remediation":"Return an object whose property names describe each value.","since":null,"source":"packages/typescript/src/rules/no-positional-tuple-return.ts","status":"active","summary":"Disallow returning a multi-field tuple from an exported function; return a named object so call sites cannot mismatch slots.","test":"packages/typescript/tests/rules/no-positional-tuple-return.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/database.ts","source":"const url = process.env.DATABASE_URL;"}],"fixedFiles":[],"focusPath":"src/database.ts","id":"raw-environment-read","outcome":"reject","title":"Do not read raw configuration"},{"expectedCount":0,"files":[{"path":"src/database.ts","source":"import { env } from './env.js'; const url = env.DATABASE_URL;"}],"fixedFiles":[],"focusPath":"src/database.ts","id":"validated-environment","outcome":"accept","title":"Read validated configuration"}],"filePatterns":[],"id":"no-raw-env","key":"eslint:no-raw-env","languages":["typescript"],"limitations":["Host markers, assignment targets, tests, scripts, build config, and validated boundaries are excluded."],"messageIds":["noRawEnv"],"optionsSchema":null,"rationale":"Raw environment reads are untyped and defer invalid configuration failures until use.","references":[],"remediation":"Validate environment values at startup and import the typed configuration object.","since":null,"source":"packages/typescript/src/rules/no-raw-env.ts","status":"active","summary":"Disallow direct `process.env` and `import.meta.env` reads outside validated boundaries.","test":"packages/typescript/tests/rules/no-raw-env.test.ts"},{"aliases":[],"autofix":"none","category":"architecture","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/routes/handler.ts","source":"const response = await billingClient.getInvoice(id);"}],"fixedFiles":[],"focusPath":"src/routes/handler.ts","id":"client-call","outcome":"accept","title":"Use a client abstraction"},{"expectedCount":1,"files":[{"path":"src/routes/handler.ts","source":"const response = await fetch('/api/invoices');"}],"fixedFiles":[],"focusPath":"src/routes/handler.ts","id":"raw-fetch","outcome":"reject","title":"Do not call global fetch here"}],"filePatterns":[],"id":"no-raw-fetch-outside-clients","key":"eslint:no-raw-fetch-outside-clients","languages":["typescript"],"limitations":["Tests, client-layer paths, constructed handoffs, and pre-signed URL transfers are excluded."],"messageIds":["rawFetch"],"optionsSchema":{"additionalProperties":false,"properties":{"allow":{"description":"Regular-expression sources matched against the filename. Replaces the defaults.","items":{"type":"string"},"type":"array"}},"type":"object"},"rationale":"Scattered fetch calls bypass shared transport policy and are harder to stub and observe consistently.","references":[],"remediation":"Move the request into a client module and call that abstraction from application code.","since":null,"source":"packages/typescript/src/rules/no-raw-fetch-outside-clients.ts","status":"active","summary":"Disallow calling the global `fetch` outside the client layer; route outbound HTTP through a client module that owns retry, timeout and status handling.","test":"packages/typescript/tests/rules/no-raw-fetch-outside-clients.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/queries.ts","source":"function one() { return 'SELECT id, status, created_at FROM candidates'; }\nfunction two() { return 'SELECT id, status, created_at FROM candidates'; }"}],"fixedFiles":[],"focusPath":"src/queries.ts","id":"repeated-query","outcome":"reject","title":"Do not copy a structured value across functions"},{"expectedCount":0,"files":[{"path":"src/queries.ts","source":"const QUERY = 'SELECT id, status, created_at FROM candidates';\nfunction one() { return QUERY; }\nfunction two() { return QUERY; }"}],"fixedFiles":[],"focusPath":"src/queries.ts","id":"shared-constant","outcome":"accept","title":"Share one structured value"}],"filePatterns":[],"id":"no-repeated-string-literal","key":"eslint:no-repeated-string-literal","languages":["typescript"],"limitations":["Test files, short strings, prose, substitutions, module sources, JSX attributes, and repetition within one function are excluded."],"messageIds":["noRepeatedStringLiteral"],"optionsSchema":null,"rationale":"Independent copies of a query, route template, or identifier can diverge and silently change behavior.","references":[],"remediation":"Extract the repeated value to one module-level constant and reference it from each function.","since":null,"source":"packages/typescript/src/rules/no-repeated-string-literal.ts","status":"active","summary":"Disallow a long structured string literal repeated across functions; the copies drift when one is edited. Extract a module-level constant.","test":"packages/typescript/tests/rules/no-repeated-string-literal.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/cache.ts","source":"// Serialize because the cache key is stable across deploys.\nconst key = serialize(input);"}],"fixedFiles":[],"focusPath":"src/cache.ts","id":"reason-comment","outcome":"accept","title":"Keep the reason the code cannot express"},{"expectedCount":1,"files":[{"path":"src/cache.ts","source":"// Serialize key\nconst key = serialize(input);"}],"fixedFiles":[],"focusPath":"src/cache.ts","id":"restated-comment","outcome":"reject","title":"Remove a comment that repeats the statement"}],"filePatterns":[],"id":"no-restated-comment","key":"eslint:no-restated-comment","languages":["typescript"],"limitations":["Directives, protected references, questions, multi-line prose, comments with novel content, and generated files are excluded."],"messageIds":["restatesLineBelow"],"optionsSchema":null,"rationale":"A comment that only repeats code adds no context and can become stale independently.","references":[],"remediation":"Delete the comment or replace it with the reason, constraint, or consequence absent from the code.","since":null,"source":"packages/typescript/src/rules/no-restated-comment.ts","status":"active","summary":"Flag a single-line comment whose every word already appears on the statement below it.","test":"packages/typescript/tests/rules/no-restated-comment.test.ts"},{"aliases":["jsdoc-restates-signature"],"autofix":"suggestion","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/users.ts","source":"/** Get the user while bypassing the read replica. */\nexport function getUser(id: string) { return id; }"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"behavioral-jsdoc","outcome":"accept","title":"Document behavior absent from the signature"},{"expectedCount":1,"files":[{"path":"src/users.ts","source":"/** Get the user by id. */\nexport function getUserById(id: string) { return id; }"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"signature-jsdoc","outcome":"reject","title":"Remove JSDoc that only repeats the signature"}],"filePatterns":[],"id":"no-restated-jsdoc","key":"eslint:no-restated-jsdoc","languages":["typescript"],"limitations":["Generated files, detached blocks, unknown tags, empty blocks, and JSDoc with information absent from the signature are excluded."],"messageIds":["deleteBlock","restatesSignature"],"optionsSchema":null,"rationale":"Signature-only JSDoc duplicates type information and drifts without helping callers.","references":[],"remediation":"Delete the block or document behavior, constraints, failures, or context the signature cannot express.","since":null,"source":"packages/typescript/src/rules/no-restated-jsdoc.ts","status":"active","summary":"Flag a JSDoc block whose description and tags only re-spell the signature they document.","test":"packages/typescript/tests/rules/no-restated-jsdoc.test.ts"},{"aliases":[],"autofix":"none","category":"architecture","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/client.ts","source":"const client = require('axios');"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"runtime-load","outcome":"reject","title":"Do not load a restricted library at runtime"},{"expectedCount":0,"files":[{"path":"src/client.ts","source":"import axios from 'axios';"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"static-import","outcome":"accept","title":"Static imports remain the static-import rule's responsibility"}],"filePatterns":[],"id":"no-restricted-library-load","key":"eslint:no-restricted-library-load","languages":["typescript"],"limitations":["Only literal dynamic imports, unshadowed CommonJS loads, and TypeScript import-equals declarations are checked."],"messageIds":["restrictedLibraryLoad"],"optionsSchema":{"additionalProperties":false,"properties":{"libraries":{"items":{"additionalProperties":false,"properties":{"id":{"minLength":1,"type":"string"},"module":{"minLength":1,"type":"string"},"note":{"minLength":1,"type":"string"},"replacement":{"minLength":1,"type":"string"}},"required":["id","module","replacement"],"type":"object"},"type":"array"}},"required":["libraries"],"type":"object"},"rationale":"Runtime module loads can bypass the replacement policy enforced for static imports.","references":[],"remediation":"Load the configured replacement library instead of the restricted module.","since":null,"source":"packages/typescript/src/rules/no-restricted-library-load.ts","status":"active","summary":"Apply a configured library-replacement policy to literal dynamic imports, CommonJS loads, and TypeScript import-equals declarations.","test":"packages/typescript/tests/rules/no-restricted-library-load.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/auth.ts","source":"logger.error('auth failed', { token });"}],"fixedFiles":[],"focusPath":"src/auth.ts","id":"logged-secret","outcome":"reject","title":"Do not send a secret to logs"},{"expectedCount":0,"files":[{"path":"src/auth.ts","source":"logger.info('auth', { tokenPrefix });"}],"fixedFiles":[],"focusPath":"src/auth.ts","id":"redacted-secret","outcome":"accept","title":"Log an explicitly redacted value"}],"filePatterns":[],"id":"no-secret-in-log","key":"eslint:no-secret-in-log","languages":["typescript"],"limitations":["Detection uses configurable logger names and statically recognizable secret names, raw-body names, and redaction markers."],"messageIds":["noRawBodyInLog","noSecretInLog"],"optionsSchema":{"additionalProperties":false,"properties":{"logFunctions":{"items":{"type":"string"},"type":"array"},"loggerNames":{"items":{"type":"string"},"type":"array"}},"type":"object"},"rationale":"Logs are widely retained and distributed, so credentials and raw bodies can become durable data leaks.","references":[],"remediation":"Omit the value or log an explicitly redacted, truncated, or derived non-sensitive field.","since":null,"source":"packages/typescript/src/rules/no-secret-in-log.ts","status":"active","summary":"Disallow passing a secret-named value or a raw request/response blob to a logging call; both leak to log sinks. Redact or omit.","test":"packages/typescript/tests/rules/no-secret-in-log.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/runs.ts","source":"db.prepare(`SELECT id, status FROM runs`).all();"}],"fixedFiles":[],"focusPath":"src/runs.ts","id":"explicit-projection","outcome":"accept","title":"Select the required columns"},{"expectedCount":1,"files":[{"path":"src/runs.ts","source":"db.prepare(`SELECT * FROM runs`).all();"}],"fixedFiles":[],"focusPath":"src/runs.ts","id":"wildcard-projection","outcome":"reject","title":"Do not select every column"}],"filePatterns":[],"id":"no-select-star","key":"eslint:no-select-star","languages":["typescript"],"limitations":["Only statically visible embedded SQL is checked; function arguments such as COUNT(*) and stars inside EXISTS are excluded."],"messageIds":["noSelectStar"],"optionsSchema":null,"rationale":"Wildcard projections couple row shape and query cost to unrelated schema changes.","references":[],"remediation":"List every required column explicitly in the projection.","since":null,"source":"packages/typescript/src/rules/no-select-star.ts","status":"active","summary":"Disallow SELECT * in embedded SQL; it over-fetches and leaves the row contract implicit, so a schema change breaks row parsing silently.","test":"packages/typescript/tests/rules/no-select-star.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/load.ts","source":"function load() { try { return read(); } catch (error) { logger.warn('load failed', error); return null; } }"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"reported-fallback","outcome":"accept","title":"Report an error before returning a fallback"},{"expectedCount":1,"files":[{"path":"src/load.ts","source":"function load() { try { return read(); } catch { return null; } }"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"silent-fallback","outcome":"reject","title":"Do not turn an unreported error into absence"}],"filePatterns":[],"id":"no-sentinel-return-on-catch","key":"eslint:no-sentinel-return-on-catch","languages":["typescript"],"limitations":["Recognized predicate, safe-parse, normal-path sentinel, deliberate parse, generated-client, and configured logging patterns are excluded."],"messageIds":["noSentinelReturn"],"optionsSchema":{"additionalProperties":false,"properties":{"logFunctions":{"items":{"type":"string"},"type":"array"},"loggerNames":{"items":{"type":"string"},"type":"array"}},"type":"object"},"rationale":"An unreported fallback makes operational failure indistinguishable from a legitimate empty result.","references":[],"remediation":"Rethrow, report the error before returning, or model expected absence with an explicit predicate, safe-parse, or result contract.","since":null,"source":"packages/typescript/src/rules/no-sentinel-return-on-catch.ts","status":"active","summary":"Disallow swallowing a caught error by returning an empty sentinel unless the error is handled or the sentinel is part of the function contract.","test":"packages/typescript/tests/rules/no-sentinel-return-on-catch.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/load.ts","source":"load().catch((error) => logger.error({ error }, 'load failed'));"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"reported-rejection","outcome":"accept","title":"Report the rejection"},{"expectedCount":1,"files":[{"path":"src/load.ts","source":"load().catch(() => null);"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"silent-rejection","outcome":"reject","title":"Do not swallow the rejection"}],"filePatterns":[],"id":"no-silent-promise-catch","key":"eslint:no-silent-promise-catch","languages":["typescript"],"limitations":["Test files, teardown calls, explanatory comments, non-function handlers, and handlers that consume or report the error are excluded."],"messageIds":["silentCatch"],"optionsSchema":null,"rationale":"A swallowed rejection hides failures and gives callers an indistinguishable fallback value.","references":[],"remediation":"Log, rethrow, or explicitly recover from the rejection; explain intentional teardown suppression.","since":null,"source":"packages/typescript/src/rules/no-silent-promise-catch.ts","status":"active","summary":"Disallow `.catch()` and second-argument `.then()` handlers that silently swallow a rejection; log, rethrow, or handle the error.","test":"packages/typescript/tests/rules/no-silent-promise-catch.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/retry.test.ts","source":"it('retries', async () => { vi.useFakeTimers(); const result = retry(); await vi.advanceTimersByTimeAsync(50); await result; });"}],"fixedFiles":[],"focusPath":"src/retry.test.ts","id":"fake-timer","outcome":"accept","title":"Advance time deterministically"},{"expectedCount":1,"files":[{"path":"src/retry.test.ts","source":"it('retries', async () => { await sleep(50); expect(done()).toBe(true); });"}],"fixedFiles":[],"focusPath":"src/retry.test.ts","id":"fixed-sleep","outcome":"reject","title":"Do not wait for wall-clock time"}],"filePatterns":["**/*.test.*","**/*.spec.*","**/tests/**","**/__tests__/**"],"id":"no-sleep-in-test-body","key":"eslint:no-sleep-in-test-body","languages":["typescript"],"limitations":["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked; nested fakes and parameterized delays are excluded."],"messageIds":["noSleepInTestBody"],"optionsSchema":null,"rationale":"Wall-clock delays make test correctness depend on scheduler and machine speed.","references":[],"remediation":"Await the observable signal or advance deterministic fake timers.","since":null,"source":"packages/typescript/src/rules/no-sleep-in-test-body.ts","status":"active","summary":"Disallow a fixed timed sleep directly in a test body; it flakes under CI load. Synchronize on the signal or use fake timers.","test":"packages/typescript/tests/rules/no-sleep-in-test-body.test.ts"},{"aliases":[],"autofix":"none","category":"architecture","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/engineer-digest/post.ts","source":"await kv.put('digest:last', timestamp);"}],"fixedFiles":[],"focusPath":"src/engineer-digest/post.ts","id":"private-storage","outcome":"reject","title":"Do not write private state in a stateless module"},{"expectedCount":0,"files":[{"path":"src/engineer-digest/post.ts","source":"const issues = await linear.listIssues();"}],"fixedFiles":[],"focusPath":"src/engineer-digest/post.ts","id":"system-of-record","outcome":"accept","title":"Read from the system of record"}],"filePatterns":[],"id":"no-storage-in-stateless-modules","key":"eslint:no-storage-in-stateless-modules","languages":["typescript"],"limitations":["The rule is disabled until module path patterns are configured and recognizes only configured storage method names."],"messageIds":["storageInStatelessModule"],"optionsSchema":{"additionalProperties":false,"properties":{"methods":{"description":"Storage method names to flag. Replaces the defaults.","items":{"type":"string"},"type":"array"},"modules":{"description":"Regex sources matched against the filename. Empty (the default) disables the rule.","items":{"type":"string"},"type":"array"}},"type":"object"},"rationale":"Private storage in a stateless workflow creates another source of truth that can silently diverge.","references":[],"remediation":"Read from the system of record or derive state from an artifact the workflow already produces.","since":null,"source":"packages/typescript/src/rules/no-storage-in-stateless-modules.ts","status":"active","summary":"Disallow SQL or key/value access inside configured stateless modules; derive state from a system of record instead.","test":"packages/typescript/tests/rules/no-storage-in-stateless-modules.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/render.ts","source":"const parts = []; for (const item of items) { parts.push(item); } const output = parts.join(\"\");"}],"fixedFiles":[],"focusPath":"src/render.ts","id":"join-fragments","outcome":"accept","title":"Join collected fragments after the loop"},{"expectedCount":1,"files":[{"path":"src/render.ts","source":"let output = ''; for (const item of items) { output = `${output}${item}`; }"}],"fixedFiles":[],"focusPath":"src/render.ts","id":"rebuild-string","outcome":"reject","title":"Do not rebuild a growing string in a loop"}],"filePatterns":[],"id":"no-string-concat-in-loop","key":"eslint:no-string-concat-in-loop","languages":["typescript"],"limitations":["Only local identifiers initialized with a string or template literal and accumulated in a loop body are inspected."],"messageIds":["noStringConcatInLoop"],"optionsSchema":null,"rationale":"Repeatedly rebuilding a growing string can copy all prior content on each iteration, making total work grow quadratically.","references":[],"remediation":"Collect each fragment in an array, then join the fragments after the loop.","since":null,"source":"packages/typescript/src/rules/no-string-concat-in-loop.ts","status":"active","summary":"Disallow O(n^2) string building via `+=` on a string variable inside a loop; push parts to an array and `join` instead.","test":"packages/typescript/tests/rules/no-string-concat-in-loop.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/add.test.ts","source":"it('works', () => { expect(true).toBe(true); });"}],"fixedFiles":[],"focusPath":"src/add.test.ts","id":"literal-only-assertion","outcome":"reject","title":"Do not compare identical literals"},{"expectedCount":0,"files":[{"path":"src/add.test.ts","source":"it('adds', () => { expect(add(1, 1)).toBe(2); });"}],"fixedFiles":[],"focusPath":"src/add.test.ts","id":"produced-value","outcome":"accept","title":"Assert on a produced value"}],"filePatterns":[],"id":"no-tautological-expect","key":"eslint:no-tautological-expect","languages":["typescript"],"limitations":["Only direct supported `expect` matcher calls in recognized test files are inspected."],"messageIds":["tautologicalComparison","tautologicalMatcher"],"optionsSchema":null,"rationale":"An assertion determined entirely by literals does not observe the code under test and can keep passing after that code is removed.","references":[],"remediation":"Assert on a value produced by the behavior under test, or remove the assertion.","since":null,"source":"packages/typescript/src/rules/no-tautological-expect.ts","status":"active","summary":"Disallow an assertion whose operands are all literals; its outcome is fixed before the code runs, so it can never fail.","test":"packages/typescript/tests/rules/no-tautological-expect.test.ts"},{"aliases":["trailing-value-narration"],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/timeouts.ts","source":"const timeout = 5 * 60; // 5 minutes for cold starts"}],"fixedFiles":[],"focusPath":"src/timeouts.ts","id":"explain-constraint","outcome":"accept","title":"Explain a domain constraint"},{"expectedCount":1,"files":[{"path":"src/timeouts.ts","source":"const staleTime = 5 * 60 * 1000; // 5 minutes"}],"fixedFiles":[],"focusPath":"src/timeouts.ts","id":"repeat-duration","outcome":"reject","title":"Do not narrate the numeric duration"}],"filePatterns":[],"id":"no-trailing-value-narration","key":"eslint:no-trailing-value-narration","languages":["typescript"],"limitations":["Only trailing comments with numeric values and recognized unit words are inspected."],"messageIds":["narratesValue"],"optionsSchema":null,"rationale":"A repeated value can disagree with the expression after either the code or comment changes.","references":[],"remediation":"Put the unit in the identifier and keep comments only when they explain a constraint or non-obvious conversion.","since":null,"source":"packages/typescript/src/rules/no-trailing-value-narration.ts","status":"active","summary":"Flag a trailing comment that repeats the line's numeric value only to name its unit.","test":"packages/typescript/tests/rules/no-trailing-value-narration.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/credentials.ts","source":"interface Credentials {\n  // Database host.\n  host?: string;\n  // Database host port.\n  port?: number;\n  // Database username.\n  username?: string;\n  // Database password.\n  password?: string;\n}"}],"fixedFiles":[],"focusPath":"src/credentials.ts","id":"restated-type-members","outcome":"reject","title":"Do not restate member names and types"},{"expectedCount":0,"files":[{"path":"src/credentials.ts","source":"interface Credentials { host: string; port: number; username: string; }"}],"fixedFiles":[],"focusPath":"src/credentials.ts","id":"uncommented-members","outcome":"accept","title":"Let clear member names and types stand alone"}],"filePatterns":[],"id":"no-type-member-comment-wall","key":"eslint:no-type-member-comment-wall","languages":["typescript"],"limitations":["Only interface and type-literal bodies meeting the configured comment-count and restatement-ratio thresholds are reported."],"messageIds":["commentWall"],"optionsSchema":{"additionalProperties":false,"properties":{"maxNovelWords":{"description":"Most content words a comment may add beyond its member's own source and still count as a restatement.","minimum":0,"type":"integer"},"minCommentedMembers":{"description":"Fewest commented members that can count as a wall.","minimum":2,"type":"integer"},"minCommentedRatio":{"description":"Least share of the members that must be commented; below it the comments are group labels.","maximum":1,"minimum":0,"type":"number"},"minRestatedRatio":{"description":"Least share of the member comments that must be restatements.","maximum":1,"minimum":0,"type":"number"}},"type":"object"},"rationale":"Repetitive member comments add scanning cost while hiding the comments that describe facts absent from the type.","references":[],"remediation":"Delete comments that restate member names or types and keep comments that add constraints or behavior.","since":null,"source":"packages/typescript/src/rules/no-type-member-comment-wall.ts","status":"active","summary":"Flag an object type whose member comments mostly re-spell the members' own names and types.","test":"packages/typescript/tests/rules/no-type-member-comment-wall.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/client.ts","source":"/** Retries when the vendor returns 429. */\nexport function fetchValue(id: string): number { return 1; }"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"behavioral-documentation","outcome":"accept","title":"Keep behavior that the signature cannot express"},{"expectedCount":1,"files":[{"path":"src/client.ts","source":"/** @param id external identifier\n * @returns the value\n */\nexport function fetchValue(id: string): number { return 1; }"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"repeated-typed-sections","outcome":"reject","title":"Do not restate typed parameters and returns"}],"filePatterns":[],"id":"no-typed-doc-sections","key":"eslint:no-typed-doc-sections","languages":["typescript"],"limitations":["Parameter and return tags are reported only when the documented function has corresponding explicit TypeScript types."],"messageIds":["typedSection"],"optionsSchema":null,"rationale":"Parameter and return tags repeat typed signatures and can drift without adding runtime behavior or constraints.","references":[],"remediation":"Remove repeated parameter and return tags; retain documentation for behavior, failures, and external contracts.","since":null,"source":"packages/typescript/src/rules/no-typed-doc-sections.ts","status":"active","summary":"Reject typed-signature repetition while preserving behavior that types cannot express.","test":"packages/typescript/tests/rules/no-typed-doc-sections.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/record.ts","source":"interface R {\n  kind: string; // 'aa' | 'bb'\n}"}],"fixedFiles":[],"focusPath":"src/record.ts","id":"comment-only-union","outcome":"reject","title":"Do not leave allowed values in a comment"},{"expectedCount":0,"files":[{"path":"src/record.ts","source":"interface R { kind: 'aa' | 'bb'; }"}],"fixedFiles":[],"focusPath":"src/record.ts","id":"literal-union","outcome":"accept","title":"Encode allowed values in the type"}],"filePatterns":[],"id":"no-union-in-comment","key":"eslint:no-union-in-comment","languages":["typescript"],"limitations":["Only bare quoted-value lists attached to supported string declarations and schema-builder fields are inspected."],"messageIds":["unionInComment"],"optionsSchema":null,"rationale":"A comment cannot prevent callers from supplying strings outside the listed set, and the list can drift from runtime behavior.","references":[],"remediation":"Move the allowed values into a string-literal union and remove the redundant comment.","since":null,"source":"packages/typescript/src/rules/no-union-in-comment.ts","status":"active","summary":"Flag a comment that lists a `string` field's allowed values instead of the type listing them.","test":"packages/typescript/tests/rules/no-union-in-comment.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/counter.tsx","source":"'use client'; import { useState } from 'react'; export default function X() { const [n] = useState(0); return <div>{n}</div>; }"}],"fixedFiles":[],"focusPath":"src/counter.tsx","id":"interactive-component","outcome":"accept","title":"Keep the directive for interactive components"},{"expectedCount":1,"files":[{"path":"src/banner.tsx","source":"'use client'; export default function X() { return <div>hello</div>; }"}],"fixedFiles":[],"focusPath":"src/banner.tsx","id":"static-component","outcome":"reject","title":"Remove the directive from static components"}],"filePatterns":[],"id":"no-unnecessary-use-client","key":"eslint:no-unnecessary-use-client","languages":["typescript"],"limitations":["Client need is inferred from recognized hooks, handlers, browser globals, exports, classes, and known client-only imports."],"messageIds":["unnecessaryUseClient"],"optionsSchema":null,"rationale":"An unnecessary client boundary sends the component and its transitive dependencies to the browser without using client-only behavior.","references":[],"remediation":"Remove the directive, or keep it only when the module uses a supported client-side API or boundary dependency.","since":null,"source":"packages/typescript/src/rules/no-unnecessary-use-client.ts","status":"active","summary":"Flag `'use client'` files with no hooks or event handlers — they could be RSC.","test":"packages/typescript/tests/rules/no-unnecessary-use-client.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/client.test.ts","source":"import type * as vi from \"vitest\"; const m = myFn as vi.Mock;"}],"fixedFiles":[],"focusPath":"src/client.test.ts","id":"mock-type-assertion","outcome":"reject","title":"Do not assert that a value is a mock"},{"expectedCount":0,"files":[{"path":"src/client.test.ts","source":"const m = vi.mocked(myFn);"}],"fixedFiles":[],"focusPath":"src/client.test.ts","id":"typed-mock-helper","outcome":"accept","title":"Use the framework helper"}],"filePatterns":[],"id":"no-unsafe-mock-casting","key":"eslint:no-unsafe-mock-casting","languages":["typescript"],"limitations":["Only mock types imported from Vitest or Jest modules are inspected."],"messageIds":["unsafeMockCast"],"optionsSchema":null,"rationale":"A type assertion can claim an unmocked value is a mock and bypass checking between the original callable and the mock API.","references":[],"remediation":"Use the test framework's `mocked` helper to obtain the typed mock reference.","since":null,"source":"packages/typescript/src/rules/no-unsafe-mock-casting.ts","status":"active","summary":"Disallow casting to mock types like `jest.Mock` or `vi.Mock`. Use `vi.mocked()` or `jest.mocked()` instead.","test":"packages/typescript/tests/rules/no-unsafe-mock-casting.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/status.ts","source":"import { z } from \"zod\"; const S = z.enum([\"active\", \"inactive\"]);"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"zod-literal-enum","outcome":"accept","title":"Declare string values directly in Zod"},{"expectedCount":1,"files":[{"path":"src/status.ts","source":"import { z } from \"zod\"; const S = z.nativeEnum({ Active: \"active\", Inactive: \"inactive\" });"}],"fixedFiles":[{"path":"src/status.ts","source":"import { z } from \"zod\"; const S = z.enum([\"active\", \"inactive\"]);"}],"focusPath":"src/status.ts","id":"zod-native-enum","outcome":"reject","title":"Do not wrap a TypeScript enum"}],"filePatterns":[],"id":"no-zod-native-enum","key":"eslint:no-zod-native-enum","languages":["typescript"],"limitations":["Automatic fixes are limited to inline object literals whose unique values are all string literals."],"messageIds":["enumOfTsEnum","nativeEnum"],"optionsSchema":null,"rationale":"Wrapping a TypeScript enum preserves its emitted runtime object and duplicates the schema's value definition across two constructs.","references":[],"remediation":"Pass string literals directly to `z.enum` and derive the TypeScript type with `z.infer`.","since":null,"source":"packages/typescript/src/rules/no-zod-native-enum.ts","status":"active","summary":"Disallow `z.nativeEnum()` (and `z.enum()` over a TypeScript enum); use `z.enum([\"a\", \"b\"])` with a string-literal union instead.","test":"packages/typescript/tests/rules/no-zod-native-enum.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/auth.ts","source":"if (await constantTimeEqual(presentedToken, expectedToken)) { allow(); }"}],"fixedFiles":[],"focusPath":"src/auth.ts","id":"constant-time-compare","outcome":"accept","title":"Use a constant-time comparison"},{"expectedCount":1,"files":[{"path":"src/auth.ts","source":"if (presentedToken === expectedToken) { allow(); }"}],"fixedFiles":[],"focusPath":"src/auth.ts","id":"secret-equality","outcome":"reject","title":"Do not compare secrets with equality"}],"filePatterns":[],"id":"prefer-constant-time-secret-compare","key":"eslint:prefer-constant-time-secret-compare","languages":["typescript"],"limitations":["Secret-like values are identified conservatively from their names; test files and public sentinel comparisons are excluded."],"messageIds":["preferConstantTimeSecretCompare"],"optionsSchema":null,"rationale":"Ordinary equality stops at the first differing byte, allowing repeated measurements to reveal secret material.","references":[],"remediation":"Compare equal-length cryptographic digests with a constant-time comparison primitive.","since":null,"source":"packages/typescript/src/rules/prefer-constant-time-secret-compare.ts","status":"active","summary":"Disallow `===`/`!==` on a secret-like value; short-circuiting comparison leaks the secret through timing. Use a constant-time compare.","test":"packages/typescript/tests/rules/prefer-constant-time-secret-compare.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/result.ts","source":"type Result = { ok: true; data: string } | { ok: false; error: string };"}],"fixedFiles":[],"focusPath":"src/result.ts","id":"explicit-result-branches","outcome":"accept","title":"Use explicit result branches"},{"expectedCount":1,"files":[{"path":"src/result.ts","source":"type Result = { ok: boolean; data?: string; error?: string };"}],"fixedFiles":[],"focusPath":"src/result.ts","id":"optional-result-payloads","outcome":"reject","title":"Do not make both result payloads optional"}],"filePatterns":[],"id":"prefer-discriminated-union","key":"eslint:prefer-discriminated-union","languages":["typescript"],"limitations":["Only local object shapes with recognized positive status and payload names are inspected."],"messageIds":["preferDiscriminatedUnion"],"optionsSchema":null,"rationale":"A boolean status plus optional branch data permits contradictory and incomplete states.","references":[],"remediation":"Represent each result branch as a discriminated union member with its required payload.","since":null,"source":"packages/typescript/src/rules/prefer-discriminated-union.ts","status":"active","summary":"Flag flat result objects with a required positive boolean status and optional success/failure payloads.","test":"packages/typescript/tests/rules/prefer-discriminated-union.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/constants.ts","source":"const VALUES = [1, 2, 3];"}],"fixedFiles":[],"focusPath":"src/constants.ts","id":"mutable-array-literal","outcome":"reject","title":"A module constant exposes a mutable array"},{"expectedCount":0,"files":[{"path":"src/constants.ts","source":"const VALUES = [1, 2, 3] as const;"}],"fixedFiles":[],"focusPath":"src/constants.ts","id":"readonly-array-literal","outcome":"accept","title":"A module constant exposes a readonly literal"}],"filePatterns":[],"id":"prefer-immutable-module-constant","key":"eslint:prefer-immutable-module-constant","languages":["typescript"],"limitations":["The rule skips generated files, test files, JavaScript files, and collections that are deliberately mutated in their declaring module."],"messageIds":["preferAsConst","preferReadonlyCollection"],"optionsSchema":null,"rationale":"A const binding prevents reassignment but does not stop callers from mutating its array, object, Set, or Map contents.","references":[],"remediation":"Expose literals with `as const` or a readonly type, and expose Set or Map values through ReadonlySet or ReadonlyMap.","since":null,"source":"packages/typescript/src/rules/prefer-immutable-module-constant.ts","status":"active","summary":"Require module-level constant collections to expose readonly state.","test":"packages/typescript/tests/rules/prefer-immutable-module-constant.test.ts"},{"aliases":[],"autofix":"none","category":"style","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/search.tsx","source":"import { Search } from 'lucide-react'; import { InputGroup, InputGroupAddon, InputGroupInput } from '@/components/ui/input-group'; const field = <InputGroup><InputGroupAddon><Search /></InputGroupAddon><InputGroupInput /></InputGroup>;"}],"fixedFiles":[],"focusPath":"src/search.tsx","id":"grouped-search","outcome":"accept","title":"Use the shared input group"},{"expectedCount":1,"files":[{"path":"src/search.tsx","source":"import { Search } from 'lucide-react'; import { Input } from '@/components/ui/input'; const field = <div><Search /><Input /></div>;"}],"fixedFiles":[],"focusPath":"src/search.tsx","id":"loose-search-input","outcome":"reject","title":"Do not pair loose search controls"}],"filePatterns":[],"id":"prefer-input-group-search","key":"eslint:prefer-input-group-search","languages":["typescript"],"limitations":["Only Search and Input bindings imported from the recognized shared modules are paired."],"messageIds":["preferInputGroup"],"optionsSchema":null,"rationale":"The shared compound control provides consistent spacing, focus behavior, and accessible composition.","references":[],"remediation":"Compose the search icon and field with InputGroup, InputGroupAddon, and InputGroupInput.","since":null,"source":"packages/typescript/src/rules/prefer-input-group-search.ts","status":"active","summary":"Require search icons and shared Input controls in the same visual wrapper to use InputGroup.","test":"packages/typescript/tests/rules/prefer-input-group-search.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/keys.ts","source":"const KEYS = ['a', 'b', 'c'] as const; function isAllowed(key: string) { return KEYS.includes(key); }"}],"fixedFiles":[],"focusPath":"src/keys.ts","id":"hoisted-collection","outcome":"accept","title":"Hoist a constant collection"},{"expectedCount":1,"files":[{"path":"src/keys.ts","source":"function isAllowed(key: string) { const KEYS = ['a', 'b', 'c']; return KEYS.includes(key); }"}],"fixedFiles":[],"focusPath":"src/keys.ts","id":"local-collection","outcome":"reject","title":"Do not recreate a constant collection"}],"filePatterns":[],"id":"prefer-module-level-constant","key":"eslint:prefer-module-level-constant","languages":["typescript"],"limitations":["Collections that are small, mutated, escape the function, or depend on local values are not reported."],"messageIds":["hoistCollection","hoistRegex"],"optionsSchema":{"additionalProperties":false,"properties":{"checkRegex":{"type":"boolean"},"ignoreTestFiles":{"type":"boolean"},"minElements":{"minimum":1,"type":"number"}},"type":"object"},"rationale":"Recreating immutable lookup data on every call wastes allocations and obscures its constant nature.","references":[],"remediation":"Declare immutable literal collections and non-stateful regular expressions once at module scope.","since":null,"source":"packages/typescript/src/rules/prefer-module-level-constant.ts","status":"active","summary":"Hoist literal-only constant collections and regexes out of function bodies to module scope so they are allocated once.","test":"packages/typescript/tests/rules/prefer-module-level-constant.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/handler.ts","source":"import { z } from 'zod'; export function handle(raw: unknown) { const ZBody = z.object({ id: z.string(), name: z.string() }); return ZBody.parse(raw); }"}],"fixedFiles":[],"focusPath":"src/handler.ts","id":"local-schema","outcome":"reject","title":"Do not rebuild a closed schema"},{"expectedCount":0,"files":[{"path":"src/handler.ts","source":"import { z } from 'zod'; const ZBody = z.object({ id: z.string(), name: z.string() }); export function handle(raw: unknown) { return ZBody.parse(raw); }"}],"fixedFiles":[],"focusPath":"src/handler.ts","id":"module-schema","outcome":"accept","title":"Declare the schema once"}],"filePatterns":[],"id":"prefer-module-level-schema","key":"eslint:prefer-module-level-schema","languages":["typescript"],"limitations":["Schemas that depend on local state or are wrapped in a recognized memoization helper are excluded."],"messageIds":["hoistSchema"],"optionsSchema":{"additionalProperties":false,"properties":{"factories":{"description":"Zod factory names to check. Defaults to the object-like composites; add `array` / `enum` to widen.","items":{"type":"string"},"type":"array"},"ignoreTestFiles":{"description":"Skip test files, where a fixture schema belongs next to its assertion.","type":"boolean"},"memoCallees":{"description":"Wrappers that construct their callback at most once. Defaults to lazy, memo, once, and useMemo.","items":{"type":"string"},"type":"array"},"minProperties":{"description":"Minimum key count before an object-like schema is reported.","minimum":0,"type":"number"}},"type":"object"},"rationale":"A closed schema created inside a function is rebuilt on every call and cannot be reused or exported for inference.","references":[],"remediation":"Move the closed schema declaration to module scope and reference it from the function.","since":null,"source":"packages/typescript/src/rules/prefer-module-level-schema.ts","status":"active","summary":"Declare a Zod schema at module scope when it closes over nothing in the enclosing function","test":"packages/typescript/tests/rules/prefer-module-level-schema.test.ts"},{"aliases":[],"autofix":"suggestion","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/id.ts","source":"const id = globalThis.crypto.randomUUID();"}],"fixedFiles":[],"focusPath":"src/id.ts","id":"native-random-uuid","outcome":"accept","title":"Use the platform UUID generator"},{"expectedCount":1,"files":[{"path":"src/id.ts","source":"import { v4 } from 'uuid'; const id = v4();"}],"fixedFiles":[],"focusPath":"src/id.ts","id":"uuid-v4-package","outcome":"reject","title":"Do not call uuid v4 without options"}],"filePatterns":[],"id":"prefer-native-random-uuid","key":"eslint:prefer-native-random-uuid","languages":["typescript"],"limitations":["Only resolved zero-argument UUID v4 calls are reported; customized and other UUID versions are excluded."],"messageIds":["preferNative","replaceWithNative"],"optionsSchema":null,"rationale":"The platform implementation avoids an unnecessary dependency for standard random UUID generation.","references":[],"remediation":"Call `globalThis.crypto.randomUUID()` and remove the unused `uuid` v4 import when possible.","since":null,"source":"packages/typescript/src/rules/prefer-native-random-uuid.ts","status":"active","summary":"Prefer `globalThis.crypto.randomUUID()` over resolved zero-argument UUID v4 bindings from the `uuid` package.","test":"packages/typescript/tests/rules/prefer-native-random-uuid.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/search.ts","source":"interface Input { items: string[] | undefined } function search({ items = [] }: Input) { return items.length; }"}],"fixedFiles":[],"focusPath":"src/search.ts","id":"defaulted-nullish-array","outcome":"reject","title":"Do not retain a redundant nullish state"},{"expectedCount":0,"files":[{"path":"src/search.ts","source":"interface Input { items: string[] } function search({ items }: Input) { return items.length; }"}],"fixedFiles":[],"focusPath":"src/search.ts","id":"non-null-array","outcome":"accept","title":"Model an always-present collection"}],"filePatterns":[],"id":"prefer-non-nullable-collection","key":"eslint:prefer-non-nullable-collection","languages":["typescript"],"limitations":["The rule requires local evidence that nullish and empty values are treated identically and skips exported wire shapes."],"messageIds":["preferNonNullableCollection"],"optionsSchema":null,"rationale":"A redundant nullish collection state spreads defaults and guards through consumers without carrying information.","references":[],"remediation":"Use a non-null collection type and normalize omitted input to an empty collection at the boundary.","since":null,"source":"packages/typescript/src/rules/prefer-non-nullable-collection.ts","status":"active","summary":"Suggest non-null arrays only when local control flow proves the nullish state is equivalent to an empty collection.","test":"packages/typescript/tests/rules/prefer-non-nullable-collection.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/client.ts","source":"async function load(response) { const body = await response.json(); return body.id; }"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"unvalidated-payload","outcome":"reject","title":"Do not trust response JSON directly"},{"expectedCount":0,"files":[{"path":"src/client.ts","source":"async function load(response) { const body = UserSchema.parse(await response.json()); return body.id; }"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"validated-payload","outcome":"accept","title":"Validate before property access"}],"filePatterns":[],"id":"prefer-schema-for-api-payload","key":"eslint:prefer-schema-for-api-payload","languages":["typescript"],"limitations":["Test fixtures, generated clients, local JSON files, and recognized validation guards are excluded."],"messageIds":["unparsedJsonAccess"],"optionsSchema":null,"rationale":"External JSON is untrusted at runtime even when its expected TypeScript shape is known statically.","references":[],"remediation":"Parse the payload through a schema or establish a recognized runtime validation guard before reading fields.","since":null,"source":"packages/typescript/src/rules/prefer-schema-for-api-payload.ts","status":"active","summary":"Require Zod (or similar) schema validation on `response.json()` / `JSON.parse()` results before property access.","test":"packages/typescript/tests/rules/prefer-schema-for-api-payload.test.ts"},{"aliases":[],"autofix":"none","category":"style","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/notice.tsx","source":"const notice = <div className=\"text-red-500\" />;"}],"fixedFiles":[],"focusPath":"src/notice.tsx","id":"raw-text-color","outcome":"reject","title":"Do not use a raw palette color"},{"expectedCount":0,"files":[{"path":"src/notice.tsx","source":"const notice = <div className=\"text-destructive\" />;"}],"fixedFiles":[],"focusPath":"src/notice.tsx","id":"semantic-text-color","outcome":"accept","title":"Use a semantic color token"}],"filePatterns":[],"id":"prefer-semantic-colors","key":"eslint:prefer-semantic-colors","languages":["typescript"],"limitations":["Email, PDF, icon artwork, masks, gradients, stories, and explicitly configured non-token projects have targeted exclusions."],"messageIds":["arbitraryColor","inlineColor","rawPalette"],"optionsSchema":{"additionalProperties":false,"properties":{"requireSemanticTokens":{"type":"boolean"}},"type":"object"},"rationale":"Semantic tokens keep themes and product meaning consistent while raw colors couple components to a palette value.","references":[],"remediation":"Replace raw palette and literal colors with the closest semantic design-system token or CSS variable.","since":null,"source":"packages/typescript/src/rules/prefer-semantic-colors.ts","status":"active","summary":"Enforce semantic color tokens over raw Tailwind palette classes, arbitrary color values, and inline color literals.","test":"packages/typescript/tests/rules/prefer-semantic-colors.test.ts"},{"aliases":[],"autofix":"none","category":"architecture","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"app/tasks/page.tsx","source":"await fetch('/api/tasks', { method: 'POST', body });"}],"fixedFiles":[],"focusPath":"app/tasks/page.tsx","id":"api-mutation","outcome":"reject","title":"Do not mutate through an API route"},{"expectedCount":0,"files":[{"path":"app/tasks/page.tsx","source":"import { createTask } from './actions'; await createTask(input);"}],"fixedFiles":[],"focusPath":"app/tasks/page.tsx","id":"server-action-call","outcome":"accept","title":"Call a Server Action"}],"filePatterns":[],"id":"prefer-server-actions","key":"eslint:prefer-server-actions","languages":["typescript"],"limitations":["Only statically recognizable /api/ mutations in applicable React modules are reported."],"messageIds":["preferServerAction"],"optionsSchema":null,"rationale":"Server Actions preserve typed application calls and avoid an internal JSON request-response boundary.","references":[],"remediation":"Move the mutation into a Server Action and invoke that action from the React client.","since":null,"source":"packages/typescript/src/rules/prefer-server-actions.ts","status":"active","summary":"Prefer Next.js Server Actions over /api/* mutations.","test":"packages/typescript/tests/rules/prefer-server-actions.test.ts"},{"aliases":[],"autofix":"none","category":"style","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/form.tsx","source":"const action = <button>Save</button>;"}],"fixedFiles":[],"focusPath":"src/form.tsx","id":"raw-button","outcome":"reject","title":"Do not use a raw button"},{"expectedCount":0,"files":[{"path":"src/form.tsx","source":"import { Button } from '@/components/ui/button'; const action = <Button>Save</Button>;"}],"fixedFiles":[],"focusPath":"src/form.tsx","id":"shared-button","outcome":"accept","title":"Use a shared button"}],"filePatterns":[],"id":"prefer-shadcn-primitives","key":"eslint:prefer-shadcn-primitives","languages":["typescript"],"limitations":["Hidden and file inputs, unassociated labels, and non-control semantic elements are excluded."],"messageIds":["preferShadcnPrimitive"],"optionsSchema":null,"rationale":"Shared primitives centralize interaction, accessibility, and visual behavior across the product.","references":[],"remediation":"Replace the raw visible control with the corresponding shared shadcn component.","since":null,"source":"packages/typescript/src/rules/prefer-shadcn-primitives.ts","status":"active","summary":"Require visible raw JSX controls to use the corresponding shared shadcn primitive.","test":"packages/typescript/tests/rules/prefer-shadcn-primitives.test.ts"},{"aliases":["strict-test-assertions"],"autofix":"safe","category":"testing","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/user.test.ts","source":"expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');"}],"fixedFiles":[{"path":"src/user.test.ts","source":"expect(user).toMatchObject({ id: 1, name: 'Ada' });\n"}],"focusPath":"src/user.test.ts","id":"member-run","outcome":"reject","title":"Do not split one object across assertions"},{"expectedCount":0,"files":[{"path":"src/user.test.ts","source":"expect(user).toMatchObject({ id: 1, name: 'Ada' });"}],"fixedFiles":[],"focusPath":"src/user.test.ts","id":"whole-object","outcome":"accept","title":"Assert the object once"}],"filePatterns":[],"id":"prefer-whole-object-assertion","key":"eslint:prefer-whole-object-assertion","languages":["typescript"],"limitations":[],"messageIds":["assertArrayOnce","combineAssertions"],"optionsSchema":null,"rationale":"One whole-object assertion presents related expectations together and produces a complete structural diff.","references":[],"remediation":"Replace consecutive member assertions with one `toMatchObject` assertion.","since":null,"source":"packages/typescript/src/rules/prefer-whole-object-assertion.ts","status":"active","summary":"Collapse consecutive assertions on one object into a whole-object assertion so related mismatches are reported together.","test":"packages/typescript/tests/rules/prefer-whole-object-assertion.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/user.ts","source":"import { z } from \"zod\"; const UserSchema = z.object({ id: z.string() }); interface User { id: string }"}],"fixedFiles":[],"focusPath":"src/user.ts","id":"handwritten-twin","outcome":"reject","title":"Do not duplicate the schema shape"},{"expectedCount":0,"files":[{"path":"src/user.ts","source":"import { z } from \"zod\"; const UserSchema = z.object({ id: z.string() }); type User = z.infer<typeof UserSchema>;"}],"fixedFiles":[],"focusPath":"src/user.ts","id":"inferred-type","outcome":"accept","title":"Infer the schema type"}],"filePatterns":[],"id":"prefer-zod-infer","key":"eslint:prefer-zod-infer","languages":["typescript"],"limitations":[],"messageIds":["handWrittenTwin"],"optionsSchema":{"additionalProperties":false,"properties":{"ignoreTypeNames":{"items":{"type":"string"},"type":"array"},"requireIdenticalShape":{"type":"boolean"}},"type":"object"},"rationale":"A derived type stays synchronized when the runtime schema changes.","references":[],"remediation":"Replace the hand-written twin with `z.infer<typeof Schema>`.","since":null,"source":"packages/typescript/src/rules/prefer-zod-infer.ts","status":"active","summary":"Derive a type from its Zod schema with `z.infer` instead of hand-writing a twin declaration beside it.","test":"packages/typescript/tests/rules/prefer-zod-infer.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/render.ts","source":"declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: assertNever(kind); }"}],"fixedFiles":[],"focusPath":"src/render.ts","id":"assert-never-default","outcome":"accept","title":"Make the default exhaustive"},{"expectedCount":1,"files":[{"path":"src/render.ts","source":"declare const kind: 'a' | 'b';\nswitch (kind) { case 'a': break; case 'b': break; default: }"}],"fixedFiles":[],"focusPath":"src/render.ts","id":"empty-default","outcome":"reject","title":"Do not leave an exhaustive default empty"}],"filePatterns":[],"id":"require-assert-never","key":"eslint:require-assert-never","languages":["typescript"],"limitations":[],"messageIds":["missingAssertNever"],"optionsSchema":null,"rationale":"An empty default silently accepts new union members instead of making the compiler identify the missing case.","references":[],"remediation":"Call `assertNever` with the discriminant in the exhaustive switch default.","since":null,"source":"packages/typescript/src/rules/require-assert-never.ts","status":"active","summary":"Require an empty switch default to call `assertNever` so discriminated unions remain exhaustive at compile time.","test":"packages/typescript/tests/rules/require-assert-never.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/client.ts","source":"await fetch(url, { signal: AbortSignal.timeout(5000) });"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"bounded-fetch","outcome":"accept","title":"Bound the request"},{"expectedCount":1,"files":[{"path":"src/client.ts","source":"await fetch('https://api.example.com/items');"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"unbounded-fetch","outcome":"reject","title":"Do not leave fetch unbounded"}],"filePatterns":[],"id":"require-fetch-timeout","key":"eslint:require-fetch-timeout","languages":["typescript"],"limitations":[],"messageIds":["missingSignal"],"optionsSchema":{"additionalProperties":false,"properties":{"allowIn":{"description":"Glob patterns for wrapper modules exempt from the rule. Matched against the ABSOLUTE file path, so anchor with a `**/` prefix (e.g. `**/http-client.ts`).","items":{"type":"string"},"type":"array"}},"type":"object"},"rationale":"An unbounded request can occupy work indefinitely when an upstream stalls.","references":[],"remediation":"Pass an abort signal, such as `AbortSignal.timeout(ms)`, in the fetch init.","since":null,"source":"packages/typescript/src/rules/require-fetch-timeout.ts","status":"active","summary":"Require an abort `signal` (e.g. `AbortSignal.timeout(ms)`) on global `fetch()` calls so stalled upstreams cannot hang the caller forever.","test":"packages/typescript/tests/rules/require-fetch-timeout.test.ts"},{"aliases":["require-interface-for-injected-service"],"autofix":"none","category":"architecture","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/service.ts","source":"export class RequestHandler { constructor(private readonly store: TaskStore) {} handle(): void { this.store.handle(); } }"}],"fixedFiles":[],"focusPath":"src/service.ts","id":"concrete-injected-service","outcome":"reject","title":"Do not expose only the concrete service"},{"expectedCount":0,"files":[{"path":"src/service.ts","source":"interface Handler { handle(): void }\nexport class RequestHandler implements Handler { constructor(private readonly store: TaskStore) {} handle(): void { this.store.handle(); } }"}],"fixedFiles":[],"focusPath":"src/service.ts","id":"declared-service-port","outcome":"accept","title":"Implement the service port"}],"filePatterns":[],"id":"require-port-for-service","key":"eslint:require-port-for-service","languages":["typescript"],"limitations":[],"messageIds":["requireInterface"],"optionsSchema":null,"rationale":"A declared port keeps consumers coupled to the service capability instead of its concrete implementation.","references":[],"remediation":"Declare and implement an interface covering the service's public methods.","since":null,"source":"packages/typescript/src/rules/require-port-for-service.ts","status":"active","summary":"Advise when an exported service with injected collaborators has public methods not covered by its declared ports.","test":"packages/typescript/tests/rules/require-port-for-service.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/middleware.ts","source":"const matcher = \"/api/:path*\"; export const config = { matcher };"}],"fixedFiles":[],"focusPath":"src/middleware.ts","id":"computed-matcher","outcome":"reject","title":"Do not compute the matcher"},{"expectedCount":0,"files":[{"path":"src/middleware.ts","source":"export const config = { matcher: \"/api/:path*\" };"}],"fixedFiles":[],"focusPath":"src/middleware.ts","id":"literal-matcher","outcome":"accept","title":"Use a literal matcher"}],"filePatterns":[],"id":"require-static-next-matcher","key":"eslint:require-static-next-matcher","languages":["typescript"],"limitations":[],"messageIds":["dynamicMatcher"],"optionsSchema":null,"rationale":"Next.js must statically analyze matcher values at build time; computed values are ignored.","references":[],"remediation":"Write matcher strings, arrays, and object fields as literals in the exported config.","since":null,"source":"packages/typescript/src/rules/require-static-next-matcher.ts","status":"active","summary":"Require Next.js middleware and proxy matcher configuration to contain only build-time literals.","test":"packages/typescript/tests/rules/require-static-next-matcher.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/action.ts","source":"const name = formData.get('name');"}],"fixedFiles":[],"focusPath":"src/action.ts","id":"raw-form-value","outcome":"reject","title":"Do not use a raw form value"},{"expectedCount":0,"files":[{"path":"src/action.ts","source":"const input = UserSchema.parse({ name: formData.get('name') });"}],"fixedFiles":[],"focusPath":"src/action.ts","id":"validated-form-value","outcome":"accept","title":"Validate the form value"}],"filePatterns":[],"id":"require-zod-form-validation","key":"eslint:require-zod-form-validation","languages":["typescript"],"limitations":[],"messageIds":["missingZodValidation"],"optionsSchema":null,"rationale":"FormData values are untrusted strings or files and need runtime validation before use.","references":[],"remediation":"Read the value inside a Zod schema's `parse` or `safeParse` input.","since":null,"source":"packages/typescript/src/rules/require-zod-form-validation.ts","status":"active","summary":"Require Zod validation (`Schema.parse(...)` / `Schema.safeParse(...)`) when reading values out of a `FormData` object.","test":"packages/typescript/tests/rules/require-zod-form-validation.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/run.ts","source":"function run() { return load(); }\nfunction load() { return 1; }"}],"fixedFiles":[],"focusPath":"src/run.ts","id":"caller-before-helper","outcome":"accept","title":"Place the caller first"},{"expectedCount":1,"files":[{"path":"src/run.ts","source":"function load() { return 1; }\nfunction run() { return load(); }"}],"fixedFiles":[],"focusPath":"src/run.ts","id":"helper-before-caller","outcome":"reject","title":"Do not lead with a sole-caller helper"}],"filePatterns":[],"id":"stepdown","key":"eslint:stepdown","languages":["typescript"],"limitations":[],"messageIds":["helperAboveOnlyCaller"],"optionsSchema":null,"rationale":"Caller-first ordering lets a reader follow the main flow before descending into implementation details.","references":[],"remediation":"Move the private helper immediately below its sole caller.","since":null,"source":"packages/typescript/src/rules/stepdown.ts","status":"active","summary":"Place a private helper below its sole direct same-scope caller.","test":"packages/typescript/tests/rules/stepdown.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/store.ts","source":"db.prepare(`INSERT INTO runs (id) VALUES (?)`).run();"}],"fixedFiles":[],"focusPath":"src/store.ts","id":"bare-insert","outcome":"reject","title":"Do not issue a replay-unsafe insert"},{"expectedCount":0,"files":[{"path":"src/store.ts","source":"db.prepare(`INSERT INTO runs (id) VALUES (?) ON CONFLICT(id) DO NOTHING`).run();"}],"fixedFiles":[],"focusPath":"src/store.ts","id":"conflict-safe-insert","outcome":"accept","title":"Handle a replayed insert"}],"filePatterns":[],"id":"store-insert-requires-on-conflict","key":"eslint:store-insert-requires-on-conflict","languages":["typescript"],"limitations":[],"messageIds":["storeInsertRequiresOnConflict"],"optionsSchema":null,"rationale":"A replayed bare insert can duplicate data or fail on a uniqueness constraint.","references":[],"remediation":"Add an appropriate `ON CONFLICT` action or supported replay-safe insert form.","since":null,"source":"packages/typescript/src/rules/store-insert-requires-on-conflict.ts","status":"active","summary":"Require an embedded SQL INSERT to carry ON CONFLICT; store writes replay under cron re-runs and queue redelivery and must be idempotent upserts.","test":"packages/typescript/tests/rules/store-insert-requires-on-conflict.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/parser.test.ts","source":"test('parses', () => { for (const value of ['a', 'b']) { expect(parse(value)).toBe(value); } });"}],"fixedFiles":[],"focusPath":"src/parser.test.ts","id":"looped-cases","outcome":"reject","title":"Do not hide cases in a loop"},{"expectedCount":0,"files":[{"path":"src/parser.test.ts","source":"test.each(['a', 'b'])('parses %s', (value) => { expect(parse(value)).toBe(value); });"}],"fixedFiles":[],"focusPath":"src/parser.test.ts","id":"parameterized-cases","outcome":"accept","title":"Use a parameterized test"}],"filePatterns":["**/*.test.*","**/*.spec.*","**/tests/**"],"id":"test-loops-over-literal-cases","key":"eslint:test-loops-over-literal-cases","languages":["typescript"],"limitations":["Only inline literal for-of cases containing framework assertions are reported."],"messageIds":["literalCaseLoop"],"optionsSchema":null,"rationale":"A loop is reported as one test, so failures hide the individual case name and may stop later cases from running.","references":[],"remediation":"Create one named parameterized test or runner-aware subtest for each literal case.","since":null,"source":"packages/typescript/src/rules/test-loops-over-literal-cases.ts","status":"active","summary":"Disallow assertions over an inline literal case loop in a test; parameterization reports and names every case independently.","test":"packages/typescript/tests/rules/test-loops-over-literal-cases.test.ts"},{"aliases":[],"autofix":"none","category":"style","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/user.ts","source":"import { z } from 'zod';\nconst userSchema = z.object({ id: z.string() });"}],"fixedFiles":[],"focusPath":"src/user.ts","id":"recognizable-schema-name","outcome":"accept","title":"Mark the value as a schema"},{"expectedCount":1,"files":[{"path":"src/user.ts","source":"import { z } from 'zod';\nconst user = z.object({ id: z.string() });"}],"fixedFiles":[],"focusPath":"src/user.ts","id":"unmarked-schema-name","outcome":"reject","title":"Do not hide the schema behind a value name"}],"filePatterns":[],"id":"zod-naming-convention","key":"eslint:zod-naming-convention","languages":["typescript"],"limitations":[],"messageIds":["schemaSuffix","zPrefix","zodSchemaName"],"optionsSchema":{"additionalProperties":false,"properties":{"convention":{"enum":["prefix","suffix","either"],"type":"string"}},"type":"object"},"rationale":"A recognizable schema name distinguishes runtime validators from ordinary values at each use site.","references":[],"remediation":"Rename the schema with a `Z` prefix or `Schema` suffix, according to the configured convention.","since":null,"source":"packages/typescript/src/rules/zod-naming-convention.ts","status":"active","summary":"Enforce a consistent Zod schema naming convention — a `Z` prefix (`ZUser`) or a `Schema` suffix (`userSchema`); both are accepted by default.","test":"packages/typescript/tests/rules/zod-naming-convention.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ202","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"# resource \"google_storage_bucket\" \"old\" {\nresource \"google_storage_bucket\" \"current\" {}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"commented-resource","outcome":"reject","title":"Disabled Terraform resource"},{"expectedCount":0,"files":[{"path":"main.tf","source":"# Keep this bucket in us-central1 for data residency.\nresource \"google_storage_bucket\" \"records\" {}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"reason-comment","outcome":"accept","title":"Comment explaining an infrastructure constraint"}],"filePatterns":[],"id":"no-comment-cruft","key":"iac:no-comment-cruft","languages":["iac"],"limitations":["Commented assignments in tfvars files are allowed because they commonly document optional inputs.","Directives and heredoc bodies are excluded, and disabled HCL runs must be code-dominant."],"messageIds":[],"optionsSchema":null,"rationale":"Disabled declarations drift from executable infrastructure, while decorative banners duplicate structure already expressed by modules and resource blocks.","references":[],"remediation":"Delete disabled HCL and decorative dividers; retain only comments that explain a non-obvious reason.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_comment_cruft.py","status":"active","summary":"Commented-out Terraform/IaC or a section-banner comment — delete it; code carries the what, comments only the why.","test":"packages/iac/tests/rules/test_no_comment_cruft.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ201","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":0,"files":[{"path":"database.tf","source":"resource \"google_sql_database_instance\" \"main\" {\n  name                = \"prod\"\n  deletion_protection = true\n}\n"}],"fixedFiles":[],"focusPath":"database.tf","id":"protected-database","outcome":"accept","title":"Stateful database with provider deletion protection"},{"expectedCount":1,"files":[{"path":"database.tf","source":"resource \"google_sql_database_instance\" \"main\" {\n  name = \"prod\"\n}\n"}],"fixedFiles":[],"focusPath":"database.tf","id":"unguarded-database","outcome":"reject","title":"Stateful database without a deletion guard"}],"filePatterns":[],"id":"require-deletion-protection","key":"iac:require-deletion-protection","languages":["iac"],"limitations":["Only the curated resource types and provider guard spellings supported by the rule are analyzed.","Dynamic guard expressions are rejected because their protection cannot be proven statically."],"messageIds":[],"optionsSchema":null,"rationale":"Stateful services can lose durable production data when an accidental Terraform change or destroy is allowed to delete the backing resource.","references":[],"remediation":"Set the supported literal provider deletion guard or add lifecycle { prevent_destroy = true }.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/require_deletion_protection.py","status":"active","summary":"Stateful resource (Cloud SQL, GKE, BigQuery, RDS, ...) must set deletion_protection = true so a stray apply cannot destroy prod data.","test":"packages/iac/tests/rules/test_require_deletion_protection.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ203","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":0,"files":[{"path":"storage.tf","source":"resource \"google_storage_bucket\" \"records\" {\n  name = \"records\"\n  lifecycle {\n    prevent_destroy = true\n  }\n}\n"}],"fixedFiles":[],"focusPath":"storage.tf","id":"protected-bucket","outcome":"accept","title":"Irreplaceable bucket protected at plan time"},{"expectedCount":1,"files":[{"path":"storage.tf","source":"resource \"google_storage_bucket\" \"records\" {\n  name = \"records\"\n}\n"}],"fixedFiles":[],"focusPath":"storage.tf","id":"unguarded-bucket","outcome":"reject","title":"Irreplaceable bucket without a deletion guard"}],"filePatterns":[],"id":"require-prevent-destroy-on-irreplaceable","key":"iac:require-prevent-destroy-on-irreplaceable","languages":["iac"],"limitations":["Only the curated resource types and documented Google provider guards are recognized.","A literal force_destroy = true is treated as an explicit declaration that the resource is disposable."],"messageIds":[],"optionsSchema":null,"rationale":"Buckets, secrets, and registries contain state that is difficult or impossible to reconstruct after an accidental infrastructure destroy.","references":[],"remediation":"Use a supported literal provider deletion guard, or add lifecycle { prevent_destroy = true }.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/require_prevent_destroy.py","status":"active","summary":"Bucket, secret, or artifact registry must use a supported literal provider-side deletion guard or lifecycle { prevent_destroy = true }.","test":"packages/iac/tests/rules/test_require_prevent_destroy.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ065","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_rows.py","source":"def test_rows():\n    rows = fetch()\n    assert len(rows) == 3\n    for row in rows:\n        assert row.id > 0\n"}],"fixedFiles":[],"focusPath":"tests/test_rows.py","id":"collection-size-assertion","outcome":"accept","title":"Test first asserts the collection size"},{"expectedCount":1,"files":[{"path":"tests/test_rows.py","source":"def test_rows():\n    rows = fetch()\n    for row in rows:\n        assert row.id > 0\n"}],"fixedFiles":[],"focusPath":"tests/test_rows.py","id":"loop-only-assertion","outcome":"reject","title":"Assertion may never run"}],"filePatterns":[],"id":"conditional-assertion-in-test","key":"python:conditional-assertion-in-test","languages":["python"],"limitations":["Only collected tests are analyzed.","Exhaustive branches and explicit failure paths count as guaranteed checks."],"messageIds":[],"optionsSchema":null,"rationale":"Assertions confined to optional branches or loops let tests pass when a branch is skipped or a collection is empty.","references":[],"remediation":"Add an unconditional assertion, assert collection size before iterating, or make every branch assert or fail.","since":null,"source":"packages/python/src/sarj_python_lint/rules/conditional_assertion_in_test.py","status":"active","summary":"Tests should guarantee that at least one assertion runs on every execution path.","test":"packages/python/tests/rules/test_conditional_assertion_in_test.py"},{"aliases":["xfail-requires-strict"],"autofix":"none","category":"testing","code":"SARJ046","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_api.py","source":"import pytest\n\n@pytest.mark.xfail(reason=\"BUG: wrong status code\")\ndef test_status():\n    assert response_status() == 200\n"}],"fixedFiles":[],"focusPath":"tests/test_api.py","id":"non-strict-defect-pin","outcome":"reject","title":"Fixed defect would pass silently"},{"expectedCount":0,"files":[{"path":"tests/test_api.py","source":"import pytest\n\n@pytest.mark.xfail(reason=\"BUG: wrong status code\", strict=True)\ndef test_status():\n    assert response_status() == 200\n"}],"fixedFiles":[],"focusPath":"tests/test_api.py","id":"strict-defect-pin","outcome":"accept","title":"Fixed defect fails loudly"}],"filePatterns":[],"id":"defect-xfail-requires-strict","key":"python:defect-xfail-requires-strict","languages":["python"],"limitations":["Only xfail reasons that explicitly identify a defect are analyzed.","Nondeterministic, property-based, integration, network, and environment-gated tests are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A non-strict defect pin stays green after the defect is fixed, leaving stale coverage and markers behind.","references":[],"remediation":"Set `strict=True` so an unexpected pass fails and prompts removal of the obsolete marker.","since":null,"source":"packages/python/src/sarj_python_lint/rules/defect_xfail_requires_strict.py","status":"active","summary":"Bug-pinning `xfail` without `strict=True` — an XPASS reports as a pass and the pin rots.","test":"packages/python/tests/rules/test_defect_xfail_requires_strict.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ086","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/widgets.py","source":"def set_timeout_ms(timeout_ms: int) -> None:\n    \"\"\"Configure the request deadline.\n\n    Args:\n        timeout_ms: Timeout in ms\n    \"\"\"\n"}],"fixedFiles":[],"focusPath":"app/widgets.py","id":"argument-documents-unit","outcome":"accept","title":"Argument description records a unit"},{"expectedCount":1,"files":[{"path":"app/widgets.py","source":"def count_widgets(tenant_id: str) -> int:\n    \"\"\"Count active widgets.\n\n    Args:\n        tenant_id: Tenant ID\n    \"\"\"\n    return 0\n"}],"fixedFiles":[],"focusPath":"app/widgets.py","id":"argument-restates-signature","outcome":"reject","title":"Argument description repeats its name"}],"filePatterns":[],"id":"docstring-args-restate-signature","key":"python:docstring-args-restate-signature","languages":["python"],"limitations":["The rule reads Google-style argument sections and requires every documented entry to be a restatement before reporting.","Generated files, runtime-consumed prompt, CLI, and route docstrings, protected facts, and empty machine-generated stubs are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Repeating parameter names and types obscures useful behavioral contracts and drifts when signatures change.","references":[],"remediation":"Remove the redundant argument section, or retain it only to document constraints, units, defaults, or semantics absent from the signature.","since":null,"source":"packages/python/src/sarj_python_lint/rules/docstring_args_restate_signature.py","status":"active","summary":"Argument documentation must add facts beyond the function signature.","test":"packages/python/tests/rules/test_docstring_args_restate_signature.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ087","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/lines.py","source":"def get_line_length(line: list[str]) -> int:\n    \"\"\"Measure a rendered line.\n\n    Returns:\n        The width in terminal cells, which is not the character count.\n    \"\"\"\n    return len(line)\n"}],"fixedFiles":[],"focusPath":"app/lines.py","id":"return-documents-semantics","outcome":"accept","title":"Return description records semantics"},{"expectedCount":1,"files":[{"path":"app/lines.py","source":"def get_line_length(line: list[str]) -> int:\n    \"\"\"Measure a rendered line.\n\n    Returns:\n        int: The length of the line.\n    \"\"\"\n    return len(line)\n"}],"fixedFiles":[],"focusPath":"app/lines.py","id":"return-restates-signature","outcome":"reject","title":"Return description repeats the signature"}],"filePatterns":[],"id":"docstring-returns-restate-signature","key":"python:docstring-returns-restate-signature","languages":["python"],"limitations":["The rule reads Google-style return and yield sections and uses conservative signature-word matching.","Generated files, runtime-consumed docstrings, protected facts, identity semantics, and whole-docstring restatements owned by SARJ050 are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Repeating the return type or function name adds noise and can become stale without explaining the result's semantics.","references":[],"remediation":"Remove the redundant return section, or document identity, units, constraints, or other behavior absent from the signature.","since":null,"source":"packages/python/src/sarj_python_lint/rules/docstring_returns_restate_signature.py","status":"active","summary":"Return documentation must add facts beyond the function name and annotation.","test":"packages/python/tests/rules/test_docstring_returns_restate_signature.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ066","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_permissions.py","source":"def test_admin():\n    user = make_user(\"admin\")\n    allowed = can_delete(user)\n    assert allowed\n\ndef test_editor():\n    user = make_user(\"editor\")\n    allowed = can_delete(user)\n    assert allowed\n"}],"fixedFiles":[],"focusPath":"tests/test_permissions.py","id":"copy-pasted-tests","outcome":"reject","title":"Two tests differ only in input literals"},{"expectedCount":0,"files":[{"path":"tests/test_permissions.py","source":"import pytest\n\n@pytest.mark.parametrize(\"role\", [\"admin\", \"editor\"], ids=[\"admin\", \"editor\"])\ndef test_can_delete(role):\n    user = make_user(role)\n    allowed = can_delete(user)\n    assert allowed\n"}],"fixedFiles":[],"focusPath":"tests/test_permissions.py","id":"parameterized-cases","outcome":"accept","title":"Inputs share one parameterized test"}],"filePatterns":[],"id":"duplicate-test-body","key":"python:duplicate-test-body","languages":["python"],"limitations":["Only substantial sibling test bodies in one non-generated module are compared.","Meaningful docstring or comment differences keep tests distinct."],"messageIds":[],"optionsSchema":null,"rationale":"Copy-pasted tests drift independently and obscure the input dimension that changes behavior.","references":[],"remediation":"Collapse the copies into `pytest.mark.parametrize` cases with descriptive `ids`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/duplicate_test_body.py","status":"active","summary":"Similar test bodies should be represented as one named parameterized case table.","test":"packages/python/tests/rules/test_duplicate_test_body.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ084","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/store.py","source":"class Store:\n    def get(self, key: str) -> str:\n        \"\"\"Get a value by key.\"\"\"\n        return key\n\nclass MemoryStore(Store):\n    def get(self, key: str) -> str:\n        \"\"\"Get a value by key.\"\"\"\n        return key\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"copied-override-docstring","outcome":"reject","title":"Override repeats its base method documentation"},{"expectedCount":0,"files":[{"path":"app/store.py","source":"class Store:\n    def get(self, key: str) -> str:\n        \"\"\"Get a value by key.\"\"\"\n        return key\n\nclass ReplicaStore(Store):\n    def get(self, key: str) -> str:\n        \"\"\"Get a value from the read replica.\"\"\"\n        return key\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"override-specific-docstring","outcome":"accept","title":"Override documents its distinct behavior"}],"filePatterns":[],"id":"duplicated-override-docstring","key":"python:duplicated-override-docstring","languages":["python"],"limitations":["Only methods whose base class is defined under an undotted name in the same file are compared.","Overloads, generated files, undocumented bases, and methods whose docstring is their entire body are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Inherited documentation is already discoverable, while a duplicate adds a second copy that can drift.","references":[],"remediation":"Delete the copied docstring, or rewrite it only when the override has behavior-specific information to add.","since":null,"source":"packages/python/src/sarj_python_lint/rules/duplicated_override_docstring.py","status":"active","summary":"Remove an override docstring copied verbatim from its local base method.","test":"packages/python/tests/rules/test_duplicated_override_docstring.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ094","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.get('/users', summary='Read users', description='Returns visible users.', status_code=200)\nasync def users() -> list[UserResponse]:\n    return []\n"}],"fixedFiles":[],"focusPath":"api.py","id":"documented-operation","outcome":"accept","title":"Operation with an explicit OpenAPI contract"},{"expectedCount":1,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.get('/users')\nasync def users() -> list[UserResponse]:\n    return []\n"}],"fixedFiles":[],"focusPath":"api.py","id":"missing-operation-metadata","outcome":"reject","title":"Visible operation without required metadata"}],"filePatterns":[],"id":"fastapi-openapi-contract","key":"python:fastapi-openapi-contract","languages":["python"],"limitations":["Hidden routes, WebSocket handlers, tests, generated files, and unrelated decorators are excluded.","Dynamic response mappings are accepted when their contents cannot be resolved statically.","Imported dependency aliases are followed only through unique, nonsymlinked relative or same-package modules inside the detected checkout; traversal is bounded and ambiguity remains diagnostic."],"messageIds":[],"optionsSchema":null,"rationale":"Complete route metadata keeps generated OpenAPI accurate for clients, validation, and review.","references":[],"remediation":"Declare route metadata, typed parameters, response schemas, and documented alternate responses.","since":null,"source":"packages/python/src/sarj_python_lint/rules/fastapi_openapi_contract.py","status":"active","summary":"FastAPI operations must publish explicit request, response, and OpenAPI contracts.","test":"packages/python/tests/rules/test_fastapi_openapi_contract.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ044","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/conftest.py","source":"import pytest\n\n@pytest.fixture\ndef stores():\n    return Stores(org=org_store, user=user_store)\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"fixture-returns-named-value","outcome":"accept","title":"Fixture fields are named"},{"expectedCount":1,"files":[{"path":"tests/conftest.py","source":"import pytest\n\n@pytest.fixture\ndef stores():\n    return org_store, user_store\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"fixture-returns-tuple","outcome":"reject","title":"Fixture fields are positional"}],"filePatterns":[],"id":"fixture-returns-bare-tuple","key":"python:fixture-returns-bare-tuple","languages":["python"],"limitations":["Only pytest and pytest-asyncio fixtures in test paths are analyzed.","Factory closures and single-field tuples are allowed."],"messageIds":[],"optionsSchema":null,"rationale":"Positional fixture results make call sites opaque and allow reordered fields to bind incorrectly.","references":[],"remediation":"Return a `NamedTuple`, frozen dataclass, or another value whose fields have stable names.","since":null,"source":"packages/python/src/sarj_python_lint/rules/fixture_returns_bare_tuple.py","status":"active","summary":"Fixture returns a bare multi-field tuple — return a NamedTuple so consumers destructure by name.","test":"packages/python/tests/rules/test_fixture_returns_bare_tuple.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ063","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_notify.py","source":"def test_notify():\n    notify(mailer, audit, user)\n    mailer.send.assert_called_once_with(user.email)\n    audit.record.assert_called_once_with(\"notified\")\n"}],"fixedFiles":[],"focusPath":"tests/test_notify.py","id":"interaction-only","outcome":"reject","title":"Test checks only collaborator calls"},{"expectedCount":0,"files":[{"path":"tests/test_notify.py","source":"def test_notify():\n    result = notify(mailer, audit, user)\n    mailer.send.assert_called_once_with(user.email)\n    audit.record.assert_called_once_with(\"notified\")\n    assert result.status == \"sent\"\n"}],"fixedFiles":[],"focusPath":"tests/test_notify.py","id":"observable-outcome","outcome":"accept","title":"Test checks the returned notification"}],"filePatterns":[],"id":"interaction-only-test","key":"python:interaction-only-test","languages":["python"],"limitations":["Only collected tests with multiple mock targets and no outcome assertion are reported."],"messageIds":[],"optionsSchema":null,"rationale":"Interaction-only assertions pin implementation call sequences without proving useful behavior.","references":[],"remediation":"Assert returned data, persisted state, emitted output, or another observable outcome.","since":null,"source":"packages/python/src/sarj_python_lint/rules/interaction_only_test.py","status":"active","summary":"Tests should verify outcomes, not only mock interaction bookkeeping.","test":"packages/python/tests/rules/test_interaction_only_test.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ400","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, Field\n\nclass RetryPolicy(BaseModel):\n    attempts: int = Field(default=1, gt=0)\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"default-satisfies-lower-bound","outcome":"accept","title":"Default satisfies the declared field bounds"},{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, Field\n\nclass RetryPolicy(BaseModel):\n    attempts: int = Field(default=0, gt=0)\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"default-violates-lower-bound","outcome":"reject","title":"Default is outside the declared field bounds"}],"filePatterns":[],"id":"invalid-pydantic-field-default","key":"python:invalid-pydantic-field-default","languages":["python"],"limitations":["The rule checks direct public fields on classes that directly inherit Pydantic `BaseModel`.","It reports only statically provable literal conflicts with nullability, `Literal` domains, and numeric or string-length bounds.","Test and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"An invalid default lets a model begin with a value that contradicts its annotation or field bounds, moving a deterministic configuration error into runtime validation.","references":[],"remediation":"Choose a default allowed by the annotation and every literal `Field` bound, or widen the contract when the value is intentional.","since":null,"source":"packages/python/src/sarj_python_lint/rules/invalid_pydantic_field_default.py","status":"active","summary":"Require literal Pydantic `Field` defaults to satisfy their declared contract.","test":"packages/python/tests/rules/test_invalid_pydantic_field_default.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ045","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_call.py","source":"def build_call(**overrides):\n    return Call(**overrides)\n\ndef test_first():\n    assert build_call(a=1)\n"}],"fixedFiles":[],"focusPath":"tests/test_call.py","id":"construction-through-builder","outcome":"accept","title":"Tests override builder defaults"},{"expectedCount":2,"files":[{"path":"tests/test_call.py","source":"def test_first():\n    assert Call(a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8, i=9)\n\ndef test_second():\n    assert Call(a=1, b=2, c=3, d=4, e=5, f=6, g=7, h=8, i=9)\n"}],"fixedFiles":[],"focusPath":"tests/test_call.py","id":"repeated-wide-construction","outcome":"reject","title":"Tests repeat every constructor argument"}],"filePatterns":[],"id":"kwarg-heavy-construction-in-test","key":"python:kwarg-heavy-construction-in-test","languages":["python"],"limitations":["Only repeated calls with more than eight named arguments directly inside test functions are reported.","Mapping construction, mock assertions, fixtures, and local helper calls are allowed."],"messageIds":[],"optionsSchema":null,"rationale":"Repeated construction boilerplate hides the field each test changes and makes schema changes noisy.","references":[],"remediation":"Extract a test builder with sensible defaults and override only values relevant to each case.","since":null,"source":"packages/python/src/sarj_python_lint/rules/kwarg_heavy_construction_in_test.py","status":"active","summary":"Object built with many keywords inline in a test — extract a helper with defaults.","test":"packages/python/tests/rules/test_kwarg_heavy_construction_in_test.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ040","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_service.py","source":"from unittest.mock import Mock\n\ndef test_service():\n    client = Mock(spec=Client)\n    assert client\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"mock-with-spec","outcome":"accept","title":"Mock follows the collaborator contract"},{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"from unittest.mock import Mock\n\ndef test_service():\n    client = Mock()\n    assert client\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"mock-without-contract","outcome":"reject","title":"Mock accepts any attribute"}],"filePatterns":[],"id":"mock-without-spec","key":"python:mock-without-spec","languages":["python"],"limitations":["Only test files and statically resolved `unittest.mock` or pytest-mock constructors are analyzed.","Mocks used only for their built-in assertion API are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"An unrestricted mock keeps accepting calls after the real collaborator's interface changes.","references":[],"remediation":"Pass `spec=`, `spec_set=`, or `autospec=True`, or use a small fake implementing the real contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/mock_without_spec.py","status":"active","summary":"Mock built without `spec=`/`autospec=` — it accepts any attribute and cannot rot loudly.","test":"packages/python/tests/rules/test_mock_without_spec.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ020","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/call_store.py","source":"QUERY = \"SELECT id FROM call ORDER BY created_at LIMIT 50\"\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"bounded-postgres-query","outcome":"accept","title":"Postgres store query reads bounded rows"},{"expectedCount":1,"files":[{"path":"app/call_store.py","source":"QUERY = \"SELECT COUNT(*) FROM call\"\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"postgres-aggregate-query","outcome":"reject","title":"Postgres store query performs aggregation"}],"filePatterns":[],"id":"no-aggregation-in-store-query","key":"python:no-aggregation-in-store-query","languages":["python"],"limitations":["Only SQL string literals in recognized store modules are analyzed.","Files and queries identified as ClickHouse or BigQuery are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Analytical aggregation competes with transactional reads and is better served by the columnar mirror.","references":[],"remediation":"Run aggregation in ClickHouse or BigQuery and keep Postgres store queries focused on point or bounded reads.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_aggregation_in_store_query.py","status":"active","summary":"Postgres store queries should not perform analytical aggregation.","test":"packages/python/tests/rules/test_no_aggregation_in_store_query.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ016","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"service.py","source":"value = load()\n# return value\nsave(value)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"commented-out-code","outcome":"reject","title":"Dead code preserved as a comment"},{"expectedCount":0,"files":[{"path":"service.py","source":"value = load()\n# Keep this ordering because Clerk caches the first lookup.\nsave(value)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"rationale-comment","outcome":"accept","title":"Comment records an external constraint"}],"filePatterns":[],"id":"no-comment-cruft","key":"python:no-comment-cruft","languages":["python"],"limitations":["Only standalone comments are classified; trailing comments, docstrings, directives, and referenced notes are excluded.","Generated files, license headers, doctests, grammar illustrations, and Sphinx configuration banners are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Mechanical narration and dead code obscure the constraints and rationale that comments should preserve.","references":[],"remediation":"Delete the cruft and keep only concise comments that explain a non-obvious reason or constraint.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_comment_cruft.py","status":"active","summary":"Comment repeats code, preserves dead code, or adds a decorative section marker.","test":"packages/python/tests/rules/test_no_comment_cruft.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ028","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/main.py","source":"from fastapi.middleware.cors import CORSMiddleware\napp.add_middleware(\n    CORSMiddleware,\n    allow_origins=[\"https://app.example.com\"],\n    allow_credentials=True,\n)\n"}],"fixedFiles":[],"focusPath":"app/main.py","id":"credentialed-trusted-origin","outcome":"accept","title":"Credentials restricted to a trusted origin"},{"expectedCount":1,"files":[{"path":"app/main.py","source":"from fastapi.middleware.cors import CORSMiddleware\napp.add_middleware(CORSMiddleware, allow_origins=[\"*\"], allow_credentials=True)\n"}],"fixedFiles":[],"focusPath":"app/main.py","id":"credentialed-wildcard-origin","outcome":"reject","title":"Credentials allowed for every origin"}],"filePatterns":[],"id":"no-cors-wildcard-with-credentials","key":"python:no-cors-wildcard-with-credentials","languages":["python"],"limitations":["The rule requires literal `True` for `allow_credentials` and either a literal `\"*\"` below `allow_origins` or an exact universal `allow_origin_regex` literal.","Dynamically computed credential flags and origin collections are not resolved."],"messageIds":[],"optionsSchema":null,"rationale":"Reflecting any origin while allowing credentials lets an untrusted site read authenticated responses.","references":[],"remediation":"Replace the wildcard with an explicit list of trusted origins.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_cors_wildcard_with_credentials.py","status":"active","summary":"Credentialed CORS must not allow a wildcard origin.","test":"packages/python/tests/rules/test_no_cors_wildcard_with_credentials.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ098","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"diagnostics/__init__.py","source":"__all__ = [\"Diagnostic\", \"AnalysisReport\", \"Diagnostic\"]\n"}],"fixedFiles":[],"focusPath":"diagnostics/__init__.py","id":"duplicate-package-export","outcome":"reject","title":"Duplicate name in a package export list"},{"expectedCount":0,"files":[{"path":"diagnostics/__init__.py","source":"__all__ = [\"Diagnostic\", \"AnalysisReport\"]\n"}],"fixedFiles":[],"focusPath":"diagnostics/__init__.py","id":"unique-package-exports","outcome":"accept","title":"Unique names in a package export list"}],"filePatterns":[],"id":"no-duplicate-dunder-all-entry","key":"python:no-duplicate-dunder-all-entry","languages":["python"],"limitations":["Only one fully static list or tuple assigned to package-level `__all__` is analyzed.","Generated, dynamically extended, reassigned, and non-package declarations are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Duplicate exports add noise to a package's public contract and commonly reveal copy-paste mistakes in generated or maintained facade lists.","references":[],"remediation":"Remove each later duplicate while preserving the first declaration of the exported name.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_duplicate_dunder_all_entry.py","status":"active","summary":"static package `__all__` declarations should list each exported name once","test":"packages/python/tests/rules/test_no_duplicate_dunder_all_entry.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ007","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/service.py","source":"try:\n    a = load_a()\n    b = load_b()\n    c = load_c()\n    d = load_d()\nexcept ValueError:\n    recover()\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"broad-exception-boundary","outcome":"reject","title":"One handler covers four raising operations"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"a = load_a()\nb = load_b()\nc = load_c()\ntry:\n    d = load_d()\nexcept ValueError:\n    recover()\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"narrow-exception-boundary","outcome":"accept","title":"Handler covers only the relevant operation"}],"filePatterns":[],"id":"no-fat-try-blocks","key":"python:no-fat-try-blocks","languages":["python"],"limitations":["Only top-level statements containing plausibly raising operations count toward the limit of three.","Generated files, `try` statements with `else` or `finally`, and handlers that always re-raise are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A broad `try` body can route unrelated failures into a handler that was written for one operation.","references":[],"remediation":"Move unrelated operations outside the `try`, or split the body into smaller exception boundaries.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_fat_try_blocks.py","status":"active","summary":"Keep a `try` body narrow enough to identify which operation a handler covers.","test":"packages/python/tests/rules/test_no_fat_try_blocks.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ054","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/service.py","source":"# ruff: noqa: TID251\nimport os\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"file-wide-escape-hatch","outcome":"reject","title":"File suppresses every banned API use"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"from unittest import mock  # noqa: TID251 — vendor SDK boundary\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"reasoned-inline-suppression","outcome":"accept","title":"One use is suppressed with a reason"}],"filePatterns":[],"id":"no-file-level-escape-hatch-noqa","key":"python:no-file-level-escape-hatch-noqa","languages":["python"],"limitations":["Detection covers file-level Ruff noqa directives naming configured escape-hatch codes.","Inline noqa comments and file-level suppressions for mechanical rules are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A file-wide suppression silently authorizes future uses that were never reviewed.","references":[],"remediation":"Suppress each intentional use inline with the exact code and a reason.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_file_level_escape_hatch_noqa.py","status":"active","summary":"File-level Ruff noqa suppresses an escape-hatch rule across the entire file.","test":"packages/python/tests/rules/test_no_file_level_escape_hatch_noqa.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ038","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"# ruff: noqa: E501\nimport os\n"}],"fixedFiles":[],"focusPath":"service.py","id":"scoped-ruff-suppression","outcome":"accept","title":"Ruff suppression names its code"},{"expectedCount":1,"files":[{"path":"service.py","source":"# ruff: noqa\nimport os\n"}],"fixedFiles":[],"focusPath":"service.py","id":"unscoped-ruff-suppression","outcome":"reject","title":"Ruff disabled for the file"}],"filePatterns":[],"id":"no-file-level-suppression","key":"python:no-file-level-suppression","languages":["python"],"limitations":["Only Ruff, mypy-compatible `type: ignore`, and Pyright file-level directives are analyzed.","Trailing per-line suppressions and directives with an explicit code list are allowed."],"messageIds":[],"optionsSchema":null,"rationale":"Blanket suppressions also hide diagnostics introduced by future tool upgrades.","references":[],"remediation":"Fix the findings or limit the directive to the specific diagnostic codes being suppressed.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_file_level_suppression.py","status":"active","summary":"Unscoped file-level suppressions disable a checker for the entire file, including diagnostics added later.","test":"packages/python/tests/rules/test_no_file_level_suppression.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ048","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":".git/keep","source":"fixture\n"},{"path":"python/core/core/__init__.py","source":"\n"},{"path":"python/core/core/helpers.py","source":"def _decode(value):\n    return value\n"},{"path":"python/core/pyproject.toml","source":"[project]\nname = \"core\"\n"},{"path":"python/service/pyproject.toml","source":"[project]\nname = \"service\"\n"},{"path":"python/service/service/__init__.py","source":"\n"},{"path":"python/service/tests/test_api.py","source":"from core.helpers import _decode\n"}],"fixedFiles":[],"focusPath":"python/service/tests/test_api.py","id":"cross-package-private-import","outcome":"reject","title":"Service imports another package's private helper"},{"expectedCount":0,"files":[{"path":".git/keep","source":"fixture\n"},{"path":"python/core/core/__init__.py","source":"\n"},{"path":"python/core/core/helpers.py","source":"def decode(value):\n    return value\n"},{"path":"python/core/pyproject.toml","source":"[project]\nname = \"core\"\n"},{"path":"python/service/pyproject.toml","source":"[project]\nname = \"service\"\n"},{"path":"python/service/service/__init__.py","source":"\n"},{"path":"python/service/tests/test_api.py","source":"from core.helpers import decode\n"}],"fixedFiles":[],"focusPath":"python/service/tests/test_api.py","id":"cross-package-public-import","outcome":"accept","title":"Service imports a public helper"}],"filePatterns":[],"id":"no-first-party-private-import","key":"python:no-first-party-private-import","languages":["python"],"limitations":["First-party ownership is resolved from repository package manifests and source trees.","Relative imports, public and dunder names, third-party and standard-library imports, and supported compiled extensions are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Cross-package private imports couple callers to internals instead of a public surface the owning package can maintain.","references":[],"remediation":"Export the capability under a public name or move the caller behind an existing public function.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_first_party_private_import.py","status":"active","summary":"Code imports a private name or module from another first-party package.","test":"packages/python/tests/rules/test_no_first_party_private_import.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ401","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, ConfigDict, model_validator\n\nclass Counter(BaseModel):\n    model_config = ConfigDict(frozen=True)\n    value: int\n\n    @model_validator(mode=\"after\")\n    def normalize(self):\n        self.value = abs(self.value)\n        return self\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"after-validator-mutates-frozen-field","outcome":"reject","title":"After-validator assigns a frozen model field"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, ConfigDict, model_validator\n\nclass Counter(BaseModel):\n    model_config = ConfigDict(frozen=True)\n    value: int\n\n    @model_validator(mode=\"after\")\n    def require_non_negative(self):\n        if self.value < 0:\n            raise ValueError(\"value must be non-negative\")\n        return self\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"after-validator-only-validates-frozen-field","outcome":"accept","title":"After-validator checks without mutating the model"}],"filePatterns":[],"id":"no-frozen-after-validator-field-write","key":"python:no-frozen-after-validator-field-write","languages":["python"],"limitations":["The rule checks direct public fields on direct Pydantic `BaseModel` subclasses configured with literal `ConfigDict(frozen=True)`.","It detects direct assignment, annotated assignment, augmented assignment, and tuple or list destructuring through the validator's receiver.","Indirect mutation through method calls, `setattr`, or `object.__setattr__` is outside its scope.","Test and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Direct field assignment contradicts the model's frozen contract and can fail only when the validator runs, making construction behavior surprising and brittle.","references":[],"remediation":"Validate without mutation, compute the value before constructing the model, or return an explicitly updated model when replacement is part of the design.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_frozen_after_validator_field_write.py","status":"active","summary":"Do not assign declared fields in after-validators on frozen Pydantic models.","test":"packages/python/tests/rules/test_no_frozen_after_validator_field_write.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ053","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/store.py","source":"SQL = \"CREATE TABLE call (id UUID PRIMARY KEY DEFAULT gen_random_uuid())\"\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"random-uuid-default","outcome":"reject","title":"Table defaults to a random UUID"},{"expectedCount":0,"files":[{"path":"app/store.py","source":"SQL = \"CREATE TABLE call (id UUID PRIMARY KEY DEFAULT uuidv7())\"\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"uuidv7-default","outcome":"accept","title":"Table defaults to UUIDv7"}],"filePatterns":[],"id":"no-gen-random-uuid-in-sql","key":"python:no-gen-random-uuid-in-sql","languages":["python"],"limitations":["Detection covers SQL-shaped Python string literals after masking SQL comments and quoted values.","Known UUIDv7 compatibility implementations that internally call gen_random_uuid() are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Random UUIDv4 primary keys scatter inserts across B-tree pages; UUIDv7 keys preserve time ordering.","references":[],"remediation":"Use uuidv7() where the supported PostgreSQL version provides it.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_gen_random_uuid_in_sql.py","status":"active","summary":"Embedded SQL calls gen_random_uuid() instead of uuidv7().","test":"packages/python/tests/rules/test_no_gen_random_uuid_in_sql.py"},{"aliases":["single-public-export"],"autofix":"none","category":"architecture","code":"SARJ022","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"utils.py","source":"def snake_case_text(value: str) -> str: ...\n"}],"fixedFiles":[],"focusPath":"utils.py","id":"generic-single-export-module","outcome":"reject","title":"Generic module contains one public definition"},{"expectedCount":0,"files":[{"path":"pagination.py","source":"class InvalidPaginationCursorError(Exception): ...\n"}],"fixedFiles":[],"focusPath":"pagination.py","id":"specific-single-export-module","outcome":"accept","title":"Specific module contains one public definition"}],"filePatterns":[],"id":"no-generic-single-export-module","key":"python:no-generic-single-export-module","languages":["python"],"limitations":["Only known generic module stems with exactly one public definition are reported.","Framework-owned, generated, test, and package initializer paths are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Names such as `utils` and `helpers` hide a module's responsibility and encourage unrelated additions.","references":[],"remediation":"Rename the module to the snake_case name of its sole public class or function.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_generic_single_export_module.py","status":"active","summary":"A generic module with one public definition should be named after that definition.","test":"packages/python/tests/rules/test_no_generic_single_export_module.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ095","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/__init__.py","source":"\n"},{"path":"app/config.py","source":"from pydantic_settings import BaseSettings\nclass Settings(BaseSettings):\n    MODEL: str = 'model'\nsettings = Settings()\n"},{"path":"app/service.py","source":"from app.config import settings\n\nclass Generator:\n    def __init__(self, *, model: str | None = None) -> None:\n        self.model = model or settings.MODEL\n\ngenerator = Generator(model='explicit')\n"},{"path":"pyproject.toml","source":"[project]\nname = 'example'\nversion = '0.1.0'\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"ambient-settings-fallback","outcome":"reject","title":"Constructor reads an implicit default from settings"},{"expectedCount":0,"files":[{"path":"app/__init__.py","source":"\n"},{"path":"app/config.py","source":"from pydantic_settings import BaseSettings\nclass Settings(BaseSettings):\n    MODEL: str = 'model'\nsettings = Settings()\n"},{"path":"app/service.py","source":"class Generator:\n    def __init__(self, *, model: str) -> None:\n        self.model = model\n\ngenerator = Generator(model='explicit')\n"},{"path":"pyproject.toml","source":"[project]\nname = 'example'\nversion = '0.1.0'\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"explicit-constructor-dependency","outcome":"accept","title":"Constructor requires its dependency"}],"filePatterns":[],"id":"no-hidden-constructor-fallback","key":"python:no-hidden-constructor-fallback","languages":["python"],"limitations":["Detection requires a proven local settings provider, a keyword-only optional parameter, and a first-party composition call.","Tests, generated files, migrations, descriptors, library environment fallbacks, and unconstructed classes are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Hidden configuration lookup obscures dependencies and makes construction vary with ambient application state.","references":[],"remediation":"Require the constructor argument and resolve any default at the composition root or call site.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_hidden_constructor_fallback.py","status":"active","summary":"Constructor option silently falls back to application settings when omitted.","test":"packages/python/tests/rules/test_no_hidden_constructor_fallback.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ003","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/events.py","source":"class Created: ...\nclass Deleted: ...\n\ndef name(event):\n    if isinstance(event, Created):\n        return 'created'\n    elif isinstance(event, Deleted):\n        return 'deleted'\n    else:\n        raise AssertionError\n"}],"fixedFiles":[],"focusPath":"app/events.py","id":"local-union-isinstance-chain","outcome":"reject","title":"Closed local union dispatched with isinstance"},{"expectedCount":0,"files":[{"path":"app/events.py","source":"from typing import assert_never\n\nclass Created: ...\nclass Deleted: ...\n\ndef name(event):\n    match event:\n        case Created():\n            return 'created'\n        case Deleted():\n            return 'deleted'\n        case unreachable:\n            assert_never(unreachable)\n"}],"fixedFiles":[],"focusPath":"app/events.py","id":"local-union-match","outcome":"accept","title":"Closed local union dispatched with match"}],"filePatterns":[],"id":"no-isinstance-union-chain","key":"python:no-isinstance-union-chain","languages":["python"],"limitations":["The rule requires at least two locally defined class arms over the same expression and a terminal fallback.","Builtin, imported, abstract collection, and open-ended dispatch types are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"An `isinstance` chain does not let a type checker prove that every member of a closed union is handled.","references":[],"remediation":"Replace the chain with `match` cases and pass the unreachable remainder to `assert_never`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_isinstance_union_chain.py","status":"active","summary":"Use exhaustive pattern matching for dispatch over a local closed class union.","test":"packages/python/tests/rules/test_no_isinstance_union_chain.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ091","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app.py","source":"\"\"\"One fact. Two facts. Three facts. Four facts.\n\nFive facts. Six facts. Seven facts. Eight facts.\n\"\"\"\n"}],"fixedFiles":[],"focusPath":"app.py","id":"structured-documentation","outcome":"accept","title":"Long documentation split into paragraphs"},{"expectedCount":1,"files":[{"path":"app.py","source":"\"\"\"One fact. Two facts. Three facts. Four facts. Five facts. Six facts. Seven facts. Eight facts.\"\"\"\n"}],"fixedFiles":[],"focusPath":"app.py","id":"unstructured-prose-wall","outcome":"reject","title":"Eight-sentence prose wall"}],"filePatterns":[],"id":"no-long-comment","key":"python:no-long-comment","languages":["python"],"limitations":["The warning threshold is eight sentence units and applies only to module, private-class, and untyped or private-function docstrings.","Typed public APIs, runtime-consumed schemas and prompts, generated files, typed sections, and structured documentation are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"An unstructured prose wall is difficult to scan and often hides a contract that belongs in clearer code or durable structured documentation.","references":[],"remediation":"Clarify the code or restructure the docstring with paragraphs, lists, code, paths, links, or other meaningful technical anchors.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_long_comment.py","status":"active","summary":"Long docstrings must use deliberate documentation structure or technical anchors.","test":"packages/python/tests/rules/test_no_long_comment.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ025","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"QUERY = \"SELECT id FROM task WHERE id > :cursor ORDER BY id LIMIT :limit\"\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"keyset-page-query","outcome":"accept","title":"Page selected after the last ordered key"},{"expectedCount":1,"files":[{"path":"app/task_store.py","source":"QUERY = \"SELECT id FROM task ORDER BY id LIMIT :limit OFFSET :offset\"\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"offset-page-query","outcome":"reject","title":"Page selected with an offset"}],"filePatterns":[],"id":"no-offset-pagination","key":"python:no-offset-pagination","languages":["python"],"limitations":["Only SQL string literals in recognized store modules are analyzed.","Dynamic SQL, comments, prose, and BigQuery `WITH OFFSET AS` array indexing are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Large offsets require scanning discarded rows and can skip or repeat results during concurrent writes.","references":[],"remediation":"Filter on the ordered key after the last result, then apply `ORDER BY` and `LIMIT`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_offset_pagination.py","status":"active","summary":"Store queries should use keyset cursors instead of `OFFSET` pagination.","test":"packages/python/tests/rules/test_no_offset_pagination.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ056","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/store.py","source":"def build(args):\n    conditions = []\n    if args.organization_id:\n        conditions.append(SQL(\"organization_id = %s\"))\n    return conditions\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"conditional-tenant-clause","outcome":"reject","title":"Tenant clause depends on an optional filter"},{"expectedCount":0,"files":[{"path":"app/store.py","source":"def build(args):\n    conditions = [SQL(\"organization_id = %s\")]\n    return conditions\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"required-tenant-clause","outcome":"accept","title":"Tenant clause is unconditional"}],"filePatterns":[],"id":"no-optional-tenant-predicate","key":"python:no-optional-tenant-predicate","languages":["python"],"limitations":["Detection follows SQL fragments inside each function and recognizes configured tenant column names.","Test files and functions containing any unconditional tenant predicate are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Fail-open tenant filtering can expose rows across organizations when a tenant value is absent.","references":[],"remediation":"Require the tenant identifier or seed the query with its tenant predicate unconditionally.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_optional_tenant_predicate.py","status":"active","summary":"Tenant predicate is added only conditionally, allowing an unscoped query.","test":"packages/python/tests/rules/test_no_optional_tenant_predicate.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ019","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/task_store.py","source":"QUERY = \"SELECT a.id FROM a JOIN b ON TRUE JOIN c ON TRUE JOIN d ON TRUE\"\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"three-table-joins","outcome":"reject","title":"Query with three joins"},{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"QUERY = \"SELECT a.id FROM a JOIN b ON TRUE JOIN c ON TRUE\"\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"two-table-joins","outcome":"accept","title":"Query with two joins"}],"filePatterns":[],"id":"no-query-with-many-joins","key":"python:no-query-with-many-joins","languages":["python"],"limitations":["Only SQL string literals in recognized store modules are analyzed.","The rule counts join syntax; it does not estimate a database query plan."],"messageIds":[],"optionsSchema":null,"rationale":"Wide join graphs couple store reads to many tables and make query cost and schema changes harder to control.","references":[],"remediation":"Split the read into focused store operations or denormalize data needed together.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_query_with_many_joins.py","status":"active","summary":"Store queries should use at most two explicit or implicit joins.","test":"packages/python/tests/rules/test_no_query_with_many_joins.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ036","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_call_store.py","source":"async def test_create(conn):\n    await conn.execute(\"INSERT INTO call (id) VALUES (1)\")\n"}],"fixedFiles":[],"focusPath":"tests/test_call_store.py","id":"raw-insert-test-seed","outcome":"reject","title":"Test seeds data with raw SQL"},{"expectedCount":0,"files":[{"path":"tests/test_call_store.py","source":"async def test_create(store):\n    await store.insert(make_call())\n"}],"fixedFiles":[],"focusPath":"tests/test_call_store.py","id":"store-api-test-seed","outcome":"accept","title":"Test seeds data through the store API"}],"filePatterns":[],"id":"no-raw-sql-in-tests","key":"python:no-raw-sql-in-tests","languages":["python"],"limitations":["Only literal `INSERT INTO` statements passed to known execution methods in test files are reported.","Fixtures in `conftest.py`, migrations, dynamic SQL, reads, updates, and cleanup statements are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Raw inserts bypass the validation, defaults, events, and invariants exercised by production writes.","references":[],"remediation":"Create test records through the owning store or service API.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_raw_sql_in_tests.py","status":"active","summary":"Tests should seed records through store or service methods instead of raw SQL inserts.","test":"packages/python/tests/rules/test_no_raw_sql_in_tests.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ024","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"queries.py","source":"def load():\n    return \"SELECT id, name, created_at FROM organization\"\n\ndef refresh():\n    return \"SELECT id, name, created_at FROM organization\"\n"}],"fixedFiles":[],"focusPath":"queries.py","id":"repeated-sql-across-functions","outcome":"reject","title":"SQL literal is copied across functions"},{"expectedCount":0,"files":[{"path":"queries.py","source":"QUERY = \"SELECT id, name, created_at FROM organization\"\n\ndef load():\n    return QUERY\n\ndef refresh():\n    return QUERY\n"}],"fixedFiles":[],"focusPath":"queries.py","id":"shared-sql-module-constant","outcome":"accept","title":"Functions reuse one SQL constant"}],"filePatterns":[],"id":"no-repeated-string-literal","key":"python:no-repeated-string-literal","languages":["python"],"limitations":["Only structured literals of at least 40 characters repeated across distinct functions are reported.","Prose, documentation scaffolding, annotations, generated files, and repeated f-string fragments are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Independent copies of SQL, route templates, and identifier-like strings can drift while appearing equivalent.","references":[],"remediation":"Extract the shared value to one named module-level constant and reference it from each function.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_repeated_string_literal.py","status":"active","summary":"Structured string literals repeated across functions should use a module constant.","test":"packages/python/tests/rules/test_no_repeated_string_literal.py"},{"aliases":[],"autofix":"suggestion","category":"maintainability","code":"SARJ049","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"def load_profile(profile_id):\n    # The replica may lag after signup, so read from primary.\n    return get_profile_by_id(profile_id)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"comment-explains-constraint","outcome":"accept","title":"Comment adds operational context"},{"expectedCount":1,"files":[{"path":"service.py","source":"def load_profile(profile_id):\n    # Get profile by ID\n    return get_profile_by_id(profile_id)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"restated-action","outcome":"reject","title":"Comment repeats the action"}],"filePatterns":[],"id":"no-restated-comment","key":"python:no-restated-comment","languages":["python"],"limitations":["Detection uses conservative lexical heuristics for short standalone comments above simple actions.","Generated files, directives, protected comments, section labels, and comments adding novel context are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Comments that repeat code add reading cost and can become stale without explaining intent.","references":[],"remediation":"Delete the comment or replace it with context the statement cannot express.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_restated_comment.py","status":"active","summary":"Comment restates the statement immediately below it.","test":"packages/python/tests/rules/test_no_restated_comment.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ012","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"logger.info('request', token_prefix=token[:6])\n"}],"fixedFiles":[],"focusPath":"service.py","id":"redacted-logging-keyword","outcome":"accept","title":"Token prefix logged under a redacted name"},{"expectedCount":1,"files":[{"path":"service.py","source":"logger.info('request', token=token)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"secret-logging-keyword","outcome":"reject","title":"Raw token passed to a logger"}],"filePatterns":[],"id":"no-secret-in-log","key":"python:no-secret-in-log","languages":["python"],"limitations":["Detection covers keyword arguments on logger-shaped receivers and known logging methods.","Positional values, message interpolation, and values under non-secret keyword names are not inspected."],"messageIds":[],"optionsSchema":null,"rationale":"Raw credentials in logs can spread to durable sinks and readers outside the request boundary.","references":[],"remediation":"Omit the secret or log a deliberately redacted derivative under a redaction-specific name.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_secret_in_log.py","status":"active","summary":"Secret-like value is passed to a logging call under a secret-like keyword.","test":"packages/python/tests/rules/test_no_secret_in_log.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ021","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/call_store.py","source":"QUERY = \"SELECT id, status FROM call\"\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"explicit-store-projection","outcome":"accept","title":"Store query selects named columns"},{"expectedCount":1,"files":[{"path":"app/call_store.py","source":"QUERY = \"SELECT * FROM call\"\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"wildcard-store-projection","outcome":"reject","title":"Store query selects every column"}],"filePatterns":[],"id":"no-select-star","key":"python:no-select-star","languages":["python"],"limitations":["Only SQL string literals in recognized store modules are analyzed.","The rule cannot infer the intended projection for an automatic fix."],"messageIds":[],"optionsSchema":null,"rationale":"Wildcard projections over-fetch data and can silently change row shapes when the schema evolves.","references":[],"remediation":"List every column consumed by the store result mapping.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_select_star.py","status":"active","summary":"Store queries should select explicit columns instead of `*`.","test":"packages/python/tests/rules/test_no_select_star.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ009","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"def load():\n    try:\n        return risky()\n    except Exception:\n        logger.warning('load failed')\n        return None\n"}],"fixedFiles":[],"focusPath":"service.py","id":"observable-sentinel-return","outcome":"accept","title":"Exception logged before returning None"},{"expectedCount":1,"files":[{"path":"service.py","source":"def load():\n    try:\n        return risky()\n    except Exception:\n        return None\n"}],"fixedFiles":[],"focusPath":"service.py","id":"silent-sentinel-return","outcome":"reject","title":"Exception silently converted to None"}],"filePatterns":[],"id":"no-sentinel-return-on-except","key":"python:no-sentinel-return-on-except","languages":["python"],"limitations":["Only final empty or false sentinel returns and bare `except: pass` handlers are reported.","Handlers that re-raise, log, print the exception, or implement a recognized result contract are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Unobservable sentinel returns erase failure context and make error handling ambiguous for callers.","references":[],"remediation":"Re-raise the exception, log it before returning, or expose failure through a typed result.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_sentinel_return_on_except.py","status":"active","summary":"Exception handler silently converts a failure into a sentinel return value.","test":"packages/python/tests/rules/test_no_sentinel_return_on_except.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ031","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_worker.py","source":"async def test_worker_finishes():\n    finished = start_worker()\n    await finished.wait()\n    assert worker_finished()\n"}],"fixedFiles":[],"focusPath":"tests/test_worker.py","id":"event-synchronized-test","outcome":"accept","title":"Test waits for the completion signal"},{"expectedCount":1,"files":[{"path":"tests/test_worker.py","source":"import asyncio\n\nasync def test_worker_finishes():\n    start_worker()\n    await asyncio.sleep(0.1)\n    assert worker_finished()\n"}],"fixedFiles":[],"focusPath":"tests/test_worker.py","id":"fixed-test-sleep","outcome":"reject","title":"Test waits a fixed duration"}],"filePatterns":[],"id":"no-sleep-in-test-body","key":"python:no-sleep-in-test-body","languages":["python"],"limitations":["Only test paths and calls resolved to imported `time.sleep` or `asyncio.sleep` are analyzed.","Zero, computed, helper-owned, and bounded polling-loop sleeps are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A fixed nonzero sleep makes test reliability and runtime depend on machine and CI timing.","references":[],"remediation":"Await the operation, wait on an event, or poll the expected condition with a bounded timeout.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_sleep_in_test_body.py","status":"active","summary":"Tests should synchronize on observable state instead of waiting a fixed duration.","test":"packages/python/tests/rules/test_no_sleep_in_test_body.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ052","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"from loguru import logger\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"house-logger-import","outcome":"accept","title":"Application imports loguru"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"import logging\n\nlogger = logging.getLogger(__name__)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"stdlib-logging-import","outcome":"reject","title":"Application imports stdlib logging"}],"filePatterns":[],"id":"no-stdlib-logging","key":"python:no-stdlib-logging","languages":["python"],"limitations":["Tests, scripts, notebooks, generated files, type-only imports, and recognized loguru bridge configuration are excluded.","Detection reports imports of the standard-library logging root, not similarly named first-party modules."],"messageIds":[],"optionsSchema":null,"rationale":"Parallel logger hierarchies can bypass shared formatting, redaction, levels, sinks, and error reporting.","references":[],"remediation":"Import the configured loguru logger; keep stdlib logging only in the explicit bridge module.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_stdlib_logging.py","status":"active","summary":"Application code imports standard-library logging instead of the configured house logger.","test":"packages/python/tests/rules/test_no_stdlib_logging.py"},{"aliases":["inefficient-string-concat-in-loop"],"autofix":"none","category":"performance","code":"SARJ002","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/render.py","source":"def render(items):\n    lines = []\n    for item in items:\n        lines.append(str(item))\n    return \"\\n\".join(lines)\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"join-string-fragments","outcome":"accept","title":"Fragments joined after the loop"},{"expectedCount":1,"files":[{"path":"app/render.py","source":"def render(items):\n    result = \"\"\n    for item in items:\n        result += f\"{item}\\n\"\n    return result\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"string-growth-in-loop","outcome":"reject","title":"String accumulator grown on every iteration"}],"filePatterns":[],"id":"no-string-concat-in-loop","key":"python:no-string-concat-in-loop","languages":["python"],"limitations":["The rule requires syntax that establishes string-like growth and excludes generated files.","Subscript targets, per-iteration reinitialization, and intermediate values consumed by the loop are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Repeated string growth copies the accumulated value on each iteration and can take quadratic time.","references":[],"remediation":"Append each fragment to a list and join the fragments once after the loop.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_string_concat_in_loop.py","status":"active","summary":"Do not grow one string with repeated concatenation inside a loop.","test":"packages/python/tests/rules/test_no_string_concat_in_loop.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ057","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"def test_service():\n    assert True\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"literal-only-assertion","outcome":"reject","title":"Assertion always passes"},{"expectedCount":0,"files":[{"path":"tests/test_service.py","source":"def test_service(result):\n    assert result == 1\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"runtime-value-assertion","outcome":"accept","title":"Assertion checks runtime output"}],"filePatterns":[],"id":"no-tautological-expect","key":"python:no-tautological-expect","languages":["python"],"limitations":["Detection covers truthy literal asserts and supported unittest methods with literal-only operands.","Always-failing markers, benchmark tests, deliberate match-arm markers, and runtime-value comparisons are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"An always-passing assertion cannot verify runtime behavior and can hide a missing comparison.","references":[],"remediation":"Assert against a value produced by the code under test.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_tautological_expect.py","status":"active","summary":"Assertion outcome is fixed entirely by literal values.","test":"packages/python/tests/rules/test_no_tautological_expect.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ092","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"publisher.py","source":"def publish(message: str, *, retry: bool) -> None:\n    \"\"\"Publish one message.\n\n    Args:\n        message: Wire payload retained for the audit record.\n        retry: Whether a prior partial write may be attempted again.\n    \"\"\"\n"}],"fixedFiles":[],"focusPath":"publisher.py","id":"behavioral-parameter-contract","outcome":"accept","title":"Argument section records behavior"},{"expectedCount":1,"files":[{"path":"app.py","source":"def decode(value: str) -> dict[str, object]:\n    \"\"\"Decode the value.\n\n    Args:\n        value (str): Text to decode.\n    \"\"\"\n    return {}\n"}],"fixedFiles":[],"focusPath":"app.py","id":"parameter-type-restatement","outcome":"reject","title":"Parameter type repeats the annotation"}],"filePatterns":[],"id":"no-typed-doc-sections","key":"python:no-typed-doc-sections","languages":["python"],"limitations":["Only fully typed functions are checked, and a documented type must match the signature before it is reported.","Runtime-consumed prompt, CLI, and route docstrings and untyped or partially typed signatures are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Duplicated type spellings drift from annotations and add noise without strengthening the behavioral contract.","references":[],"remediation":"Remove the repeated type while retaining behavioral facts, constraints, units, and error conditions.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_typed_doc_sections.py","status":"active","summary":"Docstring sections must not repeat types already present in a fully typed signature.","test":"packages/python/tests/rules/test_no_typed_doc_sections.py"},{"aliases":["parametrize-case-needs-id"],"autofix":"none","category":"testing","code":"SARJ042","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_handler.py","source":"import pytest\n\n@pytest.mark.parametrize(\"payload\", [{\"a\": 1}, {\"a\": 2}], ids=[\"first\", \"second\"])\ndef test_handler(payload):\n    assert handle(payload)\n"}],"fixedFiles":[],"focusPath":"tests/test_handler.py","id":"opaque-cases-with-ids","outcome":"accept","title":"Dictionary cases have stable names"},{"expectedCount":1,"files":[{"path":"tests/test_handler.py","source":"import pytest\n\n@pytest.mark.parametrize(\"payload\", [{\"a\": 1}, {\"a\": 2}])\ndef test_handler(payload):\n    assert handle(payload)\n"}],"fixedFiles":[],"focusPath":"tests/test_handler.py","id":"opaque-cases-without-ids","outcome":"reject","title":"Dictionary cases receive generated names"}],"filePatterns":[],"id":"opaque-parametrize-case-needs-id","key":"python:opaque-parametrize-case-needs-id","languages":["python"],"limitations":["Only static list or tuple parameter tables in test files are analyzed.","Cases with a pytest-readable scalar column or an explicit ID are allowed."],"messageIds":[],"optionsSchema":null,"rationale":"Generated case numbers are hard to diagnose and silently change when the parameter table is reordered.","references":[],"remediation":"Add `ids=` to the decorator or give each opaque `pytest.param` an explicit `id=`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py","status":"active","summary":"Opaque `parametrize` case with no `ids=`/`id=` — the failing case reports as `case0`.","test":"packages/python/tests/rules/test_opaque_parametrize_case_needs_id.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ062","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_service.py","source":"from unittest.mock import patch\n\n@patch(\"app.payment_gateway\")\ndef test_run(gateway):\n    assert run() == 1\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"external-boundary-only","outcome":"accept","title":"Test patches one external boundary"},{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"from unittest.mock import patch\n\n@patch(\"app.mod0.collaborator\")\n@patch(\"app.mod1.collaborator\")\n@patch(\"app.mod2.collaborator\")\n@patch(\"app.mod3.collaborator\")\n@patch(\"app.mod4.collaborator\")\n@patch(\"app.mod5.collaborator\")\ndef test_run(a, b, c, d, e, f):\n    assert run() == 1\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"six-patched-collaborators","outcome":"reject","title":"Test patches six collaborators"}],"filePatterns":[],"id":"over-mocked-test","key":"python:over-mocked-test","languages":["python"],"limitations":["Only collected tests are analyzed; configuration knobs do not count as collaborators."],"messageIds":[],"optionsSchema":null,"rationale":"Broad mock wiring couples tests to implementation details while exercising little production behavior.","references":[],"remediation":"Use real dependencies or a higher-level test harness and mock only true external boundaries.","since":null,"source":"packages/python/src/sarj_python_lint/rules/over_mocked_test.py","status":"active","summary":"Tests should not replace more than five distinct collaborators.","test":"packages/python/tests/rules/test_over_mocked_test.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ013","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/task_store.py","source":"from psycopg.rows import dict_row\n\ncursor = connection.cursor(row_factory=dict_row)\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"dictionary-row-factory","outcome":"reject","title":"Cursor returns unvalidated dictionaries"},{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"from psycopg.rows import class_row\n\ncursor = connection.cursor(row_factory=class_row(Task))\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"validated-class-row-factory","outcome":"accept","title":"Cursor validates rows into a model"}],"filePatterns":[],"id":"prefer-class-row","key":"python:prefer-class-row","languages":["python"],"limitations":["The rule matches any `row_factory` keyword whose value ends in `dict_row`.","Ad hoc or dynamically selected row shapes require a local suppression when a class row is unsuitable."],"messageIds":[],"optionsSchema":null,"rationale":"Dictionary rows cross the database boundary without validating field names or values against a model.","references":[],"remediation":"Pass `class_row(Model)` as the row factory for queries that return a stable model shape.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_class_row.py","status":"active","summary":"Use a validated model row instead of Psycopg `dict_row`.","test":"packages/python/tests/rules/test_prefer_class_row.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ011","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"auth.py","source":"import hmac\n\ndef authenticated(token, expected):\n    return hmac.compare_digest(token, expected)\n"}],"fixedFiles":[],"focusPath":"auth.py","id":"constant-time-token-comparison","outcome":"accept","title":"Token compared in constant time"},{"expectedCount":1,"files":[{"path":"auth.py","source":"def authenticated(token, expected):\n    return token == expected\n"}],"fixedFiles":[],"focusPath":"auth.py","id":"direct-token-comparison","outcome":"reject","title":"Token compared with equality"}],"filePatterns":[],"id":"prefer-constant-time-secret-compare","key":"python:prefer-constant-time-secret-compare","languages":["python"],"limitations":["Detection depends on authenticator-shaped identifier names and selected cryptographic imports.","Tests, equality methods, literals, container membership, and existing digest comparisons are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Direct equality can reveal authenticator contents through data-dependent comparison timing.","references":[],"remediation":"Compare secret values with `hmac.compare_digest` or `secrets.compare_digest`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py","status":"active","summary":"Secret-like values are compared with timing-sensitive equality operators.","test":"packages/python/tests/rules/test_prefer_constant_time_secret_compare.py"},{"aliases":[],"autofix":"none","category":"style","code":"SARJ068","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"src/render.py","source":"def greeting(name: str) -> str:\n    return f\"Hello, {name}!\"\n"}],"fixedFiles":[],"focusPath":"src/render.py","id":"formatted-string","outcome":"accept","title":"Interpolation uses an f-string"},{"expectedCount":1,"files":[{"path":"src/render.py","source":"def greeting(name: str) -> str:\n    return \"Hello, \" + name + \"!\"\n"}],"fixedFiles":[],"focusPath":"src/render.py","id":"literal-string-concatenation","outcome":"reject","title":"Known string is joined to literals"}],"filePatterns":[],"id":"prefer-fstring-over-concat","key":"python:prefer-fstring-over-concat","languages":["python"],"limitations":["Runtime operands must have concrete string evidence.","Logging, SQL, lazy strings, ORM expressions, templates, and generated or skill utility files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"F-strings keep interpolation and spacing visible and avoid redundant string coercion.","references":[],"remediation":"Replace the concatenation with one f-string; use `str.join` when many operands form a sequence.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_fstring_over_concat.py","status":"active","summary":"Build short strings with f-strings instead of concatenating literals and known strings.","test":"packages/python/tests/rules/test_prefer_fstring_over_concat.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ096","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"settings.py","source":"ROLE_NAMES = (\"admin\", \"member\")\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"immutable-module-constant","outcome":"accept","title":"Immutable tuple used for a module constant"},{"expectedCount":1,"files":[{"path":"settings.py","source":"ROLE_NAMES = [\"admin\", \"member\"]\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"mutable-module-constant","outcome":"reject","title":"Mutable collection exposed as a module constant"}],"filePatterns":[],"id":"prefer-immutable-module-constant","key":"python:prefer-immutable-module-constant","languages":["python"],"limitations":["Empty collections and collections intentionally mutated or passed to unknown calls are not reported.","Test and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A constant-looking mutable collection can be changed by any importer, so its value depends on process history rather than the module's source.","references":[],"remediation":"Use a tuple for ordered values, a frozenset for membership, or an immutable mapping for keyed values.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_immutable_module_constant.py","status":"active","summary":"module-level constant collections expose mutable shared state; use tuple, frozenset, or an immutable mapping","test":"packages/python/tests/rules/test_prefer_immutable_module_constant.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ059","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/fakes/s3.py","source":"class FakeS3Client:\n    def put_object(self, **kwargs):\n        return kwargs\n\n    def get_object(self, **kwargs):\n        return kwargs\n\n    def delete_object(self, **kwargs):\n        return kwargs\n"}],"fixedFiles":[],"focusPath":"tests/fakes/s3.py","id":"hand-rolled-s3-client","outcome":"reject","title":"Test defines its own S3 client"},{"expectedCount":0,"files":[{"path":"tests/test_upload.py","source":"from moto import mock_aws\n\n@mock_aws\ndef test_upload():\n    assert upload_with_real_client()\n"}],"fixedFiles":[],"focusPath":"tests/test_upload.py","id":"maintained-s3-fake","outcome":"accept","title":"Test uses the maintained AWS fake"}],"filePatterns":[],"id":"prefer-library-fake","key":"python:prefer-library-fake","languages":["python"],"limitations":["Only test and shared-double paths are analyzed.","Only recognized external services and substantial hand-rolled doubles are reported."],"messageIds":[],"optionsSchema":null,"rationale":"Hand-written doubles model only remembered protocol behavior and can let invalid requests pass.","references":[],"remediation":"Use the recognized library fake, emulator, or test container while keeping the production client.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_library_fake.py","status":"active","summary":"Tests should use maintained service fakes or emulators instead of hand-rolled third-party doubles.","test":"packages/python/tests/rules/test_prefer_library_fake.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ032","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"dispatch.py","source":"from kinds import Kind\n\ndef handle(kind):\n    match kind:\n        case Kind.A:\n            handle_a()\n        case Kind.B:\n            handle_b()\n        case _:\n            raise AssertionError(kind)\n"}],"fixedFiles":[],"focusPath":"dispatch.py","id":"explicit-closed-set-failure","outcome":"accept","title":"Closed-set match rejects an unhandled variant"},{"expectedCount":1,"files":[{"path":"dispatch.py","source":"from kinds import Kind\n\ndef handle(kind):\n    match kind:\n        case Kind.A:\n            handle_a()\n        case Kind.B:\n            handle_b()\n        case _:\n            pass\n"}],"fixedFiles":[],"focusPath":"dispatch.py","id":"silent-closed-set-wildcard","outcome":"reject","title":"Closed-set match silently ignores a variant"}],"filePatterns":[],"id":"prefer-match-assert-never","key":"python:prefer-match-assert-never","languages":["python"],"limitations":["The rule recognizes closed sets from local classes, enums, imported member owners, and static handler maps.","Guarded matches, dynamically grown maps, and open-ended value domains are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A silent wildcard, `else`, or incomplete dispatch map lets newly added variants pass unnoticed.","references":[],"remediation":"Handle every variant and use `assert_never` or an explicit exception for the unreachable fallthrough.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_match_assert_never.py","status":"active","summary":"Closed-set dispatch should fail explicitly when a variant is unhandled.","test":"packages/python/tests/rules/test_prefer_match_assert_never.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ080","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/parser.py","source":"def parse(value: object):\n    match value:\n        case None | Unset():\n            return value\n        case int():\n            return str(value)\n        case _:\n            return None\n"}],"fixedFiles":[],"focusPath":"app/parser.py","id":"match-type-cases","outcome":"accept","title":"Parser names value shapes with match arms"},{"expectedCount":1,"files":[{"path":"app/parser.py","source":"def parse(value: object):\n    if value is None:\n        return value\n    if isinstance(value, Unset):\n        return value\n    if isinstance(value, int):\n        return str(value)\n    return None\n"}],"fixedFiles":[],"focusPath":"app/parser.py","id":"sequential-type-guards","outcome":"reject","title":"Parser dispatches through sequential guards"}],"filePatterns":[],"id":"prefer-match-type-dispatch","key":"python:prefer-match-type-dispatch","languages":["python"],"limitations":["The rule targets several measured shapes, including control-flow raises, sequential guards, and repeated `isinstance` dispatch.","Generated files and code that shadows `isinstance` are excluded; test files omit the control-flow-raise check."],"messageIds":[],"optionsSchema":null,"rationale":"Pattern matching makes type cases and sentinel cases explicit without control-flow exceptions or repeated dispatch checks.","references":[],"remediation":"Replace the detected guard or exception-driven dispatch with `match` arms for each supported value shape.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_match_type_dispatch.py","status":"active","summary":"Use `match` for explicit runtime type dispatch instead of branching parser machinery.","test":"packages/python/tests/rules/test_prefer_match_type_dispatch.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ039","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"service.py","source":"def handle(value):\n    allowed = [\"a\", \"b\", \"c\"]\n    return value in allowed\n"}],"fixedFiles":[],"focusPath":"service.py","id":"list-built-per-call","outcome":"reject","title":"Static list rebuilt in a function"},{"expectedCount":0,"files":[{"path":"service.py","source":"ALLOWED = [\"a\", \"b\", \"c\"]\n\ndef handle(value):\n    return value in ALLOWED\n"}],"fixedFiles":[],"focusPath":"service.py","id":"module-level-list","outcome":"accept","title":"Static list defined once"}],"filePatterns":[],"id":"prefer-module-level-constant","key":"python:prefer-module-level-constant","languages":["python"],"limitations":["Test and generated files are excluded.","Only proven literal-only collections of at least three elements and constant `re.compile` calls are reported."],"messageIds":[],"optionsSchema":null,"rationale":"Rebuilding immutable data or a constant regex on every call wastes work and obscures its static nature.","references":[],"remediation":"Define the value once at module scope and reference that constant from the function.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_module_level_constant.py","status":"active","summary":"Literal-only collections and compiled regular expressions built inside a function should be module-level constants.","test":"packages/python/tests/rules/test_prefer_module_level_constant.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ026","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/profile.py","source":"from typing import NamedTuple\n\nclass Profile(NamedTuple):\n    name: str\n    age: int\n\ndef load_profile() -> Profile:\n    return Profile('Ada', 42)\n"}],"fixedFiles":[],"focusPath":"app/profile.py","id":"named-public-return","outcome":"accept","title":"Public function returns a named record"},{"expectedCount":1,"files":[{"path":"app/profile.py","source":"def load_profile() -> tuple[str, int]:\n    return 'Ada', 42\n"}],"fixedFiles":[],"focusPath":"app/profile.py","id":"positional-public-return","outcome":"reject","title":"Public function returns a positional pair"}],"filePatterns":[],"id":"prefer-namedtuple-over-tuple-return","key":"python:prefer-namedtuple-over-tuple-return","languages":["python"],"limitations":["Generated files, tests, private functions, nested functions, and declared overrides are excluded.","Only fixed multi-item tuple annotations or inferred tuple-literal returns are reported."],"messageIds":[],"optionsSchema":null,"rationale":"A positional tuple hides field meaning and lets callers silently swap or misread values.","references":[],"remediation":"Return a `NamedTuple`, frozen dataclass, or frozen validation model with named fields.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py","status":"active","summary":"Public functions should return named records instead of fixed positional tuples.","test":"packages/python/tests/rules/test_prefer_namedtuple_over_tuple_return.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ093","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/services/files.py","source":"def move(file_id: FileId, parent_folder_id: FolderId) -> None: ...\n"}],"fixedFiles":[],"focusPath":"app/services/files.py","id":"nominal-id-roles","outcome":"accept","title":"Distinct ID roles use nominal types"},{"expectedCount":1,"files":[{"path":"app/services/files.py","source":"def move(file_id: str, parent_folder_id: str) -> None: ...\n"}],"fixedFiles":[],"focusPath":"app/services/files.py","id":"primitive-id-roles","outcome":"reject","title":"Distinct ID roles share a primitive type"}],"filePatterns":[],"id":"prefer-nominal-id-types","key":"python:prefer-nominal-id-types","languages":["python"],"limitations":["The rule checks public module functions, classes, direct methods, and constructors with at least two ID-shaped roles.","Tests, generated code, migrations, helpers, external adapters, operational IDs, raw schemas, ambiguous containers, and private callbacks are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Primitive carriers such as str, int, and UUID allow distinct identifier roles to be swapped without a type-checking error.","references":[],"remediation":"Define or reuse NewType identifier types and propagate them through the boundary.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_nominal_id_types.py","status":"active","summary":"Production boundaries with multiple ID roles must distinguish them with nominal types.","test":"packages/python/tests/rules/test_prefer_nominal_id_types.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ082","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/resolver.py","source":"def resolve(candidates: list[str] | None = None) -> list[str]:\n    return candidates or []\n"}],"fixedFiles":[],"focusPath":"app/resolver.py","id":"equivalent-empty-list-states","outcome":"reject","title":"Nullable list is immediately normalized"},{"expectedCount":0,"files":[{"path":"app/resolver.py","source":"def resolve(candidates: list[str]) -> list[str]:\n    return candidates\n"}],"fixedFiles":[],"focusPath":"app/resolver.py","id":"required-list-input","outcome":"accept","title":"Required list has one empty state"}],"filePatterns":[],"id":"prefer-non-nullable-collection","key":"python:prefer-non-nullable-collection","languages":["python"],"limitations":["Only module functions and constructors with a nullable list defaulted to `None` are analyzed.","Overrides, tests, generated code, nested captures, multiple reads, and uses that preserve `None` as a distinct state are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Exposing two equivalent empty states expands the function contract without preserving meaningful information.","references":[],"remediation":"Require the list, or accept an immutable empty default such as `Sequence[T] = ()` and materialize a list internally.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_non_nullable_collection.py","status":"active","summary":"Avoid nullable list parameters when local use proves `None` and an empty list are equivalent.","test":"packages/python/tests/rules/test_prefer_non_nullable_collection.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ070","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/settings.py","source":"def configure(value):\n    match value:\n        case LocalSettings():\n            return build(value)\n        case RemoteSettings():\n            return build(value)\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"duplicate-match-arms","outcome":"reject","title":"Adjacent match arms repeat one body"},{"expectedCount":0,"files":[{"path":"app/settings.py","source":"def configure(value):\n    match value:\n        case LocalSettings() | RemoteSettings():\n            return build(value)\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"shared-or-pattern-arm","outcome":"accept","title":"One or-pattern owns the shared body"}],"filePatterns":[],"id":"prefer-or-pattern","key":"python:prefer-or-pattern","languages":["python"],"limitations":["Only adjacent, unguarded, refutable arms with structurally identical non-empty bodies are compared.","Arms with different bound names or comments are excluded because merging can change meaning or intent."],"messageIds":[],"optionsSchema":null,"rationale":"An or-pattern expresses shared handling once and prevents identical arms from drifting apart.","references":[],"remediation":"Join the equivalent patterns with `|` and keep their shared body under the merged arm.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_or_pattern.py","status":"active","summary":"Merge adjacent `case` arms with identical bodies into one or-pattern.","test":"packages/python/tests/rules/test_prefer_or_pattern.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ058","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/fakes/user_store.py","source":"class FakeUserStore(UserStore):\n    def __init__(self):\n        self.rows = {}\n\n    def add(self, user):\n        self.rows[user.id] = user\n\n    def get(self, user_id):\n        return self.rows.get(user_id)\n"}],"fixedFiles":[],"focusPath":"tests/fakes/user_store.py","id":"container-backed-store-double","outcome":"reject","title":"Test reimplements a store with a dictionary"},{"expectedCount":0,"files":[{"path":"tests/fakes/user_store.py","source":"class FailingUserStore(PsqlUserStore):\n    def add(self, user):\n        raise OSError(\"database unavailable\")\n"}],"fixedFiles":[],"focusPath":"tests/fakes/user_store.py","id":"real-store-subclass","outcome":"accept","title":"Test subclasses the real store to inject a failure"}],"filePatterns":[],"id":"prefer-real-store-in-tests","key":"python:prefer-real-store-in-tests","languages":["python"],"limitations":["Only test and shared-double paths are analyzed.","The class must resemble a relational persistence double backed by a mutable container or hollow methods."],"messageIds":[],"optionsSchema":null,"rationale":"Container-backed store doubles omit database constraints, transactions, ordering, and concurrency semantics.","references":[],"remediation":"Run the real store against a test database and subclass it only when a test must inject a failure.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_real_store_in_tests.py","status":"active","summary":"Tests should exercise the real persistence implementation instead of an in-memory reimplementation.","test":"packages/python/tests/rules/test_prefer_real_store_in_tests.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ097","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/settings.py","source":"# Request deadline in seconds.\nREQUEST_DEADLINE = 10\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"comment-only-constant-unit","outcome":"reject","title":"Comment is the only source of the unit"},{"expectedCount":0,"files":[{"path":"app/settings.py","source":"# Request deadline in seconds.\nREQUEST_DEADLINE_SECONDS = 10\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"unit-bearing-constant-name","outcome":"accept","title":"Constant name carries its unit"}],"filePatterns":[],"id":"prefer-self-documenting-constant","key":"python:prefer-self-documenting-constant","languages":["python"],"limitations":["Only direct module and class constants with attached comments and proven numeric or HTTP-status shapes are analyzed.","Generated code, directives, ambiguous comments, policy sentinels, and values already carrying the fact are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A comment-only fact is lost at use sites and can drift independently from the constant it describes.","references":[],"remediation":"Add the unit to the name or type, use a unit-bearing value such as `timedelta`, or replace status integers with `HTTPStatus` members.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_self_documenting_constant.py","status":"active","summary":"Encode a constant's units or HTTP status meaning in its name, type, or value.","test":"packages/python/tests/rules/test_prefer_self_documenting_constant.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ078","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/builder.py","source":"class Builder:\n    def set_name(self, name: str) -> \"Builder\":\n        self.name = name\n        return self\n"}],"fixedFiles":[],"focusPath":"app/builder.py","id":"enclosing-class-return","outcome":"reject","title":"Fluent method names its enclosing class"},{"expectedCount":0,"files":[{"path":"app/builder.py","source":"from typing import Self\n\nclass Builder:\n    def set_name(self, name: str) -> Self:\n        self.name = name\n        return self\n"}],"fixedFiles":[],"focusPath":"app/builder.py","id":"self-return-annotation","outcome":"accept","title":"Fluent method preserves subclass type"}],"filePatterns":[],"id":"prefer-self-type-annotation","key":"python:prefer-self-type-annotation","languages":["python"],"limitations":["Only methods that directly return `self` or classmethods that directly return `cls(...)` are analyzed.","Annotations naming other classes and methods without a return annotation are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"`Self` preserves the concrete subclass type, while naming the enclosing class narrows inherited return types incorrectly.","references":[],"remediation":"Import `Self` from `typing` and use it as the return annotation.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_self_type_annotation.py","status":"active","summary":"Annotate fluent methods and alternate constructors with `Self`.","test":"packages/python/tests/rules/test_prefer_self_type_annotation.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ006","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/order.py","source":"class Order:\n    statuses = (\"pending\", \"shipped\")\n    status: str = \"pending\"\n"}],"fixedFiles":[],"focusPath":"app/order.py","id":"raw-string-choice-field","outcome":"reject","title":"String field backed by a closed choice collection"},{"expectedCount":0,"files":[{"path":"app/order.py","source":"from enum import StrEnum\n\nclass Status(StrEnum):\n    PENDING = \"pending\"\n    SHIPPED = \"shipped\"\n\nclass Order:\n    status: Status = Status.PENDING\n"}],"fixedFiles":[],"focusPath":"app/order.py","id":"string-enum-field","outcome":"accept","title":"Closed domain represented by a string enum"}],"filePatterns":[],"id":"prefer-str-enum","key":"python:prefer-str-enum","languages":["python"],"limitations":["The rule requires corroborating choice collections, comparison clusters, or repeated literal domains.","Tests, generated code, external vocabularies, and open-ended name domains receive conservative exemptions."],"messageIds":[],"optionsSchema":null,"rationale":"A named closed domain lets type checking and review catch invalid values and incomplete handling.","references":[],"remediation":"Define a `StrEnum` for model fields, or reuse one named `Literal` alias across transparent builders.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_str_enum.py","status":"active","summary":"Represent corroborated closed string domains with `StrEnum` or a named `Literal` alias.","test":"packages/python/tests/rules/test_prefer_str_enum.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ015","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"models.py","source":"import collections\n\nRow = collections.namedtuple('Row', ['id', 'name'])\n"}],"fixedFiles":[],"focusPath":"models.py","id":"collections-namedtuple","outcome":"reject","title":"Functional untyped namedtuple"},{"expectedCount":0,"files":[{"path":"models.py","source":"from typing import NamedTuple\n\nclass Row(NamedTuple):\n    id: int\n    name: str\n"}],"fixedFiles":[],"focusPath":"models.py","id":"typed-named-tuple","outcome":"accept","title":"Typed NamedTuple declaration"}],"filePatterns":[],"id":"prefer-struct-over-namedtuple","key":"python:prefer-struct-over-namedtuple","languages":["python"],"limitations":["Only imports from `collections` and qualified calls through `collections` bindings are reported.","Tests, `typing.NamedTuple`, unrelated attributes, and unbound bare calls are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Typed record declarations expose field types and make construction and review less error-prone.","references":[],"remediation":"Declare a `typing.NamedTuple` class or use a frozen pydantic model for boundary values.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py","status":"active","summary":"`collections.namedtuple` creates an untyped, positionally constructed record.","test":"packages/python/tests/rules/test_prefer_struct_over_namedtuple.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ014","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"scheduler.py","source":"def schedule(timeout_seconds: int) -> None: ...\n"}],"fixedFiles":[],"focusPath":"scheduler.py","id":"numeric-duration-parameter","outcome":"reject","title":"Seconds represented as an integer"},{"expectedCount":0,"files":[{"path":"scheduler.py","source":"from datetime import timedelta\n\ndef schedule(timeout: timedelta) -> None: ...\n"}],"fixedFiles":[],"focusPath":"scheduler.py","id":"timedelta-duration-parameter","outcome":"accept","title":"Duration represented as timedelta"}],"filePatterns":[],"id":"prefer-timedelta-for-durations","key":"python:prefer-timedelta-for-durations","languages":["python"],"limitations":["Detection relies on duration-shaped names and numeric type annotations.","Tests, generated files, CLI parameters, settings fields, counts, rates, calendar units, and timestamps are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A `timedelta` makes the unit explicit and prevents incompatible duration values from mixing silently.","references":[],"remediation":"Use `datetime.timedelta` at the typed boundary and convert only at external interfaces.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_timedelta_for_durations.py","status":"active","summary":"Duration-bearing name is typed as a raw integer or float.","test":"packages/python/tests/rules/test_prefer_timedelta_for_durations.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ076","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/values.py","source":"def collect(values):\n    return [result for value in values if (result := compute(value))]\n"}],"fixedFiles":[],"focusPath":"app/values.py","id":"bound-comprehension-result","outcome":"accept","title":"Comprehension evaluates the call once"},{"expectedCount":1,"files":[{"path":"app/values.py","source":"def collect(values):\n    return [compute(value) for value in values if compute(value)]\n"}],"fixedFiles":[],"focusPath":"app/values.py","id":"repeated-comprehension-call","outcome":"reject","title":"Comprehension evaluates one call twice"}],"filePatterns":[],"id":"prefer-walrus-comprehension-filter","key":"python:prefer-walrus-comprehension-filter","languages":["python"],"limitations":["Only single-generator comprehensions inside callable bodies are analyzed.","Attribute reads, type-narrowing builtins, differing calls, and filters already using a named expression are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Calling the same function in both the filter and result duplicates work and can repeat side effects.","references":[],"remediation":"Bind the result to a fresh meaningful name in the filter and use that name in the comprehension result.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py","status":"active","summary":"Evaluate a repeated comprehension call once with a named expression.","test":"packages/python/tests/rules/test_prefer_walrus_comprehension_filter.py"},{"aliases":[],"autofix":"none","category":"style","code":"SARJ081","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/parser.py","source":"import re\n\ndef first_number(text: str) -> str | None:\n    match = re.search(r\"\\d+\", text)\n    if match:\n        return match.group(0)\n    return None\n"}],"fixedFiles":[],"focusPath":"app/parser.py","id":"assigned-regex-result","outcome":"reject","title":"Regex result is assigned before its condition"},{"expectedCount":0,"files":[{"path":"app/parser.py","source":"import re\n\ndef first_number(text: str) -> str | None:\n    if match := re.search(r\"\\d+\", text):\n        return match.group(0)\n    return None\n"}],"fixedFiles":[],"focusPath":"app/parser.py","id":"conditional-regex-binding","outcome":"accept","title":"Regex result is bound in its condition"}],"filePatterns":[],"id":"prefer-walrus-regex-match","key":"python:prefer-walrus-regex-match","languages":["python"],"limitations":["Only a simple assignment immediately followed by a truthy or `is not None` check is analyzed.","The assignment is retained when the result is used after the conditional or the regex receiver cannot be resolved."],"messageIds":[],"optionsSchema":null,"rationale":"A named expression keeps the match operation and its condition together while preserving access to the result.","references":[],"remediation":"Move the regex call into the following condition as `if (match := pattern.search(text)):`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_walrus_regex_match.py","status":"active","summary":"Bind a regex result in the `if` condition that immediately tests it.","test":"packages/python/tests/rules/test_prefer_walrus_regex_match.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ077","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/stream.py","source":"while chunk := stream.read(8192):\n    process(chunk)\n"}],"fixedFiles":[],"focusPath":"app/stream.py","id":"conditional-stream-binding","outcome":"accept","title":"Stream loop binds in its condition"},{"expectedCount":1,"files":[{"path":"app/stream.py","source":"while True:\n    chunk = stream.read(8192)\n    if not chunk:\n        break\n    process(chunk)\n"}],"fixedFiles":[],"focusPath":"app/stream.py","id":"explicit-stream-break","outcome":"reject","title":"Stream loop assigns and then breaks"}],"filePatterns":[],"id":"prefer-walrus-stream-loop","key":"python:prefer-walrus-stream-loop","languages":["python"],"limitations":["Only `while True` loops beginning with a simple assignment and an immediate falsy or `None` break are analyzed.","Loops with an `else`, complex assignment targets, or additional work before the sentinel check are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A named-expression loop states the read, sentinel check, and iteration condition in one place.","references":[],"remediation":"Replace the leading assignment and immediate sentinel break with `while (value := read()):`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py","status":"active","summary":"Bind each stream value in the `while` condition instead of using an explicit break.","test":"packages/python/tests/rules/test_prefer_walrus_stream_loop.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ008","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"def build_payload(call) -> CallPayload:\n    return CallPayload(id=call.id)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"typed-boundary-model","outcome":"accept","title":"Named model returned from a public function"},{"expectedCount":1,"files":[{"path":"service.py","source":"from typing import Any\n\ndef build_payload(call) -> dict[str, Any]:\n    return {'id': call.id}\n"}],"fixedFiles":[],"focusPath":"service.py","id":"untyped-dictionary-boundary","outcome":"reject","title":"Untyped dictionary returned from a public function"}],"filePatterns":[],"id":"pydantic-at-boundaries","key":"python:pydantic-at-boundaries","languages":["python"],"limitations":["Private functions, closures, tests, fixtures, validators, and dictionary conversion methods are excluded.","Only returned record literals and locally built fixed-shape dictionaries are recognized."],"messageIds":[],"optionsSchema":null,"rationale":"Named boundary models make field types and required keys explicit to callers and tooling.","references":[],"remediation":"Return a pydantic model, frozen dataclass, or `TypedDict` for the fixed record shape.","since":null,"source":"packages/python/src/sarj_python_lint/rules/pydantic_at_boundaries.py","status":"active","summary":"Public function or route returns a fixed-shape untyped dictionary.","test":"packages/python/tests/rules/test_pydantic_at_boundaries.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ085","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/policy.py","source":"class RetryPolicy:\n    \"\"\"Retry policy required because the upstream caps concurrency.\"\"\"\n\n    attempts: int = 3\n"}],"fixedFiles":[],"focusPath":"app/policy.py","id":"class-invariant","outcome":"accept","title":"Docstring records an invariant"},{"expectedCount":1,"files":[{"path":"app/policy.py","source":"class RetryPolicy:\n    \"\"\"The retry policy.\"\"\"\n\n    attempts: int = 3\n"}],"fixedFiles":[],"focusPath":"app/policy.py","id":"class-name-restatement","outcome":"reject","title":"Docstring repeats the class name"}],"filePatterns":[],"id":"redundant-class-docstring","key":"python:redundant-class-docstring","languages":["python"],"limitations":["Schema-carrying bases and decorators, runtime-consumed prompt decorators, generated files, and docstring-only class bodies are excluded.","The rule compares conservative word stems; a novel term keeps the docstring."],"messageIds":[],"optionsSchema":null,"rationale":"Restating a class declaration adds maintenance cost without helping a reader understand its contract.","references":[],"remediation":"Delete the redundant docstring or document an invariant, lifetime, exclusion, or other fact absent from the declaration.","since":null,"source":"packages/python/src/sarj_python_lint/rules/redundant_class_docstring.py","status":"active","summary":"Class docstrings must add information beyond the class name and bases.","test":"packages/python/tests/rules/test_redundant_class_docstring.py"},{"aliases":[],"autofix":"suggestion","category":"maintainability","code":"SARJ050","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"def update_message(message_id: str):\n    \"\"\"Replace any existing draft atomically.\"\"\"\n    return None\n"}],"fixedFiles":[],"focusPath":"service.py","id":"behavioral-docstring","outcome":"accept","title":"Docstring documents behavior"},{"expectedCount":1,"files":[{"path":"service.py","source":"def update_message(message_id: str):\n    \"\"\"Update the message.\"\"\"\n    return None\n"}],"fixedFiles":[],"focusPath":"service.py","id":"signature-restatement","outcome":"reject","title":"Docstring repeats the function name"}],"filePatterns":[],"id":"redundant-docstring","key":"python:redundant-docstring","languages":["python"],"limitations":["Detection is limited to short docstrings whose words are already present in the function, class, annotations, or parameters.","Framework-facing docstrings, generated files, directives, examples, references, and additional behavioral detail are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Restating a clear name and signature creates maintenance work without helping callers.","references":[],"remediation":"Delete the docstring or document behavior, constraints, side effects, or failure modes not evident from the signature.","since":null,"source":"packages/python/src/sarj_python_lint/rules/redundant_docstring.py","status":"active","summary":"Docstring only restates the signature — delete the whole docstring or document behavior callers cannot infer.","test":"packages/python/tests/rules/test_redundant_docstring.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ099","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"celery/utils/log.py","source":"\"\"\"Logging utilities redact credentials.\"\"\"\n\nVALUE = 1\n"}],"fixedFiles":[],"focusPath":"celery/utils/log.py","id":"module-contract","outcome":"accept","title":"Docstring records a module contract"},{"expectedCount":1,"files":[{"path":"celery/utils/log.py","source":"\"\"\"Logging utilities.\"\"\"\n\nVALUE = 1\n"}],"fixedFiles":[],"focusPath":"celery/utils/log.py","id":"module-path-restatement","outcome":"reject","title":"Docstring repeats the module path"}],"filePatterns":[],"id":"redundant-module-docstring","key":"python:redundant-module-docstring","languages":["python"],"limitations":["Only single-line summary docstrings in non-test implementation modules are checked.","Special modules, stubs, generated files, multiline documentation, and prose with protected technical facts are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A one-line restatement of a module path duplicates information already visible to readers and search tools.","references":[],"remediation":"Delete the redundant docstring or document an invariant, boundary, consumer, or compatibility constraint.","since":null,"source":"packages/python/src/sarj_python_lint/rules/redundant_module_docstring.py","status":"active","summary":"Module docstrings must add information beyond the file path.","test":"packages/python/tests/rules/test_redundant_module_docstring.py"},{"aliases":["kwonly-same-type-params"],"autofix":"none","category":"correctness","code":"SARJ034","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"def move(*, source_id: str, target_id: str) -> None: ...\n"}],"fixedFiles":[],"focusPath":"service.py","id":"keyword-only-source-and-target","outcome":"accept","title":"Source and target IDs are keyword-only"},{"expectedCount":1,"files":[{"path":"service.py","source":"def move(source_id: str, target_id: str) -> None: ...\n"}],"fixedFiles":[],"focusPath":"service.py","id":"positional-source-and-target","outcome":"reject","title":"Source and target IDs are positional"}],"filePatterns":[],"id":"require-keyword-only-swap-prone-params","key":"python:require-keyword-only-swap-prone-params","languages":["python"],"limitations":["Only high-risk groups of bare `str`, `int`, or `float` annotations are reported.","Tests, generated code, protocols, routes, CLI handlers, overrides, and conventional ordered pairs are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Callers can exchange semantically distinct positional values without a type-checking failure.","references":[],"remediation":"Insert `*` before the risky parameters and pass them by name at call sites.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py","status":"active","summary":"Swap-prone parameters with the same primitive type should be keyword-only.","test":"packages/python/tests/rules/test_require_keyword_only_swap_prone_params.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ071","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/services/thing_service.py","source":"class ThingService:\n    def __init__(self, client: ThingClient) -> None:\n        self.client = client\n\n    def read(self, key: str) -> str:\n        return self.client.get(key)\n\n    def write(self, key: str, value: str) -> None:\n        self.client.put(key, value)\n"}],"fixedFiles":[],"focusPath":"app/services/thing_service.py","id":"concrete-service-boundary","outcome":"reject","title":"Concrete service directly exposes an injected collaborator"},{"expectedCount":0,"files":[{"path":"app/services/thing_service.py","source":"class ThingService(ThingServicePort):\n    def __init__(self, client: ThingClient) -> None:\n        self.client = client\n\n    def read(self, key: str) -> str:\n        return self.client.get(key)\n\n    def write(self, key: str, value: str) -> None:\n        self.client.put(key, value)\n"}],"fixedFiles":[],"focusPath":"app/services/thing_service.py","id":"declared-service-port","outcome":"accept","title":"Service implements a declared port"}],"filePatterns":[],"id":"require-port-for-service","key":"python:require-port-for-service","languages":["python"],"limitations":["This advisory uses service-family names, constructor annotations, collaborator calls, and public-method counts as heuristics.","Tests, generated code, scripts, known framework shapes, persistence-only dependencies, and classes with declared bases are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A small port can decouple consumers when they genuinely need to substitute a concrete service boundary.","references":[],"remediation":"Define a focused `Protocol` or ABC and type substituting consumers against it, or suppress the advisory when no substitution boundary exists.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_port_for_service.py","status":"active","summary":"Consider a consumer-owned port for a service with a behaviorally used collaborator.","test":"packages/python/tests/rules/test_require_port_for_service.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ088","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_widget.py","source":"def test_widget_renders():\n    \"\"\"Verify that the widget renders correctly.\"\"\"\n    assert render(widget)\n"}],"fixedFiles":[],"focusPath":"tests/test_widget.py","id":"test-name-restatement","outcome":"reject","title":"Docstring repeats the test name"},{"expectedCount":0,"files":[{"path":"tests/test_scheduler.py","source":"def test_keeps_the_lock():\n    \"\"\"The scheduler would spin forever without this.\"\"\"\n    assert acquire()\n"}],"fixedFiles":[],"focusPath":"tests/test_scheduler.py","id":"test-regression-context","outcome":"accept","title":"Docstring explains the regression"}],"filePatterns":[],"id":"restated-test-docstring","key":"python:restated-test-docstring","languages":["python"],"limitations":["Only test functions and unbased Test-prefixed classes in recognized test files are checked.","Structured, protected, value-bearing, generated, and genuinely novel docstrings are preserved."],"messageIds":[],"optionsSchema":null,"rationale":"A docstring that narrates visible test code creates duplicate prose that can drift without explaining the regression or contract.","references":[],"remediation":"Delete the redundant docstring, improve the test name, or document a reason or constraint not visible in the test.","since":null,"source":"packages/python/src/sarj_python_lint/rules/restated_test_docstring.py","status":"active","summary":"Test docstrings must add information beyond the test name and body.","test":"packages/python/tests/rules/test_restated_test_docstring.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ047","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_worker.py","source":"import asyncio\n\nasync def test_worker():\n    await asyncio.sleep(POLL_INTERVAL * 4)\n    assert finished()\n"}],"fixedFiles":[],"focusPath":"tests/test_worker.py","id":"computed-test-sleep","outcome":"reject","title":"Test guesses a computed delay"},{"expectedCount":0,"files":[{"path":"tests/test_worker.py","source":"async def test_worker():\n    finished = start_worker()\n    await finished.wait()\n    assert finished.is_set()\n"}],"fixedFiles":[],"focusPath":"tests/test_worker.py","id":"event-synchronized-test","outcome":"accept","title":"Test waits for completion"}],"filePatterns":[],"id":"sleep-with-computed-arg-in-test","key":"python:sleep-with-computed-arg-in-test","languages":["python"],"limitations":["Only `time.sleep` and `asyncio.sleep` calls directly inside test functions are analyzed.","Numeric literal delays belong to SARJ031; helper-owned and nested-function sleeps are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A calculated delay still races the system under load and makes test duration depend on guessed timing.","references":[],"remediation":"Await completion, wait on an event, or poll the expected condition with a bounded deadline.","since":null,"source":"packages/python/src/sarj_python_lint/rules/sleep_with_computed_arg_in_test.py","status":"active","summary":"Computed `sleep()` in a test body — synchronize on the signal, don't guess a delay.","test":"packages/python/tests/rules/test_sleep_with_computed_arg_in_test.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ023","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"def handle(payload: dict) -> dict:\n    return _parse(payload)\n\ndef _parse(payload: dict) -> dict:\n    return payload\n"}],"fixedFiles":[],"focusPath":"service.py","id":"helper-after-sole-caller","outcome":"accept","title":"Private helper appears after its sole caller"},{"expectedCount":1,"files":[{"path":"service.py","source":"def _parse(payload: dict) -> dict:\n    return payload\n\ndef handle(payload: dict) -> dict:\n    return _parse(payload)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"helper-before-sole-caller","outcome":"reject","title":"Private helper appears before its sole caller"}],"filePatterns":[],"id":"stepdown","key":"python:stepdown","languages":["python"],"limitations":["Generated files, tests, `__main__.py`, recursive helpers, and helpers with multiple callers are excluded.","Dynamic references that cannot identify a sole caller are not reported."],"messageIds":[],"optionsSchema":null,"rationale":"Caller-first ordering keeps the module's public flow visible before its implementation details.","references":[],"remediation":"Move the private helper below its sole caller without changing either body.","since":null,"source":"packages/python/src/sarj_python_lint/rules/stepdown.py","status":"active","summary":"A private helper used by one caller should be defined below that caller.","test":"packages/python/tests/rules/test_stepdown.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ018","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"QUERY = \"INSERT INTO task (id) VALUES (%s) ON CONFLICT DO NOTHING\"\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"insert-with-conflict-handler","outcome":"accept","title":"Store insert with conflict handling"},{"expectedCount":1,"files":[{"path":"app/task_store.py","source":"QUERY = \"INSERT INTO task (id) VALUES (%s)\"\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"insert-without-conflict-handler","outcome":"reject","title":"Store insert without conflict handling"}],"filePatterns":[],"id":"store-insert-requires-on-conflict","key":"python:store-insert-requires-on-conflict","languages":["python"],"limitations":["Only SQL string literals in recognized store modules are analyzed.","A deliberate non-idempotent insert requires a local SARJ018 suppression."],"messageIds":[],"optionsSchema":null,"rationale":"A replayed write without conflict handling can fail or create duplicate state.","references":[],"remediation":"Use `ON CONFLICT`, `ON DUPLICATE KEY`, or SQLite `OR IGNORE`/`OR REPLACE` as appropriate.","since":null,"source":"packages/python/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py","status":"active","summary":"Embedded SQL inserts in store code must handle conflicts explicitly.","test":"packages/python/tests/rules/test_store_insert_requires_on_conflict.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ041","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_normalize.py","source":"def test_normalize():\n    for value in [\"a\", \"b\"]:\n        assert normalize(value) == value\n"}],"fixedFiles":[],"focusPath":"tests/test_normalize.py","id":"literal-cases-in-loop","outcome":"reject","title":"Cases hidden inside one test"},{"expectedCount":0,"files":[{"path":"tests/test_normalize.py","source":"import pytest\n\n@pytest.mark.parametrize(\"value\", [\"a\", \"b\"])\ndef test_normalize(value):\n    assert normalize(value) == value\n"}],"fixedFiles":[],"focusPath":"tests/test_normalize.py","id":"parametrized-literal-cases","outcome":"accept","title":"Each case is a separate test"}],"filePatterns":[],"id":"test-loops-over-literal-cases","key":"python:test-loops-over-literal-cases","languages":["python"],"limitations":["Only literal iterables with at least two cases inside test functions are analyzed.","Loops using unittest or pytest subtest contexts are allowed."],"messageIds":[],"optionsSchema":null,"rationale":"A loop collapses all cases into one test and stops at the first failing iteration.","references":[],"remediation":"Move the literal table into `@pytest.mark.parametrize` so every case has its own result.","since":null,"source":"packages/python/src/sarj_python_lint/rules/test_loops_over_literal_cases.py","status":"active","summary":"Test loops over a literal case table — use `@pytest.mark.parametrize` so cases report separately.","test":"packages/python/tests/rules/test_test_loops_over_literal_cases.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ089","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_widget.py","source":"def test_widget():\n    # Arrange\n    widget = make_widget()\n    assert widget\n"}],"fixedFiles":[],"focusPath":"tests/test_widget.py","id":"bare-phase-label","outcome":"reject","title":"Bare phase label"},{"expectedCount":0,"files":[{"path":"tests/test_widget.py","source":"def test_widget():\n    # Then the retry loop would spin forever.\n    assert works()\n"}],"fixedFiles":[],"focusPath":"tests/test_widget.py","id":"explanatory-comment","outcome":"accept","title":"Comment explains a consequence"}],"filePatterns":[],"id":"test-phase-label-comment","key":"python:test-phase-label-comment","languages":["python"],"limitations":["Only standalone comments in recognized test files are checked; nested literal comments and trailing comments are excluded.","Comments containing words beyond the bounded phase-label grammar are preserved."],"messageIds":[],"optionsSchema":null,"rationale":"Phase labels narrate test structure without explaining behavior and often indicate that a test needs clearer names or smaller units.","references":[],"remediation":"Delete the label; if the phases remain hard to follow, extract a named helper or split the test.","since":null,"source":"packages/python/src/sarj_python_lint/rules/phase_label_comment.py","status":"active","summary":"Tests must not use bare Arrange, Act, Assert, Given, When, or Then phase comments.","test":"packages/python/tests/rules/test_phase_label_comment.py"},{"aliases":[],"autofix":"suggestion","category":"maintainability","code":"SARJ051","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"settings.py","source":"BACKOFF = 2 * 60  # doubles per attempt\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"comment-explains-policy","outcome":"accept","title":"Comment explains a policy"},{"expectedCount":1,"files":[{"path":"settings.py","source":"STALE_TIME = 5 * 60 * 1000  # 5 minutes\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"repeated-duration","outcome":"reject","title":"Comment repeats the duration"}],"filePatterns":[],"id":"trailing-value-narration","key":"python:trailing-value-narration","languages":["python"],"limitations":["Detection targets simple numeric assignments with a trailing comment that repeats the number and unit.","Approximate conversions, reasons, references, directives, bracketed values, and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A unit encoded only in a comment can drift from the value and is unavailable to type checking.","references":[],"remediation":"Encode the unit in the name or value type, such as timeout_seconds or timedelta.","since":null,"source":"packages/python/src/sarj_python_lint/rules/trailing_value_narration.py","status":"active","summary":"Trailing comment restates a literal value and its unit.","test":"packages/python/tests/rules/test_trailing_value_narration.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ064","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_user.py","source":"def test_user():\n    user = User(name=\"Ada\")\n    assert user.name == \"Ada\"\n"}],"fixedFiles":[],"focusPath":"tests/test_user.py","id":"constructor-keyword-echo","outcome":"reject","title":"Assertion repeats a constructor keyword"},{"expectedCount":0,"files":[{"path":"tests/test_user.py","source":"def test_user():\n    user = User(name=\"Ada Lovelace\")\n    assert user.initials == \"AL\"\n"}],"fixedFiles":[],"focusPath":"tests/test_user.py","id":"derived-value","outcome":"accept","title":"Assertion checks a derived value"}],"filePatterns":[],"id":"trivially-true-assertion","key":"python:trivially-true-assertion","languages":["python"],"limitations":["Literal-only assertions owned by Ruff or SARJ057 are excluded.","Constructor echoes with evidence of field coercion are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Constructor keyword echoes and equivalent tautologies cannot reveal an application defect.","references":[],"remediation":"Assert a transformation, validation result, or other value produced independently of the fixture literal.","since":null,"source":"packages/python/src/sarj_python_lint/rules/trivially_true_assertion.py","status":"active","summary":"Assertions should depend on behavior rather than echoing values supplied by the test.","test":"packages/python/tests/rules/test_trivially_true_assertion.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ067","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_billing.py","source":"def test_charge():\n    gateway.charge.return_value = 1\n    assert billing.charge(gateway) == 1\n    gateway.charge.return_value = 2\n    assert billing.charge(gateway) == 2\n"}],"fixedFiles":[],"focusPath":"tests/test_billing.py","id":"mock-used-before-reset","outcome":"accept","title":"Test uses each configured value"},{"expectedCount":1,"files":[{"path":"tests/test_billing.py","source":"def test_charge():\n    gateway.charge.return_value = 1\n    gateway.charge.return_value = 2\n    assert billing.charge(gateway) == 2\n"}],"fixedFiles":[],"focusPath":"tests/test_billing.py","id":"overwritten-mock-setup","outcome":"reject","title":"Mock return value is overwritten before use"}],"filePatterns":[],"id":"unused-mock-setup","key":"python:unused-mock-setup","languages":["python"],"limitations":["Only test paths are analyzed.","Potentially effectful statements between assignments prevent a finding."],"messageIds":[],"optionsSchema":null,"rationale":"Overwritten or contradicted mock setup adds misleading, unreachable test behavior.","references":[],"remediation":"Delete the unused setup or exercise the mock before replacing or contradicting it.","since":null,"source":"packages/python/src/sarj_python_lint/rules/unused_mock_setup.py","status":"active","summary":"Tests should remove mock configuration that cannot affect execution.","test":"packages/python/tests/rules/test_unused_mock_setup.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ043","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_conditions.py","source":"def test_conditions():\n    assert evaluate_conditions(record, conditions)\n"}],"fixedFiles":[],"focusPath":"tests/test_conditions.py","id":"asserted-test-result","outcome":"accept","title":"Test verifies the result"},{"expectedCount":1,"files":[{"path":"tests/test_conditions.py","source":"def test_conditions():\n    evaluate_conditions(record, conditions)\n"}],"fixedFiles":[],"focusPath":"tests/test_conditions.py","id":"discarded-test-result","outcome":"reject","title":"Test verifies no outcome"}],"filePatterns":[],"id":"zero-assertion-test","key":"python:zero-assertion-test","languages":["python"],"limitations":["Only pytest-collected test functions in test paths are analyzed.","Skipped, xfailed, benchmark, placeholder, helper-delegating, and explicit expectation tests are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Calling production code without verifying an outcome gives the test no observable behavioral contract.","references":[],"remediation":"Assert the result or use an explicit pytest expectation such as `pytest.raises` or `pytest.warns`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/zero_assertion_test.py","status":"active","summary":"Test contains no assertion of any kind — it passes as long as nothing raises.","test":"packages/python/tests/rules/test_zero_assertion_test.py"},{"aliases":["add-constraint-not-valid"],"autofix":"none","category":"performance","code":"SARJ111","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"supabase/migrations/001_age.sql","source":"ALTER TABLE users ADD CONSTRAINT check_age CHECK (age >= 18) NOT VALID;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_age.sql","id":"deferred-check-validation","outcome":"accept","title":"Constraint added without scanning existing rows"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_age.sql","source":"ALTER TABLE users ADD CONSTRAINT check_age CHECK (age >= 18);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_age.sql","id":"validating-check-constraint","outcome":"reject","title":"Constraint validated while it is added"}],"filePatterns":[],"id":"add-constraint-requires-not-valid","key":"sql:add-constraint-requires-not-valid","languages":["sql"],"limitations":["Only PostgreSQL migration files and CHECK or foreign-key constraints added to existing tables are inspected."],"messageIds":[],"optionsSchema":null,"rationale":"Validating a new CHECK or foreign key while adding it can hold disruptive locks while PostgreSQL scans existing rows.","references":[],"remediation":"Add the constraint as NOT VALID, then validate it in a separate ALTER TABLE statement.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/add_constraint_requires_not_valid.py","status":"active","summary":"ADD CONSTRAINT (CHECK/FK) without NOT VALID blocks writes during full-table validation.","test":"packages/sql/tests/rules/test_add_constraint_requires_not_valid.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ101","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/001_orders.sql","source":"CREATE TABLE orders (created_at TIMESTAMP NOT NULL);\n"}],"fixedFiles":[],"focusPath":"migrations/001_orders.sql","id":"naive-created-at","outcome":"reject","title":"Naive timestamp column"},{"expectedCount":0,"files":[{"path":"migrations/001_orders.sql","source":"CREATE TABLE orders (created_at TIMESTAMPTZ NOT NULL);\n"}],"fixedFiles":[],"focusPath":"migrations/001_orders.sql","id":"zoned-created-at","outcome":"accept","title":"Timestamp with time-zone semantics"}],"filePatterns":[],"id":"enforce-timestamptz","key":"sql:enforce-timestamptz","languages":["sql"],"limitations":[],"messageIds":[],"optionsSchema":null,"rationale":"Naive timestamps discard offset context and make cross-time-zone comparisons ambiguous.","references":[],"remediation":"Declare persisted instants as TIMESTAMPTZ or TIMESTAMP WITH TIME ZONE.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/enforce_timestamptz.py","status":"active","summary":"TIMESTAMP without TIME ZONE — use TIMESTAMPTZ.","test":"packages/sql/tests/rules/test_enforce_timestamptz.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ102","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"migrations/001_orders.sql","source":"CREATE TABLE IF NOT EXISTS orders (id BIGINT PRIMARY KEY);\n"}],"fixedFiles":[],"focusPath":"migrations/001_orders.sql","id":"guarded-table-creation","outcome":"accept","title":"Replay-safe table creation"},{"expectedCount":1,"files":[{"path":"migrations/001_orders.sql","source":"CREATE TABLE orders (id BIGINT PRIMARY KEY);\n"}],"fixedFiles":[],"focusPath":"migrations/001_orders.sql","id":"unguarded-table-creation","outcome":"reject","title":"Table creation that fails on replay"}],"filePatterns":[],"id":"idempotent-ddl","key":"sql:idempotent-ddl","languages":["sql"],"limitations":["Dialect-specific DDL forms are checked only where the guard syntax is supported."],"messageIds":[],"optionsSchema":null,"rationale":"A partially applied migration may be retried, so unconditional object creation or removal can fail before recovery completes.","references":[],"remediation":"Use the supported IF NOT EXISTS or IF EXISTS form for the DDL statement.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/idempotent_ddl.py","status":"active","summary":"DDL without IF [NOT] EXISTS — migrations must be safe to re-run.","test":"packages/sql/tests/rules/test_idempotent_ddl.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ108","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/002_email_index.sql","source":"CREATE INDEX users_email_idx ON users(email);\n"}],"fixedFiles":[],"focusPath":"migrations/002_email_index.sql","id":"blocking-index-build","outcome":"reject","title":"Blocking index build on an existing table"},{"expectedCount":0,"files":[{"path":"migrations/002_email_index.sql","source":"CREATE INDEX CONCURRENTLY users_email_idx ON users(email);\n"}],"fixedFiles":[],"focusPath":"migrations/002_email_index.sql","id":"concurrent-index-build","outcome":"accept","title":"Concurrent index build"}],"filePatterns":[],"id":"index-concurrently","key":"sql:index-concurrently","languages":["sql"],"limitations":["Indexes on tables created earlier in the same file are exempt because no concurrent writers exist yet."],"messageIds":[],"optionsSchema":null,"rationale":"Building an index normally blocks writes to an existing PostgreSQL table for the duration of the build.","references":[],"remediation":"Use CREATE INDEX CONCURRENTLY in a nontransactional migration.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/index_concurrently.py","status":"active","summary":"CREATE INDEX without CONCURRENTLY — locks the table against writes.","test":"packages/sql/tests/rules/test_index_concurrently.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ105","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"supabase/migrations/001_seed.sql","source":"INSERT INTO plan (name) VALUES ('free') ON CONFLICT (name) DO NOTHING;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_seed.sql","id":"idempotent-seed-insert","outcome":"accept","title":"Seed insert with conflict handling"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_seed.sql","source":"INSERT INTO plan (name) VALUES ('free');\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_seed.sql","id":"non-idempotent-seed-insert","outcome":"reject","title":"Seed insert without conflict handling"}],"filePatterns":[],"id":"insert-requires-on-conflict","key":"sql:insert-requires-on-conflict","languages":["sql"],"limitations":["Only PostgreSQL migration paths and explicitly marked PostgreSQL migrations are checked."],"messageIds":[],"optionsSchema":null,"rationale":"A retried seed migration can duplicate rows or fail on a uniqueness constraint when an insert has no replay behavior.","references":[],"remediation":"Add ON CONFLICT with an explicit DO NOTHING or DO UPDATE action.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/insert_requires_on_conflict.py","status":"active","summary":"INSERT without ON CONFLICT — migration data writes must be idempotent upserts.","test":"packages/sql/tests/rules/test_insert_requires_on_conflict.py"},{"aliases":["no-limit-offset"],"autofix":"none","category":"performance","code":"SARJ107","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"queries/calls.sql","source":"SELECT id FROM call WHERE id > :cursor ORDER BY id LIMIT 50;\n"}],"fixedFiles":[],"focusPath":"queries/calls.sql","id":"cursor-pagination","outcome":"accept","title":"Pagination bounded by a stable cursor"},{"expectedCount":1,"files":[{"path":"queries/calls.sql","source":"SELECT id FROM call ORDER BY id LIMIT 50 OFFSET 100;\n"}],"fixedFiles":[],"focusPath":"queries/calls.sql","id":"offset-pagination","outcome":"reject","title":"Pagination that scans skipped rows"}],"filePatterns":[],"id":"no-offset-pagination","key":"sql:no-offset-pagination","languages":["sql"],"limitations":["The rule recognizes literal and common driver parameter markers following OFFSET."],"messageIds":[],"optionsSchema":null,"rationale":"OFFSET scans and discards every skipped row, so later pages become slower as the result set grows.","references":[],"remediation":"Filter on a stable cursor column, preserve its ordering, and retain a bounded LIMIT.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_offset_pagination.py","status":"active","summary":"OFFSET pagination — use cursor pagination (WHERE id > :cursor).","test":"packages/sql/tests/rules/test_no_offset_pagination.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ103","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/001_status.sql","source":"CREATE TYPE call_status AS ENUM ('pending', 'active', 'completed');\n"}],"fixedFiles":[],"focusPath":"migrations/001_status.sql","id":"postgres-enum-type","outcome":"reject","title":"PostgreSQL enum type"},{"expectedCount":0,"files":[{"path":"migrations/001_status.sql","source":"CREATE TABLE call (\n    status TEXT NOT NULL CHECK (status IN ('pending', 'active', 'completed'))\n);\n"}],"fixedFiles":[],"focusPath":"migrations/001_status.sql","id":"text-check-constraint","outcome":"accept","title":"Text column with an explicit value constraint"}],"filePatterns":[],"id":"no-pg-enum","key":"sql:no-pg-enum","languages":["sql"],"limitations":["PostgreSQL dump files are excluded.","Generated migrations still report, but diagnostics direct the edit to the owning schema model."],"messageIds":[],"optionsSchema":null,"rationale":"PostgreSQL enums make ordinary value changes operationally awkward and couple application evolution to database type migrations.","references":[],"remediation":"Store the value as TEXT and constrain the allowed values with an explicit CHECK expression.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_pg_enum.py","status":"active","summary":"CREATE TYPE ... AS ENUM — use TEXT + CHECK constraint instead.","test":"packages/sql/tests/rules/test_no_pg_enum.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ106","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/001_documents.sql","source":"CREATE TABLE document (metadata JSON NOT NULL);\n"}],"fixedFiles":[],"focusPath":"migrations/001_documents.sql","id":"json-column","outcome":"reject","title":"Plain JSON column"},{"expectedCount":0,"files":[{"path":"migrations/001_documents.sql","source":"CREATE TABLE document (metadata JSONB NOT NULL DEFAULT '{}'::jsonb);\n"}],"fixedFiles":[],"focusPath":"migrations/001_documents.sql","id":"jsonb-column","outcome":"accept","title":"Indexable JSONB column"}],"filePatterns":[],"id":"prefer-jsonb","key":"sql:prefer-jsonb","languages":["sql"],"limitations":["PostgreSQL dump files are excluded.","JSON tokens inside comments, string literals, and longer identifiers are ignored."],"messageIds":[],"optionsSchema":null,"rationale":"JSONB supports indexing and containment operators and avoids reparsing the stored document on every read.","references":[],"remediation":"Declare JSONB columns and use jsonb casts for JSON document values.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/prefer_jsonb.py","status":"active","summary":"JSON column type or ::json cast — use JSONB.","test":"packages/sql/tests/rules/test_prefer_jsonb.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ104","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/001_users.sql","source":"CREATE TABLE users (name VARCHAR(255) NOT NULL);\n"}],"fixedFiles":[],"focusPath":"migrations/001_users.sql","id":"bounded-varchar-column","outcome":"reject","title":"Length-limited VARCHAR column"},{"expectedCount":0,"files":[{"path":"migrations/001_users.sql","source":"CREATE TABLE users (name TEXT NOT NULL CHECK (char_length(name) <= 255));\n"}],"fixedFiles":[],"focusPath":"migrations/001_users.sql","id":"text-with-length-check","outcome":"accept","title":"Text column with an explicit length constraint"}],"filePatterns":[],"id":"prefer-text-over-varchar","key":"sql:prefer-text-over-varchar","languages":["sql"],"limitations":["MySQL and SQLite sources are excluded because their VARCHAR behavior differs."],"messageIds":[],"optionsSchema":null,"rationale":"PostgreSQL gives VARCHAR(n) no storage or performance advantage, while its length cap obscures a business constraint.","references":[],"remediation":"Use TEXT and express a real maximum length with an explicit CHECK constraint when needed.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/prefer_text_over_varchar.py","status":"active","summary":"VARCHAR(n) — use TEXT (+ CHECK length if needed).","test":"packages/sql/tests/rules/test_prefer_text_over_varchar.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ109","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/001_calls.sql","source":"CREATE TABLE call (id UUID PRIMARY KEY DEFAULT gen_random_uuid());\n"}],"fixedFiles":[],"focusPath":"migrations/001_calls.sql","id":"random-uuid-default","outcome":"reject","title":"Random UUID default"},{"expectedCount":0,"files":[{"path":"migrations/001_calls.sql","source":"CREATE TABLE call (id UUID PRIMARY KEY DEFAULT uuidv7());\n"}],"fixedFiles":[],"focusPath":"migrations/001_calls.sql","id":"time-ordered-uuid-default","outcome":"accept","title":"Time-ordered UUID default"}],"filePatterns":[],"id":"prefer-uuidv7-default","key":"sql:prefer-uuidv7-default","languages":["sql"],"limitations":["uuidv7() requires PostgreSQL 18 or an equivalent extension-provided function."],"messageIds":[],"optionsSchema":null,"rationale":"Random UUID keys scatter inserts across a B-tree, while time-ordered UUIDv7 values preserve index locality.","references":[],"remediation":"Use the PostgreSQL uuidv7() function for generated UUID defaults and values.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/prefer_uuidv7_default.py","status":"active","summary":"`gen_random_uuid()` emits a random UUIDv4 — use `uuidv7()` so keys are time-ordered.","test":"packages/sql/tests/rules/test_prefer_uuidv7_default.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ112","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"migrations/001_orders.sql","source":"CREATE TABLE orders (customer_id BIGINT REFERENCES customer(id));\nCREATE INDEX orders_customer_id_idx ON orders(customer_id);\n"}],"fixedFiles":[],"focusPath":"migrations/001_orders.sql","id":"indexed-foreign-key","outcome":"accept","title":"Foreign key covered by an index"},{"expectedCount":1,"files":[{"path":"migrations/001_orders.sql","source":"CREATE TABLE orders (customer_id BIGINT REFERENCES customer(id));\n"}],"fixedFiles":[],"focusPath":"migrations/001_orders.sql","id":"unindexed-foreign-key","outcome":"reject","title":"Foreign key without a child-table index"}],"filePatterns":[],"id":"require-fk-index","key":"sql:require-fk-index","languages":["sql"],"limitations":["A bounded scan includes indexes from sibling files in the same migration tree."],"messageIds":[],"optionsSchema":null,"rationale":"PostgreSQL does not automatically index referencing columns, so parent updates and deletes may scan the child table.","references":[],"remediation":"Create an index whose leading columns cover the foreign-key columns on the child table.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/require_fk_index.py","status":"active","summary":"FOREIGN KEY column missing index — causes full-table scans and locks on parent row deletes.","test":"packages/sql/tests/rules/test_require_fk_index.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ110","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"supabase/migrations/001_users.sql","source":"SET lock_timeout = '3s';\nALTER TABLE users ADD COLUMN note TEXT;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_users.sql","id":"bounded-ddl-lock-wait","outcome":"accept","title":"DDL preceded by a positive lock timeout"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_users.sql","source":"ALTER TABLE users ADD COLUMN note TEXT;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_users.sql","id":"unbounded-ddl-lock-wait","outcome":"reject","title":"DDL without a positive timeout"}],"filePatterns":[],"id":"require-lock-timeout","key":"sql:require-lock-timeout","languages":["sql"],"limitations":["Only PostgreSQL migration paths and explicitly marked PostgreSQL migrations are checked."],"messageIds":[],"optionsSchema":null,"rationale":"Unbounded lock waits can stall production traffic indefinitely when migration DDL contends with active transactions.","references":[],"remediation":"Set a short positive lock_timeout or statement_timeout before the DDL statement.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/require_lock_timeout.py","status":"active","summary":"DDL migration missing positive SET [LOCAL] lock_timeout or statement_timeout prior to DDL.","test":"packages/sql/tests/rules/test_require_lock_timeout.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ301","defaultLevel":"error","engine":"text","examples":[{"expectedCount":1,"files":[{"path":"config.toml","source":"# timeout = 30\ntimeout = 10\n"}],"fixedFiles":[],"focusPath":"config.toml","id":"disabled-config-entry","outcome":"reject","title":"A commented-out assignment is stale configuration"},{"expectedCount":0,"files":[{"path":"config.toml","source":"# Default:\n# timeout = 30\ntimeout = 10\n"}],"fixedFiles":[],"focusPath":"config.toml","id":"documented-default","outcome":"accept","title":"An explicitly labeled default is documentation"}],"filePatterns":["**/*.{yaml,yml,toml,jsonc,ini,cfg,conf,properties,sh,zsh,bash}"],"id":"commented-out-config","key":"text:commented-out-config","languages":["config"],"limitations":["Directive, rationale, documented-example, and YAML block-scalar comments are intentionally excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Disabled configuration becomes stale while version control already preserves its history.","references":[],"remediation":"Delete disabled configuration; document a default or constraint when that information remains useful.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"commented-out config syntax","test":"packages/standards/tests/test_textlint.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ300","defaultLevel":"error","engine":"text","examples":[{"expectedCount":1,"files":[{"path":"workflow.yml","source":"# Set build name\nname: build\n# Run build command\nrun: make build\n# Set deploy image\nimage: app\n# Run deploy command\ncommand: deploy\n"}],"fixedFiles":[],"focusPath":"workflow.yml","id":"narrated-config-wall","outcome":"reject","title":"Repeated comments restate adjacent entries"},{"expectedCount":0,"files":[{"path":"workflow.yml","source":"name: build\nrun: make build\nimage: app\ncommand: deploy\n"}],"fixedFiles":[],"focusPath":"workflow.yml","id":"self-explanatory-config","outcome":"accept","title":"Clear entries need no narration"}],"filePatterns":["**/*.{yaml,yml,toml,jsonc,ini,cfg,conf,properties,sh,zsh,bash}"],"id":"config-comment-wall","key":"text:config-comment-wall","languages":["config"],"limitations":["Only groups of attached standalone comments at the same indentation level are compared."],"messageIds":[],"optionsSchema":null,"rationale":"Repeated comments that merely narrate adjacent configuration hide constraints and make the file harder to scan.","references":[],"remediation":"Name configuration entries clearly and keep comments only for constraints or rationale.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"four-entry config narration wall with 75% weak restatements","test":"packages/standards/tests/test_textlint.py"},{"aliases":["ephemeral-ai-artifact"],"autofix":"none","category":"maintainability","code":"SARJ302","defaultLevel":"error","engine":"text","examples":[{"expectedCount":0,"files":[{"path":"docs/operations.md","source":"# Operations\n\nRun `sarj-standards check` before merging.\n"}],"fixedFiles":[],"focusPath":"docs/operations.md","id":"maintained-operations-guide","outcome":"accept","title":"A durable operations guide records current usage"},{"expectedCount":1,"files":[{"path":"FIX-BRIEF.md","source":"# Temporary execution record\n"}],"fixedFiles":[],"focusPath":"FIX-BRIEF.md","id":"temporary-fix-brief","outcome":"reject","title":"A named fix brief is an execution artifact"}],"filePatterns":["**/*.md","**/*.mdx"],"id":"ephemeral-execution-artifact","key":"text:ephemeral-execution-artifact","languages":["markdown"],"limitations":["Short artifacts with neutral names and no execution-log headings are intentionally not inferred from prose alone."],"messageIds":[],"optionsSchema":null,"rationale":"Point-in-time execution narratives quickly become misleading and obscure the durable usage or design facts a repository needs.","references":[],"remediation":"Move durable facts into maintained documentation or issues, then delete the execution artifact.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"ephemeral execution brief, audit report, or change diary","test":"packages/standards/tests/test_textlint.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ303","defaultLevel":"error","engine":"text","examples":[{"expectedCount":0,"files":[{"path":".github/workflows/ci.yml","source":"jobs:\n  test:\n    steps:\n      - uses: actions/checkout@0123456789abcdef0123456789abcdef01234567\n"}],"fixedFiles":[],"focusPath":".github/workflows/ci.yml","id":"immutable-action-commit","outcome":"accept","title":"A full action commit SHA is immutable"},{"expectedCount":1,"files":[{"path":".github/workflows/ci.yml","source":"jobs:\n  test:\n    steps:\n      - uses: actions/checkout@v4\n"}],"fixedFiles":[],"focusPath":".github/workflows/ci.yml","id":"mutable-action-tag","outcome":"reject","title":"A version tag is mutable"}],"filePatterns":[".github/workflows/**/*.yaml",".github/workflows/**/*.yml"],"id":"unpinned-github-action","key":"text:unpinned-github-action","languages":["config"],"limitations":["Only remote uses entries in .github/workflows YAML files are checked; local actions are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Mutable action tags can resolve to different code without a reviewed repository change.","references":["https://docs.github.com/en/actions/security-for-github-actions/security-guides/security-hardening-for-github-actions"],"remediation":"Pin repository actions to a full commit SHA and container actions to a sha256 digest.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"remote GitHub Action or container action without an immutable digest","test":"packages/standards/tests/test_textlint.py"}],"schemaVersion":1}
