code-standards 7.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. code_standards-7.0.0.dist-info/METADATA +53 -0
  2. code_standards-7.0.0.dist-info/RECORD +99 -0
  3. code_standards-7.0.0.dist-info/WHEEL +4 -0
  4. code_standards-7.0.0.dist-info/entry_points.txt +3 -0
  5. code_standards-7.0.0.dist-info/licenses/LICENSE +21 -0
  6. sarj_standards/__init__.py +30 -0
  7. sarj_standards/__main__.py +5 -0
  8. sarj_standards/_meta.py +22 -0
  9. sarj_standards/api.py +890 -0
  10. sarj_standards/cli/__init__.py +0 -0
  11. sarj_standards/cli/main.py +2466 -0
  12. sarj_standards/configs/cli-reference.v1.json +1 -0
  13. sarj_standards/configs/doctor.config.json +22 -0
  14. sarj_standards/configs/eslint.application.mjs +1366 -0
  15. sarj_standards/configs/eslint.peers.json +44 -0
  16. sarj_standards/configs/eslint.strict.mjs +1060 -0
  17. sarj_standards/configs/markdownlint.strict.yaml +12 -0
  18. sarj_standards/configs/pyright.strict.json +96 -0
  19. sarj_standards/configs/ruff.application.toml +363 -0
  20. sarj_standards/configs/ruff.strict.toml +338 -0
  21. sarj_standards/configs/rule-inventory.v1.json +1 -0
  22. sarj_standards/configs/rule-ledger.json +846 -0
  23. sarj_standards/configs/rule-warning-levels.v1.json +1 -0
  24. sarj_standards/configs/taplo.strict.toml +14 -0
  25. sarj_standards/configs/yamllint.strict.yaml +25 -0
  26. sarj_standards/libs/__init__.py +0 -0
  27. sarj_standards/libs/adoption/__init__.py +0 -0
  28. sarj_standards/libs/adoption/configs.py +36 -0
  29. sarj_standards/libs/adoption/doctor.py +1346 -0
  30. sarj_standards/libs/adoption/exclusions.py +66 -0
  31. sarj_standards/libs/adoption/hooks.py +423 -0
  32. sarj_standards/libs/adoption/launcher.py +240 -0
  33. sarj_standards/libs/adoption/lifecycle.py +493 -0
  34. sarj_standards/libs/adoption/manifest.py +550 -0
  35. sarj_standards/libs/adoption/packagemanager.py +285 -0
  36. sarj_standards/libs/adoption/retired_suppressions.py +371 -0
  37. sarj_standards/libs/adoption/scaffold.py +1660 -0
  38. sarj_standards/libs/adoption/service.py +441 -0
  39. sarj_standards/libs/adoption/transaction.py +274 -0
  40. sarj_standards/libs/adoption/upgrade.py +516 -0
  41. sarj_standards/libs/adoption/uvtool.py +62 -0
  42. sarj_standards/libs/catalogs/__init__.py +9 -0
  43. sarj_standards/libs/catalogs/slack_automations.py +627 -0
  44. sarj_standards/libs/corpus/__init__.py +25 -0
  45. sarj_standards/libs/corpus/manifest.py +211 -0
  46. sarj_standards/libs/corpus/snapshot.py +222 -0
  47. sarj_standards/libs/diagnostics/__init__.py +65 -0
  48. sarj_standards/libs/diagnostics/analysis.schema.json +161 -0
  49. sarj_standards/libs/diagnostics/baseline.py +131 -0
  50. sarj_standards/libs/diagnostics/models.py +574 -0
  51. sarj_standards/libs/diagnostics/serialize.py +290 -0
  52. sarj_standards/libs/diagnostics/source.py +172 -0
  53. sarj_standards/libs/filesystem.py +11 -0
  54. sarj_standards/libs/linting/__init__.py +0 -0
  55. sarj_standards/libs/linting/analysis.py +422 -0
  56. sarj_standards/libs/linting/external.py +1454 -0
  57. sarj_standards/libs/linting/library_policy.py +688 -0
  58. sarj_standards/libs/linting/policy.py +152 -0
  59. sarj_standards/libs/linting/runner.py +442 -0
  60. sarj_standards/libs/linting/textlint.py +1605 -0
  61. sarj_standards/libs/release/__init__.py +98 -0
  62. sarj_standards/libs/release/_values.py +24 -0
  63. sarj_standards/libs/release/artifacts.py +191 -0
  64. sarj_standards/libs/release/causality.py +80 -0
  65. sarj_standards/libs/release/changes.py +48 -0
  66. sarj_standards/libs/release/process.py +128 -0
  67. sarj_standards/libs/release/publish.py +85 -0
  68. sarj_standards/libs/release/registry.py +271 -0
  69. sarj_standards/libs/release/release_age.py +218 -0
  70. sarj_standards/libs/release/rollout.py +1163 -0
  71. sarj_standards/libs/release/tags.py +373 -0
  72. sarj_standards/libs/release/typescript.py +191 -0
  73. sarj_standards/libs/repository/__init__.py +0 -0
  74. sarj_standards/libs/repository/cli_reference_artifact.py +324 -0
  75. sarj_standards/libs/repository/comment_corpus.py +536 -0
  76. sarj_standards/libs/repository/config_generation.py +146 -0
  77. sarj_standards/libs/repository/docs.py +347 -0
  78. sarj_standards/libs/repository/hooks.py +118 -0
  79. sarj_standards/libs/repository/ledger.py +99 -0
  80. sarj_standards/libs/repository/repository.py +744 -0
  81. sarj_standards/libs/repository/rule_authoring.py +246 -0
  82. sarj_standards/libs/repository/rule_catalog_artifact.py +479 -0
  83. sarj_standards/libs/repository/rule_changes.py +318 -0
  84. sarj_standards/libs/repository/rule_inventory_artifact.py +142 -0
  85. sarj_standards/libs/repository/rule_lifecycle.py +167 -0
  86. sarj_standards/libs/repository/rule_maintenance.py +225 -0
  87. sarj_standards/libs/rules/__init__.py +74 -0
  88. sarj_standards/libs/rules/catalog.py +145 -0
  89. sarj_standards/libs/rules/contracts.py +382 -0
  90. sarj_standards/libs/rules/corpus_runner.py +365 -0
  91. sarj_standards/libs/rules/evaluation.py +177 -0
  92. sarj_standards/libs/setup/__init__.py +4 -0
  93. sarj_standards/libs/setup/repository.py +40 -0
  94. sarj_standards/py.typed +0 -0
  95. sarj_standards/schemas/__init__.py +4 -0
  96. sarj_standards/schemas/_paths.py +7 -0
  97. sarj_standards/schemas/rule-catalog.v1.json +1 -0
  98. sarj_standards/schemas/rule-catalog.v1.schema.json +112 -0
  99. sarj_standards/schemas/slack-automations.v1.schema.json +1751 -0
@@ -0,0 +1 @@
1
+ {"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 value = 'a'; const result = parse(value); expect(result.ok).toBe(true); expect(result.value).toBe(value); });\ntest('accepts b', () => { const value = 'b'; const result = parse(value); expect(result.ok).toBe(true); expect(result.value).toBe(value); });"}],"fixedFiles":[],"focusPath":"src/user.test.ts","id":"copied-sibling-tests","outcome":"reject","scenarioId":"primary","title":"Sibling tests repeat the same body"},{"expectedCount":0,"files":[{"path":"src/user.test.ts","source":"test.each(['a', 'b'])('accepts %s', (value) => { const result = parse(value); expect(result.ok).toBe(true); expect(result.value).toBe(value); });"}],"fixedFiles":[],"focusPath":"src/user.test.ts","id":"parameterized-cases","outcome":"accept","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"warning","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/policy.test.ts","source":"import { readFileSync } from 'node:fs'; test('policy', () => { const plan = JSON.parse(readFileSync('plan.json', 'utf8')); expect(validate(plan)).toEqual([]); });"}],"fixedFiles":[],"focusPath":"src/policy.test.ts","id":"rendered-plan-contract","outcome":"accept","scenarioId":"primary","title":"Assert on rendered plan behavior"},{"expectedCount":1,"files":[{"path":"src/policy.test.ts","source":"import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('main.tf', 'utf8'); expect(source).toMatch(/prevent_destroy/); });"}],"fixedFiles":[],"focusPath":"src/policy.test.ts","id":"terraform-substring-contract","outcome":"reject","scenarioId":"primary","title":"Do not prove Terraform behavior with a regex"}],"filePatterns":[],"id":"iac-source-coupled-test","key":"eslint:iac-source-coupled-test","languages":["typescript"],"limitations":["The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.","The warning-stage rule remains suppressible for calibration; promotion may make the locked policy non-suppressible."],"messageIds":["rawSourceOracle"],"optionsSchema":null,"rationale":"Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently.","references":[],"remediation":"Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.","since":null,"source":"packages/typescript/src/rules/iac-source-coupled-test.ts","status":"active","summary":"Disallow raw IaC source text as a test oracle; inspect a rendered plan, provider state, or runtime behavior.","test":"packages/typescript/tests/rules/iac-source-coupled-test.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"warning","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/codec.test.ts","source":"test('decodes', () => { try { decode(); } catch { return; } expect(result()).toBe('ok'); });"}],"fixedFiles":[],"focusPath":"src/codec.test.ts","id":"bare-return","outcome":"reject","scenarioId":"primary","title":"Do not silently pass"},{"expectedCount":0,"files":[{"path":"src/codec.test.ts","source":"test('decodes', () => { try { decode(); } catch (error) { throw error; } expect(result()).toBe('ok'); });"}],"fixedFiles":[],"focusPath":"src/codec.test.ts","id":"rethrow","outcome":"accept","scenarioId":"primary","title":"Preserve the failure"}],"filePatterns":["**/*.test.*","**/*.spec.*","**/tests/**","**/__tests__/**"],"id":"no-bare-return-from-test-catch","key":"eslint:no-bare-return-from-test-catch","languages":["typescript"],"limitations":["Only bare returns owned by a direct supported test callback and followed lexically by a framework assertion are reported."],"messageIds":["bareReturnFromTestCatch"],"optionsSchema":null,"rationale":"The caught failure turns into a passing test without executing the assertion that follows it.","references":[],"remediation":"Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.","since":null,"source":"packages/typescript/src/rules/no-bare-return-from-test-catch.ts","status":"active","summary":"Disallow a bare return from a test catch block when it skips a later assertion.","test":"packages/typescript/tests/rules/no-bare-return-from-test-catch.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","scenarioId":"primary","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","scenarioId":"primary","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/counter.ts","source":"// increment the counter for PLT-812\ncounter += 1;"}],"fixedFiles":[],"focusPath":"src/counter.ts","id":"owned-context","outcome":"accept","scenarioId":"primary","title":"A reference records why the counter changes"},{"expectedCount":1,"files":[{"path":"src/counter.ts","source":"// increment the counter\ncounter += 1;"}],"fixedFiles":[],"focusPath":"src/counter.ts","id":"redundant-narration","outcome":"reject","scenarioId":"primary","title":"A comment repeats the statement below"}],"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":"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","scenarioId":"primary","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","scenarioId":"primary","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":"security","code":null,"defaultLevel":"warning","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"next.config.mjs","source":"export default { images: { dangerouslyAllowSVG: false } };\n"}],"fixedFiles":[],"focusPath":"next.config.mjs","id":"svg-disabled","outcome":"accept","scenarioId":"primary","title":"Keep active SVG delivery disabled"},{"expectedCount":1,"files":[{"path":"next.config.mjs","source":"export default { images: { dangerouslyAllowSVG: true } };\n"}],"fixedFiles":[],"focusPath":"next.config.mjs","id":"svg-enabled","outcome":"reject","scenarioId":"primary","title":"Do not enable active SVG delivery"}],"filePatterns":[],"id":"no-dangerously-allow-svg","key":"eslint:no-dangerously-allow-svg","languages":["typescript"],"limitations":["Only a literal true assigned to dangerouslyAllowSVG in a next.config source file is reported; computed or imported configuration is intentionally not inferred."],"messageIds":["noDangerouslyAllowSvg"],"optionsSchema":null,"rationale":"SVG files can contain scripts and other active content; enabling dangerouslyAllowSVG makes the image optimizer serve that content from the application origin.","references":[],"remediation":"Keep dangerouslyAllowSVG disabled. If SVG delivery is unavoidable, use a separately reviewed asset path with restrictive Content-Disposition and Content-Security-Policy headers.","since":null,"source":"packages/typescript/src/rules/no-dangerously-allow-svg.ts","status":"active","summary":"Next.js image configuration enables unsanitized SVG rendering","test":"packages/typescript/tests/rules/no-dangerously-allow-svg.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","scenarioId":"primary","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","scenarioId":"primary","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":"performance","code":null,"defaultLevel":"warning","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"src/refresh.ts","source":"import { useRouter } from \"next/navigation\"; const router = useRouter(); const refresh = () => router.refresh(); window.addEventListener(\"focus\", refresh); document.addEventListener(\"visibilitychange\", refresh);"}],"fixedFiles":[],"focusPath":"src/refresh.ts","id":"duplicate-lifecycle-signals","outcome":"reject","scenarioId":"primary","title":"Do not duplicate a route refresh"},{"expectedCount":0,"files":[{"path":"src/refresh.ts","source":"import { useRouter } from \"next/navigation\"; const router = useRouter(); const refresh = () => router.refresh(); window.addEventListener(\"focus\", refresh);"}],"fixedFiles":[],"focusPath":"src/refresh.ts","id":"single-lifecycle-signal","outcome":"accept","scenarioId":"primary","title":"Listen to one lifecycle signal"}],"filePatterns":[],"id":"no-duplicate-lifecycle-refresh-listeners","key":"eslint:no-duplicate-lifecycle-refresh-listeners","languages":["typescript"],"limitations":["Only active direct window focus and document visibilitychange statements in the same block whose shared identifier callback directly calls refresh on a next/navigation useRouter binding are inspected; matching direct removals are honored, and generated and test files are excluded."],"messageIds":["duplicateLifecycleRefresh"],"optionsSchema":null,"rationale":"A browser tab activation can emit both lifecycle signals and invoke the same route-wide refresh twice.","references":[],"remediation":"Choose one lifecycle signal or route both signals through an explicitly debounced refresh policy.","since":null,"source":"packages/typescript/src/rules/no-duplicate-lifecycle-refresh-listeners.ts","status":"active","summary":"Do not register one Next.js route-refresh callback for both focus and visibilitychange.","test":"packages/typescript/tests/rules/no-duplicate-lifecycle-refresh-listeners.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","scenarioId":"primary","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","scenarioId":"primary","title":"A runtime value is interpolated into SQL"}],"filePatterns":[],"id":"no-dynamic-sql","key":"eslint:no-dynamic-sql","languages":["typescript"],"limitations":["The rule reports only visibly quoted runtime values; dynamic identifiers and unquoted fragments require provenance that syntax-only linting cannot prove.","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 values embedded inside quoted SQL values 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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":{"additionalProperties":false,"properties":{"max":{"minimum":1,"type":"integer"}},"type":"object"},"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","scenarioId":"primary","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","scenarioId":"primary","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":"Choose a responsibility-bearing module name or colocate the export with its domain.","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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; tests, stories, generated files, and the design-system implementation are excluded."],"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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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 catch-binding and constructor provenance 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","scenarioId":"primary","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","scenarioId":"primary","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":1,"files":[{"path":"src/chart.ts","source":"/**\n * The ports chart shows arrivals and departures across the network.\n * It was originally a line chart, but the lines crossed too often.\n * Bars make adjacent ports easier to compare at a glance.\n * The axis starts at zero so visual differences stay proportional.\n * A single series uses the site navy for brand consistency.\n * Empty ports use a neutral ink so missing traffic remains visible.\n * Tooltip values repeat the units shown on the vertical axis.\n * The chart intentionally keeps labels horizontal on wide screens.\n */\nexport function PortBars() { return null; }"}],"fixedFiles":[],"focusPath":"src/chart.ts","id":"prose-wall","outcome":"reject","scenarioId":"primary","title":"One paragraph narrates a chart's design history"},{"expectedCount":0,"files":[{"path":"src/composer.ts","source":"/**\n * Shared composer.\n *\n * It serves the room. It serves task comments. It stays visually calm. It accepts attachments.\n *\n * The `tone` prop supplies task styling. The `leadingTools` prop supplies controls.\n * The parent owns uploads. The component owns focus.\n */\n\nexport const Composer = forwardRef(function Composer() { return null; });"}],"fixedFiles":[],"focusPath":"src/composer.ts","id":"structured-jsdoc","outcome":"accept","scenarioId":"primary","title":"Paragraphs separate a component's durable constraints"}],"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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","title":"Do not expose positional fields"}],"filePatterns":[],"id":"no-positional-tuple-return","key":"eslint:no-positional-tuple-return","languages":["typescript"],"limitations":["Declared or syntax-proven multi-field tuple returns on named functions and public type surfaces are inspected; anonymous inline callbacks and syntax-proven TanStack Query key factories are excluded."],"messageIds":["noPositionalTupleReturn"],"optionsSchema":null,"rationale":"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 a named 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":"security","code":null,"defaultLevel":"warning","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"next.config.mjs","source":"export default { productionBrowserSourceMaps: false };\n"}],"fixedFiles":[],"focusPath":"next.config.mjs","id":"private-source-maps","outcome":"accept","scenarioId":"primary","title":"Keep browser source maps private"},{"expectedCount":1,"files":[{"path":"next.config.mjs","source":"export default { productionBrowserSourceMaps: true };\n"}],"fixedFiles":[],"focusPath":"next.config.mjs","id":"public-source-maps","outcome":"reject","scenarioId":"primary","title":"Do not publish production browser source maps"}],"filePatterns":[],"id":"no-production-browser-source-maps","key":"eslint:no-production-browser-source-maps","languages":["typescript"],"limitations":["Only a literal true assigned in a next.config source file is reported; computed or imported configuration is intentionally not inferred."],"messageIds":["noProductionBrowserSourceMaps"],"optionsSchema":null,"rationale":"Next.js production browser source maps publish original client source and implementation details to every browser that can load the deployment.","references":[],"remediation":"Leave productionBrowserSourceMaps disabled and upload private source maps directly to the error-monitoring service during the build.","since":null,"source":"packages/typescript/src/rules/no-production-browser-source-maps.ts","status":"active","summary":"Next.js production browser source maps expose application source","test":"packages/typescript/tests/rules/no-production-browser-source-maps.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"suggestion","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","scenarioId":"primary","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","scenarioId":"primary","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":["deleteComment","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"performance","code":null,"defaultLevel":"warning","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/status.tsx","source":"import { useRouter } from \"next/navigation\"; const router = useRouter(); setInterval(() => fetchStatus(), POLLING_INTERVAL_MS);"}],"fixedFiles":[],"focusPath":"src/status.tsx","id":"poll-named-action","outcome":"accept","scenarioId":"primary","title":"Poll a named action"},{"expectedCount":1,"files":[{"path":"src/status.tsx","source":"import { useRouter } from \"next/navigation\"; const router = useRouter(); setInterval(() => router.refresh(), POLLING_INTERVAL_MS);"}],"fixedFiles":[],"focusPath":"src/status.tsx","id":"poll-router-refresh","outcome":"reject","scenarioId":"primary","title":"Do not poll the whole route"}],"filePatterns":[],"id":"no-router-refresh-polling","key":"eslint:no-router-refresh-polling","languages":["typescript"],"limitations":["Only router bindings created from next/navigation useRouter and direct setInterval or window.setInterval callbacks are inspected; generated and test files are excluded."],"messageIds":["routerRefreshPolling"],"optionsSchema":null,"rationale":"A route refresh refetches and rerenders the whole route on every tick instead of loading the named resource that changed.","references":[],"remediation":"Call the dedicated fetch or server action from the timer and keep the polling interval in a named constant.","since":null,"source":"packages/typescript/src/rules/no-router-refresh-polling.ts","status":"active","summary":"Do not poll by calling a Next.js router's refresh method from a timer.","test":"packages/typescript/tests/rules/no-router-refresh-polling.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"warning","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"status-card.tsx","source":"'use client';\nimport { CLIENT_SETTINGS } from '@/client-settings';\nexport const StatusCard = () => <p>{CLIENT_SETTINGS.apiOrigin}</p>;\n"}],"fixedFiles":[],"focusPath":"status-card.tsx","id":"public-client-settings","outcome":"accept","scenarioId":"primary","title":"Import a browser-safe settings boundary"},{"expectedCount":1,"files":[{"path":"status-card.tsx","source":"'use client';\nimport { SERVER_SETTINGS } from '@/server-settings';\nexport const StatusCard = () => <p>{SERVER_SETTINGS.apiOrigin}</p>;\n"}],"fixedFiles":[],"focusPath":"status-card.tsx","id":"server-settings-client-import","outcome":"reject","scenarioId":"primary","title":"Do not pull server settings into a client bundle"}],"filePatterns":[],"id":"no-server-env-in-client-component","key":"eslint:no-server-env-in-client-component","languages":["typescript"],"limitations":["Only static value imports in files with a top-level 'use client' directive and conventionally named server-env/server-settings modules are checked."],"messageIds":["noServerEnvInClientComponent"],"optionsSchema":null,"rationale":"Next.js client modules run in the browser, where server-only environment values are unavailable; importing a server settings module can produce undefined configuration or bundle a secret-bearing module into the client graph.","references":[],"remediation":"Pass an explicitly public value from a Server Component, or import it from a separately validated client-settings module backed only by NEXT_PUBLIC_* values.","since":null,"source":"packages/typescript/src/rules/no-server-env-in-client-component.ts","status":"active","summary":"server-only environment settings imported by a client component","test":"packages/typescript/tests/rules/no-server-env-in-client-component.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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, recognizes only configured storage method names, and requires storage-like receiver evidence for the overloaded `put` method."],"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","scenarioId":"primary","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","scenarioId":"primary","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","noStringReduce"],"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","scenarioId":"primary","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","scenarioId":"primary","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":"suggestion","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","scenarioId":"primary","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","scenarioId":"primary","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":["deleteNarration","narratesValue","removeNarration"],"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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","title":"Keep behavior that the signature cannot express"},{"expectedCount":1,"files":[{"path":"src/client.ts","source":"/** @param userId the user identifier */\nexport function fetchValue(userId: string): number { return 1; }"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"restated-parameter","outcome":"reject","scenarioId":"primary","title":"Do not expand a typed parameter's name"}],"filePatterns":[],"id":"no-typed-doc-sections","key":"eslint:no-typed-doc-sections","languages":["typescript"],"limitations":["Description-free or name-restating 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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/adapter.ts","source":"// @ts-expect-error -- vendor types omit the runtime requestId field\nreturn response.requestId;"}],"fixedFiles":[],"focusPath":"src/adapter.ts","id":"concrete-runtime-mismatch","outcome":"accept","scenarioId":"primary","title":"Explain the concrete runtime contract"},{"expectedCount":1,"files":[{"path":"src/adapter.ts","source":"// @ts-expect-error -- false positive\nreturn response.requestId;"}],"fixedFiles":[],"focusPath":"src/adapter.ts","id":"generic-suppression-reason","outcome":"reject","scenarioId":"primary","title":"Reject a suppression with no auditable reason"}],"filePatterns":[],"id":"no-vague-suppression-description","key":"eslint:no-vague-suppression-description","languages":["typescript"],"limitations":["Only ESLint disable comments and TypeScript expect-error directives are checked.","The rule uses a small anchored vocabulary and does not score prose quality generally.","Generated files and descriptions containing any concrete context are excluded."],"messageIds":["vagueDescription"],"optionsSchema":null,"rationale":"Generic phrases satisfy require-description mechanically while leaving reviewers unable to audit the risk or remove stale debt.","references":[],"remediation":"Name the exact type/runtime mismatch, external contract, or safety invariant that makes this suppression acceptable.","since":null,"source":"packages/typescript/src/rules/no-vague-suppression-description.ts","status":"active","summary":"Require suppression descriptions to name the concrete mismatch or invariant instead of a generic non-reason.","test":"packages/typescript/tests/rules/no-vague-suppression-description.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","scenarioId":"primary","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","scenarioId":"primary","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":"maintainability","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/load.ts","source":"async function load() { const value = await Promise.resolve(1); return value + 1; }"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"explicit-async-transform","outcome":"accept","scenarioId":"primary","title":"Use explicit async control flow"},{"expectedCount":1,"files":[{"path":"src/load.ts","source":"async function load() { return Promise.resolve(1).then((value) => value + 1); }"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"returned-then-transform","outcome":"reject","scenarioId":"primary","title":"Do not directly return a Promise callback chain from async code"}],"filePatterns":[],"id":"prefer-await-in-async-return","key":"eslint:prefer-await-in-async-return","languages":["typescript"],"limitations":["Only a single directly returned `.then` call with an inline callback is checked.","The receiver must be proven Promise-like by TypeScript; untyped files and larger chains are intentionally ignored.","Direct loader callbacks passed to resolved `React.lazy` and `next/dynamic` imports are excluded because returning the module Promise is their framework contract."],"messageIds":["preferAwait"],"optionsSchema":null,"rationale":"Mixing a directly returned Promise callback into otherwise async control flow makes sequencing and failures harder to read.","references":[],"remediation":"Await the Promise, then return the transformed value with ordinary async statements.","since":"15.6.3","source":"packages/typescript/src/rules/prefer-await-in-async-return.ts","status":"active","summary":"Prefer explicit `await` when an async function directly returns one typed Promise `.then` transform.","test":"packages/typescript/tests/rules/prefer-await-in-async-return.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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'; import { InputGroup } from '@/components/ui/input-group'; const field = <div><Search /><Input /></div>;"}],"fixedFiles":[],"focusPath":"src/search.tsx","id":"loose-search-input","outcome":"reject","scenarioId":"primary","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.","The file must import InputGroup, proving that the repository has adopted that optional primitive."],"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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"'use client'; await fetch('/api/tasks', { method: 'POST', body });"}],"fixedFiles":[],"focusPath":"app/tasks/page.tsx","id":"api-mutation","outcome":"reject","scenarioId":"primary","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","scenarioId":"primary","title":"Call a Server Action"}],"filePatterns":[],"id":"prefer-server-actions","key":"eslint:prefer-server-actions","languages":["typescript"],"limitations":["Only statically recognizable /api/ mutations in modules with positive Next.js evidence are reported: an explicit next import, or an app/pages path with a top-level use-client directive."],"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":"import { Card } from '@/components/ui/card'; const action = <button>Save</button>;"}],"fixedFiles":[],"focusPath":"src/form.tsx","id":"raw-button","outcome":"reject","scenarioId":"primary","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","scenarioId":"primary","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.","Tests and the shared components/ui primitive implementation tree are excluded."],"messageIds":["preferShadcnPrimitive"],"optionsSchema":{"additionalProperties":false,"properties":{"assumeAvailable":{"type":"boolean"}},"type":"object"},"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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","title":"Infer the schema type"}],"filePatterns":[],"id":"prefer-zod-infer","key":"eslint:prefer-zod-infer","languages":["typescript"],"limitations":[],"messageIds":["handWrittenTwin","repeatedEnumUnion"],"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":"testing","code":null,"defaultLevel":"warning","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/parser.test.ts","source":"test.each([['a', true], ['b', false], ['c', true]])('parses %s', (input, expected) => { expect(parse(input)).toBe(expected); });"}],"fixedFiles":[],"focusPath":"src/parser.test.ts","id":"parameterized","outcome":"accept","scenarioId":"primary","title":"Name each case"},{"expectedCount":1,"files":[{"path":"src/parser.test.ts","source":"test('parses', () => { expect(parse('a')).toBe(true); expect(parse('b')).toBe(false); expect(parse('c')).toBe(true); });"}],"fixedFiles":[],"focusPath":"src/parser.test.ts","id":"repeated","outcome":"reject","scenarioId":"primary","title":"Do not repeat literal cases"}],"filePatterns":["**/*.test.*","**/*.spec.*","**/tests/**","**/__tests__/**"],"id":"repeated-static-call-cases","key":"eslint:repeated-static-call-cases","languages":["typescript"],"limitations":["Only consecutive top-level assertions with direct calls and entirely static inputs and expected values are reported."],"messageIds":["repeatedStaticCallCases"],"optionsSchema":null,"rationale":"Copy-pasted cases obscure the input table and stop later cases from being reported after the first failure.","references":[],"remediation":"Replace the repeated assertions with a named `test.each` or `it.each` table.","since":null,"source":"packages/typescript/src/rules/repeated-static-call-cases.ts","status":"active","summary":"Report three or more consecutive literal call assertions that should be independently named test cases.","test":"packages/typescript/tests/rules/repeated-static-call-cases.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"correctness","code":null,"defaultLevel":"warning","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"profile-form.tsx","source":"import { useForm } from 'react-hook-form';\nconst form = useForm({ defaultValues: { name: '' } });\n"}],"fixedFiles":[],"focusPath":"profile-form.tsx","id":"form-with-initial-values","outcome":"accept","scenarioId":"primary","title":"Give the form an explicit initial shape"},{"expectedCount":1,"files":[{"path":"profile-form.tsx","source":"import { useForm } from 'react-hook-form';\nconst form = useForm({ mode: 'onChange' });\n"}],"fixedFiles":[],"focusPath":"profile-form.tsx","id":"form-without-initial-values","outcome":"reject","scenarioId":"primary","title":"Do not leave form initialization implicit"}],"filePatterns":[],"id":"require-use-form-default-values","key":"eslint:require-use-form-default-values","languages":["typescript"],"limitations":["Only direct calls to a scope-resolved useForm value imported from react-hook-form are checked; wrapper hooks and computed option objects are intentionally not inferred."],"messageIds":["requireUseFormDefaultValues"],"optionsSchema":null,"rationale":"Without an explicit initial value, fields can change from uncontrolled to controlled as data arrives, reset behavior becomes ambiguous, and the form's initial shape no longer documents the values users can edit.","references":[],"remediation":"Pass an object with a defaultValues property to useForm; use empty strings, nulls, or schema-appropriate values deliberately for every controlled field.","since":null,"source":"packages/typescript/src/rules/require-use-form-default-values.ts","status":"active","summary":"react-hook-form useForm call without defaultValues","test":"packages/typescript/tests/rules/require-use-form-default-values.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"app/orders/actions.ts","source":"'use server';\nexport async function cancelOrder() {}\n"}],"fixedFiles":[],"focusPath":"app/orders/actions.ts","id":"server-action-module","outcome":"accept","scenarioId":"primary","title":"Mark the action module as server-only"},{"expectedCount":1,"files":[{"path":"app/orders/actions.ts","source":"export async function cancelOrder() {}\n"}],"fixedFiles":[],"focusPath":"app/orders/actions.ts","id":"unmarked-action-module","outcome":"reject","scenarioId":"primary","title":"Do not rely on the filename to create a Server Action"}],"filePatterns":[],"id":"require-use-server-in-actions-file","key":"eslint:require-use-server-in-actions-file","languages":["typescript"],"limitations":["Only exported async functions in actions.ts or *-actions.ts below an app directory are checked; other naming schemes and inline Server Actions are intentionally outside the rule."],"messageIds":["requireUseServerInActionsFile"],"optionsSchema":null,"rationale":"An exported async function is not callable as a Server Action merely because its file is named actions.ts. Without the module directive, a client import can fail or pull server-only implementation details across the client boundary.","references":[],"remediation":"Put 'use server' at the start of the route action module.","since":null,"source":"packages/typescript/src/rules/require-use-server-in-actions-file.ts","status":"active","summary":"route action module missing the use server directive","test":"packages/typescript/tests/rules/require-use-server-in-actions-file.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","scenarioId":"primary","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","scenarioId":"primary","title":"Validate the form value"}],"filePatterns":[],"id":"require-zod-form-validation","key":"eslint:require-zod-form-validation","languages":["typescript"],"limitations":["Tests are excluded; imported schema-shaped names are trusted when their implementation is outside the linted file.","Delayed raw-value use is accepted only after an unconditional successful parse in the same block; safeParse remains valid when the raw binding has no unvalidated consumer."],"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":"testing","code":null,"defaultLevel":"error","engine":"eslint","examples":[{"expectedCount":0,"files":[{"path":"src/policy.test.ts","source":"import { readFileSync } from 'node:fs'; test('policy', () => { const policy = JSON.parse(readFileSync('policy.json', 'utf8')); expect(validate(policy)).toEqual([]); });"}],"fixedFiles":[],"focusPath":"src/policy.test.ts","id":"parsed-policy-contract","outcome":"accept","scenarioId":"primary","title":"Assert on parsed policy behavior"},{"expectedCount":1,"files":[{"path":"src/policy.test.ts","source":"import { readFileSync } from 'node:fs'; test('policy', () => { const source = readFileSync('workflow.yml', 'utf8'); expect(source).toMatch(/permissions/); });"}],"fixedFiles":[],"focusPath":"src/policy.test.ts","id":"workflow-substring-contract","outcome":"reject","scenarioId":"primary","title":"Do not prove workflow behavior with a regex"}],"filePatterns":[],"id":"source-coupled-test","key":"eslint:source-coupled-test","languages":["typescript"],"limitations":["The rule follows lexical aliases, source-path collections, awaited reads, and common text operations; interprocedural flows remain unreported.","When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."],"messageIds":["rawSourceOracle"],"optionsSchema":null,"rationale":"Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.","references":[],"remediation":"Parse the artifact, execute its validator, or assert on another runtime contract.","since":null,"source":"packages/typescript/src/rules/source-coupled-test.ts","status":"active","summary":"Disallow raw repository source text as a test oracle; parse or execute the artifact instead.","test":"packages/typescript/tests/rules/source-coupled-test.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","scenarioId":"primary","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","scenarioId":"primary","title":"Do not lead with a sole-caller helper"}],"filePatterns":[],"id":"stepdown","key":"eslint:stepdown","languages":["typescript"],"limitations":["Generated and test files, cycles, dynamic references, overload targets, and helpers with multiple callers are excluded.","Class methods are reported only when both helper and caller are private so accessibility ordering remains authoritative.","Runtime class-field, static-block, computed-member, and decorator barriers are never crossed."],"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 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","scenarioId":"primary","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","scenarioId":"primary","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 callable named as an enqueue, seed, migration, schedule, ensure, or upsert promises replay safety.","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 embedded inserts in explicitly replayable callables to carry conflict handling.","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","scenarioId":"primary","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","scenarioId":"primary","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":"safe","category":"testing","code":null,"defaultLevel":"warning","engine":"eslint","examples":[{"expectedCount":1,"files":[{"path":"widget.test.ts","source":"// Arrange\nconst widget = makeWidget();"}],"fixedFiles":[{"path":"widget.test.ts","source":"const widget = makeWidget();"}],"focusPath":"widget.test.ts","id":"bare-phase-label","outcome":"reject","scenarioId":"primary","title":"Bare phase label is removed"},{"expectedCount":0,"files":[{"path":"widget.test.ts","source":"// Then the retry loop would spin forever.\nexpect(run()).toBe(true);"}],"fixedFiles":[],"focusPath":"widget.test.ts","id":"behavioral-comment","outcome":"accept","scenarioId":"primary","title":"Behavioral consequence is retained"}],"filePatterns":[],"id":"test-phase-label-comment","key":"eslint:test-phase-label-comment","languages":["typescript"],"limitations":["Only standalone line comments in recognized test files are checked.","Comments inside bracketed expressions or containing words outside the bounded phase grammar are preserved."],"messageIds":["removeLabel"],"optionsSchema":null,"rationale":"Phase labels narrate test structure without explaining behavior and often hide unclear names or oversized tests.","references":[],"remediation":"Delete the label; if the phases remain hard to follow, extract a named helper or split the test.","since":null,"source":"packages/typescript/src/rules/test-phase-label-comment.ts","status":"active","summary":"Tests must not use bare Arrange, Act, Assert, Given, When, or Then phase comments.","test":"packages/typescript/tests/rules/test-phase-label-comment.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","scenarioId":"primary","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","scenarioId":"primary","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":5,"files":[{"path":"main.tf","source":"# resource \"google_storage_bucket\" \"old\" {\n# name = \"legacy-artifacts\"\n# location = \"US\"\n# force_destroy = true\n# }\nresource \"google_storage_bucket\" \"new\" {}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"commented-resource","outcome":"reject","scenarioId":"primary","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","scenarioId":"primary","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.","Testdata and fixture trees may encode removed configuration as test input, so only banners are checked there.","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":"maintainability","code":"SARJ205","defaultLevel":"warning","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"env/dev/terraform.tfvars","source":"pagerduty_enabled = false\n"},{"path":"env/prod/terraform.tfvars","source":"pagerduty_enabled = \"false\"\n"},{"path":"variables.tf","source":"variable \"pagerduty_enabled\" {\n type = string\n default = \"true\"\n}\n"}],"fixedFiles":[],"focusPath":"env/dev/terraform.tfvars","id":"flag-constant-in-every-environment","outcome":"reject","scenarioId":"primary","title":"Boolean assigned the same semantic value in every environment"},{"expectedCount":0,"files":[{"path":"env/dev/terraform.tfvars","source":"redis_tier = \"BASIC\"\n"},{"path":"env/prod/terraform.tfvars","source":"redis_tier = \"STANDARD_HA\"\n"},{"path":"variables.tf","source":"variable \"redis_tier\" {\n type = string\n}\n"}],"fixedFiles":[],"focusPath":"env/dev/terraform.tfvars","id":"input-that-actually-varies","outcome":"accept","scenarioId":"primary","title":"Data input carrying a real per-environment difference"}],"filePatterns":[],"id":"no-dead-environment-input","key":"iac:no-dead-environment-input","languages":["iac"],"limitations":["A variable declared but never assigned in any tfvars is NOT flagged: in the measured corpus 52 of 67 such variables were module plumbing wired from parent calls, not dead flags.","Scope is root modules only — a root is a directory whose own .tf files declare at least one variable, with tfvars beside it or under an env/<name>/ layout. Shared modules without tfvars, and tfvars with no such root within two directory levels, produce nothing.","When the root's own configuration names an environment whose inputs are not readable on disk — an envs.json entry with tfvars held in a secret, an env directory without a tfvars file while siblings have one, or a JSON-only tfvars — the rule reports a blind-environment error and suppresses constant-everywhere and required-but-constant for that root instead of computing them from the visible subset. Default-equal and orphaned findings remain valid per file.","The blind-environment error is attributed once, to the root's first tfvars file in path order, so a changed-files run that omits that file shows no root-level error; the full-tree scan is the gate that always sees it.","Values compare semantically: \"false\" equals false and \"1\" equals 1 (HCL's string conversions), 1 equals 1.0 (HCL has one number type), lists and objects compare structurally; bool never equals number. Heredocs and interpolations are opaque (bodies are masked) and never compare equal, so a constant heredoc goes undetected rather than misread.","Cross-environment findings need at least two on-disk environment tfvars; a single-environment root gets only default-equal and orphaned-key findings.","A default-equal assignment is NOT flagged when a sibling environment gives the variable a different value: the line is the parallel entry for a knob in use, and deleting it hides the knob. A blind root reports no value-based finding at all, since the environment it cannot read is exactly the one whose override would keep the line.","A variable declared `type = any` is left alone: Terraform performs none of the string conversions this rule's comparison relies on, so `\"false\"` beside `false` is two values there.","An environment is named by the tfvars file's own stem, except for terraform.tfvars, which takes the name of its directory — so env/<name>/terraform.tfvars and env/<name>.tfvars both resolve. Files are keyed by resolved path, so a symlinked alias is not a second environment.","Root-level terraform.tfvars and *.auto.tfvars are auto-loaded into every plan, so they are one environment between them, never one each. Beside named environment files they are a baseline whose precedence this rule does not model, and the root reports blind-environment.","A key assigned twice in one file is a redefinition Terraform refuses to load, so that environment is reported blind rather than judged on whichever assignment came last.","The blind-environment error is suppressed by a `# sarj-noqa: SARJ205` on line 1 of the file it is reported on; scope such a suppression to the code it means to silence.","When two files define one environment (a root-level <env>.tfvars beside an env/<env>/ file) and they assign a variable different values, the effective input depends on var-file order, which is not observable here: the rule reports a blind-environment error rather than picking one.","Only boolean, number and null values are printed in a diagnostic. A tfvars string, list or map is routinely a password or token, and lint output reaches CI logs and PR annotations that the (often gitignored) tfvars file never reaches, so those values are named and never echoed.","A tfvars file or env directory labelled backup, bak, old, copy, tmp, example, sample or template is a copy or specimen, not a deployment: it is neither linted nor counted as an environment, because comparing a file against its own backup makes every shared line constant.","*.tfvars.json and *.tf.json are not parsed. A JSON tfvars makes its environment blind rather than silently half-read."],"messageIds":[],"optionsSchema":null,"rationale":"An input assigned one semantic value in every environment, or its declared default, or a variable that no longer exists, is indirection with no decision behind it — reviewers keep re-reading it.","references":[],"remediation":"Inline the constant as the variable's default and delete the per-environment assignments; delete assignments equal to the default; delete assignments for variables the root no longer declares.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_dead_environment_input.py","status":"active","summary":"A per-environment tfvars input must vary or exist: constant-everywhere, default-equal, and undeclared assignments are dead configuration.","test":"packages/iac/tests/rules/test_no_dead_environment_input.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ204","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"sandbox.tf","source":"resource \"google_storage_bucket\" \"cache\" {\n count = var.environment == \"sandbox\" ? 1 : 0\n name = \"cache\"\n}\n"}],"fixedFiles":[],"focusPath":"sandbox.tf","id":"environment-gated-resource","outcome":"reject","scenarioId":"primary","title":"Resource gated on the environment name"},{"expectedCount":2,"files":[{"path":"main.tf","source":"module \"iam\" {\n source = \"./iam\"\n team_platform_owner_privilege = var.environment == \"dev\"\n developer_secret_access_v2_enabled = var.environment == \"dev\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"module-input-derived-from-environment","outcome":"reject","scenarioId":"module-input","title":"Module input computed from the environment name"},{"expectedCount":0,"files":[{"path":"main.tf","source":"module \"iam\" {\n source = \"./iam\"\n team_platform_owner_privilege = var.team_platform_owner_privilege\n developer_secret_access_v2_enabled = var.developer_secret_access_v2_enabled\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"module-input-passed-from-tfvars","outcome":"accept","scenarioId":"module-input","title":"Module input passed through from tfvars"},{"expectedCount":0,"files":[{"path":"sandbox.tf","source":"resource \"google_storage_bucket\" \"cache\" {\n count = var.enable_object_cache ? 1 : 0\n name = \"cache\"\n}\n"}],"fixedFiles":[],"focusPath":"sandbox.tf","id":"named-capability-input","outcome":"accept","scenarioId":"primary","title":"Resource gated on a named capability input"}],"filePatterns":[],"id":"no-environment-conditional","key":"iac:no-environment-conditional","languages":["iac"],"limitations":["A comparison inside validation, precondition, postcondition, check, or assert asserts which inputs are legal and is exempt. Terraform test files are rejected separately by SARJ206.","Comparison against the empty string is an unset-input test, not an environment branch, and is ignored.","An interpolated value such as \"cache-${var.environment}\" names a resource and is not a branch. A branch written inside a template — \"${var.environment == \\\"prod\\\" ? ... }\", a %{ if } directive, or a heredoc body — goes unscanned: strings tokenize opaquely and heredoc bodies are masked.","A function result such as upper(var.environment) is not treated as the environment identity, so a comparison against it is exempt.","Only .tf and .hcl are read: the suffix filter keeps .tfvars out of scope.","A diagnostic is reported at the attribute's line. In a multi-line value the comparison itself may sit further down, so `# sarj-noqa: SARJ204` belongs on the attribute line."],"messageIds":[],"optionsSchema":null,"rationale":"Terraform that reads which environment it is in keeps the decision in code rather than in configuration, so a caller cannot see what varies, and every new environment is an edit to every expression that names the old ones — including at a module call site, where the value belongs in that environment's tfvars instead.","references":[],"remediation":"Declare a typed variable carrying the selected value, set per environment in tfvars (`tier = var.redis_tier`); use one `enable_<thing>` bool consumed by count/for_each only when the branch gates existence, never computed from the environment name.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_environment_conditional.py","status":"active","summary":"Terraform must not branch on the environment or project name; declare a variable and pass the value in from tfvars.","test":"packages/iac/tests/rules/test_no_environment_conditional.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ207","defaultLevel":"warning","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"# Set instance type\ninstance_type = var.instance_type\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"attribute-restatement","outcome":"reject","scenarioId":"primary","title":"Comment repeats an attribute name and value"},{"expectedCount":0,"files":[{"path":"main.tf","source":"# Keep this type because the provider rejects ARM nodes.\ninstance_type = var.instance_type\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"provider-constraint","outcome":"accept","scenarioId":"primary","title":"Comment records a provider constraint"}],"filePatterns":[],"id":"no-restated-comment","key":"iac:no-restated-comment","languages":["iac"],"limitations":["Only short line or block comments immediately above a declaration at the same indentation are compared.","Heredocs, directives, generated files, references, units, modality, and causal explanations are preserved."],"messageIds":[],"optionsSchema":null,"rationale":"Narrating a resource, block, or attribute duplicates executable configuration and can drift from it.","references":[],"remediation":"Delete the restatement. If the declaration is unclear, improve an author-controlled resource, module, local, or output label; keep provider constraints and operational rationale.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_restated_comment.py","status":"active","summary":"HCL comment restates the adjacent declaration — delete it; clarify an author-controlled label or extract a named local if the declaration is unclear.","test":"packages/iac/tests/rules/test_no_restated_comment.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ206","defaultLevel":"warning","engine":"iac","examples":[{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"example_service\" \"main\" {}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"terraform-module-source","outcome":"accept","scenarioId":"primary","title":"Regular Terraform configuration remains eligible for semantic rules"},{"expectedCount":1,"files":[{"path":"tests/routing.tftest.hcl","source":"run \"routing\" {}\n"}],"fixedFiles":[],"focusPath":"tests/routing.tftest.hcl","id":"terraform-test-file","outcome":"reject","scenarioId":"primary","title":"Do not commit Terraform test files"}],"filePatterns":[],"id":"no-terraform-test-file","key":"iac:no-terraform-test-file","languages":["iac"],"limitations":["The linter evaluates files supplied by the caller; the repository runner must pass every tracked IaC file.","This categorical repository policy intentionally cannot be suppressed or baselined."],"messageIds":[],"optionsSchema":null,"rationale":"Terraform test files are repository-side configuration oracles that can validate implementation shape instead of a reviewed plan, provider state, or deployed behavior.","references":[],"remediation":"Remove the .tftest.hcl or .tftest.json file and validate a real rendered plan, provider API, or runtime contract.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_terraform_test_file.py","status":"active","summary":"Committed Terraform test file couples validation to IaC source configuration.","test":"packages/iac/tests/rules/test_no_terraform_test_file.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"correctness","code":"SARJ407","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"QUERY = \"SELECT id, created_at FROM task ORDER BY created_at DESC, id DESC LIMIT 50\"\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"created-at-is-followed-by-id","outcome":"accept","scenarioId":"primary","title":"A stable key breaks equal-timestamp ties"},{"expectedCount":1,"files":[{"path":"app/task_store.py","source":"QUERY = \"SELECT id, created_at FROM task ORDER BY created_at DESC LIMIT 50\"\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"created-at-is-the-only-order-key","outcome":"reject","scenarioId":"primary","title":"A store query orders only by a timestamp"}],"filePatterns":[],"id":"created-at-order-requires-tiebreaker","key":"python:created-at-order-requires-tiebreaker","languages":["python"],"limitations":["Only fully reconstructable SQL string literals in recognized production store modules are analyzed.","The rule reports an exact, optionally qualified `created_at` column only when it is the final same-depth `ORDER BY` item; any later same-depth ordering item is accepted.","Dynamic string construction, formatted strings, quoted identifiers, expressions containing `created_at`, and non-SELECT fragments are excluded.","The intended stable key cannot be inferred safely, so the rule does not offer an autofix."],"messageIds":[],"optionsSchema":null,"rationale":"Timestamps are not unique, so rows with the same `created_at` value have no stable relative order. That can make pagination and repeated reads skip, repeat, or reorder rows.","references":[],"remediation":"Add a stable key after `created_at`, such as `ORDER BY created_at DESC, id DESC`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/created_at_order_requires_tiebreaker.py","status":"active","summary":"Store queries ordered by `created_at` should include a later tie-break key.","test":"packages/python/tests/rules/test_created_at_order_requires_tiebreaker.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","scenarioId":"primary","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","scenarioId":"primary","title":"Fixed defect fails loudly"}],"filePatterns":[],"id":"defect-xfail-requires-strict","key":"python:defect-xfail-requires-strict","languages":["python"],"limitations":["Only markers resolved through an unambiguous pytest or pytest.mark import are analyzed.","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 count_widgets(tenant_id: str) -> int:\n \"\"\"Count active widgets.\"\"\"\n return 0\n"}],"fixedFiles":[],"focusPath":"app/widgets.py","id":"argument-documents-unit","outcome":"accept","scenarioId":"primary","title":"Redundant argument section removed"},{"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","scenarioId":"primary","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":"Delete the human-only docstring or redundant argument section. Express author-controlled semantics with names and types; keep hidden constraints or units as a concise local comment.","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 number of render fragments queued for output.\n \"\"\"\n return len(line)\n"}],"fixedFiles":[],"focusPath":"app/lines.py","id":"return-documents-semantics","outcome":"accept","scenarioId":"primary","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","scenarioId":"primary","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.","Names that document the positions of a fixed tuple return are treated as semantic information.","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":"Delete the human-only docstring or redundant return section. Express author-controlled identity and units with names and types; keep hidden constraints as a concise local comment.","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_can_delete():\n user = make_user(\"admin\")\n allowed = can_delete(user)\n assert allowed\n\ndef test_editor_can_delete():\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","scenarioId":"primary","title":"Two tests differ only in input literals"},{"expectedCount":0,"files":[{"path":"tests/test_api.py","source":"def test_delete_environment():\n response = client.delete(\"/api/environments/3/\")\n result = response.json()\n assert result[\"ok\"]\n\ndef test_delete_schedule():\n response = client.delete(\"/api/schedules/3/\")\n result = response.json()\n assert result[\"ok\"]\n"}],"fixedFiles":[],"focusPath":"tests/test_api.py","id":"distinct-behaviors","outcome":"accept","scenarioId":"distinct-behavior","title":"Sibling tests preserve distinct API contracts"},{"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","scenarioId":"primary","title":"Inputs share one parameterized test"},{"expectedCount":1,"files":[{"path":"tests/test_jobs.py","source":"def test_starts_job():\n job = build_job()\n job.run()\n assert job.done\n\ndef test_stops_job():\n job = build_job()\n job.run()\n assert job.done\n"}],"fixedFiles":[],"focusPath":"tests/test_jobs.py","id":"verbatim-copy","outcome":"reject","scenarioId":"distinct-behavior","title":"A copied test never received its intended edit"}],"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.","A run of at least five two-statement embedded-source checker cases is compared because the source documents are natural parameter values.","Meaningful docstring or comment differences keep tests distinct.","Two-test groups with varying literals require corroborating behavior names; long scenario prose and distinct API resources remain separate contracts."],"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","scenarioId":"primary","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 MemoryStore(Store):\n def get(self, key: str) -> str:\n return key\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"override-specific-docstring","outcome":"accept","scenarioId":"primary","title":"Override relies on the base contract"}],"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. If author-controlled override code is unclear, clarify names or extract a helper; keep behavior-specific differences as a concise comment near the divergent code.","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', status_code=200)\nasync def read_users() -> list[UserResponse]:\n return []\n"}],"fixedFiles":[],"focusPath":"api.py","id":"documented-operation","outcome":"accept","scenarioId":"primary","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","scenarioId":"primary","title":"Visible operation without an explicit success status"}],"filePatterns":[],"id":"fastapi-openapi-contract","key":"python:fastapi-openapi-contract","languages":["python"],"limitations":["Hidden routes, WebSocket handlers, tests, generated files, documentation-source examples, 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":"Typed routes and explicit response behavior keep generated OpenAPI accurate without duplicating self-documenting names in prose.","references":[],"remediation":"Declare status codes, typed parameters, response schemas, and alternate responses raised directly by the handler.","since":null,"source":"packages/python/src/sarj_python_lint/rules/fastapi_openapi_contract.py","status":"active","summary":"FastAPI operations must publish accurate request, response, and status 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","scenarioId":"primary","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","scenarioId":"primary","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":"maintainability","code":"SARJ421","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/user_store.py","source":"class UserStore:\n async def get(self, user_id: UserId) -> User | None:\n return await self.query_one(user_id)\n\n async def get_many(self, user_ids: list[UserId]) -> list[User]:\n return await self.query_many(user_ids)\n"}],"fixedFiles":[],"focusPath":"app/user_store.py","id":"duplicate-singleton-query","outcome":"reject","scenarioId":"primary","title":"Do not maintain a second singleton query path"},{"expectedCount":0,"files":[{"path":"app/user_store.py","source":"class UserStore:\n async def get(self, user_id: UserId) -> User | None:\n rows = await self.get_many([user_id])\n return rows[0] if rows else None\n\n async def get_many(self, user_ids: list[UserId]) -> list[User]:\n return await self.query_many(user_ids)\n"}],"fixedFiles":[],"focusPath":"app/user_store.py","id":"singleton-delegates","outcome":"accept","scenarioId":"primary","title":"Delegate the singleton read to the bulk implementation"}],"filePatterns":[],"id":"get-delegates-to-get-many","key":"python:get-delegates-to-get-many","languages":["python"],"limitations":["Only concrete methods declared together in a production store module are inspected.","The methods must have one typed key, a list result for get_many or dict result for get_by_ids, and the same sync shape.","Branching singleton implementations are excluded because caching, locking, validation, or authorization may differ intentionally."],"messageIds":[],"optionsSchema":null,"rationale":"Independent singleton and bulk queries can drift in filtering, row conversion, authorization, and missing-row behavior while maintaining two database access paths.","references":[],"remediation":"Implement `get` through the compatible `get_many([key])` or `get_by_ids([key])` method and project its documented zero-or-one result.","since":null,"source":"packages/python/src/sarj_python_lint/rules/get_delegates_to_get_many.py","status":"active","summary":"Require compatible singleton store reads to delegate to their bulk implementation.","test":"packages/python/tests/rules/test_get_delegates_to_get_many.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ412","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_policy.py","source":"def test_policy():\n plan = json.loads(Path('plan.json').read_text())\n assert verify(plan) == []\n"}],"fixedFiles":[],"focusPath":"tests/test_policy.py","id":"rendered-plan-contract","outcome":"accept","scenarioId":"primary","title":"Assert on rendered Terraform plan behavior"},{"expectedCount":1,"files":[{"path":"tests/test_policy.py","source":"def test_policy():\n source = Path('main.tf').read_text()\n assert 'prevent_destroy = true' in source\n"}],"fixedFiles":[],"focusPath":"tests/test_policy.py","id":"terraform-substring-contract","outcome":"reject","scenarioId":"primary","title":"Do not prove Terraform behavior with a substring"}],"filePatterns":[],"id":"iac-source-coupled-test","key":"python:iac-source-coupled-test","languages":["python"],"limitations":["The rule follows local aliases, path collections, context-managed reads, and common normalization; interprocedural flows remain unreported.","Files produced beneath recognized temporary-directory fixtures are generated outputs and remain unreported.","The rule remains suppressible for exceptional compatibility boundaries."],"messageIds":[],"optionsSchema":null,"rationale":"Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently.","references":[],"remediation":"Parse rendered plan JSON, query the provider, or exercise the deployed runtime contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/iac_source_coupled_test.py","status":"active","summary":"Test asserts on raw IaC source text instead of a parsed plan, provider state, or runtime behavior.","test":"packages/python/tests/rules/test_iac_source_coupled_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","scenarioId":"primary","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","scenarioId":"primary","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_style(**overrides):\n defaults = dict(\n color=\"red\", bgcolor=\"black\", bold=True,\n dim=True, italic=True, underline=True,\n blink=True, blink2=True, reverse=True,\n )\n return Style(**(defaults | overrides))\n\ndef test_style():\n style = build_style()\n assert str(style)\n\ndef test_style_again():\n other = build_style(color=\"blue\", bgcolor=\"white\", bold=False)\n assert str(other)\n"}],"fixedFiles":[],"focusPath":"tests/test_call.py","id":"construction-through-builder","outcome":"accept","scenarioId":"primary","title":"Tests override builder defaults"},{"expectedCount":2,"files":[{"path":"tests/test_call.py","source":"def test_style():\n style = Style(\n color=\"red\", bgcolor=\"black\", bold=True, dim=True, italic=True,\n underline=True, blink=True, blink2=True, reverse=True,\n )\n assert str(style)\n\ndef test_style_again():\n other = Style(\n color=\"blue\", bgcolor=\"white\", bold=False, dim=True, italic=True,\n underline=True, blink=True, blink2=True, reverse=True,\n )\n assert str(other)\n"}],"fixedFiles":[],"focusPath":"tests/test_call.py","id":"repeated-wide-construction","outcome":"reject","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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, import-loader `sys.modules` stubs, and untouched constructor placeholders 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":"testing","code":"SARJ408","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_route.py","source":"def test_invalid_payload(client):\n response = client.post('/items', json={})\n assert response.status_code == 422\n"}],"fixedFiles":[],"focusPath":"tests/test_route.py","id":"exact-validation-status","outcome":"accept","scenarioId":"primary","title":"Assert the intended validation response"},{"expectedCount":1,"files":[{"path":"tests/test_route.py","source":"def test_invalid_payload(client):\n response = client.post('/items', json={})\n assert response.status_code != 500\n"}],"fixedFiles":[],"focusPath":"tests/test_route.py","id":"negative-only-server-status","outcome":"reject","scenarioId":"primary","title":"Do not accept every non-server-error response"}],"filePatterns":[],"id":"negative-only-http-status-assertion","key":"python:negative-only-http-status-assertion","languages":["python"],"limitations":["Only assertions on an attribute named `status_code` in collected Python tests are checked.","Exact status contracts, finite success sets, arbitrary `.status` attributes, and locally suppressed chaos tests are excluded.","The intended status cannot be inferred safely, so the rule has no autofix."],"messageIds":[],"optionsSchema":null,"rationale":"Authentication, routing, validation, and domain failures can all replace the intended response while a negative-only status assertion remains green.","references":[],"remediation":"Assert the intended exact status and the relevant domain payload or side effect.","since":null,"source":"packages/python/src/sarj_python_lint/rules/negative_only_http_status_assertion.py","status":"active","summary":"HTTP test assertion only excludes a server error instead of identifying the intended response.","test":"packages/python/tests/rules/test_negative_only_http_status_assertion.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","scenarioId":"primary","title":"Postgres store query reads bounded rows"},{"expectedCount":1,"files":[{"path":"app/call_store.py","source":"QUERY = \"SELECT SUM(amount) FROM call\"\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"postgres-aggregate-query","outcome":"reject","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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. If author-controlled code is unclear without narration, clarify names, types, or structure; keep only concise comments for a hidden 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":"testing","code":"SARJ426","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/conftest.py","source":"from tests.helpers import make_client\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"conftest-imports-support-module","outcome":"accept","scenarioId":"primary","title":"Conftest imports dedicated support code"},{"expectedCount":1,"files":[{"path":"tests/conftest.py","source":"from tests.test_api import make_client\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"conftest-imports-test-module","outcome":"reject","scenarioId":"primary","title":"Conftest imports a specific test module"}],"filePatterns":[],"id":"no-conftest-test-module-import","key":"python:no-conftest-test-module-import","languages":["python"],"limitations":["Only files named conftest.py are inspected.","A test module is recognized only when an explicit module-path component begins with `test_` or ends with `_test`."],"messageIds":[],"optionsSchema":null,"rationale":"pytest imports conftest for every test module in its scope; importing a specific test from conftest turns that test into global collection infrastructure and creates cycles and order dependence.","references":[],"remediation":"Move shared fixtures or helpers into conftest.py or a dedicated support module.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_conftest_test_module_import.py","status":"active","summary":"Do not import individual test modules from conftest.py.","test":"packages/python/tests/rules/test_no_conftest_test_module_import.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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 module-level `__all__` is analyzed.","Generated, dynamically reassigned, and non-Python declarations are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Duplicate exports add noise to a module'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 module `__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":"SARJ427","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/main.py","source":"from fastapi import FastAPI\napp = FastAPI()\n\n@app.on_event(\"startup\")\nasync def start() -> None:\n pass\n"}],"fixedFiles":[],"focusPath":"app/main.py","id":"deprecated-startup-event","outcome":"reject","scenarioId":"primary","title":"Do not register startup through on_event"},{"expectedCount":0,"files":[{"path":"app/main.py","source":"from contextlib import asynccontextmanager\nfrom fastapi import FastAPI\n\n@asynccontextmanager\nasync def lifespan(app: FastAPI):\n yield\n\napp = FastAPI(lifespan=lifespan)\n"}],"fixedFiles":[],"focusPath":"app/main.py","id":"lifespan-context-manager","outcome":"accept","scenarioId":"primary","title":"Pair startup and shutdown in lifespan"}],"filePatterns":[],"id":"no-fastapi-on-event","key":"python:no-fastapi-on-event","languages":["python"],"limitations":["Only decorators on a directly constructed FastAPI/Starlette application or a parameter annotated with one are checked.","Factory-returned applications and dynamically selected event names are intentionally not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"The on_event API is deprecated and splits related startup and shutdown state across callbacks; lifespan keeps acquisition and cleanup in one async context manager and is the supported lifecycle contract.","references":[],"remediation":"Define an async lifespan context manager and pass it as FastAPI(lifespan=...) or Starlette(lifespan=...).","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_fastapi_on_event.py","status":"active","summary":"Deprecated FastAPI or Starlette on_event lifecycle registration.","test":"packages/python/tests/rules/test_no_fastapi_on_event.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","scenarioId":"primary","title":"File suppresses every banned API use"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"import os # noqa: TID251 — vendor SDK boundary\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"reasoned-inline-suppression","outcome":"accept","scenarioId":"primary","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":"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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","title":"After-validator assigns a frozen model field"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, ConfigDict, field_validator\n\nclass Counter(BaseModel):\n model_config = ConfigDict(frozen=True)\n value: int\n\n @field_validator(\"value\", mode=\"before\")\n @classmethod\n def normalize(cls, value: int) -> int:\n return abs(value)\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"after-validator-only-validates-frozen-field","outcome":"accept","scenarioId":"primary","title":"Before-validator normalizes the input value"}],"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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","title":"Generic module contains one public definition"},{"expectedCount":0,"files":[{"path":"text_case.py","source":"def snake_case_text(value: str) -> str: ...\n"}],"fixedFiles":[],"focusPath":"text_case.py","id":"specific-single-export-module","outcome":"accept","scenarioId":"primary","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 junk-drawer stems with exactly one top-level public class or function are reported.","An absent `__all__`, or one static entry matching that definition, must prove the public surface.","Generated and test paths are excluded; semantic role and framework filenames are outside the stem set."],"messageIds":[],"optionsSchema":null,"rationale":"Names such as `utils` and `helpers` hide a module's responsibility and encourage unrelated additions.","references":[],"remediation":"Choose a responsibility-bearing module name or colocate the definition with its domain.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_generic_single_export_module.py","status":"active","summary":"A generic module name should not conceal a single-definition responsibility.","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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 stable name and an unreachable terminal fallback.","Builtin, imported, abstract collection, open-ended dispatch types, and generated files 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","scenarioId":"primary","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","scenarioId":"primary","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":"Delete human-only prose and clarify names, types, or structure. Keep machine-consumed documentation; move broader design context to maintained documentation.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_long_comment.py","status":"active","summary":"Unstructured docstring prose wall — delete it; clarify author-controlled names, types, and structure or move durable design context to maintained documentation.","test":"packages/python/tests/rules/test_no_long_comment.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ425","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, field_validator\n\nclass Settings(BaseModel):\n language: str\n class Config:\n extra = 'forbid'\n @field_validator('language')\n @classmethod\n def normalize(cls, value):\n return value.lower()\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"validator-nested-in-config","outcome":"reject","scenarioId":"primary","title":"Validator accidentally belongs to Config"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, field_validator\n\nclass Settings(BaseModel):\n language: str\n @field_validator('language')\n @classmethod\n def normalize(cls, value):\n return value.lower()\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"validator-owned-by-model","outcome":"accept","scenarioId":"primary","title":"Validator belongs to the model"}],"filePatterns":[],"id":"no-nested-pydantic-field-validator","key":"python:no-nested-pydantic-field-validator","languages":["python"],"limitations":["Only direct BaseModel subclasses and validators directly owned by one nested class are inspected.","Nested classes with bases are excluded because inherited Pydantic fields cannot be resolved locally.","Test and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A decorator indented into a nested class registers on that class, not on the outer Pydantic model, so the declared field silently loses validation.","references":[],"remediation":"Move the field-validator method into the outer model class.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_nested_pydantic_field_validator.py","status":"active","summary":"Do not place an outer Pydantic model's field validator inside a nested helper class.","test":"packages/python/tests/rules/test_no_nested_pydantic_field_validator.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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. For an audited cross-tenant admin/background query, suppress only the conditional fragment with `# sarj-noqa: SARJ056` and state why unscoped access is required.","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","scenarioId":"primary","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","scenarioId":"primary","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":"maintainability","code":"SARJ423","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/models.py","source":"from typing import Literal\nfrom pydantic import BaseModel, Field\n\nclass Request(BaseModel):\n kind: Literal['call'] = Field(description=\"Must be 'call'\")\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"literal-domain-restated","outcome":"reject","scenarioId":"primary","title":"Literal domain repeated in prose"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from typing import Literal\nfrom pydantic import BaseModel, Field\n\nclass Request(BaseModel):\n kind: Literal['call'] = Field(description='Routes through the realtime provider.')\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"literal-rationale-described","outcome":"accept","scenarioId":"primary","title":"Description adds rationale"}],"filePatterns":[],"id":"no-redundant-literal-description","key":"python:no-redundant-literal-description","languages":["python"],"limitations":["Only direct fields on direct BaseModel subclasses are inspected.","Enum fields are inspected only when their direct string-valued enum class is declared in the same module.","Descriptions are reported only when they begin with a narrow 'must be' or 'should be' restatement.","Test and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Pydantic already publishes literal and enum domains in JSON Schema; duplicate must-be prose can contradict the generated contract after a value changes.","references":[],"remediation":"Remove the domain-only description or replace it with rationale not encoded by the field type.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_redundant_literal_description.py","status":"active","summary":"Do not restate a Literal or Enum field's closed domain in its Pydantic description.","test":"packages/python/tests/rules/test_no_redundant_literal_description.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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. If author-controlled code is unclear without it, clarify a name or extract a named helper; keep comments only for 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 next statement — delete it; clarify an author-controlled name or extract a named helper if the code is unclear.","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":"token_prefix = token[:6]\nlogger.info('request', token_prefix=token_prefix)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"redacted-logging-keyword","outcome":"accept","scenarioId":"primary","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","scenarioId":"primary","title":"Raw token passed to a logger"}],"filePatterns":[],"id":"no-secret-in-log","key":"python:no-secret-in-log","languages":["python"],"limitations":["Detection covers secret-named direct references passed by secret-named keyword to known logger calls.","Aliases, calls, subscripts, positional values, message interpolation, and values under non-secret keywords 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":"A secret-named direct reference 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","scenarioId":"primary","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","scenarioId":"primary","title":"Store query selects every column"}],"filePatterns":[],"id":"no-select-star","key":"python:no-select-star","languages":["python"],"limitations":["Only SQL string literals in production source files 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":"SQL 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","scenarioId":"primary","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","scenarioId":"primary","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":"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","scenarioId":"primary","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","scenarioId":"primary","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(f\"{item}\\n\")\n return \"\".join(lines)\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"join-string-fragments","outcome":"accept","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"app.py","source":"def decode(value: str) -> dict[str, object]:\n \"\"\"Decode the value.\n\n Args:\n value: Text to decode.\n \"\"\"\n return {}\n"}],"fixedFiles":[],"focusPath":"app.py","id":"behavioral-parameter-contract","outcome":"accept","scenarioId":"primary","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","scenarioId":"primary","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":"Delete the human-only docstring or repeated type. Express author-controlled contracts with names and annotations; keep hidden constraints, units, or error conditions as a concise local comment.","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":[],"autofix":"none","category":"correctness","code":"SARJ404","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/store.py","source":"from psycopg import errors\n\ntry:\n save()\nexcept errors.UniqueViolation as exc:\n if 'user_email_key' in str(exc):\n raise DuplicateEmail from exc\n raise\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"message-matched-constraint","outcome":"reject","scenarioId":"primary","title":"Unique constraint selected from error text"},{"expectedCount":0,"files":[{"path":"app/store.py","source":"from psycopg import errors\n\ntry:\n save()\nexcept errors.UniqueViolation as exc:\n if exc.diag.constraint_name == 'user_email_key':\n raise DuplicateEmail from exc\n raise\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"structured-constraint-check","outcome":"accept","scenarioId":"primary","title":"Unique constraint selected from structured diagnostics"}],"filePatterns":[],"id":"no-unique-violation-message-match","key":"python:no-unique-violation-message-match","languages":["python"],"limitations":["The rule recognizes psycopg and psycopg2 `UniqueViolation` handlers with direct `in` or `not in` checks against `str(exc)`.","Aliases of the stringified exception and other message operations remain outside its scope.","Tests and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Database error text is not a stable interface and can change with driver or server versions; psycopg exposes the violated constraint as structured diagnostic data.","references":[],"remediation":"Compare `exc.diag.constraint_name` with the expected constraint name.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_unique_violation_message_match.py","status":"active","summary":"Do not identify a database unique constraint by substring-matching an exception message.","test":"packages/python/tests/rules/test_no_unique_violation_message_match.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ420","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/tools.py","source":"from agents import function_tool\n\n@function_tool\ndef lookup_account(account_id: str) -> str:\n \"\"\"Look up an account for the model.\"\"\"\n return account_id\n"}],"fixedFiles":[],"focusPath":"app/tools.py","id":"framework-docstring","outcome":"accept","scenarioId":"primary","title":"Framework consumes the function docstring"},{"expectedCount":3,"files":[{"path":"app/service.py","source":"\"\"\"Service entry points.\"\"\"\n\nclass Service:\n \"\"\"Coordinates requests.\"\"\"\n\n def run(self) -> None:\n \"\"\"Run the service.\"\"\"\n return None\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"human-only-docstrings","outcome":"reject","scenarioId":"primary","title":"Human-only module, class, and function prose"}],"filePatterns":[],"id":"no-unnecessary-docstring","key":"python:no-unnecessary-docstring","languages":["python"],"limitations":["Generated files, doctests, syntax-required stub bodies, schema classes, framework decorators, explicit __doc__/help/getdoc consumers, and local sarj-noqa suppressions are excluded.","The rule is intentionally default-deny: public API documentation without mechanically visible consumption needs an auditable SARJ420 suppression.","Dynamic Click or Typer decorator options are conservatively treated as possible help consumers; explicit non-None help leaves the docstring eligible."],"messageIds":[],"optionsSchema":null,"rationale":"A docstring with no detected consumer makes code depend on prose for ordinary meaning and creates a second maintenance surface that agents expand and maintainers must review.","references":[],"remediation":"Delete the docstring. If author-controlled code is unclear without it, clarify names, types, and structure or extract a small named helper. Keep a genuinely hidden invariant as one concise local comment. When external tooling consumes __doc__, use an exact SARJ420 suppression that names the consumer.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_unnecessary_docstring.py","status":"active","summary":"No docstring consumer detected — delete it; make author-controlled names, types, and structure explain the code.","test":"packages/python/tests/rules/test_no_unnecessary_docstring.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ419","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"adapter.py","source":"value = vendor.value # type: ignore[attr-defined] -- vendor stubs omit the runtime field\n"}],"fixedFiles":[],"focusPath":"adapter.py","id":"concrete-suppression-reason","outcome":"accept","scenarioId":"primary","title":"Concrete runtime mismatch explains the suppression"},{"expectedCount":1,"files":[{"path":"adapter.py","source":"value = vendor.value # type: ignore[attr-defined] -- false positive\n"}],"fixedFiles":[],"focusPath":"adapter.py","id":"generic-suppression-reason","outcome":"reject","scenarioId":"primary","title":"Generic reason does not make the suppression auditable"}],"filePatterns":[],"id":"no-vague-suppression-description","key":"python:no-vague-suppression-description","languages":["python"],"limitations":["Only scoped noqa, type-checker, and sarj-noqa directives with a present but closed generic reason are checked.","Missing descriptions and descriptions containing concrete context are owned by their native tools or remain unchanged."],"messageIds":[],"optionsSchema":null,"rationale":"Generic reasons satisfy review conventions without making the suppressed risk auditable or removable.","references":[],"remediation":"Name the exact tool mismatch, external contract, or invariant that makes this suppression safe.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_vague_suppression_description.py","status":"active","summary":"Suppression descriptions must name the concrete mismatch or safety invariant.","test":"packages/python/tests/rules/test_no_vague_suppression_description.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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.","Tests explicitly identified as documentation-example harnesses are excluded."],"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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","title":"Immutable tuple used for a module constant"},{"expectedCount":0,"files":[{"path":"settings.py","source":"from types import MappingProxyType\n\nROLE_LABELS = MappingProxyType({\"admin\": \"Administrator\"})\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"immutable-module-mapping","outcome":"accept","scenarioId":"keyed-values","title":"Read-only mapping used for keyed values"},{"expectedCount":1,"files":[{"path":"settings.py","source":"ROLE_NAMES = [\"admin\", \"member\"]\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"mutable-module-constant","outcome":"reject","scenarioId":"primary","title":"Mutable collection exposed as a module constant"},{"expectedCount":1,"files":[{"path":"settings.py","source":"ROLE_LABELS = {\"admin\": \"Administrator\"}\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"mutable-module-mapping","outcome":"reject","scenarioId":"keyed-values","title":"Dictionary 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 __init__(self):\n self.objects = {}\n\n def put_object(self, Bucket, Key, Body):\n self.objects[(Bucket, Key)] = Body\n return {\"ResponseMetadata\": {\"HTTPStatusCode\": 200}}\n\n def get_object(self, Bucket, Key):\n return {\"Body\": self.objects[(Bucket, Key)]}\n\n def delete_object(self, Bucket, Key):\n self.objects.pop((Bucket, Key), None)\n"}],"fixedFiles":[],"focusPath":"tests/fakes/s3.py","id":"hand-rolled-s3-client","outcome":"reject","scenarioId":"primary","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","scenarioId":"primary","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.","LLM and BigQuery doubles require a distinctive raw provider response envelope."],"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","scenarioId":"primary","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","scenarioId":"primary","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 or non-invoked 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 Text():\n return value\n case Binary():\n return value\n case PathValue():\n return value\n case _:\n return coerce(value)\n"}],"fixedFiles":[],"focusPath":"app/parser.py","id":"match-type-cases","outcome":"accept","scenarioId":"primary","title":"Parser names value shapes with match arms"},{"expectedCount":1,"files":[{"path":"app/parser.py","source":"def parse(value: object):\n if isinstance(value, Text):\n return value\n if isinstance(value, Binary):\n return value\n if isinstance(value, PathValue):\n return value\n return coerce(value)\n"}],"fixedFiles":[],"focusPath":"app/parser.py","id":"sequential-type-guards","outcome":"reject","scenarioId":"primary","title":"Parser dispatches through sequential guards"}],"filePatterns":[],"id":"prefer-match-type-dispatch","key":"python:prefer-match-type-dispatch","languages":["python"],"limitations":["Safe two-branch `isinstance` dispatch is advisory; dispatch with three or more branches and the rule's established parser shapes remain blocking.","Generated files, runtime tuple aliases, general two-arm stdlib AST visitors, subject-rebinding guards, idiomatic None/Unset prologues, and code that shadows `isinstance` are excluded; exact AST attribute projections remain advisory, and 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 repeated `isinstance` branches or 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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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, documentation examples, declared overrides, pytest fixtures, pickle protocols, and syntax-proven sort/key callbacks 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":"Functions should return named records instead of fixed positional tuples, including tuples nested in collections.","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"correctness","code":"SARJ422","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/settings_store.py","source":"async def save(cursor):\n row = await cursor.fetchone()\n assert row is not None, 'RETURNING must yield a row'\n return row\n"}],"fixedFiles":[],"focusPath":"app/settings_store.py","id":"required-returning-row","outcome":"reject","scenarioId":"primary","title":"Use the shared required-row contract"},{"expectedCount":0,"files":[{"path":"app/settings_store.py","source":"async def save(cursor):\n return one(await cursor.fetchone())\n"}],"fixedFiles":[],"focusPath":"app/settings_store.py","id":"required-row-helper","outcome":"accept","scenarioId":"primary","title":"Centralize the missing-row failure"}],"filePatterns":[],"id":"prefer-one-for-required-row","key":"python:prefer-one-for-required-row","languages":["python"],"limitations":["Only an immediate exact `row is not None` assertion after `fetchone()` in production store modules is inspected.","The rule does not infer that an optional lookup must return a row and does not choose an import path for the helper."],"messageIds":[],"optionsSchema":null,"rationale":"A bare assertion is removed by optimized Python and repeats the database contract at each call site; a shared helper preserves the invariant and raises one intentional domain error.","references":[],"remediation":"Wrap the fetch in the repository's required-row helper, for example `row = one(await cursor.fetchone())`, and remove the non-None assertion.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_one_for_required_row.py","status":"active","summary":"Use a required-row helper instead of asserting that `fetchone()` returned a row.","test":"packages/python/tests/rules/test_prefer_one_for_required_row.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","scenarioId":"primary","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","scenarioId":"primary","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, comments, or a combined pattern rejected by Python are excluded."],"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":"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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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/model/wire fields, observability boundaries, 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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"correctness","code":"SARJ416","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/fake.py","source":"from typing import NewType, override\nSipTrunkId = NewType('SipTrunkId', str)\nclass Fake:\n @override\n def route(self, sip_trunk_id: str): ...\n"}],"fixedFiles":[],"focusPath":"tests/fake.py","id":"declared-id-erased","outcome":"reject","scenarioId":"primary","title":"A declared nominal ID is widened to string"},{"expectedCount":0,"files":[{"path":"tests/fake.py","source":"from typing import NewType, override\nSipTrunkId = NewType('SipTrunkId', str)\nclass Fake:\n @override\n def route(self, sip_trunk_id: SipTrunkId): ...\n"}],"fixedFiles":[],"focusPath":"tests/fake.py","id":"declared-id-preserved","outcome":"accept","scenarioId":"primary","title":"A declared nominal ID remains nominal"}],"filePatterns":[],"id":"preserve-declared-nominal-id","key":"python:preserve-declared-nominal-id","languages":["python"],"limitations":["The field name must map exactly and unambiguously to a first-party NewType declaration.","Only explicit override methods in test files are inspected; production boundary discovery remains SARJ093's responsibility."],"messageIds":[],"optionsSchema":null,"rationale":"A fake override that widens a declared ID role back to its primitive carrier defeats type-checker swap protection.","references":[],"remediation":"Import and propagate the matching project `NewType` instead of annotating the role with its carrier.","since":null,"source":"packages/python/src/sarj_python_lint/rules/preserve_declared_nominal_id.py","status":"active","summary":"Keep project-declared nominal identifier types in test overrides.","test":"packages/python/tests/rules/test_preserve_declared_nominal_id.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ417","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/__init__.py","source":"from enum import StrEnum\nclass Status(StrEnum):\n READY = 'ready'\nclass Result:\n status: Status | None\ndef render(result):\n match result:\n case Result():\n return str(result.status)\n"}],"fixedFiles":[],"focusPath":"app/__init__.py","id":"matched-enum-erased","outcome":"reject","scenarioId":"primary","title":"A matched result enum is converted to string"},{"expectedCount":0,"files":[{"path":"app/__init__.py","source":"from enum import StrEnum\nclass Status(StrEnum):\n READY = 'ready'\nclass Result:\n status: Status | None\ndef render(result):\n match result:\n case Result():\n return result.status\n"}],"fixedFiles":[],"focusPath":"app/__init__.py","id":"matched-enum-preserved","outcome":"accept","scenarioId":"primary","title":"A matched result enum remains typed"}],"filePatterns":[],"id":"preserve-enum-types","key":"python:preserve-enum-types","languages":["python"],"limitations":["The initial rule requires an unambiguous class pattern, annotated enum field, and direct `str(subject.field)` call.","Dynamic imports, star imports, and unresolved external classes fail open."],"messageIds":[],"optionsSchema":null,"rationale":"String conversion weakens downstream validation and generated schemas while hiding the loss from the type checker.","references":[],"remediation":"Carry the enum annotation and value through the receiving model or exception boundary.","since":null,"source":"packages/python/src/sarj_python_lint/rules/preserve_enum_types.py","status":"active","summary":"Preserve a narrowed enum instead of converting it to an unconstrained string.","test":"packages/python/tests/rules/test_preserve_enum_types.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ409","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_models.py","source":"import pytest\n\nEXPECTED_MODELS = ('a', 'b')\n\n@pytest.mark.parametrize('model', EXPECTED_MODELS)\ndef test_model(model):\n assert build(model).tier == 'priority'\n"}],"fixedFiles":[],"focusPath":"tests/test_models.py","id":"independent-model-cases","outcome":"accept","scenarioId":"primary","title":"Drive behavior from an independent expected table"},{"expectedCount":1,"files":[{"path":"tests/test_models.py","source":"import pytest\nfrom app.models import ELIGIBLE_MODELS\n\n@pytest.mark.parametrize('model', ELIGIBLE_MODELS)\ndef test_model(model):\n assert build(model).tier == 'priority'\n"}],"fixedFiles":[],"focusPath":"tests/test_models.py","id":"production-derived-model-cases","outcome":"reject","scenarioId":"primary","title":"Do not derive cases from production eligibility"}],"filePatterns":[],"id":"production-derived-test-cases","key":"python:production-derived-test-cases","languages":["python"],"limitations":["Only direct imported collections, simple collection wrappers, and direct set expressions in pytest parametrization are checked.","Local expected tables, test-helper fixtures, enum exhaustiveness, names ending in `_REGISTRY`, and collections whose exact membership is asserted elsewhere in the module are excluded.","Project-wide relationships hidden behind helpers require judgment and remain unreported."],"messageIds":[],"optionsSchema":null,"rationale":"Removing a production member can remove the corresponding test case too, allowing the same defect to weaken both the implementation and its supposed oracle.","references":[],"remediation":"Define an independent expected table, assert production equals it, and drive behavior from that table.","since":null,"source":"packages/python/src/sarj_python_lint/rules/production_derived_test_cases.py","status":"active","summary":"Parametrized test cases come from the production collection whose membership they should protect.","test":"packages/python/tests/rules/test_production_derived_test_cases.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","scenarioId":"primary","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","scenarioId":"primary","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, documentation examples, 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","scenarioId":"primary","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","scenarioId":"primary","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 docstring and clarify author-controlled names, fields, or types. Keep a hidden invariant, lifetime, or exclusion as a concise comment near its enforcement.","since":null,"source":"packages/python/src/sarj_python_lint/rules/redundant_class_docstring.py","status":"active","summary":"Class docstring restates the declaration — delete it; clarify author-controlled names, fields, or types if the role is unclear.","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","scenarioId":"primary","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","scenarioId":"primary","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 and clarify author-controlled names, types, or structure. Keep a concise local comment only for a hidden constraint, side effect, or failure mode.","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; clarify author-controlled names and types if the contract is unclear.","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","scenarioId":"primary","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","scenarioId":"primary","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 docstring and clarify the author-controlled module path or exports. Keep durable boundaries and compatibility constraints near the code they govern or in maintained documentation.","since":null,"source":"packages/python/src/sarj_python_lint/rules/redundant_module_docstring.py","status":"active","summary":"Module docstring restates the file path — delete it; clarify the author-controlled module path or exports if the purpose is unclear.","test":"packages/python/tests/rules/test_redundant_module_docstring.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ413","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_parser.py","source":"@pytest.mark.parametrize(('value', 'expected'), [('a', 1), ('b', 2), ('c', 3)])\ndef test_parse(value, expected):\n assert parse(value) == expected\n"}],"fixedFiles":[],"focusPath":"tests/test_parser.py","id":"parameterized-parser-cases","outcome":"accept","scenarioId":"primary","title":"Give each parser input a runner-visible case"},{"expectedCount":1,"files":[{"path":"tests/test_parser.py","source":"def test_parse():\n assert parse('a') == 1\n assert parse('b') == 2\n assert parse('c') == 3\n"}],"fixedFiles":[],"focusPath":"tests/test_parser.py","id":"repeated-parser-assertions","outcome":"reject","scenarioId":"primary","title":"Do not hide independent inputs in one callback"}],"filePatterns":[],"id":"repeated-static-call-cases","key":"python:repeated-static-call-cases","languages":["python"],"limitations":["Only runs of at least three consecutive top-level assertions in collected pytest-style tests are checked.","Calls, inputs, and expectations must be statically representable; unittest classes, zero-argument calls, mocks, snapshots, and intervening prose or setup are excluded.","Common mapping accessors are excluded because repeated field assertions usually describe one cohesive object contract, not independent input cases.","Tests participating in a duplicate-test-body group are left to SARJ066, which has the broader finding.","Case names and parameter boundaries require judgment, so the rule has no autofix."],"messageIds":[],"optionsSchema":null,"rationale":"When several independent static inputs share one test callback, the first failure hides later cases and the runner cannot name the input that failed.","references":[],"remediation":"Move the inputs and expectations into a named pytest parameter table.","since":null,"source":"packages/python/src/sarj_python_lint/rules/repeated_static_call_cases.py","status":"active","summary":"Repeated static call assertions are hidden inside one coarse test case.","test":"packages/python/tests/rules/test_repeated_static_call_cases.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","scenarioId":"primary","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","scenarioId":"primary","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":"correctness","code":"SARJ424","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/settings.py","source":"from typing import Annotated\nfrom pydantic import field_validator\nfrom pydantic_settings import BaseSettings, NoDecode\n\nclass Settings(BaseSettings):\n emails: Annotated[list[str], NoDecode]\n @field_validator('emails', mode='before')\n @classmethod\n def split_emails(cls, value):\n return value.split(',')\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"complex-setting-split-with-nodecode","outcome":"accept","scenarioId":"primary","title":"Raw splitter receives undecoded input"},{"expectedCount":1,"files":[{"path":"app/settings.py","source":"from pydantic import field_validator\nfrom pydantic_settings import BaseSettings\n\nclass Settings(BaseSettings):\n emails: list[str]\n @field_validator('emails', mode='before')\n @classmethod\n def split_emails(cls, value):\n return value.split(',')\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"complex-setting-split-without-nodecode","outcome":"reject","scenarioId":"primary","title":"Raw splitter without NoDecode"}],"filePatterns":[],"id":"require-nodecode-for-splitting-settings-field","key":"python:require-nodecode-for-splitting-settings-field","languages":["python"],"limitations":["The rule checks direct fields and direct field validators on direct BaseSettings subclasses.","A validator is considered a raw splitter only when it calls `.split(...)` on its value parameter.","Classes that statically disable settings decoding through model_config or Config are excluded.","Custom settings sources and indirect splitter helpers are outside the rule's scope."],"messageIds":[],"optionsSchema":null,"rationale":"pydantic-settings JSON-decodes complex environment fields before field validators run; a raw-string splitter without NoDecode can fail during process startup.","references":[],"remediation":"Annotate the field as `Annotated[FieldType, NoDecode]` so the validator receives the raw string.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_nodecode_for_splitting_settings_field.py","status":"active","summary":"Require NoDecode when a before-validator splits a complex pydantic-settings field.","test":"packages/python/tests/rules/test_require_nodecode_for_splitting_settings_field.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","scenarioId":"primary","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","scenarioId":"primary","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":"correctness","code":"SARJ411","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"protocol.py","source":"import json\n\ndef parse(payload: str) -> object:\n report = json.loads(payload)\n return report.get('version')\n"}],"fixedFiles":[],"focusPath":"protocol.py","id":"manual-external-json-access","outcome":"reject","scenarioId":"primary","title":"External JSON read as a dictionary"},{"expectedCount":0,"files":[{"path":"protocol.py","source":"def parse(payload: str) -> Report:\n return Report.model_validate_json(payload)\n"}],"fixedFiles":[],"focusPath":"protocol.py","id":"pydantic-external-json-validation","outcome":"accept","scenarioId":"primary","title":"External JSON validated by a boundary model"}],"filePatterns":[],"id":"require-pydantic-for-external-json","key":"python:require-pydantic-for-external-json","languages":["python"],"limitations":["The rule follows common JSON decoders, typed HTTP response helpers and clients, and simple module-local helpers through single-assignment names.","It diagnoses literal-key record access; dynamic JSON documents without fixed-field access remain out of scope.","Repository-local JSON, json.load file handles, tests, generated files, and documentation examples are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Annotations, casts, key-by-key checks, and dictionary access do not validate a wire protocol; Pydantic makes required fields, types, and protocol versions explicit at the boundary.","references":[],"remediation":"Use `Model.model_validate_json(payload)` or `TypeAdapter(Model).validate_json(payload)`, or validate an already-decoded value with `model_validate` or `validate_python` before use.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_pydantic_for_external_json.py","status":"active","summary":"Externally sourced JSON is consumed without runtime schema validation.","test":"packages/python/tests/rules/test_require_pydantic_for_external_json.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ418","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"api.py","source":"class Detail(BaseModel):\n retry_attempt_number: int = Field(default=1, ge=1, description='1 for the first attempt')\n"}],"fixedFiles":[],"focusPath":"api.py","id":"ordinal-bound-encoded","outcome":"accept","scenarioId":"primary","title":"An ordinal minimum is enforced"},{"expectedCount":1,"files":[{"path":"api.py","source":"class Detail(BaseModel):\n retry_attempt_number: int = Field(default=1, description='1 for the first attempt')\n"}],"fixedFiles":[],"focusPath":"api.py","id":"ordinal-prose-only","outcome":"reject","scenarioId":"primary","title":"An ordinal minimum exists only in prose"}],"filePatterns":[],"id":"require-pydantic-ordinal-lower-bound","key":"python:require-pydantic-ordinal-lower-bound","languages":["python"],"limitations":["The default and the `N for the first ...` description phrase must both be literal and equal.","Names and defaults alone never imply a range."],"messageIds":[],"optionsSchema":null,"rationale":"A prose-only minimum weakens validation and generated JSON Schema relative to the documented contract.","references":[],"remediation":"Use a constrained integer type or matching `Field(ge=...)` metadata.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py","status":"active","summary":"Encode a documented Pydantic ordinal minimum as a runtime lower bound.","test":"packages/python/tests/rules/test_require_pydantic_ordinal_lower_bound.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ414","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"store.py","source":"def load(conn):\n with conn.cursor() as cur:\n return cur.fetchone()\n"}],"fixedFiles":[],"focusPath":"store.py","id":"bare-fetched-cursor","outcome":"reject","scenarioId":"primary","title":"A fetched cursor returns positional rows"},{"expectedCount":0,"files":[{"path":"store.py","source":"def load(conn):\n with conn.cursor(row_factory=class_row(PendingTimeRow)) as cur:\n return cur.fetchone()\n"}],"fixedFiles":[],"focusPath":"store.py","id":"validated-fetched-cursor","outcome":"accept","scenarioId":"primary","title":"A fetched cursor validates a row model"}],"filePatterns":[],"id":"require-validated-row-factory","key":"python:require-validated-row-factory","languages":["python"],"limitations":["Only cursors bound by a with statement and fetched in the same function are inspected.","Test files are excluded, and `dict_row` remains exclusively owned by SARJ013.","Dynamic/ad-hoc result shapes require an exact local suppression."],"messageIds":[],"optionsSchema":null,"rationale":"Bare and tuple-like cursors let unvalidated positional rows cross the database boundary.","references":[],"remediation":"Pass `row_factory=class_row(Model)`; define a small row model for a projection.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_validated_row_factory.py","status":"active","summary":"Fetched Psycopg rows must be parsed by `class_row(Model)`.","test":"packages/python/tests/rules/test_require_validated_row_factory.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","scenarioId":"primary","title":"Docstring repeats the test name"},{"expectedCount":0,"files":[{"path":"tests/test_widget.py","source":"def test_widget_renders():\n assert render(widget)\n"}],"fixedFiles":[],"focusPath":"tests/test_widget.py","id":"test-regression-context","outcome":"accept","scenarioId":"primary","title":"Redundant docstring removed"}],"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 docstring and put the scenario and expected outcome in the test name. Keep a hidden regression reason or constraint as one concise local comment.","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":"SARJ402","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_policy.py","source":"def test_policy():\n workflow = yaml.safe_load(Path('workflow.yml').read_text())\n assert verify(workflow) == []\n"}],"fixedFiles":[],"focusPath":"tests/test_policy.py","id":"parsed-workflow-contract","outcome":"accept","scenarioId":"primary","title":"Assert on parsed workflow behavior"},{"expectedCount":1,"files":[{"path":"tests/test_policy.py","source":"def test_policy():\n source = Path('workflow.yml').read_text()\n assert 'permissions:' in source\n"}],"fixedFiles":[],"focusPath":"tests/test_policy.py","id":"workflow-substring-contract","outcome":"reject","scenarioId":"primary","title":"Do not prove workflow behavior with a substring"}],"filePatterns":[],"id":"source-coupled-test","key":"python:source-coupled-test","languages":["python"],"limitations":["The rule follows local aliases, path aliases, context-managed reads, and common text normalization; interprocedural flows remain unreported.","Files produced beneath recognized temporary-directory fixtures are generated artifacts, not repository source, and remain unreported.","When raw representation is genuinely the contract (for example a golden or compatibility sentinel), use an exact line suppression with the reason."],"messageIds":[],"optionsSchema":null,"rationale":"Substring and regex checks can pass on comments or unreachable configuration and fail after behavior-preserving formatting changes.","references":[],"remediation":"Parse the artifact, execute its validator, or assert on a runtime contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/source_coupled_test.py","status":"active","summary":"Test asserts on raw repository source text instead of parsed or executable behavior.","test":"packages/python/tests/rules/test_source_coupled_test.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ415","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/fixtures/retry_rows.py","source":"async def seed(pool: AsyncConnectionPool):\n async with pool.connection() as conn:\n await conn.execute('UPDATE call SET retry = true')\n"}],"fixedFiles":[],"focusPath":"tests/fixtures/retry_rows.py","id":"fixture-pool-sql","outcome":"reject","scenarioId":"primary","title":"A free fixture helper executes through a pool"},{"expectedCount":0,"files":[{"path":"app/store.py","source":"class Store:\n def __init__(self, pool: AsyncConnectionPool):\n self.pool = pool\n async def save(self):\n async with self.pool.connection() as conn:\n await conn.execute('UPDATE call SET retry = true')\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"injected-store-sql","outcome":"accept","scenarioId":"primary","title":"A store executes through its injected pool"}],"filePatterns":[],"id":"sql-requires-injected-pool-owner","key":"python:sql-requires-injected-pool-owner","languages":["python"],"limitations":["Database receivers must be proven from pool or connection annotations and local with bindings.","Migrations are excluded; deliberate schema/corruption helpers require an exact suppression."],"messageIds":[],"optionsSchema":null,"rationale":"SQL in fixtures and free helpers duplicates persistence contracts outside their owning boundary.","references":[],"remediation":"Move the operation behind the store/service that receives and owns the connection pool.","since":null,"source":"packages/python/src/sarj_python_lint/rules/sql_requires_injected_pool_owner.py","status":"active","summary":"Execute database statements only inside a constructor-injected pool owner.","test":"packages/python/tests/rules/test_sql_requires_injected_pool_owner.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","scenarioId":"primary","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","scenarioId":"primary","title":"Private helper appears before its sole caller"}],"filePatterns":[],"id":"stepdown","key":"python:stepdown","languages":["python"],"limitations":["Generated files, tests, `__main__.py`, mutual recursion, and helpers with multiple callers are excluded.","Decorated definitions and dynamic references that cannot prove 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","scenarioId":"primary","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","scenarioId":"primary","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.","Named ordinary create methods are excluded because a uniqueness error may be their intended contract."],"messageIds":[],"optionsSchema":null,"rationale":"A method named as an enqueue, seed, migration, schedule, ensure, or upsert promises replay safety.","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 inserts in replay-contract store methods must handle conflicts explicitly.","test":"packages/python/tests/rules/test_store_insert_requires_on_conflict.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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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.","Imported constructors are excluded because their assignment, validation, and normalization behavior is not visible."],"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":"SARJ410","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_selector.py","source":"import random\n\ndef test_distribution():\n random.seed(17)\n values = [random.random() for _ in range(50)]\n assert values\n"}],"fixedFiles":[],"focusPath":"tests/test_selector.py","id":"seeded-random-sampling","outcome":"accept","scenarioId":"primary","title":"Make repeated random sampling reproducible"},{"expectedCount":1,"files":[{"path":"tests/test_selector.py","source":"import random\n\ndef test_distribution():\n values = [random.random() for _ in range(50)]\n assert values\n"}],"fixedFiles":[],"focusPath":"tests/test_selector.py","id":"unseeded-random-sampling","outcome":"reject","scenarioId":"primary","title":"Do not repeat probabilistic trials without reproducibility"}],"filePatterns":[],"id":"uncontrolled-randomness-in-test","key":"python:uncontrolled-randomness-in-test","languages":["python"],"limitations":["Only standard-library `random` calls nested in loops or comprehensions in collected tests are checked.","Single draws, injected RNG objects, Hypothesis tests, and cryptographic randomness are excluded.","Only an unconditional top-level seed before the repeated sample suppresses the finding; interprocedural seeding is not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"Repeated unseeded sampling makes pass/fail outcomes vary across identical runs and broad frequency bounds can conceal heavily biased behavior.","references":[],"remediation":"Inject a deterministic RNG, seed it explicitly, or use a property framework that records failing seeds.","since":null,"source":"packages/python/src/sarj_python_lint/rules/uncontrolled_randomness_in_test.py","status":"active","summary":"Test repeatedly samples the standard PRNG without a seed or injected deterministic random source.","test":"packages/python/tests/rules/test_uncontrolled_randomness_in_test.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","scenarioId":"primary","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","scenarioId":"primary","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":["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","scenarioId":"primary","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","scenarioId":"primary","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":"supabase/migrations/001_orders.sql","source":"CREATE TABLE orders (created_at TIMESTAMP NOT NULL);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_orders.sql","id":"naive-created-at","outcome":"reject","scenarioId":"primary","title":"Naive timestamp column"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_orders.sql","source":"CREATE TABLE orders (created_at TIMESTAMPTZ NOT NULL);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_orders.sql","id":"zoned-created-at","outcome":"accept","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":[],"autofix":"none","category":"maintainability","code":"SARJ113","defaultLevel":"warning","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"-- DROP TABLE legacy_events;\nSELECT 1;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"commented-statement","outcome":"reject","scenarioId":"primary","title":"Disabled SQL statement"},{"expectedCount":0,"files":[{"path":"migrations/001.sql","source":"-- Roll back by restoring snapshot OPS-812.\nSELECT 1;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"rollback-instruction","outcome":"accept","scenarioId":"primary","title":"Rollback instruction records an operational constraint"}],"filePatterns":[],"id":"no-comment-cruft","key":"sql:no-comment-cruft","languages":["sql"],"limitations":["The scanner distinguishes SQL comments from strings, identifiers, and executable dollar-quoted bodies.","Dialect, migration, dump, lint, rollback, and externally referenced comments are preserved."],"messageIds":[],"optionsSchema":null,"rationale":"Disabled statements drift from executable migrations while version control already preserves their history.","references":[],"remediation":"Delete disabled SQL and decorative dividers; retain only current constraints, rollback instructions, and owned references.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_comment_cruft.py","status":"active","summary":"Commented-out SQL, decorative banners, and untracked debt markers must be removed.","test":"packages/sql/tests/rules/test_no_comment_cruft.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ114","defaultLevel":"warning","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"supabase/migrations/001.sql","source":"ALTER TABLE child ADD CONSTRAINT child_tenant_fk FOREIGN KEY (organization_id, parent_id) REFERENCES parent (organization_id, id);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001.sql","id":"declarative-constraint","outcome":"accept","scenarioId":"primary","title":"Declarative database invariant"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001.sql","source":"CREATE TRIGGER update_timestamp BEFORE UPDATE ON calls EXECUTE FUNCTION set_timestamp();\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001.sql","id":"postgres-trigger","outcome":"reject","scenarioId":"primary","title":"Hidden trigger behavior"}],"filePatterns":[],"id":"no-create-trigger","key":"sql:no-create-trigger","languages":["sql"],"limitations":["PostgreSQL CREATE TRIGGER and CREATE CONSTRAINT TRIGGER statements are reported.","This is an organization-specific single-writer architecture policy, not a claim that triggers are invalid.","Dump files and non-PostgreSQL dialects are excluded.","Generated migrations report against their owning model when one can be identified."],"messageIds":[],"optionsSchema":null,"rationale":"Under a single-writer application architecture, triggers hide writes and state transitions from engineers reading application code and make the behavior difficult to exercise through ordinary unit-test seams. Triggers may still be appropriate for approved multi-writer integrity or audit needs.","references":[],"remediation":"Use a declarative database constraint where possible or explicit transactional application behavior; suppress this policy when an approved database-owned invariant requires a trigger.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_create_trigger.py","status":"active","summary":"This project keeps database behavior explicit instead of introducing PostgreSQL triggers.","test":"packages/sql/tests/rules/test_no_create_trigger.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","scenarioId":"primary","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","scenarioId":"primary","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":"supabase/migrations/001_status.sql","source":"CREATE TYPE call_status AS ENUM ('pending', 'active', 'completed');\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_status.sql","id":"postgres-enum-type","outcome":"reject","scenarioId":"primary","title":"PostgreSQL enum type"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_status.sql","source":"CREATE TABLE call (\n status TEXT NOT NULL CHECK (status IN ('pending', 'active', 'completed'))\n);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_status.sql","id":"text-check-constraint","outcome":"accept","scenarioId":"primary","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":"supabase/migrations/001_documents.sql","source":"CREATE TABLE document (metadata JSON NOT NULL);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_documents.sql","id":"json-column","outcome":"reject","scenarioId":"primary","title":"Plain JSON column"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_documents.sql","source":"CREATE TABLE document (metadata JSONB NOT NULL DEFAULT '{}'::jsonb);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_documents.sql","id":"jsonb-column","outcome":"accept","scenarioId":"primary","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.","Query and data-migration casts are excluded because an external or legacy JSON column may require them."],"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 table-DDL 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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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","scenarioId":"primary","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":"Delete narration. Where names are author-controlled, clarify jobs, steps, targets, keys, or sections; 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 `code-standards check` before merging.\n"}],"fixedFiles":[],"focusPath":"docs/operations.md","id":"maintained-operations-guide","outcome":"accept","scenarioId":"primary","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","scenarioId":"primary","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":"maintainability","code":"SARJ306","defaultLevel":"warning","engine":"text","examples":[{"expectedCount":0,"files":[{"path":"config.toml","source":"# Keep three retries because the upstream API is eventually consistent.\nretry_count = 3\n"}],"fixedFiles":[],"focusPath":"config.toml","id":"scalar-value-rationale","outcome":"accept","scenarioId":"primary","title":"A rationale adds information absent from the assignment"},{"expectedCount":1,"files":[{"path":"config.toml","source":"# Retry count is 3\nretry_count = 3\n"}],"fixedFiles":[],"focusPath":"config.toml","id":"scalar-value-restatement","outcome":"reject","scenarioId":"primary","title":"A prose comment repeats the assignment"}],"filePatterns":["**/*.yaml","**/*.yml","**/*.toml"],"id":"exact-config-comment-restatement","key":"text:exact-config-comment-restatement","languages":["config"],"limitations":["Only an immediately adjacent standalone comment using exact `key is value` or `key equals value` wording over a simple scalar entry is checked."],"messageIds":[],"optionsSchema":null,"rationale":"A comment that repeats the key and scalar value adds no information and can drift independently from the configuration it narrates.","references":[],"remediation":"Delete the restatement. If the entry is author-controlled and unclear, clarify its key or section; keep comments only for constraints or rationale absent from the value.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"YAML or TOML comment exactly repeats the adjacent scalar assignment","test":"packages/standards/tests/test_textlint.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ305","defaultLevel":"warning","engine":"text","examples":[{"expectedCount":1,"files":[{"path":"README.md","source":"<!--\n## Legacy setup\nUse the retired command.\n-->\n"}],"fixedFiles":[],"focusPath":"README.md","id":"hidden-obsolete-section","outcome":"reject","scenarioId":"primary","title":"A hidden heading disables a documentation section"},{"expectedCount":0,"files":[{"path":"README.md","source":"## Setup\n\nRun the current command.\n"}],"fixedFiles":[],"focusPath":"README.md","id":"visible-current-section","outcome":"accept","scenarioId":"primary","title":"Current documentation stays rendered"}],"filePatterns":["**/*.md","**/*.mdx"],"id":"hidden-markdown-heading","key":"text:hidden-markdown-heading","languages":["markdown"],"limitations":["Only standalone, closed HTML comments containing an ATX heading outside Markdown code are checked; template instructions and protected rationale are preserved."],"messageIds":[],"optionsSchema":null,"rationale":"A heading hidden from rendered documentation is disabled documentation that silently drifts while version control already preserves removed sections.","references":[],"remediation":"Delete the hidden section, or restore it as maintained rendered documentation.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"HTML comment hides a Markdown heading","test":"packages/standards/tests/test_textlint.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ304","defaultLevel":"error","engine":"text","examples":[{"expectedCount":0,"files":[{"path":"tests/observability.test.sh","source":"#!/bin/sh\nterraform show -json plan.out | jq -e '.resource_changes | length > 0'\n"}],"fixedFiles":[],"focusPath":"tests/observability.test.sh","id":"rendered-plan-query","outcome":"accept","scenarioId":"primary","title":"Query structured rendered plan output"},{"expectedCount":1,"files":[{"path":"tests/observability.test.sh","source":"#!/bin/sh\ngrep -q 'alert_policy' iac/alerts.tf\n"}],"fixedFiles":[],"focusPath":"tests/observability.test.sh","id":"terraform-source-grep","outcome":"reject","scenarioId":"primary","title":"Do not grep Terraform source in a shell test"}],"filePatterns":["**/*.sh","**/*.bash","**/*.zsh"],"id":"iac-source-coupled-test","key":"text:iac-source-coupled-test","languages":["config"],"limitations":["The scanner tokenizes shell quoting, comments, pipelines, direct command substitutions, and local variable flows; sourced helpers and eval remain unreported.","Only test-named shell files or shell files below a tests directory are checked."],"messageIds":[],"optionsSchema":null,"rationale":"Text searches can pass on comments, formatting, or unreachable Terraform configuration without proving provider or runtime behavior.","references":[],"remediation":"Inspect rendered plan JSON, provider state, or deployed runtime behavior instead of grepping IaC source.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"shell test asserts on raw IaC source text","test":"packages/standards/tests/test_textlint.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ307","defaultLevel":"warning","engine":"text","examples":[{"expectedCount":1,"files":[{"path":".claude/commands/lookup.md","source":"```sql\nSELECT id FROM records WHERE id = '$ARGUMENTS';\n```\n"}],"fixedFiles":[],"focusPath":".claude/commands/lookup.md","id":"query-interpolation","outcome":"reject","scenarioId":"primary","title":"Do not splice command arguments into queries"},{"expectedCount":0,"files":[{"path":".claude/commands/lookup.md","source":"```bash\nscripts/lookup.sh \"$ARGUMENTS\"\n```\n"}],"fixedFiles":[],"focusPath":".claude/commands/lookup.md","id":"quoted-wrapper-argument","outcome":"accept","scenarioId":"primary","title":"Pass an opaque argument to a validating wrapper"}],"filePatterns":[".claude/commands/*.md"],"id":"no-unsafe-command-argument-interpolation","key":"text:no-unsafe-command-argument-interpolation","languages":["markdown"],"limitations":["Only fenced executable examples in .claude/commands Markdown are checked.","A standalone quoted shell argument is accepted on the assumption that the called wrapper validates or parameterizes it."],"messageIds":[],"optionsSchema":null,"rationale":"Slash-command arguments are user-controlled. Embedding them into a shell token or query string can change command structure or query semantics when the documented command is executed.","references":[],"remediation":"Pass the argument as its own quoted shell token to a wrapper that validates or parameterizes it; never splice it into SQL, LogQL, or another query string.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"raw Claude command argument interpolated into an executable shell or query fence","test":"packages/standards/tests/test_textlint.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ308","defaultLevel":"warning","engine":"text","examples":[{"expectedCount":0,"files":[{"path":".claude/settings.json","source":"{\"permissions\":{\"allow\":[\"Bash(make pull-development-secrets)\"]}}\n"}],"fixedFiles":[],"focusPath":".claude/settings.json","id":"narrow-secret-wrapper","outcome":"accept","scenarioId":"primary","title":"Allow a validating project wrapper instead"},{"expectedCount":1,"files":[{"path":".claude/settings.json","source":"{\"permissions\":{\"allow\":[\"Bash(gcloud secrets versions access:*)\"]}}\n"}],"fixedFiles":[],"focusPath":".claude/settings.json","id":"wildcard-secret-read","outcome":"reject","scenarioId":"primary","title":"Do not preapprove every secret-value read"}],"filePatterns":[".claude/settings*.json","**/.claude/settings*.json"],"id":"no-wildcard-secret-read-permission","key":"text:no-wildcard-secret-read-permission","languages":["config"],"limitations":["Only literal wildcard allow entries for recognized cloud secret-value commands in Claude settings JSON are checked."],"messageIds":[],"optionsSchema":null,"rationale":"A wildcard allow entry for a secret-read command lets an agent retrieve every secret visible to the developer's cloud credentials without a per-command approval boundary.","references":[],"remediation":"Remove the wildcard permission. Allow a narrowly scoped wrapper that validates an explicit secret name, or require interactive approval for each secret-value read.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"Claude settings grant wildcard access to secret values","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","scenarioId":"primary","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","scenarioId":"primary","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}