{"rules":[{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ221","defaultLevel":"warning","engine":"iac","examples":[{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"example_service\" \"this\" {}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"stable-provisioning","outcome":"accept","scenarioId":"primary","title":"The desired resource is declared directly"},{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"example_service\" \"this\" {\n  count = var.use_new ? 1 : 0\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"temporary-provisioning-switch","outcome":"reject","scenarioId":"primary","title":"A migration switch retains two provisioning paths"}],"filePatterns":[],"id":"conditional-resource-presence","key":"iac:conditional-resource-presence","languages":["iac"],"limitations":["This is a review advisory, not proof that optional infrastructure is wrong. No resource deletion or state-address rewrite is automatic.","Inspect count and for_each on resource, module and ephemeral blocks, including direct same-file local aliases and collection conversions. Plain multiplicity, ordinary value selections, data lookups and member-specific comprehension filters are not targeted.","Cross-file aliases, object traversals and arbitrary function results remain opaque. Cycles and ambiguous declarations are not guessed.","The specific deployment-identity policy takes precedence; this warning takes precedence over a generic nested-selection warning at the same attribute when both rules are selected.","Only authored .tf inputs are checked; generated headers, fixtures and other HCL/JSON dialects are excluded. Review justification does not require adding a Terraform comment."],"messageIds":[],"optionsSchema":null,"rationale":"Conditional zero/empty instance selections duplicate provisioning paths and introduce indexed resource references. Temporary migration switches can outlive the migration and obscure the intended topology.","references":[],"remediation":"Prefer a direct migration and stable resource declarations. If optional provisioning is necessary, justify its lifetime and ownership in the PR or input description. Preserve state addresses and restore/cutover ordering when simplifying an existing deployment.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/conditional_resource_presence.py","status":"active","summary":"Prefer stable Terraform resource presence or an explicit review justification.","test":"packages/iac/tests/rules/test_conditional_resource_presence.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ217","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  value = true ? (true ? (true ? (true ? (true ? (true ? (true ? (1) : 2) : 2) : 2) : 2) : 2) : 2) : 2\n}\n"}],"fixedFiles":[{"path":"main.tf","source":"locals {\n  value = 1\n}\n"}],"focusPath":"main.tf","id":"closed-literal-tree","outcome":"reject","scenarioId":"closed-literal-tree","title":"Fold a complete literal conditional tree"},{"expectedCount":0,"files":[{"path":"main.tf","source":"locals {\n  value = 1\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"closed-literal-tree-fixed","outcome":"accept","scenarioId":"closed-literal-tree","title":"The selected literal retains its scalar type"},{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  ready = var.a > 0 && var.b > 0 && var.c > 0 && var.d > 0 && var.e > 0 && var.f > 0 && var.g > 0\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"large-predicate","outcome":"reject","scenarioId":"primary","title":"A statement combines too many independent checks"},{"expectedCount":0,"files":[{"path":"variables.tf","source":"variable \"port\" {\n  type = number\n  validation {\n    condition = var.port > 0 && var.port < 65536\n    error_message = \"Supply a valid port.\"\n  }\n}\n"}],"fixedFiles":[],"focusPath":"variables.tf","id":"small-domain-check","outcome":"accept","scenarioId":"primary","title":"A small range validation remains readable"}],"filePatterns":[],"id":"expression-complexity","key":"iac:expression-complexity","languages":["iac"],"limitations":["Budget: 12 points per attribute, 6 per variable validation condition. Calls, operators and comprehension filters cost one; conditionals and comprehensions cost two.","Scores depend on syntax, not formatting or line count. Actual template expressions count; literal data, type constraints and embedded languages such as CEL do not.","Does not limit variable counts or prove that a necessary security check can be deleted. Malformed HCL is left to native validation.","Conditional safe fixes reuse the existing bounded, same-type literal-conditional evaluator for a complete single-line attribute in an unlabelled top-level locals block. Symbolic expressions, real validation, mixed scalar types, escapes, templates and values outside the evaluator bounds remain manual."],"messageIds":[],"optionsSchema":null,"rationale":"Large predicates hide independent deployment requirements and make an infrastructure change difficult to review.","references":[],"remediation":"Delete redundant checks or name independently meaningful expressions. Preserve lazy evaluation, null guards and dependencies; keep genuine domain validation.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/expression_complexity.py","status":"active","summary":"Limit Terraform statement and variable-validation expression complexity.","test":"packages/iac/tests/rules/test_expression_complexity.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ220","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"variable \"keyless_storage_enabled\" {\n  type = bool\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"boolean-enable-input","outcome":"reject","scenarioId":"primary","title":"An extra switch gates the intended implementation"},{"expectedCount":1,"files":[{"path":"main.tf","source":"variable \"migration_phase\" {\n  type = string\n  description = \"Retire by: 2000-01-01\\nRemove when: Every deployment uses the new store; delete the old phase and input.\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"expired-nonboolean-migration-mode","outcome":"reject","scenarioId":"migration-mode","title":"Explicit lifecycle records also expire on migration modes"},{"expectedCount":1,"files":[{"path":"main.tf","source":"variable \"paging_enabled\" {\n  type = bool\n  description = \"Permanent:\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"permanent-control-without-rationale","outcome":"reject","scenarioId":"permanent","title":"A permanent exemption needs a rationale"},{"expectedCount":0,"files":[{"path":"main.tf","source":"variable \"paging_enabled\" {\n  type = bool\n  description = \"Permanent: Production pages on-call; disposable environments must keep paging off.\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"permanent-operational-control","outcome":"accept","scenarioId":"permanent","title":"Explain a lasting per-environment control"},{"expectedCount":0,"files":[{"path":"main.tf","source":"variable \"migration_phase\" {\n  type = string\n  description = \"Retire by: 2099-12-31\\nRemove when: Every deployment uses the new store; delete the old phase and input.\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"scheduled-nonboolean-migration-mode","outcome":"accept","scenarioId":"migration-mode","title":"Keep an explicitly scheduled migration mode until cutover"},{"expectedCount":0,"files":[{"path":"main.tf","source":"variable \"private_route_enabled\" {\n  type = bool\n  description = \"Retire by: 2099-12-31\\nRemove when: All deployments use the private route; delete this input and the legacy route.\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"temporary-switch-with-removal-plan","outcome":"accept","scenarioId":"primary","title":"Record the deadline and deletion condition in the input description"}],"filePatterns":[],"id":"no-boolean-enable-variable","key":"iac:no-boolean-enable-variable","languages":["iac"],"limitations":["This is an explicit interface policy, not proof that a switch is redundant or that its rationale is truthful.","Matches every top-level .tf variable declaration with explicit bool type, regardless of its name. This requires classification, without inferring whether the input is temporary or permanent.","An explicit Retire by: or Permanent: description line also opts any other top-level variable into this policy, including migration modes.","Lifecycle records must be unique description lines in a literal quoted string or heredoc. Comments and nested descriptions cannot satisfy them; dynamic descriptions cannot be verified.","Retirement dates are inclusive UTC calendar dates; the finding returns the following day even if the gate is already enabled. There is no hard maximum duration or automatic extension tracking.","Unannotated provider attributes, object members, non-Boolean inputs, inferred types and JSON syntax are excluded. Generators must put the lifecycle description in their generated declarations.","CI must run after the date passes to detect expiry. The rule does not change Terraform planning or apply behavior.","Result caches must include the current UTC date or leave this rule uncached; unchanged source can acquire an expiry finding.","No automatic deletion or branch selection is safe: changing count/for_each can destroy resources."],"messageIds":[],"optionsSchema":null,"rationale":"Enable switches add configuration plumbing and keep alternate implementations alive. An infrastructure change should normally configure its intended behavior directly. Necessary temporary gates need a deadline for deleting the switch and old path; lasting controls need an explicit rationale.","references":[],"remediation":"Remove the switch and configure the intended behavior directly. If deployments need different values, pass the actual typed configuration instead. Otherwise add description lines 'Retire by: YYYY-MM-DD' and 'Remove when: <cutover condition and removal action>', or 'Permanent: <why both settings remain useful>'. Prefer cutover now; aim to retire temporary gates within 30 days and remove them earlier when ready. Review every environment, resource address, prerequisite and consumer before removal.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_boolean_enable_variable.py","status":"active","summary":"Require a retirement date and removal plan, or a permanent rationale, for Boolean inputs.","test":"packages/iac/tests/rules/test_no_boolean_enable_variable.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ202","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"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 HCL resource"},{"expectedCount":1,"files":[{"path":"main.tf","source":"# ==== Networking ====\nmodule \"networking\" {}\n"}],"fixedFiles":[{"path":"main.tf","source":"\nmodule \"networking\" {}\n"}],"focusPath":"main.tf","id":"decorative-divider","outcome":"reject","scenarioId":"divider","title":"Decorative divider around a section title"},{"expectedCount":0,"files":[{"path":"main.tf","source":"# Networking is isolated to preserve the production trust boundary.\nmodule \"networking\" {}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"descriptive-section-comment","outcome":"accept","scenarioId":"divider","title":"Section comment that explains a constraint"},{"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":["Safe fixes remove diagnosed native-HCL line-comment banners and complete disabled HCL-only runs while preserving executable tokens; mixed rationale, directives, block comments and YAML remain for review.","Commented assignments in tfvars files are allowed because they commonly document optional inputs.","Generated files, testdata, and fixture trees are excluded because comments may be intentional test input.","Tool directives, heredoc bodies, YAML block scalars, and malformed block comments are excluded.","Disabled HCL comment runs must be code-dominant and produce one finding per contiguous run."],"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; use executable structure and concise rationale comments.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_comment_cruft.py","status":"active","summary":"Commented-out HCL declarations or decorative divider comments must be removed; retain comments that explain constraints or rationale.","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":"text_llm_enable_thinking = true\n"},{"path":"variables.tf","source":"variable \"text_llm_enable_thinking\" {\n  type    = bool\n  default = true\n}\n"}],"fixedFiles":[],"focusPath":"env/dev/terraform.tfvars","id":"assignment-equal-to-declared-default","outcome":"reject","scenarioId":"equals-default","title":"Assignment restating the variable's declared default"},{"expectedCount":0,"files":[{"path":"env/dev/terraform.tfvars","source":"region = \"me-central2\"\ngke_enabled = true\n"},{"path":"variables.tf","source":"variable \"region\" {\n  type = string\n}\nvariable \"gke_enabled\" {\n  type = bool\n}\n"}],"fixedFiles":[],"focusPath":"env/dev/terraform.tfvars","id":"declared-assignment","outcome":"accept","scenarioId":"orphaned-key","title":"Every tfvars assignment has a parsed root declaration"},{"expectedCount":0,"files":[{"path":"env/dev/terraform.tfvars","source":"text_llm_enable_thinking = true\n"},{"path":"env/prod/terraform.tfvars","source":"text_llm_enable_thinking = false\n"},{"path":"variables.tf","source":"variable \"text_llm_enable_thinking\" {\n  type    = bool\n  default = true\n}\n"}],"fixedFiles":[],"focusPath":"env/dev/terraform.tfvars","id":"default-is-an-active-environment-choice","outcome":"accept","scenarioId":"equals-default","title":"A sibling environment overrides the declared default"},{"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    = bool\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"},{"expectedCount":0,"files":[{"path":"dev.tfvars.json","source":"{\"region\":\"west\"}\n"},{"path":"prod.tfvars.json","source":"{\"region\":\"east\"}\n"},{"path":"variables.tf","source":"variable \"region\" {\n  type = string\n}\n"}],"fixedFiles":[],"focusPath":"dev.tfvars.json","id":"json-input-varies","outcome":"accept","scenarioId":"json-input","title":"JSON scalar values retain environment differences"},{"expectedCount":1,"files":[{"path":"dev.tfvars.json","source":"{\"region\":\"west\",\"ghost\":true}\n"},{"path":"variables.tf","source":"variable \"region\" {\n  type = string\n}\n"}],"fixedFiles":[],"focusPath":"dev.tfvars.json","id":"json-orphaned-assignment","outcome":"reject","scenarioId":"json-input","title":"JSON variable files receive the same declaration checks"},{"expectedCount":1,"files":[{"path":"env/dev/terraform.tfvars","source":"region = \"me-central2\"\ngke_enabled = true\n"},{"path":"variables.tf","source":"variable \"region\" {\n  type = string\n}\n"}],"fixedFiles":[],"focusPath":"env/dev/terraform.tfvars","id":"orphaned-assignment-after-variable-deletion","outcome":"reject","scenarioId":"orphaned-key","title":"Assignment surviving the deletion of its variable"},{"expectedCount":0,"files":[{"path":"env/dev/terraform.tfvars","source":"api_token = \"dev-placeholder\"\n"},{"path":"env/prod/terraform.tfvars","source":"api_token = \"prod-placeholder\"\n"},{"path":"variables.tf","source":"variable \"api_token\" {\n  type      = string\n  sensitive = true\n}\n"}],"fixedFiles":[],"focusPath":"env/dev/terraform.tfvars","id":"sensitive-input-that-varies-by-environment","outcome":"accept","scenarioId":"sensitive-shared-input","title":"Sensitive input carrying an actual environment decision"},{"expectedCount":1,"files":[{"path":"env/dev/terraform.tfvars","source":"api_token = \"same-placeholder\"\n"},{"path":"env/prod/terraform.tfvars","source":"api_token = \"same-placeholder\"\n"},{"path":"variables.tf","source":"variable \"api_token\" {\n  type      = string\n  sensitive = true\n}\n"}],"fixedFiles":[],"focusPath":"env/dev/terraform.tfvars","id":"sensitive-value-repeated-across-environments","outcome":"reject","scenarioId":"sensitive-shared-input","title":"Sensitive input duplicated instead of injected once"}],"filePatterns":[],"id":"no-dead-environment-input","key":"iac:no-dead-environment-input","languages":["iac"],"limitations":["Cross-environment comparisons require at least two discovered, readable HCL or JSON tfvars environments.","Only scalar variables declared exactly as bool, number, or string are compared; dynamic, collection, interpolated HCL, heredoc, and incomplete roots are conservatively skipped.","Unreadable declarations, JSON configuration declarations, and override files suppress findings because the effective variable contracts cannot be proven.","Malformed or duplicate-key JSON inputs suppress findings for that file and comparisons across the root.","Numeric-to-string comparisons abstain for non-finite values or exponents expanding beyond 10,000 decimal places; unsupported HCL string escapes are also skipped.","Named tfvars files are inferred as environments; repeated values are advisory because invocation order and intentional fail-closed contracts cannot be proven from filenames.","Generated fixtures, testdata, backend configuration, backups, templates, and other specimen inputs are excluded.","Sensitive and non-primitive values are never printed in diagnostics."],"messageIds":[],"optionsSchema":null,"rationale":"Undeclared assignments are stale configuration, while repeated scalar values can hide whether a setting is truly environment-specific or belongs in shared configuration.","references":[],"remediation":"Remove assignments that have no parsed declaration. For repeated values, confirm the setting is not an intentional per-environment contract before moving it to a default or shared input source.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_dead_environment_input.py","status":"active","summary":"Find undeclared tfvars assignments and potentially redundant scalar values across discovered Terraform environments.","test":"packages/iac/tests/rules/test_no_dead_environment_input.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ218","defaultLevel":"warning","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"cloudflare_r2_bucket\" \"a\" { account_id = \"example-account\" }\nresource \"cloudflare_r2_bucket\" \"b\" { account_id = \"example-account\" }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"repeated-cloudflare-account","outcome":"reject","scenarioId":"cloudflare-account","title":"Cloudflare resources repeat their deployment account"},{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"google_storage_bucket\" \"a\" { project = \"example-project\" }\nresource \"google_cloud_run_v2_job\" \"b\" { project = \"example-project\" }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"repeated-project","outcome":"reject","scenarioId":"primary","title":"Resources repeat one deployment project"},{"expectedCount":0,"files":[{"path":"main.tf","source":"variable \"account_id\" { type = string }\nresource \"cloudflare_r2_bucket\" \"a\" { account_id = var.account_id }\nresource \"cloudflare_r2_bucket\" \"b\" { account_id = var.account_id }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"shared-cloudflare-account-input","outcome":"accept","scenarioId":"cloudflare-account","title":"Cloudflare resources share their typed account input"},{"expectedCount":0,"files":[{"path":"main.tf","source":"variable \"project_id\" { type = string }\nresource \"google_storage_bucket\" \"a\" { project = var.project_id }\nresource \"google_cloud_run_v2_job\" \"b\" { project = var.project_id }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"shared-project-input","outcome":"accept","scenarioId":"primary","title":"Resources use their shared deployment input"}],"filePatterns":[],"id":"no-duplicate-deployment-literal","key":"iac:no-duplicate-deployment-literal","languages":["iac"],"limitations":["Checks repeated identical direct string assignments to curated cloud scope/location attributes on resource and data blocks in one .tf file. Each provider/attribute/value group produces one warning at its second use.","Cloudflare coverage includes direct account_id and zone_id scope arguments; record contents, nested policy objects and security expectations are not inferred from matching identifier text.","Explicit provider bindings are grouped separately. Provider configuration, module inputs, variable defaults, locals, snapshot maps, hashes, roles, embedded configuration, generated sources and fixture/data files are excluded.","Expressions, templates, indirect aliases, cross-file repetitions, JSON HCL and unknown providers are not inferred. The Google global location sentinel is not a deployment input.","No autofix: shared values and deliberately independent configuration need an author decision. This warning does not prove that literal infrastructure is invalid or authorize changing a security baseline."],"messageIds":[],"optionsSchema":null,"rationale":"Repeating deployment identities and locations scatters configuration across resource declarations and makes coordinated changes harder to review.","references":[],"remediation":"Use an existing resource/data reference, a shared local for a fixed deployment value, or a module input for a caller-configured value. Keep independently configurable scopes independent.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_duplicate_deployment_literal.py","status":"active","summary":"Share repeated cloud deployment values through variables, locals or resource references.","test":"packages/iac/tests/rules/test_no_duplicate_deployment_literal.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ204","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":0,"files":[{"path":"main.tf","source":"locals {\n  queue = var.project == \"analytics\" ? \"events\" : \"default\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"business-project-value","outcome":"accept","scenarioId":"ambiguous-identity","title":"Business project identity has no deployment evidence"},{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  access_count = var.project == local.catalog.prod.project ? 1 : 0\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"catalog-derived-deployment-policy","outcome":"reject","scenarioId":"catalog-comparison","title":"A static deployment catalog selector derives access policy"},{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  selected = !contains([\"acme-dev:dev\"], \"${var.project}:${var.environment}\")\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"composed-environment-membership","outcome":"reject","scenarioId":"template-operand","title":"Composed deployment identity selects hard-coded policy"},{"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":0,"files":[{"path":"main.tf","source":"locals {\n  redis_tier = var.redis_tiers_by_environment[var.environment]\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"environment-keyed-input-map","outcome":"accept","scenarioId":"data-selection","title":"Externally supplied map selects a value without hard-coded branches"},{"expectedCount":0,"files":[{"path":"main.tf","source":"locals { name = \"cache-${var.environment}\" }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"environment-resource-name","outcome":"accept","scenarioId":"template-operand","title":"Environment interpolation only names a resource"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"google_project_iam_member\" \"query_runner\" {\n  count   = var.access_count\n  project = var.project\n  role    = \"roles/bigquery.jobUser\"\n  member  = var.member\n}\nresource \"google_project_iam_member\" \"ai_roles\" {\n  for_each = var.ai_roles\n  project  = var.project\n  role     = each.value\n  member   = var.member\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"explicit-deployment-policy-inputs","outcome":"accept","scenarioId":"catalog-comparison","title":"Pass the access count and roles directly as typed inputs"},{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  redis_tier = var.environment == \"prod\" ? \"STANDARD_HA\" : \"BASIC\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"hard-coded-environment-choice","outcome":"reject","scenarioId":"data-selection","title":"Code branches on a hard-coded environment label"},{"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"},{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  tier = var.gcp_project_id == \"platform-prod\" ? \"HA\" : \"BASIC\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"project-id-with-environment-evidence","outcome":"reject","scenarioId":"ambiguous-identity","title":"Cloud project identity names a deployment environment"}],"filePatterns":[],"id":"no-environment-conditional","key":"iac:no-environment-conditional","languages":["iac"],"limitations":["Comparisons against a catalog traversal with an exact static deployment selector, such as local.catalog.prod.project, are advisory warnings when both operands have the same qualified identity leaf. Catalog keys can describe business concepts rather than deployments. Dynamic keys, computed operands, alias definitions and external catalog contents are not resolved. Existing literal-comparison findings remain errors and take precedence within the same attribute.","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. Quoted templates used as comparison operands or literal-list contains needles are inspected only when their interpolations are references or pure normalizers. Escaped markers, branches inside templates, directives, brace-containing interpolation bodies, arbitrary function results, and heredoc bodies remain opaque.","Pure case/whitespace normalization with lower, upper, or trimspace is unwrapped; arbitrary function results remain opaque.","Ambiguous identity names (`project`, `account`, `tenant`, `branch`, `stack`, and `slug`) require an environment-labelled comparison literal or literal-list member such as `platform-prod`; comparisons to ordinary business values and map indexing by those names are intentionally ignored.","Only .tf and terragrunt.hcl are read; Packer, Nomad, and other HCL dialects are outside this rule.","Environment-keyed map indexing and lookup are data selection rather than branching and are allowed.","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":"Hard-coded environment branches scatter deployment policy through expressions and obscure the actual capability or value that callers intend to vary.","references":[],"remediation":"Pass the selected typed value or one named capability from the root configuration. Retain an explicit validation, precondition, or check when environment identity is itself the safety invariant.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_environment_conditional.py","status":"active","summary":"Warn when Terraform chooses behavior from deployment-identity comparisons; prefer explicit typed capabilities or values.","test":"packages/iac/tests/rules/test_no_environment_conditional.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ219","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"google_storage_bucket\" \"this\" {\n  location = \"us-central1\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"literal-placement","outcome":"reject","scenarioId":"primary","title":"The resource ignores caller placement configuration"},{"expectedCount":0,"files":[{"path":"main.tf","source":"variable \"region\" { type = string }\nresource \"google_storage_bucket\" \"this\" {\n  location = var.region\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"placement-input","outcome":"accept","scenarioId":"primary","title":"The caller owns placement through a typed input"}],"filePatterns":[],"id":"no-hardcoded-deployment-location","key":"iac:no-hardcoded-deployment-location","languages":["iac"],"limitations":["An explicit configuration-ownership policy, not a claim that literal regions are invalid Terraform. Variable defaults, tfvars and caller module arguments are valid configuration boundaries.","Checks recognized cloud provider/resource placement attributes and enumerated nested placement schema paths, including dynamic content. Cloudflare is limited to R2 bucket placement; Hetzner to geographic location fields. Labels, tags, geographic access filters, DNS zones and provider-required global selectors are not deployment coordinates.","Recognizes literal strings/lists, direct same-file local aliases, static joins, simple conversions, and conditional/coalesce/try output alternatives. Unknown function results, object traversals, cross-file aliases and inherited provider configuration remain opaque.","At the same attribute, this error takes precedence over repeated-deployment-literal guidance when both rules are selected.","Only authored .tf inputs are checked; generated headers, fixtures and other HCL/JSON dialects are excluded. No variable name, value migration or Terraform state change is automatically inferred."],"messageIds":[],"optionsSchema":null,"rationale":"Literal placement coordinates inside infrastructure implementations bypass the caller's deployment configuration. Resources in one module can silently remain in different locations when inputs change.","references":[],"remediation":"Reuse an existing typed region, zone or location input; otherwise add the smallest required input. Define deployment policy in caller inputs, variable defaults or tfvars instead of scattering literal coordinates and matching preconditions through resource implementations.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_hardcoded_deployment_location.py","status":"active","summary":"Pass cloud deployment regions, zones and locations through explicit configuration inputs.","test":"packages/iac/tests/rules/test_no_hardcoded_deployment_location.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ211","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"google_service_account_key\" \"app\" { service_account_id = \"app\" }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"managed-key","outcome":"reject","scenarioId":"primary","title":"A managed key creates persistent credentials"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"google_service_account\" \"app\" { account_id = \"app\" }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"service-account","outcome":"accept","scenarioId":"primary","title":"A service account needs no managed key"}],"filePatterns":[],"id":"no-managed-service-account-key","key":"iac:no-managed-service-account-key","languages":["iac"],"limitations":["Only google_service_account_key resource blocks in .tf files are checked; data sources are allowed."],"messageIds":[],"optionsSchema":null,"rationale":"Managed private keys persist in Terraform state and create long-lived credentials.","references":[],"remediation":"Use workload identity federation or service-account impersonation instead of creating a key.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_managed_service_account_key.py","status":"active","summary":"Disallow Terraform-managed Google service-account keys.","test":"packages/iac/tests/rules/test_no_managed_service_account_key.py"},{"aliases":["no-terraform-test-file"],"autofix":"none","category":"testing","code":"SARJ206","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"tests/routing.tftest.hcl","source":"override_module {\n  target = module.storage\n  outputs = { arn = \"fixture-arn\" }\n}\n\nrun \"routing\" {\n  assert {\n    condition     = module.storage.arn == \"fixture-arn\"\n    error_message = \"ARN mismatch\"\n  }\n}\n"}],"fixedFiles":[],"focusPath":"tests/routing.tftest.hcl","id":"direct-override-reassertion","outcome":"reject","scenarioId":"terraform-provider-evidence","title":"Assertion repeats its own module output override"},{"expectedCount":0,"files":[{"path":"tests/routing.tftest.hcl","source":"mock_provider \"aws\" {}\n\nrun \"routing\" {\n  command = plan\n\n  assert {\n    condition     = aws_s3_bucket.main.bucket == var.bucket_name\n    error_message = \"bucket configuration drifted\"\n  }\n}\n"}],"fixedFiles":[],"focusPath":"tests/routing.tftest.hcl","id":"mock-backed-configuration-test","outcome":"accept","scenarioId":"terraform-provider-evidence","title":"Empty mock provider validates configured plan behavior"}],"filePatterns":[],"id":"no-mocked-terraform-test-oracle","key":"iac:no-mocked-terraform-test-oracle","languages":["iac"],"limitations":["Only direct literal entries in outputs maps on file-level or run-level override_module blocks in .tftest.hcl are compared. Run overrides replace file-level overrides for the entire target.","Resource/data overrides replace computed provider attributes, not configured or noncomputed default attributes. Without provider-schema proof, these overrides are excluded, even for genuine computed-value reassertions. Mock providers, generated defaults, transformed assertions, JSON test syntax, provider-scoped overrides, and dynamic expressions are also excluded."],"messageIds":[],"optionsSchema":null,"rationale":"An assertion that compares an overridden attribute directly with its authored override value is self-fulfilling; it does not exercise configuration logic or provider behavior.","references":[],"remediation":"Assert on configuration behavior derived from the override, or add provider-backed/runtime coverage for provider-dependent claims. Empty mock providers remain valid for fast configuration tests.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_mocked_terraform_test_oracle.py","status":"active","summary":"Warn when a Terraform assertion directly reasserts the same literal injected by a module override.","test":"packages/iac/tests/rules/test_no_mocked_terraform_test_oracle.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ215","defaultLevel":"warning","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  size = true ? \"large\" : false ? \"medium\" : \"small\"\n}\n"}],"fixedFiles":[{"path":"main.tf","source":"locals {\n  size = \"large\"\n}\n"}],"focusPath":"main.tf","id":"closed-literal-selection","outcome":"reject","scenarioId":"closed-literal-selection","title":"A closed literal selection already determines the selected value"},{"expectedCount":0,"files":[{"path":"main.tf","source":"variable \"size\" { type = string }\nmodule \"service\" {\n  source = \"./service\"\n  size = var.size\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"explicit-selected-value","outcome":"accept","scenarioId":"primary","title":"The caller supplies the selected value"},{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  size = var.primary ? \"large\" : var.secondary ? \"medium\" : \"small\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"nested-selection","outcome":"reject","scenarioId":"primary","title":"A nested selection hides the selected configuration"},{"expectedCount":0,"files":[{"path":"main.tf","source":"locals {\n  size = \"large\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"selected-literal-value","outcome":"accept","scenarioId":"closed-literal-selection","title":"The selected literal is already explicit"}],"filePatterns":[],"id":"no-nested-conditional","key":"iac:no-nested-conditional","languages":["iac"],"limitations":["Safe fixes select an existing literal leaf only from a closed Boolean-literal conditional tree with homogeneous Boolean, bounded decimal-number or plain quoted-string leaves on one standalone attribute line in top-level locals. All branches are validated; comments, escapes, templates, nulls, references, calls, mixed types and ambiguous source ranges remain manual.","Only direct conditional branches are inspected; independent choices inside calls, collections and templates remain out of scope.","Assertions, generated files, fixtures, testdata and non-.tf inputs are excluded.","An existing deployment-identity diagnostic takes precedence at the same attribute.","This is a review advisory; no automatic extraction or deletion is safe for every expression."],"messageIds":[],"optionsSchema":null,"rationale":"Chained conditional branches obscure the value being selected and scatter deployment policy.","references":[],"remediation":"Extract a branch into a named local or pass a typed input from the caller. Preserve lazy evaluation, null guards and resource dependencies when extracting an expression.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_nested_conditional.py","status":"active","summary":"Prefer a named value or explicit input over chained Terraform conditional selections.","test":"packages/iac/tests/rules/test_no_nested_conditional.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ214","defaultLevel":"warning","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"google_cloud_run_v2_service\" \"api\" {\n  template {\n    containers {\n      env {\n        name  = \"API_KEY\"\n        value = var.api_key\n      }\n    }\n  }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"plain-api-key","outcome":"reject","scenarioId":"primary","title":"An API key is injected as a plain value"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"google_cloud_run_v2_service\" \"api\" {\n  template {\n    containers {\n      env {\n        name = \"API_KEY\"\n        value_source {\n          secret_key_ref {\n            secret  = google_secret_manager_secret.api_key.secret_id\n            version = \"latest\"\n          }\n        }\n      }\n    }\n  }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"secret-reference","outcome":"accept","scenarioId":"primary","title":"An API key is read from Secret Manager"}],"filePatterns":[],"id":"no-plaintext-secret-env","key":"iac:no-plaintext-secret-env","languages":["iac"],"limitations":["Only `env` blocks inside Terraform resources in .tf files are checked.","The literal name must end in SECRET, TOKEN, PASSWORD, API_KEY or PRIVATE_KEY; other secrets, such as a password interpolated into a connection string, are not recognized.","Empty literal values are not reported."],"messageIds":[],"optionsSchema":null,"rationale":"A plain `value` copies the secret into Terraform state, the plan output and the service revision, where anyone who can read them sees it, and rotating it needs a redeploy.","references":[],"remediation":"Store the secret in Secret Manager and reference it with `value_source { secret_key_ref { ... } }` or `value_from { secret_key_ref { ... } }`.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_plaintext_secret_env.py","status":"active","summary":"A secret-named container environment variable should come from a secret reference, not a plain value.","test":"packages/iac/tests/rules/test_no_plaintext_secret_env.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ212","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"locals { grants = [\"roles/editor\", \"roles/logging.logWriter\"] }\nresource \"google_project_iam_member\" \"app\" {\n  for_each = toset(local.grants)\n  role = each.value\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"project-editor","outcome":"reject","scenarioId":"primary","title":"Project Editor grants broad mutable access"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"google_project_iam_member\" \"app\" { role = \"roles/logging.logWriter\" }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"specific-role","outcome":"accept","scenarioId":"primary","title":"A specific role stays scoped"}],"filePatterns":[],"id":"no-project-basic-privilege","key":"iac:no-project-basic-privilege","languages":["iac"],"limitations":["Only project IAM member, binding and policy resources in .tf files are checked.","Literal roles, literal for_each role sets, same-file unique local tuples/sets, jsonencode bindings and same-file google_iam_policy data are recognized.","Scalar role values also follow same-file unique static local aliases and bounded literal join expressions; grouping and HCL string escapes preserve the value. Cycles, duplicate declarations, templates and unknown inputs remain opaque.","Local collections must be literal strings consumed as a set; duplicate declarations, local aliases, dynamic values, maps and comprehensions are excluded.","Unknown expressions, inherited organization/folder access and other roles are not guessed."],"messageIds":[],"optionsSchema":null,"rationale":"Project Owner and Editor grants confer broad mutable access across project services.","references":[],"remediation":"Grant the specific predefined or custom roles required by the principal.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_project_basic_privilege.py","status":"active","summary":"Disallow proven project IAM Owner and Editor grants.","test":"packages/iac/tests/rules/test_no_project_basic_privilege.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ213","defaultLevel":"warning","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"google_project_iam_member\" \"api\" {\n  role = join(\"\", [\"roles/secret\", \"manager.secretAccessor\"])\n  member = \"serviceAccount:worker@example.com\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"joined-project-secret-accessor","outcome":"reject","scenarioId":"static-role-principal","title":"A static role expression still grants project-wide access"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"google_secret_manager_secret_iam_member\" \"api\" {\n  secret_id = google_secret_manager_secret.api_key.id\n  role      = \"roles/secretmanager.secretAccessor\"\n  member    = \"serviceAccount:${google_service_account.api.email}\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"per-secret-accessor","outcome":"accept","scenarioId":"primary","title":"A service account reads only the secret it needs"},{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"google_project_iam_member\" \"api\" {\n  role   = \"roles/secretmanager.secretAccessor\"\n  member = \"serviceAccount:${google_service_account.api.email}\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"project-secret-accessor","outcome":"reject","scenarioId":"primary","title":"A service account can read every project secret"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"google_project_iam_member\" \"api\" {\n  role = join(\"\", [\"roles/secret\", \"manager.secretAccessor\"])\n  member = replace(\"serviceAccount:eng@example.com\", \"serviceAccount:\", \"group:\")\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"transformed-human-member","outcome":"accept","scenarioId":"static-role-principal","title":"A source string does not prove the resulting identity"}],"filePatterns":[],"id":"no-project-secret-access-for-service-account","key":"iac:no-project-secret-access-for-service-account","languages":["iac"],"limitations":["Only google_project_iam_member and google_project_iam_binding resources in .tf files are checked.","Roles must resolve to static strings or literal for_each role sets through same-file locals and joins; members must directly declare `serviceAccount:` strings or templates, individually or in a list, including up to 32 nested `toset` and `tolist` conversions.","Human users and groups are not reported, and access inherited from folders or organizations is not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"A project-level Secret Manager accessor grant lets the workload read every secret in the project, so removing one secret's environment variable from the service does not remove its access.","references":[],"remediation":"Grant `roles/secretmanager.secretAccessor` with `google_secret_manager_secret_iam_member` on each secret the service account needs.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_project_secret_access_for_service_account.py","status":"active","summary":"Grant service accounts Secret Manager access per secret, not on the project.","test":"packages/iac/tests/rules/test_no_project_secret_access_for_service_account.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ209","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"variables.tf","source":"variable \"region\" {\n  type = string\n  validation {\n    condition     = can(tostring(var.region))\n    error_message = \"Region must be a string.\"\n  }\n}\n"}],"fixedFiles":[{"path":"variables.tf","source":"variable \"region\" {\n  type = string\n\n\n\n\n}\n"}],"focusPath":"variables.tf","id":"declared-string-conversion","outcome":"reject","scenarioId":"primary","title":"String conversion repeats the declared string type"},{"expectedCount":1,"files":[{"path":"variables.tf","source":"variable \"enabled\" {\n  type     = bool\n  nullable = false\n  validation {\n    condition     = contains([true, false], var.enabled)\n    error_message = \"Enabled must be true or false.\"\n  }\n}\n"}],"fixedFiles":[],"focusPath":"variables.tf","id":"non-nullable-boolean-domain","outcome":"reject","scenarioId":"nullability","title":"Boolean enumeration repeats a non-nullable boolean type"},{"expectedCount":0,"files":[{"path":"variables.tf","source":"variable \"enabled\" {\n  type = bool\n  validation {\n    condition     = contains([true, false], var.enabled)\n    error_message = \"Enabled must not be null.\"\n  }\n}\n"}],"fixedFiles":[],"focusPath":"variables.tf","id":"nullable-boolean-null-guard","outcome":"accept","scenarioId":"nullability","title":"Boolean enumeration rejects an otherwise permitted null"},{"expectedCount":0,"files":[{"path":"variables.tf","source":"variable \"region\" {\n  type = string\n  validation {\n    condition     = can(regex(\"^[a-z]+-[a-z]+[0-9]+$\", var.region))\n    error_message = \"Region must use the provider region format.\"\n  }\n}\n"}],"fixedFiles":[],"focusPath":"variables.tf","id":"string-domain-format","outcome":"accept","scenarioId":"primary","title":"String validation enforces a narrower domain format"}],"filePatterns":[],"id":"no-redundant-variable-validation","key":"iac:no-redundant-variable-validation","languages":["iac"],"limitations":["Safe fixes remove only complete standalone validation blocks containing exactly the proven redundant condition and a literal error message, without comments or additional content.","Only exact, syntax-parsed tautologies over the validation block's own variable are reported.","Provider constraints, compound expressions containing a real domain predicate, dynamic types, heterogeneous tuples, and validations of another variable are intentionally not inferred.","Generated files, fixtures, testdata, and non-.tf inputs are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Terraform applies declared type and nullability constraints before custom validation. Rechecking readability, conversion to the declared type, non-nullability, or an exhaustive non-nullable boolean domain cannot reject another valid input and obscures actual domain requirements.","references":[],"remediation":"Delete the redundant validation block. Keep validation for narrower domain requirements such as ranges, formats, enums, otherwise permitted nulls, and relationships between fields.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_redundant_variable_validation.py","status":"active","summary":"Flag Terraform variable validations already guaranteed by the declared type or nullability.","test":"packages/iac/tests/rules/test_no_redundant_variable_validation.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ207","defaultLevel":"warning","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  # Set instance type\n  instance_type = \"standard\"\n}\n"}],"fixedFiles":[{"path":"main.tf","source":"locals {\n\n  instance_type = \"standard\"\n}\n"}],"focusPath":"main.tf","id":"attribute-restatement","outcome":"reject","scenarioId":"primary","title":"Comment merely narrates an assignment"},{"expectedCount":0,"files":[{"path":"generated.tf","source":"/* Code generated by schema compiler.\n * DO NOT EDIT.\n */\nlocals {\n  # Set instance type\n  instance_type = \"standard\"\n}\n"}],"fixedFiles":[],"focusPath":"generated.tf","id":"generated-block-header","outcome":"accept","scenarioId":"generated-file","title":"Generated HCL is owned by its generator"},{"expectedCount":0,"files":[{"path":"main.tf","source":"locals {\n  # Service accounts\n  api_service_account_email = \"api@example.test\"\n  worker_service_account_email = \"worker@example.test\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"group-heading","outcome":"accept","scenarioId":"sibling-group","title":"Comment labels a group of sibling attributes"},{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  /* Set instance type */\n  instance_type = \"standard\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"maintained-block-comment-restatement","outcome":"reject","scenarioId":"generated-file","title":"Maintained block comment repeats its attribute"},{"expectedCount":0,"files":[{"path":"main.tf","source":"locals {\n  # Keep this type because the provider rejects ARM nodes.\n  instance_type = \"standard\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"provider-constraint","outcome":"accept","scenarioId":"primary","title":"Comment records a provider constraint"},{"expectedCount":1,"files":[{"path":"main.tf","source":"locals {\n  # Service account\n  service_account_email = \"api@example.test\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"single-declaration-label","outcome":"reject","scenarioId":"sibling-group","title":"Comment repeats one declaration label"}],"filePatterns":[],"id":"no-restated-comment","key":"iac:no-restated-comment","languages":["iac"],"limitations":["Safe fixes remove only the diagnosed complete standalone comment group, preserve line boundaries and require identical executable HCL tokens.","Only short comments immediately above native HCL declarations at the same indentation are compared.","Comment groups, sibling headings, heredocs, directives, generated files, fixtures, references, units, modality, and causal explanations are preserved."],"messageIds":[],"optionsSchema":null,"rationale":"A declaration-label comment adds no durable information and can drift; comments that explain constraints, grouping, or operational rationale remain useful.","references":[],"remediation":"Delete the restatement or replace it with the constraint the configuration cannot express. Improve an author-controlled label when the declaration itself is unclear.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_restated_comment.py","status":"active","summary":"Flag a short comment attached to an HCL declaration when it only repeats that declaration's kind, label, or attribute name.","test":"packages/iac/tests/rules/test_no_restated_comment.py"},{"aliases":[],"autofix":"safe","category":"style","code":"SARJ216","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"# Keep this bucket private.\nresource \"example_bucket\" \"private\" {}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"commented-intent","outcome":"reject","scenarioId":"primary","title":"A Terraform comment duplicates executable intent"},{"expectedCount":1,"files":[{"path":"main.tf","source":"locals { value = 1/**/+2 }\n"}],"fixedFiles":[{"path":"main.tf","source":"locals { value = 1    +2 }\n"}],"focusPath":"main.tf","id":"empty-inline-comment","outcome":"reject","scenarioId":"empty-inline-comment","title":"Keep operators separated when removing an empty block comment"},{"expectedCount":0,"files":[{"path":"main.tf","source":"locals { value = 1    +2 }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"empty-inline-comment-fixed","outcome":"accept","scenarioId":"empty-inline-comment","title":"Whitespace preserves the original Terraform operators"},{"expectedCount":1,"files":[{"path":"main.tf","source":"#\nlocals { value = 1 }\n"}],"fixedFiles":[{"path":"main.tf","source":" \nlocals { value = 1 }\n"}],"focusPath":"main.tf","id":"empty-line-comment","outcome":"reject","scenarioId":"empty-line-comment","title":"Remove an empty Terraform line comment"},{"expectedCount":0,"files":[{"path":"main.tf","source":" \nlocals { value = 1 }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"empty-line-comment-fixed","outcome":"accept","scenarioId":"empty-line-comment","title":"The executable Terraform remains unchanged"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"example_bucket\" \"private\" {\n  description = \"Private application storage\"\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"named-intent","outcome":"accept","scenarioId":"primary","title":"A resource name and description express intent"}],"filePatterns":[],"id":"no-terraform-comments","key":"iac:no-terraform-comments","languages":["iac"],"limitations":["Core consumers may opt into [iac.no-terraform-comments] module_summaries = true. This permits one plain # summary of at most 80 characters on the first line of main.tf, followed by a blank line. Suppression and generated-file markers are excluded. Terraform validation still owns syntax; no Terraform values or outputs are added.","At error level, takes precedence over the narrower comment-cruft and restated-comment rules when selected together for .tf source. Warning-stage adoption preserves existing errors.","Applies to .tf source, including real comments in template interpolations; literal strings, heredoc script text and non-.tf inputs are not comments.","One finding is emitted per comment token, including block comments and tool directives. This rule cannot suppress itself with another comment.","This is an explicit team style policy, not a claim that explanatory comments are a correctness defect. Conditional safe fixes replace empty ordinary line comments and empty block comments on one physical line with spaces, preserving executable tokens and LF/CRLF boundaries. Nonempty comments, directives, template comments, multiline block comments, generated and malformed sources remain manual."],"messageIds":[],"optionsSchema":null,"rationale":"Names and descriptions carry executable intent. An optional short entrypoint summary exposes module purpose without a separate catalog.","references":[],"remediation":"Use clear names and native descriptions. Opt into module_summaries for one short main.tf header; keep other context in the owning declaration or change review.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_terraform_comments.py","status":"active","summary":"Express Terraform intent through names and descriptions, with optional concise module summaries.","test":"packages/iac/tests/rules/test_no_terraform_comments.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ210","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"terraform_data\" \"guard\" {\n  lifecycle {\n    precondition {\n      condition = true\n      error_message = \"Always passes.\"\n    }\n  }\n}\n"}],"fixedFiles":[{"path":"main.tf","source":"resource \"terraform_data\" \"guard\" {\n  lifecycle {\n\n\n\n\n  }\n}\n"}],"focusPath":"main.tf","id":"literal-true-guard","outcome":"reject","scenarioId":"literal-true-guard","title":"A literal true assertion adds no constraint"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"terraform_data\" \"guard\" {\n  lifecycle {}\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"no-inert-guard","outcome":"accept","scenarioId":"literal-true-guard","title":"Retain the resource without an inert assertion"},{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"terraform_data\" \"deployment_guard\" {\n  lifecycle {\n    precondition {\n      condition     = var.enabled\n      error_message = \"deployment must be enabled\"\n    }\n  }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"terraform-data-plan-guard","outcome":"reject","scenarioId":"primary","title":"Do not model deployment policy as a fake resource"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"terraform_data\" \"bootstrap\" {\n  triggers_replace = [var.release]\n  provisioner \"local-exec\" {\n    command = \"bootstrap-hosts\"\n  }\n  lifecycle {\n    precondition {\n      condition = var.release != \"\"\n      error_message = \"A release is required for provisioning.\"\n    }\n  }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"terraform-data-provisioning-condition","outcome":"accept","scenarioId":"primary","title":"Keep a lifecycle condition protecting a provisioning operation"}],"filePatterns":[],"id":"no-terraform-data-condition","key":"iac:no-terraform-data-condition","languages":["iac"],"limitations":["Safe fixes remove only standalone literal-true conditions with literal messages and no comments or extra content, preserving the resource address and all other lifecycle behavior. Real assertions require ownership review.","Only lifecycle precondition and postcondition blocks directly inside terraform_data resources are checked.","Resources with a direct file, local-exec, or remote-exec provisioner are excluded; their conditions may protect provisioning operations.","Ordinary terraform_data replacement triggers, variable validation, check blocks, and lifecycle conditions on provider resources remain allowed.","Only .tf files are read; Terraform test files and other HCL dialects are outside this rule."],"messageIds":[],"optionsSchema":null,"rationale":"A terraform_data resource created only to reject a plan turns configuration policy into stateful graph noise. The assertion drifts with today's configuration while obscuring the input, resource, test, or deployment boundary that owns the invariant.","references":[],"remediation":"Delete the terraform_data guard. Put an input invariant on its variable, a provider invariant on the owning resource, and environment or rollout policy in tests and deployment review.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_terraform_data_condition.py","status":"active","summary":"Disallow lifecycle conditions attached to terraform_data guard resources.","test":"packages/iac/tests/rules/test_no_terraform_data_condition.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ222","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":1,"files":[{"path":"main.tf","source":"check \"ready\" {\n  assert {\n    condition = var.ready\n    error_message = \"Require readiness.\"\n  }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"check-block","outcome":"reject","scenarioId":"deployment-check","title":"Keep deployment assertions outside infrastructure declarations"},{"expectedCount":1,"files":[{"path":"main.tf","source":"data \"example_image\" \"selected\" {\n  lifecycle {\n    postcondition {\n      condition = self.approved\n      error_message = \"Require an approved image.\"\n    }\n  }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"data-postcondition","outcome":"reject","scenarioId":"data-lifecycle","title":"Move data-source postconditions to their owning policy"},{"expectedCount":0,"files":[{"path":"main.tf","source":"data \"example_image\" \"selected\" { name = var.image }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"data-selection","outcome":"accept","scenarioId":"data-lifecycle","title":"Read the selected image without an embedded policy block"},{"expectedCount":0,"files":[{"path":"main.tf","source":"variable \"replicas\" { type = number }\nresource \"example_service\" \"app\" {\n  count = var.enabled ? 1 : 0\n  replicas = var.replicas\n  deletion_protection = true\n  lifecycle { prevent_destroy = true }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"ordinary-declaration","outcome":"accept","scenarioId":"primary","title":"Keep typed declarations and native lifecycle protection"},{"expectedCount":0,"files":[{"path":"main.tf","source":"output \"ready\" { value = var.ready }\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"ordinary-output","outcome":"accept","scenarioId":"deployment-check","title":"Declare an output without a deployment assertion"},{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"example_service\" \"app\" {\n  lifecycle {\n    precondition {\n      condition = var.ready\n      error_message = \"Require readiness.\"\n    }\n  }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"resource-precondition","outcome":"reject","scenarioId":"resource-lifecycle","title":"Review resource preconditions with their existing enforcement owner"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"example_service\" \"app\" {\n  lifecycle { prevent_destroy = true }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"resource-protection","outcome":"accept","scenarioId":"resource-lifecycle","title":"Keep native resource lifecycle protection"},{"expectedCount":1,"files":[{"path":"main.tf","source":"variable \"replicas\" {\n  type = number\n  validation {\n    condition = var.replicas > 0\n    error_message = \"Require a positive replica count.\"\n  }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"variable-validation","outcome":"reject","scenarioId":"primary","title":"Move custom input policy out of variable declarations"}],"filePatterns":[],"id":"no-terraform-guard-blocks","key":"iac:no-terraform-guard-blocks","languages":["iac"],"limitations":["Reviewed immutable existing-guard contracts may preserve exact path, owner, kind, condition and error identities through Core. Unlisted or changed guards retain ERROR; contracts are not baseline debt.","Reports variable validation, resource/data/ephemeral lifecycle preconditions and postconditions, output preconditions, and top-level check blocks in .tf files.","Ordinary conditional expressions, count, for_each, provider condition blocks, prevent_destroy and deletion protection are allowed.","Comments, strings, heredoc payloads, provider blocks with similar names and Terraform test assertions are not guard blocks.","Terraform JSON and other HCL dialects are outside this detector. The shared parser can recover guard prefixes in malformed HCL; native Terraform validation remains necessary.","A check block receives one diagnostic even when it contains multiple assertions or nested data sources."],"messageIds":[],"optionsSchema":null,"rationale":"The team keeps infrastructure declarations concise and reuses native provider controls and existing shared policy. Guard blocks can add a second policy layer to those declarations. This is an explicit design policy, not a claim that Terraform conditions are invalid or always redundant.","references":[],"remediation":"First identify the existing native or shared owner of the invariant. Do not replace a guard with a bespoke verifier, source-text test, repeated API audit, or deployment wrapper merely to satisfy this rule. Keep investigation and rollout readbacks one-off unless they close a demonstrated recurring enforcement gap. Preserve existing deployment safety invariants. Use reviewed immutable guard contracts when removing a guard would change deployment behavior. For other guards, move an invariant only after proving equivalent enforcement phase and behavior. Preserve Terraform types, provider validation, deletion protection and lifecycle settings. Review the behavior before removing a guard; no automatic deletion is provided.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/no_terraform_guard_blocks.py","status":"active","summary":"Ban Terraform validation, precondition, postcondition, and check blocks.","test":"packages/iac/tests/rules/test_no_terraform_guard_blocks.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ201","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":0,"files":[{"path":"database.tf","source":"resource \"google_sql_database\" \"app\" {\n  name            = \"app\"\n  instance        = google_sql_database_instance.main.name\n  deletion_policy = \"PREVENT\"\n}\n"}],"fixedFiles":[],"focusPath":"database.tf","id":"protected-child-database","outcome":"accept","scenarioId":"primary","title":"Cloud SQL child database uses its provider guard"},{"expectedCount":1,"files":[{"path":"database.tf","source":"resource \"google_sql_database\" \"app\" {\n  name     = \"app\"\n  instance = google_sql_database_instance.main.name\n}\n"}],"fixedFiles":[],"focusPath":"database.tf","id":"unguarded-child-database","outcome":"reject","scenarioId":"primary","title":"Cloud SQL child database uses its deletable default"}],"filePatterns":[],"id":"require-deletion-protection","key":"iac:require-deletion-protection","languages":["iac"],"limitations":["Only curated resource-specific guard spellings with stable provider semantics are recognized.","Provider versions can change defaults; unresolved expressions are advisory because protection cannot be proven within one file.","A lifecycle prevent_destroy guard is accepted as Terraform plan-time protection but does not survive removal of the resource block."],"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":"Use the exact provider guard supported by that resource. Where none exists, consider lifecycle { prevent_destroy = true }, which no longer protects the resource after its block is removed.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/require_deletion_protection.py","status":"active","summary":"Require a curated stateful Terraform resource to declare a proven provider-native deletion guard or literal lifecycle destroy guard.","test":"packages/iac/tests/rules/test_require_deletion_protection.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ203","defaultLevel":"error","engine":"iac","examples":[{"expectedCount":0,"files":[{"path":"storage.tf","source":"resource \"google_storage_bucket\" \"records\" {\n  name            = \"records\"\n  force_destroy   = true\n  deletion_policy = \"PREVENT\"\n}\n"}],"fixedFiles":[],"focusPath":"storage.tf","id":"destructive-provider-guarded-bucket","outcome":"accept","scenarioId":"destructive-setting","title":"Provider policy still blocks a force-enabled deletion"},{"expectedCount":1,"files":[{"path":"storage.tf","source":"resource \"google_storage_bucket\" \"records\" {\n  name          = \"records\"\n  force_destroy = true\n}\n"}],"fixedFiles":[],"focusPath":"storage.tf","id":"destructive-unguarded-bucket","outcome":"reject","scenarioId":"destructive-setting","title":"Force deletion is not a disposable-resource declaration"},{"expectedCount":0,"files":[{"path":"storage.tf","source":"resource \"google_storage_bucket\" \"records\" {\n  name = \"records\"\n  deletion_policy = \"PREVENT\"\n}\n"}],"fixedFiles":[],"focusPath":"storage.tf","id":"protected-bucket","outcome":"accept","scenarioId":"primary","title":"Durable-data bucket protected by provider policy"},{"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":"Durable-data 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.","Provider controls are version-sensitive; unsupported or dynamic guard expressions do not prove safety.","Dynamic guards, ABANDON retention policies, and unresolved destructive options retain WARNING; missing or literal-disabled protection enforces the explicit protect-or-explain policy.","Terraform lifecycle.prevent_destroy is plan-time protection and does not survive removal of the resource block."],"messageIds":[],"optionsSchema":null,"rationale":"Buckets, secrets, and registries may hold durable state; unguarded deletion needs review when no authoritative recovery source exists.","references":[],"remediation":"Prefer a supported provider guard. Otherwise consider lifecycle { prevent_destroy = true }, which stops planned deletion only while the resource block remains, or suppress SARJ203 with a disposable rationale.","since":null,"source":"packages/iac/src/sarj_iac_lint/rules/require_prevent_destroy.py","status":"active","summary":"Require a curated durable-data container to declare a literal provider-side deletion guard or Terraform lifecycle destroy guard.","test":"packages/iac/tests/rules/test_require_prevent_destroy.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/report.ts","source":"/* eslint-disable no-console -- the CLI prints its report */\nconsole.log(report);\n/* eslint-enable no-console */\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"closed-block","outcome":"accept","ruleOptions":[{"allowWholeFile":false}],"scenarioId":"primary","title":"Close the block right after the excused code"},{"expectedCount":1,"files":[{"path":"src/report.ts","source":"/* eslint-disable no-console -- the CLI prints its report */\nconsole.log(report);\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"unclosed-block","outcome":"reject","ruleOptions":[{"allowWholeFile":false}],"scenarioId":"primary","title":"A block disable that runs to the end of the file"}],"filePatterns":[],"id":"directive-disable-enable-pair","key":"oxlint:directive-disable-enable-pair","languages":["typescript"],"limitations":["An `oxlint-enable` directive closes an `eslint-disable` directive and the other way round, as Oxlint applies them."],"messageIds":["missingPair","missingRulePair"],"optionsSchema":{"additionalProperties":false,"properties":{"allowWholeFile":{"description":"Allow an unpaired disable directive before the file's first token.","type":"boolean"}},"type":"object"},"rationale":"A block disable without an enable directive switches the rule off for the rest of the file, including code written long after the exception was reviewed.","references":["https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/disable-enable-pair.html"],"remediation":"Add an `eslint-enable` directive for the same rules right after the code the exception covers.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/directive-disable-enable-pair.ts","status":"active","summary":"Require an `eslint-enable` directive for every block `eslint-disable` or `oxlint-disable` directive.","test":"packages/typescript/tests/rules/directive-disable-enable-pair.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/report.ts","source":"/* eslint-disable no-console -- the CLI prints its report */\n/* eslint-disable no-debugger -- a breakpoint for the demo */\nconsole.log(report);\ndebugger;\n/* eslint-enable no-debugger */\n/* eslint-enable no-console */\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"one-enable-each","outcome":"accept","scenarioId":"primary","title":"One enable directive for each disable directive"},{"expectedCount":1,"files":[{"path":"src/report.ts","source":"/* eslint-disable no-console -- the CLI prints its report */\n/* eslint-disable no-debugger -- a breakpoint for the demo */\nconsole.log(report);\ndebugger;\n/* eslint-enable */\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"shared-enable","outcome":"reject","scenarioId":"primary","title":"One enable directive closes two disable directives"}],"filePatterns":[],"id":"directive-no-aggregating-enable","key":"oxlint:directive-no-aggregating-enable","languages":["typescript"],"limitations":[],"messageIds":["aggregatingEnable"],"optionsSchema":null,"rationale":"When one enable directive closes several disable directives, removing one exception means editing a shared comment, and it is easy to re-enable a rule early or leave one off.","references":["https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-aggregating-enable.html"],"remediation":"Write one enable directive for each disable directive, naming the same rules.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/directive-no-aggregating-enable.ts","status":"active","summary":"Disallow one `eslint-enable` or `oxlint-enable` directive closing several disable directives.","test":"packages/typescript/tests/rules/directive-no-aggregating-enable.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/report.ts","source":"/* eslint-disable no-console -- the CLI prints its report */\nconsole.log(report); // eslint-disable-line no-console -- the CLI prints its report\n/* eslint-enable no-console */\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"duplicate-line-disable","outcome":"reject","scenarioId":"primary","title":"A line disable inside a block that already disables the rule"},{"expectedCount":0,"files":[{"path":"src/report.ts","source":"/* eslint-disable no-console -- the CLI prints its report */\nconsole.log(report);\n/* eslint-enable no-console */\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"single-disable","outcome":"accept","scenarioId":"primary","title":"One directive per suppressed rule"}],"filePatterns":[],"id":"directive-no-duplicate-disable","key":"oxlint:directive-no-duplicate-disable","languages":["typescript"],"limitations":[],"messageIds":["duplicate","duplicateRule"],"optionsSchema":null,"rationale":"A second disable directive for a rule that is already off changes nothing, and it stays behind as a stale exception when the first one is removed.","references":["https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-duplicate-disable.html"],"remediation":"Delete the duplicate directive, or the rule name it repeats.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/directive-no-duplicate-disable.ts","status":"active","summary":"Disallow `eslint-disable` and `oxlint-disable` directives for rules that are already disabled there.","test":"packages/typescript/tests/rules/directive-no-duplicate-disable.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/report.ts","source":"// eslint-disable-next-line no-console -- the CLI prints its report\nconsole.log(report);\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"restricted-rule","outcome":"reject","ruleOptions":["no-console"],"scenarioId":"primary","title":"Disabling a restricted rule"},{"expectedCount":0,"files":[{"path":"src/report.ts","source":"// eslint-disable-next-line no-debugger -- a breakpoint for the demo\ndebugger;\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"unrestricted-rule","outcome":"accept","ruleOptions":["no-console"],"scenarioId":"primary","title":"Disabling a rule that is not restricted"}],"filePatterns":[],"id":"directive-no-restricted-disable","key":"oxlint:directive-no-restricted-disable","languages":["typescript"],"limitations":["Options are gitignore-style patterns over rule IDs: `foo/*` matches every rule of plugin `foo`, a pattern without a slash also matches that rule name under any plugin, and `!` re-allows.","A directive that names no rule is reported once, with the configured patterns as the rule name."],"messageIds":["disallow"],"optionsSchema":{"items":{"type":"string"},"type":"array","uniqueItems":true},"rationale":"Some rules guard policy that a local exception must not waive, such as the suppression rules themselves. A directive that disables every rule also disables those.","references":["https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-restricted-disable.html"],"remediation":"Fix the reported code instead, or name only rules that may be disabled.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/directive-no-restricted-disable.ts","status":"active","summary":"Disallow `eslint-disable` and `oxlint-disable` directives that turn off a restricted rule or every rule.","test":"packages/typescript/tests/rules/directive-no-restricted-disable.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/report.ts","source":"/* eslint-disable no-console -- the CLI prints its report */\nconsole.log(report);\n/* eslint-enable no-console */\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"paired-enable","outcome":"accept","scenarioId":"primary","title":"An enable directive that closes a disable directive"},{"expectedCount":1,"files":[{"path":"src/report.ts","source":"console.log(report);\n/* eslint-enable no-console */\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"stray-enable","outcome":"reject","scenarioId":"primary","title":"An enable directive with no disable directive before it"}],"filePatterns":[],"id":"directive-no-unused-enable","key":"oxlint:directive-no-unused-enable","languages":["typescript"],"limitations":[],"messageIds":["unused","unusedRule"],"optionsSchema":null,"rationale":"An enable directive with nothing to close is left over from an exception that is gone, and it misleads readers into looking for one.","references":["https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-unused-enable.html"],"remediation":"Delete the enable directive, or the rule name it does not need.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/directive-no-unused-enable.ts","status":"active","summary":"Disallow `eslint-enable` and `oxlint-enable` directives for rules that are not disabled.","test":"packages/typescript/tests/rules/directive-no-unused-enable.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/report.ts","source":"/* eslint-disable no-console -- the CLI prints its report */\nconsole.log(report);\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"block-disable","outcome":"reject","ruleOptions":[{"allow":["eslint-disable-line","eslint-disable-next-line","eslint-env","exported","global","globals"]}],"scenarioId":"primary","title":"A block disable hides every later line"},{"expectedCount":0,"files":[{"path":"src/report.ts","source":"// eslint-disable-next-line no-console -- the CLI prints its report\nconsole.log(report);\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"next-line","outcome":"accept","ruleOptions":[{"allow":["eslint-disable-line","eslint-disable-next-line","eslint-env","exported","global","globals"]}],"scenarioId":"primary","title":"Disable one line instead"}],"filePatterns":[],"id":"directive-no-use","key":"oxlint:directive-no-use","languages":["typescript"],"limitations":["An `oxlint-*` directive follows the `allow` entry of its `eslint-*` kind; @sarj/no-oxlint-directive reports the spelling.","Oxlint honors line comments such as `// eslint-disable`, so this rule reports them although ESLint ignores them."],"messageIds":["disallow"],"optionsSchema":{"additionalProperties":false,"properties":{"allow":{"description":"Directive kinds to allow, such as `eslint-disable-next-line`; an `oxlint-*` directive follows its `eslint-*` kind.","items":{"type":"string"},"type":"array","uniqueItems":true}},"type":"object"},"rationale":"Block disables and inline rule configuration hide diagnostics for whole regions of a file. Allowing only line-scoped directives keeps each exception next to the code it excuses.","references":["https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/no-use.html","https://oxc.rs/docs/guide/usage/linter/ignore-comments"],"remediation":"Replace the directive with one of the allowed kinds, usually an `eslint-disable-next-line` comment that names the rule and the reason.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/directive-no-use.ts","status":"active","summary":"Disallow directive comments, `eslint-*` or `oxlint-*`, except the allowed kinds.","test":"packages/typescript/tests/rules/directive-no-use.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/report.ts","source":"// eslint-disable-next-line no-console -- the CLI prints its report\nconsole.log(report);\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"described-next-line","outcome":"accept","ruleOptions":[{"ignore":[]}],"scenarioId":"primary","title":"Name the reason after `--`"},{"expectedCount":1,"files":[{"path":"src/report.ts","source":"// eslint-disable-next-line no-console\nconsole.log(report);\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"undescribed-next-line","outcome":"reject","ruleOptions":[{"ignore":[]}],"scenarioId":"primary","title":"A disable directive without a reason"}],"filePatterns":[],"id":"directive-require-description","key":"oxlint:directive-require-description","languages":["typescript"],"limitations":["A description is any text after ` -- `; @sarj/no-vague-suppression-description judges its content.","An `oxlint-*` directive follows the `ignore` entry of its `eslint-*` kind."],"messageIds":["missingDescription"],"optionsSchema":{"additionalProperties":false,"properties":{"ignore":{"description":"Directive kinds that need no description; an `oxlint-*` directive follows its `eslint-*` kind.","items":{"enum":["eslint","eslint-disable","eslint-disable-line","eslint-disable-next-line","eslint-enable","eslint-env","exported","global","globals"]},"type":"array","uniqueItems":true}},"type":"object"},"rationale":"A directive comment changes what the linter checks. Without a reason next to it, a reviewer cannot tell whether the exception still holds or can be removed.","references":["https://eslint-community.github.io/eslint-plugin-eslint-comments/rules/require-description.html","https://oxc.rs/docs/guide/usage/linter/ignore-comments"],"remediation":"Append ` -- <reason>` to the directive comment, naming why the exception is safe here.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/directive-require-description.ts","status":"active","summary":"Require a `--` description on every directive comment, `eslint-*` or `oxlint-*`.","test":"packages/typescript/tests/rules/directive-require-description.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:duplicate-test-body","languages":["typescript"],"limitations":["The rule compares substantial sibling tests within one suite and skips inline snapshots and materially different comments. Short no-substitution strings and const-asserted literals share the existing input normalization; assertion values, property names, multiline strings and long strings retain their contracts.","Matching normalized body shapes do not prove runtime equivalence or independent setup; parameterization is a manual review, not an automatic deletion."],"messageIds":["duplicateTestBody"],"optionsSchema":null,"rationale":"Copy-pasted test bodies hide the cases that differ and allow equivalent assertions to drift independently.","references":[],"remediation":"Consider a case table with one named test or subtest per case; preserve setup lifetime, test modifiers, and each case's assertions rather than deleting coverage.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:enforce-file-structure","languages":["typescript"],"limitations":["The rule skips tests and generated files, treats re-exports as neutral, and does not order body declarations. Only misplaced import type declarations in comment-free files are automatically moved; runtime imports and directive activation remain manual."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/adapter.ts","source":"/**\n * This file is the seam between the draft and backend models.\n * Everything above it uses the local application representation.\n * Everything below it uses a separately mirrored wire representation.\n * Field names use the API shape while the draft uses another shape.\n * This module translates every field between those representations.\n * The backend models changed several times during early development.\n * Each historical change required another edit in this adapter file.\n * The write half previously assembled several resources in one payload.\n * It now saves those resources separately through their own routes.\n * Clear contract types and generated clients should express this boundary.\n */\nimport type { Draft } from './draft';"}],"fixedFiles":[],"focusPath":"src/adapter.ts","id":"adapter-narration","outcome":"reject","scenarioId":"primary","title":"Adapter paragraph narrates application behavior"},{"expectedCount":0,"files":[{"path":"src/adapter.ts","source":"// Legacy clients send `execution_phase` until API-812 is retired.\n// Keep conversion in GeneratedClientAdapter.\nexport const phase = raw.execution_phase;"}],"fixedFiles":[],"focusPath":"src/adapter.ts","id":"protocol-constraint","outcome":"accept","scenarioId":"primary","title":"Concrete wire compatibility constraint remains local"}],"filePatterns":[],"id":"excessive-commentary","key":"oxlint:excessive-commentary","languages":["typescript"],"limitations":["Only an unattached file-header JSDoc block with at least ten non-empty lines and 65 words that narrates file layout or change history is inspected.","Generated files, tests, scripts, stories, licenses, typed API documentation, and comments attached to declarations are excluded.","Safe fixes blank only interior plus-only decoration when removing those non-prose lines clears the existing volume finding. All prose bytes, comment markers and line endings remain; directives, licenses, examples, literal-output/prompt/credential contexts, Markdown structure and ambiguous comments remain for review."],"messageIds":["excessive"],"optionsSchema":null,"rationale":"Narrative implementation paragraphs compete with the code and can drift independently from behavior.","references":[],"remediation":"Delete narration and clarify names, types, or structure; retain only durable constraints and external contracts.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/excessive-commentary.ts","status":"active","summary":"Flag long standalone implementation commentary that should be expressed by code.","test":"packages/typescript/tests/rules/excessive-commentary.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:iac-source-coupled-test","languages":["typescript"],"limitations":["The rule follows stable lexical bindings and static source paths, including named node:path/path join and resolve imports with a complete trailing static path. Reassigned bindings, dynamic trailing paths, namespace or unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.","Known fixture, golden, snapshot, and generated source paths remain unreported after dot-segment normalization, as do contained static paths beneath stable native temporary directories and literal mutation nonidentity guards comparing the same read.","When raw representation is genuinely the contract, use an exact line suppression explaining that contract."],"messageIds":["rawSourceOracle"],"optionsSchema":null,"rationale":"Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration while clients fail silently.","references":[],"remediation":"Reuse adequate existing validation; otherwise assert a distinct rendered-plan, provider-state, or runtime contract. Preserve deliberate representation contracts with an exact justified suppression.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"architecture","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/store.ts","source":"interface Store { load(): void } class DiskStore implements Store { load() { this.#read(); } #read() {} }"}],"fixedFiles":[],"focusPath":"src/store.ts","id":"exact-interface-surface","outcome":"accept","scenarioId":"primary","title":"Keep helpers behind the runtime boundary"},{"expectedCount":1,"files":[{"path":"src/store.ts","source":"interface Store { load(): void } class DiskStore implements Store { load() { this.read(); } read() {} }"}],"fixedFiles":[],"focusPath":"src/store.ts","id":"extra-public-method","outcome":"reject","scenarioId":"primary","title":"Do not grow an undeclared public surface"}],"filePatterns":[],"id":"interface-contract-members-private","key":"oxlint:interface-contract-members-private","languages":["typescript"],"limitations":["Automatic conversion is limited to unused private module/block classes without heritage, decorators, computed members, static execution, or eval; referenced classes remain manual.","Only concrete classes with an explicit `implements` clause are checked; constructors, static members, protected extension hooks, and overrides are excluded.","Inherited interface members are resolved by TypeScript. Computed names are excluded because their contract identity is not stable syntax.","The rule abstains for the whole class when TypeScript cannot resolve any implemented contract, avoiding false positives for missing or unavailable package declarations.","Referenced classes remain report-only because a public member can have consumers in another source file; the developer must choose whether to extend the interface or privatize it.","TypeScript-private members are left to prefer-ecmascript-private-members so one concern produces one diagnostic."],"messageIds":["nonContractMemberMustBePrivate"],"optionsSchema":null,"rationale":"An implementing class should expose exactly its declared interface while keeping implementation helpers runtime-private.","references":[],"remediation":"Add the method to the interface when it is public API, or make it `#private` and remove external or inherited access.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/interface-contract-members-private.ts","status":"active","summary":"Require methods outside an implemented interface contract to use ECMAScript private names.","test":"packages/typescript/tests/rules/interface-contract-members-private.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/record.ts","source":"import { z } from \"zod\"; const KeySchema: z.ZodEnum<{ a: \"a\"; b: \"b\" }> = z.enum([\"a\", \"b\"]); export const RecordSchema: z.ZodRecord<z.ZodEnum<{ a: \"a\"; b: \"b\" }>, z.ZodString> = z.record(KeySchema, z.string());"}],"fixedFiles":[],"focusPath":"src/record.ts","id":"copied-child","outcome":"reject","scenarioId":"primary","title":"Do not repeat the complete child annotation"},{"expectedCount":0,"files":[{"path":"src/record.ts","source":"import { z } from \"zod\"; const KeySchema: z.ZodEnum<{ a: \"a\"; b: \"b\" }> = z.enum([\"a\", \"b\"]); export const RecordSchema: z.ZodRecord<typeof KeySchema, z.ZodString> = z.record(KeySchema, z.string());"}],"fixedFiles":[],"focusPath":"src/record.ts","id":"named-child","outcome":"accept","scenarioId":"primary","title":"Reference the named child contract"}],"filePatterns":[],"id":"named-child-schema-type","key":"oxlint:named-child-schema-type","languages":["typescript"],"limitations":["Only direct module-level const z.record calls with two arguments, a ZodRecord annotation, and an earlier local const child with a complete explicit Zod type are inspected.","The entire generic child annotation must match token for token; input/output, defaults, transforms, readonly modifiers and brands are never compared by output shape alone.","Bare leaf types, broad ZodType constraints, type queries, imported or unqualified type references, annotation cycles, comments on the repeated argument, mutable bindings and generated files are excluded.","Import aliases and namespace imports resolve through scope bindings. Independent public contract pins can use one shared named type alias or an exact reasoned suppression; runtime validation and outer annotations are preserved."],"messageIds":["namedChildType"],"optionsSchema":null,"rationale":"Repeating a child schema's generic contract creates a second maintained declaration even though the record already uses that child at runtime.","references":[],"remediation":"Replace only the matching record type argument with `typeof ChildSchema`, or share one explicit named schema type alias between child and record to retain an independently named public contract. Keep the outer annotation required by isolatedDeclarations and every runtime schema operation.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/named-child-schema-type.ts","status":"active","summary":"Reference a named Zod record child's complete schema type instead of repeating its explicit annotation.","test":"packages/typescript/tests/rules/named-child-schema-type.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","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"},{"expectedCount":1,"files":[{"path":"src/codec.test.ts","source":"test.each(['valid', 'invalid'])('decodes %s', (input) => { try { decode(input); } catch { return; } expect(result()).toBe('ok'); });"}],"fixedFiles":[],"focusPath":"src/codec.test.ts","id":"table-bare-return","outcome":"reject","scenarioId":"table-catch","title":"Case tables must preserve failures"},{"expectedCount":1,"files":[{"path":"src/codec.test.ts","source":"import {test, expect} from 'vitest'; test.for(['optional'])('decodes %s', (input, context) => { try { decode(input); } catch { context.skip(false); return; } expect(result()).toBe('ok'); });"}],"fixedFiles":[],"focusPath":"src/codec.test.ts","id":"table-context-false-skip","outcome":"reject","scenarioId":"context-skip","title":"A false skip condition does not preserve the failure"},{"expectedCount":0,"files":[{"path":"src/codec.test.ts","source":"import {test, expect} from 'vitest'; test.for(['optional'])('decodes %s', (input, context) => { try { decode(input); } catch { context.skip('codec unavailable'); return; } expect(result()).toBe('ok'); });"}],"fixedFiles":[],"focusPath":"src/codec.test.ts","id":"table-context-skip","outcome":"accept","scenarioId":"context-skip","title":"A Vitest context skip preserves an optional table case"},{"expectedCount":0,"files":[{"path":"src/codec.test.ts","source":"test.each(['valid', 'invalid'])('decodes %s', (input) => { try { decode(input); } catch (error) { throw error; } expect(result()).toBe('ok'); });"}],"fixedFiles":[],"focusPath":"src/codec.test.ts","id":"table-rethrow","outcome":"accept","scenarioId":"table-catch","title":"Each table case propagates its failure"}],"filePatterns":["**/*.test.*","**/*.spec.*","**/tests/**","**/__tests__/**"],"id":"no-bare-return-from-test-catch","key":"oxlint:no-bare-return-from-test-catch","languages":["typescript"],"limitations":["Only bare returns owned by an inline supported test callback and followed lexically by a framework assertion are reported, including each/for tables and runIf/skipIf builders. Custom builders and indirect callback references are excluded. A runner skip suppresses the finding only when it is an unconditional earlier statement in the return's block. Vitest imports and unshadowed runner globals support TestContext.skip on the first ordinary-test parameter or second for-table parameter, including a directly destructured skip alias; each-table data arguments, reassigned bindings, overwritten skip methods and dynamic skip conditions are excluded."],"messageIds":["bareReturnFromTestCatch"],"optionsSchema":null,"rationale":"An unasserted catch return can swallow a failure and skip later assertions; the complete test result also depends on other assertions and hooks.","references":[],"remediation":"Rethrow the error, assert on it, or use the runner's explicit skip mechanism when the capability is optional.","requiresTypeInformation":null,"runtimeImports":[],"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":"architecture","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/user-adapter.ts","source":"import type { User } from './api-contract';\nexport const toUser = (raw: User) => ({ displayName: raw.display_name });"}],"fixedFiles":[],"focusPath":"src/user-adapter.ts","id":"bespoke-wire-case-conversion","outcome":"reject","scenarioId":"primary","title":"Do not reproduce SDK case conversion in an application adapter"},{"expectedCount":0,"files":[{"path":"src/user-adapter.ts","source":"import type { User } from './generated-client';\nexport const toUser = (raw: User) => ({ displayName: raw.displayName });"}],"fixedFiles":[],"focusPath":"src/user-adapter.ts","id":"generated-client-surface","outcome":"accept","scenarioId":"primary","title":"Use the generated client's application-facing property names"}],"filePatterns":[],"id":"no-bespoke-api-case-conversion","key":"oxlint:no-bespoke-api-case-conversion","languages":["typescript"],"limitations":["Only adapter/adapters files and receivers explicitly annotated with a scope-resolved type imported from an API/client/SDK/contract/generated module are checked. Unrelated imports, local type shadows, reassigned receivers and inferred receiver types are excluded.","Only object properties that directly translate the same identifier between snake_case and lowerCamelCase are reported.","Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and indirect conversions are intentionally excluded."],"messageIds":["noBespokeApiCaseConversion"],"optionsSchema":null,"rationale":"Duplicating wire-name translation can drift from an API client contract. When the SDK owns application-facing names, centralizing conversion avoids maintaining another mirror by hand.","references":[],"remediation":"Move wire-name ownership and case conversion into the generated SDK/model layer; keep application adapters on the generated typed surface.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-bespoke-api-case-conversion.ts","status":"active","summary":"Review direct snake_case/camelCase mirror mappings on explicitly API-typed adapter values.","test":"packages/typescript/tests/rules/no-bespoke-api-case-conversion.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/view.tsx","source":"<input type=\"password\" />"}],"fixedFiles":[],"focusPath":"src/view.tsx","id":"native-paste","outcome":"accept","scenarioId":"primary","title":"Allow native clipboard input"},{"expectedCount":1,"files":[{"path":"src/view.tsx","source":"<input type=\"password\" onPaste={event => event.preventDefault()} />"}],"fixedFiles":[],"focusPath":"src/view.tsx","id":"pure-paste-blocker","outcome":"reject","scenarioId":"primary","title":"Review unconditional paste cancellation"}],"filePatterns":[],"id":"no-blocked-input-paste","key":"oxlint:no-blocked-input-paste","languages":["typescript"],"limitations":["An unresolved alternate paste phase or spread may supply replacement handling and suppresses the warning.","Only intrinsic editable input and textarea handlers whose complete inline body is a direct preventDefault() call are reported.","Clipboard processing, conditional cancellation, referenced handlers, rich editors, non-text inputs, custom components, optional calls and uncertain effective props remain outside the detector."],"messageIds":["blockedPaste"],"optionsSchema":null,"rationale":"An editable text control that only cancels paste prevents users from entering generated passwords, authentication codes and copied data. This creates avoidable accessibility and password-manager barriers.","references":["https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/forms/forms.md","https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/forms/autofill-sign-in-form.md","https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/forms/sms-otp-form.md"],"remediation":"Allow native paste. If the product must normalize clipboard content, provide explicit replacement processing and preserve keyboard and password-manager access; review the behavior instead of applying an automatic edit.","requiresTypeInformation":false,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-blocked-input-paste.ts","status":"active","summary":"Allow clipboard input when a text control has no replacement paste handling.","test":"packages/typescript/tests/rules/no-blocked-input-paste.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/label.ts","source":"function label(enabled: boolean) { if (enabled) { return \"Enabled\"; } else { return \"Disabled\"; } }"}],"fixedFiles":[],"focusPath":"src/label.ts","id":"boolean-value-mapping","outcome":"accept","scenarioId":"primary","title":"Boolean data may select a pure value"},{"expectedCount":1,"files":[{"path":"src/book.ts","source":"const book = (premium: boolean) => premium ? premiumBook() : regularBook();"}],"fixedFiles":[],"focusPath":"src/book.ts","id":"concise-boolean-dispatch","outcome":"reject","scenarioId":"primary","title":"Name the selected operation explicitly"}],"filePatterns":[],"id":"no-boolean-selector-parameter","key":"oxlint:no-boolean-selector-parameter","languages":["typescript"],"limitations":["Requires type information. Checks an owned single-statement if/else, returned conditional or conditional expression statement, and expression-bodied arrow conditionals. Only a direct broad-boolean parameter or its negation is considered; optional, nullable, literal-only, generic, destructured and indirect predicate conditions are excluded.","Preserves setters, pure mappings, selected pure data passed to one stable lexical operation or assigned to one simple target, independently authored callable and inherited member contracts, inline callbacks, and statically proven named callback references. Independent helpers remain checked in JSX files; callback identity is established from references rather than handler names.","The block-body detector preserves the existing literal on[A-Z] object-property exemption; the concise-arrow extension does not infer a callback contract from that name. Multi-statement bodies, short-circuit guards, local parameter aliases and arbitrary control flow are not inferred. Noncomputed property reads count as data without proving getter effects; computed reads remain outside that exemption, including literal keys. Side-effect detection does not prove whether a called function is pure, and a warning requires manual design review. Generated files are excluded; tests remain checked.","Automatic fixes only reuse two already authored private zero-argument ordinary operations returning primitive string literals. A complete comment-free four-declaration module must contain those operations, one private const broad-boolean arrow selecting their direct calls, and one exported zero-argument ordinary function with an explicit string-array return that calls the selector exclusively with literal true and false. Calls are routed to the existing named operations and the exclusively used selector declaration is removed. All other APIs, callbacks, dynamic arguments, contracts and operation bodies remain manual."],"messageIds":["booleanSelector"],"optionsSchema":null,"rationale":"A positional boolean hides which operation a caller requests. Splitting independent actions can make that intent explicit, while boolean data, pure mappings and externally required callback or method contracts remain valid.","references":["https://understandlegacycode.com/blog/what-is-wrong-with-boolean-parameters/","https://martinfowler.com/bliki/FlagArgument.html"],"remediation":"Consider separate named operations or an explicit domain mode. Review the public API and its callers manually; preserve required callback, setter and inherited contracts instead of wrapping them solely to silence the warning.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-boolean-selector-parameter.ts","status":"active","summary":"Prefer intention-revealing operations over a boolean parameter selecting between actions.","test":"packages/typescript/tests/rules/no-boolean-selector-parameter.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/example.ts","source":"type Invoice = { reference: string; total: number };\nfunction toRequest(invoice: Invoice): Invoice { return invoice; }"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"after","outcome":"accept","scenarioId":"primary","title":"Use the direct contract"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"type Invoice = { reference: string; total: number };\nfunction toRequest(invoice: Invoice): Record<string, unknown> { return invoice; }"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"before","outcome":"reject","scenarioId":"primary","title":"Preserve the explicit contract"}],"filePatterns":[],"id":"no-broad-return-type","key":"oxlint:no-broad-return-type","languages":["typescript"],"limitations":["Requires TypeScript type information and an implemented function with an explicit return annotation. Checks unknown, object, and unknown-valued index-only dictionaries, including aliases and Promise results.","Unparsed unknown/any values, generics, empty object literals, and declaration-only signatures are excluded. Parameter contracts are not inferred from names. Intentional opaque return boundaries require an explained local suppression. Automatic fixes remove the annotation only from an unused private function declaration in a module or block; referenced and public contracts remain manual."],"messageIds":["avoid"],"optionsSchema":null,"rationale":"An unknown, object, or unknown-valued dictionary return hides fields that the implementation already knows.","references":[],"remediation":"Return the existing domain type or retain the inferred return contract. Keep unknown at unparsed input boundaries.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-broad-return-type.ts","status":"active","summary":"Report explicit broad return annotations that erase a known return value.","test":"packages/typescript/tests/rules/no-broad-return-type.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"app/users/page.tsx","source":"import { loadUsers } from './users-client';\nexport default async function Users() { const users = await loadUsers(); return <pre>{JSON.stringify(users)}</pre>; }"},{"path":"app/users/users-client.ts","source":"export async function loadUsers(): Promise<unknown> { const response = await fetch('https://api.example.com/users'); return response.json(); }"}],"fixedFiles":[],"focusPath":"app/users/page.tsx","id":"effect-without-fetch","outcome":"accept","scenarioId":"primary","title":"Load data while rendering on the server"},{"expectedCount":1,"files":[{"path":"src/users.tsx","source":"'use client'; import { useEffect } from 'react';\nexport function Users() { useEffect(() => { fetch('/api/users'); }, []); return null; }"}],"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":"oxlint: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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-comment-cruft","languages":["typescript"],"limitations":["The rule skips generated files and conservatively preserves prose, issue references, licenses, examples, and tool directives. Automatic fixes remove only whole-line punctuation-only banners while preserving line endings; narrated prose, unfinished work and contextual labels remain manual."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"style","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/example.ts","source":"declare const includePage: boolean;\nconst query: { limit: number; page?: number } = { limit: 20 };\nif (includePage) query.page = 1;"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"after","outcome":"accept","scenarioId":"primary","title":"Use the direct contract"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"declare const includePage: boolean;\nconst query = { limit: 20, ...(includePage ? { page: 1 } : {}) };"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"before","outcome":"reject","scenarioId":"primary","title":"Preserve the explicit contract"}],"filePatterns":[],"id":"no-conditional-empty-object-spread","key":"oxlint:no-conditional-empty-object-spread","languages":["typescript"],"limitations":["Reports ternary expressions with an empty object branch directly spread into an object literal. Does not report array or argument spreads, logical spreads, or two nonempty alternatives.","This is an explicit readability policy. Comment-free conditionals with exactly one nonempty object-literal branch are automatically rewritten as logical spreads, preserving omission, evaluation order and property ownership. Other expression branches require manual review."],"messageIds":["avoid"],"optionsSchema":null,"rationale":"An empty-object conditional spread hides whether a property is omitted, which differs from assigning undefined.","references":[],"remediation":"Construct the object explicitly and add the conditional fields only when the original condition holds, preserving evaluation order and ownership.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-conditional-empty-object-spread.ts","status":"active","summary":"Build conditional properties explicitly instead of spreading an empty-object branch.","test":"packages/typescript/tests/rules/no-conditional-empty-object-spread.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/load.ts","source":"function load(kind: \"text\"): {kind: \"text\"};\nfunction load(kind: \"count\"): {kind: \"count\"};\nfunction load(kind: \"text\" | \"count\"): {kind: \"text\" | \"count\"} { return kind === \"text\" ? {kind: \"text\"} : {kind: \"count\"}; }"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"correct-case-results","outcome":"accept","scenarioId":"primary","title":"Match each literal input with its result"},{"expectedCount":2,"files":[{"path":"src/load.ts","source":"function load(kind: \"text\"): {kind: \"text\"};\nfunction load(kind: \"count\"): {kind: \"count\"};\nfunction load(kind: \"text\" | \"count\"): {kind: \"text\" | \"count\"} { return kind === \"text\" ? {kind: \"count\"} : {kind: \"text\"}; }"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"inverted-case-results","outcome":"reject","scenarioId":"primary","title":"Do not swap domain variants behind overloads"}],"filePatterns":[],"id":"no-contradictory-overload-return","key":"oxlint:no-contradictory-overload-return","languages":["typescript"],"limitations":["Automatic fixes align only an unused private complete same-file overload group with its existing literal implementation results. Binding aliases, exports, merges, eval, declaration files, comments, nonliteral result syntax and observed callers preserve manual ownership; runtime checks and returned values never change.","Requires type information and a top-level non-generic, synchronous function with two to 32 unique, single-required-parameter overloads. Each public input must be a directly authored string, finite number, boolean or null literal. Broad, duplicate, aliased, optional, rest and multi-parameter inputs are excluded.","The implementation must have one required identifier parameter and only return a strict literal equality/inequality conditional. Both branches must be primitive literals or flat objects containing only primitive literal property assignments. Calls, computed keys, getters, spreads, duplicate keys and __proto__ are excluded.","Reports only a concrete returned literal incompatible with its primitive contract or an explicit returned property incompatible with the corresponding primitive property contract. Missing properties, arbitrary structural incompatibility, brands, enums, opaque types, effects and general control flow are outside this proof.","Compiler-invalid implementation input/output envelopes, generated files, ambient declarations, methods and callbacks are excluded. Null input overloads require strictNullChecks: under loose null checking an earlier overload can mask their advertised result. No automatic rewrite changes a public overload or its runtime behavior."],"messageIds":["contradictoryReturn"],"optionsSchema":null,"rationale":"TypeScript checks an overload implementation against an overall envelope, without proving each input/result relationship. A function can compile while returning the opposite domain variant for every advertised literal input.","references":["https://www.typescriptlang.org/docs/handbook/2/functions.html#overload-signatures-and-the-implementation-signature"],"remediation":"Return the variant promised for each input. Prefer a checked case map or separate variant-specific operations when that makes the relationship explicit; retain overloads required by public APIs.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-contradictory-overload-return.ts","status":"active","summary":"Preserve the input/result relationship promised by literal overloads.","test":"packages/typescript/tests/rules/no-contradictory-overload-return.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/guard.ts","source":"function texts(value: unknown): value is string[] { return Array.isArray(value); }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"array-elements-unchecked","outcome":"reject","scenarioId":"array-elements","title":"Array membership alone does not prove text elements"},{"expectedCount":0,"files":[{"path":"src/guard.ts","source":"function texts(value: unknown): value is string[] { return Array.isArray(value) && value.every((item: unknown) => typeof item === \"string\"); }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"array-elements-validated","outcome":"accept","scenarioId":"array-elements","title":"Establish the element domain at the boundary"},{"expectedCount":0,"files":[{"path":"src/guard.ts","source":"function ready(value: \"ready\" | null): value is \"ready\" { return typeof value === \"string\"; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"closed-literal-check","outcome":"accept","scenarioId":"closed-literal-domain","title":"A closed input supplies the literal evidence"},{"expectedCount":0,"files":[{"path":"src/guard.ts","source":"function nonPrimitive(value: unknown): value is object { return (typeof value === \"object\" && value !== null) || typeof value === \"function\"; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"complete-object-check","outcome":"accept","scenarioId":"intrinsic-null","title":"Exclude null and recognize non-primitive functions"},{"expectedCount":0,"files":[{"path":"src/guard.ts","source":"function isText(value: unknown): value is string { return typeof value === \"string\"; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"consistent-primitive-predicate","outcome":"accept","scenarioId":"primary","title":"Agree with the checked primitive"},{"expectedCount":1,"files":[{"path":"src/guard.ts","source":"function isText(value: unknown): value is string { return typeof value === \"number\"; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"contradictory-primitive-predicate","outcome":"reject","scenarioId":"primary","title":"Do not promise a different primitive"},{"expectedCount":1,"files":[{"path":"src/guard.ts","source":"function impossible(value: unknown): value is never { return typeof value === \"number\"; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"empty-domain-check-succeeds","outcome":"reject","scenarioId":"empty-domain","title":"Numbers cannot belong to an empty target"},{"expectedCount":0,"files":[{"path":"src/guard.ts","source":"function impossible(value: unknown): value is never { return false; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"empty-domain-never-succeeds","outcome":"accept","scenarioId":"empty-domain","title":"An empty target has no successful member"},{"expectedCount":1,"files":[{"path":"src/guard.ts","source":"function nonPrimitive(value: unknown): value is object { return typeof value === \"object\"; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"object-check-admits-null","outcome":"reject","scenarioId":"intrinsic-null","title":"A typeof-object check also accepts null"},{"expectedCount":1,"files":[{"path":"src/guard.ts","source":"function ready(value: unknown): value is \"ready\" { return typeof value === \"string\"; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"overclaimed-literal","outcome":"reject","scenarioId":"closed-literal-domain","title":"A string check admits other strings"},{"expectedCount":1,"files":[{"path":"src/guard.ts","source":"type Account = { id: string }; function account(value: unknown): value is Account { return typeof value === \"object\" && value !== null; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"record-fields-unchecked","outcome":"reject","scenarioId":"record-fields","title":"Object membership does not establish required fields"},{"expectedCount":0,"files":[{"path":"src/guard.ts","source":"type Account = { id: string }; function account(value: unknown): value is Account { return typeof value === \"object\" && value !== null && \"id\" in value && typeof value.id === \"string\"; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"record-fields-validated","outcome":"accept","scenarioId":"record-fields","title":"Establish fields before promising the record"}],"filePatterns":[],"id":"no-contradictory-type-predicate","key":"oxlint:no-contradictory-type-predicate","languages":["typescript"],"limitations":["Automatic fixes replace the unsound predicate with boolean only for an unused private function declaration without overloads, exports, eval, comments in the predicate, or declaration-file ownership. Runtime checks are preserved; observed callable contracts remain manual.","Requires type information, an explicit predicate over a directly named parameter and a single returned positive typeof equality for the same scope-resolved parameter. Both strict and loose equality and reversed operands are supported.","Recognizes broad primitive targets, finite primitive literal/nullish unions and never, with unknown or fully classified primitive/nullish inputs. Closed input domains are checked against the actual target, including finite numeric enum members. Mixed broad-and-literal, branded, unique-symbol, structural, generic, asserted, declaration-only and multi-statement validators are excluded rather than guessed.","For the intrinsic object target, typeof-object also admits null only when the declared input admits null and strictNullChecks is enabled. This narrow check does not infer structural property validation or classify other object/function relationships. Overloaded implementations, contextually typed callbacks and methods with class heritage or contextual object contracts retain their separate callable domain.","Compiler-invalid predicate target/input relations and typeof comparisons impossible for the input remain with existing compiler and unnecessary-condition controls. This rule does not validate arbitrary predicates or runtime refinements. Generated files are excluded; tests remain checked.","A sole native Array.isArray call on unknown cannot establish a plain primitive-element array target. Standard-library call signatures and an unshadowed Array binding are required. Element validators, any/unknown elements, tuples, enums, branded or opaque elements and independent callable contracts abstain. Visible Array or host-global aliases, mutation, escapes, extracted methods, any value constructor or unresolved computed-property access and unrelated Array namespace uses disable this proof: structural receiver types cannot establish runtime constructor identity. Native Reflect value uses and Object prototype/descriptor reflection or namespace aliases also withdraw authority. Indirect opaque runtime replacements are not inferred.","For a same-file nongeneric named record with a required data property, a sole typeof-object check, optionally conjoined with a null exclusion, cannot establish that property from unknown or object input. Optional-only records, dictionaries, classes, imported/generic contracts and implementations with field checks are excluded. A non-null object without the required own property supplies the contradiction. The same unused-private predicate proof may remove its unobserved narrowing contract; observed callable contracts and sole named-type uses remain manual."],"messageIds":["closedLiteralPredicate","contradictoryPredicate","nullAccepted","uncheckedArrayElements","uncheckedRecordFields"],"optionsSchema":null,"rationale":"TypeScript trusts an explicit type predicate without verifying its implementation. A contradictory check can admit the wrong primitive, overclaim a closed literal or empty domain, accept null as a non-null object, or remove valid alternatives from the false branch.","references":["https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html#inferred-type-predicates"],"remediation":"Match the predicate to every value the check accepts and rejects. Check literal membership for a closed domain and exclude null when promising a non-null object. Use a boolean return for an intentional value filter; preserve required public and callback contracts.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-contradictory-type-predicate.ts","status":"active","summary":"Keep explicit type predicates consistent with their primitive, array or record membership check.","test":"packages/typescript/tests/rules/no-contradictory-type-predicate.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/server.ts","source":"import cors from 'cors'; cors({ origin: 'https://app.example.com', credentials: true });"}],"fixedFiles":[],"focusPath":"src/server.ts","id":"trusted-origin-with-credentials","outcome":"accept","scenarioId":"primary","title":"Use a trusted origin for credentialed callers"},{"expectedCount":1,"files":[{"path":"src/server.ts","source":"import cors from 'cors'; cors({ origin: '*', credentials: true });"}],"fixedFiles":[],"focusPath":"src/server.ts","id":"wildcard-origin-with-credentials","outcome":"reject","scenarioId":"primary","title":"Review a wildcard that blocks access to credentialed responses"}],"filePatterns":[],"id":"no-cors-wildcard-with-credentials","key":"oxlint:no-cors-wildcard-with-credentials","languages":["typescript"],"limitations":["Options require a stable official cors default import or hono/cors named or namespace import, one literal object argument, literal origin '*', and credentials true. No-substitution templates and erased TypeScript literal wrappers preserve these values. Custom bindings, callbacks, arrays, conditional origins and dynamic options are excluded.","Literal header maps use effective case-sensitive JavaScript property writes followed by native Headers combination of differently-cased header names. Unknown values/spreads/computed keys, accessors and non-null prototypes lose proof; literals used only inside object spreads are not reported independently.","Manual set/setHeader/delete/removeHeader calls (including static computed method names) use direct identifier receivers and literal arguments in straight-line program or function bodies. Control-flow bodies, member receivers, aliases and unknown operations are excluded. Append requires a proven empty native Headers constructor or a preceding known write/removal.","The rule reviews a local configuration contract, not actual request credentials or a data disclosure. Runtime patches, custom header method implementations, interprocedural mutation and later consumers are not resolved. No autofix."],"messageIds":["corsWildcardWithCredentials"],"optionsSchema":null,"rationale":"Browsers do not expose credentialed cross-origin responses when Access-Control-Allow-Origin is '*'. Credentialless requests can still use a wildcard even when Access-Control-Allow-Credentials is emitted, so review the intended caller contract.","references":["https://fetch.spec.whatwg.org/#cors-protocol-and-credentials"],"remediation":"Use a trusted explicit origin for credentialed callers, or omit the credentials opt-in for a credentialless wildcard endpoint.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-cors-wildcard-with-credentials.ts","status":"active","summary":"Review wildcard CORS origins combined with an explicit credentials opt-in.","test":"packages/typescript/tests/rules/no-cors-wildcard-with-credentials.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/theme.ts","source":"localStorage.setItem(\"sidebar-collapsed\", \"true\");\n"}],"fixedFiles":[],"focusPath":"src/theme.ts","id":"server-cookie","outcome":"accept","scenarioId":"primary","title":"Store a non-credential preference in localStorage"},{"expectedCount":1,"files":[{"path":"src/session.ts","source":"export function remember(token: string): void {\n  localStorage.setItem(\"accessToken\", token);\n}\n"}],"fixedFiles":[],"focusPath":"src/session.ts","id":"token-in-local-storage","outcome":"reject","scenarioId":"primary","title":"Do not store an access token in localStorage"}],"filePatterns":[],"id":"no-credential-in-browser-storage","key":"oxlint:no-credential-in-browser-storage","languages":["typescript"],"limitations":["Only static keys and cookie names whose words name an access credential (token, secret, password, jwt, credential, api key) are checked.","Storage wrappers, computed keys and values stored under neutral names are not followed."],"messageIds":["noCredentialInBrowserStorage"],"optionsSchema":null,"rationale":"localStorage, sessionStorage and cookies written through document.cookie are readable by any script on the origin, so one injected script can copy the credential and replay it elsewhere.","references":[],"remediation":"Keep the credential on the server and send it as an httpOnly, Secure, SameSite cookie set by a server response, or hold a short-lived token in memory only.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-credential-in-browser-storage.ts","status":"active","summary":"Do not write credential-named values to browser storage or script-readable cookies.","test":"packages/typescript/tests/rules/no-credential-in-browser-storage.test.ts"},{"aliases":[],"autofix":"safe","category":"security","code":null,"defaultLevel":"error","engine":"oxlint","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":"SVG opt-in requires explicit hardening under this policy"},{"expectedCount":0,"files":[{"path":"next.config.mjs","source":"export default { images: { dangerouslyAllowSVG: true, contentDispositionType: \"attachment\", contentSecurityPolicy: \"script-src 'none'; sandbox;\" } };\n"}],"fixedFiles":[],"focusPath":"next.config.mjs","id":"svg-explicit-hardening","outcome":"accept","scenarioId":"explicit-hardening","title":"Allow SVG delivery with explicit hardening"},{"expectedCount":1,"files":[{"path":"next.config.mjs","source":"export default { images: { dangerouslyAllowSVG: true, contentDispositionType: \"attachment\", contentSecurityPolicy: \"script-src 'unsafe-inline'; script-src 'none'; sandbox;\" } };\n"}],"fixedFiles":[],"focusPath":"next.config.mjs","id":"svg-unsafe-first-directive","outcome":"reject","scenarioId":"explicit-hardening","title":"A later duplicate cannot replace the effective policy"}],"filePatterns":[],"id":"no-dangerously-allow-svg","key":"oxlint:no-dangerously-allow-svg","languages":["typescript"],"limitations":["Automatic boolean changes are withheld when the module contains explicit TypeScript or JSDoc contracts; those contracts can constrain an aliased property to literal true. Plain objects and as const inference remain fixable.","This rule does not resolve the installed Next version or evaluate effective response headers. The inspected Next 16.4.3 defaults already block scripts, apply sandbox, and use attachment disposition. A report for omitted explicit CSP is a policy finding, not proof of XSS.","Omitted contentDispositionType is treated as attachment. Older or customized runtime defaults are not established by this source-only check.","Only a bounded literal CSP subset is recognized: the first script-src must contain only 'none', and the first sandbox must have no tokens. Directive names and separators use CSP ASCII syntax. Valid policy lists, default-src fallbacks and other equivalent policies are not proved; omitted, dynamic or unrecognized explicit policies can still report. This is not a general CSP validator or proof of runtime security.","Automatic fixes disable SVG opt-in to satisfy this policy; recognized hardened configurations are preserved. Only literal effective properties of a directly exported object, unescaped const alias, or isolated module.exports object are analyzed. Wrappers, factories, spreads, dynamic computed keys and mutations are not inferred."],"messageIds":["noDangerouslyAllowSvg"],"optionsSchema":null,"rationale":"SVG can contain scripts and other active content. This organizational rule requires a recognized explicit script-blocking sandbox CSP when SVG delivery is enabled, with attachment disposition. It does not establish that framework defaults are missing or that a reported configuration is exploitable.","references":["https://www.w3.org/TR/2026/WD-CSP3-20261027/#parse-serialized-policy","https://github.com/vercel/next.js/blob/v16.4.3/packages/next/src/shared/lib/image-config.ts#L255"],"remediation":"Disable dangerouslyAllowSVG, or configure an explicit literal image contentSecurityPolicy such as `script-src 'none'; sandbox;` and keep attachment disposition. Verify effective headers and defaults against the application's pinned Next version.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-dangerously-allow-svg.ts","status":"active","summary":"Require an explicit hardening policy for Next Image SVG delivery","test":"packages/typescript/tests/rules/no-dangerously-allow-svg.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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', Finished = 'finished', 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":"oxlint:no-declaration-comment-wall","languages":["typescript"],"limitations":["Automatic fixes remove only an exact multiword member-name comment on its own line and retain all physical line endings; threshold-based or novel prose remains manual.","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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/client.ts","source":"class Client { readonly request = (input: RequestInfo | URL, init?: RequestInit) => fetch(input, init); }"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"forwarded-global-fetch","outcome":"accept","scenarioId":"primary","title":"Store a receiver-safe forwarding wrapper"},{"expectedCount":1,"files":[{"path":"src/client.ts","source":"class Client { readonly request = fetch; }"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"receiver-unsafe-global-fetch","outcome":"reject","scenarioId":"primary","title":"Do not store raw ambient fetch as an object method"}],"filePatterns":[],"id":"no-detached-global-fetch","key":"oxlint:no-detached-global-fetch","languages":["typescript"],"limitations":["The rule follows stable local variables and defaulted parameters from an unshadowed ambient fetch or globalThis/self/window fetch into direct class, object, or member storage.","Bare local aliases, callback arguments, returns, direct calls, compatible global or undefined bind/call/apply receivers, tests, scripts, generated files, and locally defined fetch implementations are excluded.","Interprocedural aliases, mutable aliases, collection storage, and host identity remain manual review boundaries.","Automatic edits only bind a direct canonical ambient fetch to unshadowed undefined in a sole exported zero-argument ordinary function containing a private one-property object and its sole returned literal data-URL request. The callable and object never escape. Web IDL operations use the function realm's global object for an undefined receiver. Imports, comments, wrappers, other statements, options, shadows and identity observations remain manual; this repairs receiver behavior rather than preserving a host IllegalInvocation failure."],"messageIds":["detachedGlobalFetch"],"optionsSchema":null,"rationale":"Calling a raw ambient fetch through a class or object property supplies that object as `this`; receiver-sensitive hosts can reject that invocation only after deployment.","references":["https://webidl.spec.whatwg.org/#es-operations","https://fetch.spec.whatwg.org/#fetch-method"],"remediation":"Store a forwarding wrapper such as `(input, init) => fetch(input, init)`, or bind the callable to a compatible global or undefined receiver before storing it.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-detached-global-fetch.ts","status":"active","summary":"Keep the ambient global fetch receiver-safe when storing or explicitly rebinding it.","test":"packages/typescript/tests/rules/no-detached-global-fetch.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"app.ts","source":"import { z } from 'zod'; z.string().trim().parse(raw);"}],"fixedFiles":[],"focusPath":"app.ts","id":"lost-transform","outcome":"reject","scenarioId":"primary","title":"The normalized output is discarded"},{"expectedCount":0,"files":[{"path":"app.ts","source":"import { z } from 'zod'; function parse(raw: unknown) { return z.string().trim().parse(raw); }"}],"fixedFiles":[],"focusPath":"app.ts","id":"used-transform","outcome":"accept","scenarioId":"primary","title":"Return the parsed value"}],"filePatterns":[],"id":"no-discarded-validation-result","key":"oxlint:no-discarded-validation-result","languages":["typescript"],"limitations":["Checks expression-statement parse/parseAsync/safeParse/safeParseAsync calls on import-proven Zod factories and immutable local schema aliases. Await wrappers are supported.","Throwing parse is reported only for source-visible output-changing schema operations. Plain validation-only parse, imported schemas, custom validators, mutable bindings, generated files and opaque effects are excluded. No automatic variable or control flow is invented."],"messageIds":["discarded"],"optionsSchema":null,"rationale":"Ignoring safeParse loses failure information; ignoring a transformed parse result leaves callers using values without the declared defaults, coercions or normalization.","references":[],"remediation":"Capture or return the parsed output. Branch on safeParse success and use data. Plain throwing validation without output changes remains supported.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-discarded-validation-result.ts","status":"active","summary":"Consume Zod validation outcomes and transformed outputs instead of discarding them.","test":"packages/typescript/tests/rules/no-discarded-validation-result.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint: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.","requiresTypeInformation":null,"runtimeImports":[],"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":"testing","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/format.test.ts","source":"import {it} from 'vitest'; it.each([[0, '0ms'], [1, '1ms']])('%s', (input, expected) => { expect(format(input)).toBe(expected); });"}],"fixedFiles":[],"focusPath":"src/format.test.ts","id":"distinct-boundary","outcome":"accept","scenarioId":"primary","title":"Distinct inputs have independent rows"},{"expectedCount":0,"files":[{"path":"src/format.test.ts","source":"import {test} from 'vitest'; test.for([{input: 0, expected: '0ms'}, {'input': 1, expected: '1ms'}])('$input', ({input, expected}) => { expect(format(input)).toBe(expected); });"}],"fixedFiles":[],"focusPath":"src/format.test.ts","id":"distinct-for-boundary","outcome":"accept","scenarioId":"for-table","title":"Vitest for tables preserve distinct boundaries"},{"expectedCount":1,"files":[{"path":"src/format.test.ts","source":"import {test} from 'vitest'; test.for([{input: 0, expected: '0ms'}, {'input': 0, expected: '0ms'}])('$input', ({input, expected}) => { expect(format(input)).toBe(expected); });"}],"fixedFiles":[],"focusPath":"src/format.test.ts","id":"duplicate-for-row","outcome":"reject","scenarioId":"for-table","title":"Vitest for tables retain the same case identity"},{"expectedCount":1,"files":[{"path":"src/format.test.ts","source":"import {it} from 'vitest'; it.each([[0, '0ms'], [0, '0ms']])('%s', (input, expected) => { expect(format(input)).toBe(expected); });"}],"fixedFiles":[],"focusPath":"src/format.test.ts","id":"duplicate-row","outcome":"reject","scenarioId":"primary","title":"A copied row misses the intended boundary"},{"expectedCount":0,"files":[{"path":"src/format.test.ts","source":"import {test as base} from 'vitest'; const test = base.extend({locale: 'en'}); test.for([1, 2])('formats %s', (value, {locale}) => { expect(format(value, locale)).toBe(String(value)); });"}],"fixedFiles":[],"focusPath":"src/format.test.ts","id":"fixture-distinct-row","outcome":"accept","scenarioId":"fixture-runner","title":"Fixture reuse preserves distinct independent cases"},{"expectedCount":1,"files":[{"path":"src/format.test.ts","source":"import {test as base} from 'vitest'; const test = base.extend({locale: 'en'}); test.for([1, 1])('formats %s', (value, {locale}) => { expect(format(value, locale)).toBe('1'); });"}],"fixedFiles":[],"focusPath":"src/format.test.ts","id":"fixture-duplicate-row","outcome":"reject","scenarioId":"fixture-runner","title":"Fixture reuse preserves the literal case check"}],"filePatterns":[],"id":"no-duplicate-test-case","key":"oxlint:no-duplicate-test-case","languages":["typescript"],"limitations":["Only literal rows in inline arrays or const tables used exclusively by each or Vitest for calls are compared. Calls, spreads, computed keys, getters, runtime values, tagged tables and mutable or escaped tables are excluded.","Object property order and all row fields, including names and IDs, remain part of case identity; equivalent quoted and unquoted property names share one identity. Runtime imports and unchanged local const Vitest test.extend fixture APIs are resolved; type-only imports, escaped or mutated fixture APIs, custom runners and namespace imports are excluded.","The rule reports duplicate values, not runtime equivalence. There is no autofix because repeated invocations may exercise shared state or concurrency."],"messageIds":["duplicateCase"],"optionsSchema":null,"rationale":"Identical rows repeat the same inputs and expected values without exposing a distinct case. Accidental duplicates consume test time and hide a missing boundary vector.","references":[],"remediation":"Remove the accidental duplicate or replace it with the intended distinct boundary. Preserve intentional repeated or stateful scenarios with a local explanation; do not delete their coverage automatically.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-duplicate-test-case.ts","status":"active","summary":"Report repeated literal rows in framework each tables and Vitest for tables.","test":"packages/typescript/tests/rules/no-duplicate-test-case.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"error","engine":"oxlint","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":0,"files":[{"path":"src/users.ts","source":"db.prepare('select id from users where id = ?').bind(userId);"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"bound-unquoted-value","outcome":"accept","scenarioId":"unquoted","title":"An unquoted 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"},{"expectedCount":1,"files":[{"path":"src/users.ts","source":"db.prepare(`select id from users where id = ${userId}`);"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"unquoted-runtime-fragment","outcome":"reject","scenarioId":"unquoted","title":"A runtime fragment is interpolated into SQL"}],"filePatterns":[],"id":"no-dynamic-sql","key":"oxlint:no-dynamic-sql","languages":["typescript"],"limitations":["Template and concatenation fragments are inspected at recognized execution calls, including runtime pieces surrounded by literal text inside a quoted value. Double-quoted identifiers, comments, and dollar strings are excluded; this is not a general SQL injection detector.","Literal fragments, legacy uppercase fragment names, and parameterizing tagged templates are exempt; uppercase spelling does not prove a value is static.","The bounded lexer recognizes doubled quotes, comments, and PostgreSQL dollar strings; dialect-specific escape modes and SQL generated through other APIs require separate security review."],"messageIds":["dynamicFragment","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 or fragments in SQL bypasses driver parameterization or an explicit allowlist and can introduce injection defects or unstable query plans.","references":[],"remediation":"Bind data values through SQL placeholders. Select non-bindable identifiers or clauses from fixed, reviewed fragments instead of interpolating runtime text.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-dynamic-sql.ts","status":"active","summary":"Disallow runtime values and fragments interpolated into SQL passed to statement-execution methods.","test":"packages/typescript/tests/rules/no-dynamic-sql.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/status.ts","source":"// ordinary header const marker=\"auto-generated file\"; enum Mode {Start} console.log(Mode.Start, marker);"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"authored-after-comment","outcome":"reject","scenarioId":"native-header-boundary","title":"Keep executable authored code in scope"},{"expectedCount":0,"files":[{"path":"src/status.ts","source":"// auto-generated file const marker=\"auto-generated file\"; enum Mode {Start} console.log(Mode.Start, marker);"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"generated-header","outcome":"accept","scenarioId":"native-header-boundary","title":"Respect a native generated banner"},{"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":"oxlint:no-enum","languages":["typescript"],"limitations":["This is an explicit style policy for regular and const enums, not a claim that every enum emits an object. Generated files and configured exclusions are preserved. Automatic conversion is limited to unused private ambient enums with literal string values; runtime enums and observed nominal contracts remain manual."],"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":"Literal unions keep type-only domains explicit, while constant objects make runtime values deliberate. This policy also avoids compiler-dependent const-enum inlining contracts.","references":["https://www.typescriptlang.org/docs/handbook/enums.html"],"remediation":"Use a literal union or an `as const` object after checking runtime member access, numeric reverse mappings, serialized values, and public consumers.","requiresTypeInformation":null,"runtimeImports":[],"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":"security","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/actions.ts","source":"\"use server\"; const errorMessage = (error: unknown) => error instanceof Error ? error.message : \"Unknown error\"; export async function save() { try { await persist(); } catch (error) { return { ok: false, message: errorMessage(error) }; } }"}],"fixedFiles":[],"focusPath":"src/actions.ts","id":"caught-message-in-server-action","outcome":"reject","scenarioId":"server-action","title":"Keep Server Action results independent of caught details"},{"expectedCount":1,"files":[{"path":"src/route.ts","source":"function handle() { try { run(); } catch (error) { return Response.json({ error: error instanceof Error ? error.message : 'Internal server error' }, { status: 500 }); } }"}],"fixedFiles":[],"focusPath":"src/route.ts","id":"caught-message-in-server-error","outcome":"reject","scenarioId":"primary","title":"Do not return a caught message as a server error"},{"expectedCount":0,"files":[{"path":"src/route.ts","source":"function handle() { try { run(); } catch (error) { return Response.json({ error: 'Internal server error' }, { status: 500 }); } }"}],"fixedFiles":[],"focusPath":"src/route.ts","id":"generic-server-error","outcome":"accept","scenarioId":"primary","title":"Keep internal failure details server-side"},{"expectedCount":0,"files":[{"path":"src/actions.ts","source":"\"use server\"; export async function save() { try { await persist(); } catch (error) { captureException(error); return { ok: false, message: \"Could not save\" }; } }"}],"fixedFiles":[],"focusPath":"src/actions.ts","id":"public-server-action-failure","outcome":"accept","scenarioId":"server-action","title":"Return a stable public Server Action failure"}],"filePatterns":[],"id":"no-exception-text-in-5xx-response","key":"oxlint:no-exception-text-in-5xx-response","languages":["typescript"],"limitations":["Only directly returned Response.json calls and named NextResponse imports from next/server, with an explicit literal integer 500–599 status, are inspected.","Exported async Server Actions proven by a module or function use-server directive also have their returned result values checked. This profile follows closed const aliases and one same-module, single-parameter, single-return pure helper with unknown/unannotated input and inferred or broad string output. Only catch bindings owned by the action are considered; nested callbacks, unknown helper effects, escaping aliases and direct eval cause abstention.","Simple literal branch/dead-statement checks exclude visible transfers and control-transferring finalizers. Returns nested in for-in/of bodies, including async iteration, switch returns and preceding opaque loops, labels, switches or try statements abstain. Visible Error constructor mutation/escape and function-constructor capabilities invalidate instanceof-based helper purity; external runtime monkeypatches remain outside this local proof.","Caught message/stack fields are followed through literal objects, arrays, interpolation and conditional expressions. Mutable or escaped catch values, shadowed factories and local factory/serializer overrides are excluded.","Client HTTP errors, dynamic status codes, response wrappers, custom sanitizer calls, spreads, uncertain properties or serialization hooks, tests and generated files are excluded. Response-factory aliases and interprocedural mutations or runtime monkeypatches are not resolved.","A warning requests review; syntax does not prove the exception contains a secret. There is no automatic replacement for an application's public error contract."],"messageIds":["exceptionText","serverActionExceptionText"],"optionsSchema":null,"rationale":"Exception details can contain database, provider, filesystem or credential information that belongs in server-side diagnostics rather than a client response.","references":["https://cheatsheetseries.owasp.org/cheatsheets/Error_Handling_Cheat_Sheet.html","https://nextjs.org/docs/app/api-reference/functions/next-response"],"remediation":"Return a generic public error and retain diagnostic details in server-side logs. Keep intentional public domain errors in their reviewed client-error contract.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-exception-text-in-5xx-response.ts","status":"active","summary":"Keep caught exception messages and stacks out of returned 5xx JSON responses and Server Action results.","test":"packages/typescript/tests/rules/no-exception-text-in-5xx-response.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"jsx.d.ts","source":"declare module \"react/jsx-runtime\" { export namespace JSX { interface Element {} interface IntrinsicElements {span: {children?: unknown}} } }"},{"path":"src/avatar.tsx","source":"export {}; let element: any; const opaque = {toString() {return element.props.email;}}; const name = opaque as unknown as string; const Avatar = ({displayName}: {displayName: string}) => <span>{`${displayName}`}</span>; element = <Avatar {...{displayName: name, email: 'needed'}}/>;"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"react-jsx\"}}"}],"fixedFiles":[],"focusPath":"src/avatar.tsx","id":"asserted-source-alias","outcome":"accept","scenarioId":"source-provenance","title":"An asserted alias can still observe forwarded data"},{"expectedCount":1,"files":[{"path":"jsx.d.ts","source":"declare module \"react/jsx-runtime\" { export namespace JSX { interface Element {} interface IntrinsicElements {span: {children?: unknown}} } }"},{"path":"src/avatar.tsx","source":"export {}; const Avatar = ({displayName}: {displayName: string}) => <span>{displayName}</span>; const element = <Avatar {...{displayName: 'Ada', email: 'unused'}}/>; const summary = {displayName: (element as {props: {displayName: string}}).props.displayName}; export {summary};"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"react-jsx\"}}"}],"fixedFiles":[],"focusPath":"src/avatar.tsx","id":"consumed-scalar-inspection","outcome":"reject","scenarioId":"created-value","title":"A private element exposes only its consumed scalar"},{"expectedCount":0,"files":[{"path":"jsx.d.ts","source":"declare module \"react/jsx-runtime\" { export namespace JSX { interface Element {} interface IntrinsicElements {span: {children?: unknown}} } }"},{"path":"src/avatar.tsx","source":"export {}; const target = {ref: 0}; const Avatar = ({displayName}: {displayName: string}) => <span>{displayName}</span>; (Avatar as unknown as {defaultProps: {email: string}}).defaultProps = {get email() {target.ref = 1; return 'fallback';}}; const element = <Avatar {...{displayName: 'Ada', email: 'needed'}}/>; const observation = target.ref; export {observation};"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"react-jsx\"}}"}],"fixedFiles":[],"focusPath":"src/avatar.tsx","id":"erased-callable-metadata","outcome":"accept","scenarioId":"callable-metadata","title":"Runtime metadata can observe an omitted field"},{"expectedCount":1,"files":[{"path":"jsx.d.ts","source":"declare module \"react/jsx-runtime\" { export namespace JSX { interface Element {} interface IntrinsicElements {span: {children?: unknown}} } }"},{"path":"src/avatar.tsx","source":"export {}; const Avatar = ({ displayName }: { displayName: string }) => <span>{displayName}</span>;\nconst avatar = <Avatar {...{ displayName: 'Ada', email: 'ada@example.test' }} />;"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"react-jsx\"}}"}],"fixedFiles":[],"focusPath":"src/avatar.tsx","id":"excess-runtime-field","outcome":"reject","scenarioId":"primary","title":"An unrelated field is forwarded at runtime"},{"expectedCount":0,"files":[{"path":"jsx.d.ts","source":"declare module \"react/jsx-runtime\" { export namespace JSX { interface Element {} interface IntrinsicElements {span: {children?: unknown}} } }"},{"path":"src/avatar.tsx","source":"export {}; const Avatar = ({ displayName }: { displayName: string }) => <span>{displayName}</span>;\nconst avatar = <Avatar displayName='Ada' />;"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"react-jsx\"}}"}],"fixedFiles":[],"focusPath":"src/avatar.tsx","id":"explicit-fields","outcome":"accept","scenarioId":"primary","title":"Forward only the declared field"},{"expectedCount":0,"files":[{"path":"jsx.d.ts","source":"declare module \"react/jsx-runtime\" { export namespace JSX { interface Element {} interface IntrinsicElements {span: {children?: unknown}} } }"},{"path":"src/avatar.tsx","source":"export {}; let element: any; const Avatar = ({displayName}: {displayName: string}) => <span>{displayName}</span>; element = <Avatar {...{displayName: 'Ada', email: 'needed'}}/>; Object.defineProperty(Object.prototype, 'ref', {configurable: true, get() {Object.defineProperty(this, 'children', {configurable: true, value: element.props.email}); return null;}});"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"react-jsx\"}}"}],"fixedFiles":[],"focusPath":"src/avatar.tsx","id":"inherited-runtime-hook","outcome":"accept","scenarioId":"prototype-effects","title":"An inherited runtime hook can observe forwarded data"},{"expectedCount":1,"files":[{"path":"jsx.d.ts","source":"declare module \"react/jsx-runtime\" { export namespace JSX { interface Element {} interface IntrinsicElements {span: {children?: unknown}} } }"},{"path":"src/avatar.tsx","source":"export {}; const name = 'Ada'; const Avatar = ({displayName}: {displayName: string}) => <span>{`${displayName}`}</span>; const element = <Avatar {...{displayName: name, email: 'unused'}}/>;"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"react-jsx\"}}"}],"fixedFiles":[],"focusPath":"src/avatar.tsx","id":"literal-source-alias","outcome":"reject","scenarioId":"source-provenance","title":"A proven primitive alias preserves the finding"},{"expectedCount":0,"files":[{"path":"jsx.d.ts","source":"declare module \"react/jsx-runtime\" { export namespace JSX { interface Element {} interface IntrinsicElements {span: {children?: unknown}} } }"},{"path":"src/avatar.tsx","source":"export {}; declare function inspect(value: unknown): void; const Avatar = ({displayName}: {displayName: string}) => <span>{displayName}</span>; const element = <Avatar {...{displayName: 'Ada', email: 'needed'}}/>; inspect(element);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"react-jsx\"}}"}],"fixedFiles":[],"focusPath":"src/avatar.tsx","id":"opaque-element-consumer","outcome":"accept","scenarioId":"created-value","title":"An opaque consumer can observe or mutate element props"},{"expectedCount":1,"files":[{"path":"jsx.d.ts","source":"declare module \"react/jsx-runtime\" { export namespace JSX { interface Element {} interface IntrinsicElements {span: {children?: unknown}} } }"},{"path":"src/avatar.tsx","source":"export {}; const settings = {defaultProps: {email: 'fallback'}}; settings.defaultProps = {email: 'updated'}; const Avatar = ({displayName}: {displayName: string}) => <span>{displayName}</span>; const element = <Avatar {...{displayName: 'Ada', email: 'unused'}}/>;"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"react-jsx\"}}"}],"fixedFiles":[],"focusPath":"src/avatar.tsx","id":"unrelated-object-metadata","outcome":"reject","scenarioId":"callable-metadata","title":"An unrelated data object's metadata preserves the finding"},{"expectedCount":1,"files":[{"path":"jsx.d.ts","source":"declare module \"react/jsx-runtime\" { export namespace JSX { interface Element {} interface IntrinsicElements {span: {children?: unknown}} } }"},{"path":"src/avatar.tsx","source":"export {}; const target = {ref: null}; target.ref = null; const Avatar = ({displayName}: {displayName: string}) => <span>{displayName}</span>; const element = <Avatar {...{displayName: 'Ada', email: 'unused'}}/>;"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"react-jsx\"}}"}],"fixedFiles":[],"focusPath":"src/avatar.tsx","id":"unrelated-runtime-object","outcome":"reject","scenarioId":"prototype-effects","title":"An unrelated own-property write preserves the finding"}],"filePatterns":[],"id":"no-excess-jsx-props","key":"oxlint:no-excess-jsx-props","languages":["typescript"],"limitations":["The created JSX value must initialize a private actual const binding. Only unused values or same-function-scope fixed reads of already proven consumed scalar props are supported. Enclosing invocations must have no runtime argument or arguments-object dependency, async/generator suspension, or unproved captured values. Unused simple arguments and captured scalar data reuse the existing provenance proof; destructured/default/rest arguments remain unsupported. Exported, returned, aliased, captured, aggregated or forwarded elements, whole-props access, opaque calls, type observations and writes abstain. A caller can mutate production React props after creation even when development React freezes them; initial source provenance alone is insufficient. This restriction intentionally excludes ordinary exported and rendered elements until their consumers can be proved.","The rule abstains when the receiving or calling project runtime dependency context contains a known ordinary Object-prototype effect or exposure, native eval/Function capabilities, any explicit call, constructor or tagged-template invocation, for-of or for-in iteration, an ambient or uninitialized value declaration or a declaration-file value read, an unresolved lexical value (including a shorthand or a type-only name used as a value), an erased const-enum value without a runtime binding proof, an opaque globalThis member, an implicit this/super/module-metadata capability, an extracted reflective method, or a member-write target not proven to be a fresh unrelated object/array literal. Callable and class metadata writes, including through erased const aliases, abstain: runtime metadata can observe omitted fields even when it is absent from the native JSX contract. Native shorthand value symbols preserve global capability identity instead of trusting a newly created property symbol. Property keys, control-flow labels, intrinsic JSX names and erased type positions do not evaluate lexical values; their missing symbols or Any types do not alone veto a finding. Native ESM imports/re-exports and literal dynamic imports are followed in both dependency and known-importer directions, including other dependencies of those importers and resolved bare path aliases; whole-clause import type/export type forms are skipped. Import-equals and other loader forms are unsupported. Per-specifier type imports/exports retain their runtime edges when native verbatimModuleSyntax emit keeps an empty import/export. Unresolved runtime imports and declaration-only or external-library runtime imports abstain. Literal dynamic imports retain their module-resolution proof. External code outside the analyzed project context and standard library implementations remain opaque; standard ordinary Object built-ins are assumed. Connected runtime dependency contexts beyond 128 files or const-alias proofs beyond 64 steps abstain. Safe reflective code may therefore be excluded. This finite known-effect exclusion does not prove universal prototype pristineness.","Requires parser-owned TypeScript information, a strict module implementation, a complete Object base, a self-closing identifier tag with one plain object-literal spread and no other attributes, and one nongeneric implemented synchronous arrow receiver with a resolved finite props surface. Sibling attributes or children could override the proven source values. Unknown, opaque, union, overloaded, generic, intrinsic and class receivers are excluded.","Mapped types, index signatures, namespace/value-derived contracts, unresolved inherited or computed receiver keys, nonliteral computed source keys, nested spreads, accessors and explicit __proto__ keys are excluded. Source type properties alone do not prove enumerable own runtime keys. key, ref, React's __self/__source development metadata and TypeScript's hyphenated JSX attribute names are preserved.","Native member lookup includes inherited, optional, readonly and implicit Object capabilities. The rule reports runtime keys outside the contract; it does not infer unused component fields, demand smaller public APIs, or erase producer type information.","Runtime alias references require active native checking of each reached local implementation, with no located syntax or semantic errors. Checking disabled by noCheck or native-shaped TypeScript suppression directives is unavailable evidence. JavaScript, declaration/default/external-library files, foreign SourceFile objects and project-reference programs are unsupported for this alias proof. Importer indexing does not request full semantic checks; reached files reuse the parser-owned Program's diagnostic caches. Unlocated/global diagnostics are not treated as a clean project or as proof of runtime binding presence. Namespace value reads, including merged and const-enum-only namespace carriers, abstain because a ValueModule symbol can survive erased runtime output without a native error. Pure type namespaces and nonambient namespace declaration names remain non-value positions; explicit ambient namespace declarations retain their existing exclusion. A non-import-alias value declared in another source also abstains: inclusion in the type Program does not establish that a script/global implementation executes or that an ambient binding is supplied as a data property. Native standard global identities retain their existing assumptions.","Requires a single returned view made from intrinsic JSX, literals and scalar parameter reads/operators. JSX objects may be rendered as children or returned branches, but cannot be operands of scalar coercions or intrinsic attribute values. Free runtime values, helper calls, callbacks, object-valued consumed fields, custom tags, rest/spread, reflection and dynamic access prevent a finding; indirect element.props aliases cannot be inferred safe. Every inspected or destructured root field must be an own source-literal key; an omitted field could invoke an inherited getter. Bound/read source values require concrete scalar types without undefined, Any or unknown and runtime primitive provenance: literals, operators/templates with proven primitive operands, proven conditional tests and branches, or local immutable const initializers after removing assertions. Direct own fields of a same-file const object literal are supported only when every object reference is a fixed field value in a direct object initializer, inline JSX spread, JSX expression or local binding, with no direct eval. Mutation, accessors, method receivers, dynamic keys, reflection, whole-object aliases and exports abstain. Opaque calls, getters, parameters, mutable bindings and imported source values abstain even when annotated as scalars. Unproven source paths beyond 64 expressions abstain as an analysis resource limit. Only successful value proofs are memoized. An alias or assertion cannot turn an object into a primitive; undefined can activate runtime defaults. Opaque HOC and declaration-file signatures remain unsupported.","The actual tag must resolve through immutable identifier aliases to a const arrow initializer. Constructible ordinary functions, mutable, member, proxy, call-produced and asserted receiver values are excluded even when their callable type resembles a known implementation; inherited runtime metadata can change ordinary function dispatch. Exactly one actual parameter and no explicit this parameter are supported.","Reached files containing actual JSX additionally require their compiler-owned implicit runtime import and the native module symbol's declaration owners. Known non-declaration runtime implementations are unsupported even when their bodies have no ordinary Object effects: a custom factory can reinterpret forwarded fields. External declaration files and owned top-level ambient module declarations in declaration files retain the explicit standard-React library assumption; those declarations do not prove the actual runtime JavaScript identity. Missing or ambiguous internal import metadata, missing native symbols, foreign owners and local nonambient declaration-module sources abstain. Non-JSX dependencies do not acquire a runtime edge merely because the compiler records an unused synthetic JSX import. Runtime rebinding outside the compiler-visible module context remains opaque, not validated by this proof.","Requires target ES2015 or newer, explicit native ES-module output and the standard React automatic JSX runtime (react-jsx or react-jsxdev). Downlevel arrows become constructible functions; CommonJS exports remain mutable. Preserve/classic JSX, custom factories, namespaces, import sources and per-file JSX directives are unsupported. Imported library implementations and cross-project source redirects are opaque; relative imports within this program remain supported."],"messageIds":["excess"],"optionsSchema":null,"rationale":"A spread can forward unrelated data even when structural typing accepts the larger object. Changing its annotation does not remove runtime fields.","references":["https://github.com/jsx-eslint/eslint-plugin-react/blob/master/docs/rules/jsx-props-no-spreading.md"],"remediation":"Forward the declared fields explicitly or review the receiving contract. A projection changes object identity and allocations; this rule never constructs one automatically.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-excess-jsx-props.ts","status":"active","summary":"Report statically known fields spread outside a resolved component props contract.","test":"packages/typescript/tests/rules/no-excess-jsx-props.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/example.ts","source":"function hasDisplayLabel(item: CatalogItem): boolean {\n  if (!item.visible) return false;\n  if (!item.inStock) return false;\n  if (item.label === \"\") return false;\n  return true;\n}\n\nfunction collectLabels(catalog: Catalog): string[] {\n  const labels: string[] = [];\n  for (const section of catalog.sections) {\n    for (const shelf of section.shelves) {\n      for (const item of shelf.items) {\n        if (hasDisplayLabel(item)) labels.push(item.label);\n      }\n    }\n  }\n  return labels;\n}\n"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"catalog-labels-after","outcome":"accept","scenarioId":"catalog-labels","title":"Extract eligibility: traversal 10; predicate 3"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"function collectLabels(catalog: Catalog): string[] {\n  const labels: string[] = [];\n  for (const section of catalog.sections) {\n    for (const shelf of section.shelves) {\n      for (const item of shelf.items) {\n        if (item.visible) {\n          if (item.inStock) {\n            if (item.label !== \"\") {\n              labels.push(item.label);\n            }\n          }\n        }\n      }\n    }\n  }\n  return labels;\n}\n"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"catalog-labels-before","outcome":"reject","scenarioId":"catalog-labels","title":"Synthetic catalog traversal: score 21 (error)"},{"expectedCount":0,"files":[{"path":"src/example.ts","source":"function canDownload(item: Download): boolean {\n  if (!item.published) return false;\n  if (!item.licensed) return false;\n  if (!item.available) return false;\n  if (item.quarantined) return false;\n  if (!(item.bytes > 0)) return false;\n  if (!(item.bytes <= 1000000)) return false;\n  return true;\n}\n"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"download-check-after","outcome":"accept","scenarioId":"download-check","title":"Use early returns: score 6"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"function canDownload(item: Download): boolean {\n  if (item.published) {\n    if (item.licensed) {\n      if (item.available) {\n        if (!item.quarantined) {\n          if (item.bytes > 0) {\n            if (item.bytes <= 1000000) {\n              return true;\n            }\n          }\n        }\n      }\n    }\n  }\n  return false;\n}\n"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"download-check-before","outcome":"reject","scenarioId":"download-check","title":"Synthetic download eligibility: score 21 (error)"},{"expectedCount":0,"files":[{"path":"src/decision.ts","source":"function decide(a, b, c, d, e, f, g) { if (!a) return; if (!b) return; if (!c) return; if (!d) return; if (!e) return; if (!f) return; if (!g) return; act(); }"}],"fixedFiles":[],"focusPath":"src/decision.ts","id":"guard-decisions","outcome":"accept","scenarioId":"primary","title":"Guard clauses reduce nesting"},{"expectedCount":1,"files":[{"path":"src/decision.ts","source":"function decide(a, b, c, d, e, f, g) { if (a) { if (b) { if (c) { if (d) { if (e) { if (f) { if (g) { act(); } } } } } } } }"}],"fixedFiles":[],"focusPath":"src/decision.ts","id":"nested-decisions","outcome":"reject","scenarioId":"primary","title":"Seven nested decisions exceed the error limit"}],"filePatterns":[],"id":"no-excessive-cognitive-complexity","key":"oxlint:no-excessive-cognitive-complexity","languages":["typescript"],"limitations":["Sarj metric, not exact Sonar compatibility: functions and callbacks are independent; calls, recursion, types, parameter defaults, decorators, and class initializers are not scored.","Conditions, loops, catch clauses, switch statements, and conditional expressions add one plus nesting. Else-if and else add one; their bodies add nesting. Each run of like boolean operators adds one.","Nullish coalescing and optional chaining are free. Labeled break/continue add one. Try, finally, return, unlabeled jumps, and await add no points themselves.","Generated code is excluded; authored tests, React components, and all function lengths are checked. The threshold is a shared policy constant, not a correctness boundary."],"messageIds":["excessiveComplexity"],"optionsSchema":null,"rationale":"Nested control flow increases the context a reader must retain while following a function.","references":[],"remediation":"Scores up to 20 pass; 21 or more are errors. Examples are fictional; domain types are omitted. Refactor one function at a time, starting with its largest contributors. First simplify control flow and reduce nesting within the function. Extract a helper only when it clarifies a cohesive responsibility or enables meaningful reuse; do not split code solely to lower the score. Use lookup tables only for equivalent pure dispatch. Preserve APIs, side effects, evaluation order, and exception behavior. Run relevant tests before and after; remeasure the original and extracted functions. Do not hide branches in dense expressions or add indirection just to lower a score.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-excessive-cognitive-complexity.ts","status":"active","summary":"Report an error for function bodies with cognitive complexity above 20.","test":"packages/typescript/tests/rules/no-excessive-cognitive-complexity.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/job.ts","source":"type Job = { kind: 'queued'; queue: string } | { kind: 'done'; result: string }; function render(job: Job) { return job.kind === 'done' ? job.result : job.queue; }"}],"fixedFiles":[],"focusPath":"src/job.ts","id":"direct-discriminator","outcome":"accept","scenarioId":"primary","title":"Let the compiler narrow the tagged union"},{"expectedCount":0,"files":[{"path":"src/job.ts","source":"type Job = { kind: 'queued'; queue: string } | { kind: 'done'; result: string }; const isDone = (job: Job) => job.kind === 'done';"}],"fixedFiles":[],"focusPath":"src/job.ts","id":"inferred-predicate","outcome":"accept","scenarioId":"inference","title":"Infer a discriminator helper from its body"},{"expectedCount":1,"files":[{"path":"src/job.ts","source":"type Job = { kind: 'queued'; queue: string } | { kind: 'done'; result: string }; type Done = Extract<Job, {kind: 'done'}>; function isDone(job: Job): job is Done { return job.kind === 'done'; } function render(job: Job) { return isDone(job) ? job.result : job.queue; }"}],"fixedFiles":[],"focusPath":"src/job.ts","id":"manual-predicate","outcome":"reject","scenarioId":"primary","title":"Avoid an unchecked narrowing annotation"},{"expectedCount":1,"files":[{"path":"src/job.ts","source":"type Job = { kind: 'queued'; queue: string } | { kind: 'done'; result: string }; type Done = Extract<Job, {kind: 'done'}>; const isDone = (job: Job): job is Done => job.kind === 'done';"}],"fixedFiles":[],"focusPath":"src/job.ts","id":"written-predicate","outcome":"reject","scenarioId":"inference","title":"A written predicate remains an unchecked promise"}],"filePatterns":[],"id":"no-explicit-type-predicate","key":"oxlint:no-explicit-type-predicate","languages":["typescript"],"limitations":["This is an explicit design policy, including otherwise correct handwritten predicates. Reports every authored TypeScript is predicate, including asserts value is Type, method and callback annotations, function types, overloads, ambient signatures and predicates inside casts or satisfies expressions.","Direct discriminator checks, compiler-inferred predicates, ordinary boolean returns and plain asserts condition remain valid. A tag on unknown input does not validate its payload. Imported library guard calls and imported predicate types are not redeclarations and are not inspected.","Shared generated, vendored and declaration-file exclusions apply. Maintained test implementations remain checked; strings, comments and JSDoc text are not TypeScript predicate nodes. There is no exemption based on helper names, imports or body shape.","No automatic fix or suggestion removes an annotation: public and callback contracts, overloads, both narrowing branches, emitted declarations and existing TypeScript versions may depend on it. Review callers and preserve intentional runtime behavior when migrating. Existing partial and contradictory predicate rules retain their separate correctness checks."],"messageIds":["noExplicitTypePredicate"],"optionsSchema":null,"rationale":"TypeScript trusts an explicit value is Type annotation without proving that its implementation recognizes that type in both branches. A stale or partial predicate can make callers read properties that do not exist. Direct discriminator checks and inferred predicates keep narrowing tied to the implementation.","references":["https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions","https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html#inferred-type-predicates","https://zod.dev/basics"],"remediation":"For owned variants, check the discriminated union's tag directly or let TypeScript infer a simple discriminator helper or null filter. Parse unknown external data with a specific schema and consume its validated value or discriminated result. Do not replace a predicate with a cast or a schema boolean wrapper that repeats the same unchecked type claim. If a native API cannot express the required static contract, isolate that boundary and use an exact-rule local suppression explaining the constraint.","requiresTypeInformation":false,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-explicit-type-predicate.ts","status":"active","summary":"Use compiler-checked narrowing instead of handwritten type predicates.","test":"packages/typescript/tests/rules/no-explicit-type-predicate.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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":"Review whether four selected operations share one recovery policy"},{"expectedCount":0,"files":[{"path":"src/load.ts","source":"function f() { try { const a = one(); const b = two(); const c = three(); } catch (error) { handle(error); } const d = four(); finish(); }"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"focused-try-block","outcome":"accept","scenarioId":"primary","title":"Keep only operations with the same recovery policy in the try block"}],"filePatterns":[],"id":"no-fat-try-blocks","key":"oxlint:no-fat-try-blocks","languages":["typescript"],"limitations":["The default threshold is three selected top-level operations, not a proof of every possible throw. A shared recovery policy may legitimately cover several operations; generated files, catchless finally blocks, rethrows, and terminal error boundaries are excluded."],"messageIds":["fatTryBlock"],"optionsSchema":{"additionalProperties":false,"properties":{"max":{"description":"Maximum number of statements allowed in one try body.","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.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-fat-try-blocks.ts","status":"active","summary":"Review try blocks exceeding the configured count of syntactically selected operations.","test":"packages/typescript/tests/rules/no-fat-try-blocks.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"action.test.ts","source":"const result = await runAction({ service: recordingService });"}],"fixedFiles":[],"focusPath":"action.test.ts","id":"injected-service","outcome":"accept","scenarioId":"primary","title":"Inject a narrow service port"},{"expectedCount":0,"files":[{"path":"tests/service.test.ts","source":"import { run } from './service.js'; run();"}],"fixedFiles":[],"focusPath":"tests/service.test.ts","id":"jest-esm-after","outcome":"accept","scenarioId":"jest-esm","title":"Exercise the maintained ESM implementation — after"},{"expectedCount":1,"files":[{"path":"tests/service.test.ts","source":"import { jest } from '@jest/globals'; jest.unstable_mockModule('./service.js', () => ({}));"}],"fixedFiles":[],"focusPath":"tests/service.test.ts","id":"jest-esm-before","outcome":"reject","scenarioId":"jest-esm","title":"Exercise the maintained ESM implementation — before"},{"expectedCount":1,"files":[{"path":"action.test.ts","source":"import { vi } from 'vitest'; vi.mock('./service', () => ({ run: vi.fn() }));"}],"fixedFiles":[],"focusPath":"action.test.ts","id":"relative-module-mock","outcome":"reject","scenarioId":"primary","title":"Do not replace an owned module"}],"filePatterns":[],"id":"no-first-party-module-mock","key":"oxlint:no-first-party-module-mock","languages":["typescript"],"limitations":["Only generated-free test files, provenance-resolved Vitest mock/doMock or Jest mock/doMock/unstable_mockModule APIs, relative module specifiers, and explicitly configured first-party prefixes are checked. Third-party, framework, Jest virtual modules, unknown Jest options, type-only imports and dynamic module names are excluded. Unimported framework globals and indirect namespace aliases are not inferred."],"messageIds":["noFirstPartyModuleMock"],"optionsSchema":{"additionalProperties":false,"properties":{"additionalModulePrefixes":{"description":"Additional maintained module prefixes that tests must not mock.","items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true}},"type":"object"},"rationale":"Replacing an owned module couples tests to its lookup path and can verify mock wiring while bypassing the real implementation contract.","references":["https://github.com/dmmulroy/anti-slop/blob/13909c9e07590f5a85d93c62712259fe14a883f7/src/rules/no-module-mocking.ts"],"remediation":"Extract a narrow port or pure core operation and inject a typed fake; retain module mocking only when module lookup or bootstrap behavior is the contract.","requiresTypeInformation":false,"runtimeImports":["@jest/globals","vitest"],"since":null,"source":"packages/typescript/src/rules/no-first-party-module-mock.ts","status":"active","summary":"Prefer injected collaborators over mocking maintained first-party modules in tests.","test":"packages/typescript/tests/rules/no-first-party-module-mock.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/helper.tsx","source":"export function identity(value: number) { return value; }"}],"fixedFiles":[],"focusPath":"src/helper.tsx","id":"empty-comment-removed","outcome":"accept","scenarioId":"empty-comment","title":"Preserve the return expression without a comment"},{"expectedCount":1,"files":[{"path":"src/helper.tsx","source":"export function identity(value: number) { return/**/value; }"}],"fixedFiles":[{"path":"src/helper.tsx","source":"export function identity(value: number) { return value; }"}],"focusPath":"src/helper.tsx","id":"empty-comment-separator","outcome":"reject","scenarioId":"empty-comment","title":"Keep a separating space when removing an empty comment"},{"expectedCount":1,"files":[{"path":"src/view.tsx","source":"// Show current status\nexport const View = () => <p>Ready</p>;"}],"fixedFiles":[],"focusPath":"src/view.tsx","id":"frontend-prose","outcome":"reject","scenarioId":"primary","title":"Express view behavior in code"},{"expectedCount":0,"files":[{"path":"src/view.tsx","source":"export const View = () => <p>Ready</p>;"}],"fixedFiles":[],"focusPath":"src/view.tsx","id":"named-view","outcome":"accept","scenarioId":"primary","title":"The view expresses its behavior"},{"expectedCount":1,"files":[{"path":"package.json","source":"{\"private\":true,\"dependencies\":{\"react\":\"19.2.0\"}}"},{"path":"src/helper.ts","source":"// Frontend helper\nexport const ready = true;"}],"fixedFiles":[],"focusPath":"src/helper.ts","id":"package-helper","outcome":"reject","scenarioId":"package","title":"A plain helper belongs to its frontend package"},{"expectedCount":0,"files":[{"path":"package.json","source":"{\"private\":true,\"dependencies\":{\"react\":\"19.2.0\"}}"},{"path":"src/helper.ts","source":"export const ready = true;"}],"fixedFiles":[],"focusPath":"src/helper.ts","id":"package-helper-code","outcome":"accept","scenarioId":"package","title":"The helper expresses its behavior in code"}],"filePatterns":[],"id":"no-frontend-comments","key":"oxlint:no-frontend-comments","languages":["typescript"],"limitations":["Reports every actual line, block, JSDoc and JSX comment in maintained frontend source, including legal notices and executable directives. The existing no-test-comments rule owns maintained test files and applies the same blanket frontend ban, avoiding duplicate diagnostics between these two rules. Strings, template data, rendered JSX text and generated/vendor sources are excluded.","Frontend scope is established by JSX-capable extensions, use client/use server directives, framework imports, or the nearest package manifest declaring a frontend framework dependency. Pure helpers in those packages are included. Set allFiles: true on both this rule and no-test-comments in a frontend Oxlint override for dependency-free or unsupported frameworks; file names alone do not infer application ownership.","This is a blanket frontend policy, including comments preserved by narrower comment-hygiene rules. Behavioral annotations, JavaScript type comments, required legal notices and tool directives require manual migration or an explicit policy exception; the rule never automatically deletes them.","Automatic fixes replace only whitespace-only line or block comment tokens containing no line terminators with one separating space. Every existing line terminator is preserved. Comments overlapping JSX regions, multiline comments, nonempty comments and comments owned by the test rule remain manual."],"messageIds":["noFrontendComments"],"optionsSchema":{"additionalProperties":false,"properties":{"allFiles":{"description":"Check comments without requiring frontend framework ownership.","type":"boolean"}},"type":"object"},"rationale":"Frontend comments can drift from executable behavior and conceal names, types or structure that should communicate the same information.","references":[],"remediation":"Use clear names, types, helpers and assertions, then remove the comment. Move tool settings into their owning configuration where supported. Whitespace-only single-line comments can be replaced with a space; review all other comments and their contracts manually.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-frontend-comments.ts","status":"active","summary":"Express frontend behavior through code instead of prose comments.","test":"packages/typescript/tests/rules/no-frontend-comments.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/request.ts","source":"import { z } from 'zod'; const VersionSchema = z.object({ version: z.number().int().positive() }); function load(raw: unknown) { return VersionSchema.parse(raw).version; }"}],"fixedFiles":[],"focusPath":"src/request.ts","id":"focused-record-read","outcome":"accept","scenarioId":"primary","title":"Declare only the fields being read"},{"expectedCount":1,"files":[{"path":"src/request.ts","source":"import { z } from 'zod'; const JsonSchema = z.record(z.string(), z.unknown()); function load(raw: unknown) { const body = JsonSchema.parse(raw); return body.version; }"}],"fixedFiles":[],"focusPath":"src/request.ts","id":"open-record-read","outcome":"reject","scenarioId":"primary","title":"Generic validation does not declare the field"},{"expectedCount":0,"files":[{"path":"src/example.ts","source":"import { z } from 'zod'; function load(raw: unknown) { const result = z.object({ id: z.string() }).safeParse(raw); if (!result.success) return; return result.data.id; }"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"safe-parse-fields-after","outcome":"accept","scenarioId":"safe-parse-fields","title":"Validate the fields consumed after success — after"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"import { z } from 'zod'; function load(raw: unknown) { const result = z.record(z.string(), z.unknown()).safeParse(raw); if (!result.success) return; return result.data.id; }"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"safe-parse-fields-before","outcome":"reject","scenarioId":"safe-parse-fields","title":"Validate the fields consumed after success — before"}],"filePatterns":[],"id":"no-generic-json-parser","key":"oxlint:no-generic-json-parser","languages":["typescript"],"limitations":["Checks static property reads on direct or stable-const aliases of Zod parse/parseAsync outputs and proven successful safeParse/awaited safeParseAsync data from json, any, unknown, record and partialRecord schemas. Resolves runtime Zod imports, local const schema aliases and identity-preserving schema wrappers.","Dynamic dictionary indexing, opaque JSON forwarding/storage, imported schemas, recursive lazy definitions, custom validator implementations, mutable aliases, unproven success guards and transformation/refinement callbacks are excluded. Subsequent parser validation with a specific or unresolved contract and meaningful typed/finite-key records are preserved. Generated/vendor files are excluded; maintained tests are checked.","The rule identifies a missing declared field contract; it does not infer all external payload fields or automatically change runtime validation."],"messageIds":["genericParser"],"optionsSchema":null,"rationale":"An open JSON parser checks serialization or value kinds without declaring required fields, leaving application reads outside the validation contract.","references":["https://github.com/dmmulroy/anti-slop/blob/13909c9e07590f5a85d93c62712259fe14a883f7/src/rules/no-unsafe-dictionary-type.ts"],"remediation":"Define a focused z.object schema for the fields being read and infer its output type. Keep generic JSON at deliberate opaque forwarding or storage boundaries.","requiresTypeInformation":false,"runtimeImports":["zod"],"since":null,"source":"packages/typescript/src/rules/no-generic-json-parser.ts","status":"active","summary":"Parse known JSON fields with an object schema instead of an open JSON or record parser.","test":"packages/typescript/tests/rules/no-generic-json-parser.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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/parse-order.ts","source":"export function parseOrder() { return {}; }"}],"fixedFiles":[],"focusPath":"src/parse-order.ts","id":"responsibility-named-module","outcome":"accept","scenarioId":"primary","title":"Name the module after its export"}],"filePatterns":[],"id":"no-generic-single-export-module","key":"oxlint:no-generic-single-export-module","languages":["typescript"],"limitations":["Only the fixed generic-stem vocabulary with exactly one public runtime export is checked; exported destructuring patterns are excluded rather than undercounted."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"performance","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/rows.ts","source":"export function collect(input: string[]) { const rows: string[] = []; for (const row of input) { rows.push(row); } return rows; }"}],"fixedFiles":[],"focusPath":"src/rows.ts","id":"append-rows","outcome":"accept","scenarioId":"primary","title":"Append into a fresh unobserved output"},{"expectedCount":1,"files":[{"path":"src/rows.ts","source":"export function collect(input: string[]) { let rows: string[] = []; for (const row of input) { rows = [...rows, row]; } return rows; }"}],"fixedFiles":[],"focusPath":"src/rows.ts","id":"copy-prefix","outcome":"reject","scenarioId":"primary","title":"Repeated spread copies the accumulated rows"}],"filePatterns":[],"id":"no-growing-array-spread-in-loop","key":"oxlint:no-growing-array-spread-in-loop","languages":["typescript"],"limitations":["Initially checks a native-array for-of loop with a straight-line append-only body, a fresh local empty array outside that loop, a prefix spread followed by a definite element, and no other writes or observations across the loop.","Inputs initially come from parameters, imports or declarations, optionally through plain const aliases. Locally literal-, property- and call-produced inputs are excluded while their bounds remain unproved. Retained snapshots, escaping arrays, resets, nested function references and object spreads remain outside this rule. Existing Unicorn concat-loop coverage and reducer copy analysis keep their own owners.","A finding requests performance review; it is not a measured speedup claim. Corrections remain manual."],"messageIds":["copy"],"optionsSchema":null,"rationale":"Repeatedly copying all accumulated elements can make append collection quadratic in the output size.","references":["https://github.com/dmmulroy/anti-slop/blob/13909c9e07590f5a85d93c62712259fe14a883f7/src/rules/no-reduce-accumulator-copy.ts"],"remediation":"Append into a fresh locally owned output when snapshot and observation semantics permit it.","requiresTypeInformation":true,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-growing-array-spread-in-loop.ts","status":"active","summary":"Review repeated prefix copies of a growing fresh local array inside a loop.","test":"packages/typescript/tests/rules/no-growing-array-spread-in-loop.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-hand-rolled-sleep","languages":["typescript"],"limitations":["The rule skips tests, scripts, generated files, and client modules by default, and supports explicit path exemptions.","Locally shadowed constructors/timers and value-returning timers are excluded. Only recognized browser markers are excluded; choose a runtime-compatible cancellation API for other browser modules."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","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":"oxlint:no-hand-rolled-spinner","languages":["typescript"],"limitations":["Only effective static className values on div and span elements are inspected; Tailwind utilities are combined only when they are unprefixed or share one exact variant context, while a later spread makes the value unknown. 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.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","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":"oxlint:no-impossible-zod-literal-bounds","languages":["typescript"],"limitations":["Only finite numeric literals, including unary signs and erased TypeScript wrappers, in a single schema chain are compared; number().int(), int(), int32(), uint32(), strings and arrays require an admitted integer value or length.","int32() and uint32() include their intrinsic ranges. The managed preset delegates explicit real-bound conflicts to upstream zod/no-conflicting-checks to avoid duplicate findings.","Integer-domain comparisons abstain near the JavaScript safe-integer boundary.","Chains with dynamic bounds, non-bound validators, transforms, pipes, or preprocessors are skipped.","Test and generated files are excluded."],"messageIds":["impossibleBounds"],"optionsSchema":{"additionalProperties":false,"properties":{"delegateRealContradictions":{"description":"Let enabled upstream zod/no-conflicting-checks own explicit real-bound contradictions.","type":"boolean"}},"type":"object"},"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/cache.ts","source":"declare const cache: Map<string, object>; declare const key: string; if (key in cache) use(cache);"}],"fixedFiles":[],"focusPath":"src/cache.ts","id":"map-entry-membership","outcome":"reject","scenarioId":"primary","title":"Use Map.has for entry membership"},{"expectedCount":0,"files":[{"path":"src/cache.ts","source":"declare const value: { id: string } | { slug: string }; if ('id' in value) use(value.id);"}],"fixedFiles":[],"focusPath":"src/cache.ts","id":"object-property-narrowing","outcome":"accept","scenarioId":"primary","title":"Preserve ordinary object property narrowing"}],"filePatterns":[],"id":"no-in-operator-on-built-in-collections","key":"oxlint:no-in-operator-on-built-in-collections","languages":["typescript"],"limitations":["The receiver type must resolve directly to a default-library Map, ReadonlyMap, WeakMap, Set, ReadonlySet, or WeakSet declaration; type aliases, unions, intersections, type parameters, subclasses, structural lookalikes, any, unknown, and generated files are excluded.","Automatic fixes use Reflect.has only for a sole exported ordinary function with an explicit boolean return annotation and a literal-string property check on its single direct collection parameter, with no comments, other statements, source callouts or shadowed Reflect. This preserves existing property semantics and Proxy has-traps; inferred or explicit type predicates and other cases remain manual, and .has() is never inferred.","Plain objects, arrays, TypeScript object-shape narrowing, private-brand checks, and for-in statements are not checked."],"messageIds":["ambiguousCollectionIn"],"optionsSchema":null,"rationale":"The `in` operator checks properties on the collection object and its prototype, while `.has()` checks stored entries. The similar spelling can silently test the wrong namespace.","references":[],"remediation":"Use `collection.has(key)` for entry membership. Use `Reflect.has(collection, property)` when prototype-aware property lookup or Proxy `has` behavior is intentional.","requiresTypeInformation":null,"runtimeImports":[],"since":"15.17.14","source":"packages/typescript/src/rules/no-in-operator-on-built-in-collections.ts","status":"active","summary":"Do not use the `in` operator to test entries in built-in Map and Set collections.","test":"packages/typescript/tests/rules/no-in-operator-on-built-in-collections.test.ts"},{"aliases":[],"autofix":"safe","category":"security","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-insecure-random-id","languages":["typescript"],"limitations":["Names select security-sensitive bindings heuristically; they do not prove sensitivity. Sampling in unrelated bindings or branch tests, locally shadowed Math objects, ambiguous identifiers and test files are excluded. This is not interprocedural data-flow analysis. Automatic fixes cover only an exact RFC v4 x/y recipe in a closed module with explicit string APIs and an already-used bound crypto.randomUUID provider. Canonical unmodified Web Crypto, Math, String, RegExp, Number and Function.bind are assumed; the entropy upgrade intentionally changes random traces. Other recipes and providers remain manual."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/count.ts","source":"const counter: { count: number } = { count: 1 }; Object.assign(counter, { count: 2 });"}],"fixedFiles":[],"focusPath":"src/count.ts","id":"compatible-property-write","outcome":"accept","scenarioId":"primary","title":"Preserve the declared field contract"},{"expectedCount":1,"files":[{"path":"src/count.ts","source":"const counter: { count: number } = { count: 1 }; Object.assign(counter, { count: \"invalid\" }); counter.count.toFixed();"}],"fixedFiles":[],"focusPath":"src/count.ts","id":"incompatible-property-write","outcome":"reject","scenarioId":"primary","title":"Do not bypass field compatibility through a generic mutation API"}],"filePatterns":[],"id":"no-invalid-typed-mutation","key":"oxlint:no-invalid-typed-mutation","languages":["typescript"],"limitations":["Requires type information and an explicitly annotated local const initialized with a plain own-data object literal. Only non-exported, stable bindings without escapes or nested captures qualify. Deletes and descriptors that can lock writability or configurability invalidate storage provenance. The closed non-union contract must declare the initialized data property; parameters, opaque constructors/factories, accessor, method, indexed, callable and unresolved generic targets are excluded.","Inspects native Object.assign with one plain object-literal source, native Reflect.set with a static string key and no separate receiver, and native Object/Reflect.defineProperty with a plain value descriptor. Dynamic keys, spreads, accessors, API aliases, extra properties and multiple assign sources are excluded.","Any, unknown, never, unresolved generic or overloaded property/value types are excluded because this check cannot prove their incompatibility. Type arguments and nested data fields are inspected with a 32-type recursion bound. Readonly fields are checked only for incompatible values; compatible writes are outside this rule. Under exact optional properties, authored write types are recovered only for direct non-transient declarations without mapped or generic substitution; other explicit-undefined writes can be missed.","Scope-resolved globals and standard-library signatures establish API identity. File-local namespace escapes, aliases, direct replacements or reflective mutation of Object/Reflect invalidate that namespace. Visible Object.prototype mutation or escape invalidates both APIs because descriptors inherit its fields. Indirect monkey-patching and external proxy behavior are not inferred. Generated files are excluded; tests remain checked."],"messageIds":["incompatibleWrite"],"optionsSchema":null,"rationale":"Object.assign, Reflect.set and property descriptors accept values that a normal typed property assignment rejects. They can invalidate a domain object's contract while subsequent reads retain its original type.","references":["https://typescript-eslint.io/rules/no-unsafe-assignment/","https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign"],"remediation":"Use an assignment that satisfies the declared property type, or construct and validate a complete replacement. Preserve any deliberate construction or external compatibility contract; do not cast away the incompatibility.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-invalid-typed-mutation.ts","status":"active","summary":"Keep native object mutation APIs from writing incompatible values into declared property types.","test":"packages/typescript/tests/rules/no-invalid-typed-mutation.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/view.tsx","source":"<link rel=\"preload\" as=\"font\" href=\"/font.woff2\" crossOrigin=\"anonymous\" />"}],"fixedFiles":[],"focusPath":"src/view.tsx","id":"anonymous-font-preload","outcome":"accept","scenarioId":"font-preload","title":"Match the font request mode"},{"expectedCount":0,"files":[{"path":"src/view.tsx","source":"<img loading=\"eager\" fetchPriority=\"high\" />"}],"fixedFiles":[],"focusPath":"src/view.tsx","id":"eager-critical-image","outcome":"accept","scenarioId":"image-loading","title":"Load a declared critical image eagerly"},{"expectedCount":1,"files":[{"path":"src/view.tsx","source":"<link rel=\"preload\" as=\"font\" href=\"/font.woff2\" />"}],"fixedFiles":[],"focusPath":"src/view.tsx","id":"font-preload-without-cors","outcome":"reject","scenarioId":"font-preload","title":"Review a non-reusable font preload"},{"expectedCount":1,"files":[{"path":"src/view.tsx","source":"<img loading=\"lazy\" fetchPriority=\"high\" />"}],"fixedFiles":[],"focusPath":"src/view.tsx","id":"lazy-critical-image","outcome":"reject","scenarioId":"image-loading","title":"Review conflicting image hints"}],"filePatterns":[],"id":"no-invalid-web-resource-hints","key":"oxlint:no-invalid-web-resource-hints","languages":["typescript"],"limitations":["Only intrinsic JSX img, iframe, video, audio, script and link attributes are inspected. Custom framework media components, HTML strings and DOM mutations are outside this rule.","Stable local string bindings are resolved. Later unknown spreads and dynamic values remain unproven; explicit attributes after spreads can establish a finding.","The rule does not infer which element is LCP, count page-wide priority hints, require image dimensions that may come from CSS, or require sizes when 100vw may be correct."],"messageIds":["criticalLazy","fontCors","invalidLoading","obsoleteImportance","preloadAs"],"optionsSchema":null,"rationale":"Lazy loading delays even explicitly high-priority images until layout. Font preloads with incompatible credentials modes cannot satisfy the subsequent font request. The unsupported loading=auto value and obsolete importance attribute do not express supported native loading behavior. Oxlint 1.91 still accepts importance, so this advisory fills that verified upstream gap.","references":["https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/performance/optimize-image-priority.md","https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/performance/optimize-preload-priority.md","https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/performance/optimize-script-priority.md"],"remediation":"Review whether the image is critical or offscreen, use the matching loading policy, specify the preload destination, and use anonymous CORS for font preloads. Do not change request behavior automatically.","requiresTypeInformation":false,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-invalid-web-resource-hints.ts","status":"active","summary":"Keep native resource hints consistent with browser loading and preload request modes.","test":"packages/typescript/tests/rules/no-invalid-web-resource-hints.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/value.ts","source":"import { z } from \"zod\"; const value = z.custom<string>().parse(1);"}],"fixedFiles":[],"focusPath":"src/value.ts","id":"fabricated-string","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"reject","scenarioId":"primary","title":"An unchecked parser returns the number unchanged"},{"expectedCount":0,"files":[{"path":"src/value.ts","source":"import { z } from \"zod\"; const value = z.custom<string>(value => typeof value === \"string\").parse(1);"}],"fixedFiles":[],"focusPath":"src/value.ts","id":"validated-string","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"accept","scenarioId":"primary","title":"Validate a primitive at the boundary"}],"filePatterns":[],"id":"no-invalid-zod-custom-parse","key":"oxlint:no-invalid-zod-custom-parse","languages":["typescript"],"limitations":["Requires type information and a direct inline z.custom<T>().parse(primitiveLiteral) call with no custom validator or parse options. Recognizes runtime imports from zod or zod/v4 and native installed Zod v4 declarations; schema bindings, compositions, safeParse, dynamic inputs and indirect calls are excluded.","Targets must be broad primitives, finite unbranded primitive/nullish literal unions, or never. Any, unknown, void, object, enum, branded, intersection, generic and opaque targets are preserved. Membership is checked by primitive value, not nominal assignability. Null input requires strictNullChecks.","The import binding must be used only by direct custom calls or type-only references. Other namespace uses, aliases, exports, mutation and escapes conservatively disable the proof for that binding. Generated files are excluded; tests remain checked. No autofix is offered."],"messageIds":["incompatibleCustomParse"],"optionsSchema":null,"rationale":"Without a validator, z.custom<T>() returns its input unchanged while promising T. A parsed primitive outside that value domain fabricates a contract the caller may trust.","references":["https://zod.dev/api#custom"],"remediation":"Provide actual validation, use the appropriate primitive schema, or retain an honest unknown result until validation. Preserve composed validators and independently owned domain contracts.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-invalid-zod-custom-parse.ts","status":"active","summary":"Do not parse an incompatible primitive literal through an unvalidated Zod custom schema.","test":"packages/typescript/tests/rules/no-invalid-zod-custom-parse.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/error.js","source":"JSON.stringify({error:new Error('lost'),['__proto__']:{toJSON(){return {message:'kept'};}}});"}],"fixedFiles":[],"focusPath":"src/error.js","id":"computed-prototype-data","outcome":"reject","scenarioId":"native-prototype-serializer","title":"A computed prototype name remains ordinary serialized data"},{"expectedCount":0,"files":[{"path":"src/error.js","source":"Error=class Other{message='kept'};const value=new Error();JSON.stringify(value);"}],"fixedFiles":[],"focusPath":"src/error.js","id":"constructor-after-write","outcome":"accept","scenarioId":"explicit-constructor-write","title":"A replaced constructor does not prove a native Error"},{"expectedCount":1,"files":[{"path":"src/error.js","source":"const value=new Error('lost');Error=class Other{message='kept'};JSON.stringify(value);"}],"fixedFiles":[],"focusPath":"src/error.js","id":"constructor-before-write","outcome":"reject","scenarioId":"explicit-constructor-write","title":"A later write does not change an already constructed native Error"},{"expectedCount":0,"files":[{"path":"src/error.ts","source":"function probe(){try{throw {id:1};}catch(err){if(err instanceof Error)return err.message;return JSON.stringify(\\u0065rr);}}\nconsole.log(probe());"}],"fixedFiles":[],"focusPath":"src/error.ts","id":"escaped-fallback-binding","outcome":"accept","scenarioId":"binding-provenance","title":"Keep native binding provenance"},{"expectedCount":0,"files":[{"path":"src/report.ts","source":"try { f(); } catch (err) { JSON.stringify({ error: err instanceof Error ? err.message : String(err) }); }"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"explicit-error-message","outcome":"accept","scenarioId":"primary","title":"Narrow an unknown catch value before selecting fields"},{"expectedCount":0,"files":[{"path":"src/error.js","source":"JSON.stringify({error:new Error('lost'),toJSON(){return {message:'kept'};}});"}],"fixedFiles":[],"focusPath":"src/error.js","id":"explicit-to-json-hook","outcome":"accept","scenarioId":"custom-serializer-contract","title":"An explicit callable toJSON owns its serialization contract"},{"expectedCount":0,"files":[{"path":"src/error.js","source":"JSON.stringify({error:new Error('lost'),__proto__:{toJSON(){return {message:'kept'};}}});"}],"fixedFiles":[],"focusPath":"src/error.js","id":"literal-prototype-hook","outcome":"accept","scenarioId":"native-prototype-serializer","title":"A literal prototype hook owns the payload serializer contract"},{"expectedCount":0,"files":[{"path":"src/error.js","source":"JSON.stringify=value=>'kept:'+value.message;JSON.stringify(new Error('lost'));"}],"fixedFiles":[],"focusPath":"src/error.js","id":"native-serializer-after-write","outcome":"accept","scenarioId":"explicit-serializer-write","title":"A replaced serializer owns its error-field contract"},{"expectedCount":1,"files":[{"path":"src/error.js","source":"const result=JSON.stringify(new Error('lost'));JSON.stringify=value=>'kept:'+value.message;"}],"fixedFiles":[],"focusPath":"src/error.js","id":"native-serializer-before-write","outcome":"reject","scenarioId":"explicit-serializer-write","title":"A later write does not change an earlier native serialization"},{"expectedCount":1,"files":[{"path":"src/error.js","source":"JSON.stringify({error:new Error('lost'),toJSON:42});"}],"fixedFiles":[],"focusPath":"src/error.js","id":"noncallable-to-json","outcome":"reject","scenarioId":"custom-serializer-contract","title":"A noncallable toJSON does not replace native serialization"},{"expectedCount":0,"files":[{"path":"src/error.js","source":"const error=new Error('lost',{cause:'kept'});JSON.stringify(error.cause);"}],"fixedFiles":[],"focusPath":"src/error.js","id":"scalar-cause","outcome":"accept","scenarioId":"member-value-provenance","title":"Native Error cause can be a string value"},{"expectedCount":1,"files":[{"path":"src/error.ts","source":"function probe(){try{throw {id:1};}catch(err){if(err instanceof Error)return err.message;{const err=new Error(\"lost\");return JSON.stringify(err);}}}\nconsole.log(probe());"}],"fixedFiles":[],"focusPath":"src/error.ts","id":"shadowed-error-binding","outcome":"reject","scenarioId":"binding-provenance","title":"Keep native binding provenance"},{"expectedCount":0,"files":[{"path":"src/error.js","source":"const key='message';const error=new Error('lost');JSON.stringify(error[key]);"}],"fixedFiles":[],"focusPath":"src/error.js","id":"static-error-field","outcome":"accept","scenarioId":"static-property-provenance","title":"A constant message field selects the explicit string escape hatch"},{"expectedCount":1,"files":[{"path":"src/error.js","source":"const key='stringify';JSON[key](new Error('lost'));"}],"fixedFiles":[],"focusPath":"src/error.js","id":"static-serializer-key","outcome":"reject","scenarioId":"static-property-provenance","title":"A constant computed serializer still loses native Error details"},{"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"},{"expectedCount":1,"files":[{"path":"src/error.js","source":"const error=new Error('lost',{cause:'kept'});JSON.stringify(error);"}],"fixedFiles":[],"focusPath":"src/error.js","id":"whole-error-with-cause","outcome":"reject","scenarioId":"member-value-provenance","title":"Serializing the whole Error still loses its native details"}],"filePatterns":[],"id":"no-json-stringify-error","key":"oxlint:no-json-stringify-error","languages":["typescript"],"limitations":["The rule uses stable local catch bindings and unshadowed built-in constructors, not runtime type information. Native DOMException constructor coverage is validated in Node; browser serialization is not evaluated. Custom replacers and explicit toJSON hooks are left to their serializer contract; a catch value is not guaranteed to be an Error. Member values, including cause, do not inherit the base Error's provenance; even an Error-valued member requires explicit value/dataflow proof outside this rule. Known literal __proto__ initializer chains preserve native hook inheritance and own toJSON precedence; computed __proto__ keys remain data. Unknown prototype expressions, hook values/spreads and deferred hook writes conservatively lose default serializer proof. Explicit writes invalidate constructor provenance except a later, separate top-level statement; deferred functions, loops, and unknown property keys conservatively lose constructor proof. Statically owned global/property writes and native Reflect.set/Reflect.defineProperty/Object.defineProperty/Object.defineProperties calls are tracked. Mutator aliases, replaced Reflect/Object methods, external calls, and runtime-only property names are not resolved; uncertain ordering conservatively loses proof."],"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.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-json-stringify-error.ts","status":"active","summary":"Avoid generic JSON serialization that can omit native Error details.","test":"packages/typescript/tests/rules/no-json-stringify-error.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/compare.ts","source":"type RecordState = { id: string; state: string };\nfunction sameRecord(actual: RecordState, expected: RecordState): boolean { return actual.id === expected.id && actual.state === expected.state; }\nconst same = sameRecord({ id: '1', state: 'ready' }, { state: 'ready', id: '1' });"}],"fixedFiles":[],"focusPath":"src/compare.ts","id":"domain-comparator","outcome":"accept","scenarioId":"primary","title":"Use an explicit structural comparator"},{"expectedCount":1,"files":[{"path":"src/compare.ts","source":"const same = JSON.stringify({ id: '1', state: 'ready' }) === JSON.stringify({ state: 'ready', id: '1' });"}],"fixedFiles":[],"focusPath":"src/compare.ts","id":"serialized-object-equality","outcome":"reject","scenarioId":"primary","title":"Do not compare object serialization"}],"filePatterns":[],"id":"no-json-stringify-object-equality","key":"oxlint:no-json-stringify-object-equality","languages":["typescript"],"limitations":["Only direct binary comparisons between two unshadowed global JSON.stringify calls, including stable const destructured aliases of that global, are checked.","Primitive-only arrays are excluded when TypeScript can prove their element types.","Mutable, defaulted, reassigned, transitive and unknown aliases, stored serialization results, custom JSON objects, tests, generated files, and equality hidden inside helper calls are excluded.","Automatic fixes only reuse an already called private ordinary comparator in a complete comment-free two-declaration module. Its two inline record parameter types must contain the same ordered unique string fields, including an already-authored own non-callable string toJSON field; its sole return compares every corresponding field with strict equality and conjunction. The sole export is an ordinary zero-argument function explicitly returning boolean[], whose sole returned array contains the existing comparator call and one direct strict JSON comparison of fresh literal records with those exact ordered fields and primitive string values. Imports, ambient declarations, callbacks, computed/spread/getter properties, prototype-related keys, type parameters and all other expressions remain manual.","The own primitive toJSON data field is required in every literal and is never introduced by the fix: it prevents an inherited Object.prototype.toJSON hook from changing serialization. The direct unshadowed JSON.stringify intrinsic must retain its standard behavior; no new helper, field, import or domain name is chosen."],"messageIds":["serializedObjectEquality"],"optionsSchema":null,"rationale":"JSON text equality depends on property insertion order and serialization behavior, so semantically equal objects can compare unequal and distinct values can collapse together.","references":[],"remediation":"Compare an explicit domain projection structurally, or use a reviewed canonical serializer when JSON semantics are required.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-json-stringify-object-equality.ts","status":"active","summary":"Do not use JSON serialization as structural object equality.","test":"packages/typescript/tests/rules/no-json-stringify-object-equality.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/tool.ts","source":"type Tool = { name: string; enabled: boolean };\ndeclare const tool: Tool;\nconst fields: Record<string, unknown> = tool;"}],"fixedFiles":[],"focusPath":"src/tool.ts","id":"erased-tool","outcome":"reject","scenarioId":"primary","title":"A dictionary erases a known tool contract"},{"expectedCount":0,"files":[{"path":"src/example.ts","source":"declare function load(): { id: string }; const value = load();"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"factory-initializer-after","outcome":"accept","scenarioId":"factory-initializer","title":"Retain the monomorphic factory contract — after"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"declare function load(): { id: string }; const value: unknown = load();"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"factory-initializer-before","outcome":"reject","scenarioId":"factory-initializer","title":"Retain the monomorphic factory contract — before"},{"expectedCount":0,"files":[{"path":"src/example.ts","source":"type Command = 'start'; const handlers = { start() {} } satisfies Record<Command, () => void>; handlers.start();"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"finite-registry-after","outcome":"accept","scenarioId":"finite-registry","title":"Preserve a closed registry key set — after"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"const handlers: Record<string, () => void> = { start() {} }; handlers.start();"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"finite-registry-before","outcome":"reject","scenarioId":"finite-registry","title":"Preserve a closed registry key set — before"},{"expectedCount":0,"files":[{"path":"src/example.ts","source":"const value = { id: 'u1' };"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"literal-initializer-after","outcome":"accept","scenarioId":"literal-initializer","title":"Retain the independently known literal contract — after"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"const value: unknown = { id: 'u1' };"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"literal-initializer-before","outcome":"reject","scenarioId":"literal-initializer","title":"Retain the independently known literal contract — before"},{"expectedCount":0,"files":[{"path":"src/example.ts","source":"class Store { readonly #value = { id: 'u1' }; read() { return this.#value; } }"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"private-storage-after","outcome":"accept","scenarioId":"private-storage","title":"Retain a completely owned storage contract — after"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"class Store { readonly #value: unknown = { id: 'u1' }; read() { return this.#value; } }"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"private-storage-before","outcome":"reject","scenarioId":"private-storage","title":"Retain a completely owned storage contract — before"},{"expectedCount":0,"files":[{"path":"src/tool.ts","source":"type Tool = { name: string; enabled: boolean };\ndeclare const tool: Tool;\nconst fields = tool;"}],"fixedFiles":[],"focusPath":"src/tool.ts","id":"retained-tool","outcome":"accept","scenarioId":"primary","title":"Retain the tool's fields"}],"filePatterns":[],"id":"no-known-value-widening","key":"oxlint:no-known-value-widening","languages":["typescript"],"limitations":["Requires TypeScript type information. Checks annotated local const bindings with independently known identifier, nonempty literal or monomorphic-call initializers; complete readonly private field writes and proven closed literal registries are also inspected. Parameters, direct exports, computed SDK/schema contracts, uncertain contextual inference and assertions are excluded.","Known-value erasure checks target unknown, object, and index-only dictionaries with unknown values. Separate finite-key guidance applies to proven closed registries with concrete values. Typed dynamic registries, finite-key records, empty accumulators, unknown external input and incomplete private-field write sets are preserved. Finite-key guidance requires a closed local literal with no mutations, escapes or unknown invocation receivers.","Intentional type erasure at a real abstraction boundary needs a local suppression explaining that boundary. New initializer, field and registry cases remain manual. Automatic fixes remove the annotation only from an unused private identifier-initialized binding in a module or block; referenced bindings retain their declared contract for manual review."],"messageIds":["registry","widening"],"optionsSchema":null,"rationale":"Erasing a typed value's fields forces downstream code to rediscover the same contract through casts, reflection, or representation checks.","references":["https://github.com/dmmulroy/anti-slop/blob/13909c9e07590f5a85d93c62712259fe14a883f7/src/rules/no-known-value-widening.ts"],"remediation":"Keep the inferred type, select the required domain fields, or use satisfies to check compatibility without erasing evidence.","requiresTypeInformation":true,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-known-value-widening.ts","status":"active","summary":"Preserve a known value's contract instead of widening a local binding to unknown, object, or an unknown-valued dictionary.","test":"packages/typescript/tests/rules/no-known-value-widening.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint: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.","requiresTypeInformation":null,"runtimeImports":[],"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":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/title.ts","source":"export function title(value: string) { const trimmed = value.trim(); return trimmed === \"\" ? \"Untitled\" : trimmed; }"}],"fixedFiles":[],"focusPath":"src/title.ts","id":"explicit-empty-title","outcome":"accept","scenarioId":"primary","title":"Preserve an intentional empty-string default"},{"expectedCount":1,"files":[{"path":"src/title.ts","source":"export function title(value: string) { return value.trim() || \"Untitled\"; }"}],"fixedFiles":[],"focusPath":"src/title.ts","id":"logical-or-title","outcome":"reject","scenarioId":"primary","title":"Do not hide empty-string fallback behavior"}],"filePatterns":[],"id":"no-logical-or-fallback","key":"oxlint:no-logical-or-fallback","languages":["typescript"],"limitations":["Requires TypeScript type information and strict null checks. Reports non-nullable string, number or bigint value fallbacks, including branded primitives and mixed primitive unions. Nullable inputs, including nullable booleans, remain owned by @typescript-eslint/prefer-nullish-coalescing; both rules are enabled in the managed strict config.","Preserves non-nullable boolean aggregation, conditions, negations, left-hand && guards, and calls to the native unshadowed Boolean constructor, including erased TypeScript wrappers and globalThis/window/self access. Constant truthiness stays with no-unnecessary-condition. Unknown, any, unresolved generic, object-only and generated inputs are excluded. Strings and comments are never scanned as code.","No automatic replacement is offered: replacing || with ?? would change empty-string or zero behavior. Non-nullable ||= expressions remain outside this companion rule."],"messageIds":["logicalOrFallback"],"optionsSchema":null,"rationale":"Logical OR falls back for empty strings, zero and false as well as missing values. This hides which values should actually receive a default.","references":["https://typescript-eslint.io/rules/prefer-nullish-coalescing/"],"remediation":"Use ?? for null or undefined. If an empty string, zero or false needs special handling, test that condition explicitly and preserve the intended behavior.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-logical-or-fallback.ts","status":"active","summary":"Use nullish defaults or explicit falsy-value checks instead of logical OR fallbacks.","test":"packages/typescript/tests/rules/no-logical-or-fallback.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-long-comment","languages":["typescript"],"limitations":["Only JSDoc blocks are inspected; structured API docs, tests, scripts, generated files, and versioned dependencies are excluded.","Safe fixes blank only interior plus-only decoration when the retained prose gains actual paragraph structure. All prose bytes, comment markers and line endings remain; directives, licenses, examples, literal-output/prompt/credential contexts, Markdown structure and ambiguous comments remain for review."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"security","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/listener.ts","source":"window.addEventListener(\"message\", event => { if (event.origin === \"https://trusted.example\") accept(event.data); });"}],"fixedFiles":[],"focusPath":"src/listener.ts","id":"exact-origin","outcome":"accept","scenarioId":"primary","title":"Compare an explicit trusted origin"},{"expectedCount":1,"files":[{"path":"src/listener.ts","source":"window.addEventListener(\"message\", event => { if (event.origin.startsWith(\"https://trusted.example\")) accept(event.data); });"}],"fixedFiles":[],"focusPath":"src/listener.ts","id":"prefix-origin","outcome":"reject","scenarioId":"primary","title":"Review a spoofable origin prefix"}],"filePatterns":[],"id":"no-message-origin-string-matching","key":"oxlint:no-message-origin-string-matching","languages":["typescript"],"limitations":["Only inline callbacks registered for message on ambient addEventListener, window, self or globalThis are tracked by variable identity.","Named listeners, event aliases, transformed origin strings, regular expressions and cross-file authorization helpers are not followed. Ordinary object.origin strings and shadowed globals are preserved.","This detects imprecise incoming origin matching, not all missing authorization checks. Outgoing target origins remain owned by Unicorn's require-post-message-target-origin rule."],"messageIds":["originMatch"],"optionsSchema":null,"rationale":"Prefix, suffix and substring checks do not establish an origin trust boundary. For example, https://trusted.example.attacker.invalid passes a prefix check against https://trusted.example.","references":["https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/security/validate-origins.md","https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/security/security.md"],"remediation":"Compare exact allowlisted origins or parse the origin and validate the intended scheme, host and port. Reject opaque null origins unless a separately verified source/capability contract establishes trust; do not require an experimental Origin API.","requiresTypeInformation":false,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-message-origin-string-matching.ts","status":"active","summary":"Compare incoming browser message origins against exact trusted origins.","test":"packages/typescript/tests/rules/no-message-origin-string-matching.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/order.ts","source":"function ordered(values: number[]): number[] { values.sort(); return values; }"}],"fixedFiles":[],"focusPath":"src/order.ts","id":"borrowed-sort","outcome":"reject","scenarioId":"primary","title":"Returning an array does not hide the mutation"},{"expectedCount":0,"files":[{"path":"src/order.ts","source":"function ordered(values: number[]): number[] { const result = [...values]; result.sort(); return result; }"}],"fixedFiles":[],"focusPath":"src/order.ts","id":"copied-sort","outcome":"accept","scenarioId":"primary","title":"Copy before mutating"}],"filePatterns":[],"id":"no-mutating-container-transform","key":"oxlint:no-mutating-container-transform","languages":["typescript"],"limitations":["Checks explicit array-valued return annotations and primitive-element array parameters, including Array<T> and Promise<Array<T>> forms. Untyped JavaScript, named application types, arbitrary objects, nested descendants, generic elements and scalar-returning command APIs remain outside the rule.","Tracks direct aliases and rebindings in linear statements and structured if branches. Unknown joins discard provenance; literal false branches are ignored. Loops, try blocks, nested functions and opaque statement forms cause the function to be excluded.","Reports direct indexed/length writes, indexed deletion and standard array mutator calls. Unknown helper calls and reflective mutation are not inferred; fresh local copies are preserved. Generated/vendor files are excluded. No automatic ownership-changing rewrite is offered."],"messageIds":["borrowedMutation"],"optionsSchema":null,"rationale":"A returned array does not undo writes visible through the caller's original aliases. Copying only after mutation leaves the caller changed.","references":[],"remediation":"Copy before changing the array and return the replacement, or use an explicit in-place command returning void. Preserve scalar command/result contracts.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-mutating-container-transform.ts","status":"active","summary":"Return array replacements without mutating borrowed array inputs.","test":"packages/typescript/tests/rules/no-mutating-container-transform.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/config.ts","source":"interface Config { enabled: boolean; } function enable(config: Config): Config { config.enabled = true; return config; }"}],"fixedFiles":[],"focusPath":"src/config.ts","id":"mutates-input","outcome":"reject","scenarioId":"primary","title":"A result does not undo the caller-visible write"},{"expectedCount":0,"files":[{"path":"src/config.ts","source":"interface Config { enabled: boolean; } function enable(config: Config): Config { return { ...config, enabled: true }; }"}],"fixedFiles":[],"focusPath":"src/config.ts","id":"returns-replacement","outcome":"accept","scenarioId":"primary","title":"Return the replacement explicitly"}],"filePatterns":[],"id":"no-mutating-input-with-result","key":"oxlint:no-mutating-input-with-result","languages":["typescript"],"limitations":["Checks direct property assignments, updates and deletions on stable object parameters in explicitly nonvoid functions that visibly return a value. Property contracts must come from an inline type or a same-file unextended, undecorated interface, object type alias or class. Only declared mutable data fields are checked; readonly-field errors remain compiler-owned.","Void/undefined/never commands, Promise<void>, receiver state, arbitrary imported/generic contracts, array inputs, nested descendants, parameter rebindings and alias-indirected writes remain excluded. Nested functions and class bodies have separate ownership and are not traversed. Generated/vendor files are excluded; maintained tests are checked.","No automatic rewrite chooses cloning semantics or changes function callers. Explicit command/result APIs with an intentional mutation require a local explanation."],"messageIds":["inputWrite"],"optionsSchema":null,"rationale":"A result-returning function that also writes a caller-owned object changes aliases outside the returned value, hiding its mutation contract.","references":[],"remediation":"Return a fresh replacement and update the caller to use it, or expose an explicit in-place command returning void. Do not add a dummy return or discard the operation.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-mutating-input-with-result.ts","status":"active","summary":"Return replacement object values instead of writing input fields in result-returning functions.","test":"packages/typescript/tests/rules/no-mutating-input-with-result.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/api.test.ts","source":"import {test,expect} from 'vitest'; test('reads', async () => { const response = await fetch('/versions'); expect(response.status).toBe(200); });"}],"fixedFiles":[],"focusPath":"src/api.test.ts","id":"expects-intended-status","outcome":"accept","scenarioId":"primary","title":"Assert the API's intended result"},{"expectedCount":1,"files":[{"path":"src/api.test.ts","source":"import {test,expect} from 'vitest'; test('reads', async () => { const response = await fetch('/versions'); expect(response.status).not.toBe(500); });"}],"fixedFiles":[],"focusPath":"src/api.test.ts","id":"only-excludes-server-error","outcome":"reject","scenarioId":"primary","title":"A missing route can still pass this test"}],"filePatterns":[],"id":"no-negative-only-http-status-assertion","key":"oxlint:no-negative-only-http-status-assertion","languages":["typescript"],"limitations":["Checks direct Vitest/Jest/Bun/Playwright expect assertions in recognized test callbacks on a const response obtained from native fetch, runtime-imported undici/node-fetch, or unshadowed new Response. Direct global Response parameter annotations and stable const response aliases are also resolved. A server-error exclusion or status below 500 needs an accompanying unconditional exact HTTP status assertion for the same binding.","Dynamic clients, mutable response bindings, helper functions, nested callbacks, status constants and nonliteral expected statuses are excluded. No HTTP intent is inferred from a variable name alone.","An exact expected status is never guessed or inserted automatically. Nonliteral and richer response oracles require review outside this narrow detector."],"messageIds":["expectedStatus"],"optionsSchema":null,"rationale":"A response can avoid status 500 while still fail with an unrelated 400, 401 or 404, so the test can pass without exercising the intended behavior.","references":[],"remediation":"Assert the expected response status or a deliberately bounded success/error contract. Preserve negative assertions that supplement an exact assertion for the same response.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-negative-only-http-status-assertion.ts","status":"active","summary":"Assert the intended HTTP status instead of only excluding a server error.","test":"packages/typescript/tests/rules/no-negative-only-http-status-assertion.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/page.ts","source":"export function page(params: URLSearchParams): number {\n  return parseInt(params.get(\"page\") ?? \"1\", 10);\n}\n"}],"fixedFiles":[],"focusPath":"src/page.ts","id":"parse-int-page","outcome":"reject","scenarioId":"primary","title":"Do not coerce a search parameter with parseInt"},{"expectedCount":0,"files":[{"path":"src/page.ts","source":"declare const z: { coerce: { number(): { parse(value: unknown): number } } };\nexport function page(params: URLSearchParams): number {\n  return z.coerce.number().parse(params.get(\"page\") ?? undefined);\n}\n"}],"fixedFiles":[],"focusPath":"src/page.ts","id":"schema-parsed-page","outcome":"accept","scenarioId":"primary","title":"Parse the page number with a schema"}],"filePatterns":[],"id":"no-numeric-search-param-coercion","key":"oxlint:no-numeric-search-param-coercion","languages":["typescript"],"limitations":["Requires type information: only the `get` result of a value typed as URLSearchParams or ReadonlyURLSearchParams is recognized.","Search parameter objects passed as page props, values copied into other variables, and unary `+` coercion are not followed."],"messageIds":["noNumericSearchParamCoercion"],"optionsSchema":null,"rationale":"parseInt, parseFloat and Number accept a missing or malformed query value and return NaN or a truncated number (`parseInt(\"10abc\")` is 10), which then reaches queries and pagination unchecked.","references":[],"remediation":"Parse the value with a schema that states the contract, for example `z.coerce.number().int().positive().default(10).parse(params.get(\"page\") ?? undefined)`.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-numeric-search-param-coercion.ts","status":"active","summary":"Parse numeric URL search parameters with a schema instead of parseInt or Number.","test":"packages/typescript/tests/rules/no-numeric-search-param-coercion.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/page.ts","source":"db.query(\"SELECT id FROM runs /* OFF\\x53ET 1 */\");"}],"fixedFiles":[],"focusPath":"src/page.ts","id":"decoded-comment-data","outcome":"accept","scenarioId":"native-decoded-sql","title":"Ignore native-decoded comment data"},{"expectedCount":1,"files":[{"path":"src/page.ts","source":"db.query(\"SELECT id FROM runs ORDER BY id LIMIT 1 OFF\\u0053ET 1\");"}],"fixedFiles":[],"focusPath":"src/page.ts","id":"decoded-query","outcome":"reject","scenarioId":"native-decoded-sql","title":"Inspect native-decoded query text"},{"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":"oxlint:no-offset-pagination","languages":["typescript"],"limitations":["A SELECT/FROM query shape or adjacent LIMIT/OFFSET fragment is required. Isolated OFFSET fragments and test files are excluded; this lexical context does not prove a database execution sink. Performance and concurrent-write behavior depend on indexes, ordering, isolation, and dialect; bounded pages and random page access can justify OFFSET."],"messageIds":["noOffsetPagination"],"optionsSchema":null,"rationale":"Offset pagination scans skipped rows and shifts page boundaries under concurrent writes.","references":[],"remediation":"Consider a keyset cursor that preserves the query's complete ordering, tie-breakers, and filters.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-offset-pagination.ts","status":"active","summary":"Prefer keyset pagination for embedded SQL queries using OFFSET.","test":"packages/typescript/tests/rules/no-offset-pagination.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/report.ts","source":"// eslint-disable-next-line no-console -- the CLI prints its report\nconsole.log(report);\n"}],"fixedFiles":[],"focusPath":"src/report.ts","id":"eslint-next-line","outcome":"accept","scenarioId":"primary","title":"The ESLint spelling"},{"expectedCount":1,"files":[{"path":"src/report.ts","source":"// oxlint-disable-next-line no-console -- the CLI prints its report\nconsole.log(report);\n"}],"fixedFiles":[{"path":"src/report.ts","source":"// eslint-disable-next-line no-console -- the CLI prints its report\nconsole.log(report);\n"}],"focusPath":"src/report.ts","id":"oxlint-next-line","outcome":"reject","scenarioId":"primary","title":"An Oxlint-spelled directive"}],"filePatterns":[],"id":"no-oxlint-directive","key":"oxlint:no-oxlint-directive","languages":["typescript"],"limitations":["Only directive comments that start with `oxlint-disable`, `oxlint-disable-next-line`, `oxlint-disable-line` or `oxlint-enable` are reported. A directive that disables this rule itself, or every rule, hides its own report; the Standards runtime checks those separately."],"messageIds":["oxlintDirective"],"optionsSchema":null,"rationale":"Oxlint honors both `oxlint-*` and `eslint-*` directive comments. With two spellings, finding every suppression in a repository takes two searches, and the same directive reads differently from file to file. One spelling keeps suppressions findable and reviewable alike; `eslint-*` is the spelling existing code already uses.","references":["https://oxc.rs/docs/guide/usage/linter/ignore-comments"],"remediation":"Rename the directive prefix from `oxlint-` to `eslint-`; the rule list and the description stay as they are.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-oxlint-directive.ts","status":"active","summary":"Write every suppression directive with the `eslint-*` spelling.","test":"packages/typescript/tests/rules/no-oxlint-directive.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/filter.ts","source":"import { z } from \"zod\"; export const UuidSchema: z.ZodUUID = z.uuid(); export function isUuid(value: unknown): boolean { return UuidSchema.safeParse(value).success; }"}],"fixedFiles":[],"focusPath":"src/filter.ts","id":"boolean-uuid-filter","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"accept","scenarioId":"uuid","title":"Keep a UUID value filter boolean"},{"expectedCount":0,"files":[{"path":"src/filter.ts","source":"function isPositive(value: number): boolean { return value > 0; }"}],"fixedFiles":[],"focusPath":"src/filter.ts","id":"boolean-value-filter","outcome":"accept","scenarioId":"primary","title":"Keep an intentional value filter boolean"},{"expectedCount":1,"files":[{"path":"src/guard.ts","source":"function isPositive(value: unknown): value is number { return typeof value === \"number\" && value > 0; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"partial-number-predicate","outcome":"reject","scenarioId":"primary","title":"Zero remains a number when the predicate returns false"},{"expectedCount":1,"files":[{"path":"src/guard.ts","source":"import { z } from \"zod\"; export const UuidSchema: z.ZodUUID = z.uuid(); export function isUuid(value: unknown): value is string { return UuidSchema.safeParse(value).success; }"}],"fixedFiles":[],"focusPath":"src/guard.ts","id":"partial-uuid-predicate","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"reject","scenarioId":"uuid","title":"A rejected UUID candidate can still be a string"}],"filePatterns":[],"id":"no-partial-type-predicate","key":"oxlint:no-partial-type-predicate","languages":["typescript"],"limitations":["Automatic fixes replace the unsound predicate with boolean only for an unused private function declaration without overloads, exports, eval, comments in the predicate, or declaration-file ownership. Runtime checks are preserved; observed callable contracts remain manual.","Requires type information and a non-generic, non-async function with one directly annotated parameter, an explicit broad string/number/bigint/boolean predicate and a single return. Literal, branded, structural, multi-primitive union, assertion, method and overloaded contracts are excluded; contextually typed callbacks are preserved.","Recognizes double-negation truthiness, a boolean parameter return or one same-kind literal comparison, optionally after a matching typeof equality. Without a typeof check, the input must already belong to the target primitive, except for double negation. String length comparisons use only concrete witnesses up to 256 characters. Arithmetic, dynamic bounds and complex boolean conditions are not inferred.","A broad string predicate returning local Schema.safeParse(parameter).success is also checked for a locally unchanged official z.uuid() or z.string().uuid() schema. The checker must resolve the namespace to installed node_modules/zod declaration files; local path aliases and opaque module resolution abstain. The const schema permits local parse/type/direct-export reads; every safeParse call must have exactly one direct argument, without options or spreads. Local aliases, method writes and opaque consumers abstain. Direct export alone is not mutation, but external consumer monkey-patching is not proved absent. Other value uses of its imported Zod namespace cause abstention. Transforms, brands, custom schemas, imported schemas and dynamic parse calls are excluded. Visible RegExp capabilities, regex literals or dynamic evaluation invalidate this initial native-pattern proof; external runtime monkeypatches are not inferred. The same unused-private-declaration fix boundary applies; observed or exported predicates remain manual.","A concrete member of the promised broad primitive must demonstrably return false. Numeric witnesses must also be assignable as literals to the declared input: broad number assignability alone does not prove membership in a numeric enum. Compiler-invalid target/input relations, any/never/generic inputs and opaque implementations abstain. This rule does not discourage total boundary guards or infer branded refinements. Generated files are excluded; tests remain checked."],"messageIds":["partialPredicate"],"optionsSchema":null,"rationale":"Type predicates narrow both branches, and TypeScript trusts their implementation. A positive-number or non-empty-string predicate can return false for a value belonging to its promised type, causing callers to treat that value as a different type.","references":["https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-5.html#inferred-type-predicates"],"remediation":"Use a boolean return for an intentional value filter, or recognize every member of the promised type. Preserve required callback and public contracts; a refined domain may need a distinct validated type. Recheck false-branch callers when changing a predicate.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-partial-type-predicate.ts","status":"active","summary":"Do not give a partial primitive filter an explicit type predicate that excludes valid values in the false branch.","test":"packages/typescript/tests/rules/no-partial-type-predicate.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/view.tsx","source":"<form><input type=\"password\" name=\"password\" /></form>"}],"fixedFiles":[],"focusPath":"src/view.tsx","id":"default-get-password","outcome":"reject","scenarioId":"primary","title":"Review password serialization into the URL"},{"expectedCount":0,"files":[{"path":"src/view.tsx","source":"<form method=\"post\"><input type=\"password\" name=\"password\" /></form>"}],"fixedFiles":[],"focusPath":"src/view.tsx","id":"post-password","outcome":"accept","scenarioId":"primary","title":"Submit the password with POST"}],"filePatterns":[],"id":"no-password-get-form","key":"oxlint:no-password-get-form","languages":["typescript"],"limitations":["Only native form elements and direct intrinsic/fragment descendants are proved. Custom components, expressions, external form ownership, disabled fieldsets and unknown spreads are preserved.","Forms with custom submission handlers, function or dynamic actions, or dynamic methods are left to review. React function actions submit using POST even when the method is omitted.","Unnamed, empty-name and disabled password controls are not successful controls. The detector reports once per offending form and does not infer sensitive data from field names."],"messageIds":["passwordGet"],"optionsSchema":null,"rationale":"Native GET submission serializes enabled, named password controls into the URL, exposing credentials through browser history and request logs. GET is also the default for missing or invalid method attributes.","references":["https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/forms/forms.md","https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/forms/autofill-sign-in-form.md"],"remediation":"Review the destination endpoint and submit credentials with POST. Keep server-side validation, HTTPS and the appropriate anti-CSRF protections; this rule only checks the native form serialization boundary.","requiresTypeInformation":false,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-password-get-form.ts","status":"active","summary":"Keep named password controls out of native GET form submissions.","test":"packages/typescript/tests/rules/no-password-get-form.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/keys.ts","source":"const userKeys = { all: ['users'], detail: (id: string) => [...userKeys.all, id] as const };"}],"fixedFiles":[],"focusPath":"src/keys.ts","id":"mutable-query-key-object","outcome":"reject","scenarioId":"query-key-const-anchor","title":"Keep a mutable tuple-return object checked"},{"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":0,"files":[{"path":"src/keys.ts","source":"const userKeys = { all: ['users'], detail: (id: string) => [...userKeys.all, id] as const } as const;"}],"fixedFiles":[],"focusPath":"src/keys.ts","id":"query-key-outer-const","outcome":"accept","scenarioId":"query-key-const-anchor","title":"Inherit the query-key anchor const context"},{"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":"oxlint: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.","Automatic edits only migrate a private zero-argument ordinary helper returning two labeled primitive literal fields, with one exported zero-argument ordinary caller that immediately destructures those same names and returns the already declared matching public object contract. The complete module must contain only those two functions without comments or other references. Existing labels and the public object type supply every field name; primitive evaluation order and fresh public results are retained. This subset assumes the canonical Array iterator. Public tuple APIs, aliases, callbacks, defaults, generics, rest/optional fields, unsafe property names and other callers remain manual."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"security","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-production-browser-source-maps","languages":["typescript"],"limitations":["Automatic boolean changes are withheld when the module contains explicit TypeScript or JSDoc contracts; those contracts can constrain an aliased property to literal true. Plain objects and as const inference remain fixable.","Only a literal true in the effective property of a directly exported object, unescaped const alias, or isolated module.exports object is reported. Wrappers, factories, spreads, dynamic computed keys and mutations are 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.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/env.ts","source":"import { z } from \"zod\";\nconst SettingsSchema = z.object({ API_KEY: z.string().min(1) });\nexport const settings = await SettingsSchema.parseAsync(process.env);"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"async-settings-awaited","outcome":"accept","scenarioId":"async-settings","title":"Await parsing before exporting the shared settings object"},{"expectedCount":1,"files":[{"path":"src/env.ts","source":"import { z } from \"zod\";\nconst SettingsSchema = z.object({ API_KEY: z.string().min(1) });\nexport const settings = SettingsSchema.parseAsync(process.env);"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"async-settings-promise","outcome":"reject","scenarioId":"async-settings","title":"An unfinished parser promise is not startup settings"},{"expectedCount":0,"files":[{"path":"src/env.ts","source":"import { z } from \"zod\";\nconst SettingsSchema = z.object({ API_KEY: z.string().min(1) });\nconst createEnv = input => input;\nexport const settings = SettingsSchema.parse(createEnv(process.env));"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"identity-loader-before-real-parser","outcome":"accept","scenarioId":"closed-identity-adapter","title":"An identity adapter can forward into the actual Zod parser"},{"expectedCount":1,"files":[{"path":"src/env.ts","source":"const createEnv = input => input;\nexport const settings = createEnv(process.env);"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"identity-loader-without-parser","outcome":"reject","scenarioId":"closed-identity-adapter","title":"A local identity adapter does not validate environment input"},{"expectedCount":0,"files":[{"path":"src/env.ts","source":"import { z } from \"zod\";\nconst SettingsSchema = z.object({ API_ORIGIN: z.url() });\nexport const settings = SettingsSchema.parse(JSON.parse(process.env.SETTINGS_JSON));"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"json-decoding-then-validation","outcome":"accept","scenarioId":"json-settings","title":"Validate decoded environment JSON with the actual schema"},{"expectedCount":1,"files":[{"path":"src/env.ts","source":"export const settings = JSON.parse(process.env.SETTINGS_JSON);"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"json-decoding-without-validation","outcome":"reject","scenarioId":"json-settings","title":"JSON decoding does not validate the settings contract"},{"expectedCount":1,"files":[{"path":"src/database.ts","source":"import { env as raw } from \"node:process\";\nexport const databaseUrl = raw.DATABASE_URL;"}],"fixedFiles":[],"focusPath":"src/database.ts","id":"native-env-import-read","outcome":"reject","scenarioId":"native-env-import","title":"Native env import aliases still need validation"},{"expectedCount":0,"files":[{"path":"src/database.ts","source":"import { settings } from \"./env.js\";\nexport const databaseUrl = settings.DATABASE_URL;"},{"path":"src/env.ts","source":"import { env as raw } from \"node:process\";\nimport { z } from \"zod\";\nconst SettingsSchema = z.object({ DATABASE_URL: z.url() });\nexport const settings = SettingsSchema.parse(raw);"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"native-env-import-validated","outcome":"accept","scenarioId":"native-env-import","title":"Parse the imported environment once and share settings"},{"expectedCount":1,"files":[{"path":"src/env.ts","source":"import { z } from 'zod';\nconst SettingsSchema = z.object({ DATABASE_URL: z.url() });\nexport const settings = { DATABASE_URL: process.env.DATABASE_URL };"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"raw-environment-read","outcome":"reject","scenarioId":"primary","title":"Declaring a schema does not validate a raw export"},{"expectedCount":1,"files":[{"path":"src/env.ts","source":"import { z } from \"zod\";\nconst SettingsSchema = z.object({ API_ORIGIN: z.url() });\nexport const settings = SettingsSchema.parse(process.env.LOAD_REMOTE ? fetch(\"https://example.test/config\") : {});"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"raw-input-controls-effects","outcome":"reject","scenarioId":"conditional-settings","title":"A raw conditional input is not the value reaching the parser"},{"expectedCount":1,"files":[{"path":"src/env.ts","source":"import { env as raw } from \"node:process\";\nimport { z } from \"zod\";\nconst SettingsSchema = z.object({ API_KEY: z.string().min(1) });\nconst { API_KEY, API_ORIGIN } = raw;\nexport const settings = SettingsSchema.parse({ API_KEY });\nexport const unparsedOrigin = API_ORIGIN;"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"selected-fields-raw-escape","outcome":"reject","scenarioId":"selected-settings-fields","title":"Parsing one selected field does not validate another raw export"},{"expectedCount":0,"files":[{"path":"src/env.ts","source":"import { env as raw } from \"node:process\";\nimport { z } from \"zod\";\nconst SettingsSchema = z.object({ API_KEY: z.string().min(1), API_ORIGIN: z.url() });\nconst { API_KEY, API_ORIGIN } = raw;\nexport const settings = SettingsSchema.parse({ API_KEY, API_ORIGIN });"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"selected-fields-validated","outcome":"accept","scenarioId":"selected-settings-fields","title":"Validate every selected ordinary field and reuse the parsed result"},{"expectedCount":0,"files":[{"path":"src/database.ts","source":"import { settings } from './env.js';\nexport const primaryUrl = settings.DATABASE_URL;\nexport const replicaUrl = settings.DATABASE_URL;"},{"path":"src/env.ts","source":"import { z } from 'zod';\nconst SettingsSchema = z.object({ DATABASE_URL: z.url() });\nexport const settings = SettingsSchema.parse(process.env);"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"validated-environment","outcome":"accept","scenarioId":"primary","title":"Parse once and reuse one settings object"},{"expectedCount":0,"files":[{"path":"src/env.ts","source":"import { z } from \"zod\";\nconst SettingsSchema = z.object({ LOAD_REMOTE: z.enum([\"0\", \"1\"]).transform(value => value === \"1\") });\nexport const settings = SettingsSchema.parse(process.env);\nif (settings.LOAD_REMOTE) await fetch(\"https://example.test/config\");"}],"fixedFiles":[],"focusPath":"src/env.ts","id":"validated-input-controls-effects","outcome":"accept","scenarioId":"conditional-settings","title":"Parse the control setting before remote effects"}],"filePatterns":[],"id":"no-raw-env","key":"oxlint:no-raw-env","languages":["typescript"],"limitations":["Host markers, assignment targets, tests, ordinary scripts and build config retain their policy exemptions. Boundary-named modules, including env.mjs and settings.ts, are checked. Native named process env imports and simple const { env } = process aliases follow the same read policy; foreign, type-only, shadowed and mutable sources are not inferred.","Each raw value must reach a parse/safeParse argument or supported environment loader, directly or through stable nonexported const aliases whose every use reaches validation. Directly awaited parseAsync/safeParseAsync calls also count; unawaited promises or later await-through-alias flows do not. Conditional tests and logical-and conditions are control inputs, not the parsed result; value arms and existing nullish/logical-or defaults remain supported.","Simple nonexported const object patterns may select static keys into plain identifier bindings when every ordinary read of each selected field reaches validation. Type-only references are ignored. Native whole-environment host marker keys retain their exemptions, including through stable aliases; an arbitrary raw field with a marker-shaped property does not. Unused, unparsed, escaping or later-written ordinary fields fail the proof. Opaque computed keys, defaults, rest and nested patterns remain unsupported.","Renamed value imports of createEnv from @t3-oss/env-core or @t3-oss/env-nextjs retain loader ownership. Existing unresolved project parse/safeParse and legacy createEnv protocols remain accepted; this is not a proof of their implementation. A narrowly proven private identity adapter only forwards its input: that input must still reach an actual parser.","Identity-adapter detection is limited to an explicit module, no namespace or runtime eval reference, one stable nonexported binding and a synchronous function whose sole plain identifier parameter is returned unchanged. An object may contain only one or two unique own parse/safeParse identity methods, with every ordinary receiver reference a direct method call. Exported/mutable/escaped receivers, spreads, getters, duplicate/opaque keys, extra methods, imported loaders, real Zod wrappers and optional/awaited adapter-result ambiguity retain the existing protocol. The proof stops after 128 binding references, 512 module references or 32 enclosing AST parents; it adds no whole-program library-provenance requirement.","Only unshadowed Number/String/Boolean/BigInt calls and one-argument native JSON decoding are traversed as preprocessing. JSON.parse alone does not validate settings: its decoded result must itself reach the parser. JSON revivers and arbitrary calls cannot consume raw settings under this preprocessing exemption.","This is bounded same-file syntax analysis (24 flow steps, 128 selected fields per pattern, 128 reads per binding and 512 total expression/field visits per raw read; whole-environment host-marker provenance follows at most 16 aliases); flows exceeding these budgets are unreported. It does not prove a parser's implementation, cross-file schemas, schema completeness, safeParse failure handling or a project-wide singleton. Handle safeParse failure before exposing result.data; prefer throwing parse at startup. Nested component settings and separate server/client runtime boundaries remain valid.","Assignment-target exemptions also include compound writes that read the previous value. Whole-environment spreads outside boundary modules retain the existing forwarding exemption; a spread inside a boundary must also reach its parser. No automatic rewrite."],"messageIds":["noRawEnv"],"optionsSchema":null,"rationale":"Raw environment reads are untyped and defer invalid configuration failures until use. One shared validated settings object gives consumers a consistent startup configuration.","references":[],"remediation":"Define a Zod settings schema, call SettingsSchema.parse(process.env) once at startup, and export or inject that single parsed settings object. Use Pydantic/Pydantic Settings for the equivalent Python boundary. Required values must fail before remote effects; preserve independent security policy and existing optional defaults.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","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":"oxlint:no-raw-fetch-outside-clients","languages":["typescript"],"limitations":["Tests, client-layer paths, constructed handoffs, and pre-signed URL transfers are excluded. Configure the same literal Next.js basePath here and on prefer-server-actions so one rule owns each internal mutation.","Effect and pre-signed-transfer exemptions use recognized syntax and naming conventions, not complete React or URL provenance. Those conservative exclusions are recall limitations, not evidence that every excluded request satisfies transport policy.","Unshadowed native members and bounded stable const aliases or single-property destructuring are recognized. Visible writes, callable escapes, whole-global escapes and mutation capabilities anywhere in the file invalidate that proof, including earlier captures. Known unrelated global member writes and typeof global checks do not. Static constructor members and destructuring projections invalidate the proof; indirect reflection, external monkeypatches and arbitrary forwarding functions are not inferred."],"messageIds":["rawFetch"],"optionsSchema":{"additionalProperties":false,"properties":{"allow":{"description":"Regular-expression sources matched against the filename. Replaces the defaults.","items":{"type":"string"},"type":"array"},"basePath":{"description":"Static application URL base path without query strings, fragments or parent segments.","pattern":"^/(?!$)(?!.*[?#])(?!(?:.*/)?\\.\\.?(?:/|$))(?:[^/]+/)*[^/]+$","type":"string"}},"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/status.tsx","source":"export function Status({ status }: { status: string }) { return <p>{status}</p>; }"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"preserve\"},\"include\":[\"src/**/*\"]}"}],"fixedFiles":[],"focusPath":"src/status.tsx","id":"current-prop","outcome":"accept","scenarioId":"primary","title":"Render the current prop directly"},{"expectedCount":1,"files":[{"path":"src/status.tsx","source":"import { useState } from 'react'; export function Status({ status }: { status: string }) { const [savedStatus] = useState(status); return <p>{savedStatus}</p>; }"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"preserve\"},\"include\":[\"src/**/*\"]}"}],"fixedFiles":[],"focusPath":"src/status.tsx","id":"frozen-prop","outcome":"reject","scenarioId":"primary","title":"A prop snapshot has no updater"}],"filePatterns":[],"id":"no-readonly-prop-state","key":"oxlint:no-readonly-prop-state","languages":["typescript"],"limitations":["Advisory only: initial snapshots can be intentional. There is no automatic fix because replacing state with current props changes how later updates behave.","Recognizes direct React useState imports and default/namespace member calls, including renamed imports and erased wrappers around imported namespaces, through lexical binding identity. Checks a direct array destructure in a function containing its own JSX and a direct first-parameter property path, or a zero-argument synchronous lazy initializer returning that path.","Eager initializers require parser-owned TypeScript services and primitive types, including unions composed entirely of primitives. Object and void types can hide callable values through structural assignment or ignored return values, so eager nonprimitive values, Any, unknown and unresolved generics are excluded. React executes function arguments as lazy initializers. Explicit zero-argument lazy callbacks returning a prop directly remain supported for all value types without type services. Opaque casts cannot prove runtime identity; lifecycle changes stay manual.","Direct writes to the requested imported namespace member exclude its hook calls. Unknown computed writes exclude all namespace hooks; unrelated member writes and a hook function's own properties do not replace the callable. Reflective mutation and external API patches are not modeled.","An updater read, escape, reassignment or nontrivial destructuring excludes the finding. Unused state values remain owned by unused-variable checks. Files containing direct eval are excluded because it can observe or change lexical bindings. Indirect eval and properties named eval do not establish lexical access. Opaque initialization, property aliases, optional/dynamic paths, rest/default bindings, non-JSX hooks, generated source and tests are outside scope. This does not ban interactive useState or replace the upstream set-state-in-effect rule."],"messageIds":["readonlyPropState"],"optionsSchema":null,"rationale":"State initialized from props retains the first render's value. Without an observed updater, later props can leave the displayed value stale and add a second source of truth.","references":["https://react.dev/learn/choosing-the-state-structure","https://react.dev/reference/react/useState","https://react.dev/reference/eslint-plugin-react-hooks/lints/set-state-in-effect"],"remediation":"Derive the value from current props when it should stay current. For an intentional mount-time snapshot, retain that lifecycle explicitly, for example with a ref; independently editable drafts keep their updater. Review the intended lifecycle before changing behavior.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-readonly-prop-state.ts","status":"active","summary":"Review props copied into state without a usable updater.","test":"packages/typescript/tests/rules/no-readonly-prop-state.test.ts"},{"aliases":[],"autofix":"safe","category":"performance","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/reports.ts","source":"declare const pages: string[][];\nconst rows = pages.reduce<string[]>((acc, page) => acc.concat(page), []);"}],"fixedFiles":[],"focusPath":"src/reports.ts","id":"copy-rows","outcome":"reject","scenarioId":"primary","title":"Repeated concatenation copies prior report rows"},{"expectedCount":0,"files":[{"path":"src/reports.ts","source":"declare const pages: string[][];\nconst rows = pages.flatMap(page => page);"}],"fixedFiles":[],"focusPath":"src/reports.ts","id":"flatten-rows","outcome":"accept","scenarioId":"primary","title":"Flatten report rows without copying the accumulated prefix"},{"expectedCount":0,"files":[{"path":"src/example.ts","source":"declare const pages: string[][]; pages.flatMap(page => page);"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"reducer-aliases-after","outcome":"accept","scenarioId":"reducer-aliases","title":"Retain fresh seed and accumulator identities — after"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"declare const pages: string[][]; const seed: string[] = []; pages.reduce((acc, page) => { const previous = acc; return [...previous, ...page]; }, seed);"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"reducer-aliases-before","outcome":"reject","scenarioId":"reducer-aliases","title":"Retain fresh seed and accumulator identities — before"}],"filePatterns":[],"id":"no-reduce-accumulator-copy","key":"oxlint:no-reduce-accumulator-copy","languages":["typescript"],"limitations":["Requires type information proving the built-in Array or ReadonlyArray reduce/reduceRight method, an inline callback, and a fresh empty array or object-literal seed, directly or through single-use stable const aliases.","Inspects direct accumulator spreads, Object.assign with a fresh object target, Array.from, and concat/slice/toSpliced/toSorted/toReversed/with calls. Stable callback-local const accumulator aliases are resolved. Named callbacks, mutable/escaping seeds, nested functions, and loops remain outside this rule.","A copy is evidence for review, not proof of growth or measured latency. Automatic edits only remove an exact identity reducer over a fresh nonempty literal-string array in a sole exported zero-argument function with an explicit string[] return. The direct literal return remains fresh on every call. Snapshots, aliases, callbacks, nonliteral values, reverse reduction, imports, comments and other control flow remain manual."],"messageIds":["copy"],"optionsSchema":null,"rationale":"Copying a growing accumulator at every step can make collection quadratic instead of linear in its output size.","references":["https://github.com/dmmulroy/anti-slop/blob/13909c9e07590f5a85d93c62712259fe14a883f7/src/rules/no-reduce-accumulator-copy.ts"],"remediation":"Use map or flatMap where their semantics match, or append into a fresh locally owned accumulator. Preserve retained snapshots and shared state.","requiresTypeInformation":true,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-reduce-accumulator-copy.ts","status":"active","summary":"Review copies of the accumulated collection inside a built-in reduce callback.","test":"packages/typescript/tests/rules/no-reduce-accumulator-copy.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; function label(value: Dog | Animal): string {return value.name;} label(dog);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; function label(value: Animal): string {return value.name;} label(dog);"}],"focusPath":"src/case.ts","id":"covered-alternative","outcome":"reject","scenarioId":"primary","title":"The private consumer needs the covering Animal contract"},{"expectedCount":1,"files":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; function formatName(value: string): string {return '[' + value + ']';} function label(value: Dog | Animal): string {return formatName(value.name);} label(dog);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; function formatName(value: string): string {return '[' + value + ']';} function label(value: Animal): string {return formatName(value.name);} label(dog);"}],"focusPath":"src/case.ts","id":"local-scalar-helper","outcome":"reject","scenarioId":"local-scalar-helper","title":"A local helper receives only the required name"},{"expectedCount":1,"files":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string | null} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; function label(value: Dog | Animal): string {return value.name ?? '';} label(dog);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string | null} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; function label(value: Animal): string {return value.name ?? '';} label(dog);"}],"focusPath":"src/case.ts","id":"nullable-covered-alternative","outcome":"reject","scenarioId":"nullable-readonly-contract","title":"A nullable name retains the covering Animal contract"},{"expectedCount":0,"files":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string | null} interface Dog extends Animal {readonly breed: 'Collie'} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; function label(value: Dog | Animal): string {return value.name ?? '';} label(dog);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[],"focusPath":"src/case.ts","id":"nullable-domain-contract","outcome":"accept","scenarioId":"nullable-readonly-contract","title":"A literal breed keeps the authored domain contract"},{"expectedCount":1,"files":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed?: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; function label(value: Dog | Animal): string {return value.name;} label(dog);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed?: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; function label(value: Animal): string {return value.name;} label(dog);"}],"focusPath":"src/case.ts","id":"optional-covered-alternative","outcome":"reject","scenarioId":"optional-readonly-contract","title":"An unread optional breed retains the smaller Animal surface"},{"expectedCount":0,"files":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed?: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; export function label(value: Dog | Animal): string {return value.name;} label(dog);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[],"focusPath":"src/case.ts","id":"optional-public-contract","outcome":"accept","scenarioId":"optional-readonly-contract","title":"A public optional union retains its observable alternatives"},{"expectedCount":0,"files":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; export function label(value: Dog | Animal): string {return value.name;} label(dog);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[],"focusPath":"src/case.ts","id":"public-contract","outcome":"accept","scenarioId":"primary","title":"A public union retains its observable alternatives"},{"expectedCount":0,"files":[{"path":"src/case.ts","source":"export {}; interface Animal {readonly name: string} interface Dog extends Animal {readonly breed: string} export type KeptDog = Dog; const dog: Dog = {name: \"Ada\", breed: \"Collie\"}; function formatAnimal(value: Animal): string {return value.name;} function label(value: Dog | Animal): string {return formatAnimal(value);} label(dog);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[],"focusPath":"src/case.ts","id":"whole-object-helper","outcome":"accept","scenarioId":"local-scalar-helper","title":"Passing the whole object remains outside the scalar helper proof"}],"filePatterns":[],"id":"no-redundant-consumer-union","key":"oxlint:no-redundant-consumer-union","languages":["typescript"],"limitations":["Automatic fixes additionally require every existing identifier caller to be natively assignable to the original contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Removed named types/imports must retain an independent top-level exported type-alias or direct type-export read. Other removable annotations/assertions, the covering annotation and own-definition/self references cannot keep one another live; otherwise simultaneous fixes can introduce unused-type errors. The exact authored covering type and terminator token sequence are retained; edits that would merge distinct CR and LF terminators remain manual; no producer, call or function body is rewritten.","Only an inline binary parameter union in a strict module's top-level private scalar consumer is supported. Exactly one parameter, an explicit broad primitive return annotation, a single supported returned scalar expression and direct identifier-argument callers are required. Type queries or type-only exports of the callable, public APIs, callbacks, contextual signatures, overloads, generics, decorators, async functions, direct eval and fresh-object callers prevent findings.","Scalar values may pass through one implemented same-file top-level function or raw const-arrow helper with an explicit scalar return annotation and the exact native resolved signature. Every argument must already satisfy the scalar read view; the containing source must pass the existing native checked-source proof. Imported, ambient, aliased, overloaded, generic or contextually typed helpers, member calls, optional calls, spread arguments and whole-object arguments remain unsupported. Helper permission does not add auxiliary consumer parameters or replace the required object-field read anchor. Helper bodies need not be pure because no call or body is rewritten.","Both alternatives must have complete ordinary finite structural surfaces consisting only of explicitly readonly broad string, number, boolean or bigint properties, optionally paired directly with null or undefined in an exact two-part union. Required and optional nullable fields may be read under the same native coverage proof. Every field must have one actual declaration origin throughout each alternative's inheritance/intersection graph. Shared ancestors retain that origin; repeated merged fields, explicit overrides and independently declared overlapping fields abstain, including valid compatible declarations. This excludes recovered conflicting properties without claiming whole-source health. Aliases, imports, re-exports and ordinary inheritance are resolved by the parser-owned TypeScript checker. Any, unknown, missing imports, open/generic/mapped/indexed types, classes, brands, literal discriminants, methods, properties with scalar type aliases, parenthesized or more complex property unions, callbacks and recursive or nested object fields are unsupported.","Reports proper native assignability, where covered is assignable to covering and the reverse is false, or mutual native assignability with a strict smaller declared covering surface and only optional covered-only members. Every covering member, including optional members, must exist in the covered type's enumerated declared surface. Otherwise an existing identifier may carry an incompatible extra field and be rejected after simplification; implicit Object members containing Any cannot replace explicit declared coverage or the native Optional requirement. The covering type must declare every read field and a read required field; an all-optional reduced contract can reject existing callers. Identical alternatives and primitive/top/bottom redundancy remain upstream-owned; equal-key and incomparable mutually assignable structures are excluded. The mutual case additionally requires inline contracts or locally bound interfaces/type aliases whose alias and heritage references can be traced through the parser-owned type scope. Merged interface fragments must all resolve to the reference's same native symbol; mixed exported/local recovery abstains. Imported, global and qualified-namespace references in that path abstain; proper one-way coverage retains its existing alias/import support.","General union simplification can change Extract, conditional types, in-narrowing, fresh-literal checking and public presentation. Those contexts are excluded even when ordinary assignment succeeds. Flat destructuring counts every bound field as read; whole-object escape, mutation, unproved helper calls, closures, reflection, serialization, dynamic reads, implicit Object capabilities and JSX prevent findings. Imported Python protocol assignability is not approximated by this rule."],"messageIds":["redundant"],"optionsSchema":null,"rationale":"A private consumer can advertise a redundant object alternative even though native assignability proves that the other alternative accepts every covered value. Dog | Animal keeps Animal; requiring Dog would unnecessarily restrict callers.","references":[],"remediation":"Review retaining the covering alternative, or revise the alternatives if distinct cases were intended. Preserve producer types and runtime objects. The safe fix edits only a proven private parameter annotation; public APIs, producer types and runtime objects remain unchanged.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-redundant-consumer-union.ts","status":"active","summary":"Keep the covering object contract in a private scalar consumer union.","test":"packages/typescript/tests/rules/no-redundant-consumer-union.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/request.ts","source":"export {};\nfunction request(value: string) {\n  const accountId = value;\n  return { accountId };\n}\n"}],"fixedFiles":[{"path":"src/request.ts","source":"export {};\nfunction request(value: string) {\n\n  return { accountId: value };\n}\n"}],"focusPath":"src/request.ts","id":"closed-primitive-parameter-alias","outcome":"reject","scenarioId":"closed-primitive-parameter-alias","title":"The closed primitive return preserves the data-field contract"},{"expectedCount":0,"files":[{"path":"src/request.ts","source":"export {};\nfunction request(value: string) {\n\n  return { accountId: value };\n}\n"}],"fixedFiles":[],"focusPath":"src/request.ts","id":"closed-primitive-parameter-alias-fixed","outcome":"accept","scenarioId":"closed-primitive-parameter-alias","title":"The direct data field needs no local alias"},{"expectedCount":0,"files":[{"path":"src/request.ts","source":"function request(value: string) { return { accountId: value }; }"}],"fixedFiles":[],"focusPath":"src/request.ts","id":"direct-property","outcome":"accept","scenarioId":"primary","title":"Use the original binding"},{"expectedCount":1,"files":[{"path":"src/request.ts","source":"function request(value: string) { const accountId = value; return { accountId }; }"}],"fixedFiles":[],"focusPath":"src/request.ts","id":"redundant-alias","outcome":"reject","scenarioId":"primary","title":"The property already supplies the name"}],"filePatterns":[],"id":"no-redundant-local-alias","key":"oxlint:no-redundant-local-alias","languages":["typescript"],"limitations":["Checks an unannotated local const alias of a stable parameter or earlier const. Its single read must be a shorthand property in the same function scope. Module exports, imported bindings, captures, reassignment, positional uses and typed aliases are preserved.","Safe fixes are limited to a plain module-scope function with one explicitly intrinsic-primitive parameter and exactly a const alias followed by a returned object containing that sole shorthand field. The source binding must remain the same at the returned field. Generics, return annotations, optional/default parameters, destructuring, overloads, comments, eval, async/generator functions and the special __proto__ key remain manual. Other diagnostics require review."],"messageIds":["redundantAlias"],"optionsSchema":null,"rationale":"An unannotated alias used only by its matching object field adds a second name without adding a contract.","references":[],"remediation":"Write the object property with the original binding, then remove the redundant alias. Keep annotations, captured snapshots and independently useful names.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-redundant-local-alias.ts","status":"active","summary":"Pass the original local into an object field instead of creating a single-use rename.","test":"packages/typescript/tests/rules/no-redundant-local-alias.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/schema.ts","source":"import { z } from \"zod\"; const Items = z.array(z.string()).default([]);"}],"fixedFiles":[],"focusPath":"src/schema.ts","id":"array-default","outcome":"accept","scenarioId":"primary","title":"Default an omitted list directly"},{"expectedCount":1,"files":[{"path":"src/schema.ts","source":"import { array, string } from \"zod/v4\"; const Items = array(string()).optional().default([]);"}],"fixedFiles":[],"focusPath":"src/schema.ts","id":"official-zod-subpath-array","outcome":"reject","scenarioId":"module-identity","title":"Official Zod subpaths retain the finding for manual review"},{"expectedCount":1,"files":[{"path":"src/schema.ts","source":"import { z } from \"zod\"; const Items = z.array(z.string()).optional().default([]);"}],"fixedFiles":[],"focusPath":"src/schema.ts","id":"optional-array-default","outcome":"reject","scenarioId":"primary","title":"Do not wrap a defaulted list in a redundant optional"},{"expectedCount":0,"files":[{"path":"src/my-zod/index.ts","source":"export const z = { string: () => \"string\", array: (_element: string) => ({ optional: () => ({ default: (value: readonly string[]) => ({ value, optional: true }) }), default: (value: readonly string[]) => ({ value, optional: false }) }) };"},{"path":"src/schema.ts","source":"import { z } from \"./my-zod/index.js\"; export const Items = z.array(z.string()).optional().default([]);"}],"fixedFiles":[],"focusPath":"src/schema.ts","id":"unrelated-relative-builder","outcome":"accept","scenarioId":"module-identity","title":"Preserve an unrelated builder in a directory named zod"}],"filePatterns":[],"id":"no-redundant-optional-array-default","key":"oxlint:no-redundant-optional-array-default","languages":["typescript"],"limitations":["Only syntactically local Zod array chains with adjacent `.optional().default(...)` calls are checked.","Imports must name the official zod package or its subpaths; relative modules and unproven npm aliases are excluded.","String, scalar, tuple, set, record, aliased, composed, and dynamically constructed schemas are excluded.","The reversed `.default(...).optional()` order is preserved because its outer optional can return undefined instead of the default.","Defaults must be literal arrays or synchronous functions returning one literal array; dynamic or undefined fallbacks are excluded because Zod 3 reparses the fallback.","Automatic fixes require a whole module containing only one explicit zod/v3 namespace import and one unused unannotated private const with exact z.array(z.string()).optional().default([]). Other findings remain manual: exported introspection and supported Zod 4 constructor hooks observe the removed wrapper.","Calls containing comments are excluded so the fix never deletes authored context.","Test and generated files are excluded."],"messageIds":["redundantOptionalArrayDefault"],"optionsSchema":null,"rationale":"An outer default already accepts undefined array input, so the inner optional wrapper adds no state or fallback behavior.","references":[],"remediation":"Keep the array default and remove the immediately preceding `.optional()` call.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-redundant-optional-array-default.ts","status":"active","summary":"Remove `.optional()` immediately inside a Zod array default.","test":"packages/typescript/tests/rules/no-redundant-optional-array-default.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-repeated-string-literal","languages":["typescript"],"limitations":["Test files, short strings, prose, substitutions, module sources, JSX attributes, and repetition within one function are excluded.","Automatic edits only reuse an already authored and already read private const primitive string declared first in an otherwise declaration-only explicit module. All ordinary zero-argument functions must directly return that constant or the identical literal. Imports, comments, annotations on the constant, other statements or uses, callbacks, shadowing, generics and initialization effects remain manual; no domain name is invented."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/cache.ts","source":"// Serialize key only after validation.\nconst key = serialize(input);"}],"fixedFiles":[],"focusPath":"src/cache.ts","id":"reason-comment","outcome":"accept","scenarioId":"primary","title":"Keep a condition the statement does not establish"},{"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":"oxlint:no-restated-comment","languages":["typescript"],"limitations":["Only standalone line comments of two to eight words with at least two content tokens directly above a supported single-line statement are inspected; one-word headings and sentence-ending punctuation are excluded.","Evidence comes from that statement's identifier tokens, not strings, template text, comments, or neighboring statements. Stopwords and inflection folding are heuristic, not proof of semantic equivalence.","Directives, protected references, conditions, restrictions, ordering, repetition, direction, questions, prose paragraphs, sibling-group labels, novel content, and generated files are excluded. Automatic deletion is limited to whole-line comments whose removal preserves adjacent tool directives.","The conservative sibling-group exemption includes enclosing sibling groups and declaration/type-query pairs; it can miss genuine restatements inside those groups."],"messageIds":["deleteComment","restatesLineBelow"],"optionsSchema":null,"rationale":"A comment that only repeats code adds no context and can become stale independently.","references":[],"remediation":"Remove a genuine restatement; retain conditions, constraints, rationale, and information absent from the statement.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-restated-comment.ts","status":"active","summary":"Flag short standalone comments that repeat the adjacent statement's identifiers.","test":"packages/typescript/tests/rules/no-restated-comment.test.ts"},{"aliases":["jsdoc-restates-signature"],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","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":0,"files":[{"path":"src/users.ts","source":"/** Does not cache the user. */\nexport function cacheUser(user: unknown) { return user; }"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"negated-behavior","outcome":"accept","scenarioId":"negation","title":"Keep behavior even when its words resemble the declaration"},{"expectedCount":1,"files":[{"path":"src/users.ts","source":"/** Cache the user. */\nexport function cacheUser(user: unknown) { return user; }"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"repeated-behavior-name","outcome":"reject","scenarioId":"negation","title":"Review prose that merely repeats the declaration name"},{"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":"oxlint:no-restated-jsdoc","languages":["typescript"],"limitations":["Generated files, detached blocks, intervening comments, unknown tags, explicit JSDoc type payloads, empty blocks, and JSDoc with information absent from the signature are excluded.","Negation, conditions, constraints, sentinel values, numeric details, and quoted text conservatively preserve the block, even when a declaration name contains the same words."],"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.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-restated-jsdoc.ts","status":"active","summary":"Flag JSDoc prose that appears to repeat declaration names without adding behavioral information.","test":"packages/typescript/tests/rules/no-restated-jsdoc.test.ts"},{"aliases":[],"autofix":"none","category":"architecture","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/client.ts","source":"import ky from 'ky'; const client = ky;"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"configured-replacement","outcome":"accept","ruleOptions":[{"libraries":[{"id":"LIB101","module":"axios","replacement":"Ky"}]}],"scenarioId":"primary","title":"Use the configured library replacement"},{"expectedCount":1,"files":[{"path":"src/client.ts","source":"const client = require('axios');"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"runtime-load","outcome":"reject","ruleOptions":[{"libraries":[{"id":"LIB101","module":"axios","replacement":"Ky"}]}],"scenarioId":"primary","title":"Do not load a restricted library at runtime"}],"filePatterns":[],"id":"no-restricted-library-load","key":"oxlint:no-restricted-library-load","languages":["typescript"],"limitations":["Only literal dynamic imports, unshadowed CommonJS loads/resolution calls, and runtime TypeScript import-equals declarations are checked; erased type imports are excluded. A configured restriction list is required."],"messageIds":["restrictedLibraryLoad"],"optionsSchema":{"additionalProperties":false,"properties":{"libraries":{"description":"Library import and module-load restrictions.","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":"Dynamic imports, CommonJS loads, and package resolution checks can bypass library restrictions enforced for static imports.","references":[],"remediation":"Use the configured replacement for the runtime dependency reference; resolution checks do not themselves load a module.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-restricted-library-load.ts","status":"active","summary":"Apply configured library restrictions to literal runtime loads and CommonJS resolution references.","test":"packages/typescript/tests/rules/no-restricted-library-load.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/client.ts","source":"function client() { using handle = acquire(); return handle; }"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"return-disposed-client","outcome":"reject","scenarioId":"primary","title":"The resource is disposed before return"},{"expectedCount":0,"files":[{"path":"src/client.ts","source":"function client() { using handle = acquire(); return handle.snapshot(); }"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"return-snapshot","outcome":"accept","scenarioId":"primary","title":"Return a value independent of resource lifetime"}],"filePatterns":[],"id":"no-return-disposed-resource","key":"oxlint:no-return-disposed-resource","languages":["typescript"],"limitations":["Reports directly returned bindings declared with using or await using in the same function, including unchanged const aliases. Explicit resource-management syntax proves disposal; method names such as close or destroy are not guessed.","Nested closures retaining an outer resource, returned object aggregates, dynamic wrappers, mutable aliases and deliberate disposed-resource contracts are not inferred. Awaited fixture use callbacks and snapshots remain valid.","Findings remain warnings because an intentional disposed-resource contract requires review. No automatic ownership or cleanup rewrite is offered."],"messageIds":["disposedReturn"],"optionsSchema":null,"rationale":"Leaving the resource's using scope invokes its disposer before the returned value reaches its consumer, producing an already disposed dependency.","references":[],"remediation":"Keep consumers inside the resource scope, yield it through an awaited fixture use callback, or transfer ownership without a using declaration. Return an independent snapshot when appropriate.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-return-disposed-resource.ts","status":"active","summary":"Do not return a resource whose using declaration disposes it before the caller receives it.","test":"packages/typescript/tests/rules/no-return-disposed-resource.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/status.tsx","source":"\"use client\"; import { useEffect } from \"react\"; function Status() { useEffect(() => { const timer = setInterval(() => fetchStatus(), POLLING_INTERVAL_MS); return () => clearInterval(timer); }, []); return null; }"}],"fixedFiles":[],"focusPath":"src/status.tsx","id":"poll-named-action","outcome":"accept","scenarioId":"primary","title":"Poll a named resource"},{"expectedCount":1,"files":[{"path":"src/status.tsx","source":"\"use client\"; import { useEffect } from \"react\"; import { useRouter } from \"next/navigation\"; function Status() { const router = useRouter(); useEffect(() => { const timer = setInterval(() => router.refresh(), POLLING_INTERVAL_MS); return () => clearInterval(timer); }, [router]); return null; }"}],"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":"oxlint: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.","requiresTypeInformation":null,"runtimeImports":[],"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":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/environment.ts","source":"function missing(value: unknown) { return typeof value === \"undefined\"; }"}],"fixedFiles":[],"focusPath":"src/environment.ts","id":"bound-undefined-check","outcome":"reject","scenarioId":"global-existence","title":"Read a declared value directly when checking undefined"},{"expectedCount":0,"files":[{"path":"src/value.ts","source":"type Value = { kind: \"text\"; value: string } | { kind: \"count\"; value: number }; function render(value: Value) { return value.kind === \"text\" ? value.value : value.value.toString(); }"}],"fixedFiles":[],"focusPath":"src/value.ts","id":"discriminated-domain-union","outcome":"accept","scenarioId":"domain-variants","title":"Branch on an owned domain discriminator"},{"expectedCount":0,"files":[{"path":"src/environment.ts","source":"const isServer = typeof document === \"undefined\";"}],"fixedFiles":[],"focusPath":"src/environment.ts","id":"global-existence-probe","outcome":"accept","scenarioId":"global-existence","title":"Probe a potentially absent global without reading it"},{"expectedCount":1,"files":[{"path":"src/value.ts","source":"declare const value: { id: string }; const kind = typeof value;"}],"fixedFiles":[],"focusPath":"src/value.ts","id":"runtime-query","outcome":"reject","scenarioId":"type-query","title":"Keep a type query out of executable code"},{"expectedCount":2,"files":[{"path":"src/input.ts","source":"function parse(value: unknown) { if (typeof value === \"object\" && value !== null && !Array.isArray(value) && \"id\" in value && typeof value.id === \"string\") return { id: value.id }; throw new Error(\"Expected a record with a text id\"); }"}],"fixedFiles":[],"focusPath":"src/input.ts","id":"runtime-representation-check","outcome":"reject","scenarioId":"primary","title":"Replace a representation check with an explicit contract"},{"expectedCount":0,"files":[{"path":"src/input.ts","source":"import { z } from \"zod\"; const UserSchema = z.object({ id: z.string() }); function parse(value: unknown) { return UserSchema.parse(value); }"}],"fixedFiles":[],"focusPath":"src/input.ts","id":"specific-boundary-schema","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"accept","scenarioId":"primary","title":"Validate consumed fields at their boundary"},{"expectedCount":0,"files":[{"path":"src/value.ts","source":"declare const value: { id: string }; type Value = typeof value;"}],"fixedFiles":[],"focusPath":"src/value.ts","id":"type-only-query","outcome":"accept","scenarioId":"type-query","title":"Preserve the erased TypeScript type query"},{"expectedCount":1,"files":[{"path":"src/value.ts","source":"function render(value: string | number) { return typeof value === \"string\" ? value : value.toString(); }"}],"fixedFiles":[],"focusPath":"src/value.ts","id":"untagged-domain-union","outcome":"reject","scenarioId":"domain-variants","title":"Give owned variants a domain discriminator"}],"filePatterns":[],"id":"no-runtime-typeof","key":"oxlint:no-runtime-typeof","languages":["typescript"],"limitations":["This is an explicit design policy, not a claim that runtime typeof is invalid JavaScript. It checks runtime UnaryExpression nodes in authored JavaScript and TypeScript, including type predicates and test implementations; TypeScript type queries, strings and comments remain valid.","Equality or inequality against the literal string undefined is allowed only for an identifier without a local value binding, preserving safe probes for absent globals. Erased aliases, interfaces, type-only namespaces, type parameters and expression wrappers do not create runtime bindings. Parameter initializers do not see body-only declarations. Bound parameters, implicit function arguments, imports, local values, member expressions and dynamic comparisons remain checked.","Generated and vendored sources follow shared exclusions. There is no name-based parser exemption or automatic rewrite: inserting a schema, changing a union or moving a transformation can change accepted input, errors and public contracts."],"messageIds":["runtimeTypeof"],"optionsSchema":null,"rationale":"Representation checks spread value contracts across control flow. Specific boundary schemas and discriminated unions keep validation and the inferred application contract together.","references":["https://github.com/dmmulroy/anti-slop/blob/13909c9e07590f5a85d93c62712259fe14a883f7/src/rules/no-runtime-typeof.ts","https://zod.dev/api#discriminated-unions","https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions"],"remediation":"Parse external structured input with a specific Zod schema, or model owned variants with a discriminated union. Required raw preprocessors, field-independent utilities, arbitrary metadata walkers and framework primitive unions need an exact-code local suppression explaining their contract.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-runtime-typeof.ts","status":"active","summary":"Prefer Zod boundary schemas or discriminated unions over runtime typeof checks.","test":"packages/typescript/tests/rules/no-runtime-typeof.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"error","engine":"oxlint","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', { requestId });"}],"fixedFiles":[],"focusPath":"src/auth.ts","id":"redacted-secret","outcome":"accept","scenarioId":"primary","title":"Log non-sensitive context instead of the secret"}],"filePatterns":[],"id":"no-secret-in-log","key":"oxlint:no-secret-in-log","languages":["typescript"],"limitations":["Detection uses configurable logger names and statically recognizable secret names, raw-body names, and redaction markers. Literal containers, template/string concatenation expressions, direct slice/substring/substr calls, and unshadowed global JSON.stringify without a replacer are inspected; unknown spreads, computed properties, aliases, replacers, and opaque calls are not followed. Name-based exemptions are policy heuristics, not proof that a value is safely redacted."],"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, log allowlisted non-sensitive context, or use an approved redactor; truncation alone is not a safety guarantee.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/page.ts","source":"db.query(\"SEL\\x45CT id FROM runs /* SELECT * FROM runs */\");"}],"fixedFiles":[],"focusPath":"src/page.ts","id":"decoded-comment-data","outcome":"accept","scenarioId":"native-decoded-sql","title":"Ignore native-decoded comment data"},{"expectedCount":1,"files":[{"path":"src/page.ts","source":"db.query(\"SEL\\u0045CT * FROM runs\");"}],"fixedFiles":[],"focusPath":"src/page.ts","id":"decoded-query","outcome":"reject","scenarioId":"native-decoded-sql","title":"Inspect native-decoded query text"},{"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":0,"files":[{"path":"src/literal-rows.ts","source":"import { Client } from 'pg';\nexport async function literalRows() {\n  const authoredLiteralClient = new Client();\n  try {\n    await authoredLiteralClient.connect();\n    return (await authoredLiteralClient.query('SELECT \"authored\".\"a\", \"authored\".\"b\" FROM (VALUES (1, 2)) AS \"authored\"(\"a\", \"b\")')).rows;\n  } finally {\n    await authoredLiteralClient.end();\n  }\n}\n"}],"fixedFiles":[],"focusPath":"src/literal-rows.ts","id":"owned-literal-explicit-projection","outcome":"accept","scenarioId":"owned-literal-projection","title":"Preserve the explicit authored literal columns"},{"expectedCount":0,"files":[{"path":"src/queries.ts","source":"db.query(\"SELECT \\\"units\\\" * \\\"price\\\" AS total FROM entries\");"}],"fixedFiles":[],"focusPath":"src/queries.ts","id":"quoted-column-arithmetic","outcome":"accept","scenarioId":"projection-owner","title":"Quoted arithmetic operands are not wildcards"},{"expectedCount":1,"files":[{"path":"src/literal-rows.ts","source":"import { Client } from 'pg';\nexport async function literalRows() {\n  const authoredLiteralClient = new Client();\n  try {\n    await authoredLiteralClient.connect();\n    return (await authoredLiteralClient.query('SELECT * FROM (VALUES (1, 2)) AS \"authored\"(\"a\", \"b\")')).rows;\n  } finally {\n    await authoredLiteralClient.end();\n  }\n}\n"}],"fixedFiles":[{"path":"src/literal-rows.ts","source":"import { Client } from 'pg';\nexport async function literalRows() {\n  const authoredLiteralClient = new Client();\n  try {\n    await authoredLiteralClient.connect();\n    return (await authoredLiteralClient.query('SELECT \"authored\".\"a\", \"authored\".\"b\" FROM (VALUES (1, 2)) AS \"authored\"(\"a\", \"b\")')).rows;\n  } finally {\n    await authoredLiteralClient.end();\n  }\n}\n"}],"focusPath":"src/literal-rows.ts","id":"safe-autofix","outcome":"reject","scenarioId":"owned-literal-projection","title":"Expand the authored columns of an owned literal relation"},{"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"},{"expectedCount":1,"files":[{"path":"src/queries.ts","source":"db.query(\"SELECT EXISTS((SELECT * FROM jobs)), * FROM runs\");"}],"fixedFiles":[],"focusPath":"src/queries.ts","id":"wrapped-exists-outer-wildcard","outcome":"reject","scenarioId":"projection-owner","title":"The outer projection still selects every column"}],"filePatterns":[],"id":"no-select-star","key":"oxlint:no-select-star","languages":["typescript"],"limitations":["Only statically visible embedded SQL is checked; quoted strings (including PostgreSQL dollar strings), comments, function arguments such as COUNT(*), and stars inside EXISTS are excluded. Escaped ordinary strings, brackets, and backticks are handled conservatively. This is a bounded lexical scan, not a complete SQL parser.","Autofix only expands a complete single-row integer VALUES query with authored quoted column names, directly consumed by a fresh default pg Client in a closed source-owned function. Other projections remain manual."],"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.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-select-star.ts","status":"active","summary":"Prefer explicit column projections over SELECT * in embedded SQL.","test":"packages/typescript/tests/rules/no-select-star.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint: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. Locally shadowed undefined bindings are not treated as sentinels; recognized handling patterns are not a proof that every control-flow path handles the error."],"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.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-sentinel-return-on-catch.ts","status":"active","summary":"Disallow swallowing a caught error by returning an empty sentinel unless the error is handled or the sentinel is part of the function contract.","test":"packages/typescript/tests/rules/no-sentinel-return-on-catch.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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":0,"files":[{"path":"client-barrel.tsx","source":"'use client';\nexport { CLIENT_SETTINGS } from './client-settings.js';\n"},{"path":"client-settings.ts","source":"import { z } from 'zod';\nconst SettingsSchema = z.object({ apiOrigin: z.url() });\nexport const CLIENT_SETTINGS = SettingsSchema.parse({ apiOrigin: process.env.NEXT_PUBLIC_API_ORIGIN });\n"}],"fixedFiles":[],"focusPath":"client-barrel.tsx","id":"public-client-settings-re-export","outcome":"accept","scenarioId":"client-re-export","title":"Forward public settings from the client 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"},{"expectedCount":1,"files":[{"path":"client-barrel.tsx","source":"'use client';\nexport { SERVER_SETTINGS } from './server-settings.js';\n"},{"path":"server-settings.ts","source":"import { z } from 'zod';\nconst SettingsSchema = z.object({ apiOrigin: z.url() });\nexport const SERVER_SETTINGS = SettingsSchema.parse({ apiOrigin: process.env.API_ORIGIN });\n"}],"fixedFiles":[],"focusPath":"client-barrel.tsx","id":"server-settings-client-re-export","outcome":"reject","scenarioId":"client-re-export","title":"A client barrel cannot forward server settings"}],"filePatterns":[],"id":"no-server-env-in-client-component","key":"oxlint:no-server-env-in-client-component","languages":["typescript"],"limitations":["Only direct static runtime imports and re-exports in files with a top-level 'use client' directive targeting the existing server-env/server-settings module protocol are checked. Type-only declarations/specifiers, local exports without a source and ordinary server modules are excluded.","Dynamic imports, require calls and transitive barrel graphs are not followed. Public client-settings modules and unrelated module paths are not classified as server-only."],"messageIds":["noServerEnvInClientComponent"],"optionsSchema":null,"rationale":"Next.js client modules run in the browser, where server-only environment values are unavailable; importing or re-exporting 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/re-export it from a separately validated client-settings module backed only by NEXT_PUBLIC_* values.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-server-env-in-client-component.ts","status":"active","summary":"server-only environment settings imported or re-exported by a client module","test":"packages/typescript/tests/rules/no-server-env-in-client-component.test.ts"},{"aliases":[],"autofix":"none","category":"architecture","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"app/upload.ts","source":"'use client'; export async function upload(file: File, signedUrl: string) { await fetch(signedUrl, { method: 'PUT', body: file, signal: AbortSignal.timeout(60000) }); }"}],"fixedFiles":[],"focusPath":"app/upload.ts","id":"browser-signed-url","outcome":"accept","scenarioId":"direct-upload","title":"Send the browser File directly to storage"},{"expectedCount":1,"files":[{"path":"src/server.ts","source":"import { Hono } from 'hono'; const app = new Hono(); app.post('/upload', async (c) => { const body = await c.req.parseBody(); return c.json({ uploaded: body.file instanceof File }); });"}],"fixedFiles":[],"focusPath":"src/server.ts","id":"hono-file-validation","outcome":"reject","scenarioId":"handler-metadata","title":"Do not receive a file through a Hono route"},{"expectedCount":1,"files":[{"path":"app/actions.ts","source":"'use server'; export async function upload(file: File) { return file.name; }"}],"fixedFiles":[],"focusPath":"app/actions.ts","id":"server-file-parameter","outcome":"reject","scenarioId":"direct-upload","title":"Accept an object key instead of file bytes"},{"expectedCount":0,"files":[{"path":"app/actions.ts","source":"'use server'; export async function complete(objectKey: string) { return validateUploadedObject(objectKey); }"}],"fixedFiles":[],"focusPath":"app/actions.ts","id":"server-object-metadata","outcome":"accept","scenarioId":"handler-metadata","title":"Finalize a previously authorized upload"}],"filePatterns":[],"id":"no-server-file-uploads","key":"oxlint:no-server-file-uploads","languages":["typescript"],"limitations":["Checks native File references in use-server modules/functions, server-only modules, and modules importing runtime Next.js server or Hono server constructors. Explicit use-client modules keep browser File access. Filenames and variable names do not establish a server boundary.","Recognizes unshadowed File/globalThis.File and named or namespace File imports from node:buffer or buffer. Local definitions and unrelated File imports are excluded. Capturing the native constructor is reported at the capture; arbitrary re-exports and dynamic loaders are not resolved.","Does not ban FormData, Blob, string-only forms, local file inputs or browser previews. The separate LIB120 dependency policy covers dedicated server multipart-upload libraries.","This is an architectural restriction on native File use in known server boundaries, including local File construction. It cannot prove that a URL is signed, detect every raw-byte upload or enforce object ownership, expiration, size, checksum and post-upload validation.","No automatic rewrite is offered because storage authorization and completion validation require application-specific logic."],"messageIds":["serverFile"],"optionsSchema":null,"rationale":"Accepting uploaded files routes their bytes through application workers, consuming connections, memory or temporary disk. Direct signed-URL uploads keep that transfer in object storage.","references":["https://developer.mozilla.org/en-US/docs/Web/API/File","https://hono.dev/examples/file-upload","https://docs.cloud.google.com/storage/docs/access-control/signed-urls"],"remediation":"Issue an authorized, short-lived signed URL; upload the browser File directly to storage, then submit its object key and metadata. Use Blob for server-generated binary content.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-server-file-uploads.ts","status":"active","summary":"Keep native File handling outside explicit server boundaries.","test":"packages/typescript/tests/rules/no-server-file-uploads.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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"},{"expectedCount":0,"files":[{"path":"src/load.ts","source":"const method = 'json'; new Response('')[method]().catch(() => null);"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"stable-computed-body-parser","outcome":"accept","scenarioId":"computed-method-scope","title":"Preserve a recognized optional body parse fallback"},{"expectedCount":1,"files":[{"path":"src/load.ts","source":"const method = 'catch'; Promise.reject(new Error('lost'))[method](() => null);"}],"fixedFiles":[],"focusPath":"src/load.ts","id":"stable-computed-catch","outcome":"reject","scenarioId":"computed-method-scope","title":"Report a rejection through a stable computed method"}],"filePatterns":[],"id":"no-silent-promise-catch","key":"oxlint: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. Empty statements are ignored when proving a whole handler is silent.","Recognized imported Zod construction chains and their stable local aliases are excluded, including URL schemas and primitive bigint, boolean, number and string coercion. Other untyped catch-like APIs are not proven to be Promises.","Computed methods resolve literal syntax and stable local string aliases in lexical scope. Function-derived, object-derived and mutated computed keys remain unproven.","A following then link is accepted as recovery except for directly invoked, nonoptional, one-argument nonasync arrow identities returning the same binding without effects. A later meaningful then handler preserves the recovery exclusion."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/retry.test.ts","source":"it('retries', async () => { vi.useFakeTimers(); try { const result = retry(); await vi.advanceTimersByTimeAsync(50); await result; } finally { vi.useRealTimers(); } });"}],"fixedFiles":[],"focusPath":"src/retry.test.ts","id":"fake-timer","outcome":"accept","scenarioId":"primary","title":"Advance controlled time and restore real timers"},{"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"},{"expectedCount":1,"files":[{"path":"src/retry.test.ts","source":"import {test as base} from 'vitest'; const test = base.extend({port: 3000}); test('retries', async ({port}) => { start(port); await sleep(50); expect(done()).toBe(true); });"}],"fixedFiles":[],"focusPath":"src/retry.test.ts","id":"fixture-fixed-sleep","outcome":"reject","scenarioId":"fixture-runner","title":"Fixture setup does not make a fixed sleep deterministic"},{"expectedCount":0,"files":[{"path":"src/retry.test.ts","source":"import {test as base} from 'vitest'; const test = base.extend({port: 3000}); test('retries', async ({port}) => { start(port); await waitForReady(port); expect(done()).toBe(true); });"}],"fixedFiles":[],"focusPath":"src/retry.test.ts","id":"fixture-observable-signal","outcome":"accept","scenarioId":"fixture-runner","title":"Await the signal while reusing fixture setup"}],"filePatterns":["**/*.test.*","**/*.spec.*","**/tests/**","**/__tests__/**"],"id":"no-sleep-in-test-body","key":"oxlint:no-sleep-in-test-body","languages":["typescript"],"limitations":["Only fixed nonzero sleeps directly inside test and per-test hook callbacks are checked, including unchanged local const Vitest test.extend fixture APIs and their beforeEach/afterEach hooks. Escaped or mutated fixture APIs, nested fakes, parameterized delays, local helper bindings, and Promise executors with additional work or rejection callbacks 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 fake timers when supported and restore real timers in finally or a teardown hook.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-sleep-in-test-body.ts","status":"active","summary":"Avoid fixed timed sleeps directly in test bodies; synchronize on observable behavior or use controlled timers.","test":"packages/typescript/tests/rules/no-sleep-in-test-body.test.ts"},{"aliases":[],"autofix":"none","category":"architecture","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/engineer-digest/post.ts","source":"export {}; await kv.put('digest:last', timestamp);"}],"fixedFiles":[],"focusPath":"src/engineer-digest/post.ts","id":"private-storage","outcome":"reject","ruleOptions":[{"modules":["[\\\\/]engineer-digest[\\\\/]"]}],"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","ruleOptions":[{"modules":["[\\\\/]engineer-digest[\\\\/]"]}],"scenarioId":"primary","title":"Read from the system of record"}],"filePatterns":[],"id":"no-storage-in-stateless-modules","key":"oxlint:no-storage-in-stateless-modules","languages":["typescript"],"limitations":["This opt-in architectural policy requires configured module paths and storage method names. Overloaded `put` requires storage-like receiver evidence; `prepare` requires SQL-shaped literal text or a conventional database receiver for dynamic text. These syntax heuristics do not prove database provenance or identify the system of record."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"performance","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-string-concat-in-loop","languages":["typescript"],"limitations":["Automatic fixes require a sole exported zero-argument function with exactly a literal string initializer, a two-or-more literal-string for-of accumulation, and return of that same local string. Comments, intermediate reads, callbacks, imports, coercion and other control flow remain manual.","Only local identifiers initialized with a string or template literal and accumulated in a loop body are inspected.","Deferred function bodies are excluded except recognized direct forEach callbacks. Syntax does not establish engine-specific string allocation complexity."],"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":"Consider collecting fragments and joining once; preserve intermediate observations and coercion timing, and measure hot paths.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-string-concat-in-loop.ts","status":"active","summary":"Prefer collecting string fragments over repeatedly accumulating a growing string inside a loop.","test":"packages/typescript/tests/rules/no-string-concat-in-loop.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/assertion.test.ts","source":"import {expect} from 'vitest'; expect(() => expect(true).toBeTruthy()).not.toThrow();"}],"fixedFiles":[],"focusPath":"src/assertion.test.ts","id":"captured-matcher","outcome":"accept","scenarioId":"matcher-contract","title":"A throwing matcher observes the inner assertion contract"},{"expectedCount":0,"files":[{"path":"src/service.test.ts","source":"import {expect} from 'vitest'; const result = serviceStatus(); expect(result.status).toBe('ready');"}],"fixedFiles":[],"focusPath":"src/service.test.ts","id":"independent-result-oracle","outcome":"accept","scenarioId":"immutable-identity","title":"An independent expected state can reject a wrong result"},{"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":1,"files":[{"path":"src/service.test.ts","source":"import {expect} from 'vitest'; expect(true, 'service ready').toBe(true);"}],"fixedFiles":[],"focusPath":"src/service.test.ts","id":"message-only-oracle","outcome":"reject","scenarioId":"static-message","title":"A diagnostic string does not make a literal comparison observe behavior"},{"expectedCount":0,"files":[{"path":"src/service.test.ts","source":"import {expect} from 'vitest'; expect(serviceReady(), 'service ready').toBe(true);"}],"fixedFiles":[],"focusPath":"src/service.test.ts","id":"message-produced-oracle","outcome":"accept","scenarioId":"static-message","title":"A diagnostic message can describe an observed result"},{"expectedCount":0,"files":[{"path":"src/service.test.ts","source":"import {expect as verify} from 'vitest'; it('works', () => { verify(serviceStatus()).not.toBeNull(); });"}],"fixedFiles":[],"focusPath":"src/service.test.ts","id":"negated-produced-oracle","outcome":"accept","scenarioId":"negated-setup","title":"A negated assertion can observe a runtime result"},{"expectedCount":1,"files":[{"path":"src/service.test.ts","source":"import {expect as verify} from 'vitest'; it('works', () => { const ready = false; verify(ready).not.toBeTruthy(); });"}],"fixedFiles":[],"focusPath":"src/service.test.ts","id":"negated-setup-oracle","outcome":"reject","scenarioId":"negated-setup","title":"Negating a fixed matcher still does not observe the application"},{"expectedCount":0,"files":[{"path":"src/service.test.ts","source":"it('works', () => { const status = serviceStatus(); expect(status).toBe('ok'); });"}],"fixedFiles":[],"focusPath":"src/service.test.ts","id":"produced-scalar-oracle","outcome":"accept","scenarioId":"setup-only","title":"A runtime scalar result remains a valid observation"},{"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"},{"expectedCount":0,"files":[{"path":"src/assertion.test.ts","source":"import {expect} from 'vitest'; expect.extend({toBe: () => ({pass: false, message: () => 'custom contract'})}); expect(true).toBe(true);"}],"fixedFiles":[],"focusPath":"src/assertion.test.ts","id":"replaced-custom-matcher","outcome":"accept","scenarioId":"custom-matcher","title":"A replaced matcher can reject a literal comparison"},{"expectedCount":1,"files":[{"path":"src/service.test.ts","source":"import {expect} from 'vitest'; const result = serviceStatus(); expect(result).toBe(result);"}],"fixedFiles":[],"focusPath":"src/service.test.ts","id":"self-identity-oracle","outcome":"reject","scenarioId":"immutable-identity","title":"A returned value's identity with itself cannot reject a wrong result"},{"expectedCount":1,"files":[{"path":"src/service.test.ts","source":"it('works', () => { const status = 'ok'; expect(status).toBe('ok'); });"}],"fixedFiles":[],"focusPath":"src/service.test.ts","id":"setup-only-oracle","outcome":"reject","scenarioId":"setup-only","title":"A const setup assertion never observes the application"},{"expectedCount":1,"files":[{"path":"src/assertion.test.ts","source":"import {expect} from 'vitest'; expect(true).toBeTruthy();"}],"fixedFiles":[],"focusPath":"src/assertion.test.ts","id":"uncaptured-matcher","outcome":"reject","scenarioId":"matcher-contract","title":"An uncaptured literal assertion remains redundant"},{"expectedCount":1,"files":[{"path":"src/assertion.test.ts","source":"import {expect} from 'vitest'; expect.extend({toBeReady: custom}); expect(true).toBe(true);"}],"fixedFiles":[],"focusPath":"src/assertion.test.ts","id":"unrelated-custom-matcher","outcome":"reject","scenarioId":"custom-matcher","title":"Adding a different matcher preserves the built-in proof"}],"filePatterns":[],"id":"no-tautological-expect","key":"oxlint:no-tautological-expect","languages":["typescript"],"limitations":["Only supported `expect` matcher calls in recognized test files are inspected, including imported aliases, static string messages on imported Vitest/Playwright expect calls, and a single `.not` on zero-argument matchers. Local or type-only bindings, regular expressions, async modifiers and unsupported coercions are excluded; failing constant assertions are not tautologies.","The literal proof resolves scalar const bindings and aliases through lexical scope; mutable bindings, objects, arrays, destructuring, runtime initializers and forward references are excluded from that proof.","Positive toBe assertions on the same initialized, unreassigned local identifier const binding also have fixed identity, including runtime results. Different bindings, imported live values, property/getter accesses and deep-equality reflexivity remain excluded from this identity proof.","Expected-side runtime dependence, including a complete inline snapshot computed from its actual value, remains manual review; independent static interpolation and intentional partial masking are valid.","Direct framework expect.extend registrations exclude replaced built-in matchers; opaque registrations exclude all supported matchers, and nonempty custom equality testers exclude deep equality. Direct synchronous expect callbacks observed by a throwing matcher retain their assertion contracts. Cross-file or indirect matcher customization is not inferred."],"messageIds":["tautologicalComparison","tautologicalMatcher"],"optionsSchema":null,"rationale":"An assertion fixed by literal setup or one immutable binding's identity cannot distinguish a wrong result from the intended behavior.","references":[],"remediation":"Assert on a value produced by the behavior under test against an independently specified expectation, or remove only the redundant assertion while preserving coverage.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-tautological-expect.ts","status":"active","summary":"Disallow supported assertions fixed by literal setup or immutable self identity.","test":"packages/typescript/tests/rules/no-tautological-expect.test.ts"},{"aliases":[],"autofix":"safe","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/request.test.ts","source":"expect(result.status).toBe(409);"}],"fixedFiles":[],"focusPath":"src/request.test.ts","id":"named-behavior","outcome":"accept","scenarioId":"primary","title":"The assertion expresses the behavior"},{"expectedCount":1,"files":[{"path":"src/request.test.ts","source":"expect(result.status).toBe(409); // Confirm the rejected request\n"}],"fixedFiles":[{"path":"src/request.test.ts","source":"expect(result.status).toBe(409);\n"}],"focusPath":"src/request.test.ts","id":"narrated-assertion","outcome":"reject","scenarioId":"primary","title":"Put behavior in the test name"}],"filePatterns":[],"id":"no-test-comments","key":"oxlint:no-test-comments","languages":["typescript"],"limitations":["In frontend modules and frontend packages, every comment token is reported without automatic deletion, including directives and legal notices. The no-frontend-comments rule delegates recognized tests here. Non-frontend tests retain the existing directive/legal exceptions and safe fixes.","Checks actual line and block comment tokens in recognized maintained test files, including test fixtures; strings and template text remain unchanged. Generated/vendor files are excluded.","Legal notices, shebangs, TypeScript triple-slash references, coverage/build annotations and explicit lint/type/test/formatter directives remain allowed. Executable detect-secrets 1.5.0 allowlist/whitelist pragmas retain its case-sensitive delimiter, spacing, physical-line and next-line grammar; arbitrary pragma prose remains ordinary commentary. Rationale on an executable directive remains the owning tool's responsibility.","The automatic fix removes comments and adjacent padding while retaining every line ending and necessary token boundary; it does not rename tests or remove executable statements. Bare integration/e2e directories and ambiguous hyphen/underscore test basenames require another concrete test-path signal or a runtime testing-framework import; ordinary production integration modules and rule implementations ending in -test are preserved."],"messageIds":["testComment"],"optionsSchema":{"additionalProperties":false,"properties":{"allFiles":{"description":"Check every eligible test file without requiring frontend framework ownership.","type":"boolean"}},"type":"object"},"rationale":"Test comments duplicate or obscure behavior that executable setup and assertions should communicate.","references":[],"remediation":"Move the explanation into a descriptive test or helper name, then remove the comment. Review and migrate legal notices and tool directives before removing frontend comments; preserve those contracts in non-frontend tests.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-test-comments.ts","status":"active","summary":"Express test behavior through names, fixtures and assertions instead of prose comments.","test":"packages/typescript/tests/rules/no-test-comments.test.ts"},{"aliases":["trailing-value-narration"],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-trailing-value-narration","languages":["typescript"],"limitations":["Only attached declaration, property, or assignment values containing numeric tokens and comments with recognized unit words are inspected. Deletion requires a numeric literal and the same unit on its owner. Constraints, additional prose, unknown expressions on unit-bearing owners, and cross-unit annotations are preserved; conversions are not evaluated."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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; password?: 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":"oxlint:no-type-member-comment-wall","languages":["typescript"],"limitations":["Automatic fixes remove only an exact multiword member-name comment on its own line and retain all physical line endings; threshold-based or novel prose remains manual.","Only interface and type-literal bodies meeting the configured comment-count and restatement-ratio thresholds are reported. Negation, requirements, conditional relations, and fixed-value contracts are not counted as restatements. The novel-word threshold is a review heuristic, not proof that a comment contains no useful contract."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-typed-doc-sections","languages":["typescript"],"limitations":["Description-free or name-restating tags require the adjacent explicitly typed signature and a corresponding parameter name. Optional/defaulted or nested parameter tags and unproven explicit JSDoc types are preserved."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-union-in-comment","languages":["typescript"],"limitations":["Only bare quoted-value lists directly attached to explicitly annotated string declarations are inspected. Unknown schema builders and runtime validation are not inferred.","Automatic fixes remove only the broad string member from an unused private unmerged interface/object alias whose existing quoted literal branches exactly match the adjacent comment's set. Authored literal branches, nullability and the comment are retained. Bare string declarations, observed/public types, heritage, generics, dependent this, computed/indexed members, escapes and comments inside the changed union remain manual."],"messageIds":["unionInComment"],"optionsSchema":null,"rationale":"A broad string annotation does not express a closed set documented beside it.","references":[],"remediation":"If the list is exhaustive, express it as a string-literal union; keep examples and runtime constraints documented separately.","requiresTypeInformation":null,"runtimeImports":[],"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":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/message.tsx","source":"<input placeholder=\"Search\" />"}],"fixedFiles":[],"focusPath":"src/message.tsx","id":"literal-message","outcome":"reject","scenarioId":"primary","title":"Translate a user-facing message"},{"expectedCount":0,"files":[{"path":"src/message.tsx","source":"<input placeholder={t(\"search\")} />"}],"fixedFiles":[],"focusPath":"src/message.tsx","id":"translated-message","outcome":"accept","scenarioId":"primary","title":"Resolve the message through translation"}],"filePatterns":[],"id":"no-unlocalized-jsx-attributes","key":"oxlint:no-unlocalized-jsx-attributes","languages":["typescript"],"limitations":["Shared presets set enabled:false. Standalone rule activation opts into localization for the selected files. No particular locale or translation function name is required. Generated files, tests, fixtures and stories are excluded.","Default attributes are title, placeholder, alt, aria-label and aria-description. Custom attribute names are configurable. ID-reference attributes are always excluded. Later spreads make values unknown; dynamic expressions are not proven translated."],"messageIds":["unlocalized"],"optionsSchema":{"additionalProperties":false,"properties":{"allowText":{"items":{"type":"string"},"type":"array","uniqueItems":true},"attributes":{"description":"JSX attributes whose user-visible text requires localization.","items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true},"enabled":{"type":"boolean"},"translationComponents":{"items":{"additionalProperties":false,"properties":{"export":{"minLength":1,"type":"string"},"module":{"minLength":1,"type":"string"}},"required":["module","export"],"type":"object"},"type":"array"}},"type":"object"},"rationale":"Accessible labels and placeholders must follow the interface language along with visible copy.","references":[],"remediation":"Pass the user-facing message through the configured translation API, or explicitly exempt text that must remain literal.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-unlocalized-jsx-attributes.ts","status":"active","summary":"Require translation for literal user-visible JSX attributes in opted-in localized interfaces.","test":"packages/typescript/tests/rules/no-unlocalized-jsx-attributes.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/message.tsx","source":"<p>Hello</p>"}],"fixedFiles":[],"focusPath":"src/message.tsx","id":"literal-message","outcome":"reject","scenarioId":"primary","title":"Translate a user-facing message"},{"expectedCount":0,"files":[{"path":"src/message.tsx","source":"<p>{t(\"hello\")}</p>"}],"fixedFiles":[],"focusPath":"src/message.tsx","id":"translated-message","outcome":"accept","scenarioId":"primary","title":"Resolve the message through translation"}],"filePatterns":[],"id":"no-unlocalized-jsx-text","key":"oxlint:no-unlocalized-jsx-text","languages":["typescript"],"limitations":["Shared presets set enabled:false. Standalone rule activation opts into localization for the selected files. No particular locale or translation function name is required. Generated files, tests, fixtures and stories are excluded.","Only direct JSX text and complete literal expression children containing Unicode letters are checked. Dynamic expressions are not proven translated. Text inside code and samp elements is treated as technical content; their user-facing attributes are still checked by the attribute rule. translate=\"no\" and configured translation component imports are respected; nested translate=\"yes\" re-enables checking."],"messageIds":["unlocalized"],"optionsSchema":{"additionalProperties":false,"properties":{"allowText":{"items":{"type":"string"},"type":"array","uniqueItems":true},"enabled":{"type":"boolean"},"translationComponents":{"items":{"additionalProperties":false,"properties":{"export":{"minLength":1,"type":"string"},"module":{"minLength":1,"type":"string"}},"required":["module","export"],"type":"object"},"type":"array"}},"type":"object"},"rationale":"Literal UI copy cannot change with the selected language.","references":[],"remediation":"Pass the user-facing message through the configured translation API, or explicitly exempt text that must remain literal.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-unlocalized-jsx-text.ts","status":"active","summary":"Require translation for literal JSX text in opted-in localized interfaces.","test":"packages/typescript/tests/rules/no-unlocalized-jsx-text.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/message.tsx","source":"import { toast } from \"sonner\"; toast.success(\"Saved\");"}],"fixedFiles":[],"focusPath":"src/message.tsx","id":"literal-message","outcome":"reject","scenarioId":"primary","title":"Translate a user-facing message"},{"expectedCount":0,"files":[{"path":"src/message.tsx","source":"import { toast } from \"sonner\"; toast.success(t(\"saved\"));"}],"fixedFiles":[],"focusPath":"src/message.tsx","id":"translated-message","outcome":"accept","scenarioId":"primary","title":"Resolve the message through translation"}],"filePatterns":[],"id":"no-unlocalized-toast","key":"oxlint:no-unlocalized-toast","languages":["typescript"],"limitations":["Shared presets set enabled:false. Standalone rule activation opts into localization for the selected files. No particular locale or translation function name is required. Generated files, tests, fixtures and stories are excluded.","Recognizes configured imported functions and their configured methods with message in argument zero. Defaults cover sonner toast and react-hot-toast default exports. Imports and shadows are scope-resolved. Promise/config-object notifications and dynamic messages are not analyzed."],"messageIds":["unlocalized"],"optionsSchema":{"additionalProperties":false,"properties":{"allowText":{"items":{"type":"string"},"type":"array","uniqueItems":true},"enabled":{"type":"boolean"},"functions":{"items":{"additionalProperties":false,"properties":{"export":{"minLength":1,"type":"string"},"module":{"minLength":1,"type":"string"}},"required":["module","export"],"type":"object"},"type":"array"},"methods":{"description":"Toast method names whose user-visible messages require localization.","items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true},"translationComponents":{"items":{"additionalProperties":false,"properties":{"export":{"minLength":1,"type":"string"},"module":{"minLength":1,"type":"string"}},"required":["module","export"],"type":"object"},"type":"array"}},"type":"object"},"rationale":"User-facing notifications remain in a fixed language when their messages bypass translation.","references":[],"remediation":"Pass the user-facing message through the configured translation API, or explicitly exempt text that must remain literal.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-unlocalized-toast.ts","status":"active","summary":"Require translation for literal toast messages in opted-in localized interfaces.","test":"packages/typescript/tests/rules/no-unlocalized-toast.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:no-unnecessary-use-client","languages":["typescript"],"limitations":["Client need is inferred from recognized hooks, handlers, browser globals, exports, classes, and known client-only imports. Unknown side-effect imports preserve the boundary; arbitrary transitive runtime requirements are not inspected."],"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":"Review whether the directive can be removed after checking transitive client requirements and intended export boundaries; local syntax alone does not prove server compatibility.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","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":"import { vi } from 'vitest'; const client = { read: () => 'value' }; vi.spyOn(client, 'read'); const m = vi.mocked(client.read);"}],"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":"oxlint:no-unsafe-mock-casting","languages":["typescript"],"limitations":["Only mock types imported from Vitest or Jest modules are inspected. mocked is a type helper, not runtime validation or a replacement for mock setup.","Automatic fixes remove only an erased mock assertion from an unused private unannotated const initialized by an identifier, optionally through unknown assertions. Every erased named type/generic/namespace binding must retain an independent exported type use; potentially erased annotations and self references cannot witness each other. Evaluation is unchanged; referenced/exported bindings, contextual expressions, scripts, declaration files, eval and comments remain manual."],"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":["https://vitest.dev/api/vi.html#vi-mocked"],"remediation":"Create the mock or spy first, then use the framework's mocked helper to preserve the original value's type. The helper does not create or verify a runtime mock.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-unsafe-mock-casting.ts","status":"active","summary":"Disallow casting to mock types like `jest.Mock` or `vi.Mock`. Use `vi.mocked()` or `jest.mocked()` instead.","test":"packages/typescript/tests/rules/no-unsafe-mock-casting.test.ts"},{"aliases":[],"autofix":"safe","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"service.test.ts","source":"import { vi } from 'vitest'; const client = { read: vi.fn() } as unknown as Client;"}],"fixedFiles":[],"focusPath":"service.test.ts","id":"mock-backed-double-cast","outcome":"reject","scenarioId":"primary","title":"Do not hide a partial mock behind unknown"},{"expectedCount":0,"files":[{"path":"service.test.ts","source":"const client = { read: async () => ({ id: '1' }) } satisfies Pick<Client, 'read'>;"}],"fixedFiles":[],"focusPath":"service.test.ts","id":"typed-recording-fake","outcome":"accept","scenarioId":"primary","title":"Use a typed recording fake"}],"filePatterns":[],"id":"no-unsafe-test-double-cast","key":"oxlint:no-unsafe-test-double-cast","languages":["typescript"],"limitations":["Only generated-free test files and direct double assertions containing a fn or spyOn call from a resolved Vitest/Jest import or an unshadowed vi/jest global are checked. Import aliases and mixed assertion syntax are supported; local shadowing, type-only imports, indirect value aliases, arbitrary payload casts and dynamic API replacement are not inferred.","Automatic fixes remove only erased double assertions on an unused private unannotated const initialized by a plain object containing primitive literals, nested plain objects and zero-argument canonical mock fn calls. Every erased named/generic/namespace binding must retain an independent exported type use outside self references and other erasable annotations. They retain the original runtime expression; exports, reads, contextual functions, generic mock factories, spreads, accessors, comments, scripts, declaration files and eval remain manual."],"messageIds":["noUnsafeTestDoubleCast"],"optionsSchema":null,"rationale":"Casting a partial object through unknown hides missing or stale collaborator members, so production interface changes can compile while the test double silently drifts.","references":[],"remediation":"Define the smallest injected port and implement a typed recording fake, or make the fixture satisfy the real contract without passing through unknown.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-unsafe-test-double-cast.ts","status":"active","summary":"Disallow mock-backed test doubles that bypass collaborator contracts through double assertions.","test":"packages/typescript/tests/rules/no-unsafe-test-double-cast.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/adapter.ts","source":"function requestId(response: object) {\n  // @ts-expect-error -- vendor types omit the runtime requestId field\n  return response.requestId;\n}"}],"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":"function requestId(response: object) {\n  // @ts-expect-error -- false positive\n  return response.requestId;\n}"}],"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":"oxlint:no-vague-suppression-description","languages":["typescript"],"limitations":["Only `eslint-disable*` and `oxlint-disable*` comments and TypeScript expect-error directives are checked.","The rule uses a small anchored vocabulary and does not score prose quality generally.","Descriptions outside that exact vocabulary are excluded; an unflagged reason is not proof of a justified suppression. Missing descriptions remain owned by the upstream description requirement. Explicit path exclusions own generated-source boundaries; a source marker does not waive suppression accountability."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/status-values.ts","source":"export enum Status { Active = 'active', Inactive = 'inactive' }"},{"path":"src/status.ts","source":"import { z } from 'zod'; import { Status } from './status-values.js';\nexport const StatusSchema = z.enum(Status);"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"imported-typescript-enum","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"reject","scenarioId":"imported-enum","title":"Do not hide an imported TypeScript enum behind z.enum"},{"expectedCount":0,"files":[{"path":"src/status.ts","source":"import { z } from 'zod';\nexport const StatusSchema = z.enum(['active', 'inactive']);"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"schema-owned-string-values","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"accept","scenarioId":"imported-enum","title":"Let the schema own its literal value domain"},{"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":[],"focusPath":"src/status.ts","id":"zod-native-enum","outcome":"reject","scenarioId":"primary","title":"Declare literal values instead of an enum-like object"}],"filePatterns":[],"id":"no-zod-native-enum","key":"oxlint:no-zod-native-enum","languages":["typescript"],"limitations":["Migration remains manual: enum-like object keys and supported global constructor hooks remain observable even for unused private schemas. Equal key/value objects still expose the changed nativeEnum versus enum constructor call through a hook stack."],"messageIds":["enumOfTsEnum","nativeEnum"],"optionsSchema":null,"rationale":"The project prefers literal-first schema definitions. nativeEnum also accepts plain enum-like objects, so this is an explicit declaration policy, not proof that every call duplicates a TypeScript enum's runtime object.","references":[],"remediation":"Pass string literals directly to `z.enum` and derive the TypeScript type with `z.infer`.","requiresTypeInformation":null,"runtimeImports":[],"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":"architecture","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/value.ts","source":"import { z } from \"zod\"; const RowSchema = z.object({ id: z.string() }); declare function load(): Promise<{ id: string }>; export async function read() { return RowSchema.parse(await load()); }"}],"fixedFiles":[],"focusPath":"src/value.ts","id":"typed-service-result","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"reject","scenarioId":"primary","title":"Reuse the typed service response"},{"expectedCount":0,"files":[{"path":"src/value.ts","source":"import { z } from \"zod\"; const RowSchema = z.object({ id: z.string() }); export function read(raw: unknown) { return RowSchema.parse(raw); }"}],"fixedFiles":[],"focusPath":"src/value.ts","id":"unknown-boundary","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"accept","scenarioId":"primary","title":"Validate unknown data once"}],"filePatterns":[],"id":"no-zod-parse-typed-object","key":"oxlint:no-zod-parse-typed-object","languages":["typescript"],"limitations":["Requires type information, strict null checks, and a native installed Zod parse, parseAsync, safeParse or safeParseAsync signature. Recognizes imported schemas, aliases, member access and awaited service results without guessing from names.","Reports fully known object or array inputs assignable to the schema input, when schema input and output have the same value shape. Optional-to-required defaults are included; type-changing transforms, coercions, brands, incompatible contracts, primitive roots and nested any/unknown/generic contracts are excluded.","Static types do not establish runtime trust or prove refinements and normalization redundant. This is an architectural warning with no automatic removal. Keep intentional validation at an unknown-data boundary or document an exact local suppression. Tests, stories and generated code are excluded.","Shape comparison ignores undefined introduced by optional properties and checks tuple element types and rest positions. It is bounded to 128 object pairs, visited input objects, properties, tuple elements or primitive union alternatives; object unions have at most 16 alternatives. Opaque or larger graphs are conservatively excluded."],"messageIds":["typedObject"],"optionsSchema":null,"rationale":"Parsing a typed service result duplicates its object contract and adds runtime validation and copying throughout trusted application code. Validate unknown data once at its boundary.","references":["https://zod.dev/basics"],"remediation":"Use the typed value directly. If defaults or normalization are needed, express that mapping explicitly. Keep untrusted boundary data unknown until it is validated.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/no-zod-parse-typed-object.ts","status":"active","summary":"Reuse known object contracts instead of parsing them again with Zod.","test":"packages/typescript/tests/rules/no-zod-parse-typed-object.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:prefer-await-in-async-return","languages":["typescript"],"limitations":["The default async-return scope checks one directly returned `.then` call with an inline callback. The all-promise-calls scope checks direct calls to built-in Promise/PromiseLike.then, including optional and literal-computed calls, without automatic rewrites.","The receiver must be proven Promise-like by TypeScript; an earlier chain can still produce that receiver. The default async-return scope excludes rejection handlers and named callbacks. This is not the upstream return-await policy and no scheduling equivalence is promised.","The default async-return scope excludes direct loader callbacks passed to resolved `React.lazy` and `next/dynamic` imports because returning the module Promise is their framework contract.","All-promise-calls requires type information and excludes unproven/custom thenables, dynamic property names, property reads, destructuring, and extracted method aliases. Catch and finally remain available. Framework callback exclusions apply only to the default async-return scope."],"messageIds":["preferAwait","preferAwaitCall"],"optionsSchema":{"additionalProperties":false,"properties":{"scope":{"description":"Check async return statements or every promise-valued call.","enum":["async-return","all-promise-calls"],"type":"string"}},"type":"object"},"rationale":"Promise callback chains mix explicit async control flow with nested callbacks, making sequencing and failure handling harder to read.","references":[],"remediation":"Consider awaiting the Promise and returning the transformed value with ordinary async statements. Preserve catch boundaries, callback behavior, and observable scheduling when rewriting manually.","requiresTypeInformation":null,"runtimeImports":[],"since":"15.6.3","source":"packages/typescript/src/rules/prefer-await-in-async-return.ts","status":"active","summary":"Prefer explicit `await` for proven Promise `.then` calls; the default scope checks direct async returns.","test":"packages/typescript/tests/rules/prefer-await-in-async-return.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"app/result.ts","source":"import type { Result as NativeResult } from 'neverthrow'; type Saved<T, E> = Promise<NativeResult<T, E>>; export type Output = Saved<number, { code: 'duplicate'; name: string } | { code: 'denied' }>;"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"aliased-library-domain-error","outcome":"accept","scenarioId":"aliased-library-error","title":"Keep a finite error code and only the context its consumer needs"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"import type { Result as NativeResult } from 'neverthrow'; type Saved<T, E> = Promise<NativeResult<T, E>>; export type Output = Saved<number, string>;"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"aliased-library-message","outcome":"reject","scenarioId":"aliased-library-error","title":"A transparent alias must preserve the library error contract"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"type Error = { code: 'duplicate'; name: string } | { code: 'invalid_transition' }; export type Result<T> = { ok: true; data: T } | { ok: false; error: Error };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"code-and-context","outcome":"accept","scenarioId":"primary","title":"The consumer can choose behavior and format context"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"import { err, ok, type Result } from 'neverthrow'; type Denied = { code: 'denied'; name: string }; export function save(name: string, allowed: boolean): Result<string, Denied> { return allowed ? ok(name) : err({ code: 'denied', name }); } export function format(error: Denied): string { return `Denied ${error.name}`; }"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"conditional-coded-failure","outcome":"accept","scenarioId":"conditional-result-producer","title":"Preserve the code and context and let the consumer format"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"import { err, ok } from 'neverthrow'; export const save = (name: string, allowed: boolean) => allowed ? ok(name) : err(`Denied ${name}`);"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"conditional-message-failure","outcome":"reject","scenarioId":"conditional-result-producer","title":"A conditional return still needs a useful error contract"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"type Duplicate = { code: 'duplicate'; name: string }; export function reject(name: string): { ok: false; error: Duplicate } { return { ok: false, error: { code: 'duplicate', name } }; } export function format(error: Duplicate): string { return `Duplicate ${error.name}`; }"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"consumer-renders-context","outcome":"accept","scenarioId":"consumer-formatting","title":"Retain the typed context and format after handling the code"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"export type Outcome<T> = { status: 'success'; value: T } | { status: 'failure'; reason: string };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"exclusive-error-state","outcome":"reject","scenarioId":"absence-state","title":"Conventional success and failure tags establish one Result"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"export type Outcome<T> = { ok: true; data: T } | { ok: false; error: string };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"exclusive-message-outcome","outcome":"reject","scenarioId":"partial-data","title":"An exclusive failure still needs an actionable identity"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; export type Saved = Result<number, 'duplicate' | 'invalid_transition'>;"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"finite-error-codes","outcome":"accept","scenarioId":"generic-error","title":"An error code needs no invented payload"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; export type Saved = Result<number, string>;"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"generic-error-instantiation","outcome":"reject","scenarioId":"generic-error","title":"A concrete generic error channel erases its identity"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"export type Partial<T> = { ok: true; data: T; error?: string } | { ok: false; data: T; error: string };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"independent-partial-outcomes","outcome":"accept","scenarioId":"partial-data","title":"Partial data and its independent error remain separate outcomes"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"export type Source<T> = { status: 'loaded'; value: T } | { status: 'failed' | 'unconfigured'; reason: string };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"legitimate-unconfigured-state","outcome":"accept","scenarioId":"absence-state","title":"A loading report can preserve an unconfigured absence state"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"import { err, type Result } from 'neverthrow'; type Missing = { code: 'missing'; name: string }; export function reject(name: string): Result<never, Missing> { return err({ code: 'missing', name }); }"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"library-domain-constructor","outcome":"accept","scenarioId":"library-error-presentation","title":"Reuse the established Result library with a typed domain error"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"import { err } from 'neverthrow'; export function reject(name: string) { return err(`Missing ${name}`); }"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"library-error-constructor","outcome":"reject","scenarioId":"library-error-presentation","title":"A proven Result constructor does not make formatted text actionable"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"export type Result<T> = { ok: true; data: T } | { ok: false; message: string };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"message-only-result","outcome":"reject","scenarioId":"primary","title":"A failure erases identity into a message"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"export type Outcome<T> = { ok: true; value: T } | { ok: false; fallbackError: string };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"named-error-carrier","outcome":"reject","scenarioId":"named-error-channel","title":"A failure-only Error-suffixed field can still erase its cases"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; export type Saved = Result<number, unknown>;"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"opaque-error-channel","outcome":"reject","scenarioId":"opaque-error","title":"Unknown preserves the need to narrow but hides the domain's cases"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"type SaveError = { code: 'duplicate'; name: string } | { code: 'denied' }; export type Outcome<T> = { value: T } | { error: SaveError };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"presence-selected-domain-error","outcome":"accept","scenarioId":"presence-selected-result","title":"Retain the existing presence check with a finite error union"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"export type Outcome<T> = { value: T } | { error: string; field?: 'name' };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"presence-selected-message","outcome":"reject","scenarioId":"presence-selected-result","title":"An error-presence check still needs a useful error contract"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"export function reject(name: string) { return { ok: false, code: 'duplicate', message: `Duplicate ${name}` }; }"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"producer-renders-context","outcome":"reject","scenarioId":"consumer-formatting","title":"A code alone does not move message formatting to the consumer"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"type Result<T, E> = { ok: true; value: T } | { ok: false; error: E }; export type Saved = Result<number, { code: 'unavailable' }>;"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"specific-error-channel","outcome":"accept","scenarioId":"opaque-error","title":"Expose the domain's rejection case without inventing context"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"export type Outcome<T> = { kind: 'ok'; value: T } | { kind: 'error'; error: { code: 'duplicate'; name: string } };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"string-outcome-domain-code","outcome":"accept","scenarioId":"string-outcome","title":"Preserve the established string tag and add the actionable error"},{"expectedCount":1,"files":[{"path":"app/result.ts","source":"export type Outcome<T> = { kind: 'ok'; value: T } | { kind: 'error'; message: string };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"string-outcome-tag","outcome":"reject","scenarioId":"string-outcome","title":"An outcome tag distinguishes success from failure, not the failure's cause"},{"expectedCount":0,"files":[{"path":"app/result.ts","source":"export type Outcome<T> = { ok: true; value: T } | { ok: false; fallbackError: { code: 'unsupported'; requestedKind: string } };"}],"fixedFiles":[],"focusPath":"app/result.ts","id":"typed-named-error-carrier","outcome":"accept","scenarioId":"named-error-channel","title":"Preserve the existing field name with the required context"}],"filePatterns":[],"id":"prefer-coded-error-result","key":"oxlint:prefer-coded-error-result","languages":["typescript"],"limitations":["Accepting an inferred producer does not prove a finite inferred contract: object payloads or errAsync can widen a code to string. Annotate the existing Result/ResultAsync return when necessary. Native assignability, narrowing and exhaustive-handler checks remain authoritative.","Checks source-resolved local unions with required literal ok/success booleans, or status/state/kind/type/tag values ok/success versus err/error/failure/failed (case insensitive). Conflicting outcome tags abstain. A shared outcome tag does not also count as a domain error code. Named and namespace neverthrow Result/ResultAsync imports establish their generic error position.","Presence-selected unions require an error property on every failure branch and the same single required non-control value property on every success branch. Optional error/value fields, multiple value keys, loading states and error branches carrying the success value abstain. Required errorMessage and Error-suffixed failure channels are also reviewed; multiple custom channels abstain and a primary error payload retains precedence.","Broad string and unknown error channels, unshadowed native Error-family types and open string/unknown code fields receive review. Explicit Any stays with the existing no-explicit-any rule. Finite literal codes need no invented payload. Imported domain errors, validation-library errors, generic error parameters, ordinary formatters, legitimate absence and redacted failure states remain valid.","Local aliases, unmerged interfaces without inheritance and generic substitutions are bounded to 16 resolution levels, 64 union members and 256 traversal steps. Transparent aliases retain the proven neverthrow Result/ResultAsync error position, including renamed/namespace imports, generic defaults and unshadowed native Promise/PromiseLike wrappers. Imported local declarations, conditional/intersection/mapped types and unresolved contracts abstain. Optional discriminants, independent success/error payloads and partial-data failures are excluded.","Required code/kind/type/tag fields of unresolved or imported type defer to the type checker; a finite code does not prove that its cases or payloads are useful. Generic error parameters are retained rather than guessed. Ordinary string formatters, absence, validation-library errors and redacted failure states without a string payload are not flagged.","A directly returned failure object, neverthrow err/errAsync call or new Err with an interpolated template or proven string concatenation receives review. Without a code, the diagnostic asks for a domain contract; with a literal code, it asks for consumer formatting. Literal-only compositions, including earlier stable local constants, remain valid codes. Nested plain error objects, expression-bodied arrows and awaited returns are supported. Returned conditional branches are followed without visiting their conditions, call arguments or containers; logical/sequence expressions and arbitrary aliases abstain. The entire return expression abstains beyond 16 levels, 64 leaves or 256 traversal steps. Import identity and value-space shadowing are checked; spreads, accessors, computed keys, helper formatting calls and nonliteral codes abstain. This proves formatting ownership, not which context the domain must expose; presentation adapters remain a reviewed exception.","This warning requests domain review: an infrastructure adapter may intentionally retain unknown/Error, and a final presentation boundary may accept every failure and render one message. Neither proves a recoverable domain rejection. No fix invents error codes, fields, handlers or changes serialization, exception unwinding, transactions, retries or logging."],"messageIds":["formatAtConsumer","preferCodedErrorResult"],"optionsSchema":null,"rationale":"A message-only or opaque failure hides the identity needed for repair, conflict handling, localization and exhaustive dispatch. Preserve a finite error code and the minimum handling context; let the consumer format its message. Reuse the native no-explicit-any rule, compiler narrowing and type-aware exhaustiveness checks.","references":["https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions","https://github.com/supermacro/neverthrow"],"remediation":"Use the existing Result or outcome union with a finite error code and variant-specific context. Preserve public behavior through a presentation adapter, and handle or propagate each relevant variant without parsing message text.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-coded-error-result.ts","status":"active","summary":"Return a stable error code and typed context instead of only a rendered failure message.","test":"packages/typescript/tests/rules/prefer-coded-error-result.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":".github/scripts/publish.mjs","source":"const accountId = '0123456789abcdef0123456789abcdef';\nconst base = `https://api.cloudflare.com/client/v4/accounts/${accountId}`;\nexport async function publish(path, request = fetch) { return request(`${base}/${path}`); }"}],"fixedFiles":[],"focusPath":".github/scripts/publish.mjs","id":"literal-account-scope","outcome":"reject","scenarioId":"primary","title":"Move outgoing account scope into deployment configuration"},{"expectedCount":0,"files":[{"path":"src/cloud-client.ts","source":"import { settings } from \"./env.js\";\nexport function list() { return fetch(new URL(`https://api.cloudflare.com/client/v4/accounts/${settings.CLOUDFLARE_ACCOUNT_ID}/workers/scripts`)); }"},{"path":"src/env.ts","source":"import { z } from \"zod\";\nconst SettingsSchema = z.object({ CLOUDFLARE_ACCOUNT_ID: z.string().length(32).regex(/^[a-fA-F0-9]+$/u) });\nexport const settings = SettingsSchema.parse(process.env);"}],"fixedFiles":[],"focusPath":"src/cloud-client.ts","id":"native-url-configured-account","outcome":"accept","scenarioId":"native-url-account","title":"Supply the account from shared Zod-validated settings"},{"expectedCount":1,"files":[{"path":"src/cloud-client.ts","source":"export function list() { return fetch(new URL(\"https://api.cloudflare.com/client/v4/accounts/0123456789abcdef0123456789abcdef/workers/scripts\")); }"}],"fixedFiles":[],"focusPath":"src/cloud-client.ts","id":"native-url-literal-account","outcome":"reject","scenarioId":"native-url-account","title":"A native URL wrapper still fixes the outgoing account in source"},{"expectedCount":0,"files":[{"path":".github/scripts/publish.mjs","source":"import { settings } from '../../src/env.mjs';\nconst base = `https://api.cloudflare.com/client/v4/accounts/${settings.CLOUDFLARE_ACCOUNT_ID}`;\nexport async function publish(path, request = fetch) { return request(`${base}/${path}`); }\nexport async function list(request = fetch) { return request(`${base}/workers/scripts`); }"},{"path":"src/env.mjs","source":"import { z } from 'zod';\nconst SettingsSchema = z.object({ CLOUDFLARE_ACCOUNT_ID: z.string().length(32).regex(/^[a-fA-F0-9]+$/u) });\nexport const settings = SettingsSchema.parse(process.env);"}],"fixedFiles":[],"focusPath":".github/scripts/publish.mjs","id":"validated-account-scope","outcome":"accept","scenarioId":"primary","title":"Parse one shared settings object with Zod before requests"}],"filePatterns":[],"id":"prefer-configured-cloud-scope","key":"oxlint:prefer-configured-cloud-scope","languages":["typescript"],"limitations":["Initial coverage is canonical HTTPS Cloudflare v4 account-management URLs with a 32-character hexadecimal account segment sent to an unshadowed fetch, stable const alias, or fetch-default parameter. Other providers, SDKs, arbitrary endpoints, Worker names and resource IDs require separate evidence.","Follows bounded same-file const aliases, concatenations and template strings. One-argument native URL wrappers are supported only for fully known absolute account URLs whose native normalization leaves the text unchanged. Local/imported constructors, dynamic constructor arguments, relative-base URLs, Request objects, unknown accounts, fallback expressions, function returns and cross-file values remain unreported.","Native URL constructor/prototype mutation or capability capture outside new/typeof vetoes wrapper folding. Escaping/dynamic global receiver access and ambient eval/Function also leave URL semantics unknown. A stored URL must be nonexported, have at most 128 references, and every runtime read must be a direct native fetch argument; mutable instances, aliases, injected transports and other escapes remain unknown. These URL limits do not hide independent direct string fetch findings. Direct writes to ambient fetch or its global receivers retain their existing veto; indirect runtime monkeypatches are not modeled.","Analysis stops at 16 nested expressions or fetch aliases, 256 URL-expression visits per request, 128 string fragments or 8,192 UTF-16 code units of assembled URL text. These limits deliberately leave more complex expressions unreported.","Scripts remain covered. Test/fixture and generated files are excluded. Comparisons, schemas, allowlists and unused URL strings do not produce findings. Each originating account literal is reported at most once per file.","This is a warning without an automatic fix: the deployment configuration boundary and independent authorization policy require review. Application code should import validated configuration as required by no-raw-env; browser-exposed variables must never contain secrets."],"messageIds":["configuredScope"],"optionsSchema":null,"rationale":"A source-literal cloud account scope pins operational requests to one deployment even when the credential or environment changes.","references":["https://developers.cloudflare.com/workers/wrangler/system-environment-variables/","https://eslint.org/docs/latest/rules/no-restricted-syntax"],"remediation":"Read the account ID through one shared settings object validated by a Zod schema at startup, then import or inject that parsed object into consumers. Required values must fail before remote effects. Preserve independent security allowlists; an environment read alone is not validation.","requiresTypeInformation":false,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-configured-cloud-scope.ts","status":"active","summary":"Prefer validated deployment configuration for account scope in outgoing cloud management requests.","test":"packages/typescript/tests/rules/prefer-configured-cloud-scope.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/auth.ts","source":"if (await constantTimeEqual(presentedAccessToken, expectedAccessToken)) { 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 (presentedAccessToken === expectedAccessToken) { allow(); }"}],"fixedFiles":[],"focusPath":"src/auth.ts","id":"secret-equality","outcome":"reject","scenarioId":"primary","title":"Do not compare authentication tokens with equality"}],"filePatterns":[],"id":"prefer-constant-time-secret-compare","key":"oxlint:prefer-constant-time-secret-compare","languages":["typescript"],"limitations":["This uses secret-name heuristics and bounded stable local aliases of credential headers from an unshadowed native Request or imported NextRequest. Literal header names include no-substitution templates and erased TypeScript wrappers; unknown receivers and mutated aliases are excluded. This is not proof of runtime sensitivity. Ambiguous token names require an authentication or cryptographic qualifier; test files and public sentinel comparisons are excluded."],"messageIds":["preferConstantTimeSecretCompare"],"optionsSchema":null,"rationale":"Ordinary equality offers no constant-time guarantee for comparing secrets.","references":[],"remediation":"Compare equal-length cryptographic digests with a constant-time comparison primitive.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-constant-time-secret-compare.ts","status":"active","summary":"Prefer a supported constant-time comparison primitive for secret-like values.","test":"packages/typescript/tests/rules/prefer-constant-time-secret-compare.test.ts"},{"aliases":[],"autofix":"none","category":"architecture","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"service.ts","source":"interface Store { read(): string } class SqlStore implements Store { private value = 'ok'; read() { return this.value; } } class Service { constructor(private readonly store: SqlStore) {} run() { return this.store.read(); } }"}],"fixedFiles":[],"focusPath":"service.ts","id":"concrete-store","outcome":"reject","scenarioId":"primary","title":"The consumer uses only the interface"},{"expectedCount":0,"files":[{"path":"service.ts","source":"interface Store { read(): string } class Service { constructor(private readonly store: Store) {} run() { return this.store.read(); } }"}],"fixedFiles":[],"focusPath":"service.ts","id":"interface-store","outcome":"accept","scenarioId":"primary","title":"Accept another implementation"}],"filePatterns":[],"id":"prefer-dependency-interface","key":"oxlint:prefer-dependency-interface","languages":["typescript"],"limitations":["Checks private readonly constructor parameter properties, an empty constructor body, and a local concrete class declaring exactly one local interface. Every direct this-property consumer must access an interface member.","Interface/class inheritance, imports, dynamic members, escapes, implementation-specific reads, decorators, generics, mutable dependencies and protected properties remain unresolved. The compiler owns signature compatibility. No contract or automatic rewrite is invented."],"messageIds":["interface"],"optionsSchema":null,"rationale":"A concrete annotation can require private implementation identity even when a declared interface covers every consumed operation.","references":[],"remediation":"Use the existing interface on the injected parameter. Keep concrete annotations for implementation-specific capabilities.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-dependency-interface.ts","status":"active","summary":"Annotate retained collaborators with their existing interface when the consumer uses only that contract.","test":"packages/typescript/tests/rules/prefer-dependency-interface.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/projection.ts","source":"type Phase = \"ready\" | \"waiting\" | \"reconnecting\" | \"unknown\"; declare function snapshot(): Phase; function state(timeout: boolean): { phase: Phase; timeout: boolean } { const phase = snapshot(); return { phase, timeout }; }"}],"fixedFiles":[],"focusPath":"src/projection.ts","id":"authoritative-state","outcome":"accept","scenarioId":"state-projection","title":"Return the phase while preserving the independent timeout dimension"},{"expectedCount":0,"files":[{"path":"src/job.ts","source":"function job(done: boolean): { phase: \"queued\"; ticket: string } | { phase: \"complete\" } { return done ? { phase: \"complete\" } : { phase: \"queued\", ticket: \"t\" }; }"}],"fixedFiles":[],"focusPath":"src/job.ts","id":"explicit-produced-variants","outcome":"accept","scenarioId":"produced-variants","title":"Give each produced variant its payload"},{"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":0,"files":[{"path":"src/job.ts","source":"function job(command: string): { phase: \"queued\"; ticket: string } | { phase: \"complete\" } { switch (command) { case \"start\": return { phase: \"queued\", ticket: \"t\" }; default: return { phase: \"complete\" }; } }"}],"fixedFiles":[],"focusPath":"src/job.ts","id":"explicit-switch-variants","outcome":"accept","scenarioId":"switch-producer","title":"An explicit dispatcher result supports payload narrowing"},{"expectedCount":1,"files":[{"path":"src/failure.ts","source":"function failure(retry: boolean, saved: boolean): { retriable: boolean; persisted: boolean } { return saved ? { retriable: false, persisted: true } : retry ? { retriable: true, persisted: false } : { retriable: false, persisted: false }; }"}],"fixedFiles":[],"focusPath":"src/failure.ts","id":"failure-dispositions","outcome":"reject","scenarioId":"boolean-state","title":"A producer returns retry, terminal and saved-partial combinations"},{"expectedCount":1,"files":[{"path":"src/job.ts","source":"function job(done: boolean): { phase: \"queued\" | \"complete\"; ticket?: string } { return done ? { phase: \"complete\" } : { phase: \"queued\", ticket: \"t\" }; }"}],"fixedFiles":[],"focusPath":"src/job.ts","id":"flat-produced-variants","outcome":"reject","scenarioId":"produced-variants","title":"Preserve producer variant relationships"},{"expectedCount":1,"files":[{"path":"src/job.ts","source":"function job(command: string): { phase: \"queued\" | \"complete\"; ticket?: string } { switch (command) { case \"start\": return { phase: \"queued\", ticket: \"t\" }; default: return { phase: \"complete\" }; } }"}],"fixedFiles":[],"focusPath":"src/job.ts","id":"flat-switch-producer","outcome":"reject","scenarioId":"switch-producer","title":"A complete dispatcher preserves each returned variant"},{"expectedCount":1,"files":[{"path":"src/projection.ts","source":"type Phase = \"ready\" | \"waiting\" | \"unknown\"; declare function snapshot(): Phase; function state(): { ready: boolean; waiting: boolean } { const phase = snapshot(); return { ready: phase === \"ready\", waiting: phase === \"waiting\" }; }"}],"fixedFiles":[],"focusPath":"src/projection.ts","id":"immutable-state-projections","outcome":"reject","scenarioId":"state-projection","title":"Two predicates over one immutable phase cannot both be true"},{"expectedCount":0,"files":[{"path":"src/observations.ts","source":"function observations(a: boolean, b: boolean): { started: boolean; reported: boolean } { return a ? b ? { started: true, reported: true } : { started: true, reported: false } : b ? { started: false, reported: true } : { started: false, reported: false }; }"}],"fixedFiles":[],"focusPath":"src/observations.ts","id":"independent-observations","outcome":"accept","scenarioId":"boolean-state","title":"Independent observations retain all four combinations"},{"expectedCount":1,"files":[{"path":"src/result.ts","source":"function result(ok: boolean): { ok: boolean; data?: string; error?: string } { return ok ? { ok: true, data: \"value\" } : { ok: false, error: \"failed\" }; }"}],"fixedFiles":[],"focusPath":"src/result.ts","id":"optional-result-payloads","outcome":"reject","scenarioId":"primary","title":"A producer establishes the status-to-payload relationship"}],"filePatterns":[],"id":"prefer-discriminated-union","key":"oxlint:prefer-discriminated-union","languages":["typescript"],"limitations":["Names, Boolean counts and optional-field counts never establish a finding. A producer's literal writes establish either tag-specific payload presence or a pair of required Boolean fields with two or three distinct combinations, with both fields varying. Boolean members may use boolean, true | false or one unmerged nongeneric local alias of either. All four combinations, constant dimensions and optional/unknown dimensions are excluded. Expression-valued flags require the separate typed evidence described below. At most eight Boolean fields are compared. Only one relationship is reported per contract; existing literal/payload evidence takes precedence. Independently nullable payloads do not erase Boolean evidence.","With type services enabled, a synchronous nongeneric producer with exactly one own return, a final direct object return and a supported flat local contract also exposes strict equality predicates over the same earlier local const binding. Native types at both reads must be finite domains of at most sixteen string/number literals containing both distinct singleton comparison values. Literal constants and direct named-import/local enum members are supported; enum declarations and members are resolved by symbol identity and raw runtime literal values, including renamed imports and reexports. At most 128 candidate pairs per file are analyzed; exceeding the bound abstains from all typed findings in that file. No automatic fix is offered for these projections.","The const snapshot may be destructured or copied from another primitive binding, passed to callbacks, or read by closures without losing its value. Mutable bindings, parameters and module captures, property reads/getters, open/nullable/object/symbol domains, async producers, multiple own returns, generic/inherited/imported contracts and dynamic eval abstain. Nested callback returns do not belong to the producer. Enum namespace writes, aliases, escapes, computed accesses, calls and merged declarations in the analyzed file abstain. Cross-file enum declarations can supply type/constant identity; external mutation, dishonest declarations/assertions and runtime monkeypatching are not modeled. The analysis proves a relationship under those type and mutation assumptions, not domain intent or a whole-program ownership guarantee.","Producer analysis requires a finite literal tag, every declared tag syntactically witnessed, exhaustive simple return paths without literal constant conditions, plain object returns, and consistent optional-field presence for each tag. It resolves only unmerged local aliases/interfaces without inheritance or generics, one local alias of a literal tag domain, and the built-in Promise around async returns. Callable or opaque own then properties abstain for async producers because Promise assimilation can replace the fulfilled value. Inherited then methods and external prototype mutation are not modeled. This is syntax-only evidence of field presence; explicit undefined, void and null payloads do not establish payload requirements, while expression value types are not inferred. Type-only assertion/satisfies/non-null wrappers, literal computed string keys and a sole-use const immediately before its return are recognized. Const alias chains, observation, mutation and dynamic eval abstain. Dynamic returns, spreads, dynamic computed keys, accessors, loops and try statements are deliberately skipped.","A switch producer is supported only for a non-overloaded function declaration when its sole body statement switches on an unchanged direct string or number parameter, has unique same-kind literal labels and a default, and every nonempty case body is a sole object return or throw, optionally inside sole blocks. Empty labels may share the next terminating body; nested conditions, conditional returns, statement prefixes, breaks, other fallthrough, captured/closed inputs, dynamic eval and switches under preceding control flow abstain. Broad parameter domains establish input witnesses for each label and the default; this does not infer branch-specific runtime effects or expression value types. Path visits, depth and returned objects are bounded conservatively.","These are refactoring advisories, not proven defects or proof that other writers cannot produce more states. Historical observations, partial results, unknown states and independently completing operations may need the broader contract. Review all readers and writers before migrating a shared contract; preserve every valid combination. A union of existing literal flag pairs is the smallest type-only replacement; a named disposition is a reviewed domain migration.","Automatic fixes are limited to an unused private nongeneric synchronous function with a direct inline contract and one conditional return containing at most four bare literal objects. Boolean-pair fixes narrow only the implicated pair, retaining every unrelated member's type, readonly modifier and optionality. Tag/payload fixes express the produced state-specific payload presence while preserving declared payload value types. All runtime code, condition evaluation, object creation, identity and exceptions remain unchanged. Nested conditionals are supported. Assertion/satisfies wrappers, shared/observed contracts, aliases, exports, async functions, comments, expression payloads and generated/declaration/test files remain excluded. Public interfaces, serialization and cancellation are never rewritten."],"messageIds":["preferProducedState","preferProducedVariants","preferProjectedState"],"optionsSchema":null,"rationale":"A flat object with optional branch data loses the relationship between a tag and its payload. Callers cannot narrow to the required fields, and the type permits incomplete or contradictory combinations.","references":["https://functional-architecture.org/make_illegal_states_unrepresentable/","https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions"],"remediation":"If the variants are mutually exclusive, represent each with a literal tag and its required payload. For predicates over one current state, consider returning that authoritative state instead of storing multiple derived flags. Preserve every valid state, independent dimension, shared field, payload value type and nullability. For a producer returning a subset of a broader wire contract, give the producer a precise domain return type after validation.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-discriminated-union.ts","status":"active","summary":"Preserve producer-proven tag/payload and Boolean state relationships in return types.","test":"packages/typescript/tests/rules/prefer-discriminated-union.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/vault.ts","source":"class Vault { #read() { return 1; } open() { return this.#read(); } }"}],"fixedFiles":[],"focusPath":"src/vault.ts","id":"runtime-private-member","outcome":"accept","scenarioId":"primary","title":"Use runtime-enforced privacy"},{"expectedCount":1,"files":[{"path":"src/vault.ts","source":"class Vault { private read() { return 1; } open() { return this.read(); } }"}],"fixedFiles":[],"focusPath":"src/vault.ts","id":"typescript-private-member","outcome":"reject","scenarioId":"primary","title":"Do not erase a privacy boundary"}],"filePatterns":[],"id":"prefer-ecmascript-private-members","key":"oxlint:prefer-ecmascript-private-members","languages":["typescript"],"limitations":["Ambient, abstract, computed, decorated, override, parameter-property, and generated declarations are excluded.","Automatic conversion is limited to unused private module/block classes with no heritage, decorators, computed members, static execution, or eval. Other migration is report-only: type information cannot prove that instances or constructors never escape through this, or that reflection and framework serialization do not observe ordinary private properties."],"messageIds":["preferEcmascriptPrivate"],"optionsSchema":null,"rationale":"ECMAScript private names enforce encapsulation at runtime instead of erasing the boundary during compilation.","references":["https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_elements"],"remediation":"Review reflection, instance escape and framework contracts before replacing TypeScript privacy with ECMAScript private names and updating references.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-ecmascript-private-members.ts","status":"active","summary":"Prefer ECMAScript `#private` class members over TypeScript-only `private` members.","test":"packages/typescript/tests/rules/prefer-ecmascript-private-members.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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"},{"expectedCount":0,"files":[{"path":"src/constants.ts","source":"type ToolSupport = Readonly<{ voice: boolean }>;\nexport const TOOL_SUPPORT: ToolSupport = { voice: true };"}],"fixedFiles":[],"focusPath":"src/constants.ts","id":"resolved-readonly-alias","outcome":"accept","scenarioId":"type-binding","title":"A local alias resolves to readonly properties"},{"expectedCount":1,"files":[{"path":"src/constants.ts","source":"type Readonly = { voice: boolean };\nexport const TOOL_SUPPORT: Readonly = { voice: true };"}],"fixedFiles":[],"focusPath":"src/constants.ts","id":"shadowed-readonly-alias","outcome":"reject","scenarioId":"type-binding","title":"A type named Readonly still exposes mutable properties"}],"filePatterns":[],"id":"prefer-immutable-module-constant","key":"oxlint:prefer-immutable-module-constant","languages":["typescript"],"limitations":["Automatic fixes add as const only to unannotated, unused private object/array literals in explicit modules with no eval; referenced or exported collections remain manual.","Generated, test and JavaScript files are skipped. Private constants with observed direct or alias mutation are excluded; exported mutable collections remain advisory candidates. Reassigned aliases are conservatively followed, not flow-proven.","Local type aliases are resolved before recognizing standard readonly types; imported or otherwise unknown type declarations are not inferred from their names. Readonly types have no runtime enforcement."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","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":"oxlint:prefer-input-group-search","languages":["typescript"],"limitations":["Opposite branches of the same conditional expression and icons with explicit interaction handlers are excluded; arbitrary component behavior is not inferred.","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.","requiresTypeInformation":null,"runtimeImports":[],"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":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/layout.tsx","source":"<div className=\"ms-2\" />"}],"fixedFiles":[],"focusPath":"src/layout.tsx","id":"logical-spacing","outcome":"accept","scenarioId":"primary","title":"Use direction-aware spacing"},{"expectedCount":1,"files":[{"path":"src/layout.tsx","source":"<div className=\"ml-2\" />"}],"fixedFiles":[],"focusPath":"src/layout.tsx","id":"physical-spacing","outcome":"reject","scenarioId":"primary","title":"Mirror layout spacing"}],"filePatterns":[],"id":"prefer-logical-tailwind-utilities","key":"oxlint:prefer-logical-tailwind-utilities","languages":["typescript"],"limitations":["Shared presets set enabled:false. Enable this rule only in scopes that support bidirectional layout. Standalone rule activation opts in. Insets require checkInsets:true; explicit rtl:/ltr: variants are exempt. No autofix changes physical coordinates.","Only effective complete static JSX className values are inspected. Dynamic compositions, CSS, class factories and identifier references are not inferred. Tests, stories, fixtures and generated files are excluded."],"messageIds":["physicalUtility"],"optionsSchema":{"additionalProperties":false,"properties":{"allowUtilities":{"description":"Physical utility names allowed without a logical replacement.","items":{"type":"string"},"type":"array","uniqueItems":true},"checkInsets":{"description":"Include physical inset utilities in direction checks.","type":"boolean"},"enabled":{"description":"Enable replacement of physical direction utilities with logical equivalents.","type":"boolean"}},"type":"object"},"rationale":"Physical spacing and alignment remain on the same side when reading direction changes, producing incorrectly mirrored interfaces.","references":[],"remediation":"Use the suggested logical utility; retain intentional physical geometry through allowUtilities or a scoped rule override.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-logical-tailwind-utilities.ts","status":"active","summary":"Prefer logical Tailwind utilities in explicitly opted-in bidirectional interfaces.","test":"packages/typescript/tests/rules/prefer-logical-tailwind-utilities.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/request.ts","source":"import { z } from 'zod';\nexport const RequestSchema = z.object({ timeoutMs: z.number().int().min(1000).max(300000).default(30000) });"}],"fixedFiles":[],"focusPath":"src/request.ts","id":"millisecond-timeout-schema","outcome":"accept","scenarioId":"primary","title":"Encode control timing in milliseconds"},{"expectedCount":1,"files":[{"path":"src/request.ts","source":"import { z } from 'zod';\nexport const RequestSchema = z.object({ timeout_seconds: z.number().int().min(1).max(300).default(30) });"}],"fixedFiles":[],"focusPath":"src/request.ts","id":"second-timeout-schema","outcome":"reject","scenarioId":"primary","title":"Do not expose second-granularity timeout controls"}],"filePatterns":[],"id":"prefer-millisecond-control-duration-schema","key":"oxlint:prefer-millisecond-control-duration-schema","languages":["typescript"],"limitations":["Only direct identifier keys with recognizable numeric Zod leaves in application-owned z.object/z.strictObject schemas are checked; aliases and transformations are not inferred.","The rule covers control timings such as timeout, delay, interval, backoff, TTL, lease, heartbeat, debounce, and throttle; observed durations and business-domain periods are excluded.","Quoted/computed protocol keys, generated/vendor code, tests, fixtures, and non-Zod schemas are excluded."],"messageIds":["preferMilliseconds"],"optionsSchema":null,"rationale":"Second-granularity timeout and scheduling controls lose precision and invite implicit unit conversion at API boundaries. Encoding milliseconds in the schema keeps the unit explicit and composes with platform timing APIs.","references":[],"remediation":"Rename the field with an `Ms`/`_ms` suffix and express its bounds and default in milliseconds; update the owning API contract rather than converting in application code.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-millisecond-control-duration-schema.ts","status":"active","summary":"Require application-owned Zod control-duration fields to use millisecond granularity.","test":"packages/typescript/tests/rules/prefer-millisecond-control-duration-schema.test.ts"},{"aliases":[],"autofix":"safe","category":"architecture","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: \"a\"}; const prefix = \"Hello \"; function label(prefix: string, user: {email: string; name: string}): string {return prefix + user.name + user.email;} label(prefix, account);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[],"focusPath":"src/case.ts","id":"auxiliary-all-used","outcome":"accept","scenarioId":"auxiliary-scalar-parameter","title":"Reading both object fields retains their requirements"},{"expectedCount":1,"files":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: \"a\"}; const prefix = \"Hello \"; function label(prefix: string, user: {email: string; name: string}): string {return prefix + user.name;} label(prefix, account);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: \"a\"}; const prefix = \"Hello \"; function label(prefix: string, user: { name: string}): string {return prefix + user.name;} label(prefix, account);"}],"focusPath":"src/case.ts","id":"auxiliary-scalar","outcome":"reject","scenarioId":"auxiliary-scalar-parameter","title":"A scalar prefix does not require the object's unread email"},{"expectedCount":0,"files":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: \"a\"}; function formatName(name: string): string {return name.toUpperCase();} function label(user: {email: string; name: string}): string {return formatName(user.name) + user.email;} label(account);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[],"focusPath":"src/case.ts","id":"helper-and-other-field","outcome":"accept","scenarioId":"local-scalar-helper","title":"The consumer still requires a separately read field"},{"expectedCount":0,"files":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: null}; function label(user: {email: string | null; name: string}): string {return user.email ?? user.name;} label(account);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[],"focusPath":"src/case.ts","id":"read-nullable-field","outcome":"accept","scenarioId":"nullable-private-field","title":"An inspected nullable field keeps its contract"},{"expectedCount":0,"files":[{"path":"src/case.ts","source":"export {}; const account = {name: null, email: \"a\"}; function label(user: {readonly email: string; readonly name: string | null}): string {return user.name ?? user.email;} label(account);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[],"focusPath":"src/case.ts","id":"retained-nullable-all-used","outcome":"accept","scenarioId":"retained-nullable-field","title":"Both requirements remain when the nullable view reads email too"},{"expectedCount":1,"files":[{"path":"src/case.ts","source":"export {}; const account = {name: null, email: \"a\"}; function label(user: {readonly email: string; readonly name: string | null}): string {return user.name ?? \"\";} label(account);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[{"path":"src/case.ts","source":"export {}; const account = {name: null, email: \"a\"}; function label(user: { readonly name: string | null}): string {return user.name ?? \"\";} label(account);"}],"focusPath":"src/case.ts","id":"retained-nullable-read","outcome":"reject","scenarioId":"retained-nullable-field","title":"A nullable name remains while an unread email requirement is removed"},{"expectedCount":1,"files":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: \"a\"}; function formatName(name: string): string {return name.toUpperCase();} function label(user: {email: string; name: string}): string {return formatName(user.name);} label(account);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: \"a\"}; function formatName(name: string): string {return name.toUpperCase();} function label(user: { name: string}): string {return formatName(user.name);} label(account);"}],"focusPath":"src/case.ts","id":"scalar-helper","outcome":"reject","scenarioId":"local-scalar-helper","title":"Only the name crosses a local scalar helper call"},{"expectedCount":1,"files":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: null}; function label(user: {email: string | null; name: string}): string {return user.name;} label(account);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: null}; function label(user: { name: string}): string {return user.name;} label(account);"}],"focusPath":"src/case.ts","id":"unread-nullable-field","outcome":"reject","scenarioId":"nullable-private-field","title":"A required nullable field is never inspected"},{"expectedCount":1,"files":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: \"a\"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name;} label(account);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: \"a\"}; function label(user: { readonly name: string}): string {return user.name;} label(account);"}],"focusPath":"src/case.ts","id":"unused-required-field","outcome":"reject","scenarioId":"primary","title":"A private name consumer unnecessarily requires email"},{"expectedCount":0,"files":[{"path":"src/case.ts","source":"export {}; const account = {name: \"Ada\", email: \"a\"}; function label(user: {readonly email: string; readonly name: string}): string {return user.name + user.email;} label(account);"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\"}}"}],"fixedFiles":[],"focusPath":"src/case.ts","id":"used-fields","outcome":"accept","scenarioId":"primary","title":"Every required dependency is read"}],"filePatterns":[],"id":"prefer-minimal-parameter-contract","key":"oxlint:prefer-minimal-parameter-contract","languages":["typescript"],"limitations":["Automatic fixes additionally require every existing identifier argument to be natively assignable to its corresponding original parameter contract, without Any, unknown, error or unresolved types on the relevant surface. Comments inside the annotation and TypeScript suppression directives keep the finding manual. Only unread required object property signatures are removed; every read or optional member, modifier and terminator token is preserved. Auxiliary annotations and argument order stay unchanged. Edits that would merge distinct CR and LF terminators remain manual.","Requires parser-owned TypeScript information and a strict external module. Only top-level private function declarations and raw const arrows with one explicit inline object parameter and an independent broad primitive return annotation are supported. Other parameters must be required identifiers annotated directly as string, number, boolean or bigint; optional, default, rest, destructured, domain-specific and explicit this auxiliaries are excluded. Every callable reference must be a direct call with an identifier argument for every parameter; exports including type-only exports, typeof queries, callbacks, storage, constructors, contextual signatures, overloads, generics, decorators, async functions and direct eval prevent findings.","Only anonymous inline contracts made of broad string, number, boolean or bigint property types are supported. A read field or an unread required field may additionally have exactly one such keyword and null or undefined in a direct two-part union; unread optional nullable fields and more complex unions remain excluded. Fields may be plain or readonly; the consumer body must still prove read-only usage. Every read field keeps its exact type, modifiers and optionality. Named domain contracts, brands, literal discriminants, classes, methods, callbacks, indexes, mapped types and imported parameter contracts are excluded. At least one read required field and one unread required field must be established; all-optional reduced contracts can reject existing callers through weak-type checks.","The single return expression may use only fixed scalar field reads, flat destructuring, literals, operators, templates and scalar calls to same-file implemented top-level functions or raw const arrows with an explicit return annotation. These calls require one native resolved declaration, no generic or predicate signature, scalar arguments/result and complete active native checking of the local implementation file. Their helper bodies need not be pure because only scalar values cross the call and only an erased parameter annotation changes. Imported, aliased, ambient, overloaded, optional, spread and member calls remain unsupported. Bound destructured fields count as reads even when their local aliases are unused. Whole-object escape, identity, mutation, closures, reflection, serialization, dynamic keys, implicit Object capabilities and JSX are unsupported. Unknown semantic information causes abstention. Findings outside the fix proof remain manual; this is not a requirement to minimize every object or discard useful producer fields."],"messageIds":["minimal"],"optionsSchema":null,"rationale":"A private scalar consumer can unnecessarily require data it never reads. Removing unused required fields accepts more structural values while preserving the original runtime object and producer information.","references":[],"remediation":"Review a smaller existing contract, Pick or interface containing the read fields and preserving readonly and optional modifiers. Change the consumer annotation; a new runtime projection has different identity and allocation costs. The safe fix removes only proven unused required property signatures from the private inline annotation; producer objects, calls and function bodies remain unchanged.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-minimal-parameter-contract.ts","status":"active","summary":"Prefer the proven read-only subset for a private consumer parameter.","test":"packages/typescript/tests/rules/prefer-minimal-parameter-contract.test.ts"},{"aliases":[],"autofix":"safe","category":"performance","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:prefer-module-level-constant","languages":["typescript"],"limitations":["Automatic fixes only hoist an existing readonly literal-string array from a sole exported ordinary function that only returns its native includes lookup on a string parameter. No imports, other declarations, comments, identity escapes or source prototype mutations are permitted.","Small collections, observed direct or nested mutations, nested aliases, direct escapes, and dependencies on local values are excluded. Directly invoked function expressions are excluded rather than assuming they run repeatedly. Callback effects and indirect escapes are not analyzed interprocedurally; review those before hoisting.","Builtin constructor and non-retaining call assumptions require unshadowed identifiers; local declarations conservatively abstain. Runtime replacement of globals is not modeled."],"messageIds":["hoistCollection","hoistRegex"],"optionsSchema":{"additionalProperties":false,"properties":{"checkRegex":{"description":"Include reusable regular expression literals in hoisting checks.","type":"boolean"},"ignoreTestFiles":{"description":"Skip test files when recommending module-level constants.","type":"boolean"},"minElements":{"description":"Minimum collection size before recommending a module-level constant.","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.","requiresTypeInformation":null,"runtimeImports":[],"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":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/options.ts","source":"import { z } from 'zod'; export function parse(value: unknown) { return z.string().trim().min(1).max(128).parse(value); }"}],"fixedFiles":[],"focusPath":"src/options.ts","id":"local-refinement","outcome":"reject","scenarioId":"primary","title":"Do not rebuild a closed validation chain"},{"expectedCount":0,"files":[{"path":"src/options.ts","source":"import { z } from 'zod'; const NameSchema = z.string().trim().min(1).max(128); export function parse(value: unknown) { return NameSchema.parse(value); }"}],"fixedFiles":[],"focusPath":"src/options.ts","id":"module-refinement","outcome":"accept","scenarioId":"primary","title":"Share the validation schema"}],"filePatterns":[],"id":"prefer-module-level-refined-schema","key":"oxlint:prefer-module-level-refined-schema","languages":["typescript"],"limitations":["Composite object/record/tuple/union schemas are owned by prefer-module-level-schema.","Schemas that depend on function-local or mutable state, localized text, receiver state, lazy construction, or recognized memoization are excluded.","Literal string z.enum domains are owned by prefer-shared-zod-enum.","Eager calls outside recognized Zod construction chains and new expressions are excluded. This is manual guidance, not a purity proof: review getters, callback effects, error customization, schema identity, and module initialization order before moving construction."],"messageIds":["hoistRefinedSchema"],"optionsSchema":null,"rationale":"A closed validation pipeline created inside a function is rebuilt on every invocation and obscures a reusable constraint.","references":[],"remediation":"Move the validation schema to module scope, name it with a PascalCase Schema suffix, and call parse on the shared schema.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-module-level-refined-schema.ts","status":"active","summary":"Declare closed Zod scalar, format, and wrapper schemas at module scope.","test":"packages/typescript/tests/rules/prefer-module-level-refined-schema.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:prefer-module-level-schema","languages":["typescript"],"limitations":["Schemas that depend on local state or are wrapped in a recognized memoization helper are excluded.","Eager calls outside the recognized Zod construction chain and new expressions are excluded. This is manual guidance, not a purity proof: review getters, callback effects, schema identity, error customization, and module initialization order before moving construction."],"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 schema created inside a function is rebuilt on each call. Module scope can enable reuse across callers; local schemas already support local type inference.","references":[],"remediation":"Move the closed schema declaration to module scope and reference it from the function.","requiresTypeInformation":null,"runtimeImports":[],"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":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/schema.ts","source":"import { z } from 'zod/v4'; export const Version = z.union([z.literal(1), z.literal(2), z.literal(3)]);"}],"fixedFiles":[],"focusPath":"src/schema.ts","id":"literal-union","outcome":"reject","scenarioId":"primary","title":"Avoid repeated literal wrappers"},{"expectedCount":0,"files":[{"path":"src/schema.ts","source":"import { z } from 'zod/v4'; export const Version = z.literal([1, 2, 3]);"}],"fixedFiles":[],"focusPath":"src/schema.ts","id":"multi-value","outcome":"accept","scenarioId":"primary","title":"Use one multi-value literal"}],"filePatterns":[],"id":"prefer-multi-value-zod-literal","key":"oxlint:prefer-multi-value-zod-literal","languages":["typescript"],"limitations":["Bare zod imports are analyzed only when the rule option explicitly declares zodMajorVersion: 4; explicit zod/v4 entrypoints are self-declaring.","All-string domains are left to zod/prefer-enum-over-literal-union.","Migration remains manual: supported global constructor hooks observe the original literal/literal/union construction sequence even for an unused private schema. ZodLiteral and ZodUnion also expose different introspection APIs and validation error shapes."],"messageIds":["useMultiValueLiteral"],"optionsSchema":{"additionalProperties":false,"properties":{"zodMajorVersion":{"description":"Installed Zod major version used to validate multi-value literal support.","maximum":4,"minimum":4,"type":"integer"}},"type":"object"},"rationale":"One multi-value literal expresses the same closed value domain without repeated schema wrappers.","references":[],"remediation":"Use z.literal([value1, value2, ...]) for a modern Zod 4 value-domain contract. Preserve required union introspection or error-shape contracts with an explained local exception.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-multi-value-zod-literal.ts","status":"active","summary":"Use the Zod 4 multi-value literal API instead of a union of literal schemas.","test":"packages/typescript/tests/rules/prefer-multi-value-zod-literal.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/options.ts","source":"export interface Options { done?: (boundary: 'seeded' | 'queued') => void; }"}],"fixedFiles":[],"focusPath":"src/options.ts","id":"inline-domain","outcome":"reject","scenarioId":"primary","title":"Do not inline a public callback domain"},{"expectedCount":0,"files":[{"path":"src/options.ts","source":"export type Boundary = 'seeded' | 'queued'; export interface Options { done?: (boundary: Boundary) => void; }"}],"fixedFiles":[],"focusPath":"src/options.ts","id":"named-domain","outcome":"accept","scenarioId":"primary","title":"Name the callback domain"}],"filePatterns":[],"id":"prefer-named-callback-domain","key":"oxlint:prefer-named-callback-domain","languages":["typescript"],"limitations":["Only callback types nested in an exported type alias or exported interface are reported.","Automatic fixes only reuse one existing exported nongeneric alias with exactly matching primitive literal-union tokens and the same lexical binding. Missing, ambiguous, shadowed or commented aliases remain manual."],"messageIds":["nameCallbackDomain"],"optionsSchema":null,"rationale":"An inline callback domain hides a reusable vocabulary at the point where callers and implementations must agree.","references":[],"remediation":"Extract the literal union to a named type and use that type in the callback parameter.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-named-callback-domain.ts","status":"active","summary":"Name literal-union domains used by callbacks in exported contracts.","test":"packages/typescript/tests/rules/prefer-named-callback-domain.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/queue.ts","source":"export function claim(): { state: 'idle' } | { state: 'waiting'; retryAt: number } | { state: 'claimed'; id: string } { return { state: 'idle' }; }"}],"fixedFiles":[],"focusPath":"src/queue.ts","id":"inline-result","outcome":"reject","scenarioId":"primary","title":"Do not inline a multi-state result"},{"expectedCount":0,"files":[{"path":"src/queue.ts","source":"type ClaimResult = { state: 'idle' } | { state: 'waiting'; retryAt: number } | { state: 'claimed'; id: string }; export function claim(): ClaimResult { return { state: 'idle' }; }"}],"fixedFiles":[],"focusPath":"src/queue.ts","id":"named-result","outcome":"accept","scenarioId":"primary","title":"Name a multi-state result"}],"filePatterns":[],"id":"prefer-named-complex-return-type","key":"oxlint:prefer-named-complex-return-type","languages":["typescript"],"limitations":["Automatic fixes only reuse one existing exported nongeneric alias with exactly matching context-free type tokens for an unused private function declaration. Shared/public functions, missing or ambiguous aliases and locally dependent types remain manual.","Only explicit object types with at least three members and unions with at least three object variants are reported.","Any single-argument generic wrapper is traversed to find nested shapes, not assumed semantically transparent. Inferred return types are outside this rule; extraction is manual and must preserve locally bound type parameters with generic named contracts. Types depending on polymorphic this or a local value query are excluded."],"messageIds":["nameComplexReturnType"],"optionsSchema":null,"rationale":"A large inline return annotation hides a reusable domain concept and makes signatures difficult to scan.","references":[],"remediation":"Name the complex nested shape while preserving its generic wrappers and type parameters; reference the named contract from the return annotation.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-named-complex-return-type.ts","status":"active","summary":"Prefer a named contract for structurally complex function return types.","test":"packages/typescript/tests/rules/prefer-named-complex-return-type.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/app/actions.ts","source":"\"use server\";\n\nimport ky from \"ky\";\n\nexport async function upload(url: string, body: Blob): Promise<void> {\n  await ky.put(url, { body });\n}\n"}],"fixedFiles":[],"focusPath":"src/app/actions.ts","id":"ky-in-action","outcome":"reject","scenarioId":"primary","title":"A server action imports ky"},{"expectedCount":0,"files":[{"path":"src/app/actions.ts","source":"\"use server\";\n\nexport async function upload(url: string, body: Blob): Promise<void> {\n  const response = await fetch(url, { method: \"PUT\", body, signal: AbortSignal.timeout(30_000) });\n  if (!response.ok) throw new Error(`upload failed: ${response.status}`);\n}\n"}],"fixedFiles":[],"focusPath":"src/app/actions.ts","id":"native-fetch-action","outcome":"accept","scenarioId":"primary","title":"A server action uploads with native fetch"}],"filePatterns":[],"id":"prefer-native-fetch-in-server-actions","key":"oxlint:prefer-native-fetch-in-server-actions","languages":["typescript"],"limitations":["Only modules whose directive prologue contains `use server` are checked.","Type-only imports are allowed; ky reached through a shared client module is not followed."],"messageIds":["preferNativeFetchInServerActions"],"optionsSchema":null,"rationale":"ky wraps request bodies and responses for browser runtimes; in Next.js server actions it has failed on binary uploads with `ArrayBuffer.slice` errors, and its errors copy the signed request URL into messages and metadata.","references":[],"remediation":"Use the platform `fetch` with an explicit `signal: AbortSignal.timeout(...)` and check `response.ok` in the server action.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-native-fetch-in-server-actions.ts","status":"active","summary":"Server action modules should call native fetch instead of importing ky.","test":"packages/typescript/tests/rules/prefer-native-fetch-in-server-actions.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:prefer-native-random-uuid","languages":["typescript"],"limitations":["Only resolved zero-argument UUID v4 calls are reported; customized and other UUID versions are excluded. Automatic fixes require an exact closed UUID-only module, explicit string APIs, an already-used bound crypto.randomUUID provider, and another already-read NIL import that preserves uuid package evaluation. Canonical unmodified uuid, Web Crypto and Function.bind are assumed; entropy traces may intentionally differ. Other shapes retain suggestions requiring unshadowed globalThis and comment-free calls; verify native randomUUID availability in the deployment runtime."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"performance","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"case.ts","source":"import { createHash } from 'node:crypto'; export const digest = createHash('sha256').update('value').digest('hex');"}],"fixedFiles":[],"focusPath":"case.ts","id":"mutable-one-shot-chain","outcome":"reject","scenarioId":"primary","title":"Avoid mutable state for one value"},{"expectedCount":1,"files":[{"path":"case.ts","source":"const crypto = require('node:crypto'); export const digest = crypto.createHash('sha256').update('value').digest('hex');"}],"fixedFiles":[],"focusPath":"case.ts","id":"native-namespace","outcome":"reject","scenarioId":"stable-native-namespace","title":"An unchanged namespace retains native provenance"},{"expectedCount":0,"files":[{"path":"case.ts","source":"import { hash } from 'node:crypto'; export const digest = hash('sha256', 'value', 'hex');"}],"fixedFiles":[],"focusPath":"case.ts","id":"one-shot-hash","outcome":"accept","scenarioId":"primary","title":"Use Node's one-shot hash API"},{"expectedCount":0,"files":[{"path":"case.ts","source":"const crypto = require('node:crypto'); crypto.createHash = () => ({ update() { return this; }, digest() { return 'custom'; } }); export const digest = crypto.createHash('sha256').update('value').digest('hex');"}],"fixedFiles":[],"focusPath":"case.ts","id":"replaced-namespace-member","outcome":"accept","scenarioId":"stable-native-namespace","title":"A replaced member has a different contract"}],"filePatterns":[],"id":"prefer-node-crypto-hash","key":"oxlint:prefer-node-crypto-hash","languages":["typescript"],"limitations":["Only direct imports, const bindings, and inline calls with statically proven provenance from crypto or node:crypto are analyzed; arbitrary assignments and dynamic module specifiers are excluded.","Only a literal algorithm with exactly one update call is reported; streaming and incremental hashes remain valid.","Namespace bindings must remain read-only: visible writes, aliases, exports, inherited mutation or alias methods, unknown computed members, and other escaping uses are excluded; type-only references and direct member calls remain eligible.","CommonJS loads and default imports share one mutable native module object. A visible unsafe use of that object excludes its other loads; ESM named exports remain separate unless visible native export synchronization can update them.","Automatic fixes require both existing canonical named node:crypto value imports, an already-read stable hash alias, a retained runtime createHash call outside every reportable one-shot chain, literal sha256/string/hex arguments, no shadowing/comments/optional chains, and no other value/side-effect imports, module augmentations, mock/loader APIs, test/story/support ownership or visible crypto/prototype/reflection authority changes.","Runtime support, missing imports, receiver-binding changes, other encodings or algorithms, and streaming data remain manual. The automatic subset reuses existing named imports and never inserts new ones; no guaranteed speedup is promised."],"messageIds":["preferNodeCryptoHash"],"optionsSchema":null,"rationale":"A createHash-update-digest chain allocates mutable streaming state for a single in-memory value; Node's built-in hash function expresses the one-shot operation directly and can use its optimized fast path.","references":["https://nodejs.org/api/crypto.html#cryptohashalgorithm-data-options"],"remediation":"On a supported Node runtime, consider hash(algorithm, value, encoding). Preserve the output encoding explicitly: digest() returns a Buffer, while hash defaults to hex. Keep createHash for streams or multiple updates.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-node-crypto-hash.ts","status":"active","summary":"Prefer the modern one-shot node:crypto hash API when streaming state is unnecessary.","test":"packages/typescript/tests/rules/prefer-node-crypto-hash.test.ts"},{"aliases":[],"autofix":"none","category":"performance","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/store.ts","source":"import { readFile } from 'node:fs/promises'; export async function load(path: string) { return readFile(path, 'utf8'); }"}],"fixedFiles":[],"focusPath":"src/store.ts","id":"async-read","outcome":"accept","scenarioId":"primary","title":"Use the promise API"},{"expectedCount":1,"files":[{"path":"src/store.ts","source":"import { readFileSync } from 'node:fs'; export function load(path: string) { return readFileSync(path, 'utf8'); }"}],"fixedFiles":[],"focusPath":"src/store.ts","id":"sync-read","outcome":"reject","scenarioId":"primary","title":"Do not block on a synchronous read"}],"filePatterns":[],"id":"prefer-node-fs-promises","key":"oxlint:prefer-node-fs-promises","languages":["typescript"],"limitations":["Tests and generated files are excluded.","This recommendation applies to Node-compatible runtimes. Changing a synchronous API to a promise changes its caller contract; review startup-only work and APIs that require synchronous execution rather than mechanically adding await.","ESLint rule implementations under src/rules are excluded because visitor creation and execution are synchronous by contract.","Only statically identifiable node:fs loads are inspected; filesystem objects passed through arbitrary functions or assignments require type-aware analysis."],"messageIds":["preferAsyncFs"],"optionsSchema":null,"rationale":"Synchronous filesystem work blocks the event loop and can stall unrelated daemon, server, and worker tasks.","references":[],"remediation":"Import the promise API from node:fs/promises and await it; use FileHandle.sync only where a documented durability boundary requires it.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-node-fs-promises.ts","status":"active","summary":"Prefer promise-based Node.js filesystem APIs over synchronous calls in production modules.","test":"packages/typescript/tests/rules/prefer-node-fs-promises.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/store.ts","source":"type AccountId = string & { readonly account: unique symbol }; type InvoiceId = string & { readonly invoice: unique symbol }; function load(accountId: AccountId, invoiceId: InvoiceId): void {}"}],"fixedFiles":[],"focusPath":"src/store.ts","id":"branded-identifiers","outcome":"accept","scenarioId":"primary","title":"Distinguish domain identifiers"},{"expectedCount":1,"files":[{"path":"src/store.ts","source":"function load(accountId: string, invoiceId: string): void {}"}],"fixedFiles":[],"focusPath":"src/store.ts","id":"raw-identifiers","outcome":"reject","scenarioId":"primary","title":"Do not accept interchangeable identifiers"}],"filePatterns":[],"id":"prefer-nominal-id-types","key":"oxlint:prefer-nominal-id-types","languages":["typescript"],"limitations":["Automatic fixes only copy two existing distinct exported string-brand parameter types from a sole public overload into its empty ordinary implementation. The complete module must contain only those two nongeneric brands and the overload/implementation pair; comments, imports, additional declarations, defaults/rest/optional parameters and all operational bodies remain manual.","Requires two distinct Id/Ids or _id/_ids parameter roles with the same proven string, number, or homogeneous array carrier. Plural names require array carriers. Scalars and arrays remain distinct; object fields, destructuring, tuples, inferred types and generic aliases are not checked.","Resolves only scope-correct file-local transparent aliases, up to 16 levels. Intersections, imported types, ambiguous declarations and mixed branded/raw pairs are deliberately skipped.","Generated files and request, trace, span, correlation, process, thread, timeout and interval identifiers are excluded. Raw wire object schemas are not callable boundaries; required external callable signatures need a local suppression. API and adapter paths are not blanket-excluded."],"messageIds":["preferNominalIds"],"optionsSchema":null,"rationale":"Two identifier roles represented by the same primitive accept each other's values, so swapped arguments survive type checking. Transparent aliases do not prevent this.","references":[],"remediation":"Introduce distinct branded identifier types at a validated domain boundary and use them in the callable contract. Keep wire representations primitive until validation; suppress a required external signature locally with a reason.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-nominal-id-types.ts","status":"active","summary":"Distinguish identifier roles sharing primitive types at callable boundaries.","test":"packages/typescript/tests/rules/prefer-nominal-id-types.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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[] | undefined } export type WireInput = Input; function search({ items = [] }: Input) { return items.length; }"}],"fixedFiles":[],"focusPath":"src/search.ts","id":"exported-nullish-input","outcome":"accept","scenarioId":"exported-input","title":"Preserve an exported input contract"},{"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"},{"expectedCount":1,"files":[{"path":"src/search.ts","source":"interface Input { items: string[] | undefined } type WireInput = Input; function search({ items = [] }: Input) { return items.length; }"}],"fixedFiles":[],"focusPath":"src/search.ts","id":"private-nullish-input","outcome":"reject","scenarioId":"exported-input","title":"Review a private input with an empty default"}],"filePatterns":[],"id":"prefer-non-nullable-collection","key":"oxlint:prefer-non-nullable-collection","languages":["typescript"],"limitations":["Automatic fixes remove only an explicit undefined union member from a required inline array property on an unused private nongeneric function parameter that already destructures it with an empty-array default. Null states, shared types, consumers, exports, comments, overloads and declaration files remain manual; runtime defaults are retained.","Recognized defaults and guards are manual modeling prompts, not proof of equivalence for every caller or later use. Recognized exported wire shapes and unknown object escapes are excluded; preserve meaningful null states and boundary compatibility.","Only inline object types or single unmerged top-level local declarations are resolved, using the type namespace. Array and ReadonlyArray names must be unshadowed built-ins; custom and imported constructors are not inferred. Local named, default and export-assignment type exports preserve ownership of their local interface and alias dependencies; arbitrary value exports remain opaque.","Explicit exported variable annotations and signatures of local functions or constant callable aliases preserve input ownership, including callable signatures reached by an exported typeof query. Executable bodies, arbitrary value expressions, inferred exported types, class surfaces and ambiguous callable definitions are not resolved."],"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.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-non-nullable-collection.ts","status":"active","summary":"Suggest reviewing nullish arrays that use local empty-array defaults or a shared null-or-empty guard.","test":"packages/typescript/tests/rules/prefer-non-nullable-collection.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/users.ts","source":"declare const users: readonly ({ id: string } | null)[];\nconst present = users.filter(Boolean);"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"boolean-nullish-filter","outcome":"reject","scenarioId":"primary","title":"Boolean filtering loses nullish narrowing"},{"expectedCount":0,"files":[{"path":"src/users.ts","source":"declare const users: readonly ({ id: string } | null)[];\nconst present = users.filter((user) => user !== null && user !== undefined);"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"explicit-nullish-predicate","outcome":"accept","scenarioId":"primary","title":"Nullish filtering narrows the result"}],"filePatterns":[],"id":"prefer-nullish-filter-predicate","key":"oxlint:prefer-nullish-filter-predicate","languages":["typescript"],"limitations":["Automatic fixes require a fresh literal array in the source's sole top-level const initializer or expression, with no imports, preceding callouts, or source prototype mutation; other reports retain a suggestion.","Browser HTMLAllCollection/document.all and its structural supertypes are excluded because the browser can expose a falsy object.","The receiver must resolve to the built-in Array or ReadonlyArray filter method.","Broad primitive types, falsy literals, any, unknown, generics, intersections, custom filters, and shadowed Boolean bindings are excluded."],"messageIds":["preferNullishPredicate","replaceBoolean"],"optionsSchema":null,"rationale":"An explicit nullish predicate preserves the same runtime elements while letting TypeScript remove `null` and `undefined` from the result.","references":[],"remediation":"Replace `filter(Boolean)` with `filter((value) => value !== null && value !== undefined)`.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-nullish-filter-predicate.ts","status":"active","summary":"Prefer an explicit nullish predicate when `filter(Boolean)` removes only nullish values but does not narrow the result type.","test":"packages/typescript/tests/rules/prefer-nullish-filter-predicate.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/client.ts","source":"async function load(response: 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":"import { z } from 'zod';\nconst UserSchema = z.object({ id: z.string() });\nasync function load(response: Response) { const body = UserSchema.parse(await response.json()); return body.id; }"}],"fixedFiles":[],"focusPath":"src/client.ts","id":"validated-payload","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"accept","scenarioId":"primary","title":"Validate before property access"}],"filePatterns":[],"id":"prefer-schema-for-api-payload","key":"oxlint:prefer-schema-for-api-payload","languages":["typescript"],"limitations":["A JSON.parse call alone does not prove external input; exact native JSON stringify/parse round trips are excluded. Validator implementations and other non-network parsing can still require manual review rather than a schema rewrite.","JSON.parse must resolve to the global JSON object. json() receivers require an unshadowed global Request/Response annotation or construction, or stable local aliases of global fetch results, including native global members and bounded stable const fetch aliases or destructuring; imported, inferred and unknown response types are deliberately not inferred.","Named validators remain conventions, not proof of their implementation. Their exemptions are confined to a valid branch or a preceding same-block validation statement; ignored predicate results and deferred callbacks do not validate later reads.","Test fixtures, generated clients and recognized local-file reads are excluded. This is bounded local analysis, not a general control-flow or mutation proof."],"messageIds":["unparsedJsonAccess"],"optionsSchema":null,"rationale":"External JSON is untrusted at runtime even when its expected TypeScript shape is known statically.","references":[],"remediation":"For external payloads, parse through a schema or establish runtime validation before reading fields. Review validator implementations separately rather than recursively requiring another schema.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","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":"oxlint:prefer-semantic-colors","languages":["typescript"],"limitations":["Class-composition helper objects use literal keys as class fragments; cva/tv configuration objects retain value traversal. Computed keys are not resolved. URL payloads are not color literals.","Email, PDF, video-rendering, print-only, icon artwork, masks, gradients, stories, and explicitly configured non-token projects have targeted exclusions.","Opaque-foreground checks are opt-in and require both a same-variant semantic background class and its package-local declared foreground token."],"messageIds":["arbitraryColor","inlineColor","opaqueForegroundPair","rawPalette"],"optionsSchema":{"additionalProperties":false,"properties":{"opaqueForegroundPairs":{"description":"Use the project semantic foreground roles to check opaque foreground and background pairs.","type":"boolean"},"requireSemanticTokens":{"description":"Restrict checks to projects with an existing semantic token system.","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.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","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":"oxlint:prefer-server-actions","languages":["typescript"],"limitations":["Only statically recognizable /api/ mutations through global fetch or proven Axios imports/instances in use-client modules are reported. Custom wrapper provenance, mutated configuration and unknown option overrides are not inferred; server boundaries and route handlers are excluded."],"messageIds":["preferServerAction"],"optionsSchema":{"additionalProperties":false,"properties":{"basePath":{"description":"Static application URL base path without query strings, fragments or parent segments.","pattern":"^/(?!$)(?!.*[?#])(?!(?:.*/)?\\.\\.?(?:/|$))(?:[^/]+/)*[^/]+$","type":"string"}},"type":"object"},"rationale":"Server Actions can remove a hand-written internal API wrapper while retaining typed application calls. Client invocations still cross a network and serialization boundary.","references":[],"remediation":"Consider a Server Action for application-owned mutations; preserve authorization, input validation and any public API consumers.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-server-actions.ts","status":"active","summary":"Prefer Next.js Server Actions over same-origin API mutations.","test":"packages/typescript/tests/rules/prefer-server-actions.test.ts"},{"aliases":[],"autofix":"none","category":"architecture","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/options.tsx","source":"'use client'; import { useState } from 'react'; export function Options({ rows }: { rows: readonly { language: string }[] }) { const [selected, setSelected] = useState(''); const languages = rows.map(row => row.language); return <select value={selected} onChange={event => setSelected(event.target.value)}>{languages.map(language => <option key={language}>{language}</option>)}</select>; }"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"preserve\"},\"include\":[\"src/**/*\"]}"}],"fixedFiles":[],"focusPath":"src/options.tsx","id":"client-projection","outcome":"reject","scenarioId":"primary","title":"Prepare data before the client view when possible"},{"expectedCount":0,"files":[{"path":"src/options.tsx","source":"'use client'; import { useState } from 'react'; export function Options({ languages }: { languages: readonly string[] }) { const [selected, setSelected] = useState(''); return <select value={selected} onChange={event => setSelected(event.target.value)}>{languages.map(language => <option key={language}>{language}</option>)}</select>; }"},{"path":"tsconfig.json","source":"{\"compilerOptions\":{\"strict\":true,\"target\":\"ESNext\",\"module\":\"ESNext\",\"moduleResolution\":\"Bundler\",\"jsx\":\"preserve\"},\"include\":[\"src/**/*\"]}"}],"fixedFiles":[],"focusPath":"src/options.tsx","id":"prepared-values","outcome":"accept","scenarioId":"primary","title":"The client receives prepared language values"}],"filePatterns":[],"id":"prefer-server-data-preparation","key":"oxlint:prefer-server-data-preparation","languages":["typescript"],"limitations":["Advisory only. Local syntax cannot establish whether a caller is a Server Component, whether props change interactively or whether server preparation reduces the total cost. This rule is a review lead, never proof that code can be moved to the server. No automatic fix is provided.","Requires an explicit use client directive and parser-owned TypeScript services resolving map/filter/flatMap to the standard Array or ReadonlyArray library signature. The receiver beneath erased wrappers must itself have an Array, ReadonlyArray or tuple type after nullish narrowing; casts from opaque or custom collections do not establish this. Subclass and structural collection types outside those native array types are excluded. The containing view must have an independent React useState/useReducer call or a native callable type on an intrinsic DOM onClick/onChange/onInput/onSubmit handler. Nullish, noncallable and opaque handler types do not establish interaction. Noninteractive boundaries remain owned by no-unnecessary-use-client; custom hooks and indirect client requirements are unsupported.","Every runtime reference to the source parameter binding must be a supported native data projection or a field-only JSX display loop. Inputs also needed for editing, whole-record forwarding, lookup, mutation, other fields or opaque helpers are excluded. Files containing direct eval are excluded because it can observe or change lexical bindings; indirect eval and properties named eval do not establish lexical access. Only direct first-parameter paths and synchronous single-expression callbacks are supported. Whole-row identity maps and constant maps are excluded; supported projections must read a fixed input field.","Field-only object and dense array literals can form nested data shapes, including readonly tuples and flatMap outputs. Array holes, spreads, captured values and JSX outputs remain unsupported. Hook recognition follows erased namespace wrappers and excludes direct writes to the requested imported member; reflective or external API patches are not modeled.","JSX-rendering callbacks are never reported as preparation findings. Callback calls, captured values, destructuring defaults/rest/nesting, async callbacks, event handlers, effects, memo callbacks, opaque helpers, dynamic/optional paths, custom collection signatures, missing type services, generated source and tests are excluded. First-parameter provenance is syntactic; annotations cannot prove absence of runtime getters or patched array methods. Analysis does not claim callback purity or preserve semantics of a hypothetical move.","Reports the direct source projection once; chained operations with a call receiver are not independently reported. A static subprojection may still be prepared by its producer while a union with pending local edits remains in the client."],"messageIds":["prepareData"],"optionsSchema":null,"rationale":"Preparing display data where its inputs are loaded can reduce browser work, payloads and knowledge of server data shapes. A small client component can receive the values its interaction actually needs.","references":["https://nextjs.org/docs/app/getting-started/server-and-client-components","https://react.dev/learn/you-might-not-need-an-effect"],"remediation":"When these inputs are available on the server, prepare the collection in its loader or Server Component and pass the prepared values. Otherwise lift the projection to the owning producer when useful. Keep unsaved edits, browser-dependent values and interactive filtering reactive in the client; review callers before moving anything.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-server-data-preparation.ts","status":"active","summary":"Prepare data-only input collections before an interactive client view when possible.","test":"packages/typescript/tests/rules/prefer-server-data-preparation.test.ts"},{"aliases":[],"autofix":"none","category":"style","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:prefer-shadcn-primitives","languages":["typescript"],"limitations":["Native multiple selects and controls with possibly enabled hidden attributes are excluded. Unknown JSX spreads can hide controls; visual equivalence is not inferred.","Hidden and file inputs, unassociated labels, and non-control semantic elements are excluded.","Tests and the shared components/ui primitive implementation tree are excluded.","Package-local project detection is opt-in and fails closed unless components.json, one unambiguous tsconfig/jsconfig alias, the exact primitive module, and its expected export all exist."],"messageIds":["preferShadcnPrimitive"],"optionsSchema":{"additionalProperties":false,"properties":{"assumeAvailable":{"description":"Assume supported shared UI primitives are available in this project.","type":"boolean"},"detectProjectPrimitives":{"description":"Resolve project-owned UI primitives before recommending replacements.","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.","requiresTypeInformation":null,"runtimeImports":[],"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":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":2,"files":[{"path":"src/provider.ts","source":"import { z } from 'zod'; const JobSchema = z.object({ provider: z.enum(['agy', 'claude', 'sol']) }); const StatusSchema = z.object({ provider: z.enum(['agy', 'claude', 'sol']).optional() });"}],"fixedFiles":[],"focusPath":"src/provider.ts","id":"inline-provider","outcome":"reject","scenarioId":"primary","title":"Do not repeat an inline enum domain"},{"expectedCount":0,"files":[{"path":"src/provider.ts","source":"import { z } from 'zod'; const ProviderSchema = z.enum(['agy', 'claude', 'sol']); const JobSchema = z.object({ provider: ProviderSchema }); const StatusSchema = z.object({ provider: ProviderSchema.optional() });"}],"fixedFiles":[],"focusPath":"src/provider.ts","id":"shared-provider","outcome":"accept","scenarioId":"primary","title":"Reuse a named enum schema"}],"filePatterns":[],"id":"prefer-shared-zod-enum","key":"oxlint:prefer-shared-zod-enum","languages":["typescript"],"limitations":["Only two or more exact, same-order direct z.enum calls with string-literal arrays (including no-substitution templates and erased TypeScript wrappers) and no customization argument in one module are inspected; one-off, computed, and customized domains require review. Equal values do not prove a shared business domain: retain local schemas when ownership or future evolution differs, and review initialization order before extraction."],"messageIds":["shareEnumDomain"],"optionsSchema":null,"rationale":"Repeated literal domains hide a shared contract and allow equivalent fields to drift independently.","references":[],"remediation":"Declare a module-level named Zod enum schema and reuse it at each field or contract site.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-shared-zod-enum.ts","status":"active","summary":"Give repeated literal Zod enum domains one reusable module-level schema.","test":"packages/typescript/tests/rules/prefer-shared-zod-enum.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/dispatch.ts","source":"function probe(kind){const other=\"other\";if(kind==='a')return 1;else if(other==='b')return 2;else if(kind==='c')return 3;return 0;}\nconsole.log(JSON.stringify([\"a\",\"b\",\"c\",\"d\"].map(probe)));"}],"fixedFiles":[],"focusPath":"src/dispatch.ts","id":"different-discriminants","outcome":"accept","scenarioId":"native-discriminant","title":"Use native identifier identity"},{"expectedCount":1,"files":[{"path":"src/dispatch.ts","source":"function probe(kind){if(kind==='a')return 1;else if(\\u006bind==='b')return 2;else if(kind==='c')return 3;return 0;}\nconsole.log(JSON.stringify([\"a\",\"b\",\"c\",\"d\"].map(probe)));"}],"fixedFiles":[],"focusPath":"src/dispatch.ts","id":"escaped-middle","outcome":"reject","scenarioId":"native-discriminant","title":"Use native identifier identity"},{"expectedCount":1,"files":[{"path":"src/render.ts","source":"function render(kind: string) { if (kind === 'a') return 'alpha'; else if (kind === 'b') return 'beta'; else if (kind === 'c') return 'gamma'; return 'unknown'; }"}],"fixedFiles":[],"focusPath":"src/render.ts","id":"repeated-equality","outcome":"reject","scenarioId":"primary","title":"Avoid repeating the discriminant"},{"expectedCount":0,"files":[{"path":"src/render.ts","source":"function render(kind: string) { switch (kind) { case 'a': return 'alpha'; case 'b': return 'beta'; case 'c': return 'gamma'; default: return 'unknown'; } }"}],"fixedFiles":[],"focusPath":"src/render.ts","id":"switch-dispatch","outcome":"accept","scenarioId":"primary","title":"Make finite dispatch explicit"}],"filePatterns":[],"id":"prefer-switch-for-repeated-equality","key":"oxlint:prefer-switch-for-repeated-equality","languages":["typescript"],"limitations":["Only direct if/else-if chains with at least three strict-equality tests are reported.","The discriminant must be a resolved lexical variable or parameter; unresolved globals, calls, getters, and indexed reads are excluded. Automatic conversion requires terminating compared branches and an explicit final else or type-proven open discriminant, preserving branch scopes and excluding comments, break statements, and function declarations.","Case values must be primitive literals. Member references and named constants are excluded because evaluating them can observe getters or mutate the selector. Generated sources are excluded.","The rule deliberately ignores compound predicates, loose equality, ranges, and chains that compare different discriminants."],"messageIds":["preferSwitch"],"optionsSchema":null,"rationale":"A switch makes finite dispatch cases visually uniform and easier to extend without duplicating the discriminant.","references":[],"remediation":"Replace three or more strict-equality branches over the same discriminant with a switch; keep if statements for ranges, guards, and heterogeneous predicates.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-switch-for-repeated-equality.ts","status":"active","summary":"Prefer switch over long if/else-if chains that compare one value for strict equality.","test":"packages/typescript/tests/rules/prefer-switch-for-repeated-equality.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/store.test.ts","source":"import {test as base} from 'vitest'; class Store {} const test = base.extend({store: async ({}, use) => { await use(new Store()); }}); test('reads', ({store}) => { const other = new Store(); expect(other).toBeDefined(); });"}],"fixedFiles":[],"focusPath":"src/store.test.ts","id":"duplicate-store","outcome":"reject","scenarioId":"primary","title":"The test constructs the store its fixture supplied"},{"expectedCount":0,"files":[{"path":"src/store.test.ts","source":"import {test as base} from 'vitest'; class Store {} const test = base.extend({store: async ({}, use) => { await use(new Store()); }}); test('reads', ({store}) => { expect(store).toBeDefined(); });"}],"fixedFiles":[],"focusPath":"src/store.test.ts","id":"reuse-store","outcome":"accept","scenarioId":"primary","title":"Request and use the store fixture"}],"filePatterns":[],"id":"prefer-test-fixture-reuse","key":"oxlint:prefer-test-fixture-reuse","languages":["typescript"],"limitations":["Follows unchanged local const Vitest/Playwright test.extend APIs with literal fixture objects. A fixture must supply a source-resolved new-expression directly or through one local const to its use callback. Direct value fixtures are supported.","Only constructors directly in a requesting test callback are compared by lexical binding identity. Renamed fixture parameters and stable constructor aliases are supported; nested callbacks, imported fixture APIs, dynamic factories, spread fixtures, mutable APIs and table-driven tests are excluded.","Reports variants for review without replacing constructor arguments or changing lifecycle. Interface substitution across different concrete constructors requires typed project analysis and is not inferred."],"messageIds":["reuseFixture"],"optionsSchema":null,"rationale":"A second constructor bypasses the fixture's overrides, caching, configuration and cleanup ownership.","references":[],"remediation":"Use the requested fixture. Supply a typed factory fixture when a test requires different configuration or multiple independent instances.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-test-fixture-reuse.ts","status":"active","summary":"Reuse a requested test fixture instead of constructing another instance of its class.","test":"packages/typescript/tests/rules/prefer-test-fixture-reuse.test.ts"},{"aliases":[],"autofix":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/example.ts","source":"declare const shipment: { status: string };\nconst status = shipment.status;"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"after","outcome":"accept","scenarioId":"primary","title":"Use the direct contract"},{"expectedCount":0,"files":[{"path":"src/example.ts","source":"declare function lookup(id: string): string; const args: [string] = [\"u1\"]; lookup(...args);"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"apply-tuple-after","outcome":"accept","scenarioId":"apply-tuple","title":"Keep the callable contract with a stable tuple — after"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"declare function lookup(id: string): string; const args: [string] = [\"u1\"]; Reflect.apply(lookup, undefined, args);"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"apply-tuple-before","outcome":"reject","scenarioId":"apply-tuple","title":"Keep the callable contract with a stable tuple — before"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"declare const shipment: { status: string };\nconst status = Reflect.get(shipment, \"status\");"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"before","outcome":"reject","scenarioId":"primary","title":"Preserve the explicit contract"},{"expectedCount":0,"files":[{"path":"src/example.ts","source":"declare const target: { id: string }; const id = target.id;"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"reflect-alias-after","outcome":"accept","scenarioId":"reflect-alias","title":"Keep the target contract through an alias — after"},{"expectedCount":1,"files":[{"path":"src/example.ts","source":"declare const target: { id: string }; const reflection = Reflect; const id = reflection.get(target, \"id\");"}],"fixedFiles":[],"focusPath":"src/example.ts","id":"reflect-alias-before","outcome":"reject","scenarioId":"reflect-alias","title":"Keep the target contract through an alias — before"}],"filePatterns":[],"id":"prefer-typed-reflection","key":"oxlint:prefer-typed-reflection","languages":["typescript"],"limitations":["Requires TypeScript type information. Inspects scope-resolved global Reflect.get with a literal key (including no-substitution templates and erased TypeScript wrappers) on a closed object type, and Reflect.apply with a known call signature and a literal argument array or stable, unmodified const tuple. Stable local Reflect aliases are resolved.","Dynamic keys, open dictionaries, unknown/any/generic targets, explicit Reflect.get receivers, mutated/escaping aliases, and unknown argument arrays are excluded. Expanded aliases and tuple containers remain manual. Deliberate proxy or metaprogramming calls need a local suppression. Automatic fixes cover comment-free nonoptional Reflect.get calls with a known existing property on an object. Immediate calls/tags, missing keys and Reflect.apply remain manual to preserve receiver and argument contracts."],"messageIds":["avoid"],"optionsSchema":null,"rationale":"Reflect.get and Reflect.apply can accept property or argument mistakes that normal typed access catches.","references":["https://github.com/dmmulroy/anti-slop/blob/13909c9e07590f5a85d93c62712259fe14a883f7/src/rules/no-reflect-get.ts","https://github.com/dmmulroy/anti-slop/blob/13909c9e07590f5a85d93c62712259fe14a883f7/src/rules/no-reflect-apply.ts"],"remediation":"Use direct typed property access or call the typed function, preserving receivers and evaluation semantics.","requiresTypeInformation":true,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-typed-reflection.ts","status":"active","summary":"Prefer typed access when Reflect.get or Reflect.apply discards a known contract.","test":"packages/typescript/tests/rules/prefer-typed-reflection.test.ts"},{"aliases":["strict-test-assertions"],"autofix":"none","category":"testing","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/user.test.ts","source":"expect(users[0]).toEqual('Ada');\nexpect(users[1]).toEqual('Grace');"}],"fixedFiles":[],"focusPath":"src/user.test.ts","id":"indexed-prefix","outcome":"reject","scenarioId":"array-length","title":"Review the collection contract before combining indexed assertions"},{"expectedCount":0,"files":[{"path":"src/user.test.ts","source":"expect(users).toHaveLength(3);\nexpect(users[0]).toEqual('Ada');\nexpect(users[1]).toEqual('Grace');"}],"fixedFiles":[],"focusPath":"src/user.test.ts","id":"length-and-prefix","outcome":"accept","scenarioId":"array-length","title":"A separate exact length can accompany an intentional prefix contract"},{"expectedCount":1,"files":[{"path":"src/user.test.ts","source":"expect(user.id).toBe(1);\nexpect(user.name).toBe('Ada');"}],"fixedFiles":[],"focusPath":"src/user.test.ts","id":"member-run","outcome":"reject","scenarioId":"primary","title":"Consider grouping related data properties"},{"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":"oxlint:prefer-whole-object-assertion","languages":["typescript"],"limitations":["Undefined-property assertions are excluded because whole-object matching can require previously absent properties. Indexed runs with an immediately adjacent literal exact-length assertion are excluded; non-adjacent length checks and intentional prefix contracts remain manual review. No automatic rewrite: grouping can change observable getter or proxy reads."],"messageIds":["assertArrayOnce","combineAssertions"],"optionsSchema":null,"rationale":"One whole-object assertion presents related expectations together and produces a complete structural diff.","references":[],"remediation":"Consider one `toMatchObject` assertion for ordinary data objects. Preserve missing-property checks, identity, and getter or proxy behavior when deciding whether to combine assertions.","requiresTypeInformation":null,"runtimeImports":[],"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":"maintainability","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/status.ts","source":"type Status = 'ran' | 'failed'; function status(value: unknown): Status { if (value === 'ran' || value === 'failed') return value; throw new Error('Invalid status'); }"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"manual-validator","outcome":"reject","scenarioId":"primary","title":"Replace a closed handwritten string parser"},{"expectedCount":0,"files":[{"path":"src/status.ts","source":"import { z } from 'zod'; const StatusSchema = z.enum(['ran', 'failed']); type Status = z.infer<typeof StatusSchema>; function status(value: unknown): Status { return StatusSchema.parse(value); }"}],"fixedFiles":[],"focusPath":"src/status.ts","id":"schema-validator","outcome":"accept","scenarioId":"primary","title":"Define validation and the type from one enum"}],"filePatterns":[],"id":"prefer-zod-enum-validator","key":"oxlint:prefer-zod-enum-validator","languages":["typescript"],"limitations":["Inspects synchronous single-parameter functions explicitly accepting unknown or string, whose complete body is a strict string-equality disjunction, an equivalent reject conjunction, or a closed switch that returns the input and throws otherwise. Boolean membership functions are also inspected.","At least two distinct static string values are required. Membership collections, additional guards, coercion, normalization, transformed returns, methods, contextual parameter types, and multi-parameter functions are outside this bounded detector.","Migration is manual because Zod errors, initialization and constructor hooks are observable; retain intentional failure behavior. A predicate must not be replaced with a throwing parse call. Test and generated files are excluded."],"messageIds":["useEnumSchema"],"optionsSchema":null,"rationale":"Handwritten membership branches and parallel literal types repeat the accepted values and can drift. A named enum schema supplies validation and its inferred TypeScript union.","references":["https://zod.dev/api#enums","https://zod.dev/basics#inferring-types"],"remediation":"Declare a module-level Schema = z.enum([…]), derive its type with z.infer<typeof Schema>, and use Schema.parse(value) or Schema.safeParse(value).success. Preserve any intentional error, fallback, or protocol behavior when migrating.","requiresTypeInformation":false,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-zod-enum-validator.ts","status":"active","summary":"Express closed string validators with a reusable Zod enum schema.","test":"packages/typescript/tests/rules/prefer-zod-enum-validator.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:prefer-zod-infer","languages":["typescript"],"limitations":["Automatic fixes use the original imported Zod namespace and existing schema only for an unused private unmerged interface or object alias with a positively identical shape and no declaration-file ownership or comments. Referenced/public contracts and coarse optional matching remain manual.","Only module-level const schemas and module-level type declarations are paired; local declarations are excluded rather than matched by spelling across scopes.","By default every field must positively agree; collection, nested-object and referenced-schema equivalence is not inferred from outer syntax alone.","This is a bounded syntactic comparison, not general type equivalence. Review schema input versus output, interface augmentation, and separately evolving domain contracts before replacing a declaration.","A module-level string-union alias is also inspected beside a same-named direct enum schema only when every literal agrees and its const binding has no explicit type annotation. Enum alias migrations remain manual; dynamic, widened, transformed, constrained and independently named contracts are excluded."],"messageIds":["handWrittenTwin","repeatedEnumUnion"],"optionsSchema":{"additionalProperties":false,"properties":{"ignoreTypeNames":{"description":"Regular expression patterns for type names to leave unchanged.","items":{"type":"string"},"type":"array"},"requireIdenticalShape":{"description":"Require the schema output and declared type to have identical property shapes.","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>`.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/contracts.ts","source":"export interface ParsedRow { id: string }"},{"path":"src/row.ts","source":"import { z } from \"zod\"; import type { ParsedRow } from \"./contracts.js\"; const RowSchema = z.object({ id: z.string() }); function load(): ParsedRow { return RowSchema.parse({}); }"}],"fixedFiles":[],"focusPath":"src/row.ts","id":"hand-written-parsed-return","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"reject","scenarioId":"primary","title":"Do not hand-write the parsed return shape"},{"expectedCount":0,"files":[{"path":"src/row.ts","source":"import { z } from \"zod\"; const RowSchema = z.object({ id: z.string() }); type Row = z.output<typeof RowSchema>; function load(): Row { return RowSchema.parse({}); }"}],"fixedFiles":[],"focusPath":"src/row.ts","id":"schema-derived-return","installedDependencies":[{"module":"zod","version":"4.6.7"}],"outcome":"accept","scenarioId":"primary","title":"Derive the validated return contract"}],"filePatterns":[],"id":"prefer-zod-parse-output-type","key":"oxlint:prefer-zod-parse-output-type","languages":["typescript"],"limitations":["Requires TypeScript type information and a returned output from a module-level local Zod object schema's `.parse()` or `.safeParse()` result.","Same-module name-correlated twins that `prefer-zod-infer` can prove are left to that established rule; this companion owns richer or renamed same-module and cross-module contracts proven by parse-return data flow.","Only a single plain non-generic object interface or object type alias with exact property keys and bidirectional assignability is reported.","Direct returns, nullish conditional branches, and one immutable local parse-result binding are followed; second aliases, assignments, helper calls, imported schemas, and other indirect data flow are intentionally excluded.","Contracts returned from more than one distinct local schema are excluded because no single schema has unambiguous ownership.","Readonly, augmented, indexed, callable, generated, constrained, any, unknown, and never contracts are excluded rather than guessed.","Nested contracts must preserve property keys, optionality and readonly behavior through aliases, containers and unambiguous unions. Distinct nested classes, accessor or method declarations, opaque members, ambiguous unions, or graphs beyond 128 compared pairs or 16 union alternatives are excluded.","Explicit callable bindings, independently annotated contextual callbacks or object members, and class members fulfilling an annotated heritage contract retain ownership of their named output contract. Inference from the function body alone does not establish ownership; independent helpers remain eligible.","Automatic fixes replace only the bare return annotation of an unused private nongeneric synchronous function returning one direct local-schema parse result. The removed named contract must retain an independent top-level exported type-alias or direct type-export read; references in other removable annotations/assertions or its own definition cannot keep it live. Sole-use contracts, multiple consumers, public/callback/async/nullish/aliased returns, comments, shadowing and missing original Zod import provenance preserve manual ownership; schemas and domain declarations are never moved or exported."],"messageIds":["handWrittenParsedOutput"],"optionsSchema":null,"rationale":"A hand-written contract can drift from the runtime-validated value. Schema ownership determines whether deriving the type preserves the domain architecture.","references":[],"remediation":"When the schema owns the contract, derive it with `z.output<typeof Schema>`. Preserve independently owned domain and public API contracts, and validate schema compatibility with those contracts.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/prefer-zod-parse-output-type.ts","status":"active","summary":"Consider deriving validated return contracts from the Zod schema when the schema owns the contract.","test":"packages/typescript/tests/rules/prefer-zod-parse-output-type.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/roles.test.ts","source":"import { test, expect } from 'vitest'; import { SUPPORTED_ROLES } from './roles.js'; test('membership', () => { expect(SUPPORTED_ROLES).toEqual(['reader', 'writer']); }); test.each(SUPPORTED_ROLES)('%s', role => { expect(build(role)).toBeDefined(); });"}],"fixedFiles":[],"focusPath":"src/roles.test.ts","id":"independent-membership","outcome":"accept","scenarioId":"primary","title":"An independent assertion notices missing entries"},{"expectedCount":1,"files":[{"path":"src/roles.test.ts","source":"import { test, expect } from 'vitest'; import { SUPPORTED_ROLES } from './roles.js'; test.each(SUPPORTED_ROLES)('%s', role => { expect(build(role)).toBeDefined(); });"}],"fixedFiles":[],"focusPath":"src/roles.test.ts","id":"production-cases","outcome":"reject","scenarioId":"primary","title":"Missing production entries also disappear from these cases"}],"filePatterns":[],"id":"production-derived-test-cases","key":"oxlint:production-derived-test-cases","languages":["typescript"],"limitations":["Checks direct first-party relative runtime imports named ALLOWED, ELIGIBLE, REQUIRED or SUPPORTED with underscore-delimited words, passed directly to Vitest each/for tables. Resolves test/it import aliases and unchanged local test.extend runners. Registries, enum introspection, third-party imports and test-support imports remain excluded.","An unconditional module or directly registered test assertion using imported Vitest expect and toEqual/toStrictEqual against a literal array supplies an independent oracle. A stable local const literal table is also accepted when every read belongs to a recognized expectation or case table. Helper-indirected assertions and mutable/captured tables remain outside this bounded proof.","This is a membership-contract review advisory, not a deletion instruction. It does not infer exhaustive coverage, read imported source or automatically remove dynamically generated tests. Generated/vendor files are excluded."],"messageIds":["independentCases"],"optionsSchema":null,"rationale":"A test table copied from the production allowed or supported collection changes together with that collection, so removed members silently remove their test cases.","references":[],"remediation":"Declare a literal expected membership table, assert the production collection equals it, and drive cases from that table. Keep deliberate dynamic sweeps with an explained local suppression.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/production-derived-test-cases.ts","status":"active","summary":"Keep an independent membership oracle when production collections supply test cases.","test":"packages/typescript/tests/rules/production-derived-test-cases.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:repeated-static-call-cases","languages":["typescript"],"limitations":["Only consecutive top-level assertions with direct calls and static inputs and expected values are checked. Test-local callees and fixtures are excluded; imported or outer functions can still be stateful, so manual independence review is required. Not every runner supports test.each."],"messageIds":["repeatedStaticCallCases"],"optionsSchema":null,"rationale":"Copy-pasted cases obscure the input table, and a thrown assertion can stop later cases from being reported.","references":[],"remediation":"If the calls are independent, use the runner's named parameterized cases or subtests. Preserve ordered state-transition scenarios as one test.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/repeated-static-call-cases.ts","status":"active","summary":"Review three or more consecutive static-input call assertions as potential named cases.","test":"packages/typescript/tests/rules/repeated-static-call-cases.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/render.ts","source":"function assertNever(value: never): never { throw new Error(`Unexpected kind: ${String(value)}`); }\ndeclare 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":"oxlint:require-assert-never","languages":["typescript"],"limitations":["Requires type information and singleton case values covering the current union. The helper must accept never to provide a compile-time check; its spelling alone is not a proof of that contract. Review the desired runtime behavior for unexpected external values.","Unique-symbol alternatives are compared by native compiler type identity. Distinct symbols can share the same printed type name; a handled symbol or its typed alias covers only that identity. Unavailable literal names do not establish coverage.","Automatic fixes add void (parameter satisfies never), which only reads the existing parameter at runtime and introduces no helper dependency when the exported function is serialized on its own. The complete comment-free module must contain one exported ordinary function, optionally preceded by an exactly empty private ordinary assertNever helper with one never parameter and explicit void return. The dispatcher needs one string-literal-union parameter, an explicit primitive return type, a complete switch with primitive literal returns and a final empty or break default, followed by a primitive fallback return. Other module statements, imports, defaults, aliases, async functions and dynamic behavior remain manual."],"messageIds":["missingAssertNever"],"optionsSchema":null,"rationale":"An empty default silently accepts new union members instead of making the compiler identify the missing case.","references":[],"remediation":"Use `void (value satisfies never)` or a helper accepting never, preserving the intended runtime fallback.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-assert-never.ts","status":"active","summary":"Require a never witness in empty switch defaults 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":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/action.tsx","source":"<button aria-label=\"Close\"><svg aria-hidden=\"true\" /></button>"}],"fixedFiles":[],"focusPath":"src/action.tsx","id":"named-button","outcome":"accept","scenarioId":"primary","title":"Provide the action name"},{"expectedCount":1,"files":[{"path":"src/action.tsx","source":"<button><svg aria-hidden=\"true\" /></button>"}],"fixedFiles":[],"focusPath":"src/action.tsx","id":"unnamed-button","outcome":"reject","scenarioId":"primary","title":"Name an icon action"}],"filePatterns":[],"id":"require-button-accessible-name","key":"oxlint:require-button-accessible-name","languages":["typescript"],"limitations":["Native button and conventional Button elements are checked by default. Other custom buttons require exact module/export configuration; aliases and namespace imports are resolved by scope. Lucide icons are recognized by default; other icon modules are configurable.","Composed JSX passed as props, render/asChild adapters, dynamic labels, children, unknown components, and prop spreads are left to runtime accessibility testing. Referenced IDs, CSS visibility, and meaningful wording are not verified. Generated files, tests, fixtures, and stories are excluded. No automatic labels are invented.","A hidden prop with unshadowed undefined or exact void 0 does not hide the element or its children. Shadowed undefined and dynamic hidden values remain unproven."],"messageIds":["missingName"],"optionsSchema":{"additionalProperties":false,"properties":{"components":{"items":{"additionalProperties":false,"properties":{"export":{"minLength":1,"type":"string"},"module":{"minLength":1,"type":"string"}},"required":["module","export"],"type":"object"},"type":"array"},"iconModules":{"description":"Module names whose imported components count as icons.","items":{"minLength":1,"type":"string"},"type":"array","uniqueItems":true}},"type":"object"},"rationale":"An icon-only action without an accessible name cannot communicate its purpose to assistive technology.","references":[],"remediation":"Provide meaningful visible or visually hidden text, aria-label, or aria-labelledby for the action.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-button-accessible-name.ts","status":"active","summary":"Require an accessible name on statically unnamed native or configured JSX buttons.","test":"packages/typescript/tests/rules/require-button-accessible-name.test.ts"},{"aliases":[],"autofix":"safe","category":"style","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/calls-date-range.ts","source":"export function range(): { dateFrom: string; dateTo: string } { return { dateFrom: '', dateTo: '' }; }"}],"fixedFiles":[],"focusPath":"src/calls-date-range.ts","id":"camelcase-application-shape","outcome":"accept","scenarioId":"primary","title":"Use camelCase for an application-facing return shape"},{"expectedCount":2,"files":[{"path":"src/calls-date-range.ts","source":"export function range(): { date_from: string; date_to: string } { return { dateFrom: '', dateTo: '' }; }"}],"fixedFiles":[],"focusPath":"src/calls-date-range.ts","id":"snake-case-application-shape","outcome":"reject","scenarioId":"primary","title":"Do not use unquoted wire casing as application property syntax"}],"filePatterns":[],"id":"require-camelcase-properties","key":"oxlint:require-camelcase-properties","languages":["typescript"],"limitations":["Quoted string keys and bracket access are treated as explicit external-wire boundaries.","Computed, numeric, symbol, private, generated, and PascalCase property names are excluded. External-library ownership is not inferred: preserve its spelling using quoted keys and bracket access, or an exact local suppression.","Automatic renaming is limited to an unused private interface or object type alias without heritage, exports, collisions, polymorphic this, computed/indexed keys, merged declarations, or eval. Runtime objects, referenced types and protocol boundaries remain manual."],"messageIds":["requireCamelcaseProperty"],"optionsSchema":null,"rationale":"Unquoted snake_case property syntax makes external wire naming indistinguishable from application-domain naming and lets inconsistent contracts spread through typed code.","references":[],"remediation":"Rename application properties to camelCase. At an external wire boundary, make the exception explicit with a quoted key and bracket access.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-camelcase-properties.ts","status":"active","summary":"Require unquoted lower snake_case TypeScript properties and dot access to use camelCase.","test":"packages/typescript/tests/rules/require-camelcase-properties.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/options.ts","source":"import { z } from 'zod'; const OptionsSchema = z.object({ limit: z.number().int().positive(), version: z.int().nonnegative() });"}],"fixedFiles":[],"focusPath":"src/options.ts","id":"constrained-fields","outcome":"accept","scenarioId":"primary","title":"Declare the numeric domain"},{"expectedCount":1,"files":[{"path":"src/options.ts","source":"import { z } from 'zod'; const OptionsSchema = z.object({ limit: z.number() });"}],"fixedFiles":[],"focusPath":"src/options.ts","id":"unconstrained-limit","outcome":"reject","scenarioId":"primary","title":"A number is broader than a limit"}],"filePatterns":[],"id":"require-constrained-schema-integer","key":"oxlint:require-constrained-schema-integer","languages":["typescript"],"limitations":["This is the requested field policy for direct limit/version properties of runtime-imported Zod object/strictObject/looseObject schemas. Recognizes direct numeric and coerce constructors plus ordinary same-chain bounds and wrappers. Does not infer domains from arbitrary numeric variable names.","Named schema aliases, imported domain schemas, unions, callbacks and transformations are excluded. Dynamic lower bounds count as an explicit reviewed constraint; generated/vendor files are excluded, and maintained tests are checked.","Version zero remains valid; limits require at least one. Numeric custom sentinels require a precise named domain schema or an explained local suppression. No automatic runtime contract change is offered."],"messageIds":["constrainInteger"],"optionsSchema":null,"rationale":"A bare number schema accepts fractions and negative limits or versions. Encoding the domain in validation makes downstream arithmetic and pagination contracts explicit.","references":[],"remediation":"Use an integer schema with a positive lower bound for limit and a nonnegative lower bound for version, or reuse an existing named domain schema.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-constrained-schema-integer.ts","status":"active","summary":"Declare integer and lower-bound validation for numeric limit and version schema fields.","test":"packages/typescript/tests/rules/require-constrained-schema-integer.test.ts"},{"aliases":[],"autofix":"safe","category":"architecture","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/service.ts","source":"abstract class Publisher { abstract publish(): void } class Consumer { constructor(readonly publisher: Publisher) { if (!(publisher instanceof Publisher)) throw new TypeError('Nominal publisher required'); } } class FakePublisher extends Publisher { publish() {} } new Consumer(new FakePublisher());"}],"fixedFiles":[],"focusPath":"src/service.ts","id":"runtime-nominal-guard-with-inherited-fake","outcome":"accept","scenarioId":"primary","title":"The fake satisfies the existing nominal runtime contract"},{"expectedCount":1,"files":[{"path":"src/service.ts","source":"abstract class Publisher { abstract publish(): void } class Consumer { constructor(readonly publisher: Publisher) { if (!(publisher instanceof Publisher)) throw new TypeError('Nominal publisher required'); } } class FakePublisher { publish() {} } new Consumer(new FakePublisher());"}],"fixedFiles":[],"focusPath":"src/service.ts","id":"runtime-nominal-guard-with-structural-fake","outcome":"reject","scenarioId":"primary","title":"A structurally compatible fake fails the constructor's runtime guard"}],"filePatterns":[],"id":"require-explicit-contract-implementation","key":"oxlint:require-explicit-contract-implementation","languages":["typescript"],"limitations":["Requires direct construction of both owned consumer and collaborator classes, with a first-statement consumer constructor guard rejecting the exact parameter with if (!(parameter instanceof Contract)) throw. Parentheses preserve exact constructor and guard symbol identity; casts remain excluded.","The contract must resolve to an abstract class without decorators, static blocks, unresolved ancestors, or custom computed static members in its resolved hierarchy. Escaped class constructors are checked across every owned module in the typed program.","Structural interfaces, non-rejecting branches, reassignment before the guard, casts, unresolved heritage, dynamic factories, implementation static blocks, explicit implementation constructors or initializers, involved modules referencing prototype-mutator members or assigning prototypes, escaped class constructors, and third-party classes are excluded.","Automatic edits only add the existing abstract base to a closed explicit module with exactly three private classes and one direct construction. The base has only one abstract void method and no constructor, fields, heritage, decorators or static work; the implementation has the matching empty method. The consumer retains the exact readonly parameter and TypeError nominal rejection guard. No guard is weakened. Other runtime inheritance or initialization remains manual; this intentionally repairs the diagnosed startup TypeError."],"messageIds":["declareActualContract"],"optionsSchema":null,"rationale":"A structurally assignable fake can still fail a constructor's nominal runtime guard. Pure structural interfaces do not require an implements clause.","references":[],"remediation":"Use an implementation inheriting the existing abstract class, or remove the nominal guard if the boundary is intentionally structural.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-explicit-contract-implementation.ts","status":"active","summary":"Require nominal inheritance when an injected abstract class is rejected by an instanceof guard.","test":"packages/typescript/tests/rules/require-explicit-contract-implementation.test.ts"},{"aliases":[],"autofix":"safe","category":"architecture","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/service.ts","source":"interface Runner { run(): void } class OrchestratorService implements Runner { constructor(private readonly worker: Worker) {} run(): void { this.worker.run(); } }"}],"fixedFiles":[],"focusPath":"src/service.ts","id":"explicit-single-operation-contract","outcome":"accept","scenarioId":"primary","title":"The service declares its operation"},{"expectedCount":1,"files":[{"path":"src/service.ts","source":"class OrchestratorService { constructor(private readonly worker: Worker) {} run(): void { this.worker.run(); } }"}],"fixedFiles":[],"focusPath":"src/service.ts","id":"single-operation-service-without-contract","outcome":"reject","scenarioId":"primary","title":"A single service operation still needs its contract"}],"filePatterns":[],"id":"require-explicit-service-contract","key":"oxlint:require-explicit-service-contract","languages":["typescript"],"limitations":["Automatic fixes only add the exact existing closed type alias named by an already-authored direct factory return annotation. A complete comment-free four-declaration module must contain one exported closed object type alias containing one zero-argument void method, one local matching worker interface, one ordinary nonabstract nongeneric class with an empty constructor retaining a private readonly worker parameter and one matching public forwarding method, and one exported ordinary required-worker-parameter factory returning that exact new class as the port. Unique lexical bindings, no heritage/decorators/defaults/imports/merges/other members or authority, and exact existing types/calls are required. Mergeable public interfaces are refused because downstream declaration augmentation can introduce a private-field compatibility error. No contract, method, field, import or name is invented; emitted JavaScript remains identical. Other shapes remain manual.","The rule checks service, store, and provider class declarations whose constructor stores a typed behavioral collaborator and whose public method reaches that collaborator.","Existing local interfaces, type aliases, and inherited contracts can cover the operations; the compiler owns signature compatibility.","Imported or dynamic base classes with unavailable declarations and decorated classes or operations are unresolved and do not produce a finding.","Constructor retention requires an untouched parameter property or one direct, unconditional assignment. Written or replaced operation members do not prove their declared bodies execute; external aliases, mutators, and callbacks are not resolved.","Generated files and JavaScript without implements syntax are excluded."],"messageIds":["requireExplicitServiceContract"],"optionsSchema":null,"rationale":"A one-operation service can orchestrate a collaborator without declaring the contract that callers consume and implementations and fakes satisfy.","references":[],"remediation":"Explicitly implement a focused interface or extend an abstract class covering the service operations. Export the contract only when its consumers need it.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-explicit-service-contract.ts","status":"active","summary":"Require explicit contracts for classes that invoke retained collaborators.","test":"packages/typescript/tests/rules/require-explicit-service-contract.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:require-fetch-timeout","languages":["typescript"],"limitations":["Signal presence establishes an explicit cancellation path, not a guaranteed timeout. Literal null, global undefined, and void 0 do not provide a signal. Erased TypeScript wrappers preserve the same analysis. Unknown spreads and dynamic values are not inferred. Forwarded Request objects retain their signal unless an init explicitly resets it to null.","Unshadowed native fetch members and bounded stable const aliases or destructuring are recognized. Reassigned, defaulted and unknown aliases and arbitrary forwarding functions are excluded.","Visible writes, callable escapes, whole-global escapes and mutation capabilities anywhere in the file invalidate native fetch identity, including earlier captures. Known unrelated global member writes and typeof global checks do not. Static constructor members and destructuring projections invalidate the proof; indirect reflection and external monkeypatches are not inferred.","Accessors and unproved prototypes are excluded because native option conversion can attach or replace a signal before it is read."],"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.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-fetch-timeout.ts","status":"active","summary":"Require an explicit abort signal on locally analyzable global fetch calls.","test":"packages/typescript/tests/rules/require-fetch-timeout.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/input.tsx","source":"<textarea onKeyDown={e => { if (e.nativeEvent.isComposing || e.nativeEvent.keyCode === 229) return; if (e.key === \"Enter\") { e.preventDefault(); e.currentTarget.form?.requestSubmit(); } }} />"}],"fixedFiles":[],"focusPath":"src/input.tsx","id":"guarded-composition","outcome":"accept","scenarioId":"primary","title":"guarded composition"},{"expectedCount":1,"files":[{"path":"src/input.tsx","source":"<textarea onKeyDown={e => { if (e.key === \"Enter\") e.currentTarget.form?.requestSubmit(); }} />"}],"fixedFiles":[],"focusPath":"src/input.tsx","id":"interrupted-composition","outcome":"reject","scenarioId":"primary","title":"interrupted composition"}],"filePatterns":[],"id":"require-ime-safe-enter-handler","key":"oxlint:require-ime-safe-enter-handler","languages":["typescript"],"limitations":["Only inline editable input/textarea keydown handlers with direct Enter equality (optionally combined with !shiftKey) and currentTarget.blur() or currentTarget.form.requestSubmit() are checked. Unknown guards and rebound event parameters remain unproven. Referenced handlers, custom components, arbitrary submission helpers, logical/conditional branches and DOM listeners need review.","The presence of a composition/keyCode/modifier check or composition listener suppresses the warning conservatively; this does not prove the guard dominates the action or implements the correct Safari fallback."],"messageIds":["composition"],"optionsSchema":null,"rationale":"The Enter key confirms Japanese, Chinese and Korean composition as well as triggering custom actions. An unguarded native form submission or blur interrupts composing text.","references":["https://github.com/GoogleChrome/modern-web-guidance/blob/4dd968349e54ca45a05dd5c818fb035c9054b022/skills/modern-web-guidance/guides/forms/ime-safe-enter-submit.md"],"remediation":"Check the native keyboard event composition state before the action. Retain the Safari keyCode 229 or composition-timestamp fallback required by the browser policy; offer a native submit button.","requiresTypeInformation":false,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-ime-safe-enter-handler.ts","status":"active","summary":"Preserve IME composition before Enter submits or blurs a native text control.","test":"packages/typescript/tests/rules/require-ime-safe-enter-handler.test.ts"},{"aliases":[],"autofix":"safe","category":"architecture","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/artifact-store.ts","source":"export class ArtifactStore { async read(id: string) { return new Uint8Array(); } }"}],"fixedFiles":[],"focusPath":"src/artifact-store.ts","id":"concrete-only-store","outcome":"reject","scenarioId":"primary","title":"Do not export behavior only through a concrete class"},{"expectedCount":0,"files":[{"path":"src/artifact-store.ts","source":"export interface ArtifactStorage { read(id: string): Promise<Uint8Array>; } export class ArtifactStore implements ArtifactStorage { async read(id: string) { return new Uint8Array(); } }"}],"fixedFiles":[],"focusPath":"src/artifact-store.ts","id":"implemented-store-contract","outcome":"accept","scenarioId":"primary","title":"Export behavior through a focused contract"}],"filePatterns":[],"id":"require-interface-for-exported-class","key":"oxlint:require-interface-for-exported-class","languages":["typescript"],"limitations":["Automatic fixes only add the exact existing closed type alias named by an already-authored direct factory return annotation. A complete comment-free four-declaration module must contain one exported closed object type alias containing one zero-argument void method, one local matching worker interface, one ordinary nonabstract nongeneric class with an empty constructor retaining a private readonly worker parameter and one matching public forwarding method, and one exported ordinary required-worker-parameter factory returning that exact new class as the port. Unique lexical bindings, no heritage/decorators/defaults/imports/merges/other members or authority, and exact existing types/calls are required. Mergeable public interfaces are refused because downstream declaration augmentation can introduce a private-field compatibility error. No contract, method, field, import or name is invented; emitted JavaScript remains identical. Other shapes remain manual.","JavaScript files are excluded because implements is TypeScript syntax; imported framework base contracts still require manual policy review.","An exported injected service may also receive require-port-for-service: the two diagnostics intentionally enforce distinct service-boundary and general exported-contract policy scopes.","The rule checks module-level class declarations and direct class-expression values exported directly, through local export specifiers, or through a default identifier; re-exports and expressions wrapped in other calls require review.","An extends clause satisfies the contract only when its target is a locally declared abstract class; imported base-class contracts require an explicit implements clause.","Static factories and data-only classes without public instance methods are outside the contract requirement."],"messageIds":["requireContract"],"optionsSchema":null,"rationale":"An explicit contract names the intended public capability separately from implementation details. TypeScript already supports structural compatibility; this is an architecture policy, not a prerequisite for substitution.","references":[],"remediation":"Declare a focused interface and add an implements clause, or inherit from an intentional base contract.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-interface-for-exported-class.ts","status":"active","summary":"Require exported concrete classes with public behavior to declare a contract.","test":"packages/typescript/tests/rules/require-interface-for-exported-class.test.ts"},{"aliases":[],"autofix":"none","category":"security","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"auth.ts","source":"import { jwtVerify } from 'jose'; function verify(token, key) { return jwtVerify(token, key); }"}],"fixedFiles":[],"focusPath":"auth.ts","id":"optional-expiration","outcome":"reject","scenarioId":"primary","title":"A signature alone does not require expiration"},{"expectedCount":0,"files":[{"path":"auth.ts","source":"import { jwtVerify } from 'jose'; function verify(token, key) { return jwtVerify(token, key, { requiredClaims: ['exp'] }); }"}],"fixedFiles":[],"focusPath":"auth.ts","id":"required-expiration","outcome":"accept","scenarioId":"primary","title":"Require the expiration claim"}],"filePatterns":[],"id":"require-jwt-lifetime","key":"oxlint:require-jwt-lifetime","languages":["typescript"],"limitations":["Checks direct return or concise-arrow-return calls, including await, to runtime ESM-import-proven jose jwtVerify with absent options or a literal options object lacking both lifetime controls. Aliased imports and namespace imports are resolved lexically.","Opaque options, spreads, local claim validation before return, custom verifiers, generated files and tests are excluded. Existing native jose validation is assumed. No automatic protocol policy is invented."],"messageIds":["lifetime"],"optionsSchema":null,"rationale":"JOSE validates exp when present but does not require it by default, so otherwise valid tokens without an expiration may remain accepted indefinitely.","references":["https://github.com/panva/jose/blob/main/docs/jwt/verify/interfaces/JWTVerifyOptions.md"],"remediation":"Set requiredClaims: ['exp'] or a finite maxTokenAge in jwtVerify options. Keep issuer and audience checks appropriate to the protocol.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-jwt-lifetime.ts","status":"active","summary":"Require an expiration claim or a maximum token age when returning a verified JWT.","test":"packages/typescript/tests/rules/require-jwt-lifetime.test.ts"},{"aliases":["zod-naming-convention"],"autofix":"safe","category":"style","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/user.ts","source":"import { z } from 'zod';\nexport const UserSchema = z.object({ id: z.string() });"}],"fixedFiles":[],"focusPath":"src/user.ts","id":"runtime-type-schema-name","outcome":"accept","scenarioId":"primary","title":"Name a reusable schema like a runtime type contract"},{"expectedCount":1,"files":[{"path":"src/user.ts","source":"import { z } from 'zod';\nexport const USER_SCHEMA = z.object({ id: z.string() });"}],"fixedFiles":[],"focusPath":"src/user.ts","id":"screaming-schema-name","outcome":"reject","scenarioId":"primary","title":"Do not name a schema like a scalar constant"}],"filePatterns":[],"id":"require-pascal-case-zod-schema-name","key":"oxlint:require-pascal-case-zod-schema-name","languages":["typescript"],"limitations":["Only module-level bindings proven from a Zod import or a same-file proven schema are checked.","Tests, benchmarks, generated files, imported schemas, re-export aliases, and arbitrary wrapper-factory results are excluded.","Automatic renaming is limited to private module const bindings with no collisions, exports, dynamic eval, or inferred function names. Public names and unsafe reference shapes remain manual."],"messageIds":["requirePascalSchema"],"optionsSchema":null,"rationale":"A reusable Zod schema is a runtime type contract. PascalCase mirrors that role; SCREAMING_SNAKE_CASE should remain reserved for scalar values and lookup tables.","references":[],"remediation":"Rename the binding to PascalCase ending in `Schema` (for example, `MutationRouteBaseSchema`).","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-pascal-case-zod-schema-name.ts","status":"active","summary":"Require confirmed module-level Zod schema contracts to use PascalCase with a `Schema` suffix.","test":"packages/typescript/tests/rules/require-pascal-case-zod-schema-name.test.ts"},{"aliases":["require-interface-for-injected-service"],"autofix":"safe","category":"architecture","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:require-port-for-service","languages":["typescript"],"limitations":["Automatic fixes only add the exact existing closed type alias named by an already-authored direct factory return annotation. A complete comment-free four-declaration module must contain one exported closed object type alias containing one zero-argument void method, one local matching worker interface, one ordinary nonabstract nongeneric class with an empty constructor retaining a private readonly worker parameter and one matching public forwarding method, and one exported ordinary required-worker-parameter factory returning that exact new class as the port. Unique lexical bindings, no heritage/decorators/defaults/imports/merges/other members or authority, and exact existing types/calls are required. Mergeable public interfaces are refused because downstream declaration augmentation can introduce a private-field compatibility error. No contract, method, field, import or name is invented; emitted JavaScript remains identical. Other shapes remain manual.","Retained injection requires an untouched parameter property or a single direct, unconditional constructor assignment. Conditional, repeated, replaced, updated, or deleted stores are unresolved rather than proof of retained injection."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"architecture","code":null,"defaultLevel":"warning","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/users.ts","source":"export function find(database: Database.Database, id: string) { return database.prepare('SELECT id FROM user WHERE id = ?').get(id); }"}],"fixedFiles":[],"focusPath":"src/users.ts","id":"free-query","outcome":"reject","scenarioId":"primary","title":"Do not execute SQL in a free function"},{"expectedCount":0,"files":[{"path":"src/user-repository.ts","source":"export class UserRepository { constructor(private readonly db: Database.Database) {} find(id: string) { return this.db.prepare('SELECT id FROM user WHERE id = ?').get(id); } }"}],"fixedFiles":[],"focusPath":"src/user-repository.ts","id":"injected-repository","outcome":"accept","scenarioId":"primary","title":"Own queries in an injected repository"}],"filePatterns":[],"id":"require-sql-access-class","key":"oxlint:require-sql-access-class","languages":["typescript"],"limitations":["The rule recognizes conventional database receiver names, Cloudflare DB bindings, direct pool.query calls, explicit query-builder terminals, and Prisma-style model delegates; unusually named or heavily aliased clients require architectural review.","Query construction without a recognized execution terminal is intentionally not reported.","Stable local Map, WeakMap, and URLSearchParams instances are excluded. Other conventional receiver names are heuristics, not proof of a database API.","Constructor injection inherited from a base class or transformed through a wrapper is not inferred by this syntax-only rule."],"messageIds":["moveSqlIntoClass"],"optionsSchema":null,"rationale":"An injected repository class is the preferred ownership boundary for database access under this architectural policy; free functions can also express explicit dependencies.","references":[],"remediation":"Move the query into a repository or store class and inject the pool, connection, transaction, or typed database binding through its constructor.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-sql-access-class.ts","status":"active","summary":"Keep SQL reads and writes inside a class that receives its database dependency.","test":"packages/typescript/tests/rules/require-sql-access-class.test.ts"},{"aliases":[],"autofix":"safe","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:require-static-next-matcher","languages":["typescript"],"limitations":["Only directly exported matcher configuration in middleware/proxy entry files is inspected. Literal matcher validity is left to Next.js; this rule checks static syntax, not the complete framework schema.","Automatic edits only inline an existing private unannotated const string literal in a complete two-statement module whose only export is a const config containing that sole matcher reference. The exclusively inlined binding is removed. Imports, comments, arrays, other references or properties, wrappers, annotations, mutable bindings and computed keys remain manual."],"messageIds":["dynamicMatcher"],"optionsSchema":null,"rationale":"Next.js must statically analyze matcher values at build time; computed values are ignored.","references":["https://nextjs.org/docs/app/api-reference/file-conventions/proxy"],"remediation":"Write matcher strings, arrays, and object fields as literals in the exported config.","requiresTypeInformation":null,"runtimeImports":[],"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":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/chart.tsx","source":"<svg aria-hidden=\"true\"><path /></svg>"}],"fixedFiles":[],"focusPath":"src/chart.tsx","id":"decorative-svg","outcome":"accept","scenarioId":"primary","title":"Identify decorative graphics"},{"expectedCount":1,"files":[{"path":"src/chart.tsx","source":"<svg><path /></svg>"}],"fixedFiles":[],"focusPath":"src/chart.tsx","id":"unnamed-svg","outcome":"reject","scenarioId":"primary","title":"Name meaningful graphics"}],"filePatterns":[],"id":"require-svg-accessible-name","key":"oxlint:require-svg-accessible-name","languages":["typescript"],"limitations":["Only intrinsic svg elements are checked. Dynamic labels, forwarded props, and dynamic title content remain runtime concerns. IDs, CSS visibility and graphic meaning are not inferred. Tests, stories, fixtures and generated files are excluded. No automatic title or decorative marker is invented.","A hidden prop with unshadowed undefined or exact void 0 does not hide the element. Shadowed undefined and dynamic hidden values remain unproven."],"messageIds":["missingName"],"optionsSchema":null,"rationale":"Meaningful graphics need a name; decorative graphics should not add unnamed content to the accessibility tree.","references":[],"remediation":"Add aria-label, aria-labelledby, or a direct title child; mark decorative graphics aria-hidden or presentational.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-svg-accessible-name.ts","status":"active","summary":"Require a name or explicit decorative semantics on inline SVG elements.","test":"packages/typescript/tests/rules/require-svg-accessible-name.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"profile-form.tsx","source":"'use client'; import { Controller, useForm } from 'react-hook-form'; function ProfileForm() { const form = useForm({ defaultValues: { name: '' } }); return <Controller control={form.control} name='name' render={() => null} />; }"}],"fixedFiles":[],"focusPath":"profile-form.tsx","id":"controlled-field-with-form-defaults","outcome":"accept","scenarioId":"primary","title":"Initialize controlled fields at form level"},{"expectedCount":1,"files":[{"path":"profile-form.tsx","source":"'use client'; import { Controller, useForm } from 'react-hook-form'; function ProfileForm() { const form = useForm(); return <Controller control={form.control} name='name' render={() => null} />; }"}],"fixedFiles":[],"focusPath":"profile-form.tsx","id":"controlled-field-without-default","outcome":"reject","scenarioId":"primary","title":"Do not leave a controlled field uninitialized"}],"filePatterns":[],"id":"require-use-form-default-values","key":"oxlint:require-use-form-default-values","languages":["typescript"],"limitations":["The rule proves only that an explicit form-level initialization option or field default is present; it does not prove that a particular field path occurs inside a defaultValues/values object.","The rule reports only scope-resolved Controller/useController fields explicitly bound to a directly created useForm control. FormProvider context, wrapper hooks, dynamic options, spreads, computed properties, and interprocedural flows are intentionally left unreported.","Form bindings must have one initializer and remain local and read-only; aliases, exports, property writes, and other escaping uses are left unreported."],"messageIds":["requireUseFormDefaultValues"],"optionsSchema":null,"rationale":"React Hook Form exposes form-level defaultValues/values and field-level defaultValue as its explicit initialization mechanisms. A directly bound controlled field should not omit both mechanisms.","references":[],"remediation":"Provide a non-undefined defaultValues or values option to useForm, or a non-undefined defaultValue on the associated Controller/useController field.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-use-form-default-values.ts","status":"active","summary":"Require explicit form-level initialization or a field default for directly bound Controller fields.","test":"packages/typescript/tests/rules/require-use-form-default-values.test.ts"},{"aliases":[],"autofix":"none","category":"correctness","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:require-use-server-in-actions-file","languages":["typescript"],"limitations":["Only direct named exports of async declarations or initialized functions in actions.ts or *-actions.ts below an app directory are checked. Detached/default exports and other naming schemes are not inferred; functions with their own directive are accepted."],"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":"Use a leading module directive for an action-only module, or retain a function-level directive for an inline Server Action. Do not turn mixed non-action exports into a Server Action module.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","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":"oxlint: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.","An enclosing parse does not validate an earlier call or deferred callback that consumes its input. Direct object/array construction, unshadowed Object.fromEntries and native Number/String/Boolean coercion remain supported; arbitrary preprocessing needs an explicitly reviewed boundary."],"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.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/require-zod-form-validation.ts","status":"active","summary":"Require Zod validation (`Schema.parse(...)` / `Schema.safeParse(...)`) when reading values out of a `FormData` object.","test":"packages/typescript/tests/rules/require-zod-form-validation.test.ts"},{"aliases":[],"autofix":"none","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","examples":[{"expectedCount":0,"files":[{"path":"src/artifact-store.ts","source":"export class ArtifactStore {}"}],"fixedFiles":[],"focusPath":"src/artifact-store.ts","id":"matching-class","outcome":"accept","scenarioId":"primary","title":"Match a class and module"},{"expectedCount":1,"files":[{"path":"src/artifacts.ts","source":"export class ArtifactStore {}"}],"fixedFiles":[],"focusPath":"src/artifacts.ts","id":"mismatched-class","outcome":"reject","scenarioId":"primary","title":"Do not hide a sole class behind another filename"}],"filePatterns":[],"id":"sole-export-matches-filename","key":"oxlint:sole-export-matches-filename","languages":["typescript"],"limitations":["Framework entrypoints, generic stems covered by no-generic-single-export-module, tests, generated files, anonymous defaults, CommonJS, unresolved re-exports, and emission-dependent exports are excluded.","The rule compares the primary filename stem and preserves a single private underscore prefix and conventional suffixes such as .server or .worker. Supporting types do not exempt a sole runtime export; public runtime aliases count as distinct exports.","Exported destructuring patterns are excluded rather than undercounted as public exports.","Next.js metadata routes, instrumentation, and other Next.js and Astro entrypoint exemptions require the nearest package.json to declare the framework, a conventional entrypoint path, and its expected runtime export. Custom routing roots and pageExtensions are not inferred."],"messageIds":["matchSoleExport"],"optionsSchema":null,"rationale":"When a module owns one runtime responsibility, matching names make that responsibility directly discoverable.","references":[],"remediation":"Name the module for the exported responsibility, using the full kebab-case export name; otherwise colocate genuinely related exports.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/sole-export-matches-filename.ts","status":"active","summary":"Make a module filename reflect its sole named public runtime export.","test":"packages/typescript/tests/rules/sole-export-matches-filename.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:source-coupled-test","languages":["typescript"],"limitations":["The rule follows stable lexical bindings, static source paths including JSX/TSX, CSS and HTML, awaited reads, and common text operations. Named imports of node:path/path join and resolve are followed when their complete trailing static path proves a source suffix. Reassigned bindings, dynamic trailing segments, namespace path wrappers, unknown path wrappers, iterator pipelines, and interprocedural flows remain unreported.","Assertion roots are scope-resolved for supported test runners and Node assert imports; local or unknown helpers with assertion-like names are not inferred.","Known fixture, golden, snapshot, and generated source paths are representation contracts and remain unreported; dot segments in the known path are normalized first.","Contained static paths, including stable nested join/resolve descendants, beneath a native mkdtemp/mkdtempSync result are generated artifacts. Absolute or drive-anchored resolve segments and traversal above the proven directory remain unproven. Literal replace/replaceAll nonidentity guards comparing the same raw read remain unreported; fixed-string comparisons remain candidates.","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":"Reuse adequate existing validation; otherwise assert a distinct parsed or runtime contract. Preserve deliberate representation contracts with an exact justified suppression.","requiresTypeInformation":null,"runtimeImports":[],"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":"safe","category":"maintainability","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:stepdown","languages":["typescript"],"limitations":["Generated and test files, cycles, dynamic or escaped references, overload targets, and helpers with multiple callers are excluded; immutable local callable aliases are followed only when every use stays in the caller. Function bodies include nested sibling helpers.","Class helpers must be private; their sole caller may be public, protected, or private by default. Set privateBandOnly alongside accessibility ordering to restrict class reports to private callers; module and nested function ordering is unchanged.","Runtime class-field, static-block, computed-member, and decorator barriers are never crossed; non-hoisted module helpers also cannot cross eager execution or unrelated initialization.","Automatic ordering only applies to an explicit module containing ordinary named function declarations, exactly one exported entry and a reachable acyclic single-caller helper tree, with no comments, eval, overloads, generics or eager statements. Class methods, variables and other module layouts remain manual because reflection, initialization and module cycles can matter."],"messageIds":["helperAboveOnlyCaller"],"optionsSchema":{"additionalProperties":false,"properties":{"privateBandOnly":{"description":"Restrict class reports to private callers when another rule owns accessibility-band ordering.","type":"boolean"}},"type":"object"},"rationale":"Caller-first ordering lets a reader follow the main flow before descending into implementation details.","references":[],"remediation":"Consider moving the private helper below its sole caller after reviewing initialization and reflection dependencies.","requiresTypeInformation":null,"runtimeImports":[],"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":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"src/store.ts","source":"function seed() { db.prepare(`INSERT INTO runs (id) VALUES (?)`).run(); }"}],"fixedFiles":[],"focusPath":"src/store.ts","id":"bare-insert","outcome":"reject","scenarioId":"primary","title":"Review a bare insert in a replay-named callable"},{"expectedCount":0,"files":[{"path":"src/store.ts","source":"function seed() { 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":"Review the conflict policy for a replayed insert"},{"expectedCount":0,"files":[{"path":"src/page.ts","source":"function seed(){db.query(\"SELECT id FROM runs /* INS\\x45RT INTO runs(id,note) VALUES(4,'new') */\");}"}],"fixedFiles":[],"focusPath":"src/page.ts","id":"decoded-comment-data","outcome":"accept","scenarioId":"native-decoded-sql","title":"Ignore native-decoded comment data"},{"expectedCount":1,"files":[{"path":"src/page.ts","source":"function seed(){db.query(\"INS\\u0045RT INTO runs(id,note) VALUES(4,'new')\");}"}],"fixedFiles":[],"focusPath":"src/page.ts","id":"decoded-query","outcome":"reject","scenarioId":"native-decoded-sql","title":"Inspect native-decoded query text"}],"filePatterns":[],"id":"store-insert-requires-on-conflict","key":"oxlint:store-insert-requires-on-conflict","languages":["typescript"],"limitations":["Only the nearest statically named callable is considered; top-level inserts and anonymous callbacks are excluded. Recognized conflict syntax does not prove idempotence or concurrency safety: WHERE NOT EXISTS can race, and INSERT OR REPLACE can delete an existing row. Review unique constraints and dialect semantics manually."],"messageIds":["storeInsertRequiresOnConflict"],"optionsSchema":null,"rationale":"Names such as seed, enqueue, or upsert suggest that repeated execution deserves a conflict-policy review, but do not prove a replay contract.","references":[],"remediation":"Choose conflict handling appropriate to the schema and SQL dialect, or document why this insertion must fail on a duplicate.","requiresTypeInformation":null,"runtimeImports":[],"since":null,"source":"packages/typescript/src/rules/store-insert-requires-on-conflict.ts","status":"active","summary":"Review conflict handling for embedded inserts in replay-named callables.","test":"packages/typescript/tests/rules/store-insert-requires-on-conflict.test.ts"},{"aliases":[],"autofix":"none","category":"testing","code":null,"defaultLevel":"error","engine":"oxlint","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":"oxlint:test-loops-over-literal-cases","languages":["typescript"],"limitations":["Only inline literal for-of cases containing framework assertions are reported. References to setup or parameters owned by the enclosing test, and loops followed by statements in the same or enclosing block, are excluded because they can belong to an ordered scenario. External helper purity is not inferred."],"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.","requiresTypeInformation":null,"runtimeImports":[],"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":"error","engine":"oxlint","examples":[{"expectedCount":1,"files":[{"path":"widget.test.ts","source":"const seed = 1;\n// Arrange\nconst widget = makeWidget();"}],"fixedFiles":[{"path":"widget.test.ts","source":"const seed = 1;\n\nconst 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":"oxlint: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.","requiresTypeInformation":null,"runtimeImports":[],"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":"correctness","code":"SARJ463","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/resources.py","source":"from contextlib import AsyncExitStack\nasync def close():\n    await release()\nasync def run():\n    async with AsyncExitStack() as stack:\n        stack.push_async_callback(close)\n"}],"fixedFiles":[],"focusPath":"app/resources.py","id":"awaited-cleanup","outcome":"accept","scenarioId":"primary","title":"Register cleanup with the async API"},{"expectedCount":1,"files":[{"path":"app/resources.py","source":"from contextlib import AsyncExitStack\nasync def close():\n    await release()\nasync def run():\n    async with AsyncExitStack() as stack:\n        stack.callback(close)\n"}],"fixedFiles":[],"focusPath":"app/resources.py","id":"dropped-cleanup","outcome":"reject","scenarioId":"primary","title":"Synchronous callbacks do not await cleanup"}],"filePatterns":[],"id":"async-cleanup-registered-synchronously","key":"python:async-cleanup-registered-synchronously","languages":["python"],"limitations":["Requires a proven contextlib ExitStack or AsyncExitStack constructor and an undecorated, visible async function, optionally through simple function aliases.","Unknown methods/callables, decorated functions, async generators, conditional bindings, transferred stacks, wildcard imports, and generated files are excluded.","Resource aliases and interprocedural mutation are not inferred. No autofix is offered because executing previously dropped cleanup can raise new exceptions.","Coroutine-function assignment aliases must remain unmutated and unescaped; attribute inspection, containers, returned functions and unknown helper arguments abstain. Direct calls and proven stack registrations remain supported.","Visible namespace APIs, frame/module introspection, code mutation and bound attribute setters invalidate the file's provenance proof."],"messageIds":[],"optionsSchema":null,"rationale":"The synchronous callback API discards the returned coroutine without awaiting it, so the cleanup body never runs.","references":[],"remediation":"Use AsyncExitStack and register the coroutine function with push_async_callback.","since":null,"source":"packages/python/src/sarj_python_lint/rules/async_cleanup_registered_synchronously.py","status":"active","summary":"Do not register a proven async cleanup function with ExitStack.callback.","test":"packages/python/tests/rules/test_async_cleanup_registered_synchronously.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ540","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/lifecycle.py","source":"import asyncio\nfrom concurrent.futures import ThreadPoolExecutor\n\nasync def close():\n    executor = ThreadPoolExecutor()\n    await asyncio.to_thread(executor.shutdown, wait=True)\n"}],"fixedFiles":[],"focusPath":"app/lifecycle.py","id":"shutdown-in-worker-thread","outcome":"accept","scenarioId":"primary","title":"Await waiting shutdown outside the event-loop thread"},{"expectedCount":1,"files":[{"path":"app/lifecycle.py","source":"from concurrent.futures import ThreadPoolExecutor\n\nasync def close():\n    executor = ThreadPoolExecutor()\n    executor.shutdown(wait=True)\n"}],"fixedFiles":[],"focusPath":"app/lifecycle.py","id":"waiting-shutdown-in-async","outcome":"reject","scenarioId":"primary","title":"Waiting shutdown runs on the event-loop thread"}],"filePatterns":[],"id":"async-executor-shutdown","key":"python:async-executor-shutdown","languages":["python"],"limitations":["Requires a stable direct assignment from an import-proven concurrent.futures ThreadPoolExecutor or ProcessPoolExecutor constructor and a direct shutdown call in an executing async function body.","Only omitted wait or literal wait=True is reported. Literal wait=False, dynamic arguments, nested synchronous callbacks, custom factories, borrowed receivers, context-managed bindings and explicit receiver/API mutation are excluded.","Stable enclosing-function and module bindings are recognized. Ordinary argument passing does not erase the constructor identity; external monkey-patching inside opaque consumers and runtime replacement of stdlib modules are not inferred.","Unknown bound-method consumers, receiver aliases/storage, conditional imports, namespace escapes, reflection, wildcard imports and generated files conservatively abstain. No automatic shutdown-policy change is offered."],"messageIds":[],"optionsSchema":null,"rationale":"Executor.shutdown waits for workers by default; invoking it directly in an async function can block the event loop and delay unrelated service work or cancellation.","references":[],"remediation":"Await asyncio.to_thread(executor.shutdown, wait=True), or use the application's explicit nonblocking shutdown policy after arranging worker ownership and completion.","since":null,"source":"packages/python/src/sarj_python_lint/rules/async_executor_shutdown.py","status":"active","summary":"Run waiting executor shutdown outside the async event-loop thread.","test":"packages/python/tests/rules/test_async_executor_shutdown.py"},{"aliases":[],"autofix":"safe","category":"testing","code":"SARJ456","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import AsyncMock\n\nasync def test_delivery():\n    send = AsyncMock()\n    await deliver(send)\n    send.assert_called_once_with('item')\n    send.assert_awaited_once_with('item')\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"call-and-await-async-mock","outcome":"accept","scenarioId":"primary","title":"Call and await assertions protect different contracts"},{"expectedCount":1,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import AsyncMock\n\nasync def test_delivery():\n    send = AsyncMock()\n    await deliver(send)\n    send.assert_called_once_with('item')\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"call-only-async-mock","outcome":"reject","scenarioId":"primary","title":"A call assertion does not prove awaiting"},{"expectedCount":0,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import AsyncMock\n\nasync def test_delivery(expected):\n    send = AsyncMock()\n    await deliver(send)\n    send.assert_called_once()\n    send.assert_has_awaits(expected)\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"dynamic-expected-awaits","outcome":"accept","scenarioId":"await-expectation-boundaries","title":"Unresolved await expectations retain their acceptance"},{"expectedCount":0,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import AsyncMock\nasync def test_delivery():\n    send = AsyncMock()\n    await deliver(send)\n    send.assert_called_once()\n    assert len(send.await_args_list) == 1\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"positive-await-length","outcome":"accept","scenarioId":"await-length-evidence","title":"A positive await-history length distinguishes a dropped coroutine"},{"expectedCount":0,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import AsyncMock\nasync def test_delivery():\n    send = AsyncMock()\n    await send()\n    assert send. called\n    send.assert_awaited_once()\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"spaced-called-with-await-evidence","outcome":"accept","scenarioId":"called-attribute-spacing","title":"Await evidence covers the same spaced call state"},{"expectedCount":1,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import AsyncMock\nasync def test_delivery():\n    send = AsyncMock()\n    pending = send()\n    pending.close()\n    assert send. called\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"spaced-positive-called-state","outcome":"reject","scenarioId":"called-attribute-spacing","title":"A spaced positive call state still needs await evidence"},{"expectedCount":1,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import AsyncMock\n\nasync def test_delivery():\n    send = AsyncMock()\n    await deliver(send)\n    assert send.call_count == 1\n    assert send.await_count >= 0\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"vacuous-await-count","outcome":"reject","scenarioId":"await-expectation-boundaries","title":"A nonnegative await count does not prove execution"},{"expectedCount":1,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import AsyncMock\nasync def test_delivery():\n    send = AsyncMock()\n    await deliver(send)\n    send.assert_called_once()\n    assert len(send.await_args_list) >= 0\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"vacuous-await-length","outcome":"reject","scenarioId":"await-length-evidence","title":"An empty await history still has nonnegative length"}],"filePatterns":[],"id":"async-mock-call-without-await-assertion","key":"python:async-mock-call-without-await-assertion","languages":["python"],"limitations":["Autofix only adds a matching assert_awaited_once[_with] after the original call assertion in an argument-free, undecorated module async test whose exact body creates a fresh unconfigured canonical AsyncMock, awaits it once, and asserts its single call. With-arguments forms require identical primitive literals. Ambiguous imports, mocks, metadata/frame capabilities, fixtures, helpers, configuration, comments, and other statements remain for review; existing call assertions are retained.","Only direct statements in collected test functions and methods are inspected; fixtures, helpers, nested blocks, and generated files are excluded.","Requires a fresh local unittest.mock.AsyncMock constructor with unshadowed imports; aliases, reassignment, reset, and ambiguous configuration are excluded.","Visible provider replacement, escape or unresolved lexical ownership retains WARNING; only the closed canonical AsyncMock provider proof blocks.","Only the mock itself and unchanged immediate children of unspecced, unwrapped mocks are inferred; spec children, return values, and deeper chains are excluded.","Positive call evidence includes direct assertion methods and literal numeric or boolean call-state checks; unresolved call expectations are excluded.","Await evidence excludes provably vacuous checks, including non-None count/list state, zero or nonnegative numeric bounds, and empty expected await sequences; unresolved expectations are accepted without inferring their values.","Length comparisons over await_args_list use count semantics only for an unshadowed builtin len without visible builtin namespace access; custom length functions remain unresolved.","Conjunctions need one accepted await branch; disjunctions need accepted evidence for the same mock in every branch. Argument equality and assertion strength are not compared.","Compound negation and custom expression semantics remain unresolved; no variable values or fixture graphs are inferred.","Custom assertion helpers and intentional scheduling contracts may require a reasoned local suppression."],"messageIds":[],"optionsSchema":null,"rationale":"Calling an AsyncMock records a call before its coroutine is awaited. A positive call assertion can therefore pass when production code silently drops the coroutine. Await assertions cover that gap, while call assertions still detect extra calls whose coroutines were never awaited.","references":[],"remediation":"Keep the call assertion and add an appropriate assert_awaited assertion. For a test that deliberately checks scheduling before execution, use an exact SARJ456 suppression explaining that contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/async_mock_call_without_await_assertion.py","status":"active","summary":"Pair positive AsyncMock call assertions with evidence that the mock was awaited.","test":"packages/python/tests/rules/test_async_mock_call_without_await_assertion.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ537","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/processes.py","source":"import asyncio\nfrom contextlib import suppress\n\nasync def run(args):\n    process = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)\n    try:\n        return await process.communicate()\n    except asyncio.CancelledError:\n        if process.returncode is None:\n            with suppress(ProcessLookupError):\n                process.kill()\n        await process.communicate()\n        raise\n"}],"fixedFiles":[],"focusPath":"app/processes.py","id":"async-child-cancellation-cleanup","outcome":"accept","scenarioId":"primary","title":"Settle the owned child before propagating cancellation"},{"expectedCount":1,"files":[{"path":"app/processes.py","source":"import asyncio\n\nasync def run(args):\n    process = await asyncio.create_subprocess_exec(*args, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE)\n    return await process.communicate()\n"}],"fixedFiles":[],"focusPath":"app/processes.py","id":"async-wait-without-cancellation-cleanup","outcome":"reject","scenarioId":"primary","title":"Cancelling communication can leave the owned child running"}],"filePatterns":[],"id":"async-subprocess-cancellation-cleanup","key":"python:async-subprocess-cancellation-cleanup","languages":["python"],"limitations":["Checks a unique direct local assignment from asyncio.create_subprocess_exec/shell followed by a direct, unguarded await of wait/communicate in the same async function.","Handles must not escape or have reads other than direct awaited wait/communicate calls and later read-only returncode observations. Earlier status observations/polling, try/finally, cancellation handlers, managed wrappers, borrowed/aliased handles, earlier process settlement and unknown lifecycle helpers conservatively abstain.","Visible os child-reaper imports/calls and SIGCHLD ownership exclude the file using the existing subprocess ownership proof. Native API replacement, frame/module introspection, code mutation and bound attribute setters invalidate provenance.","Cross-function lifecycle, fields, dynamic mutation, arbitrary exit paths and generated files are not analyzed. Choosing termination, escalation, process groups and pipe handling requires the application's ownership policy; no automatic rewrite is provided."],"messageIds":[],"optionsSchema":null,"rationale":"Cancelling an asyncio process wait does not terminate its child; a locally owned subprocess can keep running with unsettled pipes and exit status.","references":[],"remediation":"Arrange cancellation cleanup that settles the owned child and preserves caller cancellation under the application's termination and pipe-handling policy.","since":null,"source":"packages/python/src/sarj_python_lint/rules/async_subprocess_cancellation_cleanup.py","status":"active","summary":"Settle locally owned asyncio subprocesses when their caller is cancelled.","test":"packages/python/tests/rules/test_async_subprocess_cancellation_cleanup.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ536","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/lifecycle.py","source":"import asyncio\nfrom contextlib import asynccontextmanager\n\n@asynccontextmanager\nasync def lifespan():\n    task = asyncio.create_task(work())\n    try:\n        yield\n    finally:\n        task.cancel()\n"}],"fixedFiles":[],"focusPath":"app/lifecycle.py","id":"cancel-without-settlement","outcome":"reject","scenarioId":"primary","title":"Cancelling does not wait for the owned child"},{"expectedCount":0,"files":[{"path":"app/lifecycle.py","source":"import asyncio\nfrom contextlib import asynccontextmanager\n\n@asynccontextmanager\nasync def lifespan():\n    task = asyncio.create_task(work())\n    try:\n        yield\n    finally:\n        task.cancel()\n        await asyncio.gather(task, return_exceptions=True)\n"}],"fixedFiles":[],"focusPath":"app/lifecycle.py","id":"join-owned-child","outcome":"accept","scenarioId":"primary","title":"Wait for the child before the lifespan returns"}],"filePatterns":[],"id":"cancel-owned-task-without-join","key":"python:cancel-owned-task-without-join","languages":["python"],"limitations":["Only an import-proven asynccontextmanager async generator with one decorator, a unique direct local task assignment and a direct cancel call in a top-level try/finally finalizer is inspected.","Recognizes a single positional coroutine argument to asyncio.create_task, or ensure_future of a proven local coroutine function call. Borrowed Futures passed to ensure_future are excluded because it can return the same task.","Every read of the task must be a direct cancel method call in the same function. Any await, gather/wait, done/result observation, callback, alias, escape, unknown cleanup helper or transferred supervision conservatively abstains.","Already-settled tasks, explicit eager_start other than literal False, opaque task keyword expansion, conditional/compound ownership, custom task factories, nested callbacks, namespace mutation/reflection, wildcard imports and generated files are not inferred. This advisory proves missing settlement, not a specific downstream resource failure.","The rule offers no automatic cancellation handler: swallowing child cancellation can also swallow a new cancellation of the owner unless the lifecycle policy handles it explicitly."],"messageIds":[],"optionsSchema":null,"rationale":"Task.cancel requests cancellation without waiting for the child to finish; an async lifespan can release resources while its child still runs cancellation cleanup.","references":[],"remediation":"Await the owned task or an appropriate gather/TaskGroup settlement before releasing resources. Preserve cancellation of the owner and choose an explicit policy for unexpected child errors and repeated cancellation.","since":null,"source":"packages/python/src/sarj_python_lint/rules/cancel_owned_task_without_join.py","status":"active","summary":"Join a cancelled locally owned task before releasing its lifecycle.","test":"packages/python/tests/rules/test_cancel_owned_task_without_join.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ437","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/call_store.py","source":"import psycopg\ncursor.execute(\"SELECT call.id FROM simulated_batch JOIN batch_call ON batch_call.batch_id = simulated_batch.batch_id JOIN call ON call.batch_call_id = batch_call.id JOIN campaign_call ON campaign_call.call_id = call.id JOIN campaign ON campaign.id = campaign_call.campaign_id WHERE simulated_batch.batch_id = %s\")\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"multi-join-root-discovery","outcome":"reject","scenarioId":"multi-join-review","title":"A read repeatedly discovers a derived root through multiple joins"},{"expectedCount":0,"files":[{"path":"app/call_store.py","source":"import psycopg\n\ncursor.execute(\"WITH ranked AS (SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rank FROM call), picked AS (SELECT id FROM ranked WHERE rank <= %s) SELECT due.id FROM call AS due JOIN picked ON picked.id = due.id FOR UPDATE OF due SKIP LOCKED\")\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"named-queue-stages","outcome":"accept","scenarioId":"primary","title":"A queue claim exposes ranking as named stages"},{"expectedCount":1,"files":[{"path":"app/call_store.py","source":"import psycopg\n\ncursor.execute(\"SELECT due.id FROM call AS due JOIN (SELECT * FROM (SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rank FROM call) ranked WHERE rank <= %s) picked ON picked.id = due.id FOR UPDATE OF due SKIP LOCKED\")\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"ranked-queue-claim","outcome":"reject","scenarioId":"primary","title":"A queue claim hides ranking inside nested derived relations"},{"expectedCount":0,"files":[{"path":"app/call_store.py","source":"import psycopg\ncursor.execute(\"SELECT root_call_id FROM canary_run WHERE id = %s\")\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"stored-root-lookup","outcome":"accept","scenarioId":"multi-join-review","title":"A read uses a root resolved and stored by its owning write workflow"}],"filePatterns":[],"id":"complex-postgres-query-requires-architecture-review","key":"python:complex-postgres-query-requires-architecture-review","languages":["python"],"limitations":["Only statically recoverable SQL passed to execute, executemany, fetch, fetchrow, fetchval, or prepare in non-test modules with explicit PostgreSQL imports is analyzed, regardless of module filename.","Execution receivers use a conventional database name or import-proven Psycopg ownership. Conventional names are a heuristic and can warn on unrelated objects with matching methods. Native execution additionally follows stable module/local SQL bindings and direct same-class query-forwarding wrappers; unknown receivers abstain.","Modules mixing SQLite and PostgreSQL abstain for conventional receivers; import-proven Psycopg execution remains eligible.","Stable source-local class construction abstains from conventional receiver inference, including custom delegating instances unless native recovery independently proves execution.","Conventional receivers follow one direct, unambiguous same-scope binding. Native Psycopg recovery may follow bounded stable lexical bindings, including module constants. Unexecuted constants, branch-local assignments, mutable attributes, containers and unknown dynamic composition abstain.","Review covers derived relations in every SELECT (including LATERAL, CTEs, predicates and writes), four explicit or comma joins (including parenthesized joined tables), six query stages, three joins plus four stages, or 25 projections plus two joins. A self join over the same explicit table also merits review at 16 projected expressions.","Derived-relation review requires grouping, HAVING or windowing combined with joins or nested reads; expression density is not a separate trigger.","Projection width alone is accepted, and UNION branches are measured independently. Set operators do not add read stages beyond their SELECT blocks; execution plans and production cardinality remain authoritative.","Conventional receivers exclude ClickHouse and BigQuery syntax per query, including in mixed-backend modules. Import-proven Psycopg execution uses PostgreSQL parsing even when identifiers or function names overlap another dialect.","The rule does not infer optimizer behavior, materialization, performance, data-model quality, or datastore placement.","SQL parsing and static query recovery abstain on recursion limits; deeply nested source can withhold native recovery for the file."],"messageIds":[],"optionsSchema":null,"rationale":"Join-heavy, deeply staged, or wide joined reads can obscure cardinality, bounds, ordering, and locking semantics and can signal repeated read-time reconstruction. Syntax alone does not establish runtime cost, a bad data model, or the right datastore.","references":[],"remediation":"Prefer ClickHouse for reporting, historical analysis, and broad aggregations, with measured performance and an explicit freshness contract. For transactional work, simplify the query or document why PostgreSQL is required, with query bounds and production-like EXPLAIN evidence. Review whether the schema exposes the operational fact directly; preserve atomic coordination and do not mechanically replace database joins with application joins. Do not add reporting-specific indexes without evaluating ClickHouse.","since":null,"source":"packages/python/src/sarj_python_lint/rules/complex_postgres_query_requires_architecture_review.py","status":"active","summary":"Complex executable PostgreSQL query shapes require architecture review.","test":"packages/python/tests/rules/test_complex_postgres_query_requires_architecture_review.py"},{"aliases":["defect-xfail-requires-strict","xfail-requires-strict"],"autofix":"safe","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":"Unexpected pass does not fail the suite"},{"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":"Unexpected pass fails the suite"}],"filePatterns":[],"id":"defect-xfail-requires-explicit-strict","key":"python:defect-xfail-requires-explicit-strict","languages":["python"],"limitations":["Safe fixes add a missing literal strict=True only to direct, non-parametrized function markers. Existing strict choices, class/module policies and parametrized functions remain review-only.","Only markers resolved through an unambiguous pytest or pytest.mark import are analyzed.","Only xfail reasons that explicitly identify a defect are analyzed.","Exception narrowing and defect identity remain manual review; strictness or a typed marker alone does not establish failure origin.","Nondeterministic, recognized Hypothesis, network, environment-gated, run=False, and statically disabled pins are excluded.","Repository-level strict_xfail / xfail_strict configuration is intentionally not consulted: defect pins remain locally explicit.","Default pytest test_/Test collection names are recognized; repositories with custom collection patterns may need a local suppression."],"messageIds":[],"optionsSchema":null,"rationale":"A defect pin that relies on pytest's configurable strictness default can stay green after the defect is fixed, leaving stale coverage and markers behind. Literal local strictness keeps the invariant attached to the pin independently of repository configuration.","references":[],"remediation":"Set literal `strict=True` so an unexpected pass fails. If only some parameter cases carry the defect, move the marker to those `pytest.param(...)` cases before making it strict. When the defect's exception type or group shape is independently known, native `raises=...`, where supported, can narrow unrelated failures; same-type failures still need an origin oracle. Keep intentional broad expected-failure policies.","since":null,"source":"packages/python/src/sarj_python_lint/rules/defect_xfail_requires_explicit_strict.py","status":"active","summary":"A deterministic known-defect `xfail` must set literal `strict=True` so XPASS fails the suite.","test":"packages/python/tests/rules/test_defect_xfail_requires_explicit_strict.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ506","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.delete('/keys/{key_id}')\nasync def revoke(key_id: str) -> None:\n    return None\n"}],"fixedFiles":[],"focusPath":"api.py","id":"implicit-200","outcome":"reject","scenarioId":"primary","title":"A DELETE route answers 200 with a null body"},{"expectedCount":0,"files":[{"path":"api.py","source":"from fastapi import APIRouter, status\n\nrouter = APIRouter()\n\n@router.delete('/keys/{key_id}', status_code=status.HTTP_204_NO_CONTENT)\nasync def revoke(key_id: str) -> None:\n    return None\n"}],"fixedFiles":[],"focusPath":"api.py","id":"no-content","outcome":"accept","scenarioId":"primary","title":"A DELETE route declares 204"}],"filePatterns":[],"id":"delete-route-returns-no-content","key":"python:delete-route-returns-no-content","languages":["python"],"limitations":["Only DELETE-only decorators on a FastAPI or APIRouter receiver created in the same file are checked.","A status written as an integer, a Starlette `status.HTTP_*` name or an `HTTPStatus` member is resolved; any other status expression and decorators with `**` keyword unpacking are accepted.","Tests and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Without an explicit 204, FastAPI answers a DELETE with 200 and a JSON null body. Generated clients then type the result as nullable data, and API routes disagree on what a successful removal returns.","references":[],"remediation":"Declare `status_code=204` (or `status.HTTP_204_NO_CONTENT`) on the DELETE route and return None.","since":null,"source":"packages/python/src/sarj_python_lint/rules/delete_route_returns_no_content.py","status":"active","summary":"FastAPI DELETE routes declare status code 204 No Content.","test":"packages/python/tests/rules/test_delete_route_returns_no_content.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ086","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/widgets.py","source":"def set_timeout(timeout_ms: int) -> None:\n    \"\"\"Configure request handling.\n\n    Args:\n        timeout_ms: Request deadline in milliseconds; zero disables retries.\n    \"\"\"\n"}],"fixedFiles":[],"focusPath":"app/widgets.py","id":"argument-documents-unit","outcome":"accept","scenarioId":"primary","title":"Unit and sentinel semantics add a contract"},{"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":["Safe fixes remove only a single native-proven redundant argument section in plain unescaped private nested synchronous nongenerator functions with no detected metadata consumer. Multiple argument headings, other sections and behavioral prose are preserved; public APIs, escaped/decorated helpers and escaped/raw string literals remain review-only. Known frame and metadata capabilities, their import aliases and metadata field access prevent fixes.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","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":"Repeated names and types drift and crowd out constraints, units, ownership, and side effects. Complete parameter tables may remain when a public documentation contract requires them.","references":[],"remediation":"Remove only the redundant Args section; remove the whole docstring only when no section adds behavior. Keep constraints, accepted formats, units, non-obvious defaults, relationships, and public API semantics in the docstring.","since":null,"source":"packages/python/src/sarj_python_lint/rules/docstring_args_restate_signature.py","status":"active","summary":"Remove a wholly redundant Google-style Args section when every entry only repeats the function signature.","test":"packages/python/tests/rules/test_docstring_args_restate_signature.py"},{"aliases":[],"autofix":"safe","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        The line length.\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":["Safe fixes remove only a single native-proven redundant result section in plain unescaped private nested synchronous nongenerator functions with no detected metadata consumer. Multiple result headings, other sections and behavioral prose are preserved; public APIs, escaped/decorated helpers and escaped/raw string literals remain review-only. Known frame and metadata capabilities, their import aliases and metadata field access prevent fixes.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","The rule reads a single Google-style return or yield section and requires an informative explicit result annotation.","Names that document the positions of a fixed tuple return are treated as semantic information.","Generated files, overloads, runtime-consumed docstrings, typed sections, protected facts, result conditions, identity semantics, and whole-docstring restatements owned by SARJ050 are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Repeating a result type or callable name adds noise and can drift without explaining identity, polarity, cardinality, ordering, ownership, or empty-result behavior.","references":[],"remediation":"Remove only the redundant Returns or Yields section. Keep identity, ownership, units, ordering, shape, laziness, sentinel values, and success conditions in the public docstring.","since":null,"source":"packages/python/src/sarj_python_lint/rules/docstring_returns_restate_signature.py","status":"active","summary":"Google-style Returns and Yields documentation must add facts beyond the corresponding annotated result type.","test":"packages/python/tests/rules/test_docstring_returns_restate_signature.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ488","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"try:\n    action()\nexcept (ValueError, TypeError) as error:\n    raise RuntimeError('invalid') from error\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"one-tuple-handler","outcome":"accept","scenarioId":"primary","title":"One handler owns the shared translation"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"try:\n    action()\nexcept ValueError as error:\n    raise RuntimeError('invalid') from error\nexcept TypeError as error:\n    raise RuntimeError('invalid') from error\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"repeated-translation","outcome":"reject","scenarioId":"primary","title":"Adjacent handlers translate identically"}],"filePatterns":[],"id":"duplicate-adjacent-exception-handler","key":"python:duplicate-adjacent-exception-handler","languages":["python"],"limitations":["Safe fixes merge only comment-free adjacent handlers using proven unshadowed builtin exception names; first-party classes and documented translation seams remain review-only.","Only ordinary try statements with adjacent handlers, identical exception bindings, executable ASTs and comments are compared; exception groups and bare re-raise/pass handlers are excluded.","Handler types must be simple names or tuples of unshadowed builtin or statically resolved first-party Exception subclasses, with stable declarations/imports initialized before the enclosing module statement. Declarations after a callable's definition are unresolved and excluded.","Dynamic or explicit builtin namespace access, unknown external exception classes, generated/vendor sources, different comments, and intervening handlers are excluded. Unseen external builtin mutations are not resolved."],"messageIds":[],"optionsSchema":null,"rationale":"Repeating one translation for adjacent statically known exception classes creates extra branches to maintain without different behavior.","references":[],"remediation":"Consider one tuple handler for the adjacent exception types. Preserve distinct messages, cleanup, ordering, and intentional independently maintained translations.","since":null,"source":"packages/python/src/sarj_python_lint/rules/duplicate_adjacent_exception_handler.py","status":"active","summary":"Adjacent exception handlers repeat the same executable translation.","test":"packages/python/tests/rules/test_duplicate_adjacent_exception_handler.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ489","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/formatters.py","source":"import re\nFIRST = re.compile(r'item (\\d+)')\nSECOND = re.compile(r'code (\\d+)')\ndef _space(match: re.Match[str]) -> str:\n    full = match.group(0)\n    digits = match.group(1)\n    spaced = ' '.join(digits)\n    return full.replace(digits, spaced)\ndef _spread(match: re.Match[str]) -> str:\n    full = match.group(0)\n    digits = match.group(1)\n    spaced = ' '.join(digits)\n    return full.replace(digits, spaced)\nFIRST.sub(_space, text)\nSECOND.sub(_spread, text)\n"}],"fixedFiles":[],"focusPath":"app/formatters.py","id":"duplicate-native-transformation","outcome":"reject","scenarioId":"primary","title":"Different patterns use identical closed callbacks"},{"expectedCount":0,"files":[{"path":"app/formatters.py","source":"import re\nFIRST = re.compile(r'item (\\d+)')\nSECOND = re.compile(r'code (\\d+)')\ndef _space(match: re.Match[str]) -> str:\n    full = match.group(0)\n    digits = match.group(1)\n    spaced = ' '.join(digits)\n    return full.replace(digits, spaced)\nFIRST.sub(_space, text)\nSECOND.sub(_space, text)\n"}],"fixedFiles":[],"focusPath":"app/formatters.py","id":"shared-existing-transformation","outcome":"accept","scenarioId":"primary","title":"Patterns retain their choices and share one transformation"}],"filePatterns":[],"id":"duplicate-closed-pure-helper","key":"python:duplicate-closed-pure-helper","languages":["python"],"limitations":["Only undecorated private module-level functions in one authored source file with identical signatures and executable ASTs are compared; no cross-file extraction is suggested.","The supported pure grammar is one re.Match[str] parameter, straight-line local assignments, immutable literals, native Match.group, str.join and str.replace, and a str result. Defaults and unknown calls are excluded.","Every reference must be a known direct call or the first replacement argument to Pattern.sub on a stable, unescaped native re.compile binding with a string pattern.","Escapes, aliases, reflection, rebinding, exports, patch strings, decorators and generated/vendor paths are excluded. Distinct domain docstrings alone do not create runtime behavior; retain useful examples at the surviving owner. No autofix."],"messageIds":[],"optionsSchema":null,"rationale":"Identical private transformations duplicate maintained behavior even when different caller patterns choose the domains in which they run.","references":[],"remediation":"Reuse one existing transformation while keeping each caller's pattern, order and examples. Preserve documented independent versioning or patch contracts explicitly.","since":null,"source":"packages/python/src/sarj_python_lint/rules/duplicate_closed_pure_helper.py","status":"active","summary":"Closed private helpers repeat one proven native regex/string transformation.","test":"packages/python/tests/rules/test_duplicate_closed_pure_helper.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ524","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/conftest.py","source":"import pytest\n\n@pytest.fixture(params=(\"empty\", \"filled\"))\ndef payload(request):\n    return request.param\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"distinct-fixture-inputs","outcome":"accept","scenarioId":"primary","title":"Each fixture parameter represents a distinct input"},{"expectedCount":1,"files":[{"path":"tests/conftest.py","source":"import pytest\n\n@pytest.fixture(params=(\"empty\", \"empty\"))\ndef payload(request):\n    return request.param\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"repeated-fixture-input","outcome":"reject","scenarioId":"primary","title":"The same input schedules the dependent tests twice"}],"filePatterns":[],"id":"duplicate-pytest-fixture-params","key":"python:duplicate-pytest-fixture-params","languages":["python"],"limitations":["Only directly imported pytest/pytest-asyncio fixtures in test paths with one decorator and literal list/tuple params are analyzed.","Compares immutable scalar/tuple literals with type and floating-point sign preserved; mutable values, calls, aliases and pytest.param metadata are not compared.","Explicit IDs, decorator unpacking and fixtures using request beyond direct request.param loads are excluded.","Dependent fixtures, hooks and tests may observe case indices, generated IDs or repeated execution; findings require contract review and are never auto-deleted."],"messageIds":[],"optionsSchema":null,"rationale":"Each fixture parameter reruns its dependent tests. Repeating the same immutable literal can add execution time without exercising a distinct input; Ruff PT014 covers test parametrization, not fixture params.","references":[],"remediation":"Review the dependent test contracts, then remove an accidental duplicate. Preserve intentional repetition with explicit case IDs or an exact SARJ524 suppression explaining the repeated-run contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/duplicate_pytest_fixture_params.py","status":"active","summary":"Review repeated immutable values in a pytest fixture parameter list.","test":"packages/python/tests/rules/test_duplicate_pytest_fixture_params.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ434","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app.py","source":"def activate():\n    # Load current account profile activation settings\n    # Build current account profile activation response\n    load_current_account_profile_activation_settings()\n    return build_current_account_profile_activation_response()\n"}],"fixedFiles":[],"focusPath":"app.py","id":"activation-narration","outcome":"reject","scenarioId":"primary","title":"A paragraph exactly repeats two flat action names"},{"expectedCount":0,"files":[{"path":"app.py","source":"def activation_reasons():\n    # Everything standing between this integration and being usable.\n    # Returns all the reasons rather than the first failure.\n    # Someone activating a half-built integration wants the complete list.\n    # That avoids discovering one problem per round trip.\n    reasons = []\n    reasons.extend(integration_reasons())\n    reasons.extend(endpoint_reasons())\n    return reasons\n"}],"fixedFiles":[],"focusPath":"app.py","id":"activation-rationale","outcome":"accept","scenarioId":"primary","title":"The original activation paragraph explains why all failures are returned"}],"filePatterns":[],"id":"excessive-commentary","key":"python:excessive-commentary","languages":["python"],"limitations":["Safe fixes remove only a paragraph accepted by the shared ordered-narration proof: flat zero-argument direct calls, single-name call assignments and a terminal call return, or the exact initializer/accumulator-calls/return shape. Every line must repeat its corresponding action name; constraints, directives, qualifiers and extra statements remain protected. Each removed comment leaves its original blank line.","Contiguous standalone implementation comment blocks with at least two non-empty lines and 12 words are inspected.","An error requires each line to exactly repeat its corresponding direct zero-argument call identifier in the same order. Supported owners contain only those flat calls, single-name call assignments and call returns, or the exact accumulation shape.","Guards, loops, branches, additional statements, intervening comments, arguments, qualified calls, quoted text, novel words and meaning-bearing qualifiers cause abstention. Arbitrary prose is not inferred to be redundant from its length.","Technical facts and unproven narration produce no diagnostic. The rule emits no review-budget warnings.","Generated files, directives, licenses, docstrings, inline comments and short comments are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A paragraph that repeats the same action names in the same order adds a second copy of the implementation. Comment length alone cannot distinguish noise from a necessary technical constraint.","references":[],"remediation":"Delete the duplicate narration and keep the authored action names. Preserve constraints, references, uncertainty, ordering requirements and side-effect explanations that add meaning beyond those names.","since":null,"source":"packages/python/src/sarj_python_lint/rules/excessive_commentary.py","status":"active","summary":"Delete comment paragraphs that exactly repeat the following ordered action names.","test":"packages/python/tests/rules/test_excessive_commentary.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ538","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/recovery.py","source":"import subprocess\n\ndef read_status():\n    try:\n        result = subprocess.run([\"service\", \"status\"], capture_output=True, text=True, check=False)\n    except (subprocess.TimeoutExpired, OSError):\n        return None\n    return None if result.returncode else result.stdout\n\ndef active_count():\n    output = read_status()\n    return None if output is None else output.count(\"active\")\n\ndef recovered(baseline: int) -> bool:\n    if (active_count() or 0) >= baseline:\n        return True\n    return False\n"}],"fixedFiles":[],"focusPath":"app/recovery.py","id":"failed-probe-accepted-as-zero","outcome":"reject","scenarioId":"primary","title":"Keep failed observation distinct from zero"},{"expectedCount":0,"files":[{"path":"app/recovery.py","source":"import subprocess\n\ndef read_status():\n    try:\n        result = subprocess.run([\"service\", \"status\"], capture_output=True, text=True, check=False)\n    except (subprocess.TimeoutExpired, OSError):\n        return None\n    return None if result.returncode else result.stdout\n\ndef active_count():\n    output = read_status()\n    return None if output is None else output.count(\"active\")\n\ndef recovered(baseline: int) -> bool:\n    observed = active_count()\n    if observed is not None and observed >= baseline:\n        return True\n    return False\n"}],"fixedFiles":[],"focusPath":"app/recovery.py","id":"failed-probe-rejected","outcome":"accept","scenarioId":"primary","title":"Reject a missing observation explicitly"}],"filePatterns":[],"id":"failed-probe-zero-threshold","key":"python:failed-probe-zero-threshold","languages":["python"],"limitations":["Recognizes an undecorated same-module synchronous subprocess.run text probe returning None on failure/nonzero status, followed by one local str.count wrapper preserving None.","The initial sink is a direct if comparison of (count_probe() or 0) >= a literal zero or an int-typed parameter, with a terminal True return or a None return before a proven local raising failure helper.","Known zero-baseline skip policies, other parameter validation/uses before the comparison, unknown aliases, reassignment, escaping helpers, reflection, test/generated files, and dynamic imports abstain. Control-transferring enclosing finalizers also abstain. Function and variable names do not establish probe intent.","Async command wrappers, imported helpers, deeper interprocedural count parsing, compound success conditions and dynamically constrained thresholds are not inferred. This warning identifies an error-derived accepted sentinel; it does not prove every surrounding health check failed."],"messageIds":[],"optionsSchema":null,"rationale":"A failed observation is different from an observed count of zero. Coalescing the error sentinel can make a recovery comparison succeed when its baseline is zero.","references":[],"remediation":"Check the probe result for None before comparing it, or make an intentional zero-baseline skip policy explicit.","since":null,"source":"packages/python/src/sarj_python_lint/rules/failed_probe_zero_threshold.py","status":"active","summary":"Do not let a failed subprocess count probe meet its threshold through a zero fallback.","test":"packages/python/tests/rules/test_failed_probe_zero_threshold.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ428","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_checkout.py","source":"class FakePaymentGateway:\n    async def charge(self, amount: int) -> None:\n        pass\n"}],"fixedFiles":[],"focusPath":"tests/test_checkout.py","id":"local-fake-service","outcome":"reject","scenarioId":"primary","title":"A test module privately defines a named fake"},{"expectedCount":0,"files":[{"path":"tests/test_checkout.py","source":"def test_decline_retries_once():\n    class FakePaymentGateway:\n        async def charge(self, amount: int) -> None:\n            raise Declined()\n    assert retries(FakePaymentGateway()) == 1\n"}],"fixedFiles":[],"focusPath":"tests/test_checkout.py","id":"scenario-local-fake","outcome":"accept","scenarioId":"primary","title":"A nested double is visibly coupled to one scenario"}],"filePatterns":[],"id":"fakes-in-shared-location","key":"python:fakes-in-shared-location","languages":["python"],"limitations":["Only top-level classes in test files with an explicit Fake, Stub, Mock, InMemory, Recording, or Scripted prefix are classified as doubles.","conftest.py and conventional shared-support paths are already shared locations and are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Reusable doubles hidden in an individual test module are difficult to discover and are often recreated. A per-file warning cannot prove cross-module reuse, so scenario-local doubles may remain with an exact rationale.","references":[],"remediation":"Move a reusable double to a testing, fakes, stubs, mocks, doubles, helpers, support, or test_utils module. Keep a deliberately local double near its scenario with an exact SARJ428 suppression and rationale.","since":null,"source":"packages/python/src/sarj_python_lint/rules/fakes_in_shared_location.py","status":"active","summary":"Review named top-level test doubles for shared-support ownership unless they are intentionally scenario-local.","test":"packages/python/tests/rules/test_fakes_in_shared_location.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ451","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/routes.py","source":"from fastapi import FastAPI\napp = FastAPI()\napp.include_router(ItemRouter(service).build())\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"app-router-composition","outcome":"accept","scenarioId":"application-composition","title":"An application includes a class-owned router"},{"expectedCount":0,"files":[{"path":"app/routes.py","source":"from fastapi import APIRouter\nclass ItemRouter:\n    def __init__(self, service: Service) -> None:\n        self._service = service\n    def build(self) -> APIRouter:\n        router = APIRouter()\n        return router\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"class-router","outcome":"accept","scenarioId":"primary","title":"A class builds and returns its router"},{"expectedCount":1,"files":[{"path":"app/routes.py","source":"from fastapi import FastAPI\napp = FastAPI()\n@app.get('/items', response_model=ItemsResponse)\ndef items() -> ItemsResponse:\n    return ItemsResponse(items=[])\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"direct-app-route","outcome":"reject","scenarioId":"application-composition","title":"An application declares an HTTP handler directly"},{"expectedCount":1,"files":[{"path":"app/routes.py","source":"from fastapi import APIRouter\nrouter = APIRouter()\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"module-router","outcome":"reject","scenarioId":"primary","title":"A module owns an unscoped router"}],"filePatterns":[],"id":"fastapi-class-router-contract","key":"python:fastapi-class-router-contract","languages":["python"],"limitations":["Tests, generated code, and hidden routes are excluded. FastAPI application construction and include_router composition are allowed.","Response-model checks exclude literal or import-proven 204/205/304 statuses and proven non-JSON response classes, including raw Response.","Unpacked metadata or an explicit unresolved status cannot prove a JSON response; receiver ownership remains checked while response-model inference abstains.","Response classes require unshadowed native import provenance; direct provider writes/escapes, custom classes, and unresolved defaults disable JSON-model inference."],"messageIds":[],"optionsSchema":null,"rationale":"Class-owned construction gives dependencies one reviewable composition boundary, while explicit object models keep success envelopes stable and generated clients precise.","references":[],"remediation":"Inject dependencies through `*Router.__init__`, create and return a local `APIRouter` from `build()`, and declare `response_model=NamedResponse` with a matching named return annotation. Compose the application with `FastAPI.include_router(...)` instead of declaring HTTP handlers directly on it.","since":null,"source":"packages/python/src/sarj_python_lint/rules/fastapi_class_router_contract.py","status":"active","summary":"FastAPI HTTP routes use injected `*Router.build()` owners and explicit named object response models.","test":"packages/python/tests/rules/test_fastapi_class_router_contract.py"},{"aliases":["fastapi-openapi-contract"],"autofix":"none","category":"correctness","code":"SARJ094","defaultLevel":"warning","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('/items', status_code=200, operation_id='items')\nasync def read_items() -> ItemsResponse: ...\n\n@router.post('/items', status_code=201, operation_id='items')\nasync def create_item() -> ItemResponse: ...\n"}],"fixedFiles":[],"focusPath":"api.py","id":"duplicate-operation-id","outcome":"reject","scenarioId":"operation-id","title":"Two operations reuse one client method identifier"},{"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"},{"expectedCount":0,"files":[{"path":"api.py","source":"from fastapi import APIRouter\nfrom pydantic import BaseModel\n\nclass HealthResponse(BaseModel):\n    status: str\n\nrouter = APIRouter()\n\n@router.get('/health', status_code=200)\nasync def health() -> HealthResponse:\n    return HealthResponse(status='ok')\n"}],"fixedFiles":[],"focusPath":"api.py","id":"named-record-response","outcome":"accept","scenarioId":"unannotated-record","title":"Named response type documents the record"},{"expectedCount":1,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.get('/users', status_code=200)\nasync def read_users() -> dict:\n    return {}\n"}],"fixedFiles":[],"focusPath":"api.py","id":"schema-erasing-response","outcome":"reject","scenarioId":"schema-integrity","title":"A bare mapping erases the generated response schema"},{"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":"typed-response-model","outcome":"accept","scenarioId":"schema-integrity","title":"A named response type preserves a concrete schema"},{"expectedCount":1,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.get('/health', status_code=200)\nasync def health():\n    return {'status': 'ok'}\n"}],"fixedFiles":[],"focusPath":"api.py","id":"unannotated-record-response","outcome":"reject","scenarioId":"unannotated-record","title":"Unnamed record leaves the response schema implicit"},{"expectedCount":0,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.get('/items', status_code=200, operation_id='read_items')\nasync def read_items() -> ItemsResponse: ...\n\n@router.post('/items', status_code=201, operation_id='create_item')\nasync def create_item() -> ItemResponse: ...\n"}],"fixedFiles":[],"focusPath":"api.py","id":"unique-operation-ids","outcome":"accept","scenarioId":"operation-id","title":"Each operation has a distinct client method identifier"}],"filePatterns":[],"id":"fastapi-explicit-openapi-contract","key":"python:fastapi-explicit-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.","Response-body/model inference abstains for unresolved status/class metadata, direct native-provider writes/escapes, and opaque unpacked defaults; no-body JSON metadata includes 205.","Direct-status alias/callee/catch recognition retains its existing limits; broader coverage requires separate reachability and runtime-dispatch evidence.","Route conflicts require unconditional registrations on the same local router; conditional composition requires trusted route/schema validation.","Operation ID conflicts require unconditional registrations with literal IDs and paths on the same local router; conditional registrations, dynamic IDs, OpenAPI overrides, and cross-router composition require generated-schema validation.","Explicit status and parameter-location findings are organizational policy even when FastAPI inference would generate valid OpenAPI.","Fixed-record return inference is local and conservative: dynamic keys, opaque escapes, and ambiguous control flow are accepted when a closed response shape cannot be proven.","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":"FastAPI can infer valid schemas and default statuses. This stricter organizational policy pins locally reviewable metadata while also detecting response, status, and routing mismatches that inference cannot fix.","references":[],"remediation":"Follow the diagnostic family: pin operation metadata, annotate parameter locations, preserve concrete schemas, document escaping alternate statuses, or correct a statically proven route conflict.","since":null,"source":"packages/python/src/sarj_python_lint/rules/fastapi_explicit_openapi_contract.py","status":"active","summary":"Visible FastAPI operations pin locally reviewable metadata and avoid statically provable OpenAPI gaps.","test":"packages/python/tests/rules/test_fastapi_explicit_openapi_contract.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ563","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/routes.py","source":"from fastapi import APIRouter, Security\nrouter = APIRouter()\n@router.get('/items', dependencies=[Security(require_permission(Permission.ITEMS_READ))])\ndef items() -> Items: ...\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"decorator-security","outcome":"accept","scenarioId":"primary","title":"The decorator declares a `Security` dependency"},{"expectedCount":1,"files":[{"path":"app/routes.py","source":"from fastapi import APIRouter, Depends\nrouter = APIRouter()\n@router.get('/items', dependencies=[Depends(require_user)])\ndef items() -> Items: ...\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"depends-only","outcome":"reject","scenarioId":"primary","title":"A route whose only access dependency uses `Depends`"},{"expectedCount":0,"files":[{"path":"app/routes.py","source":"from fastapi import APIRouter, FastAPI, Security\nrouter = APIRouter()\n@router.get('/items')\ndef items(): ...\napp = FastAPI(dependencies=[Security(require_user)])\napp.include_router(router)\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"inherited-application-security","outcome":"accept","scenarioId":"inherited-router-access","title":"An included router inherits the application's Security declaration"},{"expectedCount":1,"files":[{"path":"app/routes.py","source":"from fastapi import APIRouter, FastAPI\nrouter = APIRouter()\n@router.get('/items')\ndef items(): ...\napp = FastAPI()\napp.include_router(router)\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"unguarded-application-mount","outcome":"reject","scenarioId":"inherited-router-access","title":"An included router has no inherited Security declaration"}],"filePatterns":[],"id":"fastapi-route-declares-security","key":"python:fastapi-route-declares-security","languages":["python"],"limitations":["The rule proves that an access decision is declared, not that it is sufficient: a `Security()` around a sign-in check satisfies it. Each service decides which dependency is strong enough.","Follows statically resolved include_router edges and module-level imported routers in the prepared project. Factory-returned routers, middleware, add_api_route and websockets are not modeled. Router ancestry is bounded to eight hops and 4096 visited receivers per route. Route-copy timing and arbitrary runtime mutation are not modeled. Ambiguous unpacking and mount cycles abstain; this warning is an explicit declaration contract, not proof that a route is unauthenticated.","Tests and generated code are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A route without a visible access declaration makes reviewers infer its policy from unrelated code. A Security dependency records the decision at the route or an effective router ancestor. This contract does not establish the strength of the check or replace middleware review.","references":[],"remediation":"Wrap the dependency that authenticates or authorizes callers in `Security(...)`, in the decorator's `dependencies=`, on the router, or as an `Annotated` parameter. For a route that is public on purpose, declare a no-op marker dependency such as `Security(public_route)`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/fastapi_route_declares_security.py","status":"active","summary":"Declare each FastAPI route access decision, including inherited router dependencies.","test":"packages/python/tests/rules/test_fastapi_route_declares_security.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ412","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_policy.py","source":"import json\n\ndef test_policy(rendered_plan_json: str):\n    plan = json.loads(rendered_plan_json)\n    changes = plan['resource_changes']\n    assert changes[0]['change']['actions'] == ['create']\n"}],"fixedFiles":[],"focusPath":"tests/test_policy.py","id":"rendered-plan-contract","outcome":"accept","scenarioId":"primary","title":"Assert on structured Terraform plan behavior"},{"expectedCount":1,"files":[{"path":"tests/test_policy.py","source":"from pathlib import Path\n\ndef 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, common normalization, and direct comprehension reads unpacked from stable module-level path tuples; interprocedural flows remain unreported.","Files produced beneath recognized temporary-directory fixtures are generated outputs and remain unreported.","The Python detector currently owns Terraform and HCL suffixes; YAML remains with the general source-coupled rule.","Fixture, golden, and snapshot paths are treated as deliberate representation contracts; other packaging, formatter, and compatibility contracts require an exact suppression.","Lambda bodies and unconsumed generators are not inferred. Comprehension targets and match captures cannot inherit stale outer source provenance; proven builtin all/any line-content checks remain in scope.","Exact byte copies into proven temporary outputs and same-path byte preservation following a visible write_bytes of the compared named value remain unreported. Fixed-source byte equality and unproven or overwritten writes still require an exact representation-contract suppression."],"messageIds":[],"optionsSchema":null,"rationale":"Substring and regex checks can pass on comments, formatting, or unreachable Terraform configuration without proving the plan or deployed behavior.","references":[],"remediation":"Assert on parsed configuration, Terraform test or rendered plan/state JSON, provider state, or runtime behavior. When exact source representation is the contract, suppress the assertion with that rationale.","since":null,"source":"packages/python/src/sarj_python_lint/rules/iac_source_coupled_test.py","status":"active","summary":"Test uses raw Terraform/HCL text as an infrastructure-behavior oracle.","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 typing import Annotated\nfrom pydantic import BaseModel, Field\n\nPositiveInt = Annotated[int, Field(gt=0)]\n\nclass RetryPolicy(BaseModel):\n    attempts: PositiveInt = 1\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"default-satisfies-lower-bound","outcome":"accept","scenarioId":"field-bound","title":"Default satisfies the declared field bounds"},{"expectedCount":1,"files":[{"path":"app/models.py","source":"from typing import Annotated\nfrom pydantic import BaseModel, Field\n\nPositiveInt = Annotated[int, Field(gt=0)]\n\nclass RetryPolicy(BaseModel):\n    attempts: PositiveInt = 0\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"default-violates-lower-bound","outcome":"reject","scenarioId":"field-bound","title":"Default is outside the declared field bounds"},{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel\n\nclass User(BaseModel):\n    display_name: str = None\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"non-null-field-has-null-default","outcome":"reject","scenarioId":"nullability","title":"A non-null field cannot default to None"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel\n\nclass User(BaseModel):\n    display_name: str | None = None\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"nullable-field-has-null-default","outcome":"accept","scenarioId":"nullability","title":"Declare None when it is an allowed default"}],"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 from assignment or `Annotated` Field metadata.","Fields with before, plain, or wrap validation that can transform a default are excluded.","Imported annotation aliases are followed only to a unique local Python module within the same checkout.","Test and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Pydantic does not validate defaults unless default validation is enabled. An invalid literal can therefore enter a model while contradicting its annotation or field bounds.","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 model-field defaults to satisfy their resolved contract.","test":"packages/python/tests/rules/test_invalid_pydantic_field_default.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 create_autospec\n\ndef test_service():\n    client = create_autospec(Client, instance=True, spec_set=True)\n    client.send()\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"mock-with-spec","outcome":"accept","scenarioId":"constructor","title":"Autospecced mock follows attributes and signatures"},{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"from unittest.mock import Mock\n\ndef test_service():\n    client = Mock()\n    client.send()  # A typo or removed method still passes.\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"mock-without-contract","outcome":"reject","scenarioId":"constructor","title":"Mock accepts any attribute"},{"expectedCount":0,"files":[{"path":"tests/test_service.py","source":"from unittest.mock import patch\n\ndef test_service():\n    with patch(\"app.client.Client.send\", autospec=True) as send:\n        run_service()\n        send.assert_called_once()\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"patch-with-autospec","outcome":"accept","scenarioId":"patch","title":"Patch preserves the callable contract"},{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"from unittest.mock import patch\n\ndef test_service():\n    with patch(\"app.client.Client.send\") as send:\n        run_service()\n        send.assert_called_once()\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"patch-without-contract","outcome":"reject","scenarioId":"patch","title":"Patch generates an unrestricted replacement"}],"filePatterns":[],"id":"mock-without-spec","key":"python:mock-without-spec","languages":["python"],"limitations":["Only non-generated 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.","Unknown `new_callable=` factories are treated as concrete replacements; known Mock subclasses still require a spec."],"messageIds":[],"optionsSchema":null,"rationale":"A mock without a spec permits removed or misspelled attributes; a callable mock without autospec may also accept stale call shapes.","references":[],"remediation":"Use `spec_set=RealType` or a concrete fake for collaborators, `autospec=True` for patched callables, and `create_autospec` when direct-call signatures matter. Use `object()` or `mock.sentinel` for identity markers.","since":null,"source":"packages/python/src/sarj_python_lint/rules/mock_without_spec.py","status":"active","summary":"Unrestricted mock permits attributes outside the collaborator contract.","test":"packages/python/tests/rules/test_mock_without_spec.py"},{"aliases":["named-fixed-record-return","pydantic-at-boundaries"],"autofix":"none","category":"architecture","code":"SARJ008","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"from typing import TypedDict\n\nclass CallPayload(TypedDict):\n    id: str\n\ndef build_payload(call) -> CallPayload:\n    return {'id': call.id}\n"}],"fixedFiles":[],"focusPath":"service.py","id":"typed-boundary-model","outcome":"accept","scenarioId":"primary","title":"TypedDict names the returned record"},{"expectedCount":1,"files":[{"path":"service.py","source":"def build_payload(call) -> dict[str, object]:\n    return {'id': call.id}\n"}],"fixedFiles":[],"focusPath":"service.py","id":"untyped-dictionary-boundary","outcome":"reject","scenarioId":"primary","title":"Public API returns an unnamed record"}],"filePatterns":[],"id":"named-record-at-boundaries","key":"python:named-record-at-boundaries","languages":["python"],"limitations":["Visible FastAPI routes are owned by SARJ094. Private functions and classes, closures, tests, generated files, documentation examples, recognized framework hooks, and dictionary conversion methods are excluded.","Record evidence is flow-sensitive but local: literals, proven built-in dict keyword construction, static string-key writes, and unique unconditional local type aliases are recognized. Dynamic keys, opaque escapes, ambiguous control flow, generic aliases, and cross-module aliases make the rule abstain.","An explicit unique local mapping type alias preserves a named open/pinned mapping contract. Type aliases do not add required-key validation; stricter Any-value typing remains owned by SARJ447."],"messageIds":[],"optionsSchema":null,"rationale":"A named record makes field types and required keys explicit to callers and static tooling.","references":[],"remediation":"Define and return a `TypedDict`, Pydantic model, or frozen dataclass for a closed record shape. When the API intentionally exposes an extensible concrete dictionary, use one explicit named mapping type alias to preserve its dictionary contract and runtime API.","since":null,"source":"packages/python/src/sarj_python_lint/rules/named_record_at_boundaries.py","status":"active","summary":"Public Python API returns an unnamed fixed-shape record.","test":"packages/python/tests/rules/test_named_record_at_boundaries.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ539","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/export_validation.py","source":"import math\n\ndef validate(single: float, batched: float) -> None:\n    if not math.isfinite(single) or not math.isfinite(batched):\n        raise ValueError(\"Invalid export\")\n    lowest = min(single, batched)\n    if lowest < 0.99:\n        raise ValueError(\"Invalid export\")\n"}],"fixedFiles":[],"focusPath":"app/export_validation.py","id":"candidates-checked-first","outcome":"accept","scenarioId":"primary","title":"Validate each floating candidate"},{"expectedCount":1,"files":[{"path":"app/export_validation.py","source":"import math\n\ndef validate(single: float, batched: float) -> None:\n    lowest = min(single, batched)\n    if not math.isfinite(lowest) or lowest < 0.99:\n        raise ValueError(\"Invalid export\")\n"}],"fixedFiles":[],"focusPath":"app/export_validation.py","id":"nan-hidden-before-finite-check","outcome":"reject","scenarioId":"primary","title":"Do not validate only the reduced value"}],"filePatterns":[],"id":"nan-hidden-by-builtin-min","key":"python:nan-hidden-by-builtin-min","languages":["python"],"limitations":["Requires an unshadowed multiargument built-in min/max, stable local aliases/float conversion, and a subsequent direct not math.isfinite/numpy.isfinite rejection guard. Only a single raising guard or its OR with direct comparisons is supported.","Floating candidates are direct float-typed parameters, float literals/casts, known NumPy floating constructors, or scalar min results from closed local NumPy array/asarray constructors with explicit floating dtype and optional sum. The initial array payload must be a direct unannotated or float-typed parameter; literal, locally assembled and integer-typed payloads abstain. NumPy scalar constructors require an already proved NaN-capable scalar argument. Unknown methods named min and external model arrays are not inferred.","Requires a finite-capable first operand and a distinct later NaN-capable origin. Integer-only inputs, repeated aliases of the same floating origin, a known NaN first operand, custom key/comparators, mutation/escape and unknown numeric types abstain.","Any preceding finiteness check in the same function causes abstention rather than attempting arbitrary validator/control-flow equivalence. Dynamic namespaces, import mutations/escapes, test and generated files are excluded. The warning concerns an ineffective finiteness subcheck; it does not prove every other acceptance condition is satisfiable. Control-transferring enclosing finalizers abstain. There is no automatic rewrite or rule against deliberate NaN-ignoring APIs."],"messageIds":[],"optionsSchema":null,"rationale":"Python's min/max compares operands in order. A later NaN can be ignored, leaving a finite result that passes a finiteness check even though an original candidate was invalid.","references":[],"remediation":"Check every floating candidate before reducing it, or use a deliberately NaN-propagating reduction and validate its result.","since":null,"source":"packages/python/src/sarj_python_lint/rules/nan_hidden_by_builtin_min.py","status":"active","summary":"Validate floating candidates before a built-in min/max can hide a NaN.","test":"packages/python/tests/rules/test_nan_hidden_by_builtin_min.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ408","defaultLevel":"error","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    assert response.json()['detail']\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.","Only Python `assert` statements in default test_* collection shapes are analyzed.","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. When the explicit contract is only that a fuzz, chaos, smoke, or crash-regression request avoids a server exception, suppress SARJ408 on that assertion with a concise reason.","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":["no-aggregation-in-store-query","no-analytical-queries-in-transactional-db"],"autofix":"none","category":"architecture","code":"SARJ020","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/account_store.py","source":"import psycopg\n\nQUERY = \"SELECT EXISTS(SELECT 1 FROM ledger WHERE account_id = %s)\"\n"}],"fixedFiles":[],"focusPath":"app/account_store.py","id":"operational-existence-lookup","outcome":"accept","scenarioId":"primary","title":"PostgreSQL performs an operational existence lookup"},{"expectedCount":1,"files":[{"path":"app/event_store.py","source":"import psycopg\n\nQUERY = \"SELECT DATE_TRUNC('day', occurred_at), COUNT(*), AVG(latency_ms) FROM event GROUP BY 1\"\n"}],"fixedFiles":[],"focusPath":"app/event_store.py","id":"postgres-reporting-rollup","outcome":"reject","scenarioId":"primary","title":"PostgreSQL computes a time-series reporting rollup"},{"expectedCount":1,"files":[{"path":"app/event_store.py","source":"import psycopg\n\nQUERY = \"CREATE TEMP TABLE report ON COMMIT DROP AS WITH rows AS (SELECT DISTINCT team_id FROM event) SELECT team_id FROM rows\"\n"}],"fixedFiles":[],"focusPath":"app/event_store.py","id":"temporary-table-distinct","outcome":"reject","scenarioId":"temporary-table","title":"A temporary PostgreSQL table still performs deduplication"},{"expectedCount":0,"files":[{"path":"app/event_store.py","source":"import psycopg\n\nQUERY = \"CREATE TEMP TABLE report ON COMMIT DROP AS WITH rows AS (SELECT team_id FROM event WHERE id = %s) SELECT team_id FROM rows\"\n"}],"fixedFiles":[],"focusPath":"app/event_store.py","id":"temporary-table-operational-read","outcome":"accept","scenarioId":"temporary-table","title":"A temporary PostgreSQL table can materialize an operational row lookup"}],"filePatterns":[],"id":"no-analytical-aggregation-in-postgres-store","key":"python:no-analytical-aggregation-in-postgres-store","languages":["python"],"limitations":["Statically recoverable SQL in non-test, non-generated Python modules with native PostgreSQL, MySQL, or SQLite imports is analyzed, regardless of filename. The historical SARJ020 identifier is retained.","Every parsed SELECT DISTINCT (including DISTINCT ON), deduplicating UNION/INTERSECT/EXCEPT, GROUP BY, and built-in SQL aggregate is reported, including temporary-table AS queries, CTEs, nested writes, windowed aggregates, identity-bounded queries, and LIMIT queries. ALL set operations remain valid. This deliberately replaces the former reporting-size thresholds.","A single imported backend owns module SQL. Resolvable first-party query contracts and re-exports retain their native analytical-provider evidence. Mixed backend modules require a direct query sink on a source-proven native connection or annotated parameter; unresolved receivers and separately bound queries abstain.","Known ClickHouse, BigQuery, DuckDB, Snowflake, Trino, Athena, and Databricks drivers are analytical owners. Comments, scalar SQL values, identifiers, Python docstrings, and IS DISTINCT FROM do not trigger the policy.","SQLite multi-argument MIN/MAX and MySQL ANY_VALUE retain their scalar semantics; PostgreSQL ANY_VALUE remains an aggregate.","PostgreSQL pg_catalog aggregates and lowercase quoted built-ins retain native identity. Unknown schemas and distinct quoted uppercase PostgreSQL function names abstain. Executing EXPLAIN ANALYZE is analyzed; plan-only EXPLAIN and unsupported PostgreSQL EXPLAIN options abstain.","Dynamic SQL grammar, ORM expression trees, unknown wrappers, user-defined aggregates, and malformed SQL are not inferred. No automatic datastore migration is safe."],"messageIds":[],"optionsSchema":null,"rationale":"Project architecture reserves deduplication, grouping, and aggregate queries for ClickHouse or another analytical database, so reporting work does not compete with transactional reads. This is a datastore policy; SQL syntax alone cannot establish a query's execution cost.","references":[],"remediation":"Move reporting and aggregation to ClickHouse or another analytical database with an explicit freshness contract. Use an indexed EXISTS or direct row lookup for operational existence checks. Preserve atomicity for transactional invariants; use an exact, explained SARJ020 exception when a reviewed invariant must remain in the transactional database.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_analytical_aggregation_in_postgres_store.py","status":"active","summary":"Keep DISTINCT, GROUP BY, and SQL aggregates in analytical databases.","test":"packages/python/tests/rules/test_no_analytical_aggregation_in_postgres_store.py"},{"aliases":["no-vague-annotations"],"autofix":"safe","category":"correctness","code":"SARJ447","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/offers.py","source":"from typing import Any\ndef details() -> dict[str, Any]: ...\n"}],"fixedFiles":[],"focusPath":"app/offers.py","id":"any-valued-mapping-annotation","outcome":"reject","scenarioId":"primary","title":"An Any-valued mapping erases the record schema"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"from typing import Any\nclass Rows[Any]:\n    rows: dict[str, Any]\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"generic-any-binding","outcome":"accept","scenarioId":"provider-identity","title":"A generic parameter named Any keeps its own identity"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"from typing import Any\nclass Rows[T]:\n    rows: dict[str, Any]\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"generic-typing-any","outcome":"reject","scenarioId":"provider-identity","title":"An unrelated generic parameter preserves actual Any erasure"},{"expectedCount":0,"files":[{"path":"app/offers.py","source":"from typing import TypedDict\nclass OfferDetails(TypedDict):\n    id: str\ndef details() -> OfferDetails: ...\n"}],"fixedFiles":[],"focusPath":"app/offers.py","id":"typed-dictionary-annotation","outcome":"accept","scenarioId":"primary","title":"A TypedDict preserves the record schema"}],"filePatterns":[],"id":"no-any-mapping-types","key":"python:no-any-mapping-types","languages":["python"],"limitations":["Automatic fixes replace only direct Any values in covariant Mapping parameters of closed private nested synchronous helpers whose parameter is entirely unused. Defaults, consumers, reflection/frame access, comments, shadowed object, mutable/aliased/string annotations and newly unused Any imports remain manual.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","Only proven built-in or standard-library string-keyed mappings whose value recursively contains `typing.Any` are reported; `object` is intentionally not treated as `Any`.","Unique unconditional module-level PEP 695 and explicit TypeAlias chains are resolved with cycle guards. Plain module assignments are checked only when they directly define an Any-valued mapping used in a module-level annotation; imported, conditional, rebound, and ambiguous aliases are excluded.","Generated and vendored sources are excluded; exact local suppressions remain auditable.","Annotated metadata, Literal values and unresolved generic parameter positions are excluded; only type arguments are inspected.","Proven override method signatures and direct BaseModel.model_dump return contracts are excluded; local annotations, casts and TypeAdapter targets remain checked.","Ambiguous type-import origins, attribute mutations, escaped canonical type namespaces and dynamic namespace access cause conservative file abstention."],"messageIds":[],"optionsSchema":null,"rationale":"An `Any`-valued mapping disables checking transitively and conceals whether the value is a fixed record, an arbitrary JSON tree, or a genuinely opaque Python object.","references":[],"remediation":"Use `TypedDict` (and `Unpack[TypedDict]` for closed keyword arguments) for fixed records, a Pydantic model for untrusted runtime input, `pydantic.JsonValue` for arbitrary JSON, or `object` plus explicit narrowing for a genuinely opaque Python value. Preserve unchanged external API signatures with an exact explained suppression; do not automatically widen published returns.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_any_mapping_types.py","status":"active","summary":"String-keyed mapping types must not erase their values with `Any`.","test":"packages/python/tests/rules/test_no_any_mapping_types.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ549","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/service.py","source":"import httpx\nclass Service:\n    def __init__(self):\n        self.client = httpx.AsyncClient()\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"constructor-allocation","outcome":"reject","scenarioId":"primary","title":"Do not hide client allocation in a service constructor"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"from contextlib import asynccontextmanager\nimport httpx\nclass Service:\n    def __init__(self, client: httpx.AsyncClient):\n        self.client = client\n@asynccontextmanager\nasync def service_scope(endpoint):\n    async with httpx.AsyncClient(base_url=endpoint) as client:\n        yield Service(client)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"scoped-injection","outcome":"accept","scenarioId":"primary","title":"Inject a client owned by the async composition scope"}],"filePatterns":[],"id":"no-async-client-in-constructor","key":"python:no-async-client-in-constructor","languages":["python"],"limitations":["This is an explicit architecture policy for import-proven HTTPX AsyncClient calls executed by synchronous nongenerator class __init__ methods, including import aliases and stable constructor assignment aliases bounded to 32 links, eager comprehensions and nested definition defaults/decorators.","One-hop same-file undecorated synchronous factories with stable lexical bindings are followed only when their sole statement after an optional docstring returns a proven constructor call. Zero-argument lambda factories are supported when invoked; deferred function/lambda/generator bodies are not constructor execution.","Unknown or imported factory implementations, overridable methods, wrapper chains, dynamic namespace access, wildcard imports, ambiguous/rebound aliases and visible namespace mutation are not inferred. A generator's first iterable is eager; other generator expressions remain deferred even when a caller might consume them immediately.","Constructor parameter defaults execute at class definition, not on each constructor call, and are outside this rule. No automatic ownership transfer, sharing decision or lifecycle rewrite is safe.","Annotations and lazy type-alias/type-parameter expressions are excluded: their source location does not prove constructor-time execution. Framework-driven evaluation of annotations is not inferred.","Escaped HTTPX module objects cause conservative abstention, including module-object assignment aliases. Constructor aliases must remain confined to calls, annotations, attribute access or stable single-name aliases; unknown argument, return or storage escapes and visible mutation invalidate the import proof.","Nested class bodies are followed only without bases or metaclass keywords, because custom class namespace preparation can replace imported names. Method bodies still use their normal lexical bindings. Generator factories and lambdas create deferred generators rather than clients and are excluded.","HTTPX recommends scoped client reuse and explicit asynchronous closure; its API does not universally prohibit constructor ownership. This stricter constructor boundary is the requested policy. See https://www.python-httpx.org/async/ . Ruff ASYNC212 instead owns blocking HTTPX use in async functions."],"messageIds":[],"optionsSchema":null,"rationale":"Constructor-owned clients repeat transport setup for each service instance and hide the boundary that owns pooling and asynchronous cleanup.","references":[],"remediation":"Create and close an appropriately scoped AsyncClient at the async composition boundary, and inject it into consumers. Keep tenant credentials, cookies, event loops and independent ownership boundaries separate; do not replace them with a global client.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_async_client_in_constructor.py","status":"active","summary":"Inject an HTTPX AsyncClient instead of constructing one in __init__.","test":"packages/python/tests/rules/test_no_async_client_in_constructor.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ507","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/cache.py","source":"from functools import cache\n\n@cache\nasync def load() -> int:\n    return 1\n"}],"fixedFiles":[],"focusPath":"app/cache.py","id":"cached-coroutine-function","outcome":"reject","scenarioId":"primary","title":"A standard cache retains an exhausted coroutine"},{"expectedCount":0,"files":[{"path":"app/cache.py","source":"async def load() -> int:\n    return 1\n"}],"fixedFiles":[],"focusPath":"app/cache.py","id":"fresh-coroutine-function","outcome":"accept","scenarioId":"primary","title":"Each call creates a fresh coroutine"}],"filePatterns":[],"id":"no-async-function-cache","key":"python:no-async-function-cache","languages":["python"],"limitations":["Only a proven functools import used as the innermost decorator on a coroutine function is reported.","Async generators, transformed functions, ambiguous or rebound imports, and generated files are excluded.","Wildcard imports and names rebound by relative imports or imports outside the module body are excluded conservatively.","lru_cache with disabled, dynamic, or unpacked maxsize arguments is excluded; an omitted size or literal positive integer or None is required.","Visible mutations, deletion, or escape of functools providers invalidate their native identity conservatively.","Indirect wrapping calls and opaque runtime or interprocedural mutations are not resolved. Test code is checked because coroutine reuse fails there too.","No automatic rewrite is offered because cache lifetime, cancellation, and concurrency require an explicit design decision."],"messageIds":[],"optionsSchema":null,"rationale":"`functools.cache`, `lru_cache`, and `cached_property` store the coroutine object returned by an async function rather than its awaited value. A subsequent cache hit can raise `RuntimeError: cannot reuse already awaited coroutine`.","references":[],"remediation":"Remove the standard library cache or use an async-aware cache that stores completed results.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_async_function_cache.py","status":"active","summary":"Standard library cache decorates a coroutine function and retains single-use coroutine objects.","test":"packages/python/tests/rules/test_no_async_function_cache.py"},{"aliases":["test-phase-label-comment"],"autofix":"safe","category":"testing","code":"SARJ089","defaultLevel":"error","engine":"python","examples":[{"expectedCount":3,"files":[{"path":"tests/test_widget.py","source":"def test_widget():\n    # Arrange\n    widget = make_widget()\n    # Act\n    result = widget.run()\n    # Assert\n    assert result.ok\n"}],"fixedFiles":[],"focusPath":"tests/test_widget.py","id":"bare-test-phase-comments","outcome":"reject","scenarioId":"primary","title":"Bare Arrange, Act, and Assert labels"},{"expectedCount":0,"files":[{"path":"tests/test_widget.py","source":"def test_widget():\n    # A stale token here would authorize the wrong tenant.\n    assert request_is_rejected()\n"}],"fixedFiles":[],"focusPath":"tests/test_widget.py","id":"explanatory-comment","outcome":"accept","scenarioId":"primary","title":"Comment explains a consequence"}],"filePatterns":[],"id":"no-bare-test-phase-comments","key":"python:no-bare-test-phase-comments","languages":["python"],"limitations":["Safe fixes remove only reported standalone phase labels, preserving executable tokens and meaningful rationale.","Standalone comments across recognized test and test-support files are checked, including fixtures, helpers, and module sections.","Trailing comments and comments inside bracketed expressions 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":"Test files must not use bare phase comments such as Arrange, Act, Assert, Given, When, or Then.","test":"packages/python/tests/rules/test_phase_label_comment.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ482","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel, model_validator\nclass Record(BaseModel):\n    name: str\n    @model_validator(mode='before')\n    @classmethod\n    def normalize(cls, data: dict[str, object]):\n        data = dict(data)\n        data['name'] = 'new'\n        return data\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"fresh-normalization-builder","outcome":"accept","scenarioId":"local-builder","title":"A copied outer mapping can be updated locally"},{"expectedCount":1,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel, model_validator\nclass Record(BaseModel):\n    name: str\n    @model_validator(mode='before')\n    @classmethod\n    def normalize(cls, data: dict[str, object]):\n        data.pop('legacy', None)\n        return data\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"raw-input-pop","outcome":"reject","scenarioId":"local-builder","title":"Removing a raw entry changes caller data"},{"expectedCount":1,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel, model_validator\nclass Record(BaseModel):\n    name: str\n    @model_validator(mode='before')\n    @classmethod\n    def normalize(cls, data: dict[str, object]):\n        data['name'] = 'new'\n        return data\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"raw-input-write","outcome":"reject","scenarioId":"primary","title":"Before-validation changes the original mapping"},{"expectedCount":0,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel, model_validator\nclass Record(BaseModel):\n    name: str\n    @model_validator(mode='before')\n    @classmethod\n    def normalize(cls, data: dict[str, object]):\n        return {**data, 'name': 'new'}\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"replacement-input","outcome":"accept","scenarioId":"primary","title":"Return normalized data without changing the original"}],"filePatterns":[],"id":"no-before-validator-input-mutation","key":"python:no-before-validator-input-mutation","languages":["python"],"limitations":["Only import-resolved Pydantic model_validator and field_validator decorators with literal mode='before' are analyzed.","Owned model ancestry must be undecorated and retain its effective validation contract. Even a spelled typing.final class decorator is excluded when its provider cannot be proven; class replacement or mutation can detach the validator.","Explicit descriptors and Pydantic's automatic classmethod conversion require an untouched builtin namespace; builtin imports or __builtins__ access conservatively abstain.","Standalone model files with unknown module identity and non-Pydantic/non-typing imports are excluded because self-imported model aliases cannot be resolved safely.","Class member values must be inert literals or closed canonical Field/ConfigDict constructions with literal options or known builtin default factories. Descriptor aliases, special methods, unknown decorators and wrap validators are excluded.","The raw value parameter, stable aliases, direct item writes, and type-proven builtin container mutators are checked. isinstance narrowing and resolved typing.cast preserve provenance.","After validators, ordinary methods, arbitrary same-named methods, unknown container mutators, captured closure aliases, and ambiguous control-flow bindings are excluded.","Generated/vendor sources are excluded; maintained tests are checked. No autofix is offered because copying, validation, identity, and evaluation order can differ."],"messageIds":[],"optionsSchema":null,"rationale":"Treat raw before-validator inputs as borrowed data. They can be caller-owned or reused by another validation branch; mutation can leak normalization into aliases and leave data changed after validation fails. Return replacements even when a particular caller supplies fresh data.","references":[],"remediation":"Return a normalized replacement mapping or collection. Copy the changed path before updating nested values; a fresh outer copy permits outer writes but retains shared descendants.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_before_validator_input_mutation.py","status":"active","summary":"Preserve raw inputs in Pydantic before validators.","test":"packages/python/tests/rules/test_no_before_validator_input_mutation.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ523","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/adapter.py","source":"def value() -> str:\n    return 1 # pyright: ignore\n"}],"fixedFiles":[],"focusPath":"app/adapter.py","id":"blanket-type-ignore","outcome":"reject","scenarioId":"primary","title":"An unscoped ignore hides every diagnostic on the line"},{"expectedCount":0,"files":[{"path":"app/adapter.py","source":"value = vendor.value # pyright: ignore[reportPrivateUsage] -- the dependency exposes this API only privately\n"}],"fixedFiles":[],"focusPath":"app/adapter.py","id":"exact-local-exception","outcome":"accept","scenarioId":"primary","title":"A named diagnostic retains the dependency boundary explanation"}],"filePatterns":[],"id":"no-blanket-suppression","key":"python:no-blanket-suppression","languages":["python"],"limitations":["Checks actual Python comment tokens for bare or empty sarj-noqa, unscoped pyright: ignore, basic/standard Pyright modes, report* downgrades below error, analyzeUnannotatedFunctions/disableBytesTypePromotions/strictParameterNoneValue=false and scoped Ruff/Flake8 file or Ruff range disables. SARJ054 retains precedence for Ruff banned-api file directives.","Ruff owns blanket noqa and type: ignore through PGH004 and PGH003. Exact local exceptions remain available; this rule does not judge whether their explanations justify the exception.","Directive examples inside strings and unrelated Pyright feature flags are excluded. Source-generated markers cannot waive accountability; use explicit whole-path exclusions for generated/vendor code. Consumer type-checker configuration remains outside this source rule.","The full Standards gate protects this rule and the existing suppression-accountability checks from inline waivers, rule exclusions and diagnostic baselines. Explicitly selected partial checks remain scoped."],"messageIds":[],"optionsSchema":null,"rationale":"A blanket suppression hides unrelated errors on its line. A file-level diagnostic downgrade also authorizes future defects outside the reviewed exception. Suppression checks cannot waive themselves.","references":[],"remediation":"Fix the diagnostic or use an exact, line-local rule code with a concrete boundary reason.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_blanket_suppression.py","status":"active","summary":"Use named, line-local suppressions instead of blanket, file or range disables.","test":"packages/python/tests/rules/test_no_blanket_suppression.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ477","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/adapter.py","source":"def wrapper(*args: object, **kwargs: object):\n    return target(*args, **kwargs)\n"}],"fixedFiles":[],"focusPath":"app/adapter.py","id":"broad-forwarding-arguments","outcome":"reject","scenarioId":"forwarding","title":"Forwarding broad arguments still loses the callable contract"},{"expectedCount":1,"files":[{"path":"tests/fakes/session.py","source":"class Session:\n    async def start(self, **_kwargs: object) -> None:\n        self.start_count += 1\n"}],"fixedFiles":[],"focusPath":"tests/fakes/session.py","id":"discarded-session-keywords","outcome":"reject","scenarioId":"primary","title":"An ignored catch-all weakens a fake's contract"},{"expectedCount":0,"files":[{"path":"app/templates.py","source":"def render(template: str, **values: object) -> str:  # sarj-noqa: SARJ477 — template supplies dynamic field names and object formatting\n    return template.format(**values)\n"}],"fixedFiles":[],"focusPath":"app/templates.py","id":"documented-open-template-fields","outcome":"accept","scenarioId":"dynamic-boundary","title":"Document why template fields require an open object-valued interface"},{"expectedCount":1,"files":[{"path":"app/templates.py","source":"def render(template: str, **values: object) -> str:\n    return template.format(**values)\n"}],"fixedFiles":[],"focusPath":"app/templates.py","id":"open-template-fields","outcome":"reject","scenarioId":"dynamic-boundary","title":"A deliberately open formatting interface still requests policy review"},{"expectedCount":0,"files":[{"path":"app/adapter.py","source":"from collections.abc import Callable\ndef wrapper[**P, R](target: Callable[P, R], *args: P.args, **kwargs: P.kwargs) -> R:\n    return target(*args, **kwargs)\n"}],"fixedFiles":[],"focusPath":"app/adapter.py","id":"typed-forwarding-arguments","outcome":"accept","scenarioId":"forwarding","title":"ParamSpec preserves the forwarded callable contract"},{"expectedCount":0,"files":[{"path":"tests/fakes/session.py","source":"class Session:\n    async def start(self, *, room: Room, agent: Agent) -> None:\n        _unused_inputs = room, agent\n        self.start_count += 1\n"}],"fixedFiles":[],"focusPath":"tests/fakes/session.py","id":"typed-unused-keywords","outcome":"accept","scenarioId":"primary","title":"Preserve named typed keywords with a named unused binding"}],"filePatterns":[],"id":"no-broad-keyword-capture","key":"python:no-broad-keyword-capture","languages":["python"],"limitations":["Safe fixes remove only an unused keyword capture with no annotation or proven builtin object on a private unescaped nested synchronous nongenerator helper when every direct call already satisfies the remaining declared signature. They preserve every caller argument and bare-star keyword-only contract. Module/public/callback contracts, other annotations, extra keywords, expanded calls, captures, decorators, type parameters and namespace/frame access remain review-only. Known frame and metadata capabilities, their import aliases and metadata field access prevent fixes.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","This strict interface policy is not proof of incorrect behavior or a universal ban on the object type; genuinely open boundaries use a reasoned exact-code exception.","Reports missing, Any- or object-valued keyword captures whether discarded, read or forwarded.","Precise or unresolved decorated signature-declaration families and functions with unresolved decorator effects are excluded; known classmethod/staticmethod/abstractmethod/final/override declarations remain eligible.","Homogeneous keyword types constrain values but allow arbitrary names; ParamSpec preserves only its target's existing contract.","Generated/vendor source and stubs are excluded. Unresolved imports, wildcard imports, conflicting imports, actual global or definition-time writes and shadowed annotation provenance are conservative. Identical imports and read-only global declarations remain eligible.","Cross-file aliases, external stubs, generated signatures and runtime reflection are not resolved; transparent annotation metadata and ambiguous decorated declaration families can cause conservative false negatives.","Open or used captures still require an explicit intended contract. Dynamic payload/formatting APIs can intentionally need an exact-code exception.","Runtime special methods and canonical override declarations retain WARNING because framework and inherited signatures can require an open keyword contract; ordinary declared interfaces use ERROR."],"messageIds":[],"optionsSchema":null,"rationale":"Replacing a fixed typed interface with broad **kwargs can erase required keyword names and per-key value types, making test doubles accept calls the real implementation rejects. object remains a safe typed top type, and deliberately open data interfaces can be valid; this strict interface policy requires a precise contract or a documented open-boundary exception.","references":[],"remediation":"Keep explicit typed parameters at fixed interfaces. Preserve required keyword names with named unused local bindings. Use ParamSpec for forwarding, Unpack[TypedDict] for named schemas, or a precise homogeneous value type. Document genuinely open interfaces with an exact-code reasoned suppression.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_broad_keyword_capture.py","status":"active","summary":"Prefer precise keyword interfaces over missing, Any- or object-valued captures.","test":"packages/python/tests/rules/test_no_broad_keyword_capture.py"},{"aliases":[],"autofix":"safe","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":"# NOTE: Keep this order because the registry caches it.\nregister()\n"}],"fixedFiles":[],"focusPath":"service.py","id":"prefixed-constraint","outcome":"accept","scenarioId":"editorial-prefixes","title":"An editorial prefix may still carry a real constraint"},{"expectedCount":1,"files":[{"path":"service.py","source":"# NOTE: Type aliases\ntype UserId = str\n"}],"fixedFiles":[{"path":"service.py","source":"\ntype UserId = str\n"}],"focusPath":"service.py","id":"prefixed-section-label","outcome":"reject","scenarioId":"editorial-prefixes","title":"An editorial prefix does not add meaning to a section label"},{"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":["Safe fixes remove standalone punctuation separators and bounded content-free section labels, including NOTE/NB-prefixed labels; prose, disabled code, editor regions and directives remain for review.","Standalone comments and unprotected prose trailing a single-line exception header are classified; other 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":"Commented-out code, mechanical narration, decorative banner, or untracked work marker.","test":"packages/python/tests/rules/test_no_comment_cruft.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ493","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, ConfigDict\nclass Model(BaseModel):\n    model_config = ConfigDict(validate_by_alias=False, validate_by_name=False)\n"},{"path":"pyproject.toml","source":"[project]\nname=\"example\"\nversion=\"0.1\"\ndependencies=[\"pydantic>=2.11\"]\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"disabled-paths","outcome":"reject","scenarioId":"primary","title":"Both validation paths are explicitly disabled"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, ConfigDict\nclass Model(BaseModel):\n    model_config = ConfigDict(validate_by_alias=False)\n"},{"path":"pyproject.toml","source":"[project]\nname=\"example\"\nversion=\"0.1\"\ndependencies=[\"pydantic>=2.11\"]\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"name-fallback","outcome":"accept","scenarioId":"primary","title":"Disabling alias alone retains name validation"}],"filePatterns":[],"id":"no-conflicting-pydantic-alias-validation","key":"python:no-conflicting-pydantic-alias-validation","languages":["python"],"limitations":["Requires a project dependency whose Pydantic lower bound is at least 2.11.","Only plain direct module-level BaseModel subclasses and literal model_config declarations are inspected.","Class keywords, duplicate keys, config mutation, unpacking, unknown factories and generated source are excluded.","Visible namespace APIs, dynamic module access, ambiguous imports and escaped or mutated imported objects exclude the file."],"messageIds":[],"optionsSchema":null,"rationale":"Explicitly disabling validation by both alias and field name prevents model construction.","references":[],"remediation":"Enable validation by alias, by field name, or both; disabling alias alone retains Pydantic's name fallback.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_conflicting_pydantic_alias_validation.py","status":"active","summary":"Pydantic alias configuration must retain a validation path.","test":"packages/python/tests/rules/test_no_conflicting_pydantic_alias_validation.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ426","defaultLevel":"error","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 from an explicit module-path component or test-package import name beginning with `test_` or ending with `_test`.","Literal pytest_plugins declarations are checked; dynamically assembled plugin paths are excluded."],"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":"correctness","code":"SARJ513","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/guards.py","source":"from typing import TypeGuard\ndef is_text(value: object) -> TypeGuard[str]:\n    return isinstance(value, str)\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"consistent-primitive-promise","outcome":"accept","scenarioId":"primary","title":"Recognize the primitive promised to callers"},{"expectedCount":0,"files":[{"path":"app/guards.py","source":"from typing import TypeGuard\ndef is_text(value: object) -> TypeGuard[str]:\n    return False\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"constant-false-is-legal-partial-guard","outcome":"accept","scenarioId":"constant-predicate","title":"TypeGuard can intentionally recognize no inputs"},{"expectedCount":1,"files":[{"path":"app/guards.py","source":"from typing import TypeGuard\ndef is_text(value: object) -> TypeGuard[str]:\n    return True\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"constant-true-claims-text","outcome":"reject","scenarioId":"constant-predicate","title":"A constant result does not prove a primitive type"},{"expectedCount":1,"files":[{"path":"app/guards.py","source":"from typing import TypeGuard\ndef is_text(value: object) -> TypeGuard[str]:\n    return isinstance(value, int)\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"wrong-primitive-promise","outcome":"reject","scenarioId":"primary","title":"An integer test does not prove a string"}],"filePatterns":[],"id":"no-contradictory-type-guard","key":"python:no-contradictory-type-guard","languages":["python"],"limitations":["Checks undecorated, non-generic module functions with one parameter and a single returned direct builtin isinstance call, its unary not, or a bool constant, optionally after a docstring.","Guard symbols must resolve to typing or typing_extensions TypeGuard or TypeIs. Targets and checked classes are builtin int, bool, str or bytes; bool is treated as an int subtype.","Inputs must be object or fully classified plain unions of those primitive types and None. Direct positive impossible isinstance tests and compiler-invalid TypeIs target/input relationships are excluded.","Input unions are traversed iteratively with a bounded node budget; overly complex annotations are excluded.","Literal and refined targets, aliases, custom classes, float numeric promotion, tuple isinstance tests, helper predicates, nested functions and complex bodies are not inferred.","TypeGuard may intentionally recognize a subset of its target, so TypeGuard[int] with an isinstance bool test remains legal. Visible namespace mutation and ambiguous or shadowed symbols abstain conservatively.","Constant false remains legal for TypeGuard. Constant true is reported only with a proven incompatible primitive or None input; false TypeIs needs a proven inhabited target. Negated isinstance uses the same finite witness domains.","Repeated top-level function definition names abstain because overloads can constrain the public input more narrowly than the implementation input.","Generated files are excluded; maintained tests remain checked. No autofix guesses a domain or changes false-branch behavior."],"messageIds":[],"optionsSchema":null,"rationale":"A guard annotation does not validate its implementation. Returning an incompatible primitive predicate check makes callers trust the wrong primitive; TypeIs also incorrectly excludes rejected target values in the false branch.","references":[],"remediation":"Align the declared target and runtime test. For an intentional partial predicate, TypeGuard may narrow only the true branch; TypeIs must recognize every target value admitted by its input. Review callers before changing the narrowing contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_contradictory_type_guard.py","status":"active","summary":"Primitive type guards must agree with the builtin predicate they return.","test":"packages/python/tests/rules/test_no_contradictory_type_guard.py"},{"aliases":["duplicated-override-docstring"],"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        \"\"\"Read the process-local cache before the base backend.\"\"\"\n        return key\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"override-specific-docstring","outcome":"accept","scenarioId":"primary","title":"Override documents its implementation-specific behavior"}],"filePatterns":[],"id":"no-copied-inherited-docstring","key":"python:no-copied-inherited-docstring","languages":["python"],"limitations":["Only methods inherited through a local base name, parameterized local base, or simple local class alias declared earlier in the same lexical body are compared; imported and dynamic bases are excluded.","Transitive local ancestors are followed in declared base order without guessing across lexical scopes.","Overloads, generated files, undocumented bases, and methods whose docstring is their entire body are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Inherited documentation is available through `inspect.getdoc` and `help`, while a duplicate can drift. Direct `Child.method.__doc__` consumers remain a reason to retain and suppress the copy.","references":[],"remediation":"Delete the copied docstring only when the base contract is complete and no runtime or generated-documentation consumer requires direct `__doc__`. Keep override-specific behavior in the override docstring.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_copied_inherited_docstring.py","status":"active","summary":"An override repeats the documentation of the single local base method it actually overrides.","test":"packages/python/tests/rules/test_no_copied_inherited_docstring.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 direct `\"*\"` origin in a literal collection or an exact universal `allow_origin_regex` literal. Literal keyword dictionaries and their effective nested unpacks are inspected.","Conditional origin collections are checked in each possible branch. Calls, comprehensions, nested non-string elements, dynamic keyword unpacks, computed credential flags, and variable 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":"architecture","code":"SARJ500","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.post('/tickets')\nasync def create_ticket() -> None:\n    return None\n"}],"fixedFiles":[],"focusPath":"api.py","id":"collection-path","outcome":"accept","scenarioId":"primary","title":"A POST route names its collection"},{"expectedCount":1,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.post('/tickets/create')\nasync def create_ticket() -> None:\n    return None\n"}],"fixedFiles":[],"focusPath":"api.py","id":"create-segment","outcome":"reject","scenarioId":"primary","title":"A POST route repeats the verb in its path"}],"filePatterns":[],"id":"no-create-verb-in-post-path","key":"python:no-create-verb-in-post-path","languages":["python"],"limitations":["Only literal route paths on a FastAPI or APIRouter receiver created in the same file are checked.","The final path segment is read; its first word (split on '-', '_' or '.') must be create, add or new.","A final path parameter is accepted, and tests and generated files are excluded.","Deliberate RPC-style integration endpoints exist; keep them with an exact suppression and its reason."],"messageIds":[],"optionsSchema":null,"rationale":"The HTTP method already states the action. A trailing create, add or new segment turns a resource API into ad hoc RPC names, so clients cannot predict paths and PUT, PATCH and DELETE lose their pairing.","references":[],"remediation":"POST to the collection path, use PUT on the item path for an upsert and PATCH for a partial update.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_create_verb_in_post_path.py","status":"active","summary":"POST routes name their collection instead of ending in a create, add or new segment.","test":"packages/python/tests/rules/test_no_create_verb_in_post_path.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ542","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/content.py","source":"from typing import Literal, assert_never\nfrom pydantic import BaseModel, TypeAdapter\nfrom app.outcomes import Error, Result, Success\nclass Denied(BaseModel):\n    code: Literal['denied'] = 'denied'\n    name: str\ndef load(name: str, allowed: bool) -> Result[str, Denied]:\n    return Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))\ndef format_result(result: Result[str, Denied]) -> str:\n    if result.kind == 'success':\n        return result.value\n    if result.kind == 'error':\n        return f'Denied {result.error.name}'\n    assert_never(result)\ncodec = TypeAdapter[Result[str, Denied]](Result[str, Denied])\ndef decode_result(data: str) -> Result[str, Denied]:\n    return codec.validate_json(data, strict=True)\n"},{"path":"app/outcomes.py","source":"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\nclass Success[T](BaseModel):\n    kind: Literal['success'] = 'success'\n    value: T\nclass Error[E](BaseModel):\n    kind: Literal['error'] = 'error'\n    error: E\ntype Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator='kind')]\n"}],"fixedFiles":[],"focusPath":"app/content.py","id":"code-context-and-consumer-formatting","outcome":"accept","scenarioId":"typed-payload","title":"A typed outcome retains the code and formatting context"},{"expectedCount":0,"files":[{"path":"app/outcomes.py","source":"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\nclass Success[T](BaseModel):\n    kind: Literal['success'] = 'success'\n    value: T\nclass Error[E](BaseModel):\n    kind: Literal['error'] = 'error'\n    error: E\ntype Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator='kind')]\n"},{"path":"app/service.py","source":"from typing import Literal, assert_never\nfrom pydantic import BaseModel, TypeAdapter\nfrom app.outcomes import Error, Result, Success\nclass Denied(BaseModel):\n    code: Literal['denied'] = 'denied'\n    name: str\ndef save(name: str, allowed: bool) -> Result[str, Denied]:\n    return Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))\ndef format_result(result: Result[str, Denied]) -> str:\n    if result.kind == 'success':\n        return result.value\n    if result.kind == 'error':\n        return f'Denied {result.error.name}'\n    assert_never(result)\ncodec = TypeAdapter[Result[str, Denied]](Result[str, Denied])\ndef decode_result(data: str) -> Result[str, Denied]:\n    return codec.validate_json(data, strict=True)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"direct-domain-outcome-union","outcome":"accept","scenarioId":"prepared-message","title":"Both success and rejection carry a stable discriminator"},{"expectedCount":1,"files":[{"path":"app/content.py","source":"class UnsupportedContent(ValueError):\n    pass\ndef read_content(known: bool) -> str:\n    if not known:\n        raise UnsupportedContent('Unsupported content')\n    return 'content'\n"}],"fixedFiles":[],"focusPath":"app/content.py","id":"empty-domain-exception","outcome":"reject","scenarioId":"primary","title":"A local marker hides a domain outcome outside the return contract"},{"expectedCount":1,"files":[{"path":"app/errors.py","source":"from typing import final\n@final\nclass Missing(Exception):\n    def __init__(self, record_id: str) -> None:\n        self.record_id = record_id\n        super().__init__(f'Missing {record_id}')\n"},{"path":"app/service.py","source":"from app.errors import Missing\ndef load(record_id: str, exists: bool) -> str:\n    if not exists:\n        raise Missing(record_id)\n    return record_id\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"final-imported-payload","outcome":"reject","scenarioId":"final-payload","title":"A final imported exception can still hide an expected outcome"},{"expectedCount":0,"files":[{"path":"app/outcomes.py","source":"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\nclass Success[T](BaseModel):\n    kind: Literal['success'] = 'success'\n    value: T\nclass Error[E](BaseModel):\n    kind: Literal['error'] = 'error'\n    error: E\ntype Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator='kind')]\n"},{"path":"app/service.py","source":"from typing import Literal, assert_never\nfrom pydantic import BaseModel, TypeAdapter\nfrom app.outcomes import Error, Result, Success\nclass Missing(BaseModel):\n    code: Literal['missing'] = 'missing'\n    record_id: str\ndef load(record_id: str, exists: bool) -> Result[str, Missing]:\n    return Success[str](value=record_id) if exists else Error[Missing](error=Missing(record_id=record_id))\ndef format_result(result: Result[str, Missing]) -> str:\n    if result.kind == 'success':\n        return result.value\n    if result.kind == 'error':\n        return f'Missing {result.error.record_id}'\n    assert_never(result)\ncodec = TypeAdapter[Result[str, Missing]](Result[str, Missing])\ndef decode_result(data: str) -> Result[str, Missing]:\n    return codec.validate_json(data, strict=True)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"finite-contextual-missing-outcome","outcome":"accept","scenarioId":"final-payload","title":"The error variant retains the identifier needed by its caller"},{"expectedCount":0,"files":[{"path":"app/outcomes.py","source":"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\nclass Success[T](BaseModel):\n    kind: Literal['success'] = 'success'\n    value: T\nclass Error[E](BaseModel):\n    kind: Literal['error'] = 'error'\n    error: E\ntype Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator='kind')]\n"},{"path":"app/service.py","source":"from typing import Literal, assert_never\nfrom pydantic import BaseModel, TypeAdapter\nfrom app.outcomes import Error, Result, Success\nclass Denied(BaseModel):\n    code: Literal['denied'] = 'denied'\n    name: str\ndef save(name: str, allowed: bool) -> Result[str, Denied]:\n    return Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))\ndef format_result(result: Result[str, Denied]) -> str:\n    if result.kind == 'success':\n        return result.value\n    if result.kind == 'error':\n        return f'Denied {result.error.name}'\n    assert_never(result)\ncodec = TypeAdapter[Result[str, Denied]](Result[str, Denied])\ndef decode_result(data: str) -> Result[str, Denied]:\n    return codec.validate_json(data, strict=True)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"generic-success-failure-union","outcome":"accept","scenarioId":"imported-outcome","title":"An established tagged generic union supports explicit handling and JSON decoding"},{"expectedCount":1,"files":[{"path":"app/errors.py","source":"class Rejected(Exception):\n    pass\n"},{"path":"app/service.py","source":"from collections.abc import Generator\nfrom contextlib import contextmanager\nfrom app.errors import Rejected\n@contextmanager\ndef transaction() -> Generator[None]:\n    yield None\ndef save(name: str, allowed: bool) -> str:\n    with transaction():\n        pass\n    if not allowed:\n        raise Rejected(name)\n    return name\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"guard-after-resource-exit","outcome":"reject","scenarioId":"resource-boundary","title":"A rejection after resource exit has an ordinary return contract"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"class Denied(Exception):\n    def __init__(self, detail: str) -> None:\n        self.detail = detail\n        super().__init__(detail)\ndef save(name: str, allowed: bool) -> str:\n    if not allowed:\n        message = f'Denied {name}'\n        raise Denied(message)\n    return name\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"guard-prepares-a-message","outcome":"reject","scenarioId":"prepared-message","title":"A local message variable still hides a typed rejection"},{"expectedCount":1,"files":[{"path":"app/errors.py","source":"class Rejected(Exception):\n    pass\n"},{"path":"app/service.py","source":"from app.errors import Rejected\ndef save(name: str, allowed: bool) -> str:\n    if not allowed:\n        raise Rejected(f'Denied {name}')\n    return name\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"imported-rejection-contract","outcome":"reject","scenarioId":"imported-outcome","title":"An imported rejection is absent from the successful return contract"},{"expectedCount":0,"files":[{"path":"app/errors.py","source":"class Rejected(ValueError):\n    pass\n"},{"path":"app/model.py","source":"from pydantic import BaseModel, field_validator\nfrom app.errors import Rejected\nclass Model(BaseModel):\n    name: str\n    @field_validator('name')\n    @classmethod\n    def validate_name(cls, value: str) -> str:\n        if not value:\n            raise Rejected('name required')\n        return value\n"}],"fixedFiles":[],"focusPath":"app/model.py","id":"imported-validator-rejection","outcome":"accept","scenarioId":"validator-boundary","title":"A validator keeps its native exception rejection protocol"},{"expectedCount":1,"files":[{"path":"app/errors.py","source":"class Rejected(ValueError):\n    pass\n"},{"path":"app/service.py","source":"from app.errors import Rejected\ndef save(value: str) -> str:\n    if not value:\n        raise Rejected('name required')\n    return value\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"ordinary-value-error-guard","outcome":"reject","scenarioId":"validator-boundary","title":"An ordinary guard has no validator rejection protocol"},{"expectedCount":0,"files":[{"path":"app/errors.py","source":"class Rejected(Exception):\n    pass\n"},{"path":"app/service.py","source":"from collections.abc import Generator\nfrom contextlib import contextmanager\nfrom app.errors import Rejected\n@contextmanager\ndef transaction() -> Generator[None]:\n    yield None\ndef save(name: str, allowed: bool) -> str:\n    with transaction():\n        if not allowed:\n            raise Rejected(name)\n    return name\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"protected-transaction-rejection","outcome":"accept","scenarioId":"resource-boundary","title":"A rejection under a resource protocol needs separate migration review"},{"expectedCount":0,"files":[{"path":"app/content.py","source":"from dataclasses import dataclass\nfrom typing import Literal\n@dataclass(frozen=True)\nclass Content:\n    value: str\n    kind: Literal['content'] = 'content'\n@dataclass(frozen=True)\nclass Unsupported:\n    kind: Literal['unsupported'] = 'unsupported'\ndef read_content(known: bool) -> Content | Unsupported:\n    return Content('content') if known else Unsupported()\n"}],"fixedFiles":[],"focusPath":"app/content.py","id":"tagged-domain-result","outcome":"accept","scenarioId":"primary","title":"The return contract represents the expected unsupported outcome"},{"expectedCount":1,"files":[{"path":"app/content.py","source":"class Denied(Exception):\n    def __init__(self, name: str) -> None:\n        self.name = name\n        super().__init__(f'Denied {name}')\ndef load(name: str, allowed: bool) -> str:\n    if not allowed:\n        raise Denied(name)\n    return name\n"}],"fixedFiles":[],"focusPath":"app/content.py","id":"typed-context-hidden-in-exception","outcome":"reject","scenarioId":"typed-payload","title":"A guarded rejection hides typed context in its exception contract"}],"filePatterns":[],"id":"no-custom-control-flow-exception","key":"python:no-custom-control-flow-exception","languages":["python"],"limitations":["This review warning checks stable module-level Exception/ValueError markers and fixed-message subclasses with a source-visible direct raise. It also reviews typed payload constructors limited to assignments and super().__init__ argument formatting, when raised by a simple rejection guard in an explicitly annotated function. Constructor parameters must be typed and nonvariadic, with at least one used; calls must supply their required arguments. Base ancestry must be source-proven; an opaque external base is not inferred from its name or message.","Payload guards can prepare one literal or simple formatted message before raising. Guards under try/except/finally, with, loops and match are excluded, as are generators, decorated or special-method functions, quoted return annotations and import-proven NoReturn/Never functions, including simple nongeneric module aliases. Constructor calls, arbitrary computations, extra methods or behavior, decorated/generic constructors and unknown argument expansion abstain. Source-visible formatting is not a purity proof and never authorizes an automatic rewrite.","Direct module-level from-imports, including renamed and relative imports, are followed to stable first-party exception definitions using the existing bounded local-source resolver. An imported raise must be a simple guard in an explicitly annotated ordinary function, with source-proven Exception/ValueError ancestry and compatible constructor arguments. Import-proven typing.final/typing_extensions.final class decorators preserve this proof. The rule reports once per function and originating exception, at an unsuppressed raise. Source-definition suppressions are also respected.","Imported discovery does not follow re-exports, module-attribute raises, local imports, aliases, unknown or inherited constructors, custom allocation or inherited subclass hooks (including assigned bindings), external source, generated/test/vendor definitions, registrations or namespace mutations. Source ancestry is bounded to eight classes and imported definition modules to 32 per file. A guarded raise is evidence to review, not proof of business expectedness. Preserve genuine infrastructure failure contracts even when a warning matches. Never convert a transaction rejection to a return without reviewing rollback, cleanup, logging, retries and side-effect order. Optional absence and independent outcomes remain valid.","Raising a ValueError-derived marker from an import-proven Pydantic field/model validator or legacy validator preserves that rejection protocol. Direct synchronous module callbacks used only as Annotated validator metadata on plain direct BaseModel fields, and stable nonescaping direct nested helpers, also preserve rejection; lexical helper ownership is bounded to 32 levels. Unknown external bases and visible mutations or escapes of the class or a local base to registrations, callbacks or other value consumers abstain; HTTPException and other framework-required exception types remain valid.","Expectedness is not inferred from a name, message or prefix. Genuine exceptional markers may also match and require review rather than conversion. Cross-module callers and registrations, indirect raises, dynamically mutated namespaces and class aliases are not inferred. Only simple stable module assignment aliases and nongeneric annotation aliases consumed directly by model fields are followed. Explicit string annotations, custom model hooks, async/generator callbacks, escaped helpers and broader call graphs are not followed. Generic lexical resolution can miss a module marker shadowed only in a method's class namespace.","Tests, generated and vendor code are excluded. No automatic rewrite is safe: a result return must preserve failure handling and transaction/resource unwinding. SARJ435 separately reviews joined-string reason identities.","The examples illustrate an existing project Result abstraction; do not introduce another Result library or require these class/tag names. SARJ510 owns explicit data discriminators and SARJ548 reviews discarded tagged outcomes; the native type checker owns exhaustive handling. A tag alone cannot guarantee JSON support for arbitrary T/E: qualify the concrete payloads with their codec. Keep established wire discriminators, legitimate absence and independent outcomes. This warning does not mechanically ban scalar unions or require every function to return Result."],"messageIds":[],"optionsSchema":null,"rationale":"A marker, fixed-message or payload-bearing custom exception creates another control-flow type without declaring failure data in the function's return contract. Expected domain outcomes can often use tagged result variants that callers handle explicitly. A caller may need error identifiers for an access check before reporting a rejected record; preserve that handling context. Exceptional failures and framework rejection still need exceptions.","references":[],"remediation":"Review direct raises of this exception. If the condition is an expected domain outcome, reuse an established Result[T, E] or a generic Success[T] | Error[E] union with a shared Literal discriminator, a stable domain error code and the handling context. Update callers to handle, propagate or translate each relevant variant; use the native type checker's assert_never for exhaustiveness. Extend an existing outcome union rather than wrapping it again. For a serializable contract, wrap the successful payload too; avoid a raw scalar | Error union. Use the project's established codec and validate incoming tags and payloads; Pydantic models with an Annotated Field(discriminator=...) union and TypeAdapter are one option. Format the message at the consumer. Preserve true exceptional failures, validation rejection, transport/framework protocols, cancellation, unwinding and useful cause context. A class name or a raise does not prove the condition is expected.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_custom_control_flow_exception.py","status":"active","summary":"Return coded typed outcomes for expected domain failures instead of exception-only contracts.","test":"packages/python/tests/rules/test_no_custom_control_flow_exception.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ470","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/fakes/batch_fault.py","source":"async def install_fault(conn):\n    await conn.execute(\"ALTER TABLE batch ADD CONSTRAINT fail_counter CHECK (false) NOT VALID\")\n"}],"fixedFiles":[],"focusPath":"tests/fakes/batch_fault.py","id":"constraint-fault","outcome":"accept","scenarioId":"primary","title":"Declarative failure in a disposable database"},{"expectedCount":1,"files":[{"path":"tests/fakes/batch_fault.py","source":"async def install_fault(conn):\n    await conn.execute(\"CREATE TRIGGER fail_counter BEFORE UPDATE ON batch EXECUTE FUNCTION fail_counter()\")\n"}],"fixedFiles":[],"focusPath":"tests/fakes/batch_fault.py","id":"trigger-fault","outcome":"reject","scenarioId":"primary","title":"Procedural fault fixture"}],"filePatterns":[],"id":"no-database-programmability","key":"python:no-database-programmability","languages":["python"],"limitations":["Checks inline execute/executemany SQL and import-proven psycopg.sql.SQL constructors, including aliases, concatenation, f-strings, and format calls. Test fixtures are included; generated Python is excluded.","Reports CREATE FUNCTION, CREATE OR REPLACE FUNCTION, CREATE TRIGGER, CREATE CONSTRAINT TRIGGER, and ALTER TABLE ENABLE TRIGGER. Existing function calls, DROP, and DISABLE remain valid.","SQL comments, quoted values, and dollar-quoted bodies are masked. Standalone example strings and Python functions are not SQL execution.","Runtime builders, SQL passed through named variables without SQL constructors, and dynamically generated DDL inside procedural bodies are not inferred. Non-PostgreSQL execution APIs are not distinguished."],"messageIds":[],"optionsSchema":null,"rationale":"SQL hidden in Python can bypass migration-only architecture rules. Procedural fault fixtures require extra database objects and cleanup when a declarative constraint suffices.","references":[],"remediation":"Prefer declarative constraints or explicit transactional application behavior where equivalent, including in test fixtures. Preserve transaction atomicity and database-enforced integrity; use an exact SARJ470 suppression for an approved database-owned invariant or compatibility exception.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_database_programmability.py","status":"active","summary":"Keep stored SQL functions and triggers out of embedded SQL.","test":"packages/python/tests/rules/test_no_database_programmability.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ497","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"service/settings.py","source":"from typing import Annotated\nfrom pydantic import Field\nfrom pydantic_settings import BaseSettings\nclass Settings(BaseSettings):\n    deployment: Annotated[str, Field(default='prod', alias='ENVIRONMENT')]\n"}],"fixedFiles":[],"focusPath":"service/settings.py","id":"annotated-defaulted-environment","outcome":"reject","scenarioId":"annotated-environment","title":"Annotated metadata still supplies a deployment default"},{"expectedCount":0,"files":[{"path":"service/settings.py","source":"from typing import Annotated\nfrom pydantic import Field\nfrom pydantic_settings import BaseSettings\nclass Settings(BaseSettings):\n    deployment: Annotated[str, Field(alias='ENVIRONMENT')]\n"}],"fixedFiles":[],"focusPath":"service/settings.py","id":"annotated-required-environment","outcome":"accept","scenarioId":"annotated-environment","title":"Annotated metadata can leave the input required"},{"expectedCount":1,"files":[{"path":"service/settings.py","source":"from pydantic import Field\nfrom pydantic_settings import BaseSettings\n\nclass Settings(BaseSettings):\n    environment: str = Field(default='local', alias='ENVIRONMENT')\n"}],"fixedFiles":[],"focusPath":"service/settings.py","id":"defaulted-environment","outcome":"reject","scenarioId":"primary","title":"A defaulted environment pretends to be local"},{"expectedCount":0,"files":[{"path":"service/settings.py","source":"from pydantic import Field\nfrom pydantic_settings import BaseSettings\n\nclass Settings(BaseSettings):\n    environment: str = Field(alias='ENVIRONMENT')\n"}],"fixedFiles":[],"focusPath":"service/settings.py","id":"required-environment","outcome":"accept","scenarioId":"primary","title":"A required environment fails at startup when missing"}],"filePatterns":[],"id":"no-defaulted-environment-setting","key":"python:no-defaulted-environment-setting","languages":["python"],"limitations":["Only classes deriving from pydantic-settings BaseSettings directly or through a class in the same file are checked.","A field qualifies when its name or literal alias is `environment` in any letter case.","Other environment-isolating settings, such as resource-name prefixes, have no stable name and are not checked.","Source-proven Annotated Field metadata and assignment defaults are merged in order; literal Ellipsis and the proven unshadowed bare builtin Ellipsis are equivalent required markers. Field Ellipsis retains an earlier default, bare Ellipsis resets it and default_factory=None clears an earlier factory. Only literal input aliases are followed; private fields, model_config, ClassVar/Final metadata and opaque Annotated metadata are excluded. Namespace mutation/escape, ambiguous imports and bounded metadata overflow abstain."],"messageIds":[],"optionsSchema":null,"rationale":"A defaulted environment setting lets a deployment that forgot the variable start as the default environment, so it quietly behaves like local or development code and can share state with another stack.","references":[],"remediation":"Declare the field without a default, for example `environment: Environment = Field(alias=\"ENVIRONMENT\")`, so a missing value fails at startup.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_defaulted_environment_setting.py","status":"active","summary":"A settings field that names the deployment environment must not declare a default.","test":"packages/python/tests/rules/test_no_defaulted_environment_setting.py"},{"aliases":["no-deleted-only-override-parameter"],"autofix":"safe","category":"maintainability","code":"SARJ442","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/request.py","source":"def ready():\n    def _ready():\n        unused = None\n        del unused\n        return True\n    return _ready()\n"}],"fixedFiles":[{"path":"app/request.py","source":"def ready():\n    def _ready():\n        unused = None\n\n        return True\n    return _ready()\n"}],"focusPath":"app/request.py","id":"closed-none-deletion","outcome":"reject","scenarioId":"closed-none-deletion","title":"Remove a redundant None deletion marker"},{"expectedCount":0,"files":[{"path":"app/request.py","source":"def ready():\n    def _ready():\n        unused = None\n\n        return True\n    return _ready()\n"}],"fixedFiles":[],"focusPath":"app/request.py","id":"closed-none-deletion-removed","outcome":"accept","scenarioId":"closed-none-deletion","title":"The retained assignment has no lifetime marker"},{"expectedCount":1,"files":[{"path":"app/request.py","source":"payload = dict(raw_payload)\ndel payload['secret']\n"}],"fixedFiles":[],"focusPath":"app/request.py","id":"delete-mapping-entry","outcome":"reject","scenarioId":"primary","title":"Build a mapping without the removed entry"},{"expectedCount":0,"files":[{"path":"app/request.py","source":"payload = {key: value for key, value in raw_payload.items() if key != 'secret'}\n"}],"fixedFiles":[],"focusPath":"app/request.py","id":"immutable-mapping-filter","outcome":"accept","scenarioId":"primary","title":"Construct the desired mapping directly"}],"filePatterns":[],"id":"no-delete-statement","key":"python:no-delete-statement","languages":["python"],"limitations":["Every authored Python del statement is reported once, regardless of how many targets it contains.","Autofix only blanks one standalone, comment-free local deletion in a closed private nested ordinary helper with no parameters and exactly a None assignment to that same name, that deletion, and a literal return. Existing names, the assignment, line numbers and line endings remain unchanged; multiline deletion and unsupported source line separators are refused; all other deletion requires review.","Containing callables must have proven inert execution and no observable helper references, frame or metadata access, callbacks, reachable failures or unknown finalizers. Only proven local None deletion is modeled; deleted names retain an unbound tombstone so subsequent reads cannot resolve to a builtin or global.","Generated and vendored files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Deleting a name, attribute, or collection entry introduces lifetime or in-place mutation semantics that are harder to follow than explicit value construction.","references":[],"remediation":"Remove unused-value deletion markers. Rebuild collections with comprehensions, copies, or immutable value helpers such as dataclasses.replace. When an external API or deliberate lifetime boundary requires deletion, add an exact SARJ442 suppression on the statement and explain why.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_delete_statement.py","status":"active","summary":"Avoid `del` statements; prefer constructing an immutable replacement value.","test":"packages/python/tests/rules/test_no_delete_statement.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ548","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/outcomes.py","source":"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\nclass Success[T](BaseModel):\n    kind: Literal['success'] = 'success'\n    value: T\nclass Error[E](BaseModel):\n    kind: Literal['error'] = 'error'\n    error: E\nclass Denied(BaseModel):\n    code: Literal['denied'] = 'denied'\n    name: str\ntype Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator='kind')]\ndef save(name: str, allowed: bool) -> Result[str, Denied]:\n    return Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))\npersist = save\npersist('record-7', False)\n"}],"fixedFiles":[],"focusPath":"app/outcomes.py","id":"discarded-callable-alias","outcome":"reject","scenarioId":"callable-alias-consumption","title":"A stable alias still returns an outcome that needs handling"},{"expectedCount":1,"files":[{"path":"app/outcomes.py","source":"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\nclass Success[T](BaseModel):\n    kind: Literal['success'] = 'success'\n    value: T\nclass Error[E](BaseModel):\n    kind: Literal['error'] = 'error'\n    error: E\nclass Denied(BaseModel):\n    code: Literal['denied'] = 'denied'\n    name: str\ntype Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator='kind')]\ndef save(name: str, allowed: bool) -> Result[str, Denied]:\n    return Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))\nsave('record-7', False)\n"}],"fixedFiles":[],"focusPath":"app/outcomes.py","id":"discarded-generic-result","outcome":"reject","scenarioId":"generic-result-consumption","title":"A returned generic success/error contract is discarded"},{"expectedCount":1,"files":[{"path":"app/outcomes.py","source":"from typing import Literal\nfrom pydantic import BaseModel\nclass Accepted(BaseModel):\n    kind: Literal['accepted']\n    value: str\nclass Rejected(BaseModel):\n    kind: Literal['rejected']\n    reason: str\ntype Outcome = Accepted | Rejected\ndef check(value: str) -> Outcome:\n    return Accepted(kind='accepted', value=value)\ncheck('value')\n"}],"fixedFiles":[],"focusPath":"app/outcomes.py","id":"discarded-outcome","outcome":"reject","scenarioId":"primary","title":"A tagged outcome is discarded"},{"expectedCount":0,"files":[{"path":"app/outcomes.py","source":"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\nclass Success[T](BaseModel):\n    kind: Literal['success'] = 'success'\n    value: T\nclass Error[E](BaseModel):\n    kind: Literal['error'] = 'error'\n    error: E\nclass Denied(BaseModel):\n    code: Literal['denied'] = 'denied'\n    name: str\ntype Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator='kind')]\ndef save(name: str, allowed: bool) -> Result[str, Denied]:\n    return Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))\npersist = save\ndef run() -> Result[str, Denied]:\n    return persist('record-7', False)\n"}],"fixedFiles":[],"focusPath":"app/outcomes.py","id":"forwarded-callable-alias","outcome":"accept","scenarioId":"callable-alias-consumption","title":"The alias result is propagated to its caller"},{"expectedCount":0,"files":[{"path":"app/outcomes.py","source":"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\nclass Success[T](BaseModel):\n    kind: Literal['success'] = 'success'\n    value: T\nclass Error[E](BaseModel):\n    kind: Literal['error'] = 'error'\n    error: E\nclass Denied(BaseModel):\n    code: Literal['denied'] = 'denied'\n    name: str\ntype Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator='kind')]\ndef save(name: str, allowed: bool) -> Result[str, Denied]:\n    return Success[str](value=name) if allowed else Error[Denied](error=Denied(name=name))\ndef run(name: str, allowed: bool) -> Result[str, Denied]:\n    return save(name, allowed)\n"}],"fixedFiles":[],"focusPath":"app/outcomes.py","id":"forwarded-generic-result","outcome":"accept","scenarioId":"generic-result-consumption","title":"The caller propagates the complete generic result"},{"expectedCount":0,"files":[{"path":"app/outcomes.py","source":"from typing import Literal\nfrom pydantic import BaseModel\nclass Accepted(BaseModel):\n    kind: Literal['accepted']\n    value: str\nclass Rejected(BaseModel):\n    kind: Literal['rejected']\n    reason: str\ntype Outcome = Accepted | Rejected\ndef check(value: str) -> Outcome:\n    return Accepted(kind='accepted', value=value)\ndef run() -> Outcome:\n    return check('value')\n"}],"fixedFiles":[],"focusPath":"app/outcomes.py","id":"forwarded-outcome","outcome":"accept","scenarioId":"primary","title":"The caller forwards the outcome"}],"filePatterns":[],"id":"no-discarded-tagged-outcome","key":"python:no-discarded-tagged-outcome","languages":["python"],"limitations":["Review warning for standalone synchronous calls and directly awaited async calls to stable source-visible module functions. The return must resolve to at least two direct Pydantic, dataclass or required TypedDict variants sharing a string or Boolean Literal discriminator with disjoint values. No name such as Result or Error establishes proof.","Module-local and prepared-project absolute imports, quoted bare aliases and PEP 604 returns, unquoted typing.Union and Annotated, and bounded type aliases are supported. Simple same-module callable assignment aliases are followed only through unique, earlier bindings with no escapes or mutations. Ordinary PEP 695 function parameters and explicitly specialized records/aliases are supported when tags and variant identities are independent of payload parameters. Parameter-dependent variants/tags, bounded/defaulted/variadic parameters, incorrect arity, quoted typing-wrapper combinations, unknown receivers, decorated functions, re-exports, optional/scalar branches, inheritance, dynamic namespaces, rebound or escaped callable identities, generator functions and unsupported annotations abstain.","Named assignments, returned, passed or conditionally consumed results are preserved. A direct assignment to the single underscore target is an explicit discard and is reviewed. An unawaited async call belongs to coroutine checks. Direct class-body execution, generated sources and opaque providers are excluded. Assignment aliases of imported functions, callbacks, factories and forward alias declarations remain outside the proof. This rule identifies discarding; it does not prove that an assignment handles every variant or infer expected failure from a record name.","String or Boolean Literal tags must be direct instance fields on every variant and retain their original class-scope binding. Existing Boolean wire discriminators remain valid; the analyzer does not rename tags or impose Success/Error class names. Quoted field tags remain outside the proof; quoted module return annotations retain their source declaration anchor. Dynamic fields, optional TypedDict fields, other tag types, computed Literal values and multi-field discriminator hierarchies are not inferred. A discriminator does not prove JSON serialization for arbitrary payloads. No automatic rewrite changes call behavior or error handling."],"messageIds":[],"optionsSchema":null,"rationale":"A tagged return union makes possible outcomes visible in the type contract. Discarding the value bypasses the caller's opportunity to handle its alternatives, including a declared failure.","references":[],"remediation":"Use or forward the returned outcome. Handle its variants with match and a typed exhaustive fallback such as assert_never; preserve payloads and side effects. Do not delete the operation or hide the result behind a dummy assignment.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_discarded_tagged_outcome.py","status":"active","summary":"Handle a returned tagged outcome instead of discarding it.","test":"packages/python/tests/rules/test_no_discarded_tagged_outcome.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ519","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/counts.py","source":"def checked_count(raw: str) -> int:\n    if not raw:\n        raise ValueError('empty count')\n    return int(raw)\ndef save(raw: str) -> int:\n    return checked_count(raw)\n"}],"fixedFiles":[],"focusPath":"app/counts.py","id":"consume-checked-value","outcome":"accept","scenarioId":"primary","title":"Pass the validated replacement to the next operation"},{"expectedCount":1,"files":[{"path":"app/counts.py","source":"def checked_count(raw: str) -> int:\n    if not raw:\n        raise ValueError('empty count')\n    return int(raw)\ndef save(raw: str) -> None:\n    checked_count(raw)\n"}],"fixedFiles":[],"focusPath":"app/counts.py","id":"discarded-checked-value","outcome":"reject","scenarioId":"primary","title":"A checked replacement is constructed and discarded"}],"filePatterns":[],"id":"no-discarded-validation-result","key":"python:no-discarded-validation-result","languages":["python"],"limitations":["Reports standalone calls to source-resolved, undecorated Python functions with a non-None return annotation, value returns, and an explicit raise.","Local and imported functions, import aliases, bounded re-exports, and directly awaited async functions are supported; unknown receivers, decorated callables, and dynamic namespaces are excluded.","Compiler scopes select runtime bindings; observed global/nonlocal reassignment and ambiguous scopes are excluded. Import identity is established without executing provider modules.","Observed first-party namespace writes and opaque module escapes invalidate affected providers across aliases, self imports and re-exports; package namespace hazards conservatively cover descendants.","Source-proven unittest.mock.patch APIs invalidate static contracts in the caller and provider/re-export modules, including string-target patches. Ordinary locally named patch functions do not disable checks.","Stable local callable aliases with prior direct assignments are followed through bounded chains. Conditional assignments and unresolved callable escapes are excluded.","Direct execution in class bodies is excluded because sequential lookup and metaclass-prepared namespaces can supply different callees. Method bodies remain checked.","Functions with explicit mutation, standalone command calls, generators, nested definitions, or context-manager/try/loop orchestration are excluded. This is not a general purity proof.","Calls inside returned values must resolve to supported builtin or source-resolved nominal value construction. Opaque command functions, receiver calls, and direct returned attribute/subscript reads are excluded; a conversion around an unknown command does not prove validation.","Import-resolved pytest.raises and source-proven unittest exception assertion blocks observe the exception contract and are excluded. Generated and vendored sources are excluded; maintained tests are checked.","Top-level return contracts follow stable lexical aliases, first-party type imports, unions, and Annotated type content through a bounded source-only analysis. Nullable collection elements and annotation metadata do not make the returned collection nullable.","Nullable, object, Any, Never, NoReturn, generic type-parameter, structural protocol, and unresolved return contracts are excluded. No automatic assignment, deletion, or validation rewrite is offered."],"messageIds":[],"optionsSchema":null,"rationale":"Discarding a conversion's returned value makes validation an implicit exception side effect. The caller can continue with unvalidated or unnormalized inputs even after constructing a checked replacement.","references":[],"remediation":"Use the checked result in the following operation, or expose a separate validation command returning None. Preserve exception behavior; do not delete the call or add a dummy assignment.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_discarded_validation_result.py","status":"active","summary":"Consume a value-producing validator's result instead of relying on its exceptions.","test":"packages/python/tests/rules/test_no_discarded_validation_result.py"},{"aliases":["no-typed-doc-sections"],"autofix":"safe","category":"maintainability","code":"SARJ092","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app.py","source":"def decode(payload: str) -> dict[str, object]:\n    \"\"\"Decode the payload.\n\n    Args:\n        payload: UTF-8 JSON; duplicate keys are rejected.\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(payload: str) -> dict[str, object]:\n    \"\"\"Decode the payload.\n\n    Args:\n        payload (str): UTF-8 JSON; duplicate keys are rejected.\n    \"\"\"\n    return {}\n"}],"fixedFiles":[],"focusPath":"app.py","id":"parameter-type-restatement","outcome":"reject","scenarioId":"primary","title":"Remove only the repeated type label"}],"filePatterns":[],"id":"no-docstring-type-restatement","key":"python:no-docstring-type-restatement","languages":["python"],"limitations":["Safe fixes remove only native-proven redundant type labels in plain unescaped private nested synchronous nongenerator functions with no detected metadata consumer. Behavioral descriptions, other sections and quote style remain intact; public APIs, escaped/decorated helpers and escaped/raw literals remain review-only. Known frame and metadata capabilities, their import aliases and metadata field access prevent fixes.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","Only fully typed functions and Google-like colon-headed argument, return, and yield sections are checked.","A documented label must parse as a Python type expression and match the corresponding annotation.","Generated files, runtime-consumed prompt, CLI, and route docstrings, properties, abstract methods, overloads, protocol methods, and partially typed signatures are excluded.","NumPy underlined sections and Sphinx type fields intentionally remain with their selected documentation convention.","A reasoned exact-code suppression on the function declaration or closing docstring line preserves required documentation tooling conventions; markers inside string content are not suppression comments."],"messageIds":[],"optionsSchema":null,"rationale":"A repeated type spelling can drift from the annotation and obscures the behavioral contract that only prose can express.","references":[],"remediation":"Remove only the redundant type label. Keep behavioral meaning, units, defaults, constraints, and raised-error conditions in the docstring.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_typed_doc_sections.py","status":"active","summary":"A docstring type label repeats an annotation from the fully typed signature.","test":"packages/python/tests/rules/test_no_typed_doc_sections.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ522","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/routes.py","source":"from pydantic import BaseModel\nclass Route(BaseModel):\n    destination: str\n\ndef _field(route: object, name: str) -> object:\n    if isinstance(route, Route):\n        return getattr(route, name)\n    if isinstance(route, dict):\n        return route.get(name)\n    return None\ndef destination(raw: object) -> object:\n    return _field(raw, 'destination')\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"model-or-dict-reader","outcome":"reject","scenarioId":"primary","title":"A fixed model field is hidden behind a string-selected object return"},{"expectedCount":0,"files":[{"path":"app/routes.py","source":"from pydantic import BaseModel\nclass Route(BaseModel):\n    destination: str\n\ndef destination(raw: object) -> str:\n    route = Route.model_validate(raw)\n    return route.destination\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"normalized-model","outcome":"accept","scenarioId":"primary","title":"Normalize the input and read the typed field"}],"filePatterns":[],"id":"no-dual-shape-model-field-reader","key":"python:no-dual-shape-model-field-reader","languages":["python"],"limitations":["Checks stable private module functions with two parameters, an object/Any input and return, and a string field selector in either parameter order. The body must consist of a known local Pydantic model getattr branch, a builtin dict or import-proven standard Mapping get/subscript branch and a None fallback. Import-proven typing casts are transparent.","Every visible use must be a direct call with a declared model field selected by a literal or a bounded chain of dominating single-assignment constants. Dynamic selectors, escaped/public helpers, imported/inherited models, custom attribute hooks, shadowed builtins and ambiguous or rebound bindings abstain. No application modules are imported.","Opaque values, arbitrary dictionaries and generic reflection alone are valid. Raw Pydantic validators are not prohibited, but extracting this closed dual-shape reader does not make its erased field contract typed.","Authored tests are checked; generated/vendor code is excluded. This is a warning without an autofix because normalization can change rejection, coercion, default generation and error timing."],"messageIds":[],"optionsSchema":null,"rationale":"A string-selected object return discards the model's field types and duplicates the raw and validated representations at every read. Literal field names at all local callers prove this is a fixed record API.","references":[],"remediation":"Normalize raw input once with the named model's model_validate, then use typed attributes. Preserve missing-field, coercion, default and validation-error behavior; before validators still own raw input.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_dual_shape_model_field_reader.py","status":"active","summary":"Normalize a known model instead of reading its fields through a model-or-dict accessor.","test":"packages/python/tests/rules/test_no_dual_shape_model_field_reader.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ438","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"diagnostics/__init__.py","source":"from .models import Diagnostic as Diagnostic\n\n__all__ = [\"Diagnostic\"]\n"}],"fixedFiles":[],"focusPath":"diagnostics/__init__.py","id":"duplicated-export-list","outcome":"reject","scenarioId":"primary","title":"Do not repeat an explicit re-export list"},{"expectedCount":0,"files":[{"path":"diagnostics/__init__.py","source":"from .models import Diagnostic as Diagnostic\n"}],"fixedFiles":[],"focusPath":"diagnostics/__init__.py","id":"explicit-re-export","outcome":"accept","scenarioId":"primary","title":"Make the re-export explicit in the import"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"def values():\n    __ａｌｌ__ = [\"value\"]\n    return __all__\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"normalized-local-binding","outcome":"accept","scenarioId":"normalized-module-name","title":"A local binding is outside the module export contract"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"__ａｌｌ__ = [\"value\"]\nvalue = 1\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"normalized-module-export-binding","outcome":"reject","scenarioId":"normalized-module-name","title":"Compatibility identifiers bind the native module export name"}],"filePatterns":[],"id":"no-dunder-all","key":"python:no-dunder-all","languages":["python"],"limitations":["Reads, attributes on other objects, indirect mutation through arbitrary calls, and ordinary local variables are not reported; the rule targets module-owned bindings and provable mutations.","Writes inside a function or class are reported only when that lexical scope declares `global __all__`.","Generated source, malformed source, and stub files (`.pyi`) are outside this rule's scope.","Dynamic construction through `globals()`, `setattr()`, or `exec()` is not inferred.","No autofix is offered because removal can change `module.__all__` introspection, wildcard imports, underscore-name exports, ordering, and dynamic or conditional export behavior."],"messageIds":[],"optionsSchema":null,"rationale":"`__all__` creates a second, mutable wildcard-import surface that can drift from runtime bindings; Sarj code uses explicit imports and private-name conventions instead.","references":[],"remediation":"Before deleting `__all__`, replace wildcard-import consumers with explicit imports, retain each intended binding, and prefix implementation-only bindings with `_`. Use self-aliased imports to mark intentional facade re-exports to static tooling. For a whole-module compatibility alias, replace the entry in `sys.modules` and migrate maintained consumers to the canonical path instead of copying every name.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_dunder_all.py","status":"active","summary":"modules should not define or mutate `__all__`","test":"packages/python/tests/rules/test_no_dunder_all.py"},{"aliases":[],"autofix":"safe","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":["Safe fixes remove duplicate elements only from comment-free literal sequences. Standalone literal append calls can also be removed. Any insert growth prevents autofixing the file because removing earlier entries would change positional insertion order; commented declarations remain review-only.","One fully static module-level list or tuple plus literal append, extend, and insert growth 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":"maintainability","code":"SARJ496","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"models/contact.py","source":"from pydantic import BaseModel, field_validator\n\nclass Contact(BaseModel):\n    first_name: str\n    last_name: str\n\n    @field_validator('first_name', mode='before')\n    @classmethod\n    def strip_first(cls, value: str) -> str:\n        return value.strip()\n\n    @field_validator('last_name', mode='before')\n    @classmethod\n    def strip_last(cls, raw: str) -> str:\n        return raw.strip()\n"}],"fixedFiles":[],"focusPath":"models/contact.py","id":"duplicate-strip-validators","outcome":"reject","scenarioId":"primary","title":"Two validators repeat one strip rule"},{"expectedCount":0,"files":[{"path":"models/contact.py","source":"from pydantic import BaseModel, field_validator\n\nclass Contact(BaseModel):\n    first_name: str\n    last_name: str\n\n    @field_validator('first_name', 'last_name', mode='before')\n    @classmethod\n    def strip_names(cls, value: str) -> str:\n        return value.strip()\n"}],"fixedFiles":[],"focusPath":"models/contact.py","id":"one-multi-field-validator","outcome":"accept","scenarioId":"primary","title":"One validator names every field that shares the rule"}],"filePatterns":[],"id":"no-duplicate-field-validator-body","key":"python:no-duplicate-field-validator-body","languages":["python"],"limitations":["Only methods decorated with an import-proven Pydantic `field_validator` in the same class body are compared.","Bodies must match structurally after renaming parameters by position and dropping a leading docstring.","Validators with different `mode` arguments, different decorator stacks, or near-identical bodies are not reported."],"messageIds":[],"optionsSchema":null,"rationale":"Two field validators with the same mode and the same body are one validation rule written twice; a later fix to one copy silently leaves the other field validated by the old rule.","references":[],"remediation":"List every field in one `@field_validator(...)` decorator and branch on `info.field_name` where the fields genuinely differ.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_duplicate_field_validator_body.py","status":"active","summary":"Pydantic field validators in one model repeat the same body.","test":"packages/python/tests/rules/test_no_duplicate_field_validator_body.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ557","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service/reader.py","source":"from dataclasses import dataclass\n\n@dataclass(frozen=True)\nclass User:\n    name: str\n\ndef read(value: User) -> str:\n    return value.name\n"}],"fixedFiles":[],"focusPath":"service/reader.py","id":"declared-field","outcome":"accept","scenarioId":"primary","title":"A declared field makes the contract explicit"},{"expectedCount":1,"files":[{"path":"service/reader.py","source":"def read(value, field):\n    return getattr(value, field, None)\n"}],"fixedFiles":[],"focusPath":"service/reader.py","id":"dynamic-field","outcome":"reject","scenarioId":"primary","title":"A string field name hides the object contract"}],"filePatterns":[],"id":"no-dynamic-builtins","key":"python:no-dynamic-builtins","languages":["python"],"limitations":["Ruff S307, S102, T100, B009, B043 and qualified/imported banned-api checks retain their native owners; this rule closes bare-name and simple module-alias gaps.","Locally shadowed names and unrelated methods are accepted. Arbitrary dictionary/reflection lookups and complex rebinding are not reconstructed."],"messageIds":[],"optionsSchema":null,"rationale":"Dynamic namespace inspection, source compilation, and string-based attribute access hide dependencies from type checking and refactoring. The strict policy also rejects executor/debugger references that can otherwise escape their native call checks through an alias.","references":[],"remediation":"Pass typed state explicitly, use explicit imports and declared attributes, and keep source as data. A required compiler or plugin boundary needs a local, explained suppression.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_dynamic_builtins.py","status":"active","summary":"Use explicit typed state instead of dynamic Python builtin access.","test":"packages/python/tests/rules/test_no_dynamic_builtins.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ505","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"api.py","source":"from fastapi import HTTPException\n\n\ndef load(store) -> None:\n    try:\n        store.load()\n    except Exception as exc:\n        raise HTTPException(status_code=500, detail=f'load failed: {exc}') from exc\n"}],"fixedFiles":[],"focusPath":"api.py","id":"exception-text","outcome":"reject","scenarioId":"primary","title":"A 500 detail returns the exception message"},{"expectedCount":0,"files":[{"path":"api.py","source":"from fastapi import HTTPException\n\n\ndef load(store) -> None:\n    try:\n        store.load()\n    except Exception as exc:\n        raise HTTPException(status_code=500, detail='load failed') from exc\n"}],"fixedFiles":[],"focusPath":"api.py","id":"fixed-detail","outcome":"accept","scenarioId":"primary","title":"A 500 detail is a fixed message"}],"filePatterns":[],"id":"no-exception-text-in-500-detail","key":"python:no-exception-text-in-500-detail","languages":["python"],"limitations":["Checks `HTTPException` from FastAPI or Starlette with a status resolved to 500 (integer, Starlette `status.HTTP_500_*` or `HTTPStatus.INTERNAL_SERVER_ERROR`).","The detail must use the variable bound by an enclosing `except ... as name` handler directly (not one of its attributes), call a `traceback.format_*` function, or reference `__file__`.","Exception text copied first into another variable or model is not traced. Other statuses are not checked because 4xx details commonly map domain errors. Tests and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"An unexpected exception's text can contain SQL, file paths, hostnames, provider responses or personal data. Copying it into a 500 response discloses internals to any caller and gives attackers a probe.","references":[],"remediation":"Log the exception with its traceback, then raise the 500 with a fixed detail or error code.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_exception_text_in_500_detail.py","status":"active","summary":"HTTP 500 exception details must not carry exception text or tracebacks.","test":"packages/python/tests/rules/test_no_exception_text_in_500_detail.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ444","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"src/decision.py","source":"def ready():\n    def _ready():\n        if True:\n            if True:\n                if True:\n                    if True:\n                        if True:\n                            if True:\n                                return True\n    return _ready()\n"}],"fixedFiles":[{"path":"src/decision.py","source":"def ready():\n    def _ready():\n\n\n\n\n\n\n        return True\n    return _ready()\n"}],"focusPath":"src/decision.py","id":"closed-literal-true-guards","outcome":"reject","scenarioId":"closed-literal-true-guards","title":"Remove statically redundant nesting"},{"expectedCount":0,"files":[{"path":"src/decision.py","source":"def ready():\n    def _ready():\n\n\n\n\n\n\n        return True\n    return _ready()\n"}],"fixedFiles":[],"focusPath":"src/decision.py","id":"flattened-literal-true-guards","outcome":"accept","scenarioId":"closed-literal-true-guards","title":"The authored return needs no constant guards"},{"expectedCount":0,"files":[{"path":"src/decision.py","source":"def decide(a, b, c, d, e, f):\n    if not a:\n        return\n    if not b:\n        return\n    if not c:\n        return\n    if not d:\n        return\n    if not e:\n        return\n    if not f:\n        return\n    act()\n"}],"fixedFiles":[],"focusPath":"src/decision.py","id":"guard-decisions","outcome":"accept","scenarioId":"primary","title":"Guard clauses reduce nesting"},{"expectedCount":0,"files":[{"path":"src/library.py","source":"def can_borrow(member, book):\n    if not member.active:\n        return False\n    if not member.email_verified:\n        return False\n    if member.suspended:\n        return False\n    if not (member.balance == 0):\n        return False\n    if not (member.borrowed_count < 5):\n        return False\n    if not book.available:\n        return False\n    if book.reference_only:\n        return False\n    return True\n"}],"fixedFiles":[],"focusPath":"src/library.py","id":"library-check-after","outcome":"accept","scenarioId":"library-check","title":"Synthetic library eligibility after: score 7"},{"expectedCount":1,"files":[{"path":"src/library.py","source":"def can_borrow(member, book):\n    if member.active:\n        if member.email_verified:\n            if not member.suspended:\n                if member.balance == 0:\n                    if member.borrowed_count < 5:\n                        if book.available:\n                            if not book.reference_only:\n                                return True\n    return False\n"}],"fixedFiles":[],"focusPath":"src/library.py","id":"library-check-before","outcome":"reject","scenarioId":"library-check","title":"Synthetic library eligibility before: score 28"},{"expectedCount":1,"files":[{"path":"src/decision.py","source":"def decide(a, b, c, d, e, f):\n    if a:\n        if b:\n            if c:\n                if d:\n                    if e:\n                        if f:\n                            act()\n"}],"fixedFiles":[],"focusPath":"src/decision.py","id":"nested-decisions","outcome":"reject","scenarioId":"primary","title":"Six nested decisions require a review"}],"filePatterns":[],"id":"no-excessive-cognitive-complexity","key":"python:no-excessive-cognitive-complexity","languages":["python"],"limitations":["Autofix only flattens a whole-function chain of at most 32 literal True guards with no else to its existing standalone single-line literal return, inside a closed private nested ordinary zero-parameter helper. Other complexity requires review.","All containing callables must have proven inert execution without Global/Nonlocal declarations, escaped helper references, observable frames or metadata, callbacks, reachable failures or unknown finalizers. Comments and unsupported source line separators prevent edits. The exact return spelling, physical line number, surrounding source and LF/CRLF endings are preserved.","Sarj metric, not exact Sonar compatibility: functions and lambdas are independent; calls, recursion, function annotations, parameter defaults, and decorators are not scored.","Conditions, loops, exception handlers, match statements, and conditional expressions add one plus nesting. Elif and else add one; their bodies add nesting. Each run of like boolean operators adds one.","Comprehension loops and filters add one plus nesting. Match cases are free; case guards count as nested conditions. Try, finally, with, return, break, continue, and await add no points themselves.","Generated code is excluded; authored tests and all function lengths are checked. The threshold is a shared policy constant, not a correctness boundary."],"messageIds":[],"optionsSchema":null,"rationale":"Nested control flow increases the context a reader must retain while following a function.","references":[],"remediation":"Examples are fictional and written for this documentation; domain types are omitted. The library example replaces nested eligibility checks with early returns while preserving evaluation order. Refactor one function at a time, starting with its largest contributors. First simplify control flow and reduce nesting within the function. Extract a helper only when it clarifies a cohesive responsibility or enables meaningful reuse; do not split code solely to lower the score. Use lookup tables only for equivalent pure dispatch. Preserve APIs, side effects, evaluation order, and exception behavior. Run relevant tests before and after; remeasure the original and extracted functions. Do not hide branches in dense expressions or add indirection just to lower a score.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_excessive_cognitive_complexity.py","status":"active","summary":"Error on cognitive complexity above 20; scores up to 20 pass.","test":"packages/python/tests/rules/test_no_excessive_cognitive_complexity.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ427","defaultLevel":"error","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":["Decorators on proven FastAPI, APIRouter, Starlette, and Starlette Router values or their `.router` 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":["no-file-level-escape-hatch-noqa"],"autofix":"safe","category":"maintainability","code":"SARJ054","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/service.py","source":"# ruff: file-ignore[banned-api]\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-suppression","key":"python:no-file-level-escape-hatch-suppression","languages":["python"],"limitations":["Autofix only removes an exact sole-selector TID251/banned-api comment when the entire nonempty module contains only simple name assignments of primitive literals and no other comments. Such a module has no imports, calls, attributes, definitions, annotations or API reads that could require a banned-api exception. Every executable source position and compiled code object is retained.","Mixed selectors, trailing reasons, additional directives or comments, annotations, containers, expressions and all executable API uses remain manual; no per-site suppression or rationale is invented.","Detection covers legacy `ruff: noqa: CODE` and modern `ruff: file-ignore[name]` directives naming configured escape-hatch rules.","Inline noqa comments, balanced Ruff disable/enable ranges, 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_suppression.py","status":"active","summary":"File-level Ruff suppression disables an escape-hatch rule across the entire file.","test":"packages/python/tests/rules/test_no_file_level_escape_hatch_suppression.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ048","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":".git/keep","source":"fixture\n"},{"path":"python/core/core/__init__.py","source":"\n"},{"path":"python/core/core/helpers.py","source":"def _decode(value):\n    return value\n"},{"path":"python/core/pyproject.toml","source":"[project]\nname = \"core\"\n"},{"path":"python/service/pyproject.toml","source":"[project]\nname = \"service\"\n"},{"path":"python/service/service/__init__.py","source":"\n"},{"path":"python/service/service/consumer.py","source":"from core.helpers import _decode\n"}],"fixedFiles":[],"focusPath":"python/service/service/consumer.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/service/consumer.py","source":"from core.helpers import decode\n"}],"fixedFiles":[],"focusPath":"python/service/service/consumer.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.","Static imports and literal importlib.import_module calls with stable stdlib import provenance are analyzed.","Relative imports, dynamic module names, public and dunder names, third-party and standard-library imports, and supported compiled extensions are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Private imports couple consumers, including white-box tests, to internals instead of a public surface the owning package can maintain.","references":[],"remediation":"Use the owning package's public API. Test public behavior; promote an internal only when it deserves an explicit reusable contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_first_party_private_import.py","status":"active","summary":"A first-party consumer imports a private name or private module across its package boundary.","test":"packages/python/tests/rules/test_no_first_party_private_import.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ455","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_monitor.py","source":"import asyncio\n\n\nasync def test_reports_silence(monitor):\n    monitor.start()\n    await asyncio.wait_for(monitor.fired.wait(), timeout=5)\n    assert monitor.reason == 'silence'\n"}],"fixedFiles":[],"focusPath":"tests/test_monitor.py","id":"test-awaits-signal-then-asserts","outcome":"accept","scenarioId":"primary","title":"A test awaits the signal with a deadline before asserting"},{"expectedCount":1,"files":[{"path":"tests/test_worker.py","source":"import time\n\n\ndef test_worker_drains_queue(worker, queue):\n    worker.start()\n    time.sleep(1)\n    assert queue.pending() == 0\n"}],"fixedFiles":[],"focusPath":"tests/test_worker.py","id":"test-blocks-then-asserts","outcome":"reject","scenarioId":"blocking-sleep","title":"A synchronous test blocks for a guessed interval before asserting"},{"expectedCount":0,"files":[{"path":"tests/test_worker.py","source":"import time\n\n\ndef test_worker_drains_queue(worker, queue):\n    worker.start()\n    deadline = time.monotonic() + 5\n    while queue.pending() and time.monotonic() < deadline:\n        time.sleep(0.01)\n    assert queue.pending() == 0\n"}],"fixedFiles":[],"focusPath":"tests/test_worker.py","id":"test-polls-with-deadline-then-asserts","outcome":"accept","scenarioId":"blocking-sleep","title":"A synchronous test polls the condition with a deadline before asserting"},{"expectedCount":1,"files":[{"path":"tests/test_monitor.py","source":"import asyncio\n\n\nasync def test_reports_silence(monitor):\n    monitor.start()\n    await asyncio.sleep(0.5)\n    assert monitor.reason == 'silence'\n"}],"fixedFiles":[],"focusPath":"tests/test_monitor.py","id":"test-sleeps-then-asserts","outcome":"reject","scenarioId":"primary","title":"A test waits a guessed interval before asserting"}],"filePatterns":[],"id":"no-fixed-sleep-before-assert","key":"python:no-fixed-sleep-before-assert","languages":["python"],"limitations":["Only pytest-collected test functions in test_*.py and *_test.py modules are inspected; fixtures, helpers, nested functions, and conftest.py are not.","A sleep is reported only when the next statement in the same block is an assert, so an assert placed after the enclosing with, if, or try block is not reported.","A duration is fixed only when built from numeric literals and UPPER_CASE constants; durations read from other names or from calls are not reported.","Sleeps inside loops are treated as polling, and time.sleep in an async test is left to Ruff ASYNC251."],"messageIds":[],"optionsSchema":null,"rationale":"A fixed sleep followed by an assertion guesses how long the code under test needs. On a loaded CI runner the guess is too short and the test flakes; on an idle one every run pays the whole interval.","references":[],"remediation":"Await the signal the code exposes, such as an event, future, queue item, or fake callback, or poll the condition with a deadline. When the test deliberately proves that nothing happens during a quiet window, keep the sleep with an exact SARJ455 suppression that names the window.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_fixed_sleep_before_assert.py","status":"active","summary":"Do not sleep for a fixed interval and then assert in a test.","test":"packages/python/tests/rules/test_no_fixed_sleep_before_assert.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ485","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/health.py","source":"from collections.abc import Callable\n\n\ndef wait_for_health(probe, sleep: Callable[[float], None]):\n    for _ in range(30):\n        if probe():\n            return\n        sleep(0.5)\n"}],"fixedFiles":[],"focusPath":"app/health.py","id":"blocking-loop-calls-injected-sleeper","outcome":"accept","scenarioId":"blocking-sleep","title":"A synchronous retry loop calls an injected sleeper"},{"expectedCount":1,"files":[{"path":"app/health.py","source":"import time\n\n\ndef wait_for_health(probe):\n    for _ in range(30):\n        if probe():\n            return\n        time.sleep(0.5)\n"}],"fixedFiles":[],"focusPath":"app/health.py","id":"blocking-loop-sleeps-literal","outcome":"reject","scenarioId":"blocking-sleep","title":"A synchronous retry loop blocks for a hardcoded interval"},{"expectedCount":0,"files":[{"path":"app/jobs.py","source":"import asyncio\n\n\nclass JobWaiter:\n    def __init__(self, poll_seconds: float) -> None:\n        self._poll_seconds = poll_seconds\n\n    async def wait_until_done(self, job):\n        while not await job.done():\n            await asyncio.sleep(self._poll_seconds)\n"}],"fixedFiles":[],"focusPath":"app/jobs.py","id":"polling-loop-sleeps-injected-interval","outcome":"accept","scenarioId":"primary","title":"A polling loop sleeps for an injected interval"},{"expectedCount":1,"files":[{"path":"app/jobs.py","source":"import asyncio\n\n\nasync def wait_until_done(job):\n    while not await job.done():\n        await asyncio.sleep(2)\n"}],"fixedFiles":[],"focusPath":"app/jobs.py","id":"polling-loop-sleeps-literal","outcome":"reject","scenarioId":"primary","title":"A polling loop sleeps for a hardcoded interval"}],"filePatterns":[],"id":"no-fixed-sleep-in-loop","key":"python:no-fixed-sleep-in-loop","languages":["python"],"limitations":["Only awaited asyncio.sleep, anyio.sleep, and trio.sleep, and time.sleep in synchronous functions, are inspected; import aliases are followed and shadowed names abstain. time.sleep in an async function is left to Ruff ASYNC251.","A duration is fixed when it folds with +, -, *, /, and // from numeric literals and UPPER_CASE constants bound once at module scope in the same file to such a literal; zero and negative results are yields, not waits.","Imported, reassigned, or shadowed constants, lowercase names, attributes including class constants, subscripts, and calls are treated as configurable and are not reported.","The sleep must sit in the body of a for, async for, or while loop inside the same function; loop else clauses, module-level and class-level loops, and loops in an enclosing function across a nested def, lambda, or class are not reported.","Test modules, test directories, conftest.py, test-support paths, and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A polling or cleanup loop that sleeps for a literal interval gives tests no seam: every test that drives the loop pays the whole interval in real time, and replacing the module's sleep is a monkeypatch that SARJ445 forbids. Suites slow down one hardcoded interval at a time.","references":[],"remediation":"Route the interval through a constructor or configuration parameter, or call an injected sleeper or clock, so tests can pass a zero interval or a recording fake. Keep a literal only behind an exact SARJ485 suppression that names why the interval must never change under test.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_fixed_sleep_in_loop.py","status":"active","summary":"Do not sleep for a fixed literal interval inside a production loop.","test":"packages/python/tests/rules/test_no_fixed_sleep_in_loop.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ498","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"client/records.py","source":"def fetch(response):\n    if response.status_code in (400, 404):\n        return None\n    return response.json()\n"}],"fixedFiles":[],"focusPath":"client/records.py","id":"folded-membership","outcome":"reject","scenarioId":"primary","title":"A 400 is treated as not found"},{"expectedCount":0,"files":[{"path":"client/records.py","source":"def fetch(response):\n    if response.status_code == 404:\n        return None\n    if response.status_code == 400:\n        raise ValueError(response.text)\n    return response.json()\n"}],"fixedFiles":[],"focusPath":"client/records.py","id":"separate-branches","outcome":"accept","scenarioId":"primary","title":"Not found and bad request are separate outcomes"}],"filePatterns":[],"id":"no-folded-bad-request-not-found","key":"python:no-folded-bad-request-not-found","languages":["python"],"limitations":["Membership tests against a literal collection, `or` chains of equality on one subject, and match or-patterns are checked when they name both 400 and 404 as integers or recognized status constants.","Statuses held in variables or named constants other than the HTTPStatus and Starlette names are not resolved.","Test modules are excluded because parametrized cases list statuses together."],"messageIds":[],"optionsSchema":null,"rationale":"A 400 means the request or its configuration is wrong; folding it into the not-found path makes a misconfiguration log success with an empty result, indistinguishable from a deleted record.","references":[],"remediation":"Give 404 its own branch and raise the 400 as a non-retriable client error.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_folded_bad_request_not_found.py","status":"active","summary":"Do not handle HTTP 400 and 404 statuses in one branch.","test":"packages/python/tests/rules/test_no_folded_bad_request_not_found.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)`, a literal model_config dictionary, or `frozen=True` as a class argument.","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":"correctness","code":"SARJ552","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_api.py","source":"from pydantic import JsonValue, RootModel\nclass Response(RootModel[dict[str, JsonValue]]):\n    pass\n"}],"fixedFiles":[],"focusPath":"tests/test_api.py","id":"generic-response-parser","outcome":"reject","scenarioId":"primary","title":"An open JSON mapping does not declare the response fields"},{"expectedCount":0,"files":[{"path":"tests/test_api.py","source":"from pydantic import BaseModel\nclass Response(BaseModel):\n    version: int\n"}],"fixedFiles":[],"focusPath":"tests/test_api.py","id":"partial-declarative-response","outcome":"accept","scenarioId":"primary","title":"Declare just the field this consumer needs"}],"filePatterns":[],"id":"no-generic-json-parser","key":"python:no-generic-json-parser","languages":["python"],"limitations":["Reports import-proven TypeAdapter construction and RootModel bases containing JsonValue, Json, Any or object, including module-local explicit type aliases. Tests remain in scope; generated files are excluded.","Typed dictionaries of domain values, model fields storing arbitrary JSON, opaque imported aliases and custom parser classes are excluded. Recursive arbitrary unions without a canonical open type are not inferred.","Generic schema infrastructure may need a reasoned local suppression. No model or complete request schema is synthesized automatically."],"messageIds":[],"optionsSchema":null,"rationale":"An open JSON domain validates syntax and value kinds without declaring required fields or their relationships; consumers must then reconstruct the contract manually.","references":[],"remediation":"Declare only the fields the consumer needs in a named Pydantic model and use model_validate_json or model_validate. Allow unrelated fields when the wire contract permits them. Document an exact-code exception for genuinely schema-agnostic infrastructure.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_generic_json_parser.py","status":"active","summary":"Parse consumed JSON fields with a bespoke declarative model instead of an open JSON parser.","test":"packages/python/tests/rules/test_no_generic_json_parser.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 distinct top-level public class or function name 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":"maintainability","code":"SARJ499","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"service/agent.py","source":"def model_name(settings: object) -> str:\n    return settings.model if hasattr(settings, 'model') else 'default'\n"}],"fixedFiles":[],"focusPath":"service/agent.py","id":"hasattr-probe","outcome":"reject","scenarioId":"primary","title":"A hasattr probe hides a loose type"},{"expectedCount":0,"files":[{"path":"service/agent.py","source":"from dataclasses import dataclass\n\n@dataclass\nclass LLMSettings:\n    model: str\n\ndef model_name(settings: LLMSettings) -> str:\n    return settings.model\n"}],"fixedFiles":[],"focusPath":"service/agent.py","id":"typed-access","outcome":"accept","scenarioId":"primary","title":"A typed parameter states which attributes exist"}],"filePatterns":[],"id":"no-hasattr","key":"python:no-hasattr","languages":["python"],"limitations":["Unshadowed builtin references and simple builtins-module aliases are reported, including tests, module probes and dunder probes.","Native Ruff B004 owns direct __call__ probes; TID251 owns qualified builtin access and imports. Required feature detection needs an explained local suppression."],"messageIds":[],"optionsSchema":null,"rationale":"In strictly typed code, hasattr hides whether an attribute can be missing at all: it silences the type checker instead of fixing a loose type, so a renamed field quietly takes the fallback branch.","references":[],"remediation":"Narrow the value with isinstance, a Protocol, or a typed wrapper around the untyped library; keep a reasoned SARJ suppression only for genuine runtime feature detection.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_hasattr.py","status":"active","summary":"Do not probe attributes with the builtin hasattr.","test":"packages/python/tests/rules/test_no_hasattr.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 and a first-party composition call; covers None fallbacks and settings attributes captured directly in module-level class constructor defaults.","Provider instance, class, import and reexport names require one stable module binding. Reassignments, captures, global writers, wildcard imports and unknown class decorators conservatively exclude settings provenance; annotation-only declarations, read-only globals and stable typing final/runtime_checkable decorators remain valid.","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":"SARJ491","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, Field\nclass Model(BaseModel):\n    value: int = Field(gt=0, lt=1)\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"empty-integer-interval","outcome":"reject","scenarioId":"primary","title":"No integer lies strictly between zero and one"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, Field\nclass Model(BaseModel):\n    value: int = Field(ge=0, lt=1)\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"integer-singleton","outcome":"accept","scenarioId":"primary","title":"Zero is admitted by the interval"}],"filePatterns":[],"id":"no-impossible-pydantic-field-bounds","key":"python:no-impossible-pydantic-field-bounds","languages":["python"],"limitations":["Only plain direct module-level BaseModel subclasses and built-in int, float, str and list fields are inspected.","Bounds must be finite literals; int and length bounds must be integers, and float bounds must convert exactly.","Positive integer multiple_of constraints use exact integer arithmetic; floating-point steps remain outside the rule.","Literal strict, frozen, validate_default and extra model settings preserve coverage; other or dynamic configuration is excluded.","Unknown metadata, nullable types, repeated bound names, methods, dynamic namespaces and generated files are excluded.","Visible namespace APIs, dynamic module access, ambiguous imports and escaped or mutated imported objects exclude the file.","No autofix chooses a domain or changes validation behavior."],"messageIds":[],"optionsSchema":null,"rationale":"Contradictory literal constraints reject every field value, including integer multiples and float intervals with no representable member.","references":[],"remediation":"Correct the conflicting bounds so the declared field domain admits the intended values.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_impossible_pydantic_field_bounds.py","status":"active","summary":"Pydantic field bounds must admit at least one value.","test":"packages/python/tests/rules/test_no_impossible_pydantic_field_bounds.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ471","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/service.py","source":"import importlib\ndef run(*, loader=importlib.import_module):\n    return loader('plugin').create()\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"injected-loader","outcome":"reject","scenarioId":"primary","title":"Import machinery becomes a configurable parameter"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"def run():\n    from plugin import create\n    return create()\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"lazy-import","outcome":"accept","scenarioId":"primary","title":"Implementation owns its lazy import"}],"filePatterns":[],"id":"no-injected-module-loader","key":"python:no-injected-module-loader","languages":["python"],"limitations":["Flags defaults resolving to importlib.import_module, importlib.__import__, or builtins.__import__, including stable module-level import and single-assignment aliases and unshadowed __import__.","Every body reference to the parameter must be a direct call with one identical literal module name. Dynamic names, multiple module names, forwarding, returned/captured hooks, rebinding, unpacking and unused defaults are excluded.","Tests, test support, and generated code are excluded. Required loader parameters, application loaders, wrapper lambdas, alias chains, comprehensions, and imports local to enclosing scopes are not inferred.","Conservatively skips shadowed or rebound names, wildcard-import files, reassigned loader attributes, conflicting conditional or relative imports, and exception or pattern captures of importer roots, symbols or assignment aliases. Identical repeated importer imports remain valid. A relevant-name capture in an unrelated local scope can cause a false negative because capture checks are file-wide.","Assignment aliases require one module binding and precede use by line and column. Defaults use their enclosing evaluation scope: earlier statements in the nearest executing class body can shadow loaders; methods and nested classes do not close over class attributes. Enclosing function body bindings remain conservative exclusions, but a function's parameters do not shadow its own defaults. Relevant global declarations and walrus writes exclude sensitive importer names. Compound class statements can cause conservative omissions. Dynamic namespace mutation is not inferred. No autofix: changing arguments or import placement can alter API and initialization behavior."],"messageIds":[],"optionsSchema":null,"rationale":"When every direct use imports the same literal module, a generic importer can be unnecessary customization rather than a domain dependency. Dynamic plugin/import APIs legitimately need import hooks.","references":[],"remediation":"Consider owning the fixed import inside the implementation when import-hook substitution is unnecessary. Preserve intentional import hooks and plugin APIs; do not add wrapper factories or Protocols just to silence this advisory.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_injected_module_loader.py","status":"active","summary":"Avoid a generic module-loader parameter used only to import one fixed module.","test":"packages/python/tests/rules/test_no_injected_module_loader.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ481","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel\nclass Attachment(BaseModel):\n    url: str\nclass Record(BaseModel):\n    attachment: Attachment\ndef sign(record: Record):\n    attachment = record.attachment.model_copy(update={'url': 'signed'})\n    return record.model_copy(update={'attachment': attachment})\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"copy-changed-ancestors","outcome":"accept","scenarioId":"nested-copy","title":"Replace the nested value and its parent"},{"expectedCount":1,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel\nclass Record(BaseModel):\n    name: str\ndef rename(record: Record):\n    record.name = 'new'\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"input-field-write","outcome":"reject","scenarioId":"primary","title":"A transformation changes its caller's model"},{"expectedCount":0,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel\nclass Record(BaseModel):\n    name: str\ndef rename(record: Record):\n    return record.model_copy(update={'name': 'new'})\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"returned-replacement","outcome":"accept","scenarioId":"primary","title":"Return a trusted replacement"},{"expectedCount":1,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel\nclass Attachment(BaseModel):\n    url: str\nclass Record(BaseModel):\n    attachment: Attachment\ndef sign(record: Record):\n    copied = record.model_copy()\n    copied.attachment.url = 'signed'\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"shallow-model-shares-child","outcome":"reject","scenarioId":"nested-copy","title":"A shallow model copy shares a nested model"}],"filePatterns":[],"id":"no-input-model-mutation","key":"python:no-input-model-mutation","languages":["python"],"limitations":["Only source-proven Pydantic fields, typed model collections, stable local aliases, and recognized shallow copies are analyzed; application modules are never imported.","Mutation of ordinary instance receivers and fresh owned values is excluded. Before-validator raw inputs belong to SARJ482.","Ambiguous branch bindings, mixed-ownership literals, closures, opaque calls, custom copy hooks, dynamic update mappings, and deep-copy provenance are left unreported. Readonly collection interfaces do not imply immutable elements.","Recursive or deeply nested annotation traversal is bounded. Constructor argument aliases are not propagated through potentially custom validation.","Builtin list/dict/set mutators require a known container shape. Generated/vendor sources are excluded; maintained tests are checked. No automatic copying or validation rewrite is offered."],"messageIds":[],"optionsSchema":null,"rationale":"Assignments through shared model inputs also change every caller alias. Copying only the outer list or model retains nested aliases and can make repeated transformations observe each other's changes.","references":[],"remediation":"Return replacement models and explicitly capture the result. Copy each changed ancestor; permit fresh local builders and use exact suppressions for documented in-place APIs. model_copy updates must already satisfy the complete model contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_input_model_mutation.py","status":"active","summary":"Return replacement values instead of mutating caller-owned Pydantic models.","test":"packages/python/tests/rules/test_no_input_model_mutation.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ484","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tools/check.py","source":"import subprocess\nsubprocess.run(['python3', '-m', 'tools.verify', '-c', 'strict'], check=True)\n"}],"fixedFiles":[],"focusPath":"tools/check.py","id":"external-interpreter-module","outcome":"accept","scenarioId":"primary","title":"Process argv invokes a checked module"},{"expectedCount":0,"files":[{"path":"tools/check.py","source":"import subprocess\nsubprocess.run(['python3', '-m', 'tools.verify'], input='print(1)', text=True)\n"}],"fixedFiles":[],"focusPath":"tools/check.py","id":"external-module-stdin-data","outcome":"accept","scenarioId":"stdin","title":"An external module receives stdin as data"},{"expectedCount":1,"files":[{"path":"tools/check.py","source":"import subprocess\nsubprocess.run(['python3', '-c', 'print(1)'], check=True)\n"}],"fixedFiles":[],"focusPath":"tools/check.py","id":"interpreter-source-argument","outcome":"reject","scenarioId":"primary","title":"Process argv contains unchecked Python source"},{"expectedCount":1,"files":[{"path":"tools/check.py","source":"import subprocess\nsubprocess.run(['python3', '-'], input='print(1)', text=True)\n"}],"fixedFiles":[],"focusPath":"tools/check.py","id":"literal-python-stdin","outcome":"reject","scenarioId":"stdin","title":"Process stdin contains unchecked Python source"}],"filePatterns":[],"id":"no-interpreter-source-arguments","key":"python:no-interpreter-source-arguments","languages":["python"],"limitations":["Only import-proven subprocess process APIs, asyncio.create_subprocess_exec, and os exec/spawn argv APIs with literal string vectors are checked. Import aliases are followed; shadowed or reassigned imports and methods abstain.","Script/module and interpreter option boundaries use the shared dependency-free classifier, including env/sudo/timeout wrappers and jq/awk file flags.","Dynamic argv, shell strings and shell=True, recursive shell payloads, receiver-based remote execution, and unknown interpreter option grammars are outside this narrow rule. A clean result does not prove those programs are linted.","Literal str/bytes input to subprocess.run/check_output is checked only for proven Python stdin invocations and compatible constant text/binary modes. Dynamic input, Popen.communicate, and asyncio stdin abstain.","Generated/vendor sources are excluded; maintained tests are checked. Source-checker fixtures may use an exact reasoned suppression."],"messageIds":[],"optionsSchema":null,"rationale":"Moving deployment logic from YAML into Python still bypasses language checks when the Python wrapper passes program source through interpreter flags, jq/awk argv, or literal Python stdin.","references":[],"remediation":"Invoke a repository-owned file or module covered by the normal language checks; pass configuration as arguments rather than interpreter source.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_interpreter_source_arguments.py","status":"active","summary":"Keep interpreter programs in linted files or modules, outside process argv and literal Python stdin.","test":"packages/python/tests/rules/test_no_interpreter_source_arguments.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ448","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/routes.py","source":"def route(businessFunction: str):  # ruff: ignore[invalid-argument-name]\n    ...\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"camel-case-parameter-suppression","outcome":"reject","scenarioId":"primary","title":"A wire parameter disables Python naming"},{"expectedCount":0,"files":[{"path":"app/routes.py","source":"from fastapi import Query\ndef route(business_function: str = Query(alias='businessFunction')): ...\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"snake-case-wire-alias","outcome":"accept","scenarioId":"primary","title":"A framework alias preserves external spelling"}],"filePatterns":[],"id":"no-invalid-argument-name-suppression","key":"python:no-invalid-argument-name-suppression","languages":["python"],"limitations":["Generated source is excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Naming suppressions leak an external wire vocabulary into Python APIs and normalize future exceptions.","references":[],"remediation":"Keep the Python parameter snake_case and preserve the external spelling with FastAPI `Query`/`Path` aliases or Pydantic `Field` aliases.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_invalid_argument_name_suppression.py","status":"active","summary":"External parameter spelling must use framework aliases instead of disabling snake_case naming.","test":"packages/python/tests/rules/test_no_invalid_argument_name_suppression.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ511","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel\nclass Payload(BaseModel):\n    labels: list[str]\npayload = Payload.model_construct(labels=[3])\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"construct-invalid-list-member","outcome":"reject","scenarioId":"stored-literal-contract","title":"Unchecked list elements must satisfy the stored field type"},{"expectedCount":1,"files":[{"path":"app/models.py","source":"from typing import Literal\nfrom pydantic import BaseModel\nclass Job(BaseModel):\n    kind: Literal['queued', 'done']\njob = Job.model_construct(kind='unknown')\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"construct-invalid-tag","outcome":"reject","scenarioId":"primary","title":"Unchecked construction stores a tag outside the declared vocabulary"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel\nclass Payload(BaseModel):\n    labels: list[str]\npayload = Payload.model_construct(labels=['ready'])\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"construct-trusted-list-members","outcome":"accept","scenarioId":"stored-literal-contract","title":"Trusted list elements preserve the field contract"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from typing import Literal\nfrom pydantic import BaseModel\nclass Job(BaseModel):\n    kind: Literal['queued', 'done']\njob = Job.model_construct(kind='queued')\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"construct-trusted-valid-tag","outcome":"accept","scenarioId":"primary","title":"Trusted construction still satisfies the declared vocabulary"},{"expectedCount":1,"files":[{"path":"app/settings.py","source":"from typing import Literal\nfrom pydantic_settings import BaseSettings\nclass Settings(BaseSettings):\n    environment: Literal['dev', 'prod']\nsettings = Settings.model_construct(environment='other')\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"settings-construct-invalid-environment","outcome":"reject","scenarioId":"settings-validation-boundary","title":"A settings type does not validate unchecked construction"},{"expectedCount":0,"files":[{"path":"app/settings.py","source":"from typing import Literal\nfrom pydantic_settings import BaseSettings\nclass Settings(BaseSettings):\n    environment: Literal['dev', 'prod']\nsettings = Settings()\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"settings-validated-environment","outcome":"accept","scenarioId":"settings-validation-boundary","title":"Construct the validated settings object at startup"}],"filePatterns":[],"id":"no-invalid-pydantic-bypass","key":"python:no-invalid-pydantic-bypass","languages":["python"],"limitations":["Only plain, direct, module-level Pydantic BaseModel or Pydantic Settings BaseSettings subclasses with supported built-in and typing field annotations are inspected; hooks, inheritance, class decorators and custom methods are excluded.","Checks explicit None conflicts, closed string Literal domains, and bounded side-effect-free literal values contradicting builtin primitive or container field contracts. Coercible input is not assumed to be validated stored output.","Container literals preserve their actual construction semantics, including last-wins dictionary keys. Unpacking, calls, unhashable literal containers, opaque values, numeric Literal domains, bounds, aliases and custom field types are not inferred. Bool remains an int subtype, and numeric int-to-float/complex assignability is preserved.","model_construct must use explicit keywords; omitted required fields are checked only when no unpacking or positional argument can supply them. Literal Ellipsis and the source-proven unshadowed bare builtin Ellipsis are equivalent required-field defaults; imported, assigned and shadowed aliases remain opaque.","model_copy requires a fresh exact constructor or a stable unescaped local constructor binding, plus a plain literal update dictionary. Arbitrary annotated receivers and overridden methods are excluded.","Field aliases, non-passive model configuration, class or instance mutation and escape, ambiguous namespaces, tests and generated files are excluded. Source-proven ClassVar annotations remain class metadata through bounded transparent Annotated wrappers.","Settings support is limited to the same passive shared Pydantic configuration options as models. SettingsConfigDict calls, env/CLI/source options and custom settings sources remain excluded; no environment loading or settings validation is inferred from a bypass call.","Direct model field annotations exceeding a bounded depth or node budget exclude the file rather than risking recursive analysis failure. Literal analysis also bounds the value tree and combined string/bytes content to avoid excessive witness work.","Trusted valid construction and copy operations remain legal. No autofix chooses domain values or changes validation behavior."],"messageIds":[],"optionsSchema":null,"rationale":"model_construct and model_copy(update=...) store supplied values without validation. A contradictory literal or an omitted required construction field can therefore invalidate a model's declared type while the permissive bypass signatures still pass static checking.","references":[],"remediation":"Supply values satisfying the model contract and every required field, or validate a complete input mapping with the normal constructor or model_validate. A model_validate call on an existing model instance does not necessarily revalidate it; keep trusted bypasses only when their inputs preserve the contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_invalid_pydantic_bypass.py","status":"active","summary":"Do not create a provably invalid model through Pydantic validation bypass APIs.","test":"packages/python/tests/rules/test_no_invalid_pydantic_bypass.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ515","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, field_validator\nclass Payload(BaseModel):\n    text: int\n    @field_validator('text', mode='before')\n    @classmethod\n    def replace(cls, value: str) -> str:\n        return '3'\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"before-output-is-revalidated","outcome":"accept","scenarioId":"validation-phase","title":"Input conversion is followed by ordinary field validation"},{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, field_validator\nclass Payload(BaseModel):\n    text: str\n    @field_validator('text', mode='after')\n    @classmethod\n    def replace(cls, value: str) -> int:\n        return 3\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"invalid-after-output","outcome":"reject","scenarioId":"primary","title":"An after validator stores an incompatible primitive unchanged"},{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, field_validator\nclass Payload(BaseModel):\n    text: str\n    @field_validator('text', mode='plain')\n    @classmethod\n    def replace(cls, value: str) -> int:\n        return 3\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"plain-output-is-not-revalidated","outcome":"reject","scenarioId":"validation-phase","title":"A plain validator also bypasses subsequent field validation"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, field_validator\nclass Payload(BaseModel):\n    text: str\n    @field_validator('text', mode='after')\n    @classmethod\n    def replace(cls, value: str) -> str:\n        return 'ready'\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"valid-after-output","outcome":"accept","scenarioId":"primary","title":"The validator output satisfies the stored field contract"}],"filePatterns":[],"id":"no-invalid-pydantic-validator-return","key":"python:no-invalid-pydantic-validator-return","languages":["python"],"limitations":["Checks stable direct module-level BaseModel classes containing one synchronous field-validator method, plain annotated fields, and optional literal defaults. Inheritance, configuration, custom methods, field metadata, class decorators and escaped or mutated model classes are excluded.","The validator must have the canonical field_validator/classmethod stack, explicit literal field targets, mode after or plain (including omitted default after), two required positional parameters, and a sole primitive literal return, optionally after a docstring. Before, wrap, serializer, unknown and composed validator contracts abstain.","Only contradictions with builtin scalar types, None, canonical Optional/Union and closed string Literal domains are proved. Numeric assignability preserves bool as int and int as float/complex. Opaque aliases, numeric Literal domains, containers and annotation graphs exceeding fixed budgets abstain.","Every targeted field must exist; unknown-field diagnostics belong to their existing owner. Multiple validators are excluded because a later validator can repair an intermediate value. One diagnostic identifies all contradictory fields for the returned literal.","Generated and vendor files are excluded; authored tests remain checked. No autofix changes validation phase, chooses domain values or narrows a public contract."],"messageIds":[],"optionsSchema":null,"rationale":"Pydantic trusts the result of after and plain field validators without validating it against the field again. A correctly annotated validator can therefore store an integer in a string field while the model still promises a string to callers.","references":[],"remediation":"Return a value satisfying every targeted field's stored contract. Split validators for different field domains, or perform input conversion in a before validator so the normal field validation follows it. Preserve intentional validation chains and wire serializers.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_invalid_pydantic_validator_return.py","status":"active","summary":"A terminal field validator must not return a literal contradicting its stored field type.","test":"packages/python/tests/rules/test_no_invalid_pydantic_validator_return.py"},{"aliases":[],"autofix":"safe","category":"correctness","code":"SARJ508","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"from loguru import logger\ntry:\n    int(\"invalid\")\nexcept ValueError:\n    logger.opt(exception=True).warning(\"failed\")\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"loguru-exception-option","outcome":"accept","scenarioId":"primary","title":"Loguru captures the exception through opt"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"from loguru import logger\ntry:\n    int(\"invalid\")\nexcept ValueError:\n    logger.warning(\"failed\", exc_info=True)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"stdlib-argument-on-loguru","outcome":"reject","scenarioId":"primary","title":"A standard logging argument does not capture the exception"}],"filePatterns":[],"id":"no-loguru-exc-info","key":"python:no-loguru-exc-info","languages":["python"],"limitations":["Safe fixes handle only a direct proven Loguru logger call with literal arguments and keywords, replacing unused exc_info=True with opt(exception=True). Existing bind/opt chains, dynamic argument evaluation and commented calls remain review-only.","Only unambiguous Loguru imports and immediate bind/opt chains with literal exc_info=True are checked.","A literal message is required; templates using exc_info or the record, explicit exception capture, and exception() are excluded.","Unknown receivers, stored logger aliases, dynamic exc_info values, ambiguous or escaped imports, and generated files are excluded.","Visible namespace or logger mutations invalidate import proof. Interprocedural effects and runtime monkeypatches are not resolved.","An intentionally stored field can use a diagnostic-specific suppression. More complex logging context remains review-only."],"messageIds":[],"optionsSchema":null,"rationale":"Loguru accepts logging keyword arguments as structured fields. The standard logging `exc_info=True` argument therefore does not attach an exception, leaving failure logs without their traceback.","references":[],"remediation":"Use `logger.opt(exception=True)` or `logger.exception(...)` when reporting the active exception.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_loguru_exc_info.py","status":"active","summary":"Loguru exc_info=True adds a field rather than retaining the exception traceback.","test":"packages/python/tests/rules/test_no_loguru_exc_info.py"},{"aliases":[],"autofix":"safe","category":"correctness","code":"SARJ509","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"from loguru import logger\nlogger.error(\"Request {} failed with status {}\", \"request-marker\", 503)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"brace-values-rendered","outcome":"accept","scenarioId":"primary","title":"Brace fields render positional diagnostic values"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"from loguru import logger\nlogger.error(\"Request %s failed with status %d\", \"request-marker\", 503)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"printf-values-ignored","outcome":"reject","scenarioId":"primary","title":"Loguru leaves printf placeholders unchanged"}],"filePatterns":[],"id":"no-loguru-printf-format","key":"python:no-loguru-printf-format","languages":["python"],"limitations":["Safe fixes handle direct proven logger calls with a brace-free literal message, matching argument count and exact native primitive values. Only simple %s/%r/%a and integer-literal %d/%i fields are converted; widths, numeric coercion, mappings, custom values and logger chains remain review-only.","Only unambiguous module-level Loguru imports and immediate bind/opt chains with a literal message and supplied positional formatting arguments are checked.","The decoded literal message must contain a valid percent conversion and no brace fields; Unicode and hex escapes preserve conversions. Escaped percent signs alone, malformed templates, dynamic messages and expanded arguments are excluded.","Standard logging, unknown receivers, stored aliases, shadowed or escaped imports, custom wrapper chains, keyword-only fields and generated files are excluded.","Visible namespace or logger mutations invalidate import proof. Interprocedural effects and runtime monkeypatches are not resolved.","Argument types and printf arity are not generally inferred. Only the explicitly proven primitive subset is fixed; other findings require formatting/structured-field review."],"messageIds":[],"optionsSchema":null,"rationale":"Loguru formats messages with str.format rather than printf interpolation. A percent-style message with positional values keeps its placeholders and loses the values from the rendered diagnostic.","references":[],"remediation":"Use Loguru brace fields with equivalent formatting, or retain diagnostic values as named structured fields. Review numeric, mapping and width specifications rather than mechanically replacing percent signs.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_loguru_printf_format.py","status":"active","summary":"Use brace formatting for positional values passed to Loguru.","test":"packages/python/tests/rules/test_no_loguru_printf_format.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ570","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"PCM16_SCALE = 32_768\ndef normalize(sample):\n    return sample / PCM16_SCALE\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"declared-domain","outcome":"accept","scenarioId":"primary","title":"The contract is explicit"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"def normalize(sample):\n    return sample / 32768\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"undeclared-domain","outcome":"reject","scenarioId":"primary","title":"The contract is implicit"}],"filePatterns":[],"id":"no-magic-arithmetic","key":"python:no-magic-arithmetic","languages":["python"],"limitations":["Complements Ruff PLR2004, which owns comparisons. Reviews nontrivial numeric literals in runtime multiply/divide/modulo/power/shift operations, slices, builtin range calls and import-proven sleep/timeout calls.","Named uppercase constant definitions, function and field defaults, trivial -2 through 2, ordinary positional indexes, round precision, tests and generated sources are excluded. Floats other than the trivial values remain review candidates.","The rule does not infer an intended constant name, replace mathematically meaningful parameters automatically, or claim a correctness defect. Open scientific formulas can retain a reasoned local exception.","Numeric spelling is independently owned by numeric-literal-separators; arithmetic review never changes a value."],"messageIds":[],"optionsSchema":null,"rationale":"Unnamed conversion factors, frame sizes and timing limits hide units and can drift across related operations.","references":[],"remediation":"Extract a meaningfully named constant or use validated configuration. Reuse an existing constant when it represents the same unit or limit.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_magic_arithmetic.py","status":"active","summary":"Name nontrivial numeric factors and operational limits.","test":"packages/python/tests/rules/test_no_magic_arithmetic.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ479","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_profile.py","source":"from unittest.mock import Mock\nfrom pydantic import BaseModel\n\nclass Profile(BaseModel):\n    language: str\n\ndef test_profile():\n    profile = Mock(spec=Profile)\n    profile.language = 'ar'\n"}],"fixedFiles":[],"focusPath":"tests/test_profile.py","id":"mocked-model-field-data","outcome":"reject","scenarioId":"primary","title":"A mock bypasses the model's validated data contract"},{"expectedCount":0,"files":[{"path":"tests/test_profile.py","source":"from pydantic import BaseModel\n\nclass Profile(BaseModel):\n    language: str\n\ndef make_profile(*, language: str = 'en') -> Profile:\n    return Profile(language=language)\n\ndef test_profile():\n    profile = make_profile(language='ar')\n    assert profile.language == 'ar'\n"}],"fixedFiles":[],"focusPath":"tests/test_profile.py","id":"real-model-field-data","outcome":"accept","scenarioId":"primary","title":"Real model fixtures retain validation and defaults"}],"filePatterns":[],"id":"no-mocked-pydantic-value-object","key":"python:no-mocked-pydantic-value-object","languages":["python"],"limitations":["Only non-generated test paths and statically resolved unittest.mock constructors are analyzed.","The model's Pydantic ancestry and configured field data must be proven from source; application and SDK modules are never imported at runtime.","Model-method use, child callable invocation or behavior configuration, wrapped real instances, non-instance create_autospec calls, and doubles without configured model fields are excluded.","Unresolved model ancestry, ambiguous bindings, and indirect configuration beyond the supported provenance analysis are left unreported."],"messageIds":[],"optionsSchema":null,"rationale":"Mocking a Pydantic value object bypasses validation, defaults, serialization, and field relationships. A specced mock still permits test data that the real model could never produce. Real model fixtures keep those contracts active while allowing services and external boundaries to remain mocked.","references":[],"remediation":"Construct the real model, using a small typed factory for repeated valid defaults. Keep mock behavior at collaborator boundaries; a model method deliberately configured to raise remains a valid failure-injection test.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_mocked_pydantic_value_object.py","status":"active","summary":"Build real Pydantic models when test doubles supply model field data.","test":"packages/python/tests/rules/test_no_mocked_pydantic_value_object.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ520","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/counts.py","source":"def ordered(values: list[int]) -> list[int]:\n    values.sort()\n    return values\n"}],"fixedFiles":[],"focusPath":"app/counts.py","id":"sorts-borrowed-input","outcome":"reject","scenarioId":"primary","title":"Returning a list does not hide its mutation"},{"expectedCount":0,"files":[{"path":"app/counts.py","source":"def ordered(values: list[int]) -> list[int]:\n    result = list(values)\n    result.sort()\n    return result\n"}],"fixedFiles":[],"focusPath":"app/counts.py","id":"sorts-local-copy","outcome":"accept","scenarioId":"primary","title":"Build the replacement before changing it"}],"filePatterns":[],"id":"no-mutating-container-transform","key":"python:no-mutating-container-transform","languages":["python"],"limitations":["Checks undecorated functions and methods with a direct builtin list/dict/set/tuple/frozenset return annotation, typed primitive mutable builtin container inputs, and assignments, returns, raises and structured if branches. Terminal paths must return a value or raise, with at least one value return. Returned container kinds and element types do not change borrowed input ownership.","Direct unshadowed builtin staticmethod/classmethod and canonical typing.final markers preserve the method body in plain class namespaces. Custom, patched, inherited or metaclass-prepared namespaces and multiple binding markers remain excluded.","Wildcard providers, type-parameter names that shadow builtin annotations, and method annotations in inherited, decorated or metaclass-prepared class namespaces do not prove builtin primitive ownership.","Tracks direct aliases and conditional-expression aliases with one proven borrowed owner, item writes/deletions, known builtin mutators and supported in-place operators. Rebinding to a fresh local builder ends the borrowed root's provenance.","Canonical source-resolved operator item writes/deletions and supported in-place functions are checked with positional arity and builtin kind contracts. Patched, escaped or ambiguous operator providers and unknown functional helpers are excluded.","Direct unshadowed builtin list/dict/set mutation descriptors and explicit mutating special methods use the same borrowed-target contract. Unknown classes, read methods and fresh local targets remain excluded.","Source-proven typing and typing_extensions cast/assert_type calls preserve borrowed identity. Two positional arguments are supported; cast also accepts its native typ/val keyword forms. Positional-only assert_type, duplicate/unknown arguments and opaque expansions are not guessed. Copying before the helper still ends borrowed provenance.","Branches retain only origins shared by every continuing path; uncertain joins lose provenance. Proven literal truth selects reachable statement/expression branches and respects boolean short-circuiting; return/raise paths stop flow. Eager comprehensions isolate target bindings; their outermost iterable uses the enclosing scope.","Boolean value selections preserve aliases only when every reachable result has one proven borrowed owner. Unknown conditions that can return unrelated values lose provenance.","Comprehension operands follow generator/filter order. Empty literal iterables and literal-false filters prevent later operands and bodies from being visited; effects in earlier operands remain checked.","Native collection literals supply truth facts for empty literals, explicit members and directly empty expansions. Contents are still evaluated; unknown spreads and application truth methods remain unresolved.","Literal primitive equality/order and singleton identity comparisons supply branch facts. Comparison chains stop before later operands after a proven false link; unknown application comparisons are never evaluated.","Synchronous generator expressions passed as the sole argument of a direct unshadowed native list/tuple/set/frozenset constructor are eagerly consumed and share comprehension flow. Other generator expressions evaluate only their outer iterable at creation; their bodies, filters and later iterables remain deferred, including asynchronous generators.","Local annotated assignments visit their value and evaluated target operands, not deferred annotation expressions; an annotation without a value does not perform an item write.","Chained assignment and deletion targets execute in order. Exact tuple/list literal destructuring captures right-side origins before binding targets in order; opaque, starred and mismatched destructuring remain excluded.","More than 32 nested branches, loops, try/context-manager orchestration, closures, assignment expressions, opaque type aliases, unknown receivers and dynamic namespaces are excluded.","Model-valued inputs and framework validators retain their existing rule owners. Mutation of nested descendants, receiver state, I/O and scalar command/result APIs are outside this rule's supported contract.","Generated and vendored sources are excluded; maintained tests are checked. No automatic copying, deletion, or command rewrite is offered."],"messageIds":[],"optionsSchema":null,"rationale":"A container-valued transformation can change caller aliases before returning its result. Copying after the write leaves the original input changed.","references":[],"remediation":"Copy each changed container before writing, return a fresh replacement, or expose a separate command returning None. Keep documented in-place contracts explicit; do not discard the operation or add a dummy assignment.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_mutating_container_transform.py","status":"active","summary":"Return container replacements without mutating borrowed builtin inputs.","test":"packages/python/tests/rules/test_no_mutating_container_transform.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ555","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"api.py","source":"from attrs import define\n@define\nclass Request:\n    expected_version: int\ndef send_update(body: Request) -> Response:\n    body.expected_version = 2\n    return send(body)\n"}],"fixedFiles":[],"focusPath":"api.py","id":"attrs-request-mutated-before-response","outcome":"reject","scenarioId":"primary","title":"Sending a generated request changes the caller's request"},{"expectedCount":0,"files":[{"path":"api.py","source":"from attrs import define, evolve\n@define\nclass Request:\n    expected_version: int\ndef send_update(body: Request) -> Response:\n    updated = evolve(body, expected_version=2)\n    return send(updated)\n"}],"fixedFiles":[],"focusPath":"api.py","id":"attrs-request-replaced-before-response","outcome":"accept","scenarioId":"primary","title":"Send a replacement and keep the input unchanged"}],"filePatterns":[],"id":"no-mutating-input-with-result","key":"python:no-mutating-input-with-result","languages":["python"],"limitations":["Checks direct attribute assignments, augmented assignments and deletions through a parameter or an unambiguously assigned local alias in functions with an explicit non-None annotation and at least one value return. No class-name or model-library inference is needed.","Pydantic writes and before-validator writes keep their existing SARJ481/SARJ482 owners. Builtin-container item mutation remains with SARJ520; this rule does not duplicate subscript diagnostics.","None-returning commands, receiver state named self/cls, fresh copies, nested attributes, opaque mutator calls, generators, missing return annotations and unresolved or more than eight alias links are excluded.","Intentional mutation-and-result APIs may retain a reasoned exact suppression. Warnings require review; no copying, return-type change or lifecycle rewrite is automatic."],"messageIds":[],"optionsSchema":null,"rationale":"Returning an operation result can hide writes to a request object retained by the caller. Reusing that input then observes state changed by an earlier call.","references":[],"remediation":"Build and return a replacement input before sending it, or expose a deliberate in-place command returning None. Preserve response values and input contracts; do not discard the result to silence the rule.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_mutating_input_with_result.py","status":"active","summary":"Separate input attribute mutation from functions that return a result.","test":"packages/python/tests/rules/test_no_mutating_input_with_result.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ425","defaultLevel":"error","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":"Nested Config silently does not validate Settings.language"},{"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 or BaseSettings 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.","Intentional reusable validators with check_fields=False are excluded; Pydantic v1 validator syntax is supported.","Test and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Pydantic collects validator metadata from the model class namespace. A decorator indented into a nested class is therefore invisible to the outer model, so its 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":"Outer-model Pydantic field validator is owned by a nested helper class.","test":"packages/python/tests/rules/test_no_nested_pydantic_field_validator.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ469","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"def run(factory: Factory):\n    return factory()\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"concrete-factory","outcome":"accept","scenarioId":"primary","title":"Caller supplies a concrete dependency"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"def run(factory: Factory | None = None):\n    return (factory or DefaultFactory)()\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"nullable-factory","outcome":"reject","scenarioId":"primary","title":"Omission silently selects an implementation"}],"filePatterns":[],"id":"no-nullable-dependency-fallback","key":"python:no-nullable-dependency-fallback","languages":["python"],"limitations":["Warns for None-defaulted function parameters with a fallback used as a callable or a proven application-settings fallback; genuine absent state is allowed.","Constructor settings fallbacks remain owned by SARJ095; constructor nullability alone is not a defect. Decorated functions, tests, generated code, nested scope captures, non-callable object fallbacks, and interprocedural forwarding are excluded.","Callable use through a local assignment, annotation or None guard requires at most one body binding for that name and a call after the binding statement completes. Rebinding, deletion, imports, definitions, pattern or exception captures, global/nonlocal declarations and nested-scope bindings conservatively exclude that inference; direct inline calls remain independent. Settings provenance also excludes enclosing bindings and names written by a function declaring them global, while read-only global declarations remain valid.","No autofix: removing explicit None acceptance changes the callable contract, and moving a fallback can change initialization timing or error handling."],"messageIds":[],"optionsSchema":null,"rationale":"A None dependency silently selects another implementation or ambient configuration, hiding wiring mistakes and adding unnecessary branches.","references":[],"remediation":"Resolve the dependency at the call site and pass it explicitly; a concrete callable default is also valid when its lifetime is appropriate.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_nullable_dependency_fallback.py","status":"active","summary":"Require concrete function dependencies instead of resolving a None default inside the function.","test":"packages/python/tests/rules/test_no_nullable_dependency_fallback.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ025","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/task_store.py","source":"QUERY = 'SELECT id FROM task ORDER BY id ' + f'LIMIT 10 OFFSET {offset}'\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"concatenated-dynamic-offset","outcome":"reject","scenarioId":"concatenated-sql","title":"Concatenation retains a dynamic offset"},{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"QUERY = f'SELECT id FROM task LIMIT {limit} ' + 'OFFSET 2'\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"concatenated-static-offset","outcome":"accept","scenarioId":"concatenated-sql","title":"A dynamic limit does not make the offset dynamic"},{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"QUERY = \"SELECT id FROM task WHERE (created_at, id) < (:cursor_created_at, :cursor_id) ORDER BY created_at DESC, id DESC LIMIT :limit\"\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"keyset-page-query","outcome":"accept","scenarioId":"primary","title":"Page selected with a stable composite cursor"},{"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, f-strings, and their static concatenations in recognized store modules are analyzed.","Only dynamic OFFSET values in credible SELECT queries are reported; fixed ordinal offsets are excluded.","Parenthesized arithmetic and common numeric wrappers such as GREATEST, LEAST, and COALESCE preserve the dynamic driver marker. Arbitrary SQL functions and procedural values remain opaque.","Generated files, migrations, docstrings, comments, quoted identifiers, and BigQuery `WITH OFFSET AS` array indexing are excluded.","ORM pagination APIs are outside this embedded-SQL rule."],"messageIds":[],"optionsSchema":null,"rationale":"Dynamic offsets may require scanning discarded rows and may skip or repeat results during concurrent writes.","references":[],"remediation":"Use a stable, immutable, unique ordering and a cursor predicate with the same direction; include a unique tie-breaker for non-unique sort columns. Keep OFFSET with a reasoned suppression for demonstrably bounded or static data, or an intentional random-access page contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_offset_pagination.py","status":"active","summary":"Potentially unbounded `OFFSET` pagination in store SQL.","test":"packages/python/tests/rules/test_no_offset_pagination.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ512","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/guards.py","source":"from typing import Literal, TypeIs\ndef is_selected(value: str) -> TypeIs[Literal['red', 'blue']]:\n    return value == 'red'\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"declared-literal-rejected","outcome":"reject","scenarioId":"string-literal-domain","title":"Every declared literal belongs to the TypeIs target"},{"expectedCount":0,"files":[{"path":"app/guards.py","source":"from typing import TypeGuard\ndef is_positive(value: object) -> TypeGuard[int]:\n    return isinstance(value, int) and value > 0\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"partial-type-guard","outcome":"accept","scenarioId":"primary","title":"TypeGuard permits an intentional partial predicate"},{"expectedCount":1,"files":[{"path":"app/guards.py","source":"from typing import TypeIs\ndef is_positive(value: object) -> TypeIs[int]:\n    return isinstance(value, int) and value > 0\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"positive-is-not-all-integers","outcome":"reject","scenarioId":"primary","title":"Zero is still an integer in the false branch"},{"expectedCount":0,"files":[{"path":"app/guards.py","source":"from typing import Literal, TypeIs\ndef is_selected(value: str) -> TypeIs[Literal['red']]:\n    return value == 'red'\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"total-literal-equality","outcome":"accept","scenarioId":"string-literal-domain","title":"Equality recognizes every member of a singleton target"},{"expectedCount":0,"files":[{"path":"app/guards.py","source":"def is_truthy(value: object) -> bool:\n    return bool(value)\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"truthiness-bool-value-filter","outcome":"accept","scenarioId":"truthiness-value-filter","title":"A value filter can return an ordinary bool"},{"expectedCount":1,"files":[{"path":"app/guards.py","source":"from typing import TypeIs\ndef is_text(value: object) -> TypeIs[str]:\n    return bool(value)\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"truthiness-is-not-text-membership","outcome":"reject","scenarioId":"truthiness-value-filter","title":"Truthiness can accept integers and reject the empty string"}],"filePatterns":[],"id":"no-partial-type-is","key":"python:no-partial-type-is","languages":["python"],"limitations":["Checks undecorated, non-generic module functions with one parameter and one return, optionally after a docstring.","Requires an import-proven TypeIs target and a concrete builtin member demonstrably rejected by a direct predicate or by a builtin isinstance check followed by one predicate.","Targets are builtin int, str, bytes, bool and parameterized list, dict or set. Scalar comparisons use same-kind literals; length comparisons prove rejection of the valid empty member only.","Direct bool conversion rejects the valid falsey member. Unary not is checked only for scalar targets with an inhabited truthy member. Direct ordering or length comparisons require an exact matching builtin input annotation; equality comparisons use a concrete same-kind witness. Raw parameter returns are checked only when proven bool.","Also checks bounded, import-proven string Literal targets with exact builtin str or object input: a bool constant or equality/inequality comparison must reject an actual declared literal. Single-literal total equality predicates remain valid.","Input union analysis is iterative and bounded to 256 nodes. Constrained inputs, target unions, aliases, subclasses, custom refinements, helper calls, decorated functions and complex predicates are not inferred. TypeGuard may intentionally be partial and is excluded.","Visible shadowing, wildcard imports and namespace mutation abstain conservatively. There is no autofix because changing TypeIs affects both branches at callers.","Repeated top-level function definition names are excluded because overloads can expose a more constrained public input than the implementation annotation."],"messageIds":[],"optionsSchema":null,"rationale":"TypeIs narrows both branches. A positive-int or non-empty-container filter can return false for a valid member of its target type, making callers incorrectly exclude that type in the false branch.","references":[],"remediation":"Use an ordinary bool return for a value filter. Ensure every accepted value satisfies the declared target. Use TypeGuard for an intentional partial predicate only when its true branch is sound; TypeIs must recognize exactly the target values. Check false-branch callers when changing the narrowing contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_partial_type_is.py","status":"active","summary":"TypeIs must not reject a value belonging to its declared target type.","test":"packages/python/tests/rules/test_no_partial_type_is.py"},{"aliases":["require-validated-row-factory"],"autofix":"none","category":"correctness","code":"SARJ414","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/task_store.py","source":"import psycopg\n\nasync def load(dsn: str):\n    async with await psycopg.AsyncConnection.connect(dsn) as conn:\n        async with conn.cursor() as cursor:\n            await cursor.execute(\"SELECT id, state FROM task\")\n            return await cursor.fetchone()\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"escaping-positional-record","outcome":"reject","scenarioId":"primary","title":"A positional database record escapes unchanged"},{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"from psycopg import AsyncConnection\nfrom psycopg.rows import class_row\nfrom pydantic import BaseModel\n\nclass TaskRow(BaseModel):\n    id: str\n    state: str\n\nasync def load(conn: AsyncConnection[tuple[object, ...]]):\n    async with conn.cursor(row_factory=class_row(TaskRow)) as cursor:\n        await cursor.execute(\"SELECT id, state FROM task\")\n        return await cursor.fetchone()\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"named-record-factory","outcome":"accept","scenarioId":"primary","title":"A returned record has a named row contract"}],"filePatterns":[],"id":"no-positional-psycopg-row-escape","key":"python:no-positional-psycopg-row-escape","languages":["python"],"limitations":["Only import- or annotation-proven Psycopg cursors with a proven positional factory are inspected.","Direct fetch returns and unchanged local aliases are followed within one cursor context; iteration and helper-indirected flows are excluded.","Unknown, custom, inherited, or branch-dependent row factories are intentionally not inferred.","Test, generated, and migration files are excluded; exceptional record contracts require an exact suppression."],"messageIds":[],"optionsSchema":null,"rationale":"Returning a tuple-shaped database record couples callers to selected-column order. A named row factory keeps the database boundary explicit; scalar and locally transformed results remain valid.","references":[],"remediation":"For a returned record, use `row_factory=class_row(Model)` (prefer a validating model). Use `scalar_row` for scalar projections, or consume and transform a positional row inside the store.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_positional_psycopg_row_escape.py","status":"active","summary":"A positional Psycopg record must not escape its function unchanged.","test":"packages/python/tests/rules/test_no_positional_psycopg_row_escape.py"},{"aliases":["prefer-namedtuple-over-tuple-return"],"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    active: bool\n\ndef load_profile() -> Profile:\n    return Profile(name='Ada', age=42, active=True)\n"}],"fixedFiles":[],"focusPath":"app/profile.py","id":"named-public-return","outcome":"accept","scenarioId":"primary","title":"Public function returns a tuple-compatible named record"},{"expectedCount":1,"files":[{"path":"app/profile.py","source":"def load_profile() -> tuple[str, int, bool]:\n    return 'Ada', 42, True\n"}],"fixedFiles":[],"focusPath":"app/profile.py","id":"positional-public-return","outcome":"reject","scenarioId":"primary","title":"Public function returns a positional record"}],"filePatterns":[],"id":"no-positional-tuple-record","key":"python:no-positional-tuple-record","languages":["python"],"limitations":["Generated files and documentation examples are excluded.","Provenance-resolved builtin or typing fixed tuple return annotations with at least two slots are reported, including stable local aliases, quoted annotations, Annotated wrappers and optional unions in test helpers and fixtures; variadic tuples remain valid collections.","Existing direct tuple records remain errors; newly resolved typing_extensions providers, local aliases, application types named Unpack and optional-union records start as warnings for adoption review.","Classes with explicit bases, including object, are excluded without requiring an override decorator. Local base declarations do not prove runtime member ownership remains closed; class construction, native hooks and escaped handles can change inheritance. Independent module functions and plain classes remain eligible."],"messageIds":[],"optionsSchema":null,"rationale":"A fixed tuple return makes callers remember positions and lets adjacent values be silently swapped. Named records preserve field meaning across public, private, decorated, method, and test boundaries.","references":[],"remediation":"Return a frozen dataclass or validation model. Use `typing.NamedTuple` only when an exact tuple protocol is required.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_namedtuple_over_tuple_return.py","status":"active","summary":"Fixed tuple return records should use named fields instead of positional slots.","test":"packages/python/tests/rules/test_prefer_namedtuple_over_tuple_return.py"},{"aliases":["unused-mock-setup"],"autofix":"safe","category":"testing","code":"SARJ067","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_billing.py","source":"from unittest.mock import Mock\n\ndef test_charge():\n    charge = Mock()\n    charge.return_value = 1\n    assert charge() == 1\n    charge.return_value = 2\n    assert charge() == 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":"from unittest.mock import Mock\n\ndef test_charge():\n    charge = Mock()\n    charge.return_value = 1\n    charge.return_value = 2\n    assert charge() == 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":"no-provably-dead-mock-configuration","key":"python:no-provably-dead-mock-configuration","languages":["python"],"limitations":["Safe fixes remove only proven inert standalone configuration assignments; proven inert constructor keywords are removed while preserving the constructor, binding and annotation. Commented and annotated configuration writes remain review-only.","Only non-generated test paths and stable local names unconditionally initialized from unittest.mock or an unshadowed pytest-mock fixture before configuration are analyzed. Dotted stores, auto-children, configured return-value chains, replacement or unpacked options, dynamic namespace access, and direct constructor-root or builtin namespace mutation are excluded. Unseen external mutations are not resolved.","Only same-block overwrites and exact-path terminal never-called assertions are checked; uncertain evaluation, native setter/iteration effects, custom hash/equality, control flow, reads, and terminal receiver escapes are excluded. Configuration values require literal/native primitive provenance."],"messageIds":[],"optionsSchema":null,"rationale":"A mock behavior value is dead when a same-path write replaces it before observation, or when a terminal exact-path assertion proves the configured mock was never called.","references":[],"remediation":"Delete the earlier inert configuration, retaining the mock constructor and binding when removing a keyword, or place the intended action before reconfiguration; delete behavior configuration contradicted by a terminal never-called assertion.","since":null,"source":"packages/python/src/sarj_python_lint/rules/unused_mock_setup.py","status":"active","summary":"Remove mock behavior configuration proven unable to affect a test.","test":"packages/python/tests/rules/test_unused_mock_setup.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ560","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/store.py","source":"from psycopg import AsyncConnection\nasync def remove(connection: AsyncConnection, rows):\n    for row in rows:\n        await connection.execute('DELETE FROM item WHERE id = %s', (row.id,))\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"per-item-write","outcome":"reject","scenarioId":"primary","title":"A connection executes once per item"},{"expectedCount":0,"files":[{"path":"app/store.py","source":"from psycopg import AsyncConnection\nasync def remove(connection: AsyncConnection, rows):\n    identifiers = [row.id for row in rows]\n    await connection.execute('DELETE FROM item WHERE id = ANY(%s)', (identifiers,))\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"set-based-write","outcome":"accept","scenarioId":"primary","title":"One query consumes the collected identifiers"}],"filePatterns":[],"id":"no-psycopg-execution-in-loop","key":"python:no-psycopg-execution-in-loop","languages":["python"],"limitations":["Checks import-proven Psycopg execute calls in for/async-for bodies and repeated comprehension expressions, including injected pools, connections and cursors. Explicit executemany batch loops remain valid for bounded chunking.","In-memory preparation, one-shot iteration inputs, known zero/single-item loops, deferred nested functions, unrelated execute methods and generated code are excluded. While retry/stream loops and interprocedural helper execution are not inferred.","SARJ518 owns its narrower invariant-write cases, so one location receives only that specific executemany diagnostic. Unknown SQL and result-consuming loops still require bulk-query review; no automatic rewrite changes database semantics."],"messageIds":[],"optionsSchema":null,"rationale":"Executing a database operation for each input item makes round trips scale with collection size, including loops that prepare each row before executing.","references":[],"remediation":"Collect inputs and use a bounded set-based query, typed bulk store operation or executemany batch. Preserve tenant checks, revision predicates, transaction boundaries, returned rows, ordering and deliberate sequential effects.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_psycopg_execution_in_loop.py","status":"active","summary":"Use bounded bulk queries instead of executing Psycopg operations inside item loops.","test":"packages/python/tests/rules/test_no_psycopg_execution_in_loop.py"},{"aliases":["sql-requires-injected-pool-owner"],"autofix":"none","category":"architecture","code":"SARJ415","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/retry_rows.py","source":"from psycopg_pool import AsyncConnectionPool\n\nasync def mark(pool: AsyncConnectionPool):\n    async with pool.connection() as conn:\n        await conn.execute('UPDATE call SET retry = true')\n"}],"fixedFiles":[],"focusPath":"app/retry_rows.py","id":"free-psycopg-helper","outcome":"reject","scenarioId":"primary","title":"Do not execute persistence operations in a free helper"},{"expectedCount":0,"files":[{"path":"app/store.py","source":"from psycopg_pool import AsyncConnectionPool\n\nclass Store:\n    def __init__(self, pool: AsyncConnectionPool):\n        self.pool = pool\n\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-psycopg","outcome":"accept","scenarioId":"primary","title":"Execute through an injected persistence owner"}],"filePatterns":[],"id":"no-psycopg-execution-outside-injected-owner","key":"python:no-psycopg-execution-outside-injected-owner","languages":["python"],"limitations":["Only import-proven psycopg connection/cursor and psycopg_pool ConnectionPool or AsyncConnectionPool flows are classified. A free helper receiving an imported Cursor or AsyncCursor retains external execution ownership.","Straightforward constructor injection, connection and cursor context managers, aliases, rebindings, and conservative control-flow joins are followed. Mutated module namespaces and class-local annotation shadows abstain. Cursor parameters of class methods have unknown interprocedural ownership and do not establish an external execution origin.","Collected test modules, conftest.py, migrations, generated files, conventional operational-script roots, and literal SELECT 1 probes are excluded. Shared test-support helpers must use an injected owner."],"messageIds":[],"optionsSchema":null,"rationale":"Free helpers and locally created clients can split query and transaction ownership across unrelated call sites, making persistence behavior harder to change consistently.","references":[],"remediation":"Move the operation behind a store, repository, or narrow transaction owner that receives its Psycopg pool or connection through the constructor. Use an exact SARJ415 suppression when direct execution is the deliberate behavior under test.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_psycopg_execution_outside_injected_owner.py","status":"active","summary":"Psycopg execution occurs outside a constructor-injected persistence owner.","test":"packages/python/tests/rules/test_no_psycopg_execution_outside_injected_owner.py"},{"aliases":["no-gen-random-uuid-in-sql"],"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-random-uuid-in-sql","key":"python:no-random-uuid-in-sql","languages":["python"],"limitations":["Detection covers SQL-shaped Python string literals after masking SQL comments and quoted values.","PostgreSQL gen_random_uuid() and uuid-ossp uuid_generate_v4() are covered.","Known UUIDv7 compatibility functions and calls nested inside UUIDv7 bit-building helpers are excluded per occurrence."],"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_random_uuid_in_sql.py","status":"active","summary":"Embedded SQL generates a random UUIDv4 instead of a time-ordered UUIDv7.","test":"packages/python/tests/rules/test_no_random_uuid_in_sql.py"},{"aliases":["no-raw-sql-in-tests"],"autofix":"none","category":"testing","code":"SARJ429","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/fixtures/probe.py","source":"from psycopg_pool import AsyncConnectionPool\n\nclass Probe:\n    def __init__(self, pool: AsyncConnectionPool):\n        self._pool = pool\n\n    async def seed(self):\n        async with self._pool.connection() as conn:\n            await conn.execute('INSERT INTO jobs (id) VALUES (%s)', (job_id,))\n"}],"fixedFiles":[],"focusPath":"tests/fixtures/probe.py","id":"test-probe-executes-sql","outcome":"reject","scenarioId":"primary","title":"An injected test probe still bypasses the owning store"},{"expectedCount":0,"files":[{"path":"tests/test_orders.py","source":"async def load_rows(store: OrderStore):\n    return await store.list_orders()\n"}],"fixedFiles":[],"focusPath":"tests/test_orders.py","id":"test-uses-store-boundary","outcome":"accept","scenarioId":"primary","title":"A test reads through the owning store API"}],"filePatterns":[],"id":"no-raw-connection-in-tests","key":"python:no-raw-connection-in-tests","languages":["python"],"limitations":["Raw SQL execution is checked in tests, conftest.py, shared test-support helpers and migration tests; generated code is excluded. Standalone SQL examples and store/service calls are not execution.","Import-proven Psycopg connection/pool flows, including constructor-injected members, aliases and dynamic query arguments, are followed. SQL-shaped literals in execute/query/fetch-like calls are also checked across drivers, including concatenation, f-strings and import-proven SQL/text constructors.","Opaque non-Psycopg builders and interprocedural receiver flows are not inferred. Raw connection acquisition keeps its existing fixture/support exclusions when no SQL execution is found in that scope."],"messageIds":[],"optionsSchema":null,"rationale":"Tests that reach through a pool couple assertions and setup to persistence internals, bypass the application boundary, and duplicate transaction ownership.","references":[],"remediation":"Exercise the owning store/service API. Do not move SQL into a test-only probe to hide it. Use an exact SARJ429 suppression for deliberate database bootstrap, migration or transaction fault testing.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_raw_connection_in_tests.py","status":"active","summary":"Do not execute raw SQL or acquire raw database connections in tests.","test":"packages/python/tests/rules/test_no_raw_connection_in_tests.py"},{"aliases":["source-coupled-test"],"autofix":"none","category":"testing","code":"SARJ402","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_policy.py","source":"__test__ = False\nfrom pathlib import Path\ndef test_policy():\n    source = Path('workflow.yml').read_text()\n    assert 'permissions:' in source\n"}],"fixedFiles":[],"focusPath":"tests/test_policy.py","id":"disabled-module-source-oracle","outcome":"accept","scenarioId":"module-collection","title":"An explicitly disabled module has no collected source oracle"},{"expectedCount":1,"files":[{"path":"tests/test_policy.py","source":"__test__ = True\nfrom pathlib import Path\ndef test_policy():\n    source = Path('workflow.yml').read_text()\n    assert 'permissions:' in source\n"}],"fixedFiles":[],"focusPath":"tests/test_policy.py","id":"enabled-module-source-oracle","outcome":"reject","scenarioId":"module-collection","title":"An enabled module still needs a parsed source contract"},{"expectedCount":0,"files":[{"path":"tests/test_policy.py","source":"from pathlib import Path\n\nimport yaml\n\ndef test_policy():\n    workflow = yaml.safe_load(Path('workflow.yml').read_text())\n    assert workflow['permissions']['contents'] == 'read'\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":"from pathlib import Path\n\ndef 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":"no-raw-source-text-test-oracle","key":"python:no-raw-source-text-test-oracle","languages":["python"],"limitations":["A literal module-level __test__ = False disables pytest collection. Later bindings in eager module expressions or explicitly global class scopes clear that proof; class-local names, deferred bodies, and annotation-only names do not rebind it.","The rule recognizes source-like suffixes, shell templates ending in .sh.tftpl, and the exact Dockerfile basename; it follows local aliases, context-managed reads, and common normalization.","Direct generator/list-comprehension reads unpacked from stable module-level path tuples are followed; mutable collections, rebinding, filtered comprehensions, and interprocedural flows remain unreported.","Literal shebang startswith checks are representation contracts and remain unreported; other text compatibility sentinels require an exact suppression.","Files produced beneath recognized temporary-directory fixtures are generated artifacts, not repository source, and remain unreported.","Paths beneath fixture, golden, and snapshot directories are treated as deliberate representation contracts 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.","Lambda bodies and unconsumed generators are separate scopes. Proven builtin all/any consumption follows generator and comprehension bindings; pattern captures clear prior source provenance. Empty literal iterations and literal false filters are not treated as source oracles.","Exact byte-copy checks against proven temporary outputs, and same-path byte preservation after a visible named write_bytes value, are representation contracts. Local value/path rebinding, intervening path writes, and conditional writes invalidate that proof; equality to fixed repository-source bytes still requires an exact suppression."],"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. If exact bytes or text are the deliberate compatibility contract, use an exact line suppression with the reason.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_raw_source_text_test_oracle.py","status":"active","summary":"Test uses raw text from a source-like project path as its oracle.","test":"packages/python/tests/rules/test_no_raw_source_text_test_oracle.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ544","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/fakes/store.py","source":"class Store:\n    def get(self, key): ...\n    def upsert(self, item): ...\nclass Recording(Store):\n    def __init__(self):\n        self.writes = []\n    def upsert(self, item):\n        self.writes.append(item)\n        return super().upsert(item)\n"}],"fixedFiles":[],"focusPath":"tests/fakes/store.py","id":"delegating-spy","outcome":"accept","scenarioId":"primary","title":"A spy retains actual persistence"},{"expectedCount":1,"files":[{"path":"tests/fakes/store.py","source":"class Store:\n    def get(self, key): ...\n    def upsert(self, item): ...\nclass Recording(Store):\n    def __init__(self):\n        self.writes = []\n    def upsert(self, item):\n        self.writes.append(item)\n        return item\n"}],"fixedFiles":[],"focusPath":"tests/fakes/store.py","id":"recording-persistence","outcome":"reject","scenarioId":"primary","title":"Recording replaces persistence"}],"filePatterns":[],"id":"no-recording-store","key":"python:no-recording-store","languages":["python"],"limitations":["Checks test sources for a subclass of a source-visible persistence contract declaring get and upsert. The concrete override must record to a stable instance list initialized directly in its constructor and return a synthetic result in straight-line instance code, without unknown calls, writes, await or self-method delegation.","Unknown free-function calls retain possible persistence effects and abstain. Source-proven Pydantic models without custom methods or field calls, plain dataclass constructors, native NewType wrappers and unshadowed builtin value conversions are recognized as value construction for this advice; native validation is not claimed to be runtime-pure. Mutated or escaped constructor providers and shadowed constructor calls abstain.","Class names do not trigger the rule. Imported bases need the prepared project source graph. Unknown bases, SQL-backed stores, delegated spies, unrelated call recorders, dynamic collections and unsupported inheritance remain outside scope. One warning is emitted per recording implementation, with no automatic rewrite."],"messageIds":[],"optionsSchema":null,"rationale":"An in-memory store double proves that a method was called but bypasses SQL, constraints, transactions and concurrency behavior. It can accept operations the real store rejects.","references":[],"remediation":"Inject the real PostgreSQL store with an isolated database fixture and assert persisted state. Keep operational observability around real persistence rather than replacing persistence with an appended call list.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_recording_store.py","status":"active","summary":"Use the PostgreSQL store in tests instead of a recording-only store implementation.","test":"packages/python/tests/rules/test_no_recording_store.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ554","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_save.py","source":"def test_save(seeded):\n    assert seeded.scenario.id == seeded.current.id\n"}],"fixedFiles":[],"focusPath":"tests/test_save.py","id":"direct-attribute-use","outcome":"accept","scenarioId":"primary","title":"Read names on their owner"},{"expectedCount":1,"files":[{"path":"tests/test_save.py","source":"def test_save(seeded):\n    scenario, current = seeded.scenario, seeded.current\n    assert scenario.id == current.id\n"}],"fixedFiles":[],"focusPath":"tests/test_save.py","id":"repeated-attribute-names","outcome":"reject","scenarioId":"primary","title":"Keep the named result aggregate"}],"filePatterns":[],"id":"no-redundant-attribute-unpacking","key":"python:no-redundant-attribute-unpacking","languages":["python"],"limitations":["Reports unconditional tuple/list assignments directly in function bodies only when every element aliases an identically named attribute of the same simple receiver and no target rebinds that receiver.","Conditional/loop/context-manager aliases can normalize branch results or preserve resource scopes and are excluded. Renamed attributes, mixed receivers, nested unpacking, calls, module/class bindings, generated source and malformed source are excluded.","This is an explicitly requested warning-level style preference. A retained snapshot can be meaningful when the aggregate changes; no autofix or claim of behavioral equivalence is made."],"messageIds":[],"optionsSchema":null,"rationale":"A tuple assignment that repeats every attribute name adds parallel bindings without describing a transformation or a new concept.","references":[],"remediation":"Read the aggregate's named attributes directly. Retain intentional value snapshots with an explained local suppression.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_redundant_attribute_unpacking.py","status":"active","summary":"Use an aggregate's named attributes instead of repeating their names in tuple aliases.","test":"packages/python/tests/rules/test_no_redundant_attribute_unpacking.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ531","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"case.py","source":"def render(enabled):\n    if not enabled:\n        return None\n    item = make_item()\n    payload = item\n    return build(payload=payload)\n"}],"fixedFiles":[],"focusPath":"case.py","id":"completed-guard-prefix","outcome":"reject","scenarioId":"completed-guard","title":"Initialize the original after the completed guard"},{"expectedCount":0,"files":[{"path":"case.py","source":"def render(enabled):\n    if not enabled:\n        return None\n    item = make_item()\n    return build(payload=item)\n"}],"fixedFiles":[],"focusPath":"case.py","id":"direct-after-guard","outcome":"accept","scenarioId":"completed-guard","title":"Pass the fresh original directly"},{"expectedCount":0,"files":[{"path":"case.py","source":"def render(item):\n    return dict(payload=item)\n"}],"fixedFiles":[],"focusPath":"case.py","id":"direct-builtin-tail","outcome":"accept","scenarioId":"safe-fix","title":"Return the original local in the mapping"},{"expectedCount":0,"files":[{"path":"case.py","source":"def render(item):\n    def _record(value):\n        return dict(payload=value)\n    return _record(item)\n"}],"fixedFiles":[],"focusPath":"case.py","id":"direct-closed-dict-value","outcome":"accept","scenarioId":"closed-dict-identity","title":"The same keyword receives the original parameter"},{"expectedCount":0,"files":[{"path":"case.py","source":"def render(item):\n    consume(payload=item)\n"}],"fixedFiles":[],"focusPath":"case.py","id":"direct-keyword","outcome":"accept","scenarioId":"primary","title":"Pass the original local"},{"expectedCount":1,"files":[{"path":"case.py","source":"def render(item):\n    payload = item\n    consume(payload=payload)\n"}],"fixedFiles":[],"focusPath":"case.py","id":"keyword-supplies-role","outcome":"reject","scenarioId":"primary","title":"The keyword supplies the role"},{"expectedCount":1,"files":[{"path":"case.py","source":"def render(item):\n    payload = item\n    return dict(payload=payload)\n"}],"fixedFiles":[],"focusPath":"case.py","id":"safe-builtin-tail","outcome":"reject","scenarioId":"safe-fix","title":"A safe builtin mapping return"},{"expectedCount":1,"files":[{"path":"case.py","source":"def render(item):\n    def _record(value):\n        payload = value\n        return dict(payload=payload)\n    return _record(item)\n"}],"fixedFiles":[{"path":"case.py","source":"def render(item):\n    def _record(value):\n        return dict(payload=value)\n    return _record(item)\n"}],"focusPath":"case.py","id":"safe-closed-dict-alias","outcome":"reject","scenarioId":"closed-dict-identity","title":"A closed private dict wrapper preserves the original value"}],"filePatterns":[],"id":"no-redundant-local-alias","key":"python:no-redundant-local-alias","languages":["python"],"limitations":["Only unannotated, single-target `alias = original` assignments with one binding and one read at `alias=alias` are checked.","The original must be a parameter or an earlier initialized local with one binding; reassignment, deletion, imports, alias chains and uncertain initialization are excluded.","Completed function-body if prefixes and their Boolean tests are supported only before a fresh, uniquely initialized local and alias in that same body. Parameters, earlier source bindings and aliases nested in with bodies after such prefixes are excluded.","Loops, try statements, asserts, other Boolean short-circuiting, nested scopes, global/nonlocal declarations and type aliases are excluded. Ordinary and async with bodies require ordered enclosing-suite binding evidence.","A conditional expression is admitted only when its test is a bare name bound once as a parameter, and neither local is read within the expression.","Typed aliases, files with type comments, same-scope quoted references and conditional reads of either local are excluded. Captured bindings and module/class aliases are preserved.","Aliases read inside template interpolations are preserved because templates expose expression text. This conservatively also excludes reads used only in format specifications.","Files with wildcard imports, Pydantic imports or references to known namespace readers, dynamic attribute getters or reference-count observers are excluded. Wildcard exports cannot establish builtin identity; computed type names can consume caller-local bindings without a Name read in the AST.","Dynamic class construction and class-valued references to type are excluded. Direct, unshadowed unary type(value) queries and standard-library dataclasses remain supported.","Generated source, stubs and malformed input are excluded; files must compile with normal module execution flags. Arbitrary external frame introspection is not modeled.","Native diagnostic fixes are limited to undecorated module functions with one required parameter, an optional existing docstring, a clean single-line alias and a tail return of unshadowed builtin dict with exactly the matching keyword. Recorded dict bindings, visible attribute/subscript writes, setter, mapping-mutator, ior, methodcaller or patch references, unittest imports, `|=` assignments, comments, non-UTF8 encodings, type parameters and visible function, frame or coroutine code-object inspection prevent fixes. Mutation guards also exclude unrelated ordinary mappings, nonmutating methodcaller uses, harmless unittest imports and integer `|=` assignments. Other calls remain diagnostic-only; arbitrary external tracing, reflection and monkeypatching are not modeled.","The central fixer also retains its separate closed nested private one-parameter helper profile: the whole body forwards one unannotated alias into a returned canonical builtin dict. Its closed-call, comment, annotation, escape, builtin and metadata-observation guards remain independent of native diagnostic fixes; a finding is edited through only one validated path.","Readability judgments, multiple reads, positional forwarding and more complex control flow remain outside the contract."],"messageIds":[],"optionsSchema":null,"rationale":"An extra local binding makes readers trace two names for one value when the keyword already communicates its role.","references":[],"remediation":"Apply the safe fix where offered; otherwise pass the original local to the matching keyword and remove the unannotated alias after review.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_redundant_local_alias.py","status":"active","summary":"Remove a local alias used only for its matching keyword argument.","test":"packages/python/tests/rules/test_no_redundant_local_alias.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ440","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"legacy/settings.py","source":"import sys\n\nfrom canonical import settings as _canonical\n\n\nsys.modules[__name__] = _canonical\n"}],"fixedFiles":[],"focusPath":"legacy/settings.py","id":"current-module-replacement","outcome":"reject","scenarioId":"module-replacement","title":"Do not replace a compatibility module at runtime"},{"expectedCount":0,"files":[{"path":"legacy/settings.py","source":"from canonical.settings import (\n    Settings as Settings,\n    load as load,\n)\n"}],"fixedFiles":[],"focusPath":"legacy/settings.py","id":"explicit-compatibility-exports","outcome":"accept","scenarioId":"module-replacement","title":"Expose the supported compatibility surface explicitly"},{"expectedCount":1,"files":[{"path":"app/pagination.py","source":"def _encode_cursor(value: str) -> str:\n    return value\n\n\nencode_phone_number_cursor = (_encode_cursor)\n"}],"fixedFiles":[],"focusPath":"app/pagination.py","id":"native-parenthesized-alias-matching","outcome":"reject","scenarioId":"native-parenthesized-alias","title":"Native syntax preserves the existing ownership contract"},{"expectedCount":0,"files":[{"path":"app/pagination.py","source":"__version__ = \"value\"\npublic_version = (__version__)\n"}],"fixedFiles":[],"focusPath":"app/pagination.py","id":"native-parenthesized-alias-nonmatching","outcome":"accept","scenarioId":"native-parenthesized-alias","title":"Existing semantic ownership exclusions remain valid"},{"expectedCount":1,"files":[{"path":"pagination.py","source":"def _encode_cursor(value: str) -> str:\n    return value\n\n\nencode_phone_number_cursor = _encode_cursor\n"}],"fixedFiles":[],"focusPath":"pagination.py","id":"public-alias-for-private-helper","outcome":"reject","scenarioId":"public-private-alias","title":"Define a public helper under its public name"},{"expectedCount":0,"files":[{"path":"pagination.py","source":"def encode_phone_number_cursor(value: str) -> str:\n    return value\n"}],"fixedFiles":[],"focusPath":"pagination.py","id":"public-helper-definition","outcome":"accept","scenarioId":"public-private-alias","title":"Give the implementation its intended public name"}],"filePatterns":[],"id":"no-redundant-module-alias-exports","key":"python:no-redundant-module-alias-exports","languages":["python"],"limitations":["Public-to-private name aliases are checked only as direct module-body assignments; assignments through an unconditional, unshadowed stdlib `sys` import include module-level control flow.","Aliases inside module control flow, functions, classes, and `TYPE_CHECKING` branches are excluded.","Private targets, dunder sources, attribute values, destructuring, imports, and same-visibility aliases are excluded.","Literal-key registrations, child-module registrations, method calls, reads, and arbitrary registries are excluded.","Generated source, malformed source, and stub files (`.pyi`) are outside this rule's scope.","No autofix is offered because the intended compatibility surface and identity requirements need review."],"messageIds":[],"optionsSchema":null,"rationale":"Binding a public name to a private implementation creates two names for one API and disguises the intended visibility boundary. Replacing the current module similarly mutates import identity at runtime in a way static tools cannot model. Both forms make navigation, introspection, and compatibility ownership surprising.","references":[],"remediation":"Define maintained APIs under their public names and update local callers directly. Import the canonical path inside maintained code. If a compatibility module is still required, expose its supported names with explicit same-name imports grouped by source module. Preserve a required compatibility or module-identity contract with a reasoned exact suppression rather than renaming automatically.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_redundant_module_alias_exports.py","status":"active","summary":"Do not manufacture public APIs from private names or replace the current module.","test":"packages/python/tests/rules/test_no_redundant_module_alias_exports.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ543","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/call_store.py","source":"from psycopg.sql import SQL\nQUERY = SQL(\"SELECT call.id FROM call WHERE id = %s\")\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"direct-names","outcome":"accept","scenarioId":"primary","title":"Use the existing relation and column names"},{"expectedCount":0,"files":[{"path":"app/literal_rows.py","source":"from psycopg import Connection\ndef literal_rows():\n    with Connection.connect() as connection:\n        return connection.execute('SELECT \"r\".\"id\" FROM (VALUES (1)) AS \"r\"(\"id\")').fetchall()\n"}],"fixedFiles":[],"focusPath":"app/literal_rows.py","id":"fixed-owned-literal","outcome":"accept","scenarioId":"owned-literal-self-alias","title":"A closed literal column keeps its implicit output name"},{"expectedCount":1,"files":[{"path":"app/literal_rows.py","source":"from psycopg import Connection\ndef literal_rows():\n    with Connection.connect() as connection:\n        return connection.execute('SELECT \"r\".\"id\" AS \"id\" FROM (VALUES (1)) AS \"r\"(\"id\")').fetchall()\n"}],"fixedFiles":[{"path":"app/literal_rows.py","source":"from psycopg import Connection\ndef literal_rows():\n    with Connection.connect() as connection:\n        return connection.execute('SELECT \"r\".\"id\" FROM (VALUES (1)) AS \"r\"(\"id\")').fetchall()\n"}],"focusPath":"app/literal_rows.py","id":"safe-autofix","outcome":"reject","scenarioId":"owned-literal-self-alias","title":"Remove a self-alias from a closed literal result column"},{"expectedCount":1,"files":[{"path":"app/call_store.py","source":"from psycopg.sql import SQL\nQUERY = SQL(\"SELECT call.id AS id FROM call AS call WHERE id = %s\")\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"self-aliases","outcome":"reject","scenarioId":"primary","title":"Aliases repeat the existing names"}],"filePatterns":[],"id":"no-redundant-sql-alias","key":"python:no-redundant-sql-alias","languages":["python"],"limitations":["Checks parsed PostgreSQL queries in proven psycopg SQL constructors and execution contexts. Generated sources, comments, malformed statements, opaque dynamic SQL and unrelated strings are excluded.","Only table self-aliases without alias column lists and bare-column output self-aliases are checked. Identifier folding follows PostgreSQL UTF-8 ASCII case rules. Casts, computed expressions, CTE aliases, different role names and quoted identifier case retain their contract.","Autofix only removes qualified bare-column self-aliases over one to eight equally wide VALUES rows of unsigned integer literals with explicit known unique labels, directly returned through a fresh default Psycopg Connection in a closed source-owned function. Already unaliased qualified columns remain unchanged. Table aliases, external relations, comments, configured connections, query composition and other consumers remain manual. Original token spelling, output names, values, row order and SQL clauses are preserved.","The finding is emitted once per SQL expression even when it contains several redundant aliases. It does not infer that different relation names belong to the same model."],"messageIds":[],"optionsSchema":null,"rationale":"A self-alias adds syntax without naming a distinct role or result field. It obscures useful aliases among repeated names.","references":[],"remediation":"Remove an alias whose PostgreSQL identifier equals the original table or bare column name. Keep role aliases for self joins, renamed outputs, quoted case distinctions and alias column lists.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_redundant_sql_alias.py","status":"active","summary":"Remove SQL aliases that repeat the original relation or column name.","test":"packages/python/tests/rules/test_no_redundant_sql_alias.py"},{"aliases":["no-repeated-string-literal"],"autofix":"safe","category":"maintainability","code":"SARJ024","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"queries.py","source":"def load(cursor):\n    cursor.execute(\"SELECT id, name, created_at FROM organization\")\n\ndef refresh(cursor):\n    cursor.execute(\"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":"_SELECT_ORGANIZATIONS = \"SELECT id, name, created_at FROM organization\"\n\ndef load(cursor):\n    cursor.execute(_SELECT_ORGANIZATIONS)\n\ndef refresh(cursor):\n    cursor.execute(_SELECT_ORGANIZATIONS)\n"}],"fixedFiles":[],"focusPath":"queries.py","id":"shared-sql-module-constant","outcome":"accept","scenarioId":"primary","title":"Functions reuse one SQL constant"}],"filePatterns":[],"id":"no-repeated-structured-string-literal","key":"python:no-repeated-structured-string-literal","languages":["python"],"limitations":["Autofix only reuses one existing stable enclosing local literal binding for an entire repeated group of exact builtin len(literal) calls in closed private nested ordinary synchronous nongenerator helpers. An already-authored closed helper must read that binding through len; the correction introduces no names or module lookups and exposes no string identity.","Containing callables require proven inert execution and no frame/metadata/namespace escapes, comments or type parameters. Reassigned, shadowed, conditional, competing or module bindings, unknown callouts, reachable exceptions and groups with any other use remain manual.","Only exact same-file literals of at least 40 characters repeated across distinct callable scopes are compared; near-duplicates are not detected.","Cross-scope findings require credible SQL or an absolute route; identifier-like strings require an existing unique module constant.","Prose, documentation scaffolding, annotations, tests, migrations, examples, generated files, match patterns, and f-string fragments are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"When exact copies represent one SQL, route, or protocol contract, editing one copy can silently diverge the others. Identical text can also represent independent concepts, so this rule is advisory.","references":[],"remediation":"If the occurrences are one maintained concept, reuse an existing constant or extract a descriptive module-level constant within that ownership boundary. If equality is intentional but ownership is independent, suppress with a rationale.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_repeated_string_literal.py","status":"active","summary":"Exact SQL or route literals repeated across callable scopes should share one named binding.","test":"packages/python/tests/rules/test_no_repeated_string_literal.py"},{"aliases":["duplicate-test-body"],"autofix":"none","category":"testing","code":"SARJ066","defaultLevel":"error","engine":"python","examples":[{"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":"One named case table exposes the changing inputs"},{"expectedCount":1,"files":[{"path":"tests/test_parser.py","source":"def test_parse_one():\n    result = parse(\"a\")\n    assert result == 1\n\ndef test_parse_two():\n    result = parse(\"b\")\n    assert result == 2\n\ndef test_parse_three():\n    result = parse(\"c\")\n    assert result == 3\n"}],"fixedFiles":[],"focusPath":"tests/test_parser.py","id":"short-literal-cases","outcome":"reject","scenarioId":"primary","title":"Consecutive short cases repeat the same call and assertion"},{"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":"no-repeated-test-body","key":"python:no-repeated-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.","At least three consecutive call-and-literal-equality cases with corroborating behavior names are compared even with only two statements.","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.","Differing direct string needles in asserted membership comparisons preserve distinct contracts."],"messageIds":[],"optionsSchema":null,"rationale":"Copy-pasted tests drift independently and obscure the input dimension that changes behavior.","references":[],"remediation":"Parameterize literal-varying copies with descriptive `ids` when they exercise one contract; correct or remove verbatim copies that never received their intended edit.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_repeated_test_body.py","status":"active","summary":"Substantial sibling pytest tests repeat the same structural body.","test":"packages/python/tests/rules/test_no_repeated_test_body.py"},{"aliases":["uncontrolled-randomness-in-test"],"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    rng = random.Random(17)\n    values = [rng.random() for _ in range(50)]\n    assert min(values) < 0.5\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 min(values) < 0.5\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":"no-repeated-unseeded-stdlib-random-in-test","key":"python:no-repeated-unseeded-stdlib-random-in-test","languages":["python"],"limitations":["Only standard-library `random` calls in potentially repeated loop or comprehension regions in default-collected test modules and callables are checked.","Single draws, injected RNG objects, Hypothesis tests, and cryptographic randomness are excluded.","Only a statically deterministic, unconditional seed before the repeated sample suppresses the finding; fixture, plugin, and interprocedural seeding are not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"Repeated unseeded sampling creates unreproducible generated inputs, making failures harder to replay and debug when sampled values affect behavior.","references":[],"remediation":"Use an isolated deterministic generator such as `rng = random.Random(17)`, inject one, 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":"A collected test may repeatedly sample an unseeded standard-library PRNG.","test":"packages/python/tests/rules/test_uncontrolled_randomness_in_test.py"},{"aliases":["no-redundant-literal-description"],"autofix":"safe","category":"maintainability","code":"SARJ423","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/models.py","source":"from typing import Literal\nfrom pydantic import BaseModel, Field\n\nclass Request(BaseModel):\n    mode: Literal['realtime', 'batch'] = Field(\n        description='Use batch mode for work expected to exceed the request timeout.'\n    )\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"closed-domain-behavior-described","outcome":"accept","scenarioId":"primary","title":"Description adds behavior not present in the schema"},{"expectedCount":1,"files":[{"path":"app/models.py","source":"from typing import Literal\nfrom pydantic import BaseModel, Field\n\nclass Request(BaseModel):\n    mode: Literal['realtime', 'batch'] = Field(\n        description=\"Must be 'realtime' or 'batch'.\"\n    )\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"closed-domain-restated","outcome":"reject","scenarioId":"primary","title":"Closed domain repeated in prose"}],"filePatterns":[],"id":"no-restated-closed-domain-description","key":"python:no-restated-closed-domain-description","languages":["python"],"limitations":["Safe fixes remove only the description keyword of a comment-free, error-level inert Field call whose entire literal description repeats the complete declared domain. Schema-ownership warnings remain review-only.","Only direct fields on classes with a direct BaseModel base are inspected.","Domains are resolved only from direct string Literals and static Literal aliases or string-valued Enums in the same module.","Descriptions are reported only when a narrow clause repeats the complete domain without mapping or rationale.","ERROR requires an inert direct model with no json_schema_extra options; custom construction, schema metadata, local aliases and Enum annotations retain WARNING with schema-ownership review guidance.","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 only the repeated value list. Preserve guidance about when to use each value, UX labels, examples, deprecation, or 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 string Literal or local string Enum domain in its Pydantic description.","test":"packages/python/tests/rules/test_no_redundant_literal_description.py"},{"aliases":[],"autofix":"safe","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":["Safe fixes remove only a standalone comment whose complete plain-word sequence exactly matches the next single-line direct call identifier, ignoring case and a final period only. Stem matches, omitted words, labels, directives and other prose remain review-only; blank line count is preserved.","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":"When a comment adds no rationale, scope, ordering, or constraint, it duplicates code and can become stale. Preserve comments that add a constraint or rationale rather than restating identifiers.","references":[],"remediation":"Delete it if it merely narrates one statement. Preserve or rewrite comments that explain a reason, constraint, consequence, ordering requirement, or meaningful region label.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_restated_comment.py","status":"active","summary":"Short standalone comment lexically restates the immediately following simple action.","test":"packages/python/tests/rules/test_no_restated_comment.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ012","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"logger.info('request authenticated', auth_method='bearer', credential_present=token is not None)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"redacted-logging-keyword","outcome":"accept","scenarioId":"primary","title":"Only non-sensitive authentication metadata is logged"},{"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 direct secret-named message and argument values, terminal attributes, constant secret-key subscripts, identity slices, f-string and literal format arguments, string concatenation, nested literal containers, literal keyword unpacking, and immediate structured-logger bindings.","Immediate opt calls on a proven untouched native Loguru logger expose only the first bind's values. Positional or expanded chain arguments, unsupported opt keyword names, custom wrappers, stored aliases, and visibly mutated or escaped imports are excluded from this added coverage.","This added coverage requires a native Loguru logging method and a positional message, with no terminal keyword or expanded arguments. Terminal fields are excluded even with capture=False, rather than evaluating option state or merging fields that may override bindings.","Earlier inner bindings are not merged: a safe outer binding hides earlier bindings, including nonconflicting fields. Opaque runtime effects remain unresolved.","Logger recognition and secret classification are lexical; general aliases, arbitrary calls, dynamic containers, and interprocedural dataflow 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 credential or log only approved non-sensitive metadata through a centralized sanitizer.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_secret_in_log.py","status":"active","summary":"A direct credential-like reference is passed to a recognized logging call.","test":"packages/python/tests/rules/test_no_secret_in_log.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ021","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/call_store.py","source":"row = cursor.execute(\n    'SELECT c.id, c.status FROM call AS c WHERE c.id = %s',\n    (call_id,),\n).fetchone()\ncall = Call.model_validate(row)\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"explicit-store-projection","outcome":"accept","scenarioId":"primary","title":"Store query selects named columns"},{"expectedCount":0,"files":[{"path":"app/literal_rows.py","source":"from psycopg import Connection\ndef literal_rows():\n    with Connection.connect() as connection:\n        return connection.execute('SELECT \"authored\".\"a\", \"authored\".\"b\" FROM (VALUES (1, 2)) AS \"authored\"(\"a\", \"b\")').fetchall()\n"}],"fixedFiles":[],"focusPath":"app/literal_rows.py","id":"owned-literal-explicit-projection","outcome":"accept","scenarioId":"owned-literal-projection","title":"Preserve the explicit authored literal columns"},{"expectedCount":0,"files":[{"path":"app/queries.py","source":"query = 'SELECT \"units\" * \"price\" AS total FROM entries'\n"}],"fixedFiles":[],"focusPath":"app/queries.py","id":"quoted-column-arithmetic","outcome":"accept","scenarioId":"projection-owner","title":"Quoted arithmetic operands do not project a wildcard"},{"expectedCount":1,"files":[{"path":"app/literal_rows.py","source":"from psycopg import Connection\ndef literal_rows():\n    with Connection.connect() as connection:\n        return connection.execute('SELECT * FROM (VALUES (1, 2)) AS \"authored\"(\"a\", \"b\")').fetchall()\n"}],"fixedFiles":[{"path":"app/literal_rows.py","source":"from psycopg import Connection\ndef literal_rows():\n    with Connection.connect() as connection:\n        return connection.execute('SELECT \"authored\".\"a\", \"authored\".\"b\" FROM (VALUES (1, 2)) AS \"authored\"(\"a\", \"b\")').fetchall()\n"}],"focusPath":"app/literal_rows.py","id":"safe-autofix","outcome":"reject","scenarioId":"owned-literal-projection","title":"Expand the authored columns of an owned literal relation"},{"expectedCount":1,"files":[{"path":"app/call_store.py","source":"row = cursor.execute(\n    'SELECT c.* FROM call AS c WHERE c.id = %s',\n    (call_id,),\n).fetchone()\ncall = Call.model_validate(row)\n"}],"fixedFiles":[],"focusPath":"app/call_store.py","id":"wildcard-store-projection","outcome":"reject","scenarioId":"primary","title":"Store query selects every column"},{"expectedCount":1,"files":[{"path":"app/queries.py","source":"query = 'SELECT EXISTS((SELECT * FROM jobs)), * FROM runs'\n"}],"fixedFiles":[],"focusPath":"app/queries.py","id":"wrapped-exists-outer-wildcard","outcome":"reject","scenarioId":"projection-owner","title":"The outer projection still selects every column"}],"filePatterns":[],"id":"no-select-star","key":"python:no-select-star","languages":["python"],"limitations":["Only statically recoverable SQL strings in credible query assignments or execution/composition calls in production source files are analyzed.","Dynamic SQL and SQLAlchemy expression trees are not analyzed; bounded CTE wildcards may require a documented suppression.","Autofix only expands a complete single-row integer VALUES query with authored quoted column names, directly consumed by a fresh default psycopg Connection in a closed source-owned function. Other projections remain manual."],"messageIds":[],"optionsSchema":null,"rationale":"Wildcard projections can over-fetch data and silently change result contracts when a source schema evolves.","references":[],"remediation":"List the output columns required by the query consumer. Suppress with a rationale when a bounded intermediate relation intentionally preserves its complete shape.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_select_star.py","status":"active","summary":"SQL SELECT projections should list explicit result columns instead of wildcards.","test":"packages/python/tests/rules/test_no_select_star.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ441","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/settings.py","source":"class Settings:\n    def __init__(self, store: ScheduleStore) -> None:\n        self._store = store\n\n    async def repoint(self, batch_id: str) -> int:\n        return await self._store.repoint(batch_id)\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"bare-settings-matching","outcome":"reject","scenarioId":"bare-settings","title":"Native syntax preserves the existing ownership contract"},{"expectedCount":0,"files":[{"path":"app/settings.py","source":"class Settings:\n    def __init__(self, size: int): self.size = size\n    def double(self): return self.size * 2\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"bare-settings-nonmatching","outcome":"accept","scenarioId":"bare-settings","title":"Existing semantic ownership exclusions remain valid"},{"expectedCount":0,"files":[{"path":"app/batch_settings.py","source":"from dataclasses import dataclass\n\n@dataclass(frozen=True, slots=True)\nclass BatchSettings:\n    retry_limit: int\n"}],"fixedFiles":[],"focusPath":"app/batch_settings.py","id":"settings-is-data-only","outcome":"accept","scenarioId":"primary","title":"A settings type remains a passive record"},{"expectedCount":1,"files":[{"path":"app/batch_settings.py","source":"class BatchSettings:\n    def __init__(self, store: ScheduleStore) -> None:\n        self._store = store\n\n    async def repoint(self, batch_id: str) -> int:\n        return await self._store.repoint(batch_id)\n"}],"fixedFiles":[],"focusPath":"app/batch_settings.py","id":"settings-orchestrates-store","outcome":"reject","scenarioId":"primary","title":"A settings-named class calls injected persistence"}],"filePatterns":[],"id":"no-service-behavior-in-settings","key":"python:no-service-behavior-in-settings","languages":["python"],"limitations":["Only direct module classes named *Settings, *Config, *Configuration, or *Options are checked.","A finding requires a typed collaborator field and an ordinary public method that calls that field; pure computations, properties, validators, serializers, factories, generated files, and tests are excluded. Nested scopes, containers of collaborators, class objects, and methods that rebind self are not inferred.","The rule does not require every data object to use a particular record library and does not infer behavior from a filename alone."],"messageIds":[],"optionsSchema":null,"rationale":"A class named as settings or configuration promises passive data. Calling injected stores, clients, or services from that object hides orchestration behind a data boundary and couples configuration construction to runtime behavior.","references":[],"remediation":"Move collaborator calls into a service, coordinator, or compiler with an honest behavioral name; keep the settings/configuration object as a TypedDict, Pydantic model, attrs class, or dataclass.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_service_behavior_in_settings.py","status":"active","summary":"Settings and configuration types should not orchestrate injected collaborators.","test":"packages/python/tests/rules/test_no_service_behavior_in_settings.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ472","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/values.py","source":"from typing import TypeGuard\ndef is_mapping(value: object) -> TypeGuard[dict[object, object]]:\n    return isinstance(value, dict)\n"}],"fixedFiles":[],"focusPath":"app/values.py","id":"honest-dictionary-keys","outcome":"accept","scenarioId":"primary","title":"Unchecked keys retain their object type"},{"expectedCount":1,"files":[{"path":"app/values.py","source":"from typing import TypeGuard\ndef is_mapping(value: object) -> TypeGuard[dict[str, object]]:\n    return isinstance(value, dict)\n"}],"fixedFiles":[],"focusPath":"app/values.py","id":"unchecked-dictionary-keys","outcome":"reject","scenarioId":"primary","title":"A container check does not prove string keys"}],"filePatterns":[],"id":"no-shallow-container-type-guard","key":"python:no-shallow-container-type-guard","languages":["python"],"limitations":["Checks top-level, undecorated, non-generic synchronous functions with one positional parameter proven to be builtin object and a single return isinstance(parameter, builtin_container), optionally after a docstring.","Repeated module-level function names abstain: an overload's public input contract can be narrower than the implementation annotation.","The TypeGuard or TypeIs target must be a matching builtin dict, list, or set whose slots all resolve to object, str, int, bool, float, or bytes; at least one slot must be narrower than object.","Includes authored tests; generated and vendor source is excluded. String annotations, constrained input types, unions, Any, TypeVars, Protocols, assignment aliases, nested definitions, and content validators are not inferred.","Annotation imports include TYPE_CHECKING imports. Runtime imports use conservative file-wide shadow facts, so unrelated local rebinding can cause false negatives. Wildcard imports, explicit writes to relevant builtin or typing attributes, and exception, pattern or walrus bindings that shadow relevant names or imported aliases exclude the file; dynamic namespace mutation is not inferred.","Relative or conditional imports that replace relevant builtin or typing names exclude the file unless the import proves the same canonical symbol. Unrelated relative imports remain in scope.","No autofix: widening the return contract can require caller changes, while adding content validation changes behavior and cost.","Visible __builtins__ access, canonical namespace-capability imports, module-class changes, and escaping typing/builtins module aliases abstain because namespace mutation can change the meaning of the check.","Canonical native builtin function objects outside direct calls also abstain: captured, passed, or introspected function values can expose the mutable builtin namespace. The explicit native-function inventory covers CPython 3.14 and 3.15; unrelated application methods remain in scope, and arbitrary external effects are not inferred.","Known native function names and canonical imported aliases remain possible namespace capabilities when shadowing or later rebinding obscures their identity. This conservatively excludes some benign first-class application variables with builtin function names.","Direct __lazy_import__ calls and canonical aliases also abstain: CPython 3.15 introduces lazy imports that can return mutable module namespaces."],"messageIds":[],"optionsSchema":null,"rationale":"TypeGuard and TypeIs let callers trust their declared narrowing. Checking only dict, list, or set does not validate keys, values, or elements, so callers can perform operations that fail at runtime.","references":[],"remediation":"Use object for unchecked container slots, or validate their contents when the narrower contract is required. Keep constrained input types that already prove the contents.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_shallow_container_type_guard.py","status":"active","summary":"Do not claim typed container contents from a container-only type guard.","test":"packages/python/tests/rules/test_no_shallow_container_type_guard.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ514","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/guards.py","source":"from typing import TypedDict, TypeGuard\nclass Payload(TypedDict):\n    text: str\ndef is_payload(value: object) -> TypeGuard[dict[object, object]]:\n    return isinstance(value, dict)\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"honest-container-contract","outcome":"accept","scenarioId":"primary","title":"Unchecked contents retain a container contract"},{"expectedCount":0,"files":[{"path":"app/guards.py","source":"from typing import NamedTuple, TypeGuard\nclass Payload(NamedTuple):\n    text: str\ndef is_payload(value: object) -> TypeGuard[Payload]:\n    return isinstance(value, Payload)\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"nominal-named-tuple-membership","outcome":"accept","scenarioId":"named-tuple-shape","title":"Check the actual named tuple class for nominal membership"},{"expectedCount":1,"files":[{"path":"app/guards.py","source":"from typing import NamedTuple, TypeGuard\nclass Payload(NamedTuple):\n    text: str\ndef is_payload(value: object) -> TypeGuard[Payload]:\n    return isinstance(value, tuple)\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"unchecked-named-attribute","outcome":"reject","scenarioId":"named-tuple-shape","title":"Tuple membership does not establish a named record"},{"expectedCount":1,"files":[{"path":"app/guards.py","source":"from typing import TypedDict, TypeGuard\nclass Payload(TypedDict):\n    text: str\ndef is_payload(value: object) -> TypeGuard[Payload]:\n    return isinstance(value, dict)\n"}],"fixedFiles":[],"focusPath":"app/guards.py","id":"unchecked-required-key","outcome":"reject","scenarioId":"primary","title":"A dictionary check does not prove a required key"}],"filePatterns":[],"id":"no-shallow-record-type-guard","key":"python:no-shallow-record-type-guard","languages":["python"],"limitations":["Checks unique, undecorated, non-generic module functions with one object parameter and a sole direct builtin isinstance check of that same parameter, optionally after a docstring.","Targets must be stable, unescaped local class declarations directly inheriting import-proven typing or typing_extensions TypedDict or NamedTuple. TypedDict targets need a required key; every NamedTuple target promises its nominal record API. Inheritance, class decorators, methods, aliases, imported targets and opaque field annotations abstain.","TypedDict total=False and NotRequired-only declarations are preserved. Canonical Required and ReadOnly wrappers are recognized; unknown class options and computed requiredness abstain. NamedTuple constructor defaults do not make named attributes optional, and even zero-field records promise methods absent from plain tuple.","Only bounded builtin and canonical typing field annotations with literal metadata are analyzed. Class-scope shadows, record rebinding, namespace mutation or escape, and repeated function names abstain conservatively.","Constrained input types, public overload contracts, content validators and helpers are not inferred. This rule proves missing shape; it does not claim that key presence alone validates field contents.","Authored tests remain checked; generated and vendor files are excluded. No autofix invents validation or changes narrowing at callers."],"messageIds":[],"optionsSchema":null,"rationale":"An empty dict or tuple passes a container-only check while lacking a TypedDict's required key or a NamedTuple's named attributes and record API. A guard annotation makes callers trust that missing contract.","references":[],"remediation":"Validate the required record shape and field contracts, or keep an honest container return type. For a NamedTuple, checking its actual class can establish nominal membership; tuple membership alone cannot.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_shallow_record_type_guard.py","status":"active","summary":"Do not promise a local record's contract from a container-only check.","test":"packages/python/tests/rules/test_no_shallow_record_type_guard.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ559","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service/batch.py","source":"from collections.abc import Sequence\n\ndef batch(values: Sequence[int] = ()):\n    return values\n"}],"fixedFiles":[],"focusPath":"service/batch.py","id":"immutable-default","outcome":"accept","scenarioId":"primary","title":"An immutable tuple can be shared safely"},{"expectedCount":1,"files":[{"path":"service/batch.py","source":"from collections.abc import Sequence\n\ndef batch(values: Sequence[int] = []):\n    return values\n"}],"fixedFiles":[],"focusPath":"service/batch.py","id":"read-only-annotation","outcome":"reject","scenarioId":"primary","title":"A Sequence annotation still shares a mutable list"}],"filePatterns":[],"id":"no-shared-mutable-default","key":"python:no-shared-mutable-default","languages":["python"],"limitations":["Complements Ruff B006 for mutable aliases, bytearray defaults, lambda defaults, read-only annotations and known container expressions. Native-owned shapes retain B006.","Recognizes container literals/comprehensions, generic mutable constructors, simple aliases, both-mutable conditional branches, concatenation and literal-count repetition. Unknown factory return mutability and complex rebinding are not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"A read-only annotation does not make a list or dictionary immutable, and a named mutable default still shares one object between calls.","references":[],"remediation":"Use an immutable default or accept None and create the mutable value inside the function. Keep an intentional shared cache in an explicit owner.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_shared_mutable_default.py","status":"active","summary":"Do not share mutable function defaults through aliases or read-only annotations.","test":"packages/python/tests/rules/test_no_shared_mutable_default.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ503","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter(prefix='/tasks')\n\n@router.get('')\nasync def list_tasks() -> list[str]:\n    return []\n"}],"fixedFiles":[],"focusPath":"api.py","id":"empty-root","outcome":"accept","scenarioId":"primary","title":"The collection root is registered as an empty path"},{"expectedCount":1,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter(prefix='/tasks')\n\n@router.get('/')\nasync def list_tasks() -> list[str]:\n    return []\n"}],"fixedFiles":[],"focusPath":"api.py","id":"slash-root","outcome":"reject","scenarioId":"primary","title":"The collection root is registered as a slash"}],"filePatterns":[],"id":"no-slash-collection-root-under-prefix","key":"python:no-slash-collection-root-under-prefix","languages":["python"],"limitations":["Only routers created in the same file with a non-empty literal string `prefix=` are checked.","A prefix supplied later through `include_router(..., prefix=...)` is not visible, so \"/\" stays accepted there. Tests and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"With `APIRouter(prefix=\"/items\")`, a \"/\" route serves `/items/`, so a caller of `/items` gets a 307 redirect. Some API clients do not follow it, or replay it without the body or authorization header.","references":[],"remediation":"Register the collection root with path \"\" so `/items` is served directly.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_slash_collection_root_under_prefix.py","status":"active","summary":"A collection-root route under a literal router prefix uses an empty path instead of a slash.","test":"packages/python/tests/rules/test_no_slash_collection_root_under_prefix.py"},{"aliases":["no-tautological-expect"],"autofix":"none","category":"testing","code":"SARJ057","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"def test_service(response):\n    assert True, response.status_code == 200\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"condition-in-message-slot","outcome":"reject","scenarioId":"primary","title":"The intended condition became an assertion message"},{"expectedCount":0,"files":[{"path":"tests/test_service.py","source":"def test_values(rows):\n    checks = (row.is_valid for row in rows)\n    assert all(checks)\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"consumed-generator","outcome":"accept","scenarioId":"deferred-condition","title":"Consume the conditions when the contract requires every row to be valid"},{"expectedCount":0,"files":[{"path":"tests/test_service.py","source":"def test_service():\n    status = service_status()\n    assert status == 'ok'\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"produced-scalar-oracle","outcome":"accept","scenarioId":"setup-only","title":"Assert on a value returned by the application"},{"expectedCount":0,"files":[{"path":"tests/test_service.py","source":"def test_service(response):\n    assert response.status_code == 200\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"runtime-value-assertion","outcome":"accept","scenarioId":"primary","title":"Assertion checks runtime output"},{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"def test_service():\n    status = 'ok'\n    assert status == 'ok'\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"setup-only-oracle","outcome":"reject","scenarioId":"setup-only","title":"A test asserts only on its own fixed setup"},{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"def test_values(rows):\n    checks = (row.is_valid for row in rows)\n    assert checks\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"unconsumed-generator","outcome":"reject","scenarioId":"deferred-condition","title":"A generator object does not check the yielded conditions"}],"filePatterns":[],"id":"no-statically-truthy-assertion","key":"python:no-statically-truthy-assertion","languages":["python"],"limitations":["Detection covers truthy scalar constants, definitely non-empty list/set/dict displays, and directly asserted or locally bound generator expressions and lambdas.","Generator consumption, lambda invocation, comprehensions, and runtime iterator/callable values are not inferred; empty universal-quantification contracts remain valid.","Test-local immutable scalar bindings and their aliases are tracked only through straight-line assignments; calls, control flow, mutable values, and global/nonlocal writes invalidate tracking.","Locally created generator/lambda objects and aliases are tracked for truthiness only; their creation clears earlier setup facts, and comparisons, consumption, invocation, and runtime-created iterators/callables are not inferred.","Pure Boolean conditions, compatible numeric/string/bytes ordering and chained comparisons over known scalar setup are evaluated; proven-true pure assertions retain bindings because their messages do not execute.","Ruff owns asserted strings and tuples, literal comparisons and identity, and unittest-style assertion methods.","Generated files, always-failing assertions, deliberate match-arm success markers, and runtime-value comparisons are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A condition that stays truthy whenever evaluation succeeds cannot reject an incorrect result and often means the intended condition was wrapped in a container, deferred without execution, or placed in the message slot.","references":[],"remediation":"Assert an explicit runtime-derived condition. If the test only requires an operation not to raise, evaluate it directly and retain any meaningful follow-up assertion. Consume generator values or call the lambda when their result is the contract; choose empty-input behavior explicitly.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_tautological_expect.py","status":"active","summary":"An assertion condition is statically truthy, including deferred expressions and test-local scalar setup.","test":"packages/python/tests/rules/test_no_tautological_expect.py"},{"aliases":["inefficient-string-concat-in-loop"],"autofix":"safe","category":"performance","code":"SARJ002","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/render.py","source":"def render(items):\n    result = \"\"\n    for item in items:\n        result = f\"{result}{item}\"\n    return result\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"fstring-growth-in-loop","outcome":"reject","scenarioId":"fstring-growth","title":"F-string replacement repeatedly grows the accumulator"},{"expectedCount":0,"files":[{"path":"app/render.py","source":"def render(items):\n    return \"\".join(str(item) for item in items)\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"join-string-fragments","outcome":"accept","scenarioId":"primary","title":"Fragments joined after the loop"},{"expectedCount":0,"files":[{"path":"app/render.py","source":"def render(items):\n    return \"\".join(f\"{item}\" for item in items)\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"joined-fstring-fragments","outcome":"accept","scenarioId":"fstring-growth","title":"Join rendered fragments after collecting them"},{"expectedCount":1,"files":[{"path":"app/render.py","source":"def render(items):\n    result = \"\"\n    for item in items:\n        result += str(item)\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":["Autofix only handles a private nested ordinary synchronous nongenerator helper with closed direct callers, no known frame/metadata capabilities or comments, and exactly empty-string initialization, one literal loop, and return. The loop must append its exact-string item to that accumulator and contain at least two nonempty string literals; existing names, loop order, and fresh final-string identity are preserved with list append and join.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","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 immutable-string growth can copy the accumulated value on each iteration and become quadratic.","references":[],"remediation":"Collect fragments and join once, or use `io.StringIO` when incremental writes are required.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_string_concat_in_loop.py","status":"active","summary":"Avoid repeatedly growing a proven string accumulator across a loop backedge.","test":"packages/python/tests/rules/test_no_string_concat_in_loop.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ458","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"worker.py","source":"import asyncio\n\nasync def worker():\n    try:\n        await asyncio.sleep(10)\n    except asyncio.CancelledError:\n        raise\n"}],"fixedFiles":[],"focusPath":"worker.py","id":"cancelled-operation-reraises","outcome":"accept","scenarioId":"primary","title":"Cleanup preserves cancellation"},{"expectedCount":1,"files":[{"path":"worker.py","source":"import asyncio\n\nasync def worker():\n    try:\n        await asyncio.sleep(10)\n    except asyncio.CancelledError:\n        return None\n"}],"fixedFiles":[],"focusPath":"worker.py","id":"cancelled-operation-returns-success","outcome":"reject","scenarioId":"primary","title":"Cancellation becomes a successful return"}],"filePatterns":[],"id":"no-swallowed-asyncio-cancellation","key":"python:no-swallowed-asyncio-cancellation","languages":["python"],"limitations":["Only explicit, import-proven asyncio.CancelledError handlers in async functions are inspected; broad handlers, contextlib.suppress, tests, support fixtures, and generated files are excluded.","Only straight-line handlers with a return or normal fallthrough are reported. Compound handlers, re-raising, and translated exceptions are excluded. Any attribute call named uncancel is conservatively excluded without proving its receiver, so unrelated methods can hide a finding.","A single-name task created by asyncio.create_task, immediately cancelled before a try that only awaits it, is intentionally excluded as child cleanup; this does not prove concurrent caller cancellation cannot occur.","Try statements with control-transferring or compound finalizers are excluded, including handlers nested beneath them. Runtime helper effects and interprocedural task ownership are not inferred.","Only a try body consisting of one awaited, unmodified canonical asyncio.sleep call with literal arguments is blocking. Unresolved operations and child cancellation remain advisory."],"messageIds":[],"optionsSchema":null,"rationale":"Returning or falling through after catching CancelledError can make cancelled work look successful and interfere with task groups and timeout shutdown. Child cancellation must be distinguished from cancellation of the caller.","references":[],"remediation":"Re-raise after cleanup. If child cancellation is an expected result, first propagate cancellation when asyncio.current_task().cancelling() is nonzero. Use an exact suppression for a documented boundary that deliberately consumes cancellation.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_swallowed_asyncio_cancellation.py","status":"active","summary":"Preserve cancellation when an async operation is cancelled.","test":"packages/python/tests/rules/test_no_swallowed_asyncio_cancellation.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ558","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service/bootstrap.py","source":"import sys\nsearch_path = tuple(sys.path)\n"}],"fixedFiles":[],"focusPath":"service/bootstrap.py","id":"inspect-search-path","outcome":"accept","scenarioId":"primary","title":"Inspecting a snapshot does not change import resolution"},{"expectedCount":1,"files":[{"path":"service/bootstrap.py","source":"import sys\nsys.path.insert(0, '/opt/application')\n"}],"fixedFiles":[],"focusPath":"service/bootstrap.py","id":"mutating-search-path","outcome":"reject","scenarioId":"primary","title":"Import resolution depends on process-wide mutation"}],"filePatterns":[],"id":"no-sys-path-mutation","key":"python:no-sys-path-mutation","languages":["python"],"limitations":["Covers direct writes, item/slice writes, deletion, in-place operators, mutating list methods, static setters and simple import/value aliases.","Read-only access, copied paths and locally shadowed sys names are accepted. Arbitrary reflection, container-held aliases and unknown callback effects are not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"Changing the process-wide import search path makes import resolution depend on execution order and can shadow installed packages.","references":[],"remediation":"Install the package and use explicit package imports. A required interpreter bootstrap boundary needs an explained local suppression.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_sys_path_mutation.py","status":"active","summary":"Do not mutate the interpreter import search path.","test":"packages/python/tests/rules/test_no_sys_path_mutation.py"},{"aliases":[],"autofix":"safe","category":"testing","code":"SARJ551","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_save.py","source":"def test_stale_save_is_rejected():\n    assert save_is_rejected()\n"}],"fixedFiles":[],"focusPath":"tests/test_save.py","id":"behavior-in-test-name","outcome":"accept","scenarioId":"primary","title":"The assertion documents the behavior"},{"expectedCount":1,"files":[{"path":"tests/test_save.py","source":"def test_save():\n    # A stale save is rejected.\n    assert save_is_rejected()\n"}],"fixedFiles":[],"focusPath":"tests/test_save.py","id":"comment-narrates-test","outcome":"reject","scenarioId":"primary","title":"Name the tested behavior"},{"expectedCount":2,"files":[{"path":"tests/test_activation.py","source":"def test_activation():\n    # Load current account profile activation settings\n    # Build current account profile activation response\n    load_current_account_profile_activation_settings()\n    return build_current_account_profile_activation_response()\n"}],"fixedFiles":[{"path":"tests/test_activation.py","source":"def test_activation():\n\n\n    load_current_account_profile_activation_settings()\n    return build_current_account_profile_activation_response()\n"}],"focusPath":"tests/test_activation.py","id":"flat-test-action-narration","outcome":"reject","scenarioId":"flat-test-action-narration","title":"Remove only test prose proved by the existing ordered-action contract"},{"expectedCount":0,"files":[{"path":"tests/test_activation.py","source":"def test_activation():\n\n\n    load_current_account_profile_activation_settings()\n    return build_current_account_profile_activation_response()\n"}],"fixedFiles":[],"focusPath":"tests/test_activation.py","id":"flat-test-action-narration-fixed","outcome":"accept","scenarioId":"flat-test-action-narration","title":"The exact action sequence needs no test prose"}],"filePatterns":[],"id":"no-test-comments","key":"python:no-test-comments","languages":["python"],"limitations":["Checks real comment tokens in recognized test and test-support paths, including inline comments; string contents and docstrings are outside this policy.","Ambiguous test-like filenames require collected test declarations; test and test-support directories and conftest own their source independently of declarations.","Generated files, malformed source, legal notices, shebangs, encoding declarations and tool directives are excluded. Existing comment rules retain their own diagnostic ownership.","This is an explicitly requested style policy, released as a warning. Safe fixes reuse the existing ordered-narration proof only when every line in the complete standalone paragraph is an unsuppressed finding owned by this rule and exactly repeats its corresponding flat zero-argument action or the closed accumulation shape. Every removed comment retains its blank line; all other rationale remains manual."],"messageIds":[],"optionsSchema":null,"rationale":"Test prose can drift from the setup and assertion it describes; executable contracts keep the explanation checked.","references":[],"remediation":"Remove the comment, clarify the test or helper name, or express the invariant with an assertion. Preserve legal notices and required tool directives.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_test_comments.py","status":"active","summary":"Express test behavior in names and assertions instead of ordinary comments.","test":"packages/python/tests/rules/test_no_test_comments.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ478","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import Mock\n\nclass Sender:\n    def send(self, text: str) -> None:\n        pass\n\ndef test_delivery():\n    sender = Mock(spec=Sender)\n    messages = []\n    def record(text: str) -> None:\n        messages.append(text)\n    sender.send = record\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"method-implementation-assignment","outcome":"reject","scenarioId":"primary","title":"A test replaces a mock's resolved collaborator method"},{"expectedCount":0,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import create_autospec\n\nclass Sender:\n    def send(self, text: str) -> None:\n        pass\n\ndef test_delivery():\n    sender = create_autospec(Sender, instance=True, spec_set=True)\n    messages = []\n    sender.send.side_effect = messages.append\n    sender.send('hello')\n    sender.send.assert_called_once_with('hello')\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"mock-method-side-effect","outcome":"accept","scenarioId":"primary","title":"Configure behavior through an existing mock method"}],"filePatterns":[],"id":"no-test-method-grafting","key":"python:no-test-method-grafting","languages":["python"],"limitations":["Only non-generated test paths are analyzed; a destination method and replacement callable must have statically resolved provenance.","Inference uses source declarations and conservative constructor, annotation, fixture, and helper relationships; it never imports application or SDK code at runtime.","Data fields, callable-valued fields, mock configuration attributes, whole-object fake injection, and scoped patch calls are excluded.","Wrappers visibly forwarding a captured original of the same receiver and member, and matching saved-original restorations in finally, are excluded.","Unknown types, ambiguous bindings, unresolved decorators or inheritance, and dynamically chosen members are left unreported; canonical typing.final class decorators are transparent."],"messageIds":[],"optionsSchema":null,"rationale":"Grafting a function onto a collaborator or mock hides the boundary's callable contract and bypasses the mock's call and await records. An injected fake makes behavior explicit; configuring an existing contract-aware mock preserves its observation API. Scoped patches also restore real objects reliably.","references":[],"remediation":"Inject a concrete fake or configure the existing autospecced mock method's return_value or side_effect. Preserve the real method's synchronous or asynchronous contract, including synchronous awaitable returns. Explain other intentional runtime interception with an exact-code local suppression.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_test_method_grafting.py","status":"active","summary":"Avoid assigning replacement implementations directly onto resolved methods in tests.","test":"packages/python/tests/rules/test_no_test_method_grafting.py"},{"aliases":["trailing-value-narration"],"autofix":"safe","category":"maintainability","code":"SARJ051","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"settings.py","source":"STALE_TIME = 5 * 60 * 1000  # 5 minutes\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"repeated-numeric-unit","outcome":"reject","scenarioId":"primary","title":"Comment repeats the duration"},{"expectedCount":0,"files":[{"path":"settings.py","source":"from datetime import timedelta\n\nSTALE_TIME = timedelta(minutes=5)\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"unit-encoded-in-value","outcome":"accept","scenarioId":"primary","title":"Typed value carries the unit"}],"filePatterns":[],"id":"no-trailing-numeric-unit-assignment-comment","key":"python:no-trailing-numeric-unit-assignment-comment","languages":["python"],"limitations":["Safe fixes remove only an exact number-and-unit comment on a direct nonnegative numeric literal assignment when the existing name has exactly that same unit suffix and the numeric values agree. Arithmetic, conversion, conflicting or missing units, aliases and explanatory prose remain review-only; executable code and line endings are retained.","Detection targets one-line numeric assignments with one simple name or attribute target and a trailing comment that repeats one number and unit.","Calls, returns, loops, assertions, chained or destructured assignments, multi-number comments, approximate conversions, reasons, references, directives, bracketed values, invalid syntax, and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A unit encoded only in a comment can drift; a unit-bearing name makes it machine- and reviewer-visible, while a duration type can enforce it.","references":[],"remediation":"Encode or reconcile the unit in the assigned name or typed value, such as timeout_ms or timedelta(minutes=5), then delete the duplicate comment.","since":null,"source":"packages/python/src/sarj_python_lint/rules/trailing_value_narration.py","status":"active","summary":"A trailing comment redundantly labels a numeric assignment with its value and unit.","test":"packages/python/tests/rules/test_trailing_value_narration.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ404","defaultLevel":"error","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 import-proven psycopg/psycopg2 UniqueViolation and asyncpg UniqueViolationError handlers whose translating branch matches direct or single-assignment rendered-message data.","Generic IntegrityError handlers, driver wrappers, branch-ambiguous aliases, and interprocedural message helpers remain outside its scope.","Tests and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Database error text is localized and can change with driver or server versions; supported PostgreSQL drivers expose the violated constraint as structured diagnostic data.","references":[],"remediation":"Compare the driver's structured constraint field: `exc.diag.constraint_name` for psycopg or `exc.constraint_name` for asyncpg. SQLSTATE 23505 identifies the violation category, not the constraint.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_unique_violation_message_match.py","status":"active","summary":"Do not make unique-violation control flow depend on rendered exception text.","test":"packages/python/tests/rules/test_no_unique_violation_message_match.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ492","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, field_validator\nclass Model(BaseModel):\n    value: int\n    @field_validator('value')\n    @classmethod\n    def check(cls, value: int) -> int: return value\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"declared-field","outcome":"accept","scenarioId":"primary","title":"Decorator names the declared field"},{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, field_validator\nclass Model(BaseModel):\n    value: int\n    @field_validator('missing')\n    @classmethod\n    def check(cls, value: int) -> int: return value\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"missing-field","outcome":"reject","scenarioId":"primary","title":"Decorator targets a field the model does not declare"}],"filePatterns":[],"id":"no-unknown-pydantic-validator-field","key":"python:no-unknown-pydantic-validator-field","languages":["python"],"limitations":["Only direct module-level BaseModel subclasses with a static namespace and resolved field decorators are inspected.","Unknown inheritance, computed fields, custom decorators, overwritten methods, wildcard fields and uncertain check_fields are excluded.","Visible namespace APIs, dynamic module access, ambiguous imports and escaped or mutated imported objects exclude the file.","One diagnostic is emitted per decorator even if several field targets are missing."],"messageIds":[],"optionsSchema":null,"rationale":"A field validator or serializer naming a missing field prevents the model from being constructed.","references":[],"remediation":"Use the declared field name, or explicitly set check_fields=False for a validator intended for subclass fields.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_unknown_pydantic_validator_field.py","status":"active","summary":"Pydantic field decorators must target declared fields.","test":"packages/python/tests/rules/test_no_unknown_pydantic_validator_field.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ420","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/metadata.py","source":"from functools import lru_cache\n@lru_cache(maxsize=1)\ndef count() -> int:\n    \"\"\"Copied onto the wrapper.\"\"\"\n    return 1\n"}],"fixedFiles":[],"focusPath":"app/metadata.py","id":"canonical-cache-consumer","outcome":"accept","scenarioId":"metadata-copy","title":"A cache wrapper copies the function's documentation"},{"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"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"\"\"\"Service entry points.\n\nCoordinates requests.\n\"\"\"\nVALUE = 1\n"}],"fixedFiles":[{"path":"app/service.py","source":"VALUE = 1\n"}],"focusPath":"app/service.py","id":"module-docstring-autofix","outcome":"reject","scenarioId":"module-docstring-autofix","title":"Unconsumed top-of-file prose is deleted"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"\"\"\"Service entry points.\"\"\"\nDESCRIPTION = __doc__\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"module-docstring-consumer","outcome":"accept","scenarioId":"module-docstring-autofix","title":"A module description consumed at runtime is retained"},{"expectedCount":1,"files":[{"path":"app/metadata.py","source":"from functools import wraps\n\ndef original() -> int:\n    \"\"\"This prose is not copied.\"\"\"\n    return 1\n\n@wraps(original, assigned=())\ndef wrapped() -> int:\n    return original()\n"}],"fixedFiles":[],"focusPath":"app/metadata.py","id":"wraps-without-doc-copy","outcome":"reject","scenarioId":"metadata-copy","title":"A wrapper excluding __doc__ does not consume input prose"}],"filePatterns":[],"id":"no-unnecessary-docstring","key":"python:no-unnecessary-docstring","languages":["python"],"limitations":["Safe fixes delete standalone module docstrings with no detected consumer, dynamic namespace access or frame/metadata observation. Legal notices, adjacent string expressions, trailing comments and same-line code prevent deletion. A docstring-only module may become empty.","Function fixes require a second native error-level exact-name-restatement finding on an unescaped private nested synchronous nongenerator function. All references must be direct calls; public callable APIs, classes, behavioral function prose, escapes and metadata consumers remain review-only. Known frame and metadata capabilities, their import aliases and metadata field access prevent fixes.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","Generated files, doctests, syntax-required function stub bodies, schema classes, framework decorators, explicit __doc__/help/getdoc consumers, canonical metadata-copy decorators, and local sarj-noqa suppressions are excluded.","Ordinary class prose is checked even when it is the entire body or dataclass retains it as incidental metadata. Replace an otherwise empty class body with pass; explicit documentation consumers and schema descriptions remain protected.","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.","Source-proven direct decorators returning a wraps-decorated wrapper consume only their input doc; factory and wrapper implementation prose remain eligible. Unknown assigned/updated options, shadowed bindings, lazy outer factories, opaque wrapper headers, and unresolved imports do not prove metadata consumption."],"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":"correctness","code":"SARJ483","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/records.py","source":"from copy import replace\ndef rename(record):\n    result = replace(record, name='new')\n    return result\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"consumed-standard-copy","outcome":"accept","scenarioId":"standard-replacement","title":"Capture the standard-library replacement"},{"expectedCount":1,"files":[{"path":"app/records.py","source":"from dataclasses import replace\ndef rename(record):\n    replace(record, name='new')\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"discarded-dataclass-replacement","outcome":"reject","scenarioId":"standard-replacement","title":"A standard-library replacement is ignored"},{"expectedCount":1,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel\nclass Record(BaseModel):\n    name: str\ndef rename(record: Record):\n    record.model_copy(update={'name': 'new'})\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"discarded-model-copy","outcome":"reject","scenarioId":"primary","title":"A replacement model is ignored"},{"expectedCount":0,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel\nclass Record(BaseModel):\n    name: str\ndef rename(record: Record):\n    return record.model_copy(update={'name': 'new'})\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"returned-copy","outcome":"accept","scenarioId":"primary","title":"Return the replacement to its caller"}],"filePatterns":[],"id":"no-unused-copy-result","key":"python:no-unused-copy-result","languages":["python"],"limitations":["Import-resolved pytest.raises blocks consume copy operations as exception assertions and are excluded.","Only standalone calls to import-resolved dataclasses.replace, copy.replace, and source-proven inherited Pydantic model_copy methods are reported.","Arbitrary copy/replace methods, shadowed imports, Pydantic method or copy-hook overrides, and unknown receivers are excluded.","Assignments, returns, and passing the result to another call consume the result and are not reported. Copy hooks can run user code, so no deletion or assignment autofix is offered.","Generated/vendor sources are excluded; maintained tests are checked."],"messageIds":[],"optionsSchema":null,"rationale":"Copy-on-write replacement APIs return another value rather than updating the original. Discarding their result can silently leave subsequent operations using unchanged data.","references":[],"remediation":"Assign, return, or otherwise consume the replacement. A copy hook intentionally exercised for its effects or exceptions can use a narrow suppression; do not automatically delete the call.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_unused_copy_result.py","status":"active","summary":"Use the replacement returned by a resolved copy API.","test":"packages/python/tests/rules/test_no_unused_copy_result.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ475","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/sink.py","source":"def _emit(*, _unused: bool=True):\n    return 'ready'\n_emit()\n_emit(_unused=False)\n"}],"fixedFiles":[],"focusPath":"app/sink.py","id":"closed-singleton-keyword-input","outcome":"reject","scenarioId":"primary","title":"A private helper's unused singleton keyword has no contract or retained value"},{"expectedCount":0,"files":[{"path":"app/sink.py","source":"def emit():\n    return 'ready'\n"}],"fixedFiles":[],"focusPath":"app/sink.py","id":"removed-keyword-input","outcome":"accept","scenarioId":"primary","title":"The callable exposes only inputs it uses"}],"filePatterns":[],"id":"no-unused-underscored-keyword-parameter","key":"python:no-unused-underscored-keyword-parameter","languages":["python"],"limitations":["Safe fixes remove unannotated bool/None-default keyword-only inputs only from private unescaped nested synchronous nongenerator helpers with proven direct callers, updating only bool/None literal caller keywords. Module/public/callback contracts, type parameters, comments, reflection, annotations, expanded calls and effectful values remain review-only; native severities are unchanged. Known frame and metadata capabilities, their import aliases and metadata field access prevent fixes.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","ERROR is limited to private synchronous module functions with proven direct callers, no dynamic namespace or variadic/decorated contract, inert builtin bool/None annotations or no annotation, and bool/None defaults and supplied values. Public, method, callback, required-slot and effectful signature/argument cases retain WARNING.","Only underscore-prefixed keyword-only declarations are checked; positional and variadic callback slots are excluded.","Decorated functions other than unshadowed builtin staticmethod/classmethod, inherited methods, and intentional stubs are excluded.","Any reference within the body counts as use, including nested scopes; shadowing and reassignment can produce false negatives.","Functions accessing locals, vars, eval, or exec through recognized builtin bindings are excluded.","Callables referenced as values are excluded because their external keyword contract is unknown. Lambda parameters, unknown dynamic reflection, and cross-module callback contracts are not resolved.","Generated and vendored files are excluded; test source is checked."],"messageIds":[],"optionsSchema":null,"rationale":"A keyword-only parameter requires callers to use its declared name. Prefixing an unused input with an underscore silences unused-argument lint while retaining an input whose value has no effect. Removing the dead input and its callers keeps the callable's interface honest.","references":[],"remediation":"Remove the unused keyword-only parameter and update callers together. If an external contract requires the input, preserve its required spelling and use a narrow, reasoned unused-argument suppression. Do not erase a precise callback signature with **kwargs or add dummy reads to silence this warning.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_unused_underscored_keyword_parameter.py","status":"active","summary":"Remove unused underscore-prefixed keyword-only parameters instead of hiding them from lint.","test":"packages/python/tests/rules/test_no_unused_underscored_keyword_parameter.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ452","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/adapter.py","source":"def render(root: Path, manifest: Path) -> str:\n    _ = root, manifest\n    return 'ready'\n"}],"fixedFiles":[],"focusPath":"app/adapter.py","id":"callback-parameter-marker","outcome":"reject","scenarioId":"callback-parameters","title":"A callback disguises unused parameters with a discard binding"},{"expectedCount":0,"files":[{"path":"app/adapter.py","source":"refresh_cache()\n"}],"fixedFiles":[],"focusPath":"app/adapter.py","id":"direct-call","outcome":"accept","scenarioId":"call-result","title":"Run the call without creating a discard binding"},{"expectedCount":1,"files":[{"path":"app/adapter.py","source":"_ = refresh_cache()\n"}],"fixedFiles":[],"focusPath":"app/adapter.py","id":"discarded-call-result","outcome":"reject","scenarioId":"call-result","title":"Invoke a call directly when its result is intentionally unused"},{"expectedCount":1,"files":[{"path":"app/adapter.py","source":"value = 1\n_ = value\n"}],"fixedFiles":[],"focusPath":"app/adapter.py","id":"discarded-nonparameter-value","outcome":"reject","scenarioId":"primary","title":"A standalone discard binding is unnecessary"},{"expectedCount":0,"files":[{"path":"app/adapter.py","source":"def on(event: str, callback: object) -> None:\n    pass\n"}],"fixedFiles":[],"focusPath":"app/adapter.py","id":"intentional-callback-stub","outcome":"accept","scenarioId":"callback-parameters","title":"An intentional stub preserves its callback signature"},{"expectedCount":0,"files":[{"path":"app/adapter.py","source":"name, _ = load_pair()\n"}],"fixedFiles":[],"focusPath":"app/adapter.py","id":"unpacking-placeholder","outcome":"accept","scenarioId":"primary","title":"Keep a conventional placeholder inside unpacking"}],"filePatterns":[],"id":"no-unused-value-marker","key":"python:no-unused-value-marker","languages":["python"],"limitations":["Safe fixes remove only primitive literal markers directly in function bodies, inserting pass when necessary. Calls, parameters, resource retention, chained assignments and annotations remain review-only.","Plain and annotated assignments whose standalone target is `_` are reported, including chained assignments.","Formal-parameter references are reported too, including positional, keyword-only, and variadic parameters.","Unpacking placeholders, loop targets, wildcard imports, and underscore-prefixed names are excluded.","A binding read in the same enclosing scope is not a discard. Reads in nested scopes conservatively preserve it, even when shadowing cannot be resolved.","Generated and vendored files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A standalone `_ = value` assignment adds a binding without clarifying intent. A side-effecting call can be invoked directly. Reading unused parameters only to satisfy a linter hides whether a callback is intentionally inert or implements an incomplete contract.","references":[],"remediation":"Invoke calls directly when their result needs no retention. When a result owns a lease or resource lifetime, retain it under an informative name such as `_retained_lease`; do not drop it. Remove or rename locally owned unused values; when an external interface requires the exact unused parameter, put a narrow reasoned linter suppression on its declaration. Use an explicit empty body for an intentional stub, `@override` for a real override, or `NotImplementedError` for an unsupported operation; preserve required keyword names.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_unused_value_marker.py","status":"active","summary":"Do not use standalone assignments to `_` to discard values.","test":"packages/python/tests/rules/test_no_unused_value_marker.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ419","defaultLevel":"error","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":0,"files":[{"path":"adapter.py","source":"value = vendor.value # pyright: ignore[reportAny] -- the dependency's runtime value has no published stub\n"}],"fixedFiles":[],"focusPath":"adapter.py","id":"explained-suppression-reason","outcome":"accept","scenarioId":"missing-reason","title":"Explain the concrete dependency typing boundary"},{"expectedCount":1,"files":[{"path":"adapter.py","source":"value = vendor.value  # ruff: 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"},{"expectedCount":1,"files":[{"path":"adapter.py","source":"value = vendor.value # pyright: ignore[reportAny]\n"}],"fixedFiles":[],"focusPath":"adapter.py","id":"missing-suppression-reason","outcome":"reject","scenarioId":"missing-reason","title":"A scoped suppression still needs an explanation"}],"filePatterns":[],"id":"no-vague-suppression-description","key":"python:no-vague-suppression-description","languages":["python"],"limitations":["Checks missing descriptions and supported Python suppression descriptions that exactly match a bounded generic phrase. Multiple directives on one line may share its trailing boundary explanation.","Blanket directives, formatter and coverage pragmas, Pylint directives, and whether a suppression is necessary remain outside this rule.","Ordinary scoped directives must be attached to code; recognized Ruff and Flake8 file directives may stand alone.","Generated-source markers do not waive this check. Generated/vendor boundaries belong in explicit whole-path exclusions."],"messageIds":[],"optionsSchema":null,"rationale":"Generic reasons satisfy review conventions without making the suppressed risk auditable or removable.","references":[],"remediation":"Name the concrete tool mismatch or safety invariant and the condition that would allow removal. Cite an upstream issue or affected version when that is the durable boundary.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_vague_suppression_description.py","status":"active","summary":"Suppressions need a concrete description of the exception.","test":"packages/python/tests/rules/test_no_vague_suppression_description.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ436","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"logger.error('provider failed', request_id=request.id, response_status=response.status_code)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"response-metadata-logged","outcome":"accept","scenarioId":"primary","title":"Only explicit response metadata is logged"},{"expectedCount":1,"files":[{"path":"service.py","source":"response_body = response.json()\nlogger.error('provider failed', response_body=response_body)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"response-payload-logged","outcome":"reject","scenarioId":"primary","title":"Whole response payload passed to a logger"}],"filePatterns":[],"id":"no-whole-request-response-payload-in-log","key":"python:no-whole-request-response-payload-in-log","languages":["python"],"limitations":["Detection follows direct request/response body, content, data, JSON, payload, and text references through stable same-block aliases, interpolation, and immutable literal logging mappings.","Generic request or response objects, unrelated body/payload values, explicitly sanitized summaries, mutated or branch-dependent aliases, dynamic containers, and interprocedural flows are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Request and response bodies often contain personal, financial, or echoed authentication data, while log sinks commonly have broader readership and longer retention.","references":[],"remediation":"Log explicit non-sensitive metadata such as request IDs, response status, counts, or a deliberately sanitized summary. Suppress with a rationale when the payload type is proven public and bounded.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_whole_request_response_payload_in_log.py","status":"active","summary":"Whole request or response payloads passed to logging calls require review.","test":"packages/python/tests/rules/test_no_whole_request_response_payload_in_log.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ502","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.get('/batches/{batch_id}/retry')\nasync def retry(batch_id: str) -> None:\n    return None\n"}],"fixedFiles":[],"focusPath":"api.py","id":"get-retry","outcome":"reject","scenarioId":"primary","title":"A GET route retries a batch"},{"expectedCount":0,"files":[{"path":"api.py","source":"from fastapi import APIRouter\n\nrouter = APIRouter()\n\n@router.post('/batches/{batch_id}/retry')\nasync def retry(batch_id: str) -> None:\n    return None\n"}],"fixedFiles":[],"focusPath":"api.py","id":"post-retry","outcome":"accept","scenarioId":"primary","title":"The retry is a POST"}],"filePatterns":[],"id":"no-work-verb-get-route","key":"python:no-work-verb-get-route","languages":["python"],"limitations":["Only literal route paths on a FastAPI or APIRouter receiver created in the same file are checked.","The final path segment is read; its first word (split on '-', '_' or '.') must be run, trigger, send, dispatch, execute, launch, enqueue or retry. Plural nouns such as runs are accepted.","A GET that dispatches work under any other name is not detected. Tests and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"GET must be safe. A GET that dispatches work can be fired cross-site by an image tag or link prefetch, bypasses CSRF protection, and is retried freely by proxies and clients.","references":[],"remediation":"Change the operation to POST and keep GET for reads.","since":null,"source":"packages/python/src/sarj_python_lint/rules/no_work_verb_get_route.py","status":"active","summary":"GET routes must not end in a work verb such as run, send, trigger or retry.","test":"packages/python/tests/rules/test_no_work_verb_get_route.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ567","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"RATE = 16_000\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"declared-domain","outcome":"accept","scenarioId":"primary","title":"The contract is explicit"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"RATE = 16000\n"}],"fixedFiles":[{"path":"app/service.py","source":"RATE = 16_000\n"}],"focusPath":"app/service.py","id":"undeclared-domain","outcome":"reject","scenarioId":"primary","title":"The contract is implicit"}],"filePatterns":[],"id":"numeric-literal-separators","key":"python:numeric-literal-separators","languages":["python"],"limitations":["Decimal integer parts with at least five digits are checked; four-digit years, base-prefixed numbers, exponents, imaginary numbers, fractional digits, comments and strings are left alone.","Safe fixes change numeric token spelling only and preserve the parsed AST. Interpolated strings are review-only because changing a debug expression also changes its displayed label.","Generated files and exact-code suppressions are respected. No constant names or numeric domains are invented."],"messageIds":[],"optionsSchema":null,"rationale":"Long ungrouped decimal quantities are harder to read and easy to miscount.","references":[],"remediation":"Use three-digit groups such as 10_000_000; retain a named constant when the value has domain meaning.","since":null,"source":"packages/python/src/sarj_python_lint/rules/numeric_literal_separators.py","status":"active","summary":"Group long decimal numeric literals with thousands separators.","test":"packages/python/tests/rules/test_numeric_literal_separators.py"},{"aliases":["parametrize-case-needs-id"],"autofix":"none","category":"testing","code":"SARJ042","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_handler.py","source":"import pytest\n\n@pytest.mark.parametrize(\n    \"payload\",\n    [\n        pytest.param({}, id=\"empty-payload\"),\n        pytest.param({\"status\": \"invalid\"}, id=\"invalid-status\"),\n    ],\n)\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\", [{}, {\"status\": \"invalid\"}])\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 import-proven pytest parametrization over static list or tuple tables in maintained test files is analyzed.","Cases with a pytest-readable scalar column or a statically non-None explicit ID are allowed.","Dynamic and callable IDs and repository-level pytest_make_parametrize_id hooks are not evaluated."],"messageIds":[],"optionsSchema":null,"rationale":"Without a semantic naming owner, fallback IDs such as `payload0` are hard to diagnose and silently change when the table is reordered.","references":[],"remediation":"Check the owning pytest_make_parametrize_id hook before adding IDs; reuse an existing semantic naming owner. Otherwise give each opaque row a semantic `pytest.param(..., id=...)`, or use a semantic callable `ids=` when one mapping applies to the entire table.","since":null,"source":"packages/python/src/sarj_python_lint/rules/opaque_parametrize_case_needs_id.py","status":"active","summary":"Opaque static pytest parameter cases lack table-local semantic IDs.","test":"packages/python/tests/rules/test_opaque_parametrize_case_needs_id.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ062","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_service.py","source":"from unittest.mock import patch\n\n@patch(\"shop.payment_gateway.charge\")\ndef test_checkout(gateway, test_database, inventory_service):\n    result = checkout(test_database, inventory_service)\n    assert result.status == \"confirmed\"\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"external-boundary-only","outcome":"accept","scenarioId":"primary","title":"Component test keeps one external boundary"},{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"from unittest.mock import patch\n\n@patch(\"shop.inventory.reserve\")\n@patch(\"shop.payments.charge\")\n@patch(\"shop.shipping.quote\")\n@patch(\"shop.tax.calculate\")\n@patch(\"shop.email.send_receipt\")\n@patch(\"shop.risk.approve\")\ndef test_checkout(risk, email, tax, shipping, payments, inventory):\n    assert checkout() == \"confirmed\"\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 maintained, collected tests are analyzed; generated files and nested lexical scopes are excluded.","The warning counts independently rooted doubles and inferred collaborator owners, including mock-named fixtures.","Class-level patches, configuration and process-state knobs, and identified composition-root or documentation harnesses are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Broad double setup obscures the behavior under test and often couples tests to implementation wiring.","references":[],"remediation":"Prefer real in-process dependencies, a component harness, or purpose-built fakes; keep mocks at true external boundaries.","since":null,"source":"packages/python/src/sarj_python_lint/rules/over_mocked_test.py","status":"active","summary":"Tests should not use more than five independently rooted test doubles or collaborator substitutions.","test":"packages/python/tests/rules/test_over_mocked_test.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ545","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/scenario_store.py","source":"from psycopg import AsyncCursor\nasync def load(cursor: AsyncCursor):\n    await cursor.execute('SELECT s.id, u.email FROM scenario s LEFT JOIN \"user\" u ON u.id = s.actor_id WHERE s.id = %s', (1,))\n"}],"fixedFiles":[],"focusPath":"app/scenario_store.py","id":"cross-model-join","outcome":"reject","scenarioId":"primary","title":"Separate model relations share one SQL projection"},{"expectedCount":0,"files":[{"path":"app/scenario_store.py","source":"from psycopg import AsyncCursor\nasync def load(cursor: AsyncCursor):\n    await cursor.execute('SELECT s.id, u.email FROM scenario s LEFT JOIN scenario u ON u.id = s.actor_id WHERE s.id = %s', (1,))\n"}],"fixedFiles":[],"focusPath":"app/scenario_store.py","id":"self-join","outcome":"accept","scenarioId":"primary","title":"A self join does not prove distinct model ownership"}],"filePatterns":[],"id":"prefer-application-model-joins","key":"python:prefer-application-model-joins","languages":["python"],"limitations":["An architecture review warning, not a claim that SQL joins are inherently incorrect or slow. Distinct literal PostgreSQL relations are a source-visible proxy for ownership; syntax cannot establish whether two relations represent the same domain model.","Checks native Psycopg execution and direct same-class query-forwarding wrappers in non-test Python. Each SELECT block needs at least two different direct table relations joined in that block. Self joins, CTE references, derived relations, table-valued functions, dynamic relation names, schema-ambiguous qualification and unsupported SQL abstain. Relation, CTE or table-alias identifiers over 63 UTF-8 bytes or with invalid Unicode also abstain.","Ordinary explicit JOINs and comma joins over direct relations remain reviewable. A join to a CTE does not borrow its underlying relation as a direct model boundary. One warning is emitted per authored SQL expression; no automatic rewrite moves transaction or locking boundaries."],"messageIds":[],"optionsSchema":null,"rationale":"A SQL join across separate persistence relations couples their read representations and can bypass the owning store's access and conversion policy. This architecture prefers bounded store reads and typed application composition when their consistency contract permits it.","references":[],"remediation":"Review the relation owners, then load the related records through their typed stores, batch by IDs and compose them in application code. Preserve tenant filters, null-extension, cardinality, snapshot consistency, authorization and FOR UPDATE locks. Keep an explained exception for an atomic database operation; replacing a join with unrelated reads can change behavior or introduce N+1 queries.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_application_model_joins.py","status":"active","summary":"Compose distinct model records in application code instead of a cross-model SQL join.","test":"packages/python/tests/rules/test_prefer_application_model_joins.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ474","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import create_autospec\ndef send(*, recipient: str) -> None:\n    pass\ndef test_send():\n    double = create_autospec(send, spec_set=True)\n    double(recipient='sample')\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"autospecced-function-direct-call","outcome":"accept","scenarioId":"primary","title":"Autospec checks the function call shape"},{"expectedCount":1,"files":[{"path":"tests/test_delivery.py","source":"from unittest.mock import Mock\ndef send(*, recipient: str) -> None:\n    pass\ndef test_send():\n    double = Mock(spec=send)\n    double(recipient='sample')\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"specced-function-direct-call","outcome":"reject","scenarioId":"primary","title":"A function spec leaves invocation permissive"}],"filePatterns":[],"id":"prefer-autospec-for-callable-mock","key":"python:prefer-autospec-for-callable-mock","languages":["python"],"limitations":["Only authored test files, proven unittest.mock constructors, earlier undecorated non-generic module-local function specs without **kwargs, and unconditional simple mock assignments are analyzed.","Requires a stable binding and at least one direct call with explicit keywords in the same scope. Escapes, aliases, rebinding, dynamic argument unpacking, wildcard imports and reflective namespace mutation are excluded.","Imported functions, class or descriptor specs, wrapping, side effects and uncertain configuration are excluded. AsyncMock requires an async function spec so autospec preserves awaiting. Import and function shadow checks, including exception/pattern captures and importer-root walrus/global writes, are conservative across the file. Proven builtins reflection aliases are excluded.","Exact call-shape assertions such as assert_called_once_with can already validate a spec's signature; this policy requires validation during invocation. Autospec does not enforce argument value types.","Passing autospec=True to a Mock constructor merely configures an attribute; it does not activate autospeccing. Use create_autospec instead.","Specs whose function objects escape to aliases, helpers, or attribute access are excluded so altered signatures and descriptor metadata remain explicit contracts. No autofix is offered."],"messageIds":[],"optionsSchema":null,"rationale":"Mock, MagicMock and AsyncMock with spec or spec_set allow incompatible arguments during invocation. Tests that assert only results or call counts can therefore accept a misspelled keyword or stale call shape.","references":[],"remediation":"Use create_autospec(function, spec_set=True), preserving configured results. An async function produces an awaitable mock whose signature is checked when awaited. Retain a reasoned local exception for a deliberately permissive double.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_autospec_for_callable_mock.py","status":"active","summary":"Prefer autospec for specced function mocks called with explicit keywords.","test":"packages/python/tests/rules/test_prefer_autospec_for_callable_mock.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ516","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"class UserStore:\n    async def get(self, key: str) -> User | None:\n        ...\n    async def get_many(self, keys: list[str]) -> list[User]:\n        ...\nasync def load(store: UserStore, keys: list[str]):\n    return await store.get_many(keys)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"bulk-read","outcome":"accept","scenarioId":"read-comprehension","title":"Read the batch after reviewing missing rows and ordering"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"class UserStore:\n    async def get(self, key: str) -> User | None:\n        ...\n    async def get_many(self, keys: list[str]) -> list[User]:\n        ...\nasync def load(store: UserStore, keys: list[str]):\n    return await store.get_many(keys)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"bulk-read-without-fanout","outcome":"accept","scenarioId":"async-fanout","title":"Use bounded bulk reads after reviewing concurrency and failures"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"class UserStore:\n    async def upsert(self, user: UserInput) -> User:\n        ...\n    async def upsert_many(self, users: list[UserInput]) -> list[User]:\n        ...\nasync def save(store: UserStore, users: list[UserInput]):\n    await store.upsert_many(users)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"bulk-upsert","outcome":"accept","scenarioId":"primary","title":"Use the existing bulk command"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"class UserStore:\n    async def upsert(self, user: UserInput) -> User:\n        ...\n    async def upsert_many(self, users: list[UserInput]) -> list[User]:\n        ...\nasync def save(store: UserStore, users: list[UserInput]):\n    return await store.upsert_many(users)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"bulk-write-results","outcome":"accept","scenarioId":"write-comprehension","title":"Return bulk results after reviewing cardinality and failures"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"import asyncio\nclass UserStore:\n    async def get(self, key: str) -> User | None:\n        ...\n    async def get_many(self, keys: list[str]) -> list[User]:\n        ...\nasync def load(store: UserStore, keys: list[str]):\n    return await asyncio.gather(*(store.get(key) for key in keys))\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"eager-read-fanout","outcome":"reject","scenarioId":"async-fanout","title":"Concurrency still schedules one operation per input"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"class UserStore:\n    async def get(self, key: str) -> User | None:\n        ...\n    async def get_many(self, keys: list[str]) -> list[User]:\n        ...\nasync def load(store: UserStore, keys: list[str]):\n    return [await store.get(key) for key in keys]\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"read-comprehension","outcome":"reject","scenarioId":"read-comprehension","title":"Review repeated reads against an available bulk API"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"class UserStore:\n    async def upsert(self, user: UserInput) -> User:\n        ...\n    async def upsert_many(self, users: list[UserInput]) -> list[User]:\n        ...\nasync def save(store: UserStore, users: list[UserInput]):\n    for user in users:\n        await store.upsert(user)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"serial-upserts","outcome":"reject","scenarioId":"primary","title":"Collect writes for an available bulk command"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"class UserStore:\n    async def upsert(self, user: UserInput) -> User:\n        ...\n    async def upsert_many(self, users: list[UserInput]) -> list[User]:\n        ...\nasync def save(store: UserStore, users: list[UserInput]):\n    return [await store.upsert(user) for user in users]\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"write-comprehension","outcome":"reject","scenarioId":"write-comprehension","title":"Review a comprehension of singleton writes"}],"filePatterns":[],"id":"prefer-bulk-upsert","key":"python:prefer-bulk-upsert","languages":["python"],"limitations":["Synchronous for loops require one discarded singleton call. List/dict comprehensions require one synchronous generator and a value consisting solely of the singleton call. Writes use compatible upsert/create/insert/update/delete and corresponding *_many pairs; get uses get_many or get_by_ids. Await mode must match the declared method.","Eager fan-out requires a directly awaited, import-proven stdlib asyncio.gather with one starred list/generator comprehension, a raw async singleton element, no filters, additional calls or keywords, and an async compatible bulk peer. TaskGroup, task factories and opaque scheduling are excluded. Concurrency, failure, cancellation and backpressure require review.","Receivers must resolve to typed parameters or constructor-injected attributes. Signatures require one required input and matching scalar/collection input types. Writes retain matching scalar/collection output types. Read output may be T or T | None/Optional[T] versus list/Sequence[T] or dict/Mapping[K, T], with K matching the input key type. Missing-row placeholders, duplicate keys, cardinality and request order require review.","Annotated first-party imports use the project index; unresolved, inherited, overloaded, decorated, rebound, reassigned and dynamic contracts are excluded, as are explicit metaclasses and class keywords. Unknown backend APIs and dictionary/cache joins do not prove a batch opportunity.","Other result-dependent loops, filtered/nested comprehensions, async streams, loop else clauses, extra effects and per-row exception or transaction blocks are excluded. Batch atomicity and tenant/auth or snapshot behavior require review; this remains a warning without an autofix.","Maintained tests are checked; generated/vendor sources are excluded. A bulk method is not invented when none is declared."],"messageIds":[],"optionsSchema":null,"rationale":"Serial singleton calls and eager coroutine fan-out can repeat database or remote work. A source-declared compatible bulk API identifies an opportunity to review batching, including bulk implementations that repeat backend singleton calls.","references":[],"remediation":"Collect inputs and use the declared bulk API in bounded batches after reviewing missing rows, cardinality, duplicate keys, ordering, failure atomicity, concurrency, cancellation, backpressure, tenant scope, and transaction or snapshot behavior. Preserve deliberately per-row transactions or effects with an exact suppression.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_bulk_upsert.py","status":"active","summary":"Use declared compatible bulk APIs for repeated singleton reads and writes.","test":"packages/python/tests/rules/test_prefer_bulk_upsert.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ013","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/task_store.py","source":"from psycopg.rows import dict_row\n\nasync def load(conn):\n    async with conn.cursor(row_factory=dict_row) as cursor:\n        await cursor.execute(\"SELECT id, state FROM task\")\n        row = await cursor.fetchone()\n        return Task.model_validate(row)\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"dict-row-then-model","outcome":"reject","scenarioId":"primary","title":"Fetched dictionary is immediately converted"},{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"from psycopg.rows import class_row\n\nasync def load(conn):\n    async with conn.cursor(row_factory=class_row(Task)) as cursor:\n        await cursor.execute(\"SELECT id, state FROM task\")\n        return await cursor.fetchone()\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"model-row-factory","outcome":"accept","scenarioId":"primary","title":"Cursor constructs the model directly"}],"filePatterns":[],"id":"prefer-class-row","key":"python:prefer-class-row","languages":["python"],"limitations":["Only maintained production code with a locally bound `.cursor(row_factory=dict_row)` is analyzed.","The fetched value must flow unchanged into `Model(**row)` or `Model.model_validate(row)` in the same scope.","Renamed fields, preprocessing, connection-level factories, dynamic models, and multi-shape cursors are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Folding an immediate model conversion into the row factory removes an intermediate mapping and gives fetches the declared model type.","references":[],"remediation":"Use `row_factory=class_row(Model)` when selected column names map directly to that model; retain `dict_row` for dynamic, derived, connection-wide, or multi-shape results.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_class_row.py","status":"active","summary":"Avoid fetching a Psycopg dictionary row only to construct the same model manually.","test":"packages/python/tests/rules/test_prefer_class_row.py"},{"aliases":[],"autofix":"safe","category":"style","code":"SARJ430","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/intervals.py","source":"def clipped(items, bounds):\n    return [\n        value\n        for item in items\n        if (value := item.clipped(bounds)) is not None\n    ]\n"}],"fixedFiles":[],"focusPath":"app/intervals.py","id":"computed-filtered-list-comprehension","outcome":"accept","scenarioId":"computed-filter","title":"Keep a computed filtered list declarative"},{"expectedCount":1,"files":[{"path":"app/intervals.py","source":"def clipped(items, bounds):\n    result: list[Interval] = []\n    for item in items:\n        value = item.clipped(bounds)\n        if value is not None:\n            result.append(value)\n    return result\n"}],"fixedFiles":[],"focusPath":"app/intervals.py","id":"computed-filtered-list-loop","outcome":"reject","scenarioId":"computed-filter","title":"Compute and filter a list candidate directly"},{"expectedCount":0,"files":[{"path":"app/views.py","source":"def views(rows, labels):\n    return {key: EntryView(id=key, label=labels.get(key)) for key, row in rows}\n"}],"fixedFiles":[],"focusPath":"app/views.py","id":"constructor-dict-comprehension","outcome":"accept","scenarioId":"constructor-dict","title":"Keep constructor-valued dictionaries declarative"},{"expectedCount":1,"files":[{"path":"app/views.py","source":"def views(rows, labels):\n    result = {}\n    for key, row in rows:\n        result[key] = EntryView(id=key, label=labels.get(key))\n    return result\n"}],"fixedFiles":[],"focusPath":"app/views.py","id":"constructor-dict-projection-loop","outcome":"reject","scenarioId":"constructor-dict","title":"Build constructor-valued dictionaries directly"},{"expectedCount":1,"files":[{"path":"app/capacity.py","source":"def organization_caps(rows):\n    caps: dict[str, int] = {}\n    for key, row in rows:\n        caps[key] = row.organization_cap\n    return caps\n"}],"fixedFiles":[],"focusPath":"app/capacity.py","id":"derived-dict-projection-loop","outcome":"reject","scenarioId":"primary","title":"Build a projected dictionary directly"},{"expectedCount":0,"files":[{"path":"app/capacity.py","source":"def organization_caps(rows):\n    return {key: row.organization_cap for key, row in rows}\n"}],"fixedFiles":[],"focusPath":"app/capacity.py","id":"direct-dict-comprehension","outcome":"accept","scenarioId":"primary","title":"Keep the collection projection declarative"}],"filePatterns":[],"id":"prefer-collection-comprehension","key":"python:prefer-collection-comprehension","languages":["python"],"limitations":["Safe fixes apply only error-level candidates in undecorated module functions with no parameters and exactly initialization, one loop over literal primitive elements or correctly unpacked literal pairs, and return. List/set elements and dictionary keys/values must be primitive literals, bound loop names, or tuples composed of them. Unknown iterators, callbacks, properties, overloads, reachable errors, extra statements and advisory candidates remain review-only; the initialized binding and annotation are retained.","Supported list/set builders and dictionary builders with stable loop-local-name or literal keys block as errors. Dictionary attribute/descriptor keys remain advisory because assignment evaluates value before key while a comprehension evaluates key before value.","Only function-local, adjacent empty initializers and one synchronous single-purpose loop are checked.","The rule fills gaps left by Ruff PERF401, PERF403, and FURB142: derived dict projections, constructor-valued and filtered dict projections, destructured list projections, narrowly filtered computed list candidates, and filtered set builders.","Comments, loop-target leakage, try blocks, aliases, complex projections, and replacements that do not fit a 120-column line or a simple formatter-style multiline comprehension are excluded. Namespace observers, explicit inspect/sys frame access, and collections referenced by nested functions are excluded. Dynamic function aliases and helper-hidden frame reads are not resolved. Only the literal-domain subset is autofixed; other dictionary key/value evaluation order remains review-only.","Attribute projections can invoke properties or descriptors, so reviewers should keep the loop when the relative order of those reads is observable.","Computed list candidates are limited to one local assignment, a truthiness or None filter, and one bounded projection. Validation, logging, exceptions, multiple guards, and secondary mutation remain imperative."],"messageIds":[],"optionsSchema":null,"rationale":"An empty collection followed by a loop whose only behavior is one projection or filtered insertion spreads a declarative map/filter across mutable scaffolding.","references":[],"remediation":"Build the fresh collection with one dict, list, or set comprehension. When a list candidate must be computed once and reused in the element, bind it in the filter with :=. Keep the loop when mutation is incremental, evaluation order is observable, or the imperative form carries additional behavior.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_collection_comprehension.py","status":"active","summary":"Single-purpose fresh collection builder loop — prefer a direct comprehension.","test":"packages/python/tests/rules/test_prefer_collection_comprehension.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ566","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/publish.py","source":"from typing import Annotated\nimport httpx\nfrom pydantic import Field\nfrom pydantic_settings import BaseSettings\nclass Settings(BaseSettings):\n    cloudflare_account_id: Annotated[str, Field(min_length=32, max_length=32, pattern=r'^[a-fA-F0-9]+$')]\ndef publish(settings: Settings):\n    return httpx.post(f'https://api.cloudflare.com/client/v4/accounts/{settings.cloudflare_account_id}/workers/scripts')\ndef list_scripts(settings: Settings):\n    return httpx.get(f'https://api.cloudflare.com/client/v4/accounts/{settings.cloudflare_account_id}/workers/scripts')\nsettings = Settings()\npublish(settings)\nlist_scripts(settings)\n"}],"fixedFiles":[],"focusPath":"app/publish.py","id":"configured-account-request","outcome":"accept","scenarioId":"primary","title":"Construct one Pydantic Settings object and inject it into both consumers"},{"expectedCount":1,"files":[{"path":"app/publish.py","source":"import httpx\nACCOUNT = '0123456789abcdef0123456789abcdef'\nhttpx.get(f'https://api.cloudflare.com/client/v4/accounts/{ACCOUNT}/workers/scripts')\n"}],"fixedFiles":[],"focusPath":"app/publish.py","id":"literal-account-request","outcome":"reject","scenarioId":"primary","title":"The outgoing API request is bound to a literal account"}],"filePatterns":[],"id":"prefer-configured-cloud-scope","key":"python:prefer-configured-cloud-scope","languages":["python"],"limitations":["Initial scope is an explicit 32-digit hexadecimal Cloudflare account in an HTTPS v4 account-management URL passed to import-proven requests/httpx module functions or urllib.request.urlopen. Imported aliases, literal concatenation, f-strings and stable direct same-file assignments are followed.","Reports each originating account literal once. Unknown account values, arbitrary service endpoints, worker names, UUIDs, unused strings and validation allowlists are not inferred. Settings and environment expressions are left to their existing validation controls; absence of a finding does not prove validation or one settings instance. Nested settings and separate runtime/security boundaries remain valid.","Client instances, injected request functions, format calls, environment fallbacks and cross-file values are not traced. Conditional/reassigned bindings, generated sources, tests and test support are excluded. Literal expansion stops at 16 nested expressions, 128 fragments or 8 KiB of text. A deliberately fixed operational target can retain an exact-code exception; there is no automatic rewrite."],"messageIds":[],"optionsSchema":null,"rationale":"A literal account in a management API request binds the caller to one deployment. Reusing the script in another environment can target the original account even when its credentials change.","references":[],"remediation":"Load the account ID into one shared Pydantic/Pydantic Settings object at startup and inject that same validated object into consumers. Fail before remote effects when required configuration is absent or invalid; preserve independent security allowlists and existing optional defaults, without a production account fallback. Reading an environment variable alone is not validation.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_configured_cloud_scope.py","status":"active","summary":"Read Cloudflare account targets from validated deployment configuration.","test":"packages/python/tests/rules/test_prefer_configured_cloud_scope.py"},{"aliases":[],"autofix":"safe","category":"security","code":"SARJ011","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"auth.py","source":"import hmac\n\ndef authenticated(request, settings):\n    provided = request.headers[\"X-API-Key\"]\n    return hmac.compare_digest(provided.encode(\"utf-8\"), settings.api_key.encode(\"utf-8\"))\n"}],"fixedFiles":[],"focusPath":"auth.py","id":"request-api-key-constant-time","outcome":"accept","scenarioId":"primary","title":"Request API key uses constant-time comparison"},{"expectedCount":1,"files":[{"path":"auth.py","source":"def authenticated(request, settings):\n    return request.headers[\"X-API-Key\"] == settings.api_key\n"}],"fixedFiles":[],"focusPath":"auth.py","id":"request-api-key-equality","outcome":"reject","scenarioId":"primary","title":"Request API key uses ordinary equality"}],"filePatterns":[],"id":"prefer-constant-time-secret-compare","key":"python:prefer-constant-time-secret-compare","languages":["python"],"limitations":["The diagnostic requires evidence of an externally supplied credential and configured or stored secret state.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","Adjacent equality pairs, literal encode normalization, and unambiguous same-scope aliases from headers, cookies, settings, or environment are traced.","Generated files, tests, test-support modules, equality methods, public sentinels, and type-like values are excluded.","`compare_digest` requires compatible ASCII string or bytes-like operands and may still reveal type or length.","Automatic fixes require a closed private nested synchronous helper whose two required local operands are forwarded directly from its containing function parameters. Three existing defaults must already capture canonical compare_digest, type and bytes; every closed call omits those defaults. Only captured exact-type guards, the equality return and False fallback are allowed; unknown calls, attribute/subscript reads, imports, mutations, frame escapes, comments and type-parameter shadows abstain. No bindings, captures or imports are added; the intended timing behavior changes."],"messageIds":[],"optionsSchema":null,"rationale":"Ordinary equality may short-circuit based on matching content when attacker-controlled credentials are checked against configured secret state.","references":[],"remediation":"For opaque tokens and MACs, normalize both operands to the same supported type and use `hmac.compare_digest`; for passwords, use the password-hashing library's verification API.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_constant_time_secret_compare.py","status":"active","summary":"Externally supplied authenticators are compared with timing-sensitive equality.","test":"packages/python/tests/rules/test_prefer_constant_time_secret_compare.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ565","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"playback.py","source":"import asyncio\nclass Playback:\n    def __init__(self) -> None:\n        self._observers: dict[str, asyncio.Task[None]] = {}\n        self._playbacks: dict[str, asyncio.Task[bool]] = {}\n    def start(self, key: str, observer: asyncio.Task[None], playback: asyncio.Task[bool]) -> None:\n        self._observers[key] = observer\n        self._playbacks[key] = playback\n    def finish(self, key: str) -> None:\n        self._observers.pop(key, None)\n        self._playbacks.pop(key, None)\n"}],"fixedFiles":[],"focusPath":"playback.py","id":"coordinated-playback-bookkeeping","outcome":"reject","scenarioId":"primary","title":"Observer and playback entries repeat paired bookkeeping"},{"expectedCount":0,"files":[{"path":"playback.py","source":"import asyncio\nfrom dataclasses import dataclass\n@dataclass(frozen=True)\nclass ActivePlayback:\n    observer: asyncio.Task[None]\n    playback: asyncio.Task[bool]\nclass Playback:\n    def __init__(self) -> None:\n        self._active: dict[str, ActivePlayback] = {}\n    def start(self, key: str, observer: asyncio.Task[None], playback: asyncio.Task[bool]) -> None:\n        self._active[key] = ActivePlayback(observer, playback)\n    def finish(self, key: str) -> None:\n        self._active.pop(key, None)\n"}],"fixedFiles":[],"focusPath":"playback.py","id":"one-typed-playback-owner","outcome":"accept","scenarioId":"primary","title":"One typed entry retains both task identities"}],"filePatterns":[],"id":"prefer-coordinated-state-owner","key":"python:prefer-coordinated-state-owner","languages":["python"],"limitations":["Warning only. Names and field counts never establish a finding. Each reported group requires consecutive same-key assignments and consecutive same-key removals in different function scopes. Every observed per-key mutation of every member inside the class must participate. Writers and readers outside that receiver scope are not inferred. Maximal groups are preferred, and disjoint groups can report separately without repeating fields.","Distinct private receiver fields must be initialized directly to empty dict literals or unshadowed dict() calls in a synchronous __init__, with builtin dict annotations and the same unshadowed str, int or bytes key type. Annotations may be inline or unique annotation-only class declarations. Keys resolve to the same unchanged parameter with that explicit type. Receivers resolve by binding, including captures in nested callbacks. Shadowed parameters are distinct; ambiguous rebinding abstains. Private names use lexical class mangling, including explicitly mangled aliases; mangled names over 255 characters abstain.","Subscript writes may insert or overwrite; key existence is not inferred. Only direct subscript assignment/deletion and direct pop(key[, None]) statements are mutations. Subscript reads and get/items/keys/values/copy calls are inventoried. Aliases/escapes, escaped bound methods, class/state reflection including implicit super()/__class__ access, replacement, augmented mutation, other methods, unpaired writes, unknown bases/decorators, stored class attributes, nested classes, generic scopes and ambiguous namespaces abstain. A proven builtin object base and import-proven typing.final class/method decorators are allowed. Classes over 4096 nodes, 16 candidate fields or 256 distinct mutation groups, and test/generated/malformed files, are excluded.","The rule does not prove atomic updates, equal key sets, reachable paths, cross-module ownership or runtime value types. Calls can raise and async callbacks can interleave. Explicit await/yield or implicit async-comprehension suspension inside an operation, or any intervening statement, prevents a group. Helper-mediated coordination and independently removed entries remain unsupported. Mirrored indexes or protocol-mandated stores can legitimately match; use a reasoned exact-code suppression after review.","No automatic owner migration is safe: construction timing, identity, serializers, callback capture, exceptions and cancellation require scoped review. Existing model-union and task-cancellation rules retain their own contracts. This advisory never authorizes deleting state or cleanup."],"messageIds":[],"optionsSchema":null,"rationale":"Repeated same-key writes and removals in separate dictionaries distribute a possible entry-lifetime invariant across writers. One typed owner can hold the coordinated values. This is structural evidence for review, not proof of a defect or a shared domain lifetime.","references":[],"remediation":"Review the reported fields, writers and readers. If entries have one lifetime, use one dictionary of typed owner records retaining each value's annotation. Preserve task identities, start/stop order, exception observation, cancellation and joining. Keep independent outcome reporting, historical observations, partial results and independently removed entries separate.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_coordinated_state_owner.py","status":"active","summary":"Review dictionaries with repeated same-key writes and removals for one typed owner.","test":"packages/python/tests/rules/test_prefer_coordinated_state_owner.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ494","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/batches.py","source":"from pydantic import BaseModel\nclass Item(BaseModel):\n    value: int\ndef load(rows: list[object]):\n    return [Item.model_validate(row) for row in rows]\n"}],"fixedFiles":[],"focusPath":"app/batches.py","id":"homogeneous-validation-comprehension","outcome":"reject","scenarioId":"primary","title":"Declare collection validation once"},{"expectedCount":0,"files":[{"path":"app/batches.py","source":"from pydantic import BaseModel, RootModel\nclass Item(BaseModel):\n    value: int\nclass Items(RootModel[list[Item]]):\n    pass\ndef load(rows: list[object]):\n    return Items.model_validate(rows).root\n"}],"fixedFiles":[],"focusPath":"app/batches.py","id":"named-batch-validation","outcome":"accept","scenarioId":"primary","title":"A named collection schema owns the batch"}],"filePatterns":[],"id":"prefer-declarative-batch-validation","key":"python:prefer-declarative-batch-validation","languages":["python"],"limitations":["Only locally proven, undecorated Pydantic BaseModel classes, local inheritance and unique aliases are recognized; imported application models and dynamic ancestry remain unreported.","Inputs must be a literal collection, an unambiguous list/tuple/Sequence declaration, or materialized rows from a locally bound Psycopg dict_row/class_row cursor. Unknown and streaming iterables are excluded.","Comprehensions must have one synchronous generator and no filter. Loops require an adjacent fresh list and one validation-only try block with a proven ValidationError handler that only logs and drops a record.","Changing models or context, preprocessing, rejection reports, secondary effects, observable loop locals, and validation options other than literal from_attributes=True are excluded.","Simple append loops remain owned by Ruff PERF401; its resulting model-validation comprehension is checked here. Proven SARJ013 cursor conversions take precedence over this warning.","There is no autofix: collection validation can change error aggregation, record omission, logging, evaluation after the first invalid record, and failure timing. Tests, generated files and vendor sources are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Per-record validation comprehensions and catch/log/drop builders spread a collection contract across repeated model calls and mutable error-handling scaffolding. A named collection schema makes the batch's error policy explicit.","references":[],"remediation":"Use class_row(Model) for unchanged Psycopg model rows, or a named RootModel[list[Model]] for collection validation. Declare whether invalid records reject the batch or are intentionally omitted (for example with OnErrorOmit). Preserve required logging and rejection reports.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_declarative_batch_validation.py","status":"active","summary":"Validate a homogeneous Pydantic batch with a declared collection contract.","test":"packages/python/tests/rules/test_prefer_declarative_batch_validation.py"},{"aliases":[],"autofix":"none","category":"style","code":"SARJ529","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app.py","source":"import importlib\nplugin = importlib.import_module('plugins.billing')\n"}],"fixedFiles":[],"focusPath":"app.py","id":"literal-eager-loader","outcome":"reject","scenarioId":"primary","title":"A known eager dependency is loaded dynamically"},{"expectedCount":0,"files":[{"path":"app.py","source":"import plugins.billing as plugin\n"}],"fixedFiles":[],"focusPath":"app.py","id":"static-module-import","outcome":"accept","scenarioId":"primary","title":"A static import preserves the module binding"}],"filePatterns":[],"id":"prefer-direct-import-module","key":"python:prefer-direct-import-module","languages":["python"],"limitations":["Checks direct module-level assignments and expression statements calling the proven stdlib import_module function, through qualified or directly imported names, with one literal absolute module name.","Function/class bodies, conditional or guarded imports, variable module names, relative imports, callback references, shadowed or mutated providers and generated code are excluded. Static import syntax must be valid for every module-name segment.","This is a warning without automatic rewriting: moving an import can change execution order, optional-dependency behavior or cycle handling. The rule does not prefer one spelling of genuinely dynamic import_module calls."],"messageIds":[],"optionsSchema":null,"rationale":"Static imports expose known dependencies to readers and tooling without dynamic loader indirection.","references":[],"remediation":"Replace a fixed eager module load with a module-level import, preserving its binding: plugin = importlib.import_module('plugins.billing') becomes import plugins.billing as plugin. Use import plugins.billing when callers use the qualified module name; avoid a redundant alias for a single-component module. Keep dynamic loading when module selection or import timing requires it.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_direct_import_module.py","status":"active","summary":"Prefer static imports for eager module loads with a literal module name.","test":"packages/python/tests/rules/test_prefer_direct_import_module.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ510","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/models.py","source":"from typing import Literal, Self\nfrom pydantic import BaseModel, model_validator\nclass Delivery(BaseModel):\n    kind: Literal['email', 'pickup']\n    address: str | None = None\n    @model_validator(mode='after')\n    def validate_address(self) -> Self:\n        if self.kind == 'email' and self.address is None:\n            raise ValueError('Email requires an address')\n        return self\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"literal-tag-dependent-payload","outcome":"reject","scenarioId":"primary","title":"An after validator encodes a field dependency that its type cannot express"},{"expectedCount":0,"files":[{"path":"app/access.py","source":"from dataclasses import dataclass, field\nfrom typing import Literal\n@dataclass(frozen=True, slots=True)\nclass TeamAccess:\n    scope: Literal['team'] = field(default='team', init=False)\n    team_id: str\n@dataclass(frozen=True, slots=True)\nclass GlobalAccess:\n    scope: Literal['global'] = field(default='global', init=False)\n    group: str\ntype Access = TeamAccess | GlobalAccess\n"}],"fixedFiles":[],"focusPath":"app/access.py","id":"literal-tags-preserve-constructor-calls","outcome":"accept","scenarioId":"explicit-data-discriminator","title":"Explicit tags preserve positional constructor calls with init=False"},{"expectedCount":0,"files":[{"path":"app/outcomes.py","source":"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\nclass Success[T](BaseModel):\n    kind: Literal['success'] = 'success'\n    value: T\nclass Error[E](BaseModel):\n    kind: Literal['error'] = 'error'\n    error: E\ntype Result[T, E] = Annotated[Success[T] | Error[E], Field(discriminator='kind')]\n"}],"fixedFiles":[],"focusPath":"app/outcomes.py","id":"tagged-generic-result","outcome":"accept","scenarioId":"generic-outcome-discriminator","title":"A tagged generic result supports field narrowing and codec validation"},{"expectedCount":1,"files":[{"path":"app/access.py","source":"from dataclasses import dataclass\n@dataclass(frozen=True, slots=True)\nclass TeamAccess:\n    team_id: str\n@dataclass(frozen=True, slots=True)\nclass GlobalAccess:\n    group: str\ntype Access = TeamAccess | GlobalAccess\n"}],"fixedFiles":[],"focusPath":"app/access.py","id":"untagged-data-variants","outcome":"reject","scenarioId":"explicit-data-discriminator","title":"Data variants lack a common explicit tag"},{"expectedCount":1,"files":[{"path":"app/outcomes.py","source":"from pydantic import BaseModel\nclass Success[T](BaseModel):\n    value: T\nclass Error[E](BaseModel):\n    error: E\ntype Result[T, E] = Success[T] | Error[E]\n"}],"fixedFiles":[],"focusPath":"app/outcomes.py","id":"untagged-generic-result","outcome":"reject","scenarioId":"generic-outcome-discriminator","title":"Generic data wrappers still need an explicit discriminator"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from typing import Annotated, Literal\nfrom pydantic import BaseModel, Field\nclass EmailDelivery(BaseModel):\n    kind: Literal['email']\n    address: str\nclass PickupDelivery(BaseModel):\n    kind: Literal['pickup']\n    address: str | None = None\ntype Delivery = Annotated[EmailDelivery | PickupDelivery, Field(discriminator='kind')]\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"variants-preserve-other-accepted-states","outcome":"accept","scenarioId":"primary","title":"Only the email variant requires an address; pickup preserves its optional address"}],"filePatterns":[],"id":"prefer-discriminated-model-union","key":"python:prefer-discriminated-model-union","languages":["python"],"limitations":["Nullable outcome records without a proven validator dependency are reviewed separately by prefer-outcome-variants (SARJ571).","Declared module-level unions of at least two source-visible direct dataclasses, Pydantic BaseModels or TypedDicts require a shared disjoint Literal field. PEP 604, typing Union, Optional, Annotated, forward references and bounded local aliases are supported; a None branch is allowed.","The explicit-tag policy is a modeling preference: nominal variants already support isinstance narrowing. Explicitly specialized PEP 695 records and direct generic union declarations support ordinary payload parameters. Parameter-dependent tag domains remain uncertain; bounded, defaulted or variadic parameters, bare generic records and specialization through another generic union alias abstain. Scalar/container unions, inline annotations, inherited variants, behavior classes, unresolved variants and dynamic or ambiguous namespaces are excluded. Imported tag aliases are not resolved; an explicit literal field paired with an unresolved imported or local class domain abstains, as do opaque explicit Literal domains. Qualified imported domains and unsupported computed aliases remain uncertain rather than proving a missing tag.","String, bytes, integer, Boolean and None tag values are supported. Boolean and integer Literal values retain their distinct static types; use identity checks when equality would conflate their runtime values. A bounded hierarchy of shared literal fields may distinguish nested groups; overlapping tags without a further discriminator still fail. Nested Literal domains and transparent Final, ReadOnly, Annotated and local alias wrappers are supported. TypedDict tags must be required; dataclass ClassVar tags must have a value. Direct import-proven InitVar annotations without defaults are constructor inputs rather than instance tags; quoted, wrapped, locally aliased or defaulted InitVar forms remain uncertain. Import-proven typing final decorators preserve record recognition. Explicit Pydantic callable Discriminators, bounded stable reusable metadata, single-value plain local class namespaces, literal immutable tuple projections and imported pydantic.types constructors are respected; source-proven string field names, including local bindings and concatenation, still need Literal fields. Source-proven string discriminator writes through stable aliases invalidate callable coverage; opaque mutation, helper escapes and excessive alias graphs abstain. Module bindings are independent of unrelated local parameters and their attribute writes; class annotation shadows and global rebinding remain excluded. Other runtime configuration and default correctness remain separate contracts. No automatic field addition is safe for every constructor or serializer.","For nullable-field dependencies, only stable direct module-level import-proven Pydantic BaseModel subclasses with pure direct after model validators are inspected.","Bounded paths may contain finite tag comparisons or literal-container membership, None identity tests, Boolean combinations, nested guards and returns of the same instance. Every rejection region must prove one tag-dependent nullable-field requirement; joint missing-field or independent-tag-axis conditions abstain.","Tags must use imported Literal domains, optionally composed through direct unions, with at least two distinct homogeneous string, integer or Boolean values, optionally including an explicit None state. Mixed primitive families, bool/int equality collisions, open or computed tags abstain. Payloads must explicitly include None in a union, imported Union or Optional.","Every non-null payload member must have a source-proven inhabited built-in scalar or container type; opaque aliases, custom types and None-only or bottom-type unions are excluded.","A field required by every admitted tag value is a common field constraint rather than evidence for a discriminated union and is excluded.","Unsupported Field validation constraints cannot prove non-null payload inhabitance; defaults, aliases and passive metadata remain eligible. Literal exception messages or local assignments of those messages are supported with import-proven builtin rejection types.","Opaque helpers, effectful validators, inherited models, model mutation or escape, ambiguous namespaces, tests and generated files are excluded. The analyzer bounds validator nodes, Boolean depth, paths and rejection regions; complex forms abstain.","This error requires explicit data modeling; it does not require absent payloads in other variants or change external compatibility automatically."],"messageIds":[],"optionsSchema":null,"rationale":"An explicit shared tag identifies data variants and supports field-based narrowing. A nullable field remains statically nullable even when an after validator requires it for a literal tag. A union of models with distinct Literal tags carries that dependency in the type and supports safe narrowing without repeating runtime guards.","references":[],"remediation":"Give every data variant a common field with disjoint Literal values. For dataclasses, field(default=..., init=False) can preserve existing constructor calls; review serialized contracts. Consider a discriminated union of model variants. Make the payload non-nullable only in variants proven to reject None, and preserve every other accepted field combination, defaults, aliases and wire shape. Independent optional fields can remain optional within each variant. For recoverable success/error contracts, reuse the established Result[T, E] with tagged success and error wrappers, a finite domain error code and only the context callers need. Validate concrete serialized payloads with the project's codec; keep message formatting at the consumer.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_discriminated_model_union.py","status":"active","summary":"Give data unions explicit Literal discriminators and express tag-dependent payloads as variants.","test":"packages/python/tests/rules/test_prefer_discriminated_model_union.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ569","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"from httpx import AsyncClient\nfrom typing import Protocol\nclass ExecutionPort(Protocol):\n    def run(self): ...\n    def cancel(self): ...\nclass Execution(ExecutionPort):\n    def __init__(self, client: AsyncClient, state):\n        self.client = client\n        self.state = state\n        self.cancelled = False\n    def run(self):\n        return self.client.send(self.state)\n    def cancel(self):\n        self.state = None\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"declared-domain","outcome":"accept","scenarioId":"primary","title":"The contract is explicit"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"from httpx import AsyncClient\nclass Execution:\n    def __init__(self, client: AsyncClient, state):\n        self.client = client\n        self.state = state\n        self.cancelled = False\n    def run(self):\n        return self.client.send(self.state)\n    def cancel(self):\n        self.state = None\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"undeclared-domain","outcome":"reject","scenarioId":"primary","title":"The contract is implicit"}],"filePatterns":[],"id":"prefer-explicit-class-contract","key":"python:prefer-explicit-class-contract","languages":["python"],"limitations":["Reviews direct public module classes with no base other than builtin object, at least two public operations, at least two constructor-owned state fields, fresh lifecycle/state initialization and a retained import-proven non-stdlib collaborator read by an operation. Configuration-only classes and constructor-only argument holders do not establish an execution boundary. Names alone do not trigger this advice.","Existing bases, decorators, dataclasses and records, stateless utilities, nested classes, test doubles and generated code are excluded. A final class is an explicit concrete contract; this policy does not mandate inheritance for all Python classes.","No base class or final decorator is added automatically because real consumers, subclassing and framework registration must be reviewed."],"messageIds":[],"optionsSchema":null,"rationale":"A class coordinating mutable state and multiple operations exposes a concrete implementation without declaring its substitution or inheritance contract.","references":[],"remediation":"Declare a focused shared ABC or Protocol for the execution behavior and type consumers against it. If the class is intentionally concrete with no substitution boundary, declare it final instead of inventing an unused abstraction.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_explicit_class_contract.py","status":"active","summary":"Declare a contract for a stateful behavior class.","test":"packages/python/tests/rules/test_prefer_explicit_class_contract.py"},{"aliases":[],"autofix":"safe","category":"style","code":"SARJ068","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"src/render.py","source":"PREFIX = \"Question: \"\ndef render(value: str) -> str:\n    return f\"{PREFIX}{value}\"\n"}],"fixedFiles":[],"focusPath":"src/render.py","id":"formatted-literal-prefix","outcome":"accept","scenarioId":"literal-prefix","title":"The prefix and value share one f-string"},{"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"},{"expectedCount":1,"files":[{"path":"src/render.py","source":"PREFIX = \"Question: \"\ndef render(value: str) -> str:\n    return PREFIX + value\n"}],"fixedFiles":[],"focusPath":"src/render.py","id":"named-literal-prefix","outcome":"reject","scenarioId":"literal-prefix","title":"A literal prefix still participates in interpolation"}],"filePatterns":[],"id":"prefer-fstring-over-concat","key":"python:prefer-fstring-over-concat","languages":["python"],"limitations":["Safe fixes require every runtime operand to be an explicit unshadowed builtin str, repr or ascii conversion of a proven exact native primitive. Bare strings, subclasses, custom operators and formatting contracts remain review-only.","Runtime operands must have concrete string evidence.","Unique literal-string bindings and aliases supply surrounding text, including module prefixes. Rebinding, global/nonlocal writers, conditional assignments, imported values, and unresolved scopes are not inferred.","String evidence alone does not prove custom concatenation and formatting equivalence; only exact-native-primitive builtin-conversion subsets are fixed.","Logging, source and configuration fragments, generated headers, common direct SQL and regular-expression forms, lazy strings, ORM expressions, generated files, long literals, and punctuation-oriented fragment composition are excluded.","The policy is stylistic and intentionally leaves URL, path, protocol and declarative fragments alone. Stable aliases of join-built strings retain the fragment-composition exclusion; unresolved or rebound values are not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"For prose-like strings, f-strings keep interpolated values and surrounding text visible together.","references":[],"remediation":"Use one f-string when it preserves the required conversion and operator semantics. Preserve an explicit `str(value)` conversion as `{value!s}` and `repr(value)` as `{value!r}` when those conversion semantics matter. Retain required custom concatenation or formatting behavior; a `str` annotation alone does not prove equivalence.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_fstring_over_concat.py","status":"active","summary":"Prefer f-strings for short human-readable interpolation when they make the result clearer.","test":"packages/python/tests/rules/test_prefer_fstring_over_concat.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ532","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/settings.py","source":"from pydantic_settings import BaseSettings\nclass Settings(BaseSettings):\n    embedder_url: str\n    embedder_token: str\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"flat-component-settings","outcome":"reject","scenarioId":"primary","title":"Related settings expose a repeated component namespace"},{"expectedCount":0,"files":[{"path":"app/settings.py","source":"from pydantic import Field\nfrom pydantic_settings import BaseSettings, SettingsConfigDict\nclass EmbedderSettings(BaseSettings):\n    model_config = SettingsConfigDict(env_prefix='EMBEDDER_')\n    url: str\n    token: str\nclass Settings(BaseSettings):\n    embedder: EmbedderSettings = Field(default_factory=EmbedderSettings)\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"nested-component-settings","outcome":"accept","scenarioId":"primary","title":"The component owns its settings and environment prefix"}],"filePatterns":[],"id":"prefer-grouped-settings","key":"python:prefer-grouped-settings","languages":["python"],"limitations":["Only source-proven Pydantic BaseSettings classes and directly owned public fields are checked. Two distinct fields must share the first complete snake-case namespace token; character prefixes, environment aliases, inherited fields and unrelated data classes do not establish a group.","Boolean, expectation and quantitative modifiers such as is, has, use, expected, min, max, num, total and default do not establish component namespaces. A prefix already present as a field is left for explicit API review.","ClassVar, Final, private attributes, model_config, computed fields, tests, generated code, unknown decorators and unresolved bases are excluded. Each namespace produces one error, anchored at its first field; an exact class or first-field suppression is honored.","Source-proven nested BaseSettings fields do not count as flat candidates. A source-proven nested child already groups a namespace when its parent field has the same first complete Python namespace token, or when its public directly owned fields share that prefix apart from bare fields that are not other source-proven nested settings objects. A competing namespace or nested settings component under a different parent namespace retains subgroup findings. Class names alone, unknown annotations and unknown parent types do not establish ownership.","There is no automatic rewrite: nesting may change environment-variable decoding, aliases, source precedence and consumer access paths."],"messageIds":[],"optionsSchema":null,"rationale":"Repeated namespace prefixes flatten one component's configuration across the parent settings API. A focused settings object gives that component an explicit boundary.","references":[],"remediation":"Move the related fields into a focused nested settings object, normally constructed with Field(default_factory=...). Preserve existing environment aliases, prefixes, validation, defaults and source precedence, and verify real environment loading before changing the settings API.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_grouped_settings.py","status":"active","summary":"Group settings fields that share a Python namespace prefix into a nested settings object.","test":"packages/python/tests/rules/test_prefer_grouped_settings.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ096","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"settings.py","source":"if voice_enabled:\n    TOOL_LABELS = dict.fromkeys((\"call\", \"transfer\"), \"Voice\")\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"conditional-module-mapping","outcome":"reject","scenarioId":"conditional-factory","title":"Conditional dictionary factory needs constant intent"},{"expectedCount":0,"files":[{"path":"settings.py","source":"from typing import Final\n\nif voice_enabled:\n    TOOL_LABELS: Final[dict[str, str]] = dict.fromkeys((\"call\", \"transfer\"), \"Voice\")\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"final-conditional-module-mapping","outcome":"accept","scenarioId":"conditional-factory","title":"Conditional dictionary factory declares a Final binding"},{"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 typing import Final\n\nROLE_LABELS: Final[dict[str, str]] = {\"admin\": \"Administrator\"}\n"}],"fixedFiles":[],"focusPath":"settings.py","id":"immutable-module-mapping","outcome":"accept","scenarioId":"keyed-values","title":"Final dictionary binding 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.","Direct declarations and declarations in module-level if/try suites are checked; repeated bindings, loops, functions, and class bodies are excluded.","Final has no runtime enforcement and permits item mutation on collections; tuple and frozenset are shallow.","Changing a list or set to an immutable type can affect consumers, serialization, and concrete API methods.","Test, test-support, and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Tuple and frozenset protect collection membership. Final makes a collection's constant binding explicit without changing its runtime type, identity, serialization, or concrete collection API.","references":[],"remediation":"When the concrete list or set API is not part of the contract, use a tuple for ordered values or a frozenset for membership. Preserve a concrete list or set contract with Final[list[T]] or Final[set[T]], and a dictionary contract with Final[dict[K, V]]. Final prevents reassignment in type-checked code but does not freeze contents. Use MappingProxyType when runtime mapping write protection is required.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_immutable_module_constant.py","status":"active","summary":"Module collection constants should use native immutable containers or declare their bindings as Final.","test":"packages/python/tests/rules/test_prefer_immutable_module_constant.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ445","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_adapter.py","source":"import pytest\ndef unavailable_client():\n    raise ConnectionError\ndef test_constructor_failure():\n    with pytest.raises(AdapterUnavailable):\n        open_adapter(client_factory=unavailable_client)\n"}],"fixedFiles":[],"focusPath":"tests/test_adapter.py","id":"injected-constructor-failure","outcome":"accept","scenarioId":"constructor-failure","title":"Inject a factory to exercise constructor failure"},{"expectedCount":0,"files":[{"path":"tests/test_checkout.py","source":"def test_checkout(recording_gateway):\n    service = CheckoutService(payment_gateway=recording_gateway)\n    assert service.run() == 'paid'\n"}],"fixedFiles":[],"focusPath":"tests/test_checkout.py","id":"injected-recording-collaborator","outcome":"accept","scenarioId":"primary","title":"Test injects a purpose-built collaborator"},{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"import pytest\ndef test_service():\n    patcher = pytest.ＭｏｎｋｅｙＰａｔｃｈ()\n    patcher.setattr(service, 'clock', fake)\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"native-monkeypatch-factory","outcome":"reject","scenarioId":"native-symbol-binding","title":"A normalized pytest factory replaces a dependency"},{"expectedCount":0,"files":[{"path":"tests/test_service.py","source":"import pytest\ndef test_service():\n    patcher = pytest.ＭｏｎｋｅｙＰａｔｃｈ()\n    patcher.setenv('MODE', 'test')\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"native-process-state","outcome":"accept","scenarioId":"native-symbol-binding","title":"Process environment operations remain excluded"},{"expectedCount":1,"files":[{"path":"tests/test_adapter.py","source":"import pytest\nfrom unittest.mock import patch\ndef test_constructor_failure():\n    with patch('transport.Client', side_effect=ConnectionError):\n        with pytest.raises(AdapterUnavailable):\n            open_adapter()\n"}],"fixedFiles":[],"focusPath":"tests/test_adapter.py","id":"patched-constructor-failure","outcome":"reject","scenarioId":"constructor-failure","title":"Avoid replacing the SDK constructor lookup"},{"expectedCount":1,"files":[{"path":"tests/test_checkout.py","source":"def test_checkout(monkeypatch):\n    monkeypatch.setattr(checkout, 'charge_card', lambda _card: True)\n    assert checkout.run() == 'paid'\n"}],"fixedFiles":[],"focusPath":"tests/test_checkout.py","id":"patched-first-party-collaborator","outcome":"reject","scenarioId":"primary","title":"Test replaces an application collaborator"}],"filePatterns":[],"id":"prefer-injected-dependency-over-monkeypatch","key":"python:prefer-injected-dependency-over-monkeypatch","languages":["python"],"limitations":["Only maintained test paths are analyzed; generated files and production helpers are excluded.","The rule recognizes pytest monkeypatch fixtures, unambiguous local MonkeyPatch constructors and context handles, and statically resolved unittest.mock or pytest-mock patch APIs.","Environment, mapping, import-path, and working-directory mutations are intentionally allowed.","Untyped handles are recognized only on pytest tests or fixtures; directly parametrized values are excluded.","A nested function that captures a monkeypatch handle from an outer scope is not inferred.","Instance ownership, collaborator contracts, and active consumer lookups are not inferred; justified scoped patches need a local suppression."],"messageIds":[],"optionsSchema":null,"rationale":"Attribute patching hides collaborators and configuration behind ambient module or object state, coupling tests to lookup locations instead of an explicit boundary. Replacing a scoped patch with bare assignment does not fix that coupling and loses automatic restoration. Pytest monkeypatch remains appropriate for reversible process state such as environment variables and the working directory.","references":[],"remediation":"Pass the collaborator, settings, clock, sleeper, random source, client, or factory explicitly; prefer a real in-process dependency, framework override, or purpose-built ABC/Protocol fake. A scoped patch may replace an existing constructor-injected collaborator on a test-owned instance with an interface-conforming fake when rebuilding the fixture would add wiring without improving the boundary. Keep attribute-existence checks enabled and the tested implementation intact. Suppress SARJ445 only at that call, naming the owned instance and injection seam. For inherently global boundaries or lookup interception, explain why injection would invalidate the test. Establish the binding the exercised consumer actually reads, including already-bound import aliases, and show that the oracle rejects a wrong consumer. Do not switch to bare assignment merely to avoid this rule.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_injected_dependency_over_monkeypatch.py","status":"active","summary":"Prefer explicit test dependencies; justify scoped attribute patches at the boundary.","test":"packages/python/tests/rules/test_prefer_injected_dependency_over_monkeypatch.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ564","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/service.py","source":"class Worker:\n    def __init__(self, store):\n        self.store = store\n    def run(self):\n        return self.store.load()\nclass Consumer:\n    def __init__(self, store):\n        self.store = store\n    def run(self):\n        return Worker(self.store).run()\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"constructs-service-during-operation","outcome":"reject","scenarioId":"primary","title":"The consumer hardwires a collaborator using an existing dependency"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"class Worker:\n    def __init__(self, store):\n        self.store = store\n    def run(self):\n        return self.store.load()\nclass Consumer:\n    def __init__(self, worker):\n        self.worker = worker\n    def run(self):\n        return self.worker.run()\n\ndef compose(store):\n    return Consumer(Worker(store))\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"injects-composed-service","outcome":"accept","scenarioId":"primary","title":"The composition boundary supplies the collaborator"}],"filePatterns":[],"id":"prefer-injected-service","key":"python:prefer-injected-service","languages":["python"],"limitations":["Warning-stage Python policy, based on resolved first-party class behavior rather than service names. A collaborator must directly retain constructor arguments and invoke them through a public operation.","Covers construction retained by the consumer's constructor or immediately followed by a service operation. All arguments must be constructor parameters or directly retained consumer dependencies. Runtime arguments, returned factories, composition containers, tests and generated files are excluded.","Inherited implementations, decorated methods, class-level state or descriptors, resource lifecycles, generators, instance-field reassignment, expanded arguments, reexports, unknown constructors and ambiguous bindings abstain. Local construction followed by later local-variable use is not inferred.","Source inspection cannot establish every ownership or lifetime requirement. Review warning findings before changing composition; no automatic rewrite is offered."],"messageIds":[],"optionsSchema":null,"rationale":"Hardwired collaborator construction hides its lifetime and prevents callers from substituting the service directly.","references":[],"remediation":"Construct the collaborator at the application's composition boundary with the appropriate lifetime and inject it into the consumer. Do not introduce a global singleton or share request-specific state.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_injected_service.py","status":"active","summary":"Inject a service instead of constructing it from dependencies already available to the consumer.","test":"packages/python/tests/rules/test_prefer_injected_service.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ059","defaultLevel":"warning","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":"import boto3\nfrom moto import mock_aws\n\n@mock_aws\ndef test_upload():\n    client = boto3.client(\"s3\", region_name=\"us-east-1\")\n    client.create_bucket(Bucket=\"uploads\")\n    upload(client, bucket=\"uploads\", key=\"avatar\", body=b\"image\")\n    result = client.get_object(Bucket=\"uploads\", Key=\"avatar\")\n    assert result[\"Body\"].read() == b\"image\"\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.","Most service families require module-level cross-method state plus multiple recognized protocol methods.","LLM and BigQuery doubles instead require a distinctive raw provider response envelope.","Generated files, application-owned ports, thin response seams without provider envelopes, and structurally injected clocks are excluded.","A matching unconditional module-level maintained-tool import suppresses that service family."],"messageIds":[],"optionsSchema":null,"rationale":"A substantial hand-written protocol model can drift from supported request, state, and error behavior.","references":[],"remediation":"When the test depends on protocol fidelity, use a maintained fake, emulator, recorder, or test service while keeping the production client. Keep narrow application-port and failure-scripting doubles local.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_library_fake.py","status":"active","summary":"Prefer a maintained fake, emulator, recorder, or test service for substantial third-party protocols.","test":"packages/python/tests/rules/test_prefer_library_fake.py"},{"aliases":[],"autofix":"safe","category":"correctness","code":"SARJ032","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"dispatch.py","source":"from enum import StrEnum\nfrom typing import assert_never\n\nclass Kind(StrEnum):\n    A = \"a\"\n    B = \"b\"\n    C = \"c\"\n\ndef handle(kind: Kind) -> None:\n    match kind:\n        case Kind.A:\n            handle_a()\n        case Kind.B:\n            handle_b()\n        case Kind.C:\n            handle_c()\n        case _ as unreachable:\n            assert_never(unreachable)\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 enum import StrEnum\n\nclass Kind(StrEnum):\n    A = \"a\"\n    B = \"b\"\n    C = \"c\"\n\ndef handle(kind: Kind) -> None:\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":["Safe fixes remove only an exact single argument-type ignore from an existing terminal assert_never call after every member of a canonical local StrEnum is already handled by terminating identity branches. Missing members, other suppressions, reasons, aliases, guarded branches and dynamic domains remain manual.","Only subjects explicitly annotated as a locally declared Enum, StrEnum, IntEnum, or ReprEnum are checked.","Flags, imported or untyped domains, class-pattern unions, guarded arms, generated files, and dynamic dispatch are excluded.","Independently warns on import-proven assert_never calls with a trailing blanket or argument-type ignore; module-wide disables are not analyzed."],"messageIds":[],"optionsSchema":null,"rationale":"A no-op wildcard or else branch hides missing enum members and lets newly added members pass unnoticed. Suppressing the argument-type error at assert_never also disables its static exhaustiveness guarantee.","references":[],"remediation":"Handle every enum member, bind the catch-all value, and pass it to `typing.assert_never`; raise an explicit exception when static exhaustiveness is unavailable. Narrow the accepted type or handle missing variants instead of suppressing assert_never's argument check.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_match_assert_never.py","status":"active","summary":"Typed enum dispatch must not silently ignore unhandled members.","test":"packages/python/tests/rules/test_prefer_match_assert_never.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ453","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/errors.py","source":"from providers import APIConnectionError, APIStatusError, APITimeoutError\n\ndef category(error: BaseException) -> str:\n    match error:\n        case TimeoutError() | APITimeoutError():\n            return 'timeout'\n        case APIStatusError() if error.status_code == 429:\n            return 'rate_limit'\n        case APIStatusError() if error.status_code >= 500:\n            return 'provider_5xx'\n        case APIConnectionError():\n            return 'connection'\n        case _:\n            return 'unknown'\n"}],"fixedFiles":[],"focusPath":"app/errors.py","id":"guarded-provider-error-cases","outcome":"accept","scenarioId":"refined-exception-dispatch","title":"Guarded class cases retain refinement fallthrough"},{"expectedCount":1,"files":[{"path":"app/errors.py","source":"from providers import APIConnectionError, APIStatusError, APITimeoutError\n\ndef category(error: BaseException) -> str:\n    if isinstance(error, TimeoutError | APITimeoutError):\n        return 'timeout'\n    if isinstance(error, APIStatusError):\n        if error.status_code == 429:\n            return 'rate_limit'\n        if error.status_code >= 500:\n            return 'provider_5xx'\n    if isinstance(error, APIConnectionError):\n        return 'connection'\n    return 'unknown'\n"}],"fixedFiles":[],"focusPath":"app/errors.py","id":"refined-provider-error-classifier","outcome":"reject","scenarioId":"refined-exception-dispatch","title":"Repeated type checks hide an ordered provider error classifier"}],"filePatterns":[],"id":"prefer-match-exception-dispatch","key":"python:prefer-match-exception-dispatch","languages":["python"],"limitations":["Only functions whose dispatch subject is a simple parameter annotated Exception or BaseException are checked.","The classifier must contain at least three adjacent isinstance branches and an immediate return or raise fallback; one or more branches must contain two or more terminating refinement checks.","Type operands must be unshadowed builtin or imported names ending in Error or Exception, including literal tuples and PEP 604 unions. Imported runtime groups with plural or unrelated names are excluded.","This is an advisory, not a safe automatic rewrite: imported bindings and custom instance checks cannot be proven from syntax, and guarded cases must preserve overlap and fallthrough.","Generated files and projects proven to support Python before 3.10 are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A long exception classifier that repeats isinstance checks and then refines one exception type hides the ordered set of provider outcomes. Guarded class cases make the type and refinement order explicit.","references":[],"remediation":"Consider repeated guarded class cases while preserving subclass order, guard evaluation, and fallthrough. Keep constant comparisons in guards; a bare name in a class-pattern field captures instead of comparing.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_match_exception_dispatch.py","status":"active","summary":"Prefer guarded match/case for refined exception type dispatch.","test":"packages/python/tests/rules/test_prefer_match_exception_dispatch.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ080","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/bindings.py","source":"import ast\ndef has_binding(node, name):\n    match node:\n        case ast.ListComp():\n            return True\n        case ast.FunctionDef():\n            if name in bindings(node):\n                return True\n        case ast.ClassDef():\n            if name in bindings(node):\n                return True\n    return False\n"}],"fixedFiles":[],"focusPath":"app/bindings.py","id":"class-patterns-with-nested-checks","outcome":"accept","scenarioId":"mixed-type-dispatch","title":"Class patterns preserve the nested checks within their case bodies"},{"expectedCount":0,"files":[{"path":"app/parser.py","source":"def parse(value: object):\n    match value:\n        case str():\n            return parse_text(value)\n        case bytes():\n            return parse_bytes(value)\n        case dict():\n            return parse_mapping(value)\n        case _:\n            raise TypeError(type(value))\n"}],"fixedFiles":[],"focusPath":"app/parser.py","id":"match-type-cases","outcome":"accept","scenarioId":"primary","title":"Class patterns make the type dispatch explicit"},{"expectedCount":0,"files":[{"path":"app/types.py","source":"def is_text(value):\n    match value:\n        case str() | bytes():\n            return True\n    return False\n"}],"fixedFiles":[],"focusPath":"app/types.py","id":"or-class-pattern","outcome":"accept","scenarioId":"or-type-dispatch","title":"One OR pattern declares shared handling"},{"expectedCount":1,"files":[{"path":"app/types.py","source":"def is_text(value):\n    if isinstance(value, str) or isinstance(value, bytes):\n        return True\n    return False\n"}],"fixedFiles":[],"focusPath":"app/types.py","id":"repeated-isinstance-or","outcome":"reject","scenarioId":"or-type-dispatch","title":"Repeated boolean checks share one type subject"},{"expectedCount":0,"files":[{"path":"app/authorization.py","source":"from pydantic import BaseModel\nclass Session(BaseModel):\n    allowed: bool\ndef authorize(subject):\n    match subject:\n        case Session():\n            if not subject.allowed:\n                raise PermissionError\n        case _:\n            return\n"}],"fixedFiles":[],"focusPath":"app/authorization.py","id":"single-model-class-pattern","outcome":"accept","scenarioId":"single-model-type-dispatch","title":"A class pattern keeps the nested permission check in its body"},{"expectedCount":1,"files":[{"path":"app/authorization.py","source":"from pydantic import BaseModel\nclass Session(BaseModel):\n    allowed: bool\ndef authorize(subject):\n    if isinstance(subject, Session):\n        if not subject.allowed:\n            raise PermissionError\n"}],"fixedFiles":[],"focusPath":"app/authorization.py","id":"single-model-type-branch","outcome":"reject","scenarioId":"single-model-type-dispatch","title":"One model-specific branch still dispatches on runtime type"},{"expectedCount":1,"files":[{"path":"app/bindings.py","source":"import ast\ndef has_binding(node, name):\n    if isinstance(node, ast.ListComp):\n        return True\n    if isinstance(node, ast.FunctionDef):\n        if name in bindings(node):\n            return True\n    elif isinstance(node, ast.ClassDef):\n        if name in bindings(node):\n            return True\n    return False\n"}],"fixedFiles":[],"focusPath":"app/bindings.py","id":"terminal-prefix-with-conditional-tail","outcome":"reject","scenarioId":"mixed-type-dispatch","title":"A terminating prefix and conditional if/elif tail dispatch on the same AST node"},{"expectedCount":1,"files":[{"path":"app/parser.py","source":"def parse(value: object):\n    if isinstance(value, str):\n        return parse_text(value)\n    elif isinstance(value, bytes):\n        return parse_bytes(value)\n    elif isinstance(value, dict):\n        return parse_mapping(value)\n    raise TypeError(type(value))\n"}],"fixedFiles":[],"focusPath":"app/parser.py","id":"three-branch-type-dispatch","outcome":"reject","scenarioId":"primary","title":"Repeated branches dispatch on runtime type"},{"expectedCount":0,"files":[{"path":"app/ast_names.py","source":"import ast\n\ndef dotted_tail(node: ast.expr) -> str | None:\n    match node:\n        case ast.Name():\n            return node.id\n        case ast.Attribute():\n            return node.attr\n        case _:\n            return None\n"}],"fixedFiles":[],"focusPath":"app/ast_names.py","id":"two-arm-ast-class-pattern","outcome":"accept","scenarioId":"two-arm-ast-projection","title":"Class patterns expose the two AST identifier shapes"},{"expectedCount":1,"files":[{"path":"app/ast_names.py","source":"import ast\n\ndef dotted_tail(node: ast.expr) -> str | None:\n    if isinstance(node, ast.Name):\n        return node.id\n    if isinstance(node, ast.Attribute):\n        return node.attr\n    return None\n"}],"fixedFiles":[],"focusPath":"app/ast_names.py","id":"two-arm-ast-projection","outcome":"reject","scenarioId":"two-arm-ast-projection","title":"Two isinstance returns project one AST identifier shape"},{"expectedCount":0,"files":[{"path":"app/types.py","source":"def label(value):\n    match value:\n        case str():\n            return 'text'\n        case bytes():\n            return 'binary'\n    return 'other'\n"}],"fixedFiles":[],"focusPath":"app/types.py","id":"two-class-patterns","outcome":"accept","scenarioId":"two-type-dispatch","title":"Class patterns name both alternatives"},{"expectedCount":1,"files":[{"path":"app/types.py","source":"def label(value):\n    if isinstance(value, str):\n        return 'text'\n    elif isinstance(value, bytes):\n        return 'binary'\n    return 'other'\n"}],"fixedFiles":[],"focusPath":"app/types.py","id":"two-type-dispatch","outcome":"reject","scenarioId":"two-type-dispatch","title":"Two branches already repeat the same type dispatch"}],"filePatterns":[],"id":"prefer-match-type-dispatch","key":"python:prefer-match-type-dispatch","languages":["python"],"limitations":["Safe fixes require a closed private nested synchronous helper with proven inert containing scopes and exact primitive literal callers. Only comment-free builtin class-check ladders on one parameter qualify; unknown subjects, callbacks, frame/closure escapes, sibling checks, grouped conditions, guards and custom classes remain review-only.","General dispatch requires two or more adjacent, unguarded `isinstance` branches over the same simple name. Repeated isinstance calls joined only by or on that same name are also checked; use an OR class pattern for shared behavior. A single isinstance call with a type union remains allowed.","Source-proven Pydantic/Enum dispatch is an error even for a single positive isinstance branch. Mixed model/builtin dispatch is supported. These findings have no automatic fixes; preserve nested checks inside case bodies. Predicate expressions, assertions, negated checks and compound guards remain allowed.","A terminating sibling prefix may precede a two-arm if/elif tail whose bodies can fall through. Preserve nested checks inside case bodies rather than moving them into case guards.","The checked types must be unshadowed builtins, undecorated module-local classes without an explicit or known locally inherited custom metaclass, or proven stdlib ast classes; unresolved imports, runtime type groups, repeated type references, generated files, and non-terminating sibling checks are excluded.","Local ancestry must resolve to stable undeclared-metaclass classes or proven builtin/stdlib ast bases. External bases, runtime base aliases, and parameterized base expressions remain excluded because their metaclass or __class_getitem__ effects are unproven.","Nested attribute-validation guards are excluded: converting them to keyword class patterns can turn attribute errors into match fallthrough, and an imported isinstance operand may be a runtime tuple rather than a class.","A terminal-looking context-manager body does not prove a sibling branch terminates: exceptions can be suppressed. An unconditional return or raise after the context manager remains eligible.","Declared support for Python before 3.10 suppresses this recommendation when proven by the nearest project metadata or exact installed-distribution ownership. Missing or ambiguous target metadata retains advisory behavior; it does not prove a modern target."],"messageIds":[],"optionsSchema":null,"rationale":"A repeated mutually exclusive `isinstance` dispatch repeats its subject and hides the closed list of runtime shapes. Class patterns make the alternatives explicit without repeating the dispatch subject.","references":[],"remediation":"Replace the branches with `match subject` and one class-pattern arm per distinct type; combine types that share behavior with an OR-pattern.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_match_type_dispatch.py","status":"active","summary":"Prefer `match` for structural runtime type dispatch.","test":"packages/python/tests/rules/test_prefer_match_type_dispatch.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ439","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/migrate.py","source":"def migrate(path):\n    match path.name:\n        case 'eslint.json':\n            return rewrite_eslint(path)\n        case 'ratchet.json':\n            return rewrite_ratchet(path)\n        case _:\n            return rewrite(path)\n"}],"fixedFiles":[],"focusPath":"app/migrate.py","id":"literal-match-dispatch","outcome":"accept","scenarioId":"primary","title":"Use value cases and retain the fallback"},{"expectedCount":0,"files":[{"path":"app/errors.py","source":"from provider import ErrorId\ndef classify(error_id):\n    match error_id:\n        case ErrorId.INSUFFICIENT:\n            return insufficient()\n        case ErrorId.INVALID_OPTION:\n            return invalid_option()\n        case _:\n            return rejected()\n"}],"fixedFiles":[],"focusPath":"app/errors.py","id":"qualified-value-match-retains-fallback","outcome":"accept","scenarioId":"qualified-terminal-dispatch","title":"Value cases preserve the terminal fallback"},{"expectedCount":1,"files":[{"path":"app/parse.py","source":"def parse(kind):\n    if kind == 'json': return json_parser()\n    elif kind == 'yaml': return yaml_parser()\n    else: return fallback()\n"},{"path":"pyproject.toml","source":"[project]\nrequires-python = '>=3.10'\n"}],"fixedFiles":[],"focusPath":"app/parse.py","id":"stable-literal-parameter","outcome":"reject","scenarioId":"primary","title":"Dispatch a stable parameter through literal cases"},{"expectedCount":1,"files":[{"path":"app/errors.py","source":"from provider import ErrorId\ndef classify(error_id):\n    if error_id == ErrorId.INSUFFICIENT:\n        return insufficient()\n    if error_id == ErrorId.INVALID_OPTION:\n        return invalid_option()\n    return rejected()\n"},{"path":"pyproject.toml","source":"[project]\nrequires-python = '>=3.10'\n"}],"fixedFiles":[],"focusPath":"app/errors.py","id":"terminal-qualified-value-dispatch","outcome":"reject","scenarioId":"qualified-terminal-dispatch","title":"Keep error variants and their fallback under one dispatch subject"}],"filePatterns":[],"id":"prefer-match-value-dispatch","key":"python:prefer-match-value-dispatch","languages":["python"],"limitations":["Safe fixes require a closed private nested synchronous helper with proven inert containing scopes and exact primitive literal callers. Only comment-free literal-equality ladders on one parameter qualify; unknown subjects, callbacks, frame/closure escapes, sibling checks, grouped conditions and guards remain review-only.","ERROR covers direct literal equality ladders on an unrebound local parameter and terminal equality dispatch on a stable local name against distinct dotted values from one import-proven namespace or local enum. A proven minimum Python version of 3.10 and no visible namespace access are required. Other supported dispatch remains WARNING.","Equality-only ladders require two or more tests on the same left-hand name or attribute chain, followed by else.","Cases must be distinct string, bytes, or numeric literals, or singly bound module constants containing them; shadowed constants and singleton values are excluded.","This is a readability suggestion, not proof of exhaustiveness or a safe automatic rewrite: attributes can have side effects and match evaluates its subject once.","Grouped or guarded ladders and terminal sibling branches require three distinct arms on a bare name; only the last arm may have a selector-first guard.","Two or more terminal sibling equality branches with a terminal fallback, or an if/elif ladder with else, also qualify when comparing one bare name with distinct values from the same qualified namespace. Imported values are not assumed to be exhaustive enum members. This subset remains review-only because import attributes and equality may have custom behavior.","Groups contain two to six distinct string literals in a tuple, list, or set. Set hashing and custom equality can differ from pattern matching; no runtime string type is inferred.","Declared support for Python before 3.10 suppresses this recommendation when proven by the nearest project metadata or exact installed-distribution ownership. Missing or ambiguous target metadata retains advisory behavior; it does not prove a modern target."],"messageIds":[],"optionsSchema":null,"rationale":"A single dispatch subject makes distinct value cases and their fallback easier to review.","references":[],"remediation":"Consider match/case while preserving fallback and constant semantics; use literal or dotted value patterns, never bare constant names that capture instead of compare. Review membership versus equality, subject evaluation, and guard order before rewriting.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_match_value_dispatch.py","status":"active","summary":"Prefer match/case for repeated dispatch on one value with a fallback.","test":"packages/python/tests/rules/test_prefer_match_value_dispatch.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ039","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"service.py","source":"from typing import Final\n\nLABELS: Final[dict[str, int]] = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8}\n\ndef handle(key):\n    return LABELS.get(key)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"final-module-lookup","outcome":"accept","scenarioId":"keyed-values","title":"Final lookup dictionary defined once"},{"expectedCount":0,"files":[{"path":"service.py","source":"ALLOWED = (\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\")\n\ndef handle(value):\n    return value in ALLOWED\n"}],"fixedFiles":[],"focusPath":"service.py","id":"immutable-module-membership","outcome":"accept","scenarioId":"primary","title":"Immutable membership table defined once"},{"expectedCount":1,"files":[{"path":"service.py","source":"def handle(key):\n    labels = {'a': 1, 'b': 2, 'c': 3, 'd': 4, 'e': 5, 'f': 6, 'g': 7, 'h': 8}\n    return labels.get(key)\n"}],"fixedFiles":[],"focusPath":"service.py","id":"static-lookup-built-per-call","outcome":"reject","scenarioId":"keyed-values","title":"Static lookup dictionary rebuilt per call"},{"expectedCount":1,"files":[{"path":"service.py","source":"def handle(value):\n    allowed = [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\", \"g\", \"h\"]\n    return value in allowed\n"}],"fixedFiles":[],"focusPath":"service.py","id":"static-membership-built-per-call","outcome":"reject","scenarioId":"primary","title":"Substantial static membership table rebuilt per call"}],"filePatterns":[],"id":"prefer-module-level-constant","key":"python:prefer-module-level-constant","languages":["python"],"limitations":["Test, test-support, and generated files are excluded.","Collections require at least eight deeply immutable elements, or three when allocated inside a loop; only representation-insensitive reads are accepted.","Final dictionaries retain mutable contents at runtime; unknown calls, mutation, and escaping references are excluded from hoisting candidates.","Visible frame or dynamic-namespace observation retains WARNING because moving a local binding can change observed caller state.","Regex findings require a proven stdlib `re.compile` binding and exclude `re.DEBUG`; regex cache lookup findings remain warnings because invocation frequency and initialization timing are unproved. Immutable literal collection tables enforce the explicit static-lifetime policy at error severity; preserve any intentional per-call ownership with an exact-code exception."],"messageIds":[],"optionsSchema":null,"rationale":"Rebuilding a substantial static collection repeats allocation, while calling `re.compile` with a constant pattern repeats a regex-cache lookup. Native immutable collections or Final dictionary bindings make the static lifetime explicit for proven read-only use.","references":[],"remediation":"Define the value once at module scope as a tuple, frozenset, Final[dict[K, V]], or compiled pattern, then reference it from the function. Final prevents reassignment in type-checked code but does not freeze dictionary contents; use MappingProxyType when runtime write protection is required. Preserve ordering and concrete-type behavior used by callers.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_module_level_constant.py","status":"active","summary":"Hoist repeatedly read static values when a module constant preserves behavior.","test":"packages/python/tests/rules/test_prefer_module_level_constant.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ446","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_plugin.py","source":"import sys\n\ndef test_plugin(fake_plugin):\n    sys.modules['optional_plugin'] = fake_plugin\n"}],"fixedFiles":[],"focusPath":"tests/test_plugin.py","id":"direct-module-registry-mutation","outcome":"reject","scenarioId":"primary","title":"Test installs a module without guaranteed restoration"},{"expectedCount":0,"files":[{"path":"tests/test_plugin.py","source":"import sys\n\ndef test_plugin(monkeypatch, fake_plugin):\n    monkeypatch.setitem(sys.modules, 'optional_plugin', fake_plugin)\n"}],"fixedFiles":[],"focusPath":"tests/test_plugin.py","id":"scoped-module-registry-mutation","outcome":"accept","scenarioId":"primary","title":"Pytest restores the module registry after the test"}],"filePatterns":[],"id":"prefer-monkeypatch-for-process-state-in-test","key":"python:prefer-monkeypatch-for-process-state-in-test","languages":["python"],"limitations":["Teardown ownership and original-state restoration across yield are not inferred. Valid cleanup or restoration may still warn and require a reasoned local suppression.","Only maintained test paths are analyzed; generated files, production code, and module/class bootstrap mutations are excluded.","The rule covers os.chdir, sys.path.insert(0, ...), and direct sys.modules set, delete, or unused pop operations with an equivalent pytest restoring helper.","Environment variables remain owned by Ruff TID251/B003; argv, locale, timezone, warning filters, and process APIs without an equivalent helper are outside this rule.","Imports and import aliases are resolved conservatively. Indirect container aliases and dynamically selected modules are not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"The working directory and Python import registries are shared by every test in the process. A direct mutation can survive an assertion or setup failure, making later tests depend on execution order.","references":[],"remediation":"Establish restoration in setup with pytest's monkeypatch.chdir, monkeypatch.syspath_prepend, monkeypatch.setitem, or monkeypatch.delitem so the original state is recorded before the mutation. Alternatively, explicitly restore the original state with a matching try/finally. Adding monkeypatch operations only during teardown can undo intended cleanup: undoing a teardown deletion can reinstall a temporary sys.modules entry. Intentional cleanup with established ownership may use a local, reasoned SARJ446 suppression.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_monkeypatch_for_process_state_in_test.py","status":"active","summary":"Test mutates process-wide state without a restoring test scope.","test":"packages/python/tests/rules/test_prefer_monkeypatch_for_process_state_in_test.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ461","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/metrics.py","source":"import time\n\ndef measure():\n    started = time.monotonic()\n    while time.monotonic() - started < 5:\n        work()\n"}],"fixedFiles":[],"focusPath":"app/metrics.py","id":"monotonic-elapsed","outcome":"accept","scenarioId":"primary","title":"A monotonic clock measures duration"},{"expectedCount":1,"files":[{"path":"app/metrics.py","source":"import time\n\ndef measure():\n    started = time.time()\n    while time.time() - started < 5:\n        work()\n"}],"fixedFiles":[],"focusPath":"app/metrics.py","id":"wall-clock-elapsed","outcome":"reject","scenarioId":"primary","title":"A local wall clock measures duration"}],"filePatterns":[],"id":"prefer-monotonic-for-elapsed-time","key":"python:prefer-monotonic-for-elapsed-time","languages":["python"],"limitations":["A start used exclusively in direct while-loop duration comparisons against finite nonnegative literal thresholds blocks as an error. Returned, logged, and other elapsed-time measurements retain advisory findings because wall-clock telemetry can be intentional.","Only an unconditional, single-assignment function-local start reading whose every read is the right operand of time.time() - start is reported.","Only unshadowed module-level clock imports are resolved. Reassignment, additional timestamp uses, escapes, nested-scope reads, comprehensions, reflection, and mixed-clock expressions are excluded.","Instance fields, process or persisted deadlines, indirect end-clock readings, and generated files are not inferred. No automatic rewrite is provided."],"messageIds":[],"optionsSchema":null,"rationale":"System-clock corrections can make wall-clock differences negative or unexpectedly large. A monotonic clock measures elapsed intervals without these jumps.","references":[],"remediation":"Use time.monotonic() or time.perf_counter() for both the start reading and its elapsed-time readings. Keep time.time() when measuring absolute timestamps or deliberate wall-clock adjustments.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_monotonic_for_elapsed_time.py","status":"active","summary":"Use a monotonic clock for a local wall-clock value used only to measure elapsed time.","test":"packages/python/tests/rules/test_prefer_monotonic_for_elapsed_time.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ480","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_receipt.py","source":"from pydantic import TypeAdapter\ndef test_receipt():\n    assert TypeAdapter(str).validate_python(receipt[\"id\"]) == \"abc\"\n"}],"fixedFiles":[],"focusPath":"tests/test_receipt.py","id":"adapter-for-string-check","outcome":"reject","scenarioId":"primary","title":"A string assertion unnecessarily constructs a schema validator"},{"expectedCount":1,"files":[{"path":"app/responses.py","source":"from pydantic import TypeAdapter, JsonValue\nfrom fastapi.encoders import jsonable_encoder\ndef response_content(body):\n    return TypeAdapter(dict[str, JsonValue]).validate_python(jsonable_encoder({'body': body}, custom_encoder={bytes: lambda value: value.decode('utf-8', errors='replace')}))\n"}],"fixedFiles":[],"focusPath":"app/responses.py","id":"generic-json-after-encoding","outcome":"reject","scenarioId":"json-output","title":"A generic validator wraps an already encoded response mapping"},{"expectedCount":0,"files":[{"path":"tests/test_receipt.py","source":"def test_receipt():\n    value = receipt[\"id\"]\n    assert isinstance(value, str)\n    assert value == \"abc\"\n"}],"fixedFiles":[],"focusPath":"tests/test_receipt.py","id":"native-string-assertion","outcome":"accept","scenarioId":"primary","title":"A native assertion rejects unexpected types and narrows the value"},{"expectedCount":0,"files":[{"path":"app/responses.py","source":"from fastapi.encoders import jsonable_encoder\nfrom fastapi.responses import JSONResponse\ndef response(body) -> JSONResponse:\n    return JSONResponse(content=jsonable_encoder({'body': body}))\n"}],"fixedFiles":[],"focusPath":"app/responses.py","id":"response-serialization-boundary","outcome":"accept","scenarioId":"json-output","title":"The response owns encoded content without annotation-only validation"}],"filePatterns":[],"id":"prefer-native-string-check","key":"python:prefer-native-string-check","languages":["python"],"limitations":["Plain built-in str adapters are checked only inside assertions. The JSON branch checks exactly dict[str, pydantic.JsonValue] immediately wrapping FastAPI jsonable_encoder of a dictionary literal with string literal keys; dynamic values and keyword encoder options are allowed. Custom encoders must be a literal mapping with only proven built-in bytes keys, so they cannot replace the root mapping or its string keys. Unknown input shapes, opaque or shape-changing encoders, schema aliases, other containers, constrained types, unions, models, configured adapters and validation options are excluded.","Scoped import aliases and unescaped unconditional module or local adapter bindings are recognized, including independent functions reusing a name. Conditional, reassigned, indirect or class/comprehension/parameter/global/nonlocal cached bindings are excluded, as are shadowed symbols, wildcard imports and imports with visible mutation or escape.","String assertion messages and deferred expressions are excluded. The JSON branch excludes lambdas, comprehensions, function/class headers and cached encoded values; proven pytest.raises(ValidationError) contexts and try bodies catching ValidationError are preserved. Broader catches and contextlib.suppress are not inferred. This advisory does not prove equivalent behavior: intentional validation or byte-decoding contracts need a local reasoned suppression; there is no autofix."],"messageIds":[],"optionsSchema":null,"rationale":"A plain string adapter obscures an assertion and can decode bytes. Generic JSON-dictionary validation after encoding adds a separate runtime contract, often only to satisfy an annotation; it can reject nested integer keys that a response serializer accepts and does not prove response serializability.","references":[],"remediation":"Use isinstance(value, str) for string assertions. For annotation-only JSON adaptation, return a typed Response/JSONResponse at the serialization boundary. Keep structured input and specific schema validation. Preserve intentional coercion, recursive string-key validation or ValidationError contracts explicitly.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_native_string_check.py","status":"active","summary":"Avoid unconstrained adapters for string assertions or already encoded JSON mappings.","test":"packages/python/tests/rules/test_prefer_native_string_check.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ093","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/services/files.py","source":"from typing import NewType\nFileId = NewType('FileId', str)\ndef load(file_id: str) -> None: ...\n"}],"fixedFiles":[],"focusPath":"app/services/files.py","id":"established-single-id-erased","outcome":"reject","scenarioId":"established-single-id","title":"A single ID retains its established domain type"},{"expectedCount":0,"files":[{"path":"app/services/files.py","source":"from typing import NewType\nFileId = NewType('FileId', str)\ndef load(file_id: FileId) -> None: ...\n"}],"fixedFiles":[],"focusPath":"app/services/files.py","id":"established-single-id-preserved","outcome":"accept","scenarioId":"established-single-id","title":"The boundary uses the existing nominal ID"},{"expectedCount":0,"files":[{"path":"app/services/files.py","source":"from typing import NewType\n\nFileId = NewType('FileId', str)\nFolderId = NewType('FolderId', str)\n\ndef 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":"NewType makes the two domain roles distinct"},{"expectedCount":1,"files":[{"path":"app/contracts.py","source":"from abc import ABC, abstractmethod\nclass FilePort(ABC):\n    @abstractmethod\n    def move(self, file_id: str, folder_id: str) -> None: ...\n"}],"fixedFiles":[],"focusPath":"app/contracts.py","id":"owned-abstract-contract-id-roles","outcome":"reject","scenarioId":"owned-contract","title":"Abstract contracts declare their own nominal ID boundary"},{"expectedCount":0,"files":[{"path":"app/contracts.py","source":"from abc import ABC, abstractmethod\nfrom typing import NewType\nFileId = NewType('FileId', str)\nFolderId = NewType('FolderId', str)\nclass FilePort(ABC):\n    @abstractmethod\n    def move(self, file_id: FileId, folder_id: FolderId) -> None: ...\n"}],"fixedFiles":[],"focusPath":"app/contracts.py","id":"owned-abstract-contract-nominal-ids","outcome":"accept","scenarioId":"owned-contract","title":"Abstract contracts use distinct domain identifiers"},{"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":"swappable-primitive-id-roles","outcome":"reject","scenarioId":"primary","title":"Two domain ID roles share the same primitive carrier"}],"filePatterns":[],"id":"prefer-nominal-id-types","key":"python:prefer-nominal-id-types","languages":["python"],"limitations":["A single primitive ID field on a direct Pydantic model receives a warning when an explicit pattern already demonstrates its own identifier domain. Reuse or introduce a nominal ID while retaining the pattern and wire representation; an ID-shaped name alone is insufficient.","The rule checks functions, methods, constructors, and classes for at least two ID-shaped roles with the same proven carrier. Existing coverage remains an error.","A single raw _id/_ids role in an explicitly declared ABC/Protocol contract now receives a warning even when no matching nominal type has been declared. This advisory does not claim two unlike carriers are swappable; operational IDs, external/inherited contracts and raw-schema exclusions still apply.","Direct implementations of stable same-file abstract contracts also receive the raw-ID advisory, including overrides. Imported, ambiguous, decorated, mutated and multiple-inheritance bases remain excluded. A contract with an existing swappable-ID error owns that diagnostic instead of repeating its implementation as warnings.","Imported first-party carriers and non-ID roles backed by a unique matching NewType in the owning distribution or a declared local dependency are errors. A single raw ID role also qualifies when that distribution already declares an unambiguous matching nominal type, including optional and collection forms. Ambiguous exports, conditional or relative imports, cycles, and unresolved ownership are skipped.","Non-ID role discovery uses exact snake-case names and proven matching carriers; it does not infer brands from names alone. Dependency discovery respects declared workspace members and exclusions, and excludes optional and transitive dependencies.","Rebound aliases, generic aliases, carrier names shadowed by type parameters, and mutated or escaped canonical provider namespaces are skipped conservatively across the file.","Generated code, migrations, external adapters, operational context, raw schemas, SQLAlchemy Mapped fields, ambiguous imports, and unlike carrier shapes are excluded.","Canonical object, ABC, Protocol and Generic bases do not hide fields or methods declared by the class. Mutated, escaped or rebound providers remain unknown. Other explicit bases stay excluded without requiring an override decorator; their inherited API may prescribe raw carriers. Local custom bases do not prove runtime member ownership remains closed."],"messageIds":[],"optionsSchema":null,"rationale":"Two identifiers with the same primitive or container carrier can be exchanged without a type-checking error. Erasing an established domain ID has the same problem even when a boundary accepts one identifier. Nominal types make those role mistakes visible while leaving unlike carriers alone.","references":[],"remediation":"Use `typing.NewType` as the low-runtime-cost default and propagate it from the raw edge through the domain boundary. A nominal value object is also valid when runtime validation or behavior is required.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_nominal_id_types.py","status":"active","summary":"Python boundaries should distinguish swappable identifier roles with nominal types.","test":"packages/python/tests/rules/test_prefer_nominal_id_types.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ568","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"from typing import TypedDict\nclass Timings(TypedDict):\n    total: float\nclass Report:\n    timings: Timings\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"declared-domain","outcome":"accept","scenarioId":"primary","title":"The contract is explicit"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"class Report:\n    timings: dict[str, float]\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"undeclared-domain","outcome":"reject","scenarioId":"primary","title":"The contract is implicit"}],"filePatterns":[],"id":"prefer-nominal-mapping-keys","key":"python:prefer-nominal-mapping-keys","languages":["python"],"limitations":["Reviews public function parameters and returns, public class fields and public module type aliases whose mappings use primitive keys and primitive or primitive-container values. Private class implementation maps are excluded. This is domain-modeling advice, not proof that every dictionary is a closed record.","Private algorithm helpers, local variables, ClassVar/Final metadata, Any/object/JsonValue-valued mappings, nominal or enum keys, tests and generated files are excluded. Existing schema-free mapping rules own Any/object payloads.","Standard import aliases, Optional/Annotated wrappers, nested containers and quoted annotations are recognized. A local mapping alias is reported at its declaration rather than again at every use. Imported aliases and opaque value types are not expanded.","No automatic TypedDict, NewType or enum construction is safe without knowing allowed keys, persistence and serialization contracts."],"messageIds":[],"optionsSchema":null,"rationale":"A primitive mapping erases both fixed record keys and the domain identity of dynamic keys.","references":[],"remediation":"Use a TypedDict or validated model for a fixed record; for a dynamic domain mapping use a nominal NewType identifier or enum key. Preserve intentionally open transport and algorithm contracts with a reasoned local exception.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_nominal_mapping_keys.py","status":"active","summary":"Declare record shapes or nominal keys at primitive-keyed mapping boundaries.","test":"packages/python/tests/rules/test_prefer_nominal_mapping_keys.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ082","defaultLevel":"warning","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":"collapsed-nullable-list","outcome":"reject","scenarioId":"primary","title":"Nullable list is immediately collapsed to an empty list"},{"expectedCount":0,"files":[{"path":"app/resolver.py","source":"from collections.abc import Sequence\n\ndef resolve(candidates: Sequence[str] = ()) -> list[str]:\n    return list(candidates)\n"}],"fixedFiles":[],"focusPath":"app/resolver.py","id":"immutable-empty-input","outcome":"accept","scenarioId":"primary","title":"Immutable empty default keeps omission non-nullable"}],"filePatterns":[],"id":"prefer-non-nullable-collection","key":"python:prefer-non-nullable-collection","languages":["python"],"limitations":["Only undecorated module functions and constructors without a non-object base are checked for nullable list parameters defaulted to None.","The warning requires a syntax-proven `items or []` normalization, either as the only read or as the first assignment; tests, generated code, ambiguous imports, guards, and conditional expressions are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"When an implementation immediately replaces both `None` and an empty list with the same fresh empty list, the callable contract exposes an extra input state that its body does not retain.","references":[],"remediation":"After reviewing callers and list identity or mutation behavior, either require the list or accept `Sequence[T] = ()` and materialize a list internally. Removing explicit `None` acceptance is an API migration.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_non_nullable_collection.py","status":"active","summary":"Avoid nullable list parameters that are immediately collapsed to an empty list.","test":"packages/python/tests/rules/test_prefer_non_nullable_collection.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ541","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel\nclass Usage(BaseModel):\n    actor_id: str\n    actor_name: str | None\n    since: str\nclass State(BaseModel):\n    in_use: Usage | None\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"one-optional-composite","outcome":"accept","scenarioId":"primary","title":"A present usage value can still have an unavailable actor name"},{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel\nclass State(BaseModel):\n    in_use_by: str | None\n    in_use_by_name: str | None\n    in_use_since: str | None\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"scattered-optional-value","outcome":"reject","scenarioId":"primary","title":"Review a usage value split across nullable fields"}],"filePatterns":[],"id":"prefer-optional-composite","key":"python:prefer-optional-composite","languages":["python"],"limitations":["This is a broad modeling review warning, not proof of an all-or-nothing invariant or an invalid runtime state. A shared underscore-delimited prefix and at least two explicitly nullable fields are the deterministic trigger; independent same-prefix settings can also match and should remain independent when that is their contract.","Checks stable direct module-level source-declared Pydantic BaseModels, plain direct dataclasses and TypedDicts. This advisory relationship follows unshadowed runtime model imports and explicit namespace writes, without proving the runtime effects of unrelated callbacks. PEP 604, Optional, Union, Annotated, quoted annotations and bounded module type aliases are recognized; container-element nullability alone is not field nullability. Runtime ClassVar metadata and dataclass InitVar/KW_ONLY metadata are excluded; dataclasses retain Annotated metadata wrappers and PEP 695 metadata aliases as fields. A required or unresolved same-prefix sibling prevents grouping that broader prefix.","Larger overlapping groups take precedence, with the longest common prefix for the same field set. Each field belongs to at most one finding. Inherited/generic models, unknown decorators or namespaces, replaced annotation providers, rebound owners, tests, generated and vendor code are excluded.","The rule does not infer relationships from shared suffixes, enforce non-null child fields or automatically nest public models. SARJ510 owns proven tag-dependent variant payloads."],"messageIds":[],"optionsSchema":null,"rationale":"Several nullable fields with a common record prefix can scatter one optional value across a larger model, requiring repeated presence checks and obscuring which partial states are intentional. A nested value can give that concept one boundary while retaining independently nullable children.","references":[],"remediation":"Review whether the fields represent one value and, if so, introduce a typed composite with one optional outer field. Preserve valid partial states, omitted-key behavior, defaults, aliases, validation and wire/storage compatibility. Keep independent optional children nullable. A shared prefix does not prove that all fields must be present together.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_optional_composite.py","status":"active","summary":"Review related optional fields as one optional composite value.","test":"packages/python/tests/rules/test_prefer_optional_composite.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ070","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/settings.py","source":"def status(value):\n    match value:\n        case 'queued':\n            return 'active'\n        case 'running':\n            return 'active'\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 status(value):\n    match value:\n        case 'queued' | 'running':\n            return 'active'\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":["Safe fixes merge only comment-free proven runs and preserve the first body verbatim; runs containing comments remain review-only.","Only adjacent, unguarded, refutable arms with structurally identical non-empty bodies are compared.","Arms with different bound names or comments, intervening or leading comments, and combined patterns rejected by Python are excluded.","Bodies that read pattern captures or names used by the match subject are excluded: merging can lose correlated narrowing required by typed calls. This syntax-only check also abstains from safe forwarding of those values."],"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":"architecture","code":"SARJ571","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel\nclass AudioResult(BaseModel):\n    passed: bool = False\n    error: str | None = None\n    value: bytes | None = None\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"nullable-outcome-record","outcome":"reject","scenarioId":"primary","title":"A nullable error and success flag admit contradictory outcomes"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from typing import Literal\nfrom pydantic import BaseModel\nclass RunSuccess(BaseModel):\n    passed: Literal[True] = True\n    value: bytes\nclass RunError(BaseModel):\n    passed: Literal[False] = False\n    error: str\ntype RunResult = RunSuccess | RunError\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"separate-outcome-variants","outcome":"accept","scenarioId":"primary","title":"Success and error variants have non-null payloads"}],"filePatterns":[],"id":"prefer-outcome-variants","key":"python:prefer-outcome-variants","languages":["python"],"limitations":["A nullable error must be paired with a success Boolean, a finite success/error Literal tag, a nullable payload, or a Result/Outcome name plus other fields. Names alone never trigger this advisory.","Recognizes direct import-proven Pydantic, dataclass, TypedDict, attrs and msgspec records, bounded Optional/Union/Annotated aliases and quoted annotations. Opaque ancestry and imported aliases are not guessed.","Separate variants, independent last-error telemetry, scalar numeric uncertainty, mandatory error context, class metadata, tests and generated files are excluded. Exact-code suppressions are respected.","Source-proven validator dependencies retain SARJ510 as their stronger owner when both rules are enabled. This warning does not change existing SARJ510 errors or automatically migrate constructors, persistence or partial progress."],"messageIds":[],"optionsSchema":null,"rationale":"An optional error beside success or payload state admits contradictory combinations. Tagged variants expose complete success payloads and explicit failure context to callers.","references":[],"remediation":"Reuse an established Result[T, E] or declare Success | Error variants with a shared Literal discriminator. Require success payloads and the error value in their respective variants; retain typed partial progress when failures need it. Preserve accepted inputs and serialized contracts during migration.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_outcome_variants.py","status":"active","summary":"Express nullable success and error state as tagged outcome variants.","test":"packages/python/tests/rules/test_prefer_outcome_variants.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ528","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app.py","source":"from collections.abc import Callable\nfrom types import ModuleType\ndef load(import_module: Callable[[str], ModuleType]): ...\n"}],"fixedFiles":[],"focusPath":"app.py","id":"module-contract","outcome":"accept","scenarioId":"primary","title":"A module result has its standard type"},{"expectedCount":1,"files":[{"path":"app.py","source":"from collections.abc import Callable\ndef load(import_module: Callable[[str], object]): ...\n"}],"fixedFiles":[],"focusPath":"app.py","id":"opaque-importer","outcome":"reject","scenarioId":"primary","title":"The importer erases its module result"},{"expectedCount":0,"files":[{"path":"app/contracts.py","source":"def ready() -> bool:\n    def _value() -> str:\n        return 'ready'\n    if _value():\n        return True\n    return False\n"}],"fixedFiles":[],"focusPath":"app/contracts.py","id":"safe-autofix-result","outcome":"accept","scenarioId":"private-literal-truthiness","title":"The proven private literal return already has its precise type"},{"expectedCount":1,"files":[{"path":"app/contracts.py","source":"def ready() -> bool:\n    def _value() -> object:\n        return 'ready'\n    if _value():\n        return True\n    return False\n"}],"fixedFiles":[{"path":"app/contracts.py","source":"def ready() -> bool:\n    def _value() -> str:\n        return 'ready'\n    if _value():\n        return True\n    return False\n"}],"focusPath":"app/contracts.py","id":"safe-autofix-subset","outcome":"reject","scenarioId":"private-literal-truthiness","title":"Existing source proof permits a narrow convergent fix"}],"filePatterns":[],"id":"prefer-precise-object-contract","key":"python:prefer-precise-object-contract","languages":["python"],"limitations":["Safe fixes narrow only a bare builtin object return annotation to builtin str for a closed private nested synchronous helper returning one exact string literal, with every call used directly as an if test and all containing return contracts explicit. Existing inert/metadata/binding proofs must hold; public/domain/container/parameter/model contracts and other result uses remain review-only.","Checks builtin object in function signatures and class field annotations, including supported containers, callables, optional unions and quoted annotations. Local opaque values, object() sentinels, Literal/Annotated metadata, framework hooks, proven override signatures, and registered raw Pydantic callbacks are excluded. Functions that directly validate an object parameter through a source-visible BaseModel, BaseSettings or RootModel model_validate call, positionally or with obj=, retain that raw boundary. JSON text inputs still require a text or byte contract. Aliases must resolve to their source declaration and their bodies use definition scope. Alias graphs are bounded and shared branches reuse their facts. Imported or shadowed application types named object are preserved. Genuine opaque public contracts can use an explained exact-code exception; narrowing outside the exact private truthiness-only subset remains manual.","An import-proven TypeGuard or TypeIs retains bare object on its guarded positional parameter and unchecked object slots in its declared refinement result; other object parameters remain checked. Guard correctness belongs to the type-guard rules. Universal type[object] class contracts and the return of a canonical module-level PEP 562 __getattr__ hook retain their opaque runtime contracts.","Ordinary raw validators retain object inputs tied to builtin isinstance, exact builtin type comparisons or an actual source-resolved TypeGuard/TypeIs through rejecting branches, actual type/refinement assertions, accepting terminal branches with a rejecting fallback, or conditional expressions. Stable projections from containers with unchecked value slots retain their raw root contract when those values are validated. Source-visible local and imported validators may delegate those inputs and raw results; precise result narrowing does not justify a bare object output. Related unchecked mapping projections and transforms retain unknown values; import-proven native parsers and JSONDecoder preserve raw outputs. Universal builtin and stdlib interoperation may retain raw inputs. Independent object parameters, domain constructors and unproven parser names remain checked. Source resolution and dataflow use a bounded per-file proof cache, captured source inputs and cycle guards; rebinding, decorators and namespace mutation invalidate delegated proof.","Native opaque contracts include canonical ast.Constant.value reads and captures, FunctionType invocations, identity-only inputs and sys.getrefcount. Actual exec/eval globals and locals retain unknown values only through bounded native dict construction and alias flow; unrelated dictionaries and source domain factories do not borrow that ownership. Stub property returns on an import-proven runtime_checkable Protocol retain unchecked incoming attributes. Generic callable results, ordinary Protocol methods, application properties and unused parameters remain checked.","Raw input unions may include scalar or None branches; only genuine unchecked container branches seed raw projections. Mapping consumers that inspect only keys retain uninspected value slots. Projected aliases require simple statement dominance, and precise active guards permit typed consumption after validation. Native namespace output reads require a preceding direct function-scope execution and abstain after authored mutations. Benign builtins imports do not hide builtin object contracts; explicit object shadowing and source-visible object mutations remain outside scope.","An import-proven Iterable of unchecked values retains its raw element contract when every input use is a direct synchronous iteration and every reachable element read directly enters source-proven Pydantic model_validate. Deferred closures, rebinding, other iterator/element uses, unknown model providers and JSON-text parsers do not establish that boundary.","Rejecting branches may end in a stable source-visible synchronous helper whose body ends with an unconditional raise and contains no return or yield path. Names and NoReturn annotations alone do not establish termination; decorated, mutated, asynchronous, generator and returning helpers remain unknown."],"messageIds":[],"optionsSchema":null,"rationale":"An object annotation hides the operations and data the caller actually supplies. It often spreads casts and handwritten shape checks into consumers.","references":[],"remediation":"Use a concrete domain type, Protocol, TypeVar, Pydantic model or JsonValue as appropriate. Importer callables return types.ModuleType. Keep object for genuinely opaque Python values and raw validation boundaries with an explained local exception when necessary; never replace it with Any or a cast.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_precise_object_contract.py","status":"active","summary":"Use a precise type for declared application contracts instead of object.","test":"packages/python/tests/rules/test_prefer_precise_object_contract.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ518","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/user_store.py","source":"from psycopg import AsyncCursor\nasync def save(cursor: AsyncCursor, rows):\n    await cursor.executemany('INSERT INTO users (id) VALUES (%s)', [(row.id,) for row in rows])\n"}],"fixedFiles":[],"focusPath":"app/user_store.py","id":"batch-execute","outcome":"accept","scenarioId":"primary","title":"Use the cursor's batch API"},{"expectedCount":1,"files":[{"path":"app/user_store.py","source":"from psycopg import AsyncCursor\nasync def save(cursor: AsyncCursor, rows):\n    for row in rows:\n        await cursor.execute('INSERT INTO users (id) VALUES (%s)', (row.id,))\n"}],"fixedFiles":[],"focusPath":"app/user_store.py","id":"serial-execute","outcome":"reject","scenarioId":"primary","title":"Batch the same parameterized statement"}],"filePatterns":[],"id":"prefer-psycopg-executemany","key":"python:prefer-psycopg-executemany","languages":["python"],"limitations":["Only a synchronous for loop with one discarded execute call on an import-proven typed Psycopg Cursor or AsyncCursor parameter is inspected. The query must be one literal INSERT, UPDATE or DELETE with placeholders and loop-dependent bound parameters.","RETURNING, queries, dynamic SQL, extra execute options, async streams, loop else clauses, additional statements and per-row exception or transaction contexts are excluded. Connection.execute is excluded because connections do not expose the same executemany API.","No automatic batching or performance claim is made for a particular workload. Advisory severity preserves deliberately sequential database effects; exact suppressions document those contracts.","Generated/vendor sources are excluded; maintained tests are checked. Stable aliases and cursors acquired through context managers are intentionally outside the initial proof boundary.","Cursor state reads, iteration and opaque cursor escapes are excluded: executemany accumulates rowcount and can change observable execution interleaving."],"messageIds":[],"optionsSchema":null,"rationale":"A bare execute loop repeats the same statement through separate client calls. Psycopg executemany centralizes batch execution and can pipeline those operations.","references":[],"remediation":"Prefer an existing bulk store method; otherwise send bounded parameter batches through cursor.executemany. Preserve transaction ownership, per-row failure behavior, ordering and backpressure.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_psycopg_executemany.py","status":"active","summary":"Use Psycopg executemany for a serial loop of invariant parameterized writes without row results.","test":"packages/python/tests/rules/test_prefer_psycopg_executemany.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ454","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"src/app/types.py","source":"from pydantic import JsonValue\nvalue: JsonValue\n"}],"fixedFiles":[],"focusPath":"src/app/types.py","id":"canonical-pydantic-json-value","outcome":"accept","scenarioId":"primary","title":"The Pydantic alias is already canonical"},{"expectedCount":1,"files":[{"path":"pyproject.toml","source":"[project]\nname = \"app\"\nversion = \"0.1.0\"\ndependencies = [\"pydantic>=2.5\"]\n"},{"path":"src/app/types.py","source":"type _JsonValue = str | int | float | bool | None | list[_JsonValue] | dict[str, _JsonValue]\ndef _accept(value: _JsonValue) -> None:\n    pass\n"}],"fixedFiles":[],"focusPath":"src/app/types.py","id":"home-grown-json-value","outcome":"reject","scenarioId":"primary","title":"Use Pydantic's canonical recursive JSON value"},{"expectedCount":0,"files":[{"path":"pyproject.toml","source":"[project]\nname = \"app\"\nversion = \"0.1.0\"\ndependencies = [\"pydantic>=2\"]\n"},{"path":"src/app/types.py","source":"from pydantic import JsonValue as 数据\nvalue: 数据\n"}],"fixedFiles":[],"focusPath":"src/app/types.py","id":"native-unicode-canonical-alias","outcome":"accept","scenarioId":"unicode-explicit-alias","title":"Import the canonical JSON value using a native Unicode binding"},{"expectedCount":1,"files":[{"path":"pyproject.toml","source":"[project]\nname = \"app\"\nversion = \"0.1.0\"\ndependencies = [\"pydantic>=2.5\"]\n"},{"path":"src/app/types.py","source":"type _数据 = str | int | float | bool | None | list[_数据] | dict[str, _数据]\ndef _accept(value: _数据) -> None:\n    pass\n"}],"fixedFiles":[],"focusPath":"src/app/types.py","id":"native-unicode-recursive-alias","outcome":"reject","scenarioId":"unicode-explicit-alias","title":"Native Unicode alias names preserve the recursive JSON domain"}],"filePatterns":[],"id":"prefer-pydantic-json-value","key":"python:prefer-pydantic-json-value","languages":["python"],"limitations":["Only exact recursive JSON domains in distributions proven to depend on Pydantic v2 are reported.","Generic, imported, narrower, extended, mutually recursive, ambiguous, generated, and vendored aliases are excluded.","ERROR requires a declared Pydantic floor of at least 2.5 and private annotation-only uses in undecorated, non-escaping module functions. Public aliases, runtime schemas/metadata/introspection, dynamic namespaces and older supported Pydantic targets retain WARNING."],"messageIds":[],"optionsSchema":null,"rationale":"Pydantic's canonical JSON value type avoids divergent recursive static domains in packages that already support it. Runtime schema coercion is a separate contract and must be preserved deliberately.","references":[],"remediation":"For private static contracts, import pydantic.JsonValue under the existing alias name. Preserve public alias ownership and runtime coercion/validation contracts; equivalent static JSON domains do not guarantee equivalent TypeAdapter behavior.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_pydantic_json_value.py","status":"active","summary":"Recursive JSON value alias duplicates `pydantic.JsonValue`.","test":"packages/python/tests/rules/test_prefer_pydantic_json_value.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ527","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_user.py","source":"from pydantic import BaseModel\nclass User(BaseModel):\n    id: int\n    name: str\ndef test_user(result: User):\n    assert result.model_dump(include={'id', 'name'}) == {'id': 1, 'name': 'Alice'}\n"}],"fixedFiles":[],"focusPath":"tests/test_user.py","id":"explicit-projection","outcome":"accept","scenarioId":"primary","title":"One assertion states the selected field contract"},{"expectedCount":1,"files":[{"path":"tests/test_user.py","source":"from pydantic import BaseModel\nclass User(BaseModel):\n    id: int\n    name: str\ndef test_user(result: User):\n    assert result.id == 1\n    assert result.name == 'Alice'\n"}],"fixedFiles":[],"focusPath":"tests/test_user.py","id":"scattered-fields","outcome":"reject","scenarioId":"primary","title":"Two assertions describe one expected model"}],"filePatterns":[],"id":"prefer-pydantic-model-assertion","key":"python:prefer-pydantic-model-assertion","languages":["python"],"limitations":["Only consecutive plain equality or bool/None identity assertions on two distinct declared fields of a source-proven Pydantic model are considered. Models are resolved through declared parameter/local/return annotations and simple construction/validation calls. Fields inherited through a single source-visible base chain are included; ambiguous multiple inheritance is not expanded. Plain properties, private attributes, model_config and ClassVar type wrappers are excluded; Annotated metadata does not turn a data field into a class variable; Final annotations are conservatively excluded as well. Unknown receivers, non-model objects, calls in expected values, assertion messages, repeated fields, nested attributes and custom comparison operators are excluded. This is a review advisory, never an automatic rewrite or assertion deletion."],"messageIds":[],"optionsSchema":null,"rationale":"Consecutive assertions on the same model scatter one expected record across statements and obscure which fields form the test's contract.","references":[],"remediation":"Prefer 'assert result == ExpectedModel(...)' for the complete contract, or 'assert result.model_dump(include={...}) == {...}' for an intentional subset. Keep any needed None guard. Preserve identity, approximate comparisons, custom serialization and private-attribute semantics; do not fill irrelevant fields or use model_construct/model_copy to bypass validation.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_pydantic_model_assertion.py","status":"active","summary":"Compare a Pydantic model or an explicit field projection instead of consecutive property assertions.","test":"packages/python/tests/rules/test_prefer_pydantic_model_assertion.py"},{"aliases":[],"autofix":"safe","category":"testing","code":"SARJ526","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/test_service.py","source":"from typing import Protocol\nclass Port(Protocol):\n    def read(self): ...\nclass Store:\n    def read(self): return 1\nclass Service:\n    def __init__(self, store: Port):\n        self.store = store\n    def read(self): return self.store.read()\ndef test_read():\n    store = Store()\n    service = Service(store)\n    assert service.read() == 1\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"constructed-collaborator","outcome":"reject","scenarioId":"primary","title":"A test constructs its service's store"},{"expectedCount":1,"files":[{"path":"tests/test_store.py","source":"from typing import Protocol\nclass Port(Protocol):\n    def read(self): ...\nclass Store:\n    def read(self): return 1\nclass Service:\n    def __init__(self, store: Port):\n        self.store = store\n    def read(self): return self.store.read()\ndef test_read(store: Store):\n    replacement = Store()\n    assert replacement.read() == 1\n"}],"fixedFiles":[],"focusPath":"tests/test_store.py","id":"duplicate-supplied-store","outcome":"reject","scenarioId":"supplied-store","title":"A second store bypasses the supplied fixture"},{"expectedCount":0,"files":[{"path":"tests/test_service.py","source":"from typing import Protocol\nclass Port(Protocol):\n    def read(self): ...\nclass Store:\n    def read(self): return 1\nclass Service:\n    def __init__(self, store: Port):\n        self.store = store\n    def read(self): return self.store.read()\ndef test_read(store):\n    service = Service(store)\n    assert service.read() == 1\n"}],"fixedFiles":[],"focusPath":"tests/test_service.py","id":"injected-collaborator","outcome":"accept","scenarioId":"primary","title":"The store arrives through pytest"},{"expectedCount":0,"files":[{"path":"app/__init__.py","source":"\"\"\"Example application package.\"\"\"\n"},{"path":"app/stores.py","source":"from abc import ABC, abstractmethod\nclass Queue(ABC):\n    @abstractmethod\n    def retry(self) -> bool: ...\nclass QueueStore(Queue):\n    def __init__(self, resource: object) -> None:\n        self.resource = resource\n    def retry(self) -> bool:\n        return True\nclass Owner:\n    def __init__(self, resource: object) -> None:\n        self.resource = resource\n    def read(self) -> bool:\n        return True\n"},{"path":"tests/__init__.py","source":"\"\"\"Example test package.\"\"\"\n"},{"path":"tests/conftest.py","source":"pytest_plugins = ['tests.fixtures']\n"},{"path":"tests/fixtures.py","source":"import pytest\nfrom app.stores import Owner, QueueStore\n@pytest.fixture\ndef resource() -> object:\n    return object()\n@pytest.fixture\ndef owner(resource: object) -> Owner:\n    return Owner(resource)\n@pytest.fixture\ndef queue_store(resource: object) -> QueueStore:\n    return QueueStore(resource)\n"},{"path":"tests/test_retry.py","source":"def test_retry(queue_store):\n    assert queue_store.retry()\n"}],"fixedFiles":[],"focusPath":"tests/test_retry.py","id":"requested-plugin-collaborator","outcome":"accept","scenarioId":"borrowed-fixture-resource","title":"Request the collaborator through pytest"},{"expectedCount":0,"files":[{"path":"tests/test_store.py","source":"from typing import Protocol\nclass Port(Protocol):\n    def read(self): ...\nclass Store:\n    def read(self): return 1\nclass Service:\n    def __init__(self, store: Port):\n        self.store = store\n    def read(self): return self.store.read()\ndef test_read(store: Store):\n    assert store.read() == 1\n"}],"fixedFiles":[],"focusPath":"tests/test_store.py","id":"reuse-supplied-store","outcome":"accept","scenarioId":"supplied-store","title":"Use the supplied store fixture"},{"expectedCount":1,"files":[{"path":"app/__init__.py","source":"\"\"\"Example application package.\"\"\"\n"},{"path":"app/stores.py","source":"from abc import ABC, abstractmethod\nclass Queue(ABC):\n    @abstractmethod\n    def retry(self) -> bool: ...\nclass QueueStore(Queue):\n    def __init__(self, resource: object) -> None:\n        self.resource = resource\n    def retry(self) -> bool:\n        return True\nclass Owner:\n    def __init__(self, resource: object) -> None:\n        self.resource = resource\n    def read(self) -> bool:\n        return True\n"},{"path":"tests/__init__.py","source":"\"\"\"Example test package.\"\"\"\n"},{"path":"tests/conftest.py","source":"pytest_plugins = ['tests.fixtures']\n"},{"path":"tests/fixtures.py","source":"import pytest\nfrom app.stores import Owner, QueueStore\n@pytest.fixture\ndef resource() -> object:\n    return object()\n@pytest.fixture\ndef owner(resource: object) -> Owner:\n    return Owner(resource)\n@pytest.fixture\ndef queue_store(resource: object) -> QueueStore:\n    return QueueStore(resource)\n"},{"path":"tests/test_retry.py","source":"from app.stores import Owner, QueueStore\ndef test_retry(owner: Owner):\n    queue = QueueStore(owner.resource)\n    queue.retry()\n    assert owner.read()\n"}],"fixedFiles":[],"focusPath":"tests/test_retry.py","id":"unrequested-plugin-collaborator","outcome":"reject","scenarioId":"borrowed-fixture-resource","title":"A visible plugin fixture owns the collaborator"}],"filePatterns":[],"id":"prefer-pytest-dependency-fixtures","key":"python:prefer-pytest-dependency-fixtures","languages":["python"],"limitations":["Source-visible module-level collaborator instances must also enter tests through fixtures. Argument binding must be explicit: starred positional arguments, keyword unpacking and duplicate bindings abstain rather than guessing collaborator ownership.","Safe fixes only reuse an already-requested typed factory inside an authored runtime identity guard over a closed source-owned collaborator graph. They change only its immediate constructor callee; no fixture, signature, guard, setup event or lifecycle is introduced. All other findings require review.","Checks collected pytest functions and plain test classes. A local or inline class construction must feed a proven stored-and-invoked ABC/Protocol collaborator, or a proven BaseSettings instance must be passed to a call. Names alone never identify a service. Constructor failure tests, fixtures, helpers, dynamic/unresolved sources, generated and test-support paths are excluded. No automatic fixture extraction or lifecycle change.","Native HTTPX Client/AsyncClient constructors also enter through fixtures, including managed clients and runtime-import aliases; source definitions are not required. Opaque constructor factories and reassigned or patched providers are excluded. No transport or teardown is synthesized.","Also reports source-resolved constructors whose class matches a supplied fixture annotation or implements its ABC/Protocol. Parametrized values, shadowed constructors and unresolved types are excluded. A different configuration needs a typed factory fixture; the rule never replaces an instance automatically.","Also recognizes an unrequested function-scoped provider in the test module, ancestor conftest or a literal pytest_plugins registration when a source-owned ABC implementation borrows an attribute of a typed requested fixture. Its single assigned instance must perform an operation before a later assertion directly observes the original fixture; constructor-only and direct-subject observations abstain. Only direct one-input providers are supported; factories, variants, broader lifetimes, class-local providers, unexplained overrides and nested/dynamic registration abstain. Parametrized owner data, unresolved marks and visible generation hooks abstain. Ambient marks need direct module/class declarations; imported, read/escaped, augmented or indirectly assigned pytestmark bindings abstain. Provider class matching does not prove resource or lifecycle equivalence; these findings require manual fixture/factory selection and have no automatic fix."],"messageIds":[],"optionsSchema":null,"rationale":"Constructing collaborators in a test bypasses pytest's dependency graph, caching and overrides even when only one test currently needs the setup.","references":[],"remediation":"Request function-scoped store, service and settings fixtures by parameter. Use a typed factory fixture for variants or multiple instances. Keep construction under test explicit and preserve setup ordering and the actual owner's cleanup; do not create global singletons or autouse fixtures.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_pytest_dependency_fixtures.py","status":"active","summary":"Reuse supplied pytest fixtures and pass test collaborators through fixture dependencies.","test":"packages/python/tests/rules/test_prefer_pytest_dependency_fixtures.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ550","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/conftest.py","source":"import pytest\nfrom abc import ABC, abstractmethod\nclass Store(ABC):\n    @abstractmethod\n    def read(self) -> int: ...\nclass SqlStore(Store):\n    def read(self) -> int: return 1\nclass Router:\n    def __init__(self, store: Store) -> None:\n        self.store = store\n@pytest.fixture\ndef router(store: Store) -> Router:\n    return Router(store=store)\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"abstract-injected-collaborator","outcome":"accept","scenarioId":"injected-collaborator","title":"The composition fixture accepts the existing collaborator contract"},{"expectedCount":1,"files":[{"path":"tests/conftest.py","source":"import pytest\nfrom abc import ABC, abstractmethod\nclass Store(ABC):\n    @abstractmethod\n    def read(self) -> int: ...\nclass SqlStore(Store):\n    def read(self) -> int: return 1\nclass Router:\n    def __init__(self, store: Store) -> None:\n        self.store = store\n@pytest.fixture\ndef router(store: SqlStore) -> Router:\n    return Router(store=store)\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"concrete-injected-collaborator","outcome":"reject","scenarioId":"injected-collaborator","title":"A composition fixture exposes a concrete collaborator parameter"}],"filePatterns":[],"id":"prefer-pytest-fixture-contract","key":"python:prefer-pytest-fixture-contract","languages":["python"],"limitations":["Proven injected-parameter matches are blocking errors. Fixture export suggestions remain advisory warnings because cross-module consumers are unknown.","Only proven pytest/pytest-asyncio fixtures and source-visible concrete subclasses of an ABC with declared abstract members or a Protocol are checked. No interface is invented or inferred from a class name.","Single-instance annotations and Iterator/AsyncIterator/Generator/AsyncGenerator fixture annotations are supported; factory, union and collection contracts are excluded.","Required fixture and ordinary pytest test parameters support single-instance annotations, compatible direct method calls and source-resolved forwarding to existing contract-typed parameters. Defaults, positional-only arguments, receivers and parametrized values are excluded.","Implementation-only operations, variant operation signatures, owner-relative Self, runtime identity checks, unknown escapes and nested captures retain their concrete annotation. Cross-module fixture consumers remain unknown; exported fixture findings require review.","Generated files, unresolved or opaque decorated classes/functions, generic and ambiguous contracts, dynamic collection, inheritance beyond eight levels and automatic annotation rewrites are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Concrete fixture exports and injected parameter annotations couple consumers to an implementation even when an existing ABC or Protocol supports the consumed operations.","references":[],"remediation":"Keep concrete construction in the fixture and annotate supplied instances and injected parameters with their existing ABC or Protocol. Retain a reviewed concrete annotation for implementation-specific behavior or stronger concrete guarantees.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_pytest_fixture_contract.py","status":"active","summary":"Expose existing abstract contracts from pytest fixtures and injected parameters.","test":"packages/python/tests/rules/test_prefer_pytest_fixture_contract.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ476","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/conftest.py","source":"import pytest\nfrom psycopg_pool import AsyncConnectionPool\n\n@pytest.fixture\ndef pool() -> AsyncConnectionPool[object]:\n    return AsyncConnectionPool[object](open=False)\n"},{"path":"tests/test_app.py","source":"async def test_app(pool):\n    app = Application(pool)\n    assert pool.closed\n    async with lifespan(app):\n        assert not pool.closed\n    assert pool.closed\n"}],"fixedFiles":[],"focusPath":"tests/test_app.py","id":"allocation-fixture-keeps-application-lifespan","outcome":"accept","scenarioId":"application-lifecycle","title":"An unopened fixture leaves open and close ownership with the application"},{"expectedCount":0,"files":[{"path":"tests/conftest.py","source":"import pytest\nfrom psycopg_pool import ConnectionPool\n\n@pytest.fixture\ndef pool() -> ConnectionPool[object]:\n    return ConnectionPool[object](open=False)\n"},{"path":"tests/test_app.py","source":"def test_app(pool):\n    app = Application(pool)\n    assert app\n"}],"fixedFiles":[],"focusPath":"tests/test_app.py","id":"injected-generic-pool","outcome":"accept","scenarioId":"primary","title":"A test requests its shared resource"},{"expectedCount":1,"files":[{"path":"tests/conftest.py","source":"from psycopg_pool import AsyncConnectionPool\n\ndef pool() -> AsyncConnectionPool[object]:\n    return AsyncConnectionPool[object](open=False)\n"},{"path":"tests/test_app.py","source":"from tests.conftest import pool\n\nasync def test_app():\n    resource = pool()\n    app = Application(resource)\n    async with lifespan(app):\n        assert not resource.closed\n    assert resource.closed\n"}],"fixedFiles":[],"focusPath":"tests/test_app.py","id":"manual-pool-with-application-lifespan","outcome":"reject","scenarioId":"application-lifecycle","title":"Application-owned lifecycle still uses a fixture dependency"},{"expectedCount":1,"files":[{"path":"tests/conftest.py","source":"from psycopg_pool import ConnectionPool\n\ndef pool() -> ConnectionPool[object]:\n    return ConnectionPool[object](open=False)\n"},{"path":"tests/test_app.py","source":"from tests.conftest import pool\n\ndef test_app():\n    resource = pool()\n    app = Application(resource)\n    assert app\n"}],"fixedFiles":[],"focusPath":"tests/test_app.py","id":"manual-shared-generic-pool","outcome":"reject","scenarioId":"primary","title":"A test calls shared generic pool setup"}],"filePatterns":[],"id":"prefer-pytest-fixture-injection","key":"python:prefer-pytest-fixture-injection","languages":["python"],"limitations":["Only directly collected module-level test functions in conventional pytest module names are checked.","Only unshadowed runtime imports from a uniquely resolved conftest in the test's ancestor directories are followed under conventional pytest discovery.","Helpers must be undecorated synchronous functions with no formal inputs and one direct return of an imported psycopg_pool pool constructor.","Async pools require an explicit literal open=False, no unpacked arguments, and no calls inside constructor arguments; opened or dynamically configured async pools need lifecycle-specific fixture design.","Setup must be the first executable statement and immediately feed a plain call, or appear inline as a direct argument of that first call.","Class tests, decorated helpers, unknown test decorators, dynamic imports, re-exports, and ambiguous or out-of-bound source paths are excluded.","Tests with statically visible callable references in any scope are excluded; scope-ambiguous references are handled conservatively.","Command-line conftest discovery overrides and external pytest plugin registrations are not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"Calling a shared conftest pool helper during initial test setup bypasses an explicit pytest fixture dependency, per-test caching, and fixture overrides, even when the helper already centralizes allocation. This preference does not establish a resource leak.","references":[],"remediation":"Request one function-scoped fixture visible to the test. Declare required setup ordering as fixture dependencies and preserve initial pool state and cleanup ownership. An unopened allocation fixture should return the pool without opening or closing it when application lifespan owns that lifecycle. Use yield teardown only when the fixture owns cleanup; keep factories for variants or fresh instances.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_pytest_fixture_injection.py","status":"active","summary":"Request shared database-pool setup through a pytest fixture.","test":"packages/python/tests/rules/test_prefer_pytest_fixture_injection.py"},{"aliases":[],"autofix":"safe","category":"correctness","code":"SARJ460","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/validation.py","source":"import re\n\ndef validate(value):\n    if not re.match(r'^[a-z]+$', value):\n        raise ValueError('invalid value')\n"}],"fixedFiles":[],"focusPath":"app/validation.py","id":"anchored-rejection","outcome":"reject","scenarioId":"primary","title":"An end anchor permits a final newline"},{"expectedCount":1,"files":[{"path":"app/validation.py","source":"import re\n\ndef validate(value):\n    if not re.ｍａｔｃｈ(r'^a$', value):\n        raise ValueError('invalid')\n"}],"fixedFiles":[],"focusPath":"app/validation.py","id":"native-match-name","outcome":"reject","scenarioId":"native-symbol-binding","title":"Normalized match still permits a final newline"},{"expectedCount":0,"files":[{"path":"app/validation.py","source":"import re\n\ndef validate(value):\n    if not re.ｍａｔｃｈ(r'^a\\Z', value):\n        raise ValueError('invalid')\n"}],"fixedFiles":[],"focusPath":"app/validation.py","id":"native-strict-end","outcome":"accept","scenarioId":"native-symbol-binding","title":"A strict end anchor rejects the final newline"},{"expectedCount":0,"files":[{"path":"app/validation.py","source":"import re\n\ndef validate(value):\n    if not re.fullmatch(r'[a-z]+', value):\n        raise ValueError('invalid value')\n"}],"fixedFiles":[],"focusPath":"app/validation.py","id":"whole-value-rejection","outcome":"accept","scenarioId":"primary","title":"Validate the entire value"}],"filePatterns":[],"id":"prefer-regex-fullmatch","key":"python:prefer-regex-fullmatch","languages":["python"],"limitations":["Autofix only replaces the final `$` with strict `\\Z` in one simple anchored ASCII character-class pattern passed to an existing canonical module-qualified re.match call with an exact string-literal subject containing no newline and no flags. It preserves the existing callee, argument order, literal quoting, and boolean-validation context; explicit mocks, namespace reflection, mutations, escapes, dynamic inputs, and other patterns remain for review.","Checks direct negative match/is-None rejection guards and returns of bool(match), not match, or match is/is-not None. Match-object returns, extraction, search, and compound conditions are excluded.","Patterns must be valid string or bytes literals bounded by `^` or `\\A` and an unescaped outer `$`; top-level alternation, inline extension groups other than noncapturing groups, and flags other than literal zero are excluded.","Only imported standard-library re functions and unique, unconditional module or same-function compile bindings are resolved. Rebinding, import shadowing, escaped regex objects or modules, explicit match positions, tests, and generated files are excluded.","Binding checks are deliberately file-wide, so unrelated same-named locals can suppress findings. Dynamic monkeypatching and interprocedural changes are not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"The regex `$` anchor can match before a final newline, so an anchored match can accept a value that was not fully validated.","references":[],"remediation":"Use `re.fullmatch` or the compiled pattern's `fullmatch` method for strict whole-value validation. Preserve deliberate terminal-newline acceptance or an existing match contract with a reasoned exact SARJ460 exception; do not rewrite that behavior automatically.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_regex_fullmatch.py","status":"active","summary":"An anchored regex validation predicate does not require a full-string match.","test":"packages/python/tests/rules/test_prefer_regex_fullmatch.py"},{"aliases":["discourage-nullable-constructor-parameters"],"autofix":"none","category":"architecture","code":"SARJ468","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/service.py","source":"class Service:\n    def __init__(self, agent_name: str = \"\") -> None:\n        self.agent_name = agent_name\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"defaulted-choice","outcome":"reject","scenarioId":"primary","title":"Constructor silently selects an empty configuration"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"class Service:\n    def __init__(self, agent_name: str | None) -> None:\n        self.agent_name = agent_name\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"explicit-absent-choice","outcome":"accept","scenarioId":"primary","title":"Caller explicitly chooses an absent configuration"}],"filePatterns":[],"id":"prefer-required-constructor-parameters","key":"python:prefer-required-constructor-parameters","languages":["python"],"limitations":["This architectural preference is advisory: a default is not necessarily a correctness defect.","Only directly defined __init__ signatures are checked, including class-suite branches and static/class methods; assigned or generated constructors and dataclass fields are not inferred.","Tests, generated files, stubs, and proven overload declarations are excluded. Ambiguous imports, namespace mutation, or escaped overload handles retain the advisory conservatively.","Dynamic decorator replacement and cross-function mutation timing are not inferred.","Removing defaults can break callers, so the rule does not autofix signatures."],"messageIds":[],"optionsSchema":null,"rationale":"Defaulted constructor parameters hide choices at the composition boundary. Replacing None with an empty string, a constant, or a factory still permits an implicit choice. A required nullable parameter makes a meaningful absent value explicit without inventing a sentinel.","references":[],"remediation":"Require the parameter and update callers to pass their choice explicitly; preserve meaningful None values. Suppress intentional framework or public-library defaults with a compatibility reason.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_required_constructor_parameters.py","status":"active","summary":"Prefer required constructor parameters so callers choose dependencies and configuration explicitly.","test":"packages/python/tests/rules/test_prefer_required_constructor_parameters.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ534","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/records.py","source":"from typing import TypeGuard\ndef is_record(value: object) -> TypeGuard[dict[str, str]]:\n    return isinstance(value, dict)\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"type-guard-assertion","outcome":"reject","scenarioId":"primary","title":"A claimed narrowing is not a schema validator"},{"expectedCount":0,"files":[{"path":"app/records.py","source":"from pydantic import BaseModel\nclass Record(BaseModel):\n    name: str\ndef parse_record(value: object) -> Record:\n    return Record.model_validate(value)\n"}],"fixedFiles":[],"focusPath":"app/records.py","id":"validated-result","outcome":"accept","scenarioId":"primary","title":"Return a validated domain value"}],"filePatterns":[],"id":"prefer-runtime-validation","key":"python:prefer-runtime-validation","languages":["python"],"limitations":["Warns on import-proven typing/typing_extensions TypeGuard return annotations, including quoted annotations and stable module type aliases. This is an architecture preference even when a particular guard correctly validates its result.","Shadowed or mutated imports, dynamic namespaces, wildcard imports, generated/vendor files and unresolved imported application aliases abstain. Boolean predicates and TypeIs are outside this policy; existing guard correctness rules still check unsafe narrowing.","No automatic rewrite: validated values, boolean predicates and rejecting parsers have different APIs and error behavior. Runtime validation must actually inspect or construct the accepted value."],"messageIds":[],"optionsSchema":null,"rationale":"TypeGuard tells the type checker to trust a predicate's claimed result; its annotation performs no runtime validation. Returning a validated domain value keeps the accepted schema and downstream contract together.","references":[],"remediation":"At data boundaries, validate into a named Pydantic model or TypeAdapter and return the validated value. For type dispatch, use actual isinstance checks or match class patterns. Do not replace TypeGuard with a cast or merely change its annotation to TypeIs. Explain indispensable low-level refinements with a local exact-code exception.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_runtime_validation.py","status":"active","summary":"Prefer runtime validation to TypeGuard assertions.","test":"packages/python/tests/rules/test_prefer_runtime_validation.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ097","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/retry.py","source":"# Retry HTTP responses 408 and 429.\nRETRYABLE_HTTP_STATUS_CODES = {408, 429}\n"}],"fixedFiles":[],"focusPath":"app/retry.py","id":"bare-http-status-collection","outcome":"reject","scenarioId":"http-status","title":"HTTP status collection uses bare integers"},{"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/retry.py","source":"from http import HTTPStatus\n\nRETRYABLE_HTTP_STATUS_CODES = {\n    HTTPStatus.REQUEST_TIMEOUT,\n    HTTPStatus.TOO_MANY_REQUESTS,\n}\n"}],"fixedFiles":[],"focusPath":"app/retry.py","id":"named-http-status-collection","outcome":"accept","scenarioId":"http-status","title":"HTTP status members carry protocol meaning"},{"expectedCount":0,"files":[{"path":"app/settings.py","source":"# The upstream gateway closes idle requests first.\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, non-HTTP protocols, vendor status codes, and values already carrying the fact are excluded; tests remain in scope."],"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":"safe","category":"maintainability","code":"SARJ078","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/builder.py","source":"from typing import final\nclass Builder:\n    @final\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":"fluent-method","title":"Fluent method names its enclosing class"},{"expectedCount":0,"files":[{"path":"app/builder.py","source":"from typing import Self, final\n\nclass Builder:\n    @final\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":"fluent-method","title":"Fluent method preserves subclass type"}],"filePatterns":[],"id":"prefer-self-type-annotation","key":"python:prefer-self-type-annotation","languages":["python"],"limitations":["Safe fixes replace only error-level return annotations when Self is already imported unambiguously. Advisory contracts, added imports, shadowed aliases and commented annotations remain review-only.","Only final instance methods on stable undecorated module classes without bases, with a closed canonical final provider and every return equal to the unrebound receiver, block as errors. Other fluent methods and class constructors remain advisory to preserve valid override and construction contracts.","Owned class and method mutation, namespace access, and escaped class references retain warning severity.","Class creation must have inert literal members and supported method signatures/decorators; special methods and unknown descriptor values remain advisory.","Standalone files with unknown module identity and non-typing imports retain warnings because self-imported class aliases cannot be resolved safely.","Only undecorated or provenance-safe methods whose every return directly preserves the receiver type are analyzed.","Static methods, generators, final classes, metaclasses, inherited-base annotations, and behavior-changing decorators are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"`Self` preserves the concrete subclass type when an inherited method returns its receiver or constructs through its class receiver.","references":[],"remediation":"Import `Self` from `typing` (or `typing_extensions` on Python before 3.11) 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":"Prefer `Self` for self-returning methods and alternate constructors.","test":"packages/python/tests/rules/test_prefer_self_type_annotation.py"},{"aliases":[],"autofix":"safe","category":"style","code":"SARJ431","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/policy.py","source":"class AccessCase:\n    tags: frozenset[str]\n\ndef accepts(case: AccessCase):\n    return bool(case.tags & {'read', 'write'})\n"},{"path":"pyproject.toml","source":"[project]\nname = \"example\"\nversion = \"0.1.0\"\n"}],"fixedFiles":[],"focusPath":"app/policy.py","id":"discarded-intersection","outcome":"reject","scenarioId":"primary","title":"A declared set field is used only for an overlap test"},{"expectedCount":0,"files":[{"path":"app/policy.py","source":"allowed = {'read', 'write'}\nrequested = set(scopes)\nif allowed.isdisjoint(requested):\n    deny()\n"}],"fixedFiles":[],"focusPath":"app/policy.py","id":"named-disjoint-predicate","outcome":"accept","scenarioId":"primary","title":"Set overlap is expressed without allocating an intersection"}],"filePatterns":[],"id":"prefer-set-isdisjoint","key":"python:prefer-set-isdisjoint","languages":["python"],"limitations":["Safe fixes require both operands to be comment-free set literals containing only native primitive constants. Dynamic/custom elements and nonliteral operands remain review-only.","Explicit unshadowed built-in bool calls are predicates even in assignments and returns. Raw stored or returned intersections remain values, not predicates.","Set receivers must come from a literal, comprehension, constructor, dominating local assignment, or an unchanged parameter's directly declared first-party set/frozenset field.","Dictionary key-view receivers require a literal/comprehension/constructor or one unreassigned, dominating local binding. Custom mappings and unproven parameters are excluded.","Declared fields rely on the annotated contract. Unknown or union field types, properties, explicit owner/field reassignment, branch-merged bindings, stored intersections, and generated files are excluded. Captured bindings in deferred lambda/generator bodies and starred intersection arguments are excluded. The other operand must be a known iterable.","Nonliteral intersections remain suggestions because short-circuiting may make custom element equality or hashing side effects observable."],"messageIds":[],"optionsSchema":null,"rationale":"`isdisjoint` names the overlap predicate directly and avoids allocating an intersection that is immediately discarded.","references":[],"remediation":"Use `left.isdisjoint(right)` and negate it when the condition requires overlap.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_set_isdisjoint.py","status":"active","summary":"Prefer `set.isdisjoint` when a built-in set intersection is used only as a boolean predicate.","test":"packages/python/tests/rules/test_prefer_set_isdisjoint.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ561","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/store.py","source":"from psycopg import AsyncConnection\nasync def load(connection: AsyncConnection):\n    async with connection.cursor() as cursor:\n        await cursor.execute('SELECT id FROM item')\n        rows = await cursor.fetchall()\n        await cursor.execute('SELECT id FROM owner')\n        return rows, await cursor.fetchall()\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"consumed-results","outcome":"accept","scenarioId":"primary","title":"Reuse a cursor after consuming the result"},{"expectedCount":1,"files":[{"path":"app/store.py","source":"from psycopg import AsyncConnection\nasync def load(connection: AsyncConnection):\n    async with connection.cursor() as cursor:\n        await cursor.execute('SELECT id FROM item')\n    async with connection.cursor() as cursor:\n        await cursor.execute('SELECT id FROM owner')\n"}],"fixedFiles":[],"focusPath":"app/store.py","id":"repeated-scopes","outcome":"reject","scenarioId":"primary","title":"Review repeated cursor scopes"}],"filePatterns":[],"id":"prefer-single-psycopg-cursor","key":"python:prefer-single-psycopg-cursor","languages":["python"],"limitations":["Checks import-proven Psycopg cursor creation, including typed connection parameters and constructor-injected pools. Emits one review warning at the first repeated creation in each lexical function.","Cursors in separate functions or directly mutually exclusive if/else branches do not establish repeated creation. Wider path feasibility, mutually exclusive match/exception cases and helper-created cursors are not inferred.","Multiple cursors are valid for independent result sets, different row factories, different connections and pipelines. This advisory requires review rather than claiming a database defect or automatically merging resource scopes.","Generated code and unknown cursor-like APIs are excluded. Tests remain checked when they directly create proven Psycopg cursors."],"messageIds":[],"optionsSchema":null,"rationale":"Repeated cursor construction can obscure one function's query orchestration and duplicate setup that could share a cursor.","references":[],"remediation":"Review whether one cursor can own these operations. Consume each result before reuse, retain distinct row factories or independent pipeline results when required, and preserve connection and transaction boundaries.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_single_psycopg_cursor.py","status":"active","summary":"Review repeated Psycopg cursor creation within one function.","test":"packages/python/tests/rules/test_prefer_single_psycopg_cursor.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ006","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/service.py","source":"class Order[T]:\n    statuses = ('pending', 'shipped')\n    status: str\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"generic-builtin-string","outcome":"reject","scenarioId":"provider-identity","title":"An unrelated generic parameter preserves the builtin string field"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"class Order[str]:\n    statuses = ('pending', 'shipped')\n    status: str\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"generic-string-binding","outcome":"accept","scenarioId":"provider-identity","title":"A generic parameter does not become the builtin string type"},{"expectedCount":0,"files":[{"path":"app/render.py","source":"def render(kind: str) -> str:\n    if kind == \"text\":\n        return \"Text\"\n    if kind == \"image\":\n        return \"Image\"\n    return render_plugin(kind)\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"open-string-dispatch","outcome":"accept","scenarioId":"rejecting-dispatch","title":"Dispatch forwards unlisted strings"},{"expectedCount":0,"files":[{"path":"app/render.py","source":"def render(kind: str) -> str:\n    return kind in ({r\"text\", r\"image\"})\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"parenthesized-open-membership","outcome":"accept","scenarioId":"membership-admission","title":"Membership data without rejection keeps strings"},{"expectedCount":1,"files":[{"path":"app/render.py","source":"def render(kind: str) -> str:\n    if kind not in ({r\"text\", r\"image\"}):\n        raise ValueError(\"unsupported kind\")\n    return kind\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"parenthesized-rejecting-membership","outcome":"reject","scenarioId":"membership-admission","title":"Parentheses do not open a rejecting string domain"},{"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":"choice-field","title":"String field backed by a closed choice collection"},{"expectedCount":1,"files":[{"path":"app/render.py","source":"def render(kind: str) -> str:\n    match kind:\n        case \"text\":\n            return \"Text\"\n        case \"image\":\n            return \"Image\"\n        case _:\n            raise ValueError(\"unsupported kind\")\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"rejecting-string-dispatch","outcome":"reject","scenarioId":"rejecting-dispatch","title":"Dispatch rejects every unlisted string"},{"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":"choice-field","title":"Closed domain represented by a string enum"},{"expectedCount":0,"files":[{"path":"app/render.py","source":"def render(kind: str) -> str:\n    match\tkind:\n        case\t\"text\":\n            return \"Text\"\n        case\t\"image\":\n            return \"Image\"\n        case\t_:\n            return kind\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"tabbed-open-match","outcome":"accept","scenarioId":"tabbed-match-admission","title":"An open tab-separated match keeps string dispatch"},{"expectedCount":1,"files":[{"path":"app/render.py","source":"def render(kind: str) -> str:\n    match\tkind:\n        case\t\"text\":\n            return \"Text\"\n        case\t\"image\":\n            return \"Image\"\n        case\t_:\n            raise ValueError(\"unsupported kind\")\n"}],"fixedFiles":[],"focusPath":"app/render.py","id":"tabbed-rejecting-match","outcome":"reject","scenarioId":"tabbed-match-admission","title":"Tab-separated matching keeps the closed string domain"}],"filePatterns":[],"id":"prefer-str-enum","key":"python:prefer-str-enum","languages":["python"],"limitations":["Automatic fixes reuse one existing canonical module Literal alias only when its finite string domain strictly contains a closed private nested helper's handled literal match cases and every direct caller supplies a handled-case literal. Reachable fallback exceptions retain manual ownership because their tracebacks can expose private helper frames. An unhandled alias member must keep the existing rejecting fallback reachable; exact handled-domain aliases remain manual because narrowing would introduce an unreachable-pattern type error. Only the str parameter annotation changes; defaults, comments, generics, aliases/escapes, reflection/frame access, shared bodies and runtime enum creation remain manual.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","The rule requires a sibling choice collection or dispatch that explicitly rejects unlisted strings.","Generated code, external vocabularies, open-ended name domains, and test-only comparison clusters are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A named closed domain lets type checking and review catch invalid values and incomplete handling.","references":[],"remediation":"Define a `StrEnum`, or a named `Literal` alias when enum runtime behavior is unnecessary.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_str_enum.py","status":"active","summary":"Prefer `StrEnum` for application-owned string domains with explicit closed-set evidence.","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":"from collections import namedtuple\n\nRow = namedtuple('Row', ['id', 'name'])\n"}],"fixedFiles":[],"focusPath":"models.py","id":"collections-namedtuple","outcome":"reject","scenarioId":"primary","title":"Static record omits field types"},{"expectedCount":0,"files":[{"path":"models.py","source":"from typing import NamedTuple\n\nclass Row(NamedTuple):\n    id: int\n    name: str\n\nrow = Row(1, 'Ada')\nid, name = row\n"}],"fixedFiles":[],"focusPath":"models.py","id":"typed-named-tuple","outcome":"accept","scenarioId":"primary","title":"Typed tuple-compatible record"}],"filePatterns":[],"id":"prefer-struct-over-namedtuple","key":"python:prefer-struct-over-namedtuple","languages":["python"],"limitations":["Only statically named declarations with non-empty literal fields and proven `collections` provenance are reported.","Tests, generated or vendored code, compatibility branches, dynamic factories, renamed fields, and already annotated records are excluded.","Visible collections-module mutations, namespace capabilities, and escaped native factory references withdraw identity proof conservatively across the file; opaque external mutation is not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"A static `collections.namedtuple` factory declares field names but cannot declare their types.","references":[],"remediation":"Use a class-based `typing.NamedTuple` when tuple behavior is part of the contract. Use a frozen, slotted dataclass or an existing validation model only when changing tuple semantics is safe.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_struct_over_namedtuple.py","status":"active","summary":"Prefer typed declarations for static application-owned `collections.namedtuple` records.","test":"packages/python/tests/rules/test_prefer_struct_over_namedtuple.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ533","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/cli.py","source":"import typer\ndef main(service: str, attempts: int = 3) -> None:\n    deploy(service, attempts)\nif __name__ == '__main__':\n    typer.run(main)\n"}],"fixedFiles":[],"focusPath":"app/cli.py","id":"annotated-typer","outcome":"accept","scenarioId":"command-annotations","title":"Typer parses the declared parameter type"},{"expectedCount":1,"files":[{"path":"app/cli.py","source":"import sys\ndef main(arguments: list[str]) -> None:\n    deploy(arguments[0])\nmain(sys.argv[1:])\n"}],"fixedFiles":[],"focusPath":"app/cli.py","id":"raw-argv","outcome":"reject","scenarioId":"primary","title":"Positional strings hide the CLI contract"},{"expectedCount":0,"files":[{"path":"app/cli.py","source":"import typer\ndef main(service: str, attempts: int = 3) -> None:\n    deploy(service, attempts)\nif __name__ == '__main__':\n    typer.run(main)\n"}],"fixedFiles":[],"focusPath":"app/cli.py","id":"typed-typer","outcome":"accept","scenarioId":"primary","title":"The command declares each argument"},{"expectedCount":1,"files":[{"path":"app/cli.py","source":"import typer\ndef main(service):\n    deploy(service)\ntyper.run(main)\n"}],"fixedFiles":[],"focusPath":"app/cli.py","id":"untyped-typer","outcome":"reject","scenarioId":"command-annotations","title":"Typer still needs parameter annotations"}],"filePatterns":[],"id":"prefer-typed-cli","key":"python:prefer-typed-cli","languages":["python"],"limitations":["Checks import-proven sys.argv reads, argparse/optparse parser construction, getopt parsing and Click command construction. sys.argv[0] and its direct nonempty guard with a literal string fallback, assignment targets and forwarding through args= to a proven local Typer application are retained. A source-proven isolated, fixed-arity path transport with a raising guard and checked Path conversion is also retained.","Checks parameters of source-visible functions passed directly to typer.run or registered with a stable local Typer application's command/callback decorator. Missing annotations and bare Any/object, including Annotated wrappers, are reported. Ordinary functions and unrelated command decorators are excluded.","Stable local import and application aliases are followed. Shadowed or mutated imports, wildcard imports, dynamic namespaces and generated/vendor files abstain. Imported command functions, dynamic registrations and composite erased types require separate type checking.","No automatic rewrite: positional/option compatibility and conversion behavior require review. This policy does not claim every argparse or Click program is incorrectly validated."],"messageIds":[],"optionsSchema":null,"rationale":"A list of argv strings does not declare positional meaning, conversions or option validation. Typer derives those contracts from the command signature and rejects invalid arguments before calling application code.","references":[],"remediation":"Use typer.run on a command with explicit parameter types, or register typed functions with Typer.command/callback. Preserve option names, defaults, validation and exit behavior during migration.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_typed_cli.py","status":"active","summary":"Declare CLI arguments with typed Typer commands.","test":"packages/python/tests/rules/test_prefer_typed_cli.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ535","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/__init__.py","source":"# package\n"},{"path":"app/parser.py","source":"import json\ndef parse(text: str) -> dict[str, object]:\n    return json.loads(text)\n"},{"path":"app/state.py","source":"from .parser import parse\ndef name(text: str):\n    return parse(text).get('name')\n"}],"fixedFiles":[],"focusPath":"app/state.py","id":"imported-parser-contract","outcome":"reject","scenarioId":"imported-return","title":"Imported parsing helpers keep their raw mapping contract"},{"expectedCount":0,"files":[{"path":"app/__init__.py","source":"# package\n"},{"path":"app/parser.py","source":"from pydantic import BaseModel\nclass State(BaseModel):\n    labels: dict[str, str]\ndef parse(text: str) -> State:\n    return State.model_validate_json(text)\n"},{"path":"app/state.py","source":"from .parser import parse\ndef name(text: str):\n    return parse(text).labels.get('name')\n"}],"fixedFiles":[],"focusPath":"app/state.py","id":"imported-validated-model","outcome":"accept","scenarioId":"imported-return","title":"An imported parser validates a named model"},{"expectedCount":1,"files":[{"path":"app/state.py","source":"def service_name(state: dict[str, object]):\n    return state.get('name')\n"}],"fixedFiles":[],"focusPath":"app/state.py","id":"opaque-get","outcome":"reject","scenarioId":"primary","title":"The field has no declared type"},{"expectedCount":0,"files":[{"path":"app/state.py","source":"from typing import TypedDict\nclass State(TypedDict):\n    name: str\ndef service_name(state: State) -> str | None:\n    return state.get('name')\n"}],"fixedFiles":[],"focusPath":"app/state.py","id":"typed-get","outcome":"accept","scenarioId":"primary","title":"TypedDict retains the field contract"},{"expectedCount":1,"files":[{"path":"app/state.py","source":"import json\ndef name(text: str):\n    state = json.loads(text)\n    return state.get('name')\n"}],"fixedFiles":[],"focusPath":"app/state.py","id":"unchecked-json","outcome":"reject","scenarioId":"decoded-json","title":"Decoded JSON still needs validation"},{"expectedCount":0,"files":[{"path":"app/state.py","source":"from pydantic import BaseModel\nclass State(BaseModel):\n    name: str\ndef name(text: str) -> str:\n    return State.model_validate_json(text).name\n"}],"fixedFiles":[],"focusPath":"app/state.py","id":"validated-json","outcome":"accept","scenarioId":"decoded-json","title":"Validate JSON before reading its fields"}],"filePatterns":[],"id":"prefer-typed-mapping-get","key":"python:prefer-typed-mapping-get","languages":["python"],"limitations":["Checks get calls on string mappings annotated with object, Any or JsonValue values, bare standard dict/Mapping annotations, explicit Any/object receivers and import-proven json.load/loads results. Source-visible undecorated local or imported function returns, stable aliases, relative imports, reexports, projections and chained gets retain raw provenance through bounded resolution. Unannotated helpers are followed when every return is proven schema-free; precise return annotations retain their contract.","Precise mappings and TypedDicts, arbitrary unannotated parameters, application methods named get, unresolved external return types, ambiguous assignments and custom JSON object hooks abstain. Imported helpers are parsed without executing them, using a bounded per-file resolver that shares captured source inputs; dynamic or mutated namespaces, decorated functions and recursive or unresolved returns abstain. This syntax analysis does not replace the type checker.","Registered Pydantic raw normalization callbacks retain their validation boundary. Generated/vendor sources and wildcard or dynamic namespaces are excluded. No automatic rewrite because validation, defaults, missing keys and error handling can change behavior.","Existing mapping-shape and external-boundary diagnostics take precedence at the same read when selected together; selecting this rule alone still reports it.","Chained reads with the same starting position produce one actionable warning for their raw receiver."],"messageIds":[],"optionsSchema":null,"rationale":"A get call on an Any/object-valued mapping or unchecked decoded JSON does not declare the field's type or distinguish a misspelled field from missing data. A default can conceal that gap.","references":[],"remediation":"Validate external data into a named Pydantic model before reading its attributes. For trusted internal records, declare a TypedDict or another precise record. Preserve legitimate get calls on typed mappings, optional TypedDict fields and validated normalization callbacks.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_typed_mapping_get.py","status":"active","summary":"Read named fields from typed records instead of schema-free mappings.","test":"packages/python/tests/rules/test_prefer_typed_mapping_get.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ530","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/settings.py","source":"from enum import StrEnum\nfrom pydantic import Field\nfrom pydantic_settings import BaseSettings\nclass Mode(StrEnum):\n    CLOUD = 'cloud'\nclass Settings(BaseSettings):\n    mode: str = Field(default=Mode.CLOUD, alias='MODE')\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"enum-default-erased","outcome":"reject","scenarioId":"primary","title":"A primitive annotation erases an enum default"},{"expectedCount":0,"files":[{"path":"app/settings.py","source":"from enum import StrEnum\nfrom pydantic import Field\nfrom pydantic_settings import BaseSettings\nclass Mode(StrEnum):\n    CLOUD = 'cloud'\nclass Settings(BaseSettings):\n    mode: Mode = Field(default=Mode.CLOUD, alias='MODE')\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"enum-field-preserved","outcome":"accept","scenarioId":"primary","title":"The field retains its domain type"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, PositiveFloat\nclass Audio(BaseModel):\n    duration_seconds: PositiveFloat\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"positive-model-measurement","outcome":"accept","scenarioId":"numeric-model-domain","title":"The model declares a positive measurement"},{"expectedCount":1,"files":[{"path":"app/settings.py","source":"from pydantic_settings import BaseSettings\nclass Settings(BaseSettings):\n    voice: str = 'voice_a'\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"raw-token-default","outcome":"reject","scenarioId":"token-default-domain","title":"A raw token default does not declare its domain"},{"expectedCount":0,"files":[{"path":"app/settings.py","source":"from enum import StrEnum\nfrom pydantic_settings import BaseSettings\nclass Voice(StrEnum):\n    MAIN = 'voice_a'\nclass Settings(BaseSettings):\n    voice: Voice = Voice.MAIN\n"}],"fixedFiles":[],"focusPath":"app/settings.py","id":"typed-token-default","outcome":"accept","scenarioId":"token-default-domain","title":"A closed token default retains its enum domain"},{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel\nclass Audio(BaseModel):\n    duration_seconds: float\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"unbounded-model-measurement","outcome":"reject","scenarioId":"numeric-model-domain","title":"A measurement has no declared numeric domain"}],"filePatterns":[],"id":"prefer-typed-settings-field","key":"python:prefer-typed-settings-field","languages":["python"],"limitations":["Source-proven BaseSettings and BaseModel fields are checked, including source-visible local inheritance and standard import aliases. Existing constrained types, explicit signed bounds, private attributes, model_config and ClassVar/Final metadata remain valid.","Raw numeric fields request an explicit valid domain. Exact zero bounds prefer the matching standard constrained type; stronger positive bounds retain every original constraint. Numeric intent is not inferred from names and a signed domain remains a deliberate option.","Nonempty short identifier-like string defaults request a StrEnum for closed choices or a nominal string type for an open vocabulary. Empty strings, prose, URL handling and existing enum/NewType contracts remain distinct. A default does not prove that every possible value is in a finite enum.","Field providers and annotations use lexical scope, including generic shadowing; Optional and Annotated forms retain their original optionality. Dynamic Field keyword expansions abstain. No automatic rewrite changes coercion, aliases, environment parsing, signed sentinels, NaN/infinity or serialized contracts."],"messageIds":[],"optionsSchema":null,"rationale":"Bare model numbers omit their valid domain; raw token defaults hide a choice or identifier domain. A primitive annotation can also erase an enum, nominal identifier or URL contract demonstrated by its default.","references":[],"remediation":"Reuse the default's enum or NewType, use a Pydantic URL type with the accepted schemes, and prefer PositiveInt/NonNegativeInt or PositiveFloat/NonNegativeFloat for exact zero bounds. Keep units, aliases and source precedence explicit. Use SecretStr for secrets, existing domain identifiers, nested settings with default_factory, declarative bounds and pure computed properties where their contracts justify them. Validate textual environment values; do not enable blanket strict mode or change coercion, optionality or startup behavior without checking real environment inputs.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_typed_settings_field.py","status":"active","summary":"Declare numeric model domains and use typed choices for token-valued defaults.","test":"packages/python/tests/rules/test_prefer_typed_settings_field.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ562","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/router.py","source":"from uuid import UUID\ndef same_owner(item, request):\n    return UUID(item.owner_id) == UUID(request.owner_id)\n"}],"fixedFiles":[],"focusPath":"app/router.py","id":"late-reparsing","outcome":"reject","scenarioId":"primary","title":"Both identifiers are reparsed at comparison time"},{"expectedCount":0,"files":[{"path":"app/router.py","source":"from uuid import UUID\nfrom typing import NewType\nOwnerId = NewType('OwnerId', UUID)\ndef same_owner(current: OwnerId, requested: OwnerId) -> bool:\n    return current == requested\n"}],"fixedFiles":[],"focusPath":"app/router.py","id":"typed-comparison","outcome":"accept","scenarioId":"primary","title":"Compare the validated nominal identifiers"}],"filePatterns":[],"id":"prefer-validated-uuid-comparison","key":"python:prefer-validated-uuid-comparison","languages":["python"],"limitations":["Checks equality/inequality where both adjacent operands directly call the import-proven uuid.UUID constructor. Imported aliases and module qualification are supported; shadowed, rebound or mutated factories abstain.","Single boundary parsing, direct comparisons of typed IDs, literal values on one side, arbitrary conversion helpers and UUID attribute comparisons are not inferred. Tests remain checked; generated sources are excluded.","The comparison may itself be an intentional ingestion boundary, so this remains a review warning. No autofix removes validation or changes string/UUID equality behavior."],"messageIds":[],"optionsSchema":null,"rationale":"Reparsing both operands while comparing identifiers moves input validation into business logic, repeats normalization and can raise on malformed input after the boundary has accepted it.","references":[],"remediation":"Parse and normalize UUIDs in the router's request schema or ingestion adapter, use the same nominal UUID-backed ID in domain and store contracts, then compare the typed values directly. Preserve canonicalization of existing string-backed data before removing constructors.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_validated_uuid_comparison.py","status":"active","summary":"Validate UUID identifiers at input boundaries before comparing them.","test":"packages/python/tests/rules/test_prefer_validated_uuid_comparison.py"},{"aliases":[],"autofix":"safe","category":"style","code":"SARJ432","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"service.py","source":"async def load(store, item_id):\n    item = await store.get(item_id)\n    if item is None:\n        return\n    return item.name\n"}],"fixedFiles":[],"focusPath":"service.py","id":"awaited-optional-lookup","outcome":"reject","scenarioId":"primary","title":"An awaited lookup is split from its absence guard"},{"expectedCount":0,"files":[{"path":"service.py","source":"async def load(store, item_id):\n    if (item := await store.get(item_id)) is None:\n        return\n    return item.name\n"}],"fixedFiles":[],"focusPath":"service.py","id":"guard-bound-lookup","outcome":"accept","scenarioId":"primary","title":"The awaited lookup is bound in its absence guard"}],"filePatterns":[],"id":"prefer-walrus-awaited-none-guard","key":"python:prefer-walrus-awaited-none-guard","languages":["python"],"limitations":["Safe fixes reuse the proven assignment/guard candidate and preserve the original condition, awaited expression, body and sentinel.","Only one-line awaited calls followed immediately by an exact `is None` or `is not None` guard ending in return or raise are checked.","Statements before the terminal operation must be simple assignments; expressions and compound control flow are excluded.","An `is None` guard requires a later use on the surviving path and must not use or rebind the name inside the guard; an `is not None` guard must consume the binding inside the terminal branch and leave no later use before rebinding.","The combined condition must fit 120 columns; comments, else branches, ambiguous name flow, generated files, and broader assignment-expression preferences are excluded.","A final awaited assignment in a with/async-with block followed by an outer terminal None guard receives a manual-review warning. Keep the await inside its resource context; folding the guard inward must preserve context-manager exit, exception suppression and return semantics. This cross-context form is never automatically fixed."],"messageIds":[],"optionsSchema":null,"rationale":"When an awaited lookup and its terminal None guard are adjacent, binding in the condition keeps the operation, branch decision, and temporary name in one compact expression.","references":[],"remediation":"Rewrite `value = await lookup(); if value is None: return` as `if (value := await lookup()) is None: return`. Preserve `is not None` when the terminal branch returns or raises with the bound value. A short message assignment may remain inside the guard before the terminal statement. Keep two statements when the awaited assignment is an intentional debugging or tracing boundary.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_walrus_awaited_none_guard.py","status":"active","summary":"Bind a compact awaited lookup in its immediately following terminal None guard.","test":"packages/python/tests/rules/test_prefer_walrus_awaited_none_guard.py"},{"aliases":[],"autofix":"safe","category":"performance","code":"SARJ076","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/values.py","source":"def normalized(values):\n    return [clean for value in values if (clean := value.strip())]\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 normalized(values):\n    return [value.strip() for value in values if value.strip()]\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":["Autofix only handles a private nested ordinary synchronous nongenerator helper whose entire body returns one list comprehension over literal exact strings, with one identical call as its filter and result: proven builtin len, or zero-argument strip/lower/upper/lstrip/rstrip/casefold/capitalize/title/swapcase/expandtabs. Exact direct callers, fresh file-wide binding names, and absence of known frame or metadata capabilities are required; public, escaped, generic, variadic, and effectful shapes remain for review.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","Only single-generator comprehensions inside non-test callable bodies are analyzed; generated code and nested or deferred expression scopes are excluded.","Nested calls, differing sibling calls, unsafe short-circuit branches, and recognized type-inspection, consuming, or nondeterministic calls are excluded.","General findings cannot prove referential transparency. Outside the exact literal-string autofix subset, a named expression binds in the containing callable, so the chosen name must be fresh and reviewed."],"messageIds":[],"optionsSchema":null,"rationale":"When both evaluations are intended to produce one stable value, repeating the call wastes work and obscures that relationship.","references":[],"remediation":"If reevaluation is unintended, bind the result once in the filter with a fresh containing-scope name and reuse it. Keep separate calls or use an explicit loop when each evaluation is meaningful.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_walrus_comprehension_filter.py","status":"active","summary":"The same call runs in a comprehension filter and its result.","test":"packages/python/tests/rules/test_prefer_walrus_comprehension_filter.py"},{"aliases":[],"autofix":"safe","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":["Safe fixes preserve the original regex expression, identifier spelling, condition and body.","Only a short, single-line assignment physically followed by a truthy or `is not None` check is analyzed.","The receiver must resolve to an imported `re` or `regex` module, a unique compile binding, or a parameter annotated as its Pattern type.","Tests, generated files, comments or blank lines between statements, rewrites over 100 columns, rebindings, and later result uses are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A named expression can keep a short regex operation with its only condition while preserving access to the result.","references":[],"remediation":"For a truthy check, use `if (match := pattern.search(text)):`. For an explicit None check, preserve it as `if (match := pattern.search(text)) is not None:`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_walrus_regex_match.py","status":"active","summary":"A proven regex Match-or-None result is assigned only for the following condition.","test":"packages/python/tests/rules/test_prefer_walrus_regex_match.py"},{"aliases":[],"autofix":"safe","category":"style","code":"SARJ077","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/messages.py","source":"while (message := receive()) is not None:\n    consume(message)\n"}],"fixedFiles":[],"focusPath":"app/messages.py","id":"conditional-none-sentinel-binding","outcome":"accept","scenarioId":"none-sentinel","title":"Named-expression loop keeps accepting falsy messages"},{"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/messages.py","source":"while True:\n    message = receive()\n    if message is None:\n        break\n    consume(message)\n"}],"fixedFiles":[],"focusPath":"app/messages.py","id":"explicit-none-sentinel-break","outcome":"reject","scenarioId":"none-sentinel","title":"Producer loop preserves a None sentinel"},{"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":["Safe fixes preserve the original producer expression, sentinel comparison and remaining loop body; comments on the loop header abstain.","Only `while True` loops beginning with a single-line call assignment and a physically adjacent falsy or `None` break are analyzed.","Tests, generated files, comments attached to or separating the matched statements, blank lines, type comments, rewrites over 100 columns, loop or guard `else` clauses, and empty rewritten bodies are excluded.","The producer receiver is not resolved; calls and awaited calls are accepted when the surrounding loop shape is otherwise exact."],"messageIds":[],"optionsSchema":null,"rationale":"A named-expression loop can state a repeated producer call and its termination condition together.","references":[],"remediation":"For `if not value: break`, use `while (value := read()):`. For `if value is None: break`, preserve the sentinel with `while (value := read()) is not None:`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/prefer_walrus_stream_loop.py","status":"active","summary":"Collapse a compact producer assignment and immediate sentinel break into a named-expression loop.","test":"packages/python/tests/rules/test_prefer_walrus_stream_loop.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ409","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/tests/test_models.py","source":"import pytest\nfrom app.models import ELIGIBLE_MODELS\n\nEXPECTED_MODELS = ('a', 'b')\n\ndef test_model_membership():\n    assert ELIGIBLE_MODELS == EXPECTED_MODELS\n\n@pytest.mark.parametrize('model', EXPECTED_MODELS)\ndef test_model(model):\n    assert build(model).tier == 'priority'\n"}],"fixedFiles":[],"focusPath":"app/tests/test_models.py","id":"independent-model-cases","outcome":"accept","scenarioId":"primary","title":"Drive behavior from an independent expected table"},{"expectedCount":0,"files":[{"path":"app/tests/test_models.py","source":"import pytest\nfrom app.models import ELIGIBLE_MODELS\n\ndef test_membership():\n    expected = ['a', 'b']\n    assert ELIGIBLE_MODELS == expected\n\n@pytest.mark.parametrize('model', ELIGIBLE_MODELS)\ndef test_model(model):\n    assert build(model)\n"}],"fixedFiles":[],"focusPath":"app/tests/test_models.py","id":"local-independent-membership-oracle","outcome":"accept","scenarioId":"oracle-lexical-bindings","title":"A fresh local expected table observes the actual production collection"},{"expectedCount":1,"files":[{"path":"app/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":"app/tests/test_models.py","id":"production-derived-model-cases","outcome":"reject","scenarioId":"primary","title":"Do not derive cases from production eligibility"},{"expectedCount":1,"files":[{"path":"app/tests/test_models.py","source":"import pytest\nfrom app.models import ELIGIBLE_MODELS\n\ndef test_membership(ELIGIBLE_MODELS):\n    assert ELIGIBLE_MODELS == ('a', 'b')\n\n@pytest.mark.parametrize('model', ELIGIBLE_MODELS)\ndef test_model(model):\n    assert build(model)\n"}],"fixedFiles":[],"focusPath":"app/tests/test_models.py","id":"shadowed-membership-oracle","outcome":"reject","scenarioId":"oracle-lexical-bindings","title":"A fixture equality does not observe the imported production collection"}],"filePatterns":[],"id":"production-derived-test-cases","key":"python:production-derived-test-cases","languages":["python"],"limitations":["Only proven first-party, non-test imports with eligible, supported, allowed, or required in their names are checked in collected pytest parametrization.","Direct collections, simple unshadowed builtin wrappers, set expressions, and positional or keyword argvalues are recognized.","A direct module-level or collected-test equality against a distinct literal expected table proves an independent oracle; helper-indirected relationships remain unreported.","Collected-test oracles must read the imported production binding without parameter/local shadowing. Expected tuple bindings may be module-level or local and must be assigned once; fresh local literal lists/sets are accepted only when their sole read is the assertion.","Unchanged same-name default captures of production imports or immutable expected tuples, and single direct local reimports of the exact absolute module and symbol, retain provenance. Class-scope default shadowing, later writes, and imports after the assertion do not prove a production read."],"messageIds":[],"optionsSchema":null,"rationale":"When exact eligibility or support membership is contractual, deriving cases from production lets a member change without an independent test oracle failing.","references":[],"remediation":"For contractual membership, define a literal expected table, assert the production collection equals it, and drive behavior from the expected table. Suppress the warning with rationale for an intentional dynamic sweep.","since":null,"source":"packages/python/src/sarj_python_lint/rules/production_derived_test_cases.py","status":"active","summary":"Warn when pytest membership-contract cases are derived only from the first-party production collection.","test":"packages/python/tests/rules/test_production_derived_test_cases.py"},{"aliases":["fixture-returns-bare-tuple"],"autofix":"none","category":"testing","code":"SARJ044","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/conftest.py","source":"import pytest\n\nfrom tests.support.stores import Stores\n\n@pytest.fixture\ndef stores() -> Stores:\n    return Stores(org=org_store, user=user_store)\n"},{"path":"tests/support/stores.py","source":"from dataclasses import dataclass\n\n@dataclass(frozen=True)\nclass Stores:\n    org: object\n    user: object\n"},{"path":"tests/test_users.py","source":"from tests.support.stores import Stores\n\ndef test_user_lookup(stores: Stores):\n    assert stores.user.get('u1')\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"fixture-returns-named-value","outcome":"accept","scenarioId":"primary","title":"Fixture consumers use named fields"},{"expectedCount":1,"files":[{"path":"tests/conftest.py","source":"import pytest\n\n@pytest.fixture\ndef stores():\n    return org_store, user_store\n"},{"path":"tests/test_users.py","source":"def test_user_lookup(stores):\n    org_store, user_store = stores\n    assert user_store.get('u1')\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"fixture-returns-tuple","outcome":"reject","scenarioId":"primary","title":"Fixture consumers must remember field positions"}],"filePatterns":[],"id":"pytest-fixture-returns-bare-tuple","key":"python:pytest-fixture-returns-bare-tuple","languages":["python"],"limitations":["Only pytest and pytest-asyncio fixtures in test paths are analyzed.","Decorator identity follows direct absolute imports and preceding module/class bindings; dynamic provider aliases and plugin registration are not inferred.","Explicit writes to a proven provider's fixture attribute invalidate later module imports, including through executed class bodies; arbitrary call effects and assignment aliases are not inferred.","Factory closures, single-field tuples, starred tuple literals, and variadic tuple annotations are allowed.","Homogeneous variadic tuple annotations are treated as sequences and excluded; fixed domain tuple protocols require an exact suppression."],"messageIds":[],"optionsSchema":null,"rationale":"Tuple-shaped fixture APIs encode each value's role in its position, so call sites are opaque and a reordered result can silently bind the wrong test dependency.","references":[],"remediation":"Return a `NamedTuple`, frozen dataclass, or another result object and access its named fields; split independent values into separate fixtures when they do not form one record. Keep the tuple and use an exact SARJ044 suppression when tuple identity or ordering is itself the tested domain contract.","since":null,"source":"packages/python/src/sarj_python_lint/rules/pytest_fixture_returns_bare_tuple.py","status":"active","summary":"Pytest fixture exposes a fixed positional record as an unnamed tuple.","test":"packages/python/tests/rules/test_pytest_fixture_returns_bare_tuple.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ525","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"tests/conftest.py","source":"from io import StringIO\nimport pytest\n\n@pytest.fixture\ndef stream():\n    with StringIO(\"payload\") as handle:\n        return handle\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"fixture-returns-closed-stream","outcome":"reject","scenarioId":"primary","title":"Returning exits the stream context before the test runs"},{"expectedCount":0,"files":[{"path":"tests/conftest.py","source":"from io import StringIO\nimport pytest\n\n@pytest.fixture\ndef stream():\n    with StringIO(\"payload\") as handle:\n        yield handle\n"}],"fixedFiles":[],"focusPath":"tests/conftest.py","id":"fixture-yields-live-stream","outcome":"accept","scenarioId":"primary","title":"Yield keeps the stream open for the requesting test"}],"filePatterns":[],"id":"pytest-fixture-returns-closed-resource","key":"python:pytest-fixture-returns-closed-resource","languages":["python"],"limitations":["Only proven pytest/pytest-asyncio fixtures in test paths with one decorator and a direct return inside the owning with/async with are analyzed.","Recognizes built-in/io streams, tempfile TemporaryFile/NamedTemporaryFile, HTTPX Client and async-with AsyncClient; arbitrary context managers and TestClient are excluded.","Aliases, escaped/rebound resources, provider mutation, nested helpers, generator fixtures, returned snapshots and returns after the context are not inferred.","Unreachable branches and intentional closed-resource contracts are not inferred; findings are warnings requiring review."],"messageIds":[],"optionsSchema":null,"rationale":"Returning a context-managed stream or HTTPX client exits its context before pytest receives the value. The requesting test receives an already closed resource instead of a usable dependency.","references":[],"remediation":"Yield the resource inside its context so teardown follows the requesting test, updating the generator annotation as needed. If the contract deliberately supplies a closed resource, use an exact SARJ525 suppression with that reason. Review exception and cleanup behavior; no automatic rewrite is provided.","since":null,"source":"packages/python/src/sarj_python_lint/rules/pytest_fixture_returns_closed_resource.py","status":"active","summary":"Yield a managed fixture resource until its requesting test finishes.","test":"packages/python/tests/rules/test_pytest_fixture_returns_closed_resource.py"},{"aliases":["class-docstring-restates-name"],"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 adds a concrete constraint"},{"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 only repeats the class name"}],"filePatterns":[],"id":"redundant-class-docstring","key":"python:redundant-class-docstring","languages":["python"],"limitations":["Decorated or inherited classes, recognized in-file runtime docstring reads, generated files, and docstring-only class bodies are excluded.","The rule compares conservative word stems from the class name; any non-grammatical novel term keeps the docstring."],"messageIds":[],"optionsSchema":null,"rationale":"A name-only restatement adds maintenance cost without documenting the class contract.","references":[],"remediation":"Remove the restatement after confirming `__doc__` is not an external contract. Keep schema descriptions, runtime-consumed prose, hidden invariants, lifetimes, and exclusions.","since":null,"source":"packages/python/src/sarj_python_lint/rules/redundant_class_docstring.py","status":"active","summary":"Undecorated base-free class docstring only repeats the class name.","test":"packages/python/tests/rules/test_redundant_class_docstring.py"},{"aliases":["function-docstring-restates-declaration"],"autofix":"safe","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":"Function docstring adds an atomicity guarantee"},{"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":"Function docstring only repeats its declaration"}],"filePatterns":[],"id":"redundant-docstring","key":"python:redundant-docstring","languages":["python"],"limitations":["Safe fixes remove exact error-level name restatements only from unescaped private nested synchronous nongenerator functions, with no decorators or namespace hazards and a matching native no-consumer finding. Public/escaped functions and meaningful or ambiguous prose remain review-only. Known frame and metadata capabilities, their import aliases and metadata field access prevent fixes.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","Decorated functions, methods on decorated or inherited classes, recognized in-file runtime docstring reads, generated files, and docstring-only bodies are excluded.","Every non-grammatical Unicode word or number must already occur in the function name, parameters, or annotations; multi-parameter prose must include a distinguishing word from every parameter.","ERROR is limited to exact ordered function-name restatements with grammatical filler and an optional terminal period; directional punctuation, reordered words and broader stem-set matches retain WARNING."],"messageIds":[],"optionsSchema":null,"rationale":"Restating a clear name and signature creates maintenance work without helping callers.","references":[],"remediation":"Remove the restatement after confirming `__doc__` is not an external contract. Clarify author-controlled names or types, and keep prose that documents constraints, side effects, or failure modes.","since":null,"source":"packages/python/src/sarj_python_lint/rules/redundant_docstring.py","status":"active","summary":"Function or plain-method docstring only repeats its declaration.","test":"packages/python/tests/rules/test_redundant_docstring.py"},{"aliases":["module-docstring-restates-path"],"autofix":"safe","category":"maintainability","code":"SARJ099","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"\"\"\"Service command help.\"\"\"\nDESCRIPTION = __doc__\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"module-consumer","outcome":"accept","scenarioId":"primary","title":"Keep a module description consumed at runtime"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"\"\"\"Service entry points.\n\nCoordinates requests.\n\"\"\"\nVALUE = 1\n"}],"fixedFiles":[{"path":"app/service.py","source":"VALUE = 1\n"}],"focusPath":"app/service.py","id":"module-prose","outcome":"reject","scenarioId":"primary","title":"Delete the module's implementation walkthrough"}],"filePatterns":[],"id":"redundant-module-docstring","key":"python:redundant-module-docstring","languages":["python"],"limitations":["The historical rule ID is retained for compatibility, but every unconsumed module docstring is checked, including multiline prose, tests, special modules, stubs and executable scripts.","Generated files, doctests, explicit __doc__/help/getdoc consumers and exact SARJ099 suppressions are excluded. Module consumer analysis is shared with SARJ420; when both rules are selected, SARJ099 takes diagnostic precedence.","Safe fixes delete standalone module docstrings only when SARJ420 also confirms no detected consumer. Visible dynamic namespace or frame observation, module escapes, adjacent strings, comments within the expression, same-line code and trailing comments prevent fixes.","A fix must retain source encoding and shebang interpretation. Unknown external documentation consumers require an explained local exception; classes and callable documentation remain owned by SARJ420.","Imports of __main__ or pydoc conservatively withhold module fixes because they can publish the module or its help. They do not establish that an imported __main__ is the current module, so diagnostics still require an explicit consumer proof or exception.","Legal/license notices are never automatically deleted. Keep their text as comments when removing the surrounding module docstring manually."],"messageIds":[],"optionsSchema":null,"rationale":"Module prose creates a second maintenance surface; names and code structure should explain the implementation, with hidden constraints kept next to their owner.","references":[],"remediation":"Delete the module docstring. Preserve legal notices as comments, express ordinary meaning through names and types, and move a necessary hidden constraint next to the code it governs. Keep externally consumed documentation with an explained exact-code exception.","since":null,"source":"packages/python/src/sarj_python_lint/rules/redundant_module_docstring.py","status":"active","summary":"Delete top-of-file module docstrings without a detected runtime or documentation consumer.","test":"packages/python/tests/rules/test_redundant_module_docstring.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ487","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/__init__.py","source":"# Example test package.\n"},{"path":"tests/test_record.py","source":"from pydantic import BaseModel\nclass Record(BaseModel):\n    value: str\ndef test_record():\n    assert Record(value='item').value == 'item'\n"}],"fixedFiles":[],"focusPath":"tests/test_record.py","id":"direct-constructor","outcome":"accept","scenarioId":"primary","title":"The record owns its signature and validation"},{"expectedCount":1,"files":[{"path":"tests/__init__.py","source":"# Example test package.\n"},{"path":"tests/test_record.py","source":"from pydantic import BaseModel\nclass Record(BaseModel):\n    value: str\ndef _record(value: str) -> Record:\n    return Record(value=value)\ndef test_record():\n    assert _record('item').value == 'item'\n"}],"fixedFiles":[],"focusPath":"tests/test_record.py","id":"mirrored-constructor","outcome":"reject","scenarioId":"primary","title":"Required parameters only repeat record fields"}],"filePatterns":[],"id":"redundant-test-constructor-forwarder","key":"python:redundant-test-constructor-forwarder","languages":["python"],"limitations":["Only undecorated private module-level helpers in authored test files are considered; conftest, generated and vendor sources are excluded.","The helper has only an optional docstring and one keyword construction, no defaults, and one required bare parameter per supplied field with equivalent resolved annotations.","Only non-generic first-party Pydantic records with statically known fields, plain ancestry, and no custom methods, decorators or field aliases are proven; application modules are never imported.","Unknown calls, unpacking, escapes, reflection, rebinding, patch strings, narrower parameter types, and unresolved constructor ownership are excluded. No autofix preserves intentional seams."],"messageIds":[],"optionsSchema":null,"rationale":"A forwarding helper repeats the constructor's fields and types, creating a second signature to maintain without scenario defaults or setup.","references":[],"remediation":"Use the real typed constructor at known callers. Keep helpers that choose defaults, transform data, own resources, narrow types, or provide an intentional patch boundary.","since":null,"source":"packages/python/src/sarj_python_lint/rules/redundant_test_constructor_forwarder.py","status":"active","summary":"A private test helper mirrors a resolved value-record constructor without adding a contract.","test":"packages/python/tests/rules/test_redundant_test_constructor_forwarder.py"},{"aliases":["kwarg-heavy-construction-in-test"],"autofix":"none","category":"testing","code":"SARJ045","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_call.py","source":"ORDER_DEFAULTS = {\n    \"organization_id\": \"org-1\", \"actor_id\": \"user-1\", \"currency\": \"SAR\",\n    \"locale\": \"ar-SA\", \"channel\": \"web\", \"notify_customer\": True, \"retry_limit\": 3,\n}\n\ndef make_order(status):\n    return CreateOrderRequest(**ORDER_DEFAULTS, status=status)\n\ndef test_pending_order():\n    assert make_order(\"pending\").status == \"pending\"\n\ndef test_completed_order():\n    assert make_order(\"completed\").status == \"completed\"\n"}],"fixedFiles":[],"focusPath":"tests/test_call.py","id":"construction-through-builder","outcome":"accept","scenarioId":"primary","title":"Tests keep incidental order defaults in one helper"},{"expectedCount":2,"files":[{"path":"tests/test_call.py","source":"def test_pending_order():\n    request = CreateOrderRequest(\n        organization_id=\"org-1\", actor_id=\"user-1\", currency=\"SAR\",\n        locale=\"ar-SA\", channel=\"web\", notify_customer=True, retry_limit=3,\n        status=\"pending\",\n    )\n    assert request.status == \"pending\"\n\ndef test_completed_order():\n    request = CreateOrderRequest(\n        organization_id=\"org-1\", actor_id=\"user-1\", currency=\"SAR\",\n        locale=\"ar-SA\", channel=\"web\", notify_customer=True, retry_limit=3,\n        status=\"completed\",\n    )\n    assert request.status == \"completed\"\n"}],"fixedFiles":[],"focusPath":"tests/test_call.py","id":"repeated-wide-construction","outcome":"reject","scenarioId":"primary","title":"Tests repeat incidental order setup"}],"filePatterns":[],"id":"repeated-kwarg-heavy-call-in-test","key":"python:repeated-kwarg-heavy-call-in-test","languages":["python"],"limitations":["Only calls to the same syntactic callee that share at least seven named arguments directly inside test functions are reported.","Mapping construction, data-record helpers, mock assertions, fixtures, nested closures, generated files, and dynamic callees are allowed."],"messageIds":[],"optionsSchema":null,"rationale":"Large repeated argument lists duplicate incidental setup, bury scenario differences, and make signature changes noisy within a test file.","references":[],"remediation":"Extract a scenario helper with sensible defaults, or use a parametrized case table, and override only values relevant to each case. Suppress the finding when every argument intentionally specifies the contract, an ordered state transition, or a retry.","since":null,"source":"packages/python/src/sarj_python_lint/rules/repeated_kwarg_heavy_call_in_test.py","status":"active","summary":"Tests repeat at least seven explicit keyword names across calls to the same callee.","test":"packages/python/tests/rules/test_repeated_kwarg_heavy_call_in_test.py"},{"aliases":["repeated-static-call-assertions"],"autofix":"none","category":"testing","code":"SARJ413","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_parser.py","source":"import pytest\n\n@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 undecorated, zero-parameter pytest functions and methods on undecorated, base-free Test* classes are checked.","The entire executable test body must be at least three top-level assertions of the same direct-name call with literal inputs and expectations.","Assertion messages, zero-argument calls, likely mutators, mocks, snapshots, spies, comments, and intervening setup are excluded.","Exception and pytest.raises case tables are intentionally outside this assertion-only signal.","Common mapping accessors are excluded because repeated field assertions usually describe one cohesive object contract, not independent input cases.","Tests participating in a no-repeated-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 the calls are independent, named parameters isolate failures and identify the input that failed. Literal syntax alone cannot prove independence.","references":[],"remediation":"Consider a named pytest parameter table only when call order, shared state, fixture lifecycle, and object identity are not part of the contract; otherwise keep the assertions together or suppress the warning.","since":null,"source":"packages/python/src/sarj_python_lint/rules/repeated_static_call_cases.py","status":"active","summary":"Three same-shape literal call assertions may be independent parameter cases.","test":"packages/python/tests/rules/test_repeated_static_call_cases.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ457","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_client.py","source":"async def test_response(client):\n    assert await client.get('/')\n"}],"fixedFiles":[],"focusPath":"tests/test_client.py","id":"fixture-client","outcome":"accept","scenarioId":"primary","title":"Request a managed test client fixture"},{"expectedCount":3,"files":[{"path":"tests/test_client.py","source":"from httpx import AsyncClient, ASGITransport\nasync def test_case_0(app):\n    async with AsyncClient(transport=ASGITransport(app=app), base_url='http://test') as client:\n        assert await client.get('/')\n\nasync def test_case_1(app):\n    async with AsyncClient(transport=ASGITransport(app=app), base_url='http://test') as client:\n        assert await client.get('/')\n\nasync def test_case_2(app):\n    async with AsyncClient(transport=ASGITransport(app=app), base_url='http://test') as client:\n        assert await client.get('/')\n"}],"fixedFiles":[],"focusPath":"tests/test_client.py","id":"repeated-managed-clients","outcome":"reject","scenarioId":"primary","title":"Share managed test-client assembly"}],"filePatterns":[],"id":"repeated-test-composition","key":"python:repeated-test-composition","languages":["python"],"limitations":["Requires three sibling tests with equivalent wiring of at least two stored and invoked first-party ABC/Protocol dependencies.","Collaborators must resolve to equivalent fixture parameters or first-party constructor expressions, optionally through unambiguous local aliases.","Requires an unconditional construction followed by a subject method call; dynamic arguments, parametrized tests, constructor validation, multiple instances, mutated dependencies, fixtures, helpers, generated files, and inherited test classes are excluded.","HTTPX detection requires a managed AsyncClient with an inline or unchanged local ASGITransport and equivalent options except its app; unambiguous constructor aliases are accepted."],"messageIds":[],"optionsSchema":null,"rationale":"Repeated assembly hides scenario differences and spreads dependency signature changes across tests.","references":[],"remediation":"Move shared assembly into a function-scoped fixture or typed factory fixture; keep scenario values explicit.","since":null,"source":"packages/python/src/sarj_python_lint/rules/repeated_test_composition.py","status":"active","summary":"Sibling tests repeat service collaborator wiring or managed ASGI HTTP clients.","test":"packages/python/tests/rules/test_repeated_test_composition.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ490","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/__init__.py","source":"# Example test package.\n"},{"path":"tests/test_store.py","source":"from abc import ABC, abstractmethod\nclass Store(ABC):\n    @abstractmethod\n    def read(self, key: str) -> str:\n        pass\nclass First(Store):\n    def read(self, key: str) -> str:\n        raise NotImplementedError\nclass Second(Store):\n    def read(self, key: str) -> str:\n        return key\n"}],"fixedFiles":[],"focusPath":"tests/test_store.py","id":"configured-operation","outcome":"accept","scenarioId":"primary","title":"Configured fake behavior belongs to the leaf"},{"expectedCount":1,"files":[{"path":"tests/__init__.py","source":"# Example test package.\n"},{"path":"tests/test_store.py","source":"from abc import ABC, abstractmethod\nclass Store(ABC):\n    @abstractmethod\n    def read(self, key: str) -> str:\n        pass\nclass First(Store):\n    def read(self, key: str) -> str:\n        raise NotImplementedError\nclass Second(Store):\n    def read(self, key: str) -> str:\n        raise NotImplementedError\n"}],"fixedFiles":[],"focusPath":"tests/test_store.py","id":"repeated-strict-operation","outcome":"reject","scenarioId":"primary","title":"Two test fakes repeat one unconfigured operation"}],"filePatterns":[],"id":"repeated-unimplemented-test-contract","key":"python:repeated-unimplemented-test-contract","languages":["python"],"limitations":["Only authored test classes directly inheriting one resolved first-party ABC are considered; decorated classes, metaclasses, generic classes and reflective modules are excluded.","Duplicate operations must implement the same abstract member with identical resolved typed signatures and immutable literal defaults. Only bare built-in raise NotImplementedError bodies, optionally with typing.override, qualify.","Methods with messages, side effects, generator bodies, other decorators, captured defaults or super dispatch remain leaf-owned. Signatures must match the abstract contract; unresolved names are not guessed.","Visible non-call observations of abstract operations, imported namespace writes and subclass/MRO hooks exclude a contract family. String annotation references are unresolved; external or dynamic observations still require review.","One diagnostic is emitted per duplicate class, retaining the first source occurrence as the comparison owner. No autofix or dynamic mock generation; intentional independent contracts require a reasoned exception."],"messageIds":[],"optionsSchema":null,"rationale":"Copied unconfigured operations add test support code and drift independently when a contract changes.","references":[],"remediation":"Use one explicit typed strict test base for the contract. Preserve each leaf fake's configured behavior and failure timing; verify that the complete base and wiring reduce maintained code.","since":null,"source":"packages/python/src/sarj_python_lint/rules/repeated_unimplemented_test_contract.py","status":"active","summary":"Share repeated typed fail-fast abstract operations between test fakes.","test":"packages/python/tests/rules/test_repeated_unimplemented_test_contract.py"},{"aliases":["replay-named-store-insert-requires-duplicate-policy","store-insert-requires-on-conflict"],"autofix":"none","category":"correctness","code":"SARJ018","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"def ensure_task(cursor, task_id):\n    cursor.execute(\"INSERT INTO task (id) VALUES (%s) ON CONFLICT DO NOTHING\", (task_id,))\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"insert-with-duplicate-policy","outcome":"accept","scenarioId":"primary","title":"Replay-named store callable declares duplicate behavior"},{"expectedCount":1,"files":[{"path":"app/task_store.py","source":"def ensure_task(cursor, task_id):\n    cursor.execute(\"INSERT INTO task (id) VALUES (%s)\", (task_id,))\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"insert-without-duplicate-policy","outcome":"reject","scenarioId":"primary","title":"Replay-named store callable omits duplicate behavior"}],"filePatterns":[],"id":"replay-contract-insert-requires-duplicate-policy","key":"python:replay-contract-insert-requires-duplicate-policy","languages":["python"],"limitations":["Only SQL literals passed directly, or through a stable local binding, to execute-like calls in explicitly replay-named callables in recognized store modules are analyzed.","Dynamic SQL policy composition and ordinary create, seed, migration, schedule, and upsert methods are excluded.","Target AS aliases and OVERRIDING clauses preserve INSERT detection. Conflict detection remains lexical within one SQL statement; per-CTE ownership is not established by this embedded-SQL rule."],"messageIds":[],"optionsSchema":null,"rationale":"A store callable named ensure, enqueue, record_once, get_or_create, create_if_absent, or insert_if_absent promises that a retry will not create duplicate state.","references":[],"remediation":"Add an explicit conflict action or same-target NOT EXISTS guard. If duplicate failure or append-only insertion is deliberate, add an exact SARJ018 suppression explaining it; rename only when the callable does not promise replay safety.","since":null,"source":"packages/python/src/sarj_python_lint/rules/store_insert_requires_on_conflict.py","status":"active","summary":"A literal INSERT in a replay-named store callable must declare duplicate behavior.","test":"packages/python/tests/rules/test_store_insert_requires_on_conflict.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ553","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"api.py","source":"from fastapi import APIRouter\nfrom pydantic import PositiveInt\nrouter = APIRouter()\n@router.get('/versions')\ndef versions(limit: PositiveInt = 25):\n    return limit\n"}],"fixedFiles":[],"focusPath":"api.py","id":"positive-history-limit","outcome":"accept","scenarioId":"primary","title":"Declare the valid numeric domain"},{"expectedCount":1,"files":[{"path":"api.py","source":"from fastapi import APIRouter\nrouter = APIRouter()\n@router.get('/versions')\ndef versions(limit: int = 25):\n    return limit\n"}],"fixedFiles":[],"focusPath":"api.py","id":"unbounded-history-limit","outcome":"reject","scenarioId":"primary","title":"The history API accepts any signed integer"}],"filePatterns":[],"id":"require-constrained-api-integer","key":"python:require-constrained-api-integer","languages":["python"],"limitations":["Checks scalar builtin int annotations, optional unions and Annotated forms on source-proven FastAPI routes. Explicit bound metadata is accepted without guessing the intended minimum from a parameter name.","Dependency/body/header/cookie injections, noninteger types, aliases, tests and generated sources are excluded. Query/Path defaults and Annotated metadata are recognized; cross-module aliases and custom validators are not interpreted.","No automatic PositiveInt substitution is offered because zero, negative sentinels and signed domains need deliberate decisions."],"messageIds":[],"optionsSchema":null,"rationale":"A bare integer request parameter accepts negative and arbitrarily large values even when the service expects a bounded page size or ordinal.","references":[],"remediation":"Use PositiveInt, NonNegativeInt or a domain Annotated type, or declare the appropriate Query/Path bounds. Keep signed and sentinel contracts explicit with a reasoned local exception.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_constrained_api_integer.py","status":"active","summary":"Declare numeric bounds for scalar integer FastAPI request parameters.","test":"packages/python/tests/rules/test_require_constrained_api_integer.py"},{"aliases":[],"autofix":"safe","category":"security","code":"SARJ504","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"auth.py","source":"import jwt\n\n\ndef claims(token: str, key: str) -> dict[str, object]:\n    return jwt.decode(token, key, algorithms=['HS256'])\n"}],"fixedFiles":[],"focusPath":"auth.py","id":"exp-optional","outcome":"reject","scenarioId":"primary","title":"A token without exp is accepted"},{"expectedCount":0,"files":[{"path":"auth.py","source":"import jwt\n\n\ndef claims(token: str, key: str) -> dict[str, object]:\n    return jwt.decode(token, key, algorithms=['HS256'], options={'require': ['exp']})\n"}],"fixedFiles":[],"focusPath":"auth.py","id":"exp-required","outcome":"accept","scenarioId":"primary","title":"The verifier requires exp"}],"filePatterns":[],"id":"require-exp-on-jwt-decode","key":"python:require-exp-on-jwt-decode","languages":["python"],"limitations":["Safe fixes require an unmutated PyJWT import, no expanded/positional options, and absent options or a static mapping with a static string require sequence. They add only exp and preserve existing option flags and claims; maximum lifetime remains a separate policy decision.","Calls resolved to PyJWT module-level `jwt.decode` or `jwt.decode_complete` are checked; `PyJWT` instances are not.","A literal `options` mapping must contain a literal `require` collection with `exp`. Non-literal options, `**` unpacking, and literal options that disable signature verification are accepted.","A verifier that enforces its own lifetime from `iat` instead still needs an exact suppression with that reason.","Tests and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"PyJWT validates `exp` only when the claim is present. A token minted without `exp` (for example by a helper whose lifetime defaults to None) passes verification forever. RFC 8725 tells verifiers to enforce the claims they rely on rather than trust the minter.","references":[],"remediation":"Pass `options={\"require\": [\"exp\"]}` to `jwt.decode` and bound the accepted lifetime.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_exp_on_jwt_decode.py","status":"active","summary":"PyJWT decode calls require the exp claim.","test":"packages/python/tests/rules/test_require_exp_on_jwt_decode.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ467","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/__init__.py","source":"# package\n"},{"path":"app/consumer.py","source":"from abc import ABC, abstractmethod\nclass Publisher(ABC):\n    @abstractmethod\n    def publish(self) -> None: ...\nclass Consumer:\n    def __init__(self, publisher: Publisher) -> None:\n        self.publisher = publisher\n"},{"path":"app/fake.py","source":"from app.consumer import Publisher\nclass FakePublisher(Publisher):\n    def publish(self) -> None: ...\n"},{"path":"app/usage.py","source":"from app.consumer import Consumer\nfrom app.fake import FakePublisher\ndef setup() -> None:\n    Consumer(publisher=FakePublisher())\n"}],"fixedFiles":[],"focusPath":"app/usage.py","id":"explicit-fake-at-injection","outcome":"accept","scenarioId":"primary","title":"The fake declares the actual contract"},{"expectedCount":1,"files":[{"path":"app/__init__.py","source":"# package\n"},{"path":"app/consumer.py","source":"from abc import ABC, abstractmethod\nclass Publisher(ABC):\n    @abstractmethod\n    def publish(self) -> None: ...\nclass Consumer:\n    def __init__(self, publisher: Publisher) -> None:\n        self.publisher = publisher\n"},{"path":"app/fake.py","source":"class FakePublisher:\n    def publish(self) -> None: ...\n"},{"path":"app/usage.py","source":"from app.consumer import Consumer\nfrom app.fake import FakePublisher\ndef setup() -> None:\n    Consumer(publisher=FakePublisher())\n"}],"fixedFiles":[],"focusPath":"app/usage.py","id":"nominal-abc-fake-at-injection","outcome":"reject","scenarioId":"primary","title":"A fake is supplied without inheriting its nominal ABC"}],"filePatterns":[],"id":"require-explicit-contract-implementation","key":"python:require-explicit-contract-implementation","languages":["python"],"limitations":["Only direct constructor arguments and unreassigned local constructor results supplied to a resolvable typed parameter are checked. Opaque argument escapes and receiver calls invalidate cached origins, including aliases and TypeGuard adaptation.","Structural Protocols, dynamic factories, containers, casts, unresolved or multiple owned ABC ancestry, and third-party implementations are excluded.","Contracts with virtual registration, subclass/instance hooks, custom metaclasses or decorators, escaped class values, or dynamic namespaces are excluded. Consumer constructors must have one effective direct declaration; implementation constructors, __new__, class-creation hooks, custom descriptors and unknown ancestry are excluded throughout the hierarchy."],"messageIds":[],"optionsSchema":null,"rationale":"An ABC is a nominal contract: inheriting it makes its abstract operations enforceable at instantiation. Protocols intentionally use structural typing and do not require inheritance.","references":[],"remediation":"Make the owned implementation inherit the nominal ABC expected by the consumer.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_explicit_contract_implementation.py","status":"active","summary":"Require owned substitutes to inherit an injected nominal abstract contract.","test":"packages/python/tests/rules/test_require_explicit_contract_implementation.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ462","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"repository.py","source":"from psycopg_pool import AsyncConnectionPool\nasync def save(pool: AsyncConnectionPool):\n    async with pool.connection() as conn, conn.transaction():\n        await conn.execute(\"SELECT id FROM items FOR UPDATE\")\n        await conn.execute(\"UPDATE items SET n = 1\")\n"}],"fixedFiles":[],"focusPath":"repository.py","id":"explicit-lock-transaction","outcome":"accept","scenarioId":"primary","title":"Retain locks for the atomic operation"},{"expectedCount":1,"files":[{"path":"repository.py","source":"from psycopg_pool import AsyncConnectionPool\nasync def save(pool: AsyncConnectionPool):\n    async with pool.connection() as conn:\n        await conn.execute(\"SELECT id FROM items FOR UPDATE\")\n        await conn.execute(\"UPDATE items SET n = 1\")\n"}],"fixedFiles":[],"focusPath":"repository.py","id":"unprotected-lock","outcome":"reject","scenarioId":"primary","title":"A pool checkout alone does not retain a lock"}],"filePatterns":[],"id":"require-explicit-psycopg-transaction","key":"python:require-explicit-psycopg-transaction","languages":["python"],"limitations":["Reports import-proven Psycopg connections and cursors from typed parameters or constructor-injected pools. Dynamic factories, interprocedural SQL, and untyped ownership are not inferred.","Inspects literal SQL, simple local/module constants, and psycopg.sql.SQL wrappers. Dynamic SQL and unsupported SQL syntax are excluded.","A locking SELECT followed by a write on the same connection outside transaction() is reported; a single statement containing a mutation is treated as atomic. Multiple writes on a reachable local path are a warning because independent writes can be intentional.","Loops, exception handlers, match statements, comprehensions, and nested closures are not analyzed as execution paths. Manual BEGIN/commit protocols are not inferred. Tests, support fixtures, and generated files are excluded.","Non-autocommit connections can already provide implicit transaction protection. A manual commit is not itself evidence of a defect or of safety in autocommit mode; document the proven connection configuration when retaining that protocol. The rule cannot prove isolation level, database constraints, or application-level atomicity; fault-injection integration tests remain necessary."],"messageIds":[],"optionsSchema":null,"rationale":"Pool checkout does not start a transaction in autocommit mode. Locks can expire before use and earlier writes can survive a later failure.","references":[],"remediation":"Enclose the complete atomic operation in the same connection's transaction() context, or use one atomic SQL statement. Document intentionally independent writes with an exact suppression.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_explicit_psycopg_transaction.py","status":"active","summary":"Make Psycopg locking reads and related writes explicitly transactional.","test":"packages/python/tests/rules/test_require_explicit_psycopg_transaction.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ465","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"from abc import ABC, abstractmethod\nclass Runner(ABC):\n    @abstractmethod\n    def run(self) -> None: ...\nclass OrchestratorService(Runner):\n    def __init__(self, worker: Worker) -> None:\n        self.worker = worker\n    def run(self) -> None:\n        self.worker.run()\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"explicit-single-operation-contract","outcome":"accept","scenarioId":"primary","title":"The service declares its operation"},{"expectedCount":0,"files":[{"path":"app/batching.py","source":"import asyncio\nfrom collections.abc import Callable\nfrom typing import Protocol\nclass Operations(Protocol):\n    async def run(self) -> None: ...\nclass Batcher(Operations):\n    def __init__(self, operation: Callable[[], None]) -> None:\n        self.operation = operation\n    async def run(self) -> None:\n        await asyncio.get_running_loop().run_in_executor(None, self.operation)\n"}],"fixedFiles":[],"focusPath":"app/batching.py","id":"retained-executor-callback-with-contract","outcome":"accept","scenarioId":"executor-dispatch","title":"The dispatcher declares its public operation"},{"expectedCount":1,"files":[{"path":"app/batching.py","source":"import asyncio\nfrom collections.abc import Callable\nclass Batcher:\n    def __init__(self, operation: Callable[[], None]) -> None:\n        self.operation = operation\n    async def run(self) -> None:\n        await asyncio.get_running_loop().run_in_executor(None, self.operation)\n"}],"fixedFiles":[],"focusPath":"app/batching.py","id":"retained-executor-callback-without-contract","outcome":"reject","scenarioId":"executor-dispatch","title":"Dispatching injected work establishes a service boundary"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"class OrchestratorService:\n    def __init__(self, worker: Worker) -> None:\n        self.worker = worker\n    def run(self) -> None:\n        self.worker.run()\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"single-operation-service-without-contract","outcome":"reject","scenarioId":"primary","title":"One service operation still needs its contract"}],"filePatterns":[],"id":"require-explicit-service-contract","key":"python:require-explicit-service-contract","languages":["python"],"limitations":["Module-level service, store, and provider roles qualify when a public operation invokes a directly retained constructor collaborator. Classes that dispatch retained callbacks through asyncio or issue requests through locally constructed httpx clients also qualify without a role name or directory.","Resolvable inherited ABC and Protocol operations, including private and generic contracts, satisfy this policy; unresolved ancestry is not treated as a missing contract.","Generated code, tests, data values, abstract declarations, descriptors, static/class helpers, and unknown rewriting decorators are excluded.","The bounded analysis follows directly retained constructor parameters and private instance helpers; dynamic field aliases and structurally implemented Protocols declared only by external consumers are not inferred.","An unannotated bare callback factory does not by itself establish a service collaborator. Called collaborator methods and proven native callback dispatch remain in scope.","Executor dispatch requires a directly acquired or uniquely assigned asyncio event loop, or asyncio.to_thread, and an explicit callback argument. Wrapper dispatchers and ambiguous starred arguments are not inferred.","HTTP client construction and request calls require stable native imports and an unescaped local client binding. An awaited typed callback invoked with a locally constructed first-party wrapper also qualifies when its active constructor or entered lifecycle reaches a source-proven native HTTP client factory. This establishes a resource dispatch boundary; the callback's network behavior is not inferred.","First-party wrapper source may be generated, but must belong to the owning distribution or a declared workspace dependency. Unknown third-party wrappers, conditional or decorated lifecycle/helper dispatch, mere construction or cleanup without callback dispatch, mutated or escaped clients, and non-HTTP values abstain.","Method signatures are checked by the type checker. This explicit-contract policy has no automatic rewrite; preserve required framework boundaries with a reasoned exact-code exception."],"messageIds":[],"optionsSchema":null,"rationale":"A one-operation service can orchestrate other services without declaring the contract that callers consume and implementations and fakes satisfy.","references":[],"remediation":"Declare a focused ABC or Protocol covering the service operations and make that contract explicit on the implementation; reuse an existing contract instead of adding a nominal duplicate.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_explicit_service_contract.py","status":"active","summary":"Require an explicit contract for services that invoke collaborators or perform HTTP operations.","test":"packages/python/tests/rules/test_require_explicit_service_contract.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ486","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/retry.py","source":"from collections.abc import Awaitable, Callable\n\nfrom tenacity import AsyncRetrying, stop_after_attempt, wait_exponential\n\n\ndef client_retrying(sleep: Callable[[float], Awaitable[None]]) -> AsyncRetrying:\n    return AsyncRetrying(sleep=sleep, stop=stop_after_attempt(4), wait=wait_exponential(multiplier=2))\n"}],"fixedFiles":[],"focusPath":"app/retry.py","id":"retry-backoff-with-injected-sleep","outcome":"accept","scenarioId":"primary","title":"A retry factory passes an injected sleep"},{"expectedCount":1,"files":[{"path":"app/retry.py","source":"from tenacity import retry, stop_after_attempt, wait_exponential\n\nclient_retry = retry(stop=stop_after_attempt(4), wait=wait_exponential(multiplier=2))\n"}],"fixedFiles":[],"focusPath":"app/retry.py","id":"retry-backoff-without-sleep","outcome":"reject","scenarioId":"primary","title":"A module-level retry decorator backs off on the wall clock"}],"filePatterns":[],"id":"require-injectable-retry-sleep","key":"python:require-injectable-retry-sleep","languages":["python"],"limitations":["Only calls to tenacity.retry, tenacity.Retrying, and tenacity.AsyncRetrying resolved through imports are inspected; bare @retry, positional arguments, and **kwargs expansion abstain.","A wait is reported only when it is built inline from tenacity wait_* calls, optionally combined with +; wait_none(), literal-zero wait_fixed and wait_random calls, and known zero-only wait_combine or wait_chain calls do not wait. A wait read from a name, attribute, or other call is treated as injected.","Any sleep= argument other than a wall-clock sleep imported from asyncio, anyio, trio, time, tenacity, or tenacity.nap counts as a seam; the rule does not prove that tests replace it.","The backoff library has no sleep parameter, so its decorators are not inspected. Test modules, test directories, conftest.py, test-support paths, and generated files are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A tenacity policy built with a backoff wait and no sleep argument sleeps on the wall clock. Every test that exercises a transient failure pays the backoff, and exhausting an exponential policy costs the sum of every interval. Replacing tenacity's sleep from a test is a monkeypatch that SARJ445 forbids.","references":[],"remediation":"Build the policy in a factory or constructor that accepts a sleep callable and pass it as sleep=, or accept the wait strategy itself as a dependency. Tests then inject a no-op or recording sleeper, or wait_none().","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_injectable_retry_sleep.py","status":"active","summary":"Pass an injectable sleep to a tenacity retry policy that waits between attempts.","test":"packages/python/tests/rules/test_require_injectable_retry_sleep.py"},{"aliases":["kwonly-same-type-params"],"autofix":"safe","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\nmove(source_id='inbox', target_id='archive')\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\n_move('inbox', 'archive')\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":["Autofix only migrates private nested ordinary synchronous nongenerator helpers with no escapes, known frame or metadata capabilities, type parameters, defaults, positional-only parameters, variadics, or existing keyword-only parameters. All exact direct callers must be valid and comment-free; existing parameter names and argument evaluation order are preserved.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","Private module functions with closed, statically resolved direct callers block as errors; public and compatibility-sensitive signatures retain advisory findings. Use distinct nominal parameter types when positional ABI must remain unchanged.","Only high-risk groups of bare `str`, `int`, or `float` annotations are reported.","Tests, generated code, migrations, conventional ordered groups, explicit inheritance, local structural protocols, and recognized routes, CLI handlers, callbacks, and runtime protocols are excluded.","External and published calling contracts cannot be proven from one file, so public and module signatures remain advisory without autofix; only proven closed private owners block."],"messageIds":[],"optionsSchema":null,"rationale":"A caller can exchange semantically distinct positional values without a type-checking failure.","references":[],"remediation":"If API, callback, and protocol compatibility permit, make the risky parameters keyword-only and update callers; otherwise retain the contract and suppress the warning locally.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_keyword_only_swap_prone_params.py","status":"active","summary":"Risky-name positional parameters sharing a primitive annotation may be confused.","test":"packages/python/tests/rules/test_require_keyword_only_swap_prone_params.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ424","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/settings.py","source":"import os\nfrom 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\nos.environ['EMAILS'] = 'one@example.com,two@example.com'\nSettings()\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":"import os\nfrom 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\nos.environ['EMAILS'] = 'one@example.com,two@example.com'\nSettings()\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.","Only a single-field before-validator whose sole executable statement returns an expression that splits its value parameter is treated as an unconditional raw splitter.","Classes that statically disable settings decoding through a class keyword, 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":"After confirming the intended environment format, annotate the field as `Annotated[FieldType, NoDecode]` or disable decoding for the settings class 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":"Warn when an unconditional raw-string splitter lacks a pydantic-settings decoding policy.","test":"packages/python/tests/rules/test_require_nodecode_for_splitting_settings_field.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ071","defaultLevel":"warning","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\ndef sync(service: ThingService) -> None:\n    service.write('inbox', service.read('outbox'))\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":"from typing import Protocol\n\nclass ThingServicePort(Protocol):\n    def read(self, key: str) -> str: ...\n    def write(self, key: str, value: str) -> None: ...\n\nclass 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\ndef sync(service: ThingServicePort) -> None:\n    service.write('inbox', service.read('outbox'))\n"}],"fixedFiles":[],"focusPath":"app/services/thing_service.py","id":"declared-service-port","outcome":"accept","scenarioId":"primary","title":"A visible structural port already describes the service boundary"},{"expectedCount":1,"files":[{"path":"app/native.py","source":"from typing import Protocol\n\nclass Backend(Protocol):\n    def read(self) -> str: ...\n    def write(self, value: str) -> None: ...\n\nclass Coordinator:\n    def __init__(self) -> None:\n        self.backend: Backend = native_backend()\n    def read(self) -> str:\n        return self.backend.read()\n    def write(self, value: str) -> None:\n        self.backend.write(value)\n\nclass Consumer:\n    def receive(self) -> str:\n        return Coordinator().read()\n    def send(self, value: str) -> None:\n        Coordinator().write(value)\n"},{"path":"pyproject.toml","source":"[project]\nname = \"boundary-example\"\nversion = \"0.1.0\"\n"}],"fixedFiles":[],"focusPath":"app/native.py","id":"factory-created-boundary","outcome":"reject","scenarioId":"factory-created-boundary","title":"A native collaborator is hidden behind a directly constructed concrete boundary"},{"expectedCount":0,"files":[{"path":"app/native.py","source":"from typing import Protocol, final\n\nclass Backend(Protocol):\n    def read(self) -> str: ...\n    def write(self, value: str) -> None: ...\n\nclass Operations(Protocol):\n    def read(self) -> str: ...\n    def write(self, value: str) -> None: ...\n\n@final\nclass Coordinator:\n    def __init__(self) -> None:\n        self.backend: Backend = native_backend()\n    def read(self) -> str:\n        return self.backend.read()\n    def write(self, value: str) -> None:\n        self.backend.write(value)\n\ndef coordinator() -> Operations:\n    return Coordinator()\n\nclass Consumer:\n    def receive(self) -> str:\n        return coordinator().read()\n    def send(self, value: str) -> None:\n        coordinator().write(value)\n"},{"path":"pyproject.toml","source":"[project]\nname = \"boundary-example\"\nversion = \"0.1.0\"\n"}],"fixedFiles":[],"focusPath":"app/native.py","id":"factory-created-public-port","outcome":"accept","scenarioId":"factory-created-boundary","title":"A typed factory exposes the consumer's public Protocol"}],"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.","Only direct module classes are checked; tests, generated code, scripts, framework callbacks, Store/Repository persistence dependencies, and external or interface-like bases are excluded.","A suffixless class can also qualify when it retains a locally typed factory-created collaborator driving multiple public operations and production code constructs it directly to call at least two distinct operations. Constructor aliases and explicit module imports are resolved within the bounded project index; package-export indirection, wildcard imports, duplicate module identities, variable-held instances, factory indirection, and ambiguous control flow are not inferred. Top-level functions are analyzed in indexed modules; ordinary function-only modules may be excluded by the shared type index's lexical admission.","Otherwise, a one-method, suffixless, or store-backed class requires a concrete production type dependency and a test subclass or typed mock, or at least two production consumers for a multi-method class. A port owned in another module may require an exact suppression on the implementation.","The factory-created path requires stable local runtime classes. Unknown class decorators are not inferred; proven typing final and runtime_checkable decorators preserve class identity. Definition-time defaults and decorator expressions count as enclosing runtime bindings, while nested function bodies do not.","Factory-created collaborator storage and public operations also require original method bodies; unknown method decorators are excluded while canonical typing final and override aliases remain in scope. An unrelated decorated operation does not hide two proven consumed operations."],"messageIds":[],"optionsSchema":null,"rationale":"A small port can decouple consumers when they genuinely need to substitute a concrete service boundary.","references":[],"remediation":"When a real consumer needs substitution, define a focused consumer-owned `Protocol` and type that consumer against it; otherwise suppress the advisory instead of adding an unused abstraction.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_port_for_service.py","status":"active","summary":"Consider a consumer-owned port when visible service structure suggests a substitution boundary.","test":"packages/python/tests/rules/test_require_port_for_service.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ473","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"from functools import partial\nclass Result:\n    def __init__(self, *, size: int):\n        self.size = size\ndef build():\n    factory = partial(Result)\n    return factory(size=4)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"direct-partial-factory","outcome":"accept","scenarioId":"partial-erasure","title":"Direct partial construction keeps native argument checking"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"from collections.abc import Callable\nclass Result: pass\ndef _build(factory: Callable[..., Result]):\n    return factory(size=4)\n_build(Result)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"erased-factory","outcome":"reject","scenarioId":"primary","title":"Keyword arguments escape checking"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"from functools import partial\nclass Result: pass\ndef _build(factory: partial[Result]):\n    return factory(size=4)\n_build(Result)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"erased-partial-factory","outcome":"reject","scenarioId":"partial-erasure","title":"An injected partial retains its result type but loses argument checking"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"from typing import Protocol\nclass Result: pass\nclass Factory(Protocol):\n    def __call__(self, *, size: int) -> Result: ...\ndef build(factory: Factory):\n    return factory(size=4)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"exact-factory","outcome":"accept","scenarioId":"primary","title":"Factory declares its keyword contract"}],"filePatterns":[],"id":"require-precise-factory-signature","key":"python:require-precise-factory-signature","languages":["python"],"limitations":["Private module-level functions with closed, statically resolved direct callers block as errors. Public, async, or escaped callable contracts retain advisory findings so published dynamic APIs are preserved.","Only undecorated module-level functions with proven standard Callable[..., OwnedClass] or functools.partial[OwnedClass] parameters directly called with at least one named keyword are considered. Imported classes require scanned first-party declarations; direct partial construction and calls remain native type-checker ownership.","Tests, test support, generated code, unresolved/string/generic annotations, positional-only calls, unpacking, forwarding, nested captures, rebinding and ambiguous imports are excluded. Import capture checks are conservative across the file, so an unrelated local pattern, exception or walrus binding can prevent a warning.","A fixed call demonstrates signature erasure, not a current runtime defect. Genuine dynamic APIs can use a reasoned suppression. No autofix: the correct contract depends on whether classes or function factories are intended."],"messageIds":[],"optionsSchema":null,"rationale":"Callable[..., T] and functools.partial[T] erase argument checking at injected boundaries, so misspelled keywords and incompatible factories pass strict typing and can fail at runtime.","references":[],"remediation":"If factory substitution is unnecessary, call the existing constructor directly. Otherwise reuse an existing precise callable contract; use type[T] when the dependency requires a class, accounting for subclass constructor compatibility. Introduce a callable Protocol only when a substitutable keyword factory actually needs a new contract; preserve the accepted keyword names instead of replacing them with unchecked **kwargs.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_precise_factory_signature.py","status":"active","summary":"Keep fixed keyword factory calls behind a precise callable signature.","test":"packages/python/tests/rules/test_require_precise_factory_signature.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ466","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/__init__.py","source":"# package\n"},{"path":"app/consumer.py","source":"from app.publisher import Publisher, HttpPublisher\nclass Consumer:\n    def __init__(self, publisher: HttpPublisher) -> None: self.publisher = publisher\n    def run(self) -> None: self.publisher.publish()\n"},{"path":"app/publisher.py","source":"from abc import ABC, abstractmethod\nclass Publisher(ABC):\n    @abstractmethod\n    def publish(self) -> None: ...\nclass HttpPublisher(Publisher):\n    def publish(self) -> None: ...\nclass QueuePublisher(Publisher):\n    def publish(self) -> None: ...\n"}],"fixedFiles":[],"focusPath":"app/consumer.py","id":"existing-contract-httppublisher","outcome":"reject","scenarioId":"primary","title":"Use the existing substitutable publisher contract"},{"expectedCount":0,"files":[{"path":"app/__init__.py","source":"# package\n"},{"path":"app/consumer.py","source":"from app.publisher import Publisher, HttpPublisher\nclass Consumer:\n    def __init__(self, publisher: Publisher) -> None: self.publisher = publisher\n    def run(self) -> None: self.publisher.publish()\n"},{"path":"app/publisher.py","source":"from abc import ABC, abstractmethod\nclass Publisher(ABC):\n    @abstractmethod\n    def publish(self) -> None: ...\nclass HttpPublisher(Publisher):\n    def publish(self) -> None: ...\nclass QueuePublisher(Publisher):\n    def publish(self) -> None: ...\n"}],"fixedFiles":[],"focusPath":"app/consumer.py","id":"existing-contract-publisher","outcome":"accept","scenarioId":"primary","title":"The consumer already uses the existing contract"}],"filePatterns":[],"id":"require-public-dependency-contract","key":"python:require-public-dependency-contract","languages":["python"],"limitations":["Requires a resolved direct public ABC or Protocol base with multiple owned implementations explicitly covering its public operations.","Only directly retained constructor dependencies used exclusively through the contract's declared operations are checked; parameter aliases or other constructor uses are excluded.","Private consumer-owned Protocols, unresolved bases, indirect inheritance, single implementations, data-only dependencies, tests, and generated files are excluded.","ERROR requires identical resolved signatures for every used operation, including return type, accepted inputs, parameter kinds, names and literal defaults. Generic/decorated classes, descriptor methods and unresolved or variant signatures retain WARNING; preserve concrete postconditions and accepted-input variance before widening an annotation."],"messageIds":[],"optionsSchema":null,"rationale":"A concrete dependency annotation obstructs substitution even when an existing public contract already has multiple implementations.","references":[],"remediation":"Annotate the dependency with the existing public contract; do not create a new interface.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_public_dependency_contract.py","status":"active","summary":"Reuse an existing substitutable contract instead of its concrete implementation annotation.","test":"packages/python/tests/rules/test_require_public_dependency_contract.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ411","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"protocol.py","source":"import httpx\n\nclass Envelope:\n    def __init__(self, body):\n        self.body = body\n    def payload(self):\n        return self.body\n\ndef wrap(body):\n    result = Envelope(body)\n    return result\n\ndef fetch():\n    raw = httpx.get('https://api.example/report').json()\n    return wrap(raw).payload()['version']\n"}],"fixedFiles":[],"focusPath":"protocol.py","id":"factory-forwarded-external-json","outcome":"reject","scenarioId":"factory-forwarding","title":"A local factory forwards an unvalidated response"},{"expectedCount":0,"files":[{"path":"protocol.py","source":"import httpx\n\nclass Envelope:\n    def __init__(self, body):\n        self.body = body\n    def payload(self):\n        return self.body\n\ndef wrap(body):\n    return Envelope(body)\n\ndef fetch():\n    remote = httpx.get('https://api.example/report').json()\n    return wrap({'version': 1}).payload()['version']\n"}],"fixedFiles":[],"focusPath":"protocol.py","id":"factory-local-record","outcome":"accept","scenarioId":"factory-forwarding","title":"A factory receives a locally constructed record"},{"expectedCount":1,"files":[{"path":"protocol.py","source":"import httpx\n\ndef fetch() -> object:\n    report = httpx.get('https://api.example/report').json()\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":"import httpx\nfrom pydantic import BaseModel\n\nclass Report(BaseModel):\n    version: int\n\ndef fetch() -> Report:\n    raw = httpx.get('https://api.example/report').json()\n    report = Report.model_validate(raw)\n    return report\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 import-proven JSON decoders, requests/httpx/aiohttp responses and clients, environment or subprocess results, and simple module-local helpers through single-assignment names.","Simple same-module wrappers preserve constructor arguments through direct stored-field accessor methods, including local identity-or-empty-dict narrowing helpers; inheritance, decorators, mutation, nested captures, unknown member escapes, and unknown helpers are excluded.","Synchronous module factories preserve direct wrapper returns, single-assignment local aliases, and required positional or keyword parameters substituted from proven call-site inputs. Defaults, variadics, branches, unknown effects or transforms, async/method/decorated factories, captures, and factory chains are excluded; annotations alone do not establish factory provenance.","It diagnoses literal-key subscription and get calls; iteration and other dynamic JSON use remain out of scope.","Unannotated parameters and unknown expressions are not assumed to be external; interprocedural and framework-specific boundaries can remain unreported.","Names bound by loops, comprehensions, context managers, exception handlers, pattern matching, imports, or destructuring are excluded when their provenance is ambiguous; unrelated binders do not suppress the rest of a function.","Repository-local JSON, json.load file handles, tests, generated files, and documentation examples are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"Annotations, casts, and partial key checks do not validate a wire protocol; a maintained runtime schema makes required fields, types, and protocol versions explicit at the boundary.","references":[],"remediation":"Validate raw JSON with `Model.model_validate_json(...)`; use a named RootModel for scalar or collection roots. For an already-decoded response, use `model_validate` or another maintained runtime schema validator before reading fields.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_pydantic_for_external_json.py","status":"active","summary":"Proven external JSON record fields are consumed before runtime schema validation.","test":"packages/python/tests/rules/test_require_pydantic_for_external_json.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ521","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/claims.py","source":"from typing import Annotated\nfrom pydantic import BaseModel, Field, StrictInt, ValidationError\nclass FactorClaim(BaseModel):\n    factor_age: tuple[StrictInt, Annotated[StrictInt, Field(ge=0)]]\ndef verified(raw: object) -> bool:\n    try:\n        FactorClaim.model_validate(raw)\n    except ValidationError:\n        return False\n    return True\n"}],"fixedFiles":[],"focusPath":"app/claims.py","id":"declared-mapping-shape","outcome":"accept","scenarioId":"primary","title":"Strict fields and bounds declare the accepted shape"},{"expectedCount":1,"files":[{"path":"app/claims.py","source":"def verified(raw: dict[str, object]) -> bool:\n    match raw.get('factor_age'):\n        case [int(), int(age)] if not isinstance(age, bool):\n            return age >= 0\n        case _:\n            return False\n"}],"fixedFiles":[],"focusPath":"app/claims.py","id":"manual-mapping-shape","outcome":"reject","scenarioId":"primary","title":"A match statement parses an opaque field by hand"}],"filePatterns":[],"id":"require-pydantic-for-mapping-shape","key":"python:require-pydantic-for-mapping-shape","languages":["python"],"limitations":["Checks literal-string get/subscript reads on parameters or local names annotated as standard string mappings with object, Any or pydantic.JsonValue values. Stable module type aliases, quoted annotations, wrappers and local value aliases are recognized with bounded resolution.","A field must feed builtin isinstance/type checks, literal comparisons, truth tests or a constrained match pattern. Ordinary reads/forwarding, dynamic keys, homogeneous typed maps, imported application aliases, attributes and ambiguous/rebound assignments are excluded.","Import-resolved Pydantic before/wrap decorators with literal or stable local mode aliases, metadata validators and registered Discriminator callbacks own raw normalization. Stable function aliases, functools.partial binding only keyword-only configuration and simple bound/static/class methods are recognized. Prefer declarative fields before introducing callbacks.","Callback classes must be stable direct local declarations with no bases, class options, custom constructor or descriptor hooks. Import-proven typing.final and empty __slots__ preserve ownership. Other decorators, imported/inherited classes and directly mutated callback owners abstain. Nonescaping direct local helpers inherit raw ownership; escaped helpers remain checked. Generic type parameters shadow matching builtin/import annotation names.","Parsing helpers in authored tests are checked, but assert-contained test oracles are excluded. Generated/vendor sources and dynamic namespaces are excluded. This is a warning with no automatic rewrite because coercion, missing-field handling and error policy can differ."],"messageIds":[],"optionsSchema":null,"rationale":"Fixed-key reads followed by runtime type, shape, or discriminator checks spread a record contract across control flow. An object/Any/JsonValue mapping does not declare those field contracts.","references":[],"remediation":"Parse the raw object with a named Pydantic BaseModel or RootModel before using typed attributes. Prefer strict fields, Field bounds, aliases/AliasPath and discriminated unions to handwritten probes. For an already trusted internal record, declare a precise named record such as TypedDict instead of parsing it again. Preserve rejection, defaults and fail-closed behavior explicitly; bool is an int subclass.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_pydantic_for_mapping_shape.py","status":"active","summary":"Declare opaque mapping field shapes in Pydantic instead of probing them by hand.","test":"packages/python/tests/rules/test_require_pydantic_for_mapping_shape.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ450","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/routes.py","source":"from fastapi import APIRouter\nrouter = APIRouter()\n@router.post('/actions')\ndef action(body: RequestModel):\n    payload = CardPayload.model_validate(body.payload)\n    return payload.name_on_card\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"parsed-nested-route-payload","outcome":"accept","scenarioId":"primary","title":"A route validates a nested request model"},{"expectedCount":1,"files":[{"path":"app/routes.py","source":"from fastapi import APIRouter\nrouter = APIRouter()\n@router.post('/actions')\ndef action(body: RequestModel):\n    payload = body.payload\n    return payload.get('nameOnCard')\n"}],"fixedFiles":[],"focusPath":"app/routes.py","id":"raw-nested-route-payload","outcome":"reject","scenarioId":"primary","title":"A route reads a fixed key from a nested request mapping"}],"filePatterns":[],"id":"require-pydantic-for-structured-payload","key":"python:require-pydantic-for-structured-payload","languages":["python"],"limitations":["Tests, generated sources, dynamic-key access, and non-FastAPI functions are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A validated outer request does not validate the shape of an open nested mapping; fixed-key reads bypass the route's declared contract.","references":[],"remediation":"Validate the nested value with `PayloadModel.model_validate(...)`, then use typed attributes.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_pydantic_for_structured_payload.py","status":"active","summary":"Structured nested FastAPI payloads must be parsed into a named Pydantic model before field access.","test":"packages/python/tests/rules/test_require_pydantic_for_structured_payload.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ418","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"from pydantic import BaseModel, Field\nclass Page(BaseModel):\n    page_number: int = Field(default=1, ge=1, description=\"1 for the \\x66irst page\")\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"escaped-bounded-description","outcome":"accept","scenarioId":"decoded-description","title":"The same decoded ordinal description has an enforced lower bound"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"from pydantic import BaseModel, Field\nclass Page(BaseModel):\n    page_number: int = Field(default=1, description=\"1 for the \\x66irst page\")\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"escaped-ordinal-description","outcome":"reject","scenarioId":"decoded-description","title":"Escaped description values preserve the ordinal minimum contract"},{"expectedCount":0,"files":[{"path":"api.py","source":"from pydantic import BaseModel, Field, PositiveInt\n\nclass CallDetail(BaseModel):\n    retry_attempt_number: PositiveInt = Field(default=1, description='Which dial this call is within its retry group (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":"from pydantic import BaseModel, Field\n\nclass CallDetail(BaseModel):\n    retry_attempt_number: int = Field(default=1, description='Which dial this call is within its retry group (1 for the first attempt).')\n"}],"fixedFiles":[],"focusPath":"api.py","id":"ordinal-prose-only","outcome":"reject","scenarioId":"primary","title":"An unqualified ordinal origin lacks a lower bound"}],"filePatterns":[],"id":"require-pydantic-ordinal-lower-bound","key":"python:require-pydantic-ordinal-lower-bound","languages":["python"],"limitations":["Only direct, import-proven Pydantic BaseModel fields with a literal default and an unqualified `N for the first ...` clause at the start of a description or parenthetical clause are inspected.","Descriptions that mention a smaller numeric value or explicit sentinel and exception language are skipped because the ordinal origin may not be the field minimum.","Import-proven field or model validators cause conservative abstention; their bodies are not interpreted.","Unknown named aliases, indirect model subclasses, nested container constraints, tests, and generated files are out of scope.","ERROR requires one direct BaseModel base, no class decorators or keywords, and an inert field/docstring/pass body. Descriptors, methods, configuration, mixed bases and computed field values retain WARNING because class-creation effects can change validation."],"messageIds":[],"optionsSchema":null,"rationale":"An unqualified `N for the first ...` mapping defines the origin of an ordinal field; matching schema metadata keeps runtime validation and generated JSON Schema aligned with that public contract.","references":[],"remediation":"Use `PositiveInt` for a 1-based ordinal, `NonNegativeInt` for a 0-based ordinal, or encode the origin with `Field(ge=N)` or equivalent top-level `Annotated` metadata.","since":null,"source":"packages/python/src/sarj_python_lint/rules/require_pydantic_ordinal_lower_bound.py","status":"active","summary":"A Pydantic ordinal field maps its first position to N but accepts smaller integers.","test":"packages/python/tests/rules/test_require_pydantic_ordinal_lower_bound.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    \"\"\"Without the stable key, retries would render the widget twice.\"\"\"\n    assert render(widget)\n"}],"fixedFiles":[],"focusPath":"tests/test_widget.py","id":"test-regression-context","outcome":"accept","scenarioId":"primary","title":"Docstring records a hidden failure mode"}],"filePatterns":[],"id":"restated-test-docstring","key":"python:restated-test-docstring","languages":["python"],"limitations":["Only module-level test* functions and methods of base-free, constructor-free Test* classes in recognized test files are checked; import-proven pytest marks are allowed, while other decorators abstain.","Structured, protected, value-bearing, generated, runtime-consumed, and lexically non-restating docstrings are preserved; semantic paraphrases are not inferred.","Literal or ambiguous __test__ opt-outs are excluded; repository-specific pytest collection-name overrides are not inferred."],"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 only the restatement and put the scenario and expected outcome in the test name. Keep concise non-obvious regression, constraint, or contract context in the docstring or a local comment.","since":null,"source":"packages/python/src/sarj_python_lint/rules/restated_test_docstring.py","status":"active","summary":"Collected test docstring only repeats names and visible code.","test":"packages/python/tests/rules/test_restated_test_docstring.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ556","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/users.py","source":"class UserService:\n    async def get(self, key: str):\n        rows = await self.get_many([key])\n        return rows[0] if rows else None\n    async def get_many(self, keys: list[str]):\n        return await self.store.get_many(keys)\n"}],"fixedFiles":[],"focusPath":"app/users.py","id":"bulk-first-adapter","outcome":"accept","scenarioId":"primary","title":"Implement singleton access as a one-item bulk operation"},{"expectedCount":0,"files":[{"path":"app/users.py","source":"class UserService:\n    def upsert(self, user):\n        return self.upsert_many([user])[0]\n    def upsert_many(self, users):\n        return self.store.upsert_many(users)\n"}],"fixedFiles":[],"focusPath":"app/users.py","id":"bulk-first-write","outcome":"accept","scenarioId":"write","title":"Write through the bulk primitive"},{"expectedCount":1,"files":[{"path":"app/users.py","source":"class UserService:\n    def upsert(self, user):\n        return self.upsert_many([user])[0]\n    def upsert_many(self, users):\n        return [self.upsert(user) for user in users]\n"}],"fixedFiles":[],"focusPath":"app/users.py","id":"inverted-bulk-write","outcome":"reject","scenarioId":"write","title":"Bulk writes must not call back through the singleton"},{"expectedCount":1,"files":[{"path":"app/users.py","source":"class UserService:\n    async def get(self, key: str):\n        return await self.store.get(key)\n"}],"fixedFiles":[],"focusPath":"app/users.py","id":"missing-bulk-api","outcome":"reject","scenarioId":"primary","title":"A singleton operation needs a bulk primitive"}],"filePatterns":[],"id":"service-singleton-delegates-to-bulk","key":"python:service-singleton-delegates-to-bulk","languages":["python"],"limitations":["Checks concrete instance CRUD methods declared on stable module-level Service, Store and Repository classes and their same-file implementations, with at least one input; inherited singleton declarations and arbitrary domain verbs are not inferred. Existing get_by_ids bulk reads are accepted alongside get_many.","Tests, fakes, generated/vendor code, stubs, data classes, protocols, static/class methods, abstract/overload declarations and rewriting decorators are excluded.","Missing-bulk update checks defer an implicit-resource ambiguity when an own stable concrete parameterless get and update explicitly return the same earlier, stable same-file data model and no CRUD bulk peer is declared or uncertain. This bounded source shape reduces recall; it does not prove that the resource is global or that existing bulk adapters are safe.","Same-file inherited methods use a local C3 order across at most 64 earlier, uniquely bound, undecorated or neutral-decorated classes without explicit metaclasses, with a 4096-entry merge budget. Known data-model descendants and larger role/ancestry graphs are excluded. Unknown or inconsistent MROs defer inherited evidence; missing bulk methods are not inferred through unknown external bases. Aliases declared as class members, rebound, overloaded, descriptor and decorated bulk members are deferred. Direct instance member writes and source-proven literal builtin setattr/delattr calls in visible local ancestors remain uncertain across method overrides; known receiver __class__ writes defer the whole API. Constructor chaining and opaque mutation calls are not inferred. Dynamic dispatch and imported inheritance remain type-checker and review concerns.","Adapters use straight-line calls, scalar and one-item list/tuple/set aliases, stable local aliases of bound self methods, and private instance helpers, with a bounded 64-state walk. Local callable aliases must retain the original self binding and be called without rebinding or escape. Bulk async generators defer adapter consumption proof, including anext and awaitability; known reverse calls remain errors. Complex control flow requires an exact exception or a simple adapter helper.","Requires the original item and shared parameters to reach the bulk call and rejects original-receiver reverse calls through at most 64 visible same-class methods. Reverse scans stop each statement list after a direct return or raise and prune literal Boolean branches and certainly short-circuited Boolean operands. Unknown flow, active comprehensions and potentially consumed generators retain potential reverse edges; no general control-flow or consumption proof is attempted. It does not prove database batching, result projection, authorization, atomicity or arbitrary external side effects."],"messageIds":[],"optionsSchema":null,"rationale":"A singleton-only API forces callers into serial operations. Separate singleton and bulk implementations duplicate filtering, validation and mutation behavior; making bulk loop through singleton preserves that cost and can recurse when the singleton is later made an adapter.","references":[],"remediation":"Implement get_many, create_many, insert_many, upsert_many, update_many or delete_many as the primitive. Delegate the matching singleton through a one-item collection, forwarding shared context and awaiting async operations. Preserve result projection, missing-item, authorization and transaction semantics; use a reasoned exact-code suppression for a boundary that cannot support batching.","since":null,"source":"packages/python/src/sarj_python_lint/rules/service_singleton_delegates_to_bulk.py","status":"active","summary":"Service singleton CRUD operations must delegate to a bulk primitive.","test":"packages/python/tests/rules/test_service_singleton_delegates_to_bulk.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ459","defaultLevel":"error","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, Field\nSHARED = []\nclass Model(BaseModel):\n    values: list[int] = Field(default_factory=lambda: SHARED)\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"captured-list","outcome":"reject","scenarioId":"primary","title":"A factory returns the same mutable list for every model"},{"expectedCount":0,"files":[{"path":"app/models.py","source":"from pydantic import BaseModel, Field\nclass Model(BaseModel):\n    values: list[int] = Field(default_factory=list)\n"}],"fixedFiles":[],"focusPath":"app/models.py","id":"fresh-list","outcome":"accept","scenarioId":"primary","title":"The factory creates an independent list"}],"filePatterns":[],"id":"shared-mutable-pydantic-factory","key":"python:shared-mutable-pydantic-factory","languages":["python"],"limitations":["Only direct module-level BaseModel subclasses without configuration or methods and public annotated Field declarations are inspected.","Factories must be zero-argument lambdas or undecorated module functions containing one return, optionally after a docstring.","Captured values require unique, acyclic module bindings to built-in mutable containers or direct unconfigured BaseModel instances.","Rebound names, shadowed imports, inherited or configured models, unknown services, nested scopes, and generated files are excluded.","Fields enabling default validation or supplying unknown keyword options are excluded because validation may copy or transform the factory result.","Factories producing new values, explicit copies, or values derived from validated data are excluded; intentional sharing needs a local suppression.","ERROR requires an inert body of plain field declarations, literal values, docstrings and pass statements. Descriptors, computed members and other class-creation effects retain WARNING because they can replace the source factory."],"messageIds":[],"optionsSchema":null,"rationale":"A default factory returning an existing mutable object gives different models the same state. Mutating one model can therefore silently change another model.","references":[],"remediation":"Construct a fresh value in the factory, or deliberately copy the required initial state.","since":null,"source":"packages/python/src/sarj_python_lint/rules/shared_mutable_pydantic_factory.py","status":"active","summary":"Pydantic default factories should not return a captured mutable singleton.","test":"packages/python/tests/rules/test_shared_mutable_pydantic_factory.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ023","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/service.py","source":"from string.templatelib import Template\n\n\ndef render(value: str = 'sample') -> Template:\n    return t\"value: {_parse(value)}\"\n\ndef _parse(value: str) -> str:\n    return value.strip()\n\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"already-ordered-deferred-template","outcome":"accept","scenarioId":"deferred-template","title":"Already ordered: Template syntax proves deferred module annotations"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"\ndef render() -> 'tuple':\n    return _pack()\n\ndef _pack(value: 'tuple' = (-1, (None, -0.0, +1j))) -> 'tuple':\n    return value\n\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"already-ordered-immutable-defaults","outcome":"accept","scenarioId":"immutable-defaults","title":"Already ordered: Signed numeric and deeply immutable tuple defaults"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"\ndef render(value: 'str' = 'sample') -> 'str':\n    return _parse(value)\n\ndef _parse(value: 'str' = 'sample') -> 'str':\n    return value.strip()\n\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"already-ordered-literal-headers","outcome":"accept","scenarioId":"literal-headers","title":"Already ordered: Literal headers preserve annotations and defaults"},{"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"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"from __future__ import annotations\n\n\ndef render(value: str) -> bool:\n    return _parse(value)\n\ndef _parse(value: str) -> bool:\n    return bool(value)\n\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"postponed-helper-after-sole-caller","outcome":"accept","scenarioId":"postponed-annotations","title":"Postponed module helper already follows its sole caller"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"from string.templatelib import Template\n\ndef _parse(value: str) -> str:\n    return value.strip()\n\ndef render(value: str = 'sample') -> Template:\n    return t\"value: {_parse(value)}\"\n"}],"fixedFiles":[{"path":"app/service.py","source":"from string.templatelib import Template\n\n\ndef render(value: str = 'sample') -> Template:\n    return t\"value: {_parse(value)}\"\n\ndef _parse(value: str) -> str:\n    return value.strip()\n\n"}],"focusPath":"app/service.py","id":"safe-autofix-deferred-template","outcome":"reject","scenarioId":"deferred-template","title":"Template syntax proves deferred module annotations"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"def _pack(value: 'tuple' = (-1, (None, -0.0, +1j))) -> 'tuple':\n    return value\n\ndef render() -> 'tuple':\n    return _pack()\n"}],"fixedFiles":[{"path":"app/service.py","source":"\ndef render() -> 'tuple':\n    return _pack()\n\ndef _pack(value: 'tuple' = (-1, (None, -0.0, +1j))) -> 'tuple':\n    return value\n\n"}],"focusPath":"app/service.py","id":"safe-autofix-immutable-defaults","outcome":"reject","scenarioId":"immutable-defaults","title":"Signed numeric and deeply immutable tuple defaults"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"def _parse(value: 'str' = 'sample') -> 'str':\n    return value.strip()\n\ndef render(value: 'str' = 'sample') -> 'str':\n    return _parse(value)\n"}],"fixedFiles":[{"path":"app/service.py","source":"\ndef render(value: 'str' = 'sample') -> 'str':\n    return _parse(value)\n\ndef _parse(value: 'str' = 'sample') -> 'str':\n    return value.strip()\n\n"}],"focusPath":"app/service.py","id":"safe-autofix-literal-headers","outcome":"reject","scenarioId":"literal-headers","title":"Literal headers preserve annotations and defaults"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"from __future__ import annotations\n\ndef _parse(value: str) -> bool:\n    return bool(value)\n\ndef render(value: str) -> bool:\n    return _parse(value)\n"}],"fixedFiles":[{"path":"app/service.py","source":"from __future__ import annotations\n\n\ndef render(value: str) -> bool:\n    return _parse(value)\n\ndef _parse(value: str) -> bool:\n    return bool(value)\n\n"}],"focusPath":"app/service.py","id":"safe-postponed-annotations-autofix","outcome":"reject","scenarioId":"postponed-annotations","title":"Postponed module annotations retain their signatures during a safe move"}],"filePatterns":[],"id":"stepdown","key":"python:stepdown","languages":["python"],"limitations":["Safe fixes require immutable literal defaults, no decorators/type parameters/comments, and annotations that are absent, literal strings/None, or deferred by an explicit future import or Python 3.14 t-string syntax. Signed numeric constants and deeply immutable tuples are accepted within a small expression budget. Mutable/computed defaults, class members and other header evaluation remain review-only.","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.","Only lexically resolved builtin classmethod/staticmethod decorators are transparent; other decorators and executable statements are movement barriers.","Only direct calls and nonescaping local callable aliases establish callers; escaped values and ambiguous receivers are excluded."],"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":"maintainability","code":"SARJ517","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/user_store.py","source":"from pydantic import BaseModel\nclass UserInput(BaseModel):\n    id: str\n    name: str\nclass UserStore:\n    async def upsert(self, data: UserInput) -> User:\n        await self.cursor.execute('INSERT INTO users (id, name) VALUES (%s, %s) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name', (data.id, data.name))\n    async def set_name(self, id_: str, name: str) -> User:\n        await self.cursor.execute('UPDATE users SET name = %s WHERE id = %s', (name, id_))\n"}],"fixedFiles":[],"focusPath":"app/user_store.py","id":"duplicate-field-update","outcome":"reject","scenarioId":"primary","title":"Consolidate an ordinary field write"},{"expectedCount":0,"files":[{"path":"app/user_store.py","source":"from pydantic import BaseModel\nclass UserInput(BaseModel):\n    id: str\n    name: str\nclass UserStore:\n    async def upsert(self, data: UserInput) -> User:\n        await self.cursor.execute('INSERT INTO users (id, name) VALUES (%s, %s) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name', (data.id, data.name))\n    async def set_name(self, id_: str, name: str, version: int) -> User:\n        await self.cursor.execute('UPDATE users SET name = %s WHERE id = %s AND version = %s', (name, id_, version))\n"}],"fixedFiles":[],"focusPath":"app/user_store.py","id":"guarded-field-update","outcome":"accept","scenarioId":"primary","title":"Keep an update with a distinct concurrency contract"}],"filePatterns":[],"id":"store-field-update-reuses-upsert","key":"python:store-field-update-reuses-upsert","languages":["python"],"limitations":["Only a single literal PostgreSQL UPDATE on the same table as a literal upsert is checked, with equality predicates exactly matching the conflict key and direct bound-value assignments already covered by EXCLUDED field assignments.","The command's fields must resolve from a source-declared class and flow directly into one INSERT VALUES row through literal positional or named parameters. Shadowed annotations, explicit descriptors and class keywords do not prove writable fields. The setter and upsert must declare the same result type, RETURNING projection and updated_at policy. SQL composition, inherited fields, dynamic commands, conditional or transformed upserts, unknown decorators, Python branching, arithmetic, joins, CTEs and multiple statements are excluded.","The finding is a consolidation review, not proof that INSERT ON CONFLICT preserves update-only behavior. A missing row must not be created accidentally; omitted fields must not be overwritten by defaults.","Context-manager behavior and other surrounding effects are not proven equivalent. Share ordinary write implementation behind existing authorization or transaction wrappers while preserving those contracts.","Generated and test sources are excluded. There is no automatic replacement and no requirement to expose immutable or privileged model fields."],"messageIds":[],"optionsSchema":null,"rationale":"Independent per-field SQL methods duplicate write policy and can drift from the existing model command's validation, conflict handling and timestamps.","references":[],"remediation":"Prefer one typed model command for ordinary writable fields. Reuse or extend its write implementation while preserving update-only, missing-row, omission/null, authorization and concurrency contracts; retain domain transitions as separate operations.","since":null,"source":"packages/python/src/sarj_python_lint/rules/store_field_update_reuses_upsert.py","status":"active","summary":"A plain field UPDATE duplicates fields already writable through the store's upsert command.","test":"packages/python/tests/rules/test_store_field_update_reuses_upsert.py"},{"aliases":["get-delegates-to-get-many"],"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.fetchrow('SELECT * FROM users WHERE id = %s', user_id)\n\n    async def get_many(self, user_ids: list[UserId]) -> list[User]:\n        return await self.fetch('SELECT * FROM users WHERE id = ANY(%s)', 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":1,"files":[{"path":"app/task_store.py","source":"class TaskStore:\n    async def set_to_failed(self, task_id: str) -> Task:\n        return await self._update_status(task_id, TaskStatus.FAILED)\n\n    async def set_many_to_status(\n        self, task_ids: Collection[str], status: TaskStatus\n    ) -> list[Task]:\n        return await self._update_many(task_ids, status)\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"duplicate-singleton-write","outcome":"reject","scenarioId":"write","title":"Do not maintain a second singleton mutation 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_by_ids([user_id])\n        return rows.get(user_id)\n\n    async def get_by_ids(self, user_ids: list[UserId]) -> dict[UserId, 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"},{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"class TaskStore:\n    async def set_to_failed(self, task_id: str) -> Task:\n        rows = await self.set_many_to_status([task_id], TaskStatus.FAILED)\n        return rows[0]\n\n    async def set_many_to_status(\n        self, task_ids: Collection[str], status: TaskStatus\n    ) -> list[Task]:\n        return await self._update_many(task_ids, status)\n"}],"fixedFiles":[],"focusPath":"app/task_store.py","id":"singleton-write-delegates","outcome":"accept","scenarioId":"write","title":"Route singleton mutations through the bulk primitive"}],"filePatterns":[],"id":"store-get-delegates-to-bulk-read","key":"python:store-get-delegates-to-bulk-read","languages":["python"],"limitations":["Concrete wrappers declared with a canonical bulk method in a production Store class are inspected; the canonical method may be abstract.","Reads retain the strict get/get_many contract. Writes require a typed collection key, compatible context and result types, and a recognizable many-method name.","The canonical write topology is the exact set_many_to_status(..., status) shape, with wrappers binding the matching status enum member.","A source-declared upsert/upsert_many pair with one matching typed model input and scalar/collection result also shares one canonical write implementation. Preserve conflict, tenant, null/omission and missing-row behavior; implement a real batch primitive before delegating to avoid recursion.","Wrappers with try/finally or other unsupported compound control flow are not diagnosed.","Singleton implementations with explicit cache or lock identifiers are excluded because their access path differs intentionally.","Signature compatibility is advisory: dynamic helpers and behavior not visible in the two method bodies require review.","Closed literal starred collection expansions preserve singleton delegation; unknown iterable expansion or additional keys do not prove the singleton contract.","Straight-line command and singleton-collection aliases preserve delegation; reassigned inputs and unsupported alias mutations do not prove that the original command reaches the bulk primitive.","Rebound or duplicate class members and visible instance member writes, including source-proven literal builtin setattr/delattr, defer advisory pairs; receiver __class__ writes defer the whole API. Same-file ancestor mutation discovery stops at 64 classes; opaque calls and imported constructor effects are not inferred. Unknown or rewriting class/method decorators defer body equivalence; supported abc and typing markers must retain their declaration-time binding."],"messageIds":[],"optionsSchema":null,"rationale":"Independent singleton and bulk queries can drift in filtering, mutation semantics, row conversion, authorization, timestamps, and missing-row behavior while maintaining two database access paths.","references":[],"remediation":"Delegate only when tenant, authorization, consistency, cache, lock, transaction, conversion, and missing-row semantics match. Otherwise add an exact SARJ421 suppression naming the concrete semantic difference.","since":null,"source":"packages/python/src/sarj_python_lint/rules/store_get_delegates_to_bulk_read.py","status":"active","summary":"A store singleton operation should reuse its equivalent bulk implementation.","test":"packages/python/tests/rules/test_store_get_delegates_to_bulk_read.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ547","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/fixtures/rows.py","source":"from psycopg import AsyncCursor\nclass RowsFixture:\n    def __init__(self, cursor: AsyncCursor):\n        self.cursor = cursor\n    async def insert(self):\n        await self.cursor.execute(\"INSERT INTO item(id) VALUES(%s) ON CONFLICT DO NOTHING\", (1,))\n"}],"fixedFiles":[],"focusPath":"tests/fixtures/rows.py","id":"fixture-declared-policy","outcome":"accept","scenarioId":"primary","title":"A fixture declares its duplicate behavior"},{"expectedCount":1,"files":[{"path":"tests/fixtures/rows.py","source":"from psycopg import AsyncCursor\nclass RowsFixture:\n    def __init__(self, cursor: AsyncCursor):\n        self.cursor = cursor\n    async def insert(self):\n        await self.cursor.execute(\"INSERT INTO item(id) VALUES(%s)\", (1,))\n"}],"fixedFiles":[],"focusPath":"tests/fixtures/rows.py","id":"fixture-insert-policy","outcome":"reject","scenarioId":"primary","title":"Fixture inserts need an explicit duplicate decision"}],"filePatterns":[],"id":"store-insert-requires-explicit-conflict-policy","key":"python:store-insert-requires-explicit-conflict-policy","languages":["python"],"limitations":["Checks recovered INSERT statements reaching import-proven Psycopg execution in recognized store modules or class-owned persistence helpers, including test fixtures and direct same-class forwarding wrappers. Generated Python and unresolved SQL identifiers outside an INSERT's own RETURNING expressions abstain. Unresolved fields wholly within its RETURNING expressions cannot supply a preceding ON CONFLICT clause and do not hide a missing policy; nested INSERTs establish their own RETURNING boundary.","Each recovered INSERT, including one inside a data-modifying CTE, needs its own explicit PostgreSQL ON CONFLICT clause to declare a policy. This rule does not prove its target, concurrency correctness, replay behavior or side effects. SELECT-side existence checks alone do not establish a concurrency-safe duplicate policy.","Plain INSERT remains a review finding when Python handles UniqueViolation or a fixture comment states invalid-row intent; preserve those contracts with an explained exact exception.","Replay-named store INSERT findings already owned by SARJ018 take precedence. This broader policy remains a warning and requires an explained exact exception for intentional database-error fixtures."],"messageIds":[],"optionsSchema":null,"rationale":"A visible SQL conflict policy makes retry and fixture behavior reviewable at the write boundary. Plain INSERT can intentionally reject duplicates; record that contract with an exact rule exception.","references":[],"remediation":"Declare the intended ON CONFLICT behavior, or add an exact SARJ547 exception explaining why duplicate rejection, append-only insertion, or deliberately invalid fixture data is required. Preserve uniqueness, errors, return shape and transaction behavior; DO NOTHING is not an automatic replacement.","since":null,"source":"packages/python/src/sarj_python_lint/rules/store_insert_requires_explicit_conflict_policy.py","status":"active","summary":"Declare SQL conflict behavior for store INSERT statements, including fixture writes.","test":"packages/python/tests/rules/test_store_insert_requires_explicit_conflict_policy.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ546","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":1,"files":[{"path":"app/item_store.py","source":"from psycopg import AsyncCursor\nclass ItemStore:\n    def __init__(self, cursor: AsyncCursor):\n        self.cursor = cursor\n    async def upsert(self, item):\n        await self.cursor.execute('INSERT INTO item(id, name) VALUES(%s, %s) ON CONFLICT(id) DO UPDATE SET name=EXCLUDED.name', (item.id, item.name))\n    async def set_name_before_start(self, key, name):\n        await self.cursor.execute('UPDATE item SET name=%s WHERE id=%s AND started_at IS NULL', (name, key))\n"}],"fixedFiles":[],"focusPath":"app/item_store.py","id":"conditional-setter","outcome":"reject","scenarioId":"primary","title":"Review a separate guarded setter"},{"expectedCount":0,"files":[{"path":"app/item_store.py","source":"from psycopg import AsyncCursor\nclass ItemStore:\n    def __init__(self, cursor: AsyncCursor):\n        self.cursor = cursor\n    async def upsert(self, item):\n        await self.cursor.execute('INSERT INTO item(id, name) VALUES(%s, %s) ON CONFLICT(id) DO UPDATE SET name=EXCLUDED.name', (item.id, item.name))\n    async def set_name_before_start(self, key, name):\n        await self.cursor.execute('UPDATE other SET name=%s WHERE id=%s AND started_at IS NULL', (name, key))\n"}],"fixedFiles":[],"focusPath":"app/item_store.py","id":"different-relation","outcome":"accept","scenarioId":"primary","title":"Different relation ownership is preserved"}],"filePatterns":[],"id":"store-mutation-command-fanout","key":"python:store-mutation-command-fanout","languages":["python"],"limitations":["This is API consolidation advice, not proof that writes are equivalent. Checks non-test store modules with an upsert-named command reaching an INSERT and a set_-named direct UPDATE of the same exactly qualified relation. A bounded direct same-class helper call can establish that INSERT target; the command name does not prove conflict handling. Dynamic relation names, inherited upserts, unknown decorators or receivers, unknown owners and unrelated relations abstain. Duplicate or overwritten declarations and direct method attribute assignments are excluded; indirect runtime monkey-patching remains unsupported.","Also reviews source-visible abstract set_ methods declared beside an abstract upsert in a store interface. The explicit setter API spelling triggers this interface policy; it does not prove the method is redundant or identify its implementation.","The precise ordinary-field duplication owner SARJ517 takes precedence at the same method. Guarded and multi-statement operations can still be reported for review, but their transaction and concurrency contracts must be retained. One diagnostic is emitted per method, with no automatic edit."],"messageIds":[],"optionsSchema":null,"rationale":"Per-field mutation entry points beside a model upsert can scatter write validation and concurrency policy. A smaller typed command surface makes those contracts easier to review together.","references":[],"remediation":"Review whether these methods can share a typed mutation command or internal write implementation. Preserve update-only and missing-row behavior, authorization, omission versus null, and guarded transitions. Do not replace a conditional UPDATE with an insert-capable upsert; model distinct transitions explicitly when they must stay separate.","since":null,"source":"packages/python/src/sarj_python_lint/rules/store_mutation_command_fanout.py","status":"active","summary":"Review per-field store mutations beside an existing model write command.","test":"packages/python/tests/rules/test_store_mutation_command_fanout.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ464","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/processes.py","source":"import subprocess\ndef run(args):\n    process = subprocess.Popen(args)\n    try:\n        process.communicate(timeout=1)\n    except subprocess.TimeoutExpired:\n        process.kill()\n        process.communicate()\n"}],"fixedFiles":[],"focusPath":"app/processes.py","id":"reaped-child","outcome":"accept","scenarioId":"primary","title":"Drain and wait after killing"},{"expectedCount":1,"files":[{"path":"app/processes.py","source":"import subprocess\ndef run(args):\n    process = subprocess.Popen(args)\n    try:\n        process.communicate(timeout=1)\n    except subprocess.TimeoutExpired:\n        process.kill()\n"}],"fixedFiles":[],"focusPath":"app/processes.py","id":"unreaped-child","outcome":"reject","scenarioId":"primary","title":"Killing does not complete cleanup"}],"filePatterns":[],"id":"subprocess-kill-without-reap","key":"python:subprocess-kill-without-reap","languages":["python"],"limitations":["Requires a unique, dominating local subprocess.Popen assignment and a resolved TimeoutExpired handler ending with a direct kill() after that process's wait or communicate.","Borrowed processes, custom factories, context-managed ownership, aliases, escaped handles or cleanup callbacks, and any later wait/communicate/poll are excluded conservatively.","Only direct timeout handlers are checked. Cross-function lifecycle, fields, dynamic mutation, arbitrary exit paths, and generated files are not analyzed. Choosing wait versus communicate requires reviewing output ownership.","Visible os child-reaper imports/calls and SIGCHLD ownership exclude the file. Later internal Popen cleanup helpers are recognized conservatively, without recommending private APIs.","Visible namespace APIs, frame/module introspection, code mutation and bound attribute setters invalidate the file's provenance proof."],"messageIds":[],"optionsSchema":null,"rationale":"Sending a kill signal does not wait for process termination or collect the exit status; deterministic cleanup still requires waiting or communication.","references":[],"remediation":"After kill(), call communicate() when owning captured pipes, or wait() when output is handled separately; an enclosing Popen context manager can also own cleanup.","since":null,"source":"packages/python/src/sarj_python_lint/rules/subprocess_kill_without_reap.py","status":"active","summary":"Reap a locally owned subprocess after killing it on timeout.","test":"packages/python/tests/rules/test_subprocess_kill_without_reap.py"},{"aliases":[],"autofix":"none","category":"testing","code":"SARJ495","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_delivery.py","source":"def test_delivery(sender):\n    deliver('hello', sender)\n    assert sender.messages == ['hello']\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"observed-effect","outcome":"accept","scenarioId":"primary","title":"The assertion distinguishes a missing delivery"},{"expectedCount":1,"files":[{"path":"tests/test_delivery.py","source":"def test_delivery():\n    deliver('hello')\n"}],"fixedFiles":[],"focusPath":"tests/test_delivery.py","id":"unobserved-effect","outcome":"reject","scenarioId":"primary","title":"Observe whether the requested side effect occurred"}],"filePatterns":[],"id":"test-without-explicit-oracle","key":"python:test-without-explicit-oracle","languages":["python"],"limitations":["This opt-in advisory is excluded from default Python checks; select it explicitly or use code-standards observe.","Only default-collected test_* functions and methods of undecorated or pytest-marked, constructor-free, base-free Test* classes are inspected.","Assertions, explicit raises, verification-named calls, import aliases, and local helpers containing or forwarding an oracle are accepted conservatively.","Unknown decorators, test opt-outs, benchmark/LLM/skipped tests, and callable fixture parameters are left unreported; external helper behavior is not inferred.","An intentional no-throw test may match. Findings describe syntax and require contract review; they never authorize automatic deletion or error-level promotion."],"messageIds":[],"optionsSchema":null,"rationale":"A test can execute a side effect without observing whether the intended effect occurred. The absence of an explicit oracle is a review candidate, not proof that a smoke or no-throw contract is weak.","references":[],"remediation":"Identify the intended contract. Reuse adequate existing coverage, observe the returned value, state, or emitted effect, or retain the test when successful execution is itself the contract. Do not add a tautological assertion or delete a test solely to silence this advisory.","since":null,"source":"packages/python/src/sarj_python_lint/rules/test_without_explicit_oracle.py","status":"active","summary":"A collected test has no visible assertion or exception oracle.","test":"packages/python/tests/rules/test_test_without_explicit_oracle.py"},{"aliases":["created-at-order-requires-tiebreaker"],"autofix":"none","category":"correctness","code":"SARJ407","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/task_store.py","source":"QUERY = (\n    \"SELECT id, created_at FROM task \"\n    \"WHERE (created_at, id) < (%s, %s) \"\n    \"ORDER BY created_at DESC, id DESC LIMIT 50\"\n)\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 = (\n    \"SELECT id, created_at FROM task \"\n    \"WHERE created_at < %s \"\n    \"ORDER BY created_at DESC LIMIT 50\"\n)\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":"timestamp-order-requires-tiebreaker","key":"python:timestamp-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 unquoted `*_at` column only when it is the final same-depth `ORDER BY` item before LIMIT, OFFSET, or FETCH, or when only a volatile/constant pseudo-key follows. Uniqueness is not schema-proven.","Dynamic string construction, formatted strings, quoted identifiers, expressions containing a timestamp column, and non-SELECT fragments are excluded.","The intended stable key cannot be inferred safely, so the rule does not offer an autofix.","Grouped, DISTINCT, WITH TIES, unbounded, window-only, aggregate-only, and docstring SQL are excluded.","A scalar query that projects only the exact timestamp ordering expression is excluded because equal-timestamp rows produce the same observable value."],"messageIds":[],"optionsSchema":null,"rationale":"Timestamp columns are not necessarily unique, so bounded reads can choose different rows at a page or top-N boundary. Keyset pagination also needs the same secondary key in its cursor predicate.","references":[],"remediation":"Add a deterministic unique key after the timestamp. For keyset pagination, include the key in both the ORDER BY tuple and cursor predicate, such as `(created_at, id) < (%s, %s)`.","since":null,"source":"packages/python/src/sarj_python_lint/rules/timestamp_order_requires_tiebreaker.py","status":"active","summary":"Bounded store SQL whose final result-order key looks like a `*_at` timestamp should include a deterministic secondary key.","test":"packages/python/tests/rules/test_timestamp_order_requires_tiebreaker.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ435","defaultLevel":"warning","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"app/errors.py","source":"class MissingFilesError(Exception):\n    def __init__(self, paths: list[str]) -> None:\n        self.paths = paths\n        super().__init__('Files are missing')\n\ndef render_error(error: MissingFilesError) -> str:\n    return ', '.join(error.paths)\n"}],"fixedFiles":[],"focusPath":"app/errors.py","id":"dynamic-context-kept-typed","outcome":"accept","scenarioId":"dynamic-context","title":"Keep arbitrary paths as typed context without an enum"},{"expectedCount":1,"files":[{"path":"app/errors.py","source":"class MissingFilesError(Exception):\n    def __init__(self, paths: list[str]) -> None:\n        super().__init__(', '.join(paths))\n"}],"fixedFiles":[],"focusPath":"app/errors.py","id":"dynamic-context-review","outcome":"reject","scenarioId":"dynamic-context","title":"Review a contextual list without converting paths to enum values"},{"expectedCount":1,"files":[{"path":"app/errors.py","source":"class DomainError(Exception): ...\n\nclass IncompleteError(DomainError):\n    def __init__(self, reasons: list[str]) -> None:\n        self.reasons = reasons\n        super().__init__(f\"Incomplete: {'; '.join(reasons)}\")\n"}],"fixedFiles":[],"focusPath":"app/errors.py","id":"rendered-reason-list","outcome":"reject","scenarioId":"primary","title":"An exception renders raw reason strings"},{"expectedCount":0,"files":[{"path":"app/service.py","source":"class BatchError(Exception):\n    def __init__(self, reason: str) -> None:\n        super ().__init__(reason)\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"spaced-contextual-string","outcome":"accept","scenarioId":"native-token-spacing","title":"Separate contextual strings remain outside the sole list parameter contract"},{"expectedCount":1,"files":[{"path":"app/service.py","source":"class BatchError(Exception):\n    def __init__(self, reasons: list [ str ]) -> None:\n        super ().__init__(\"; \". join (reasons))\n"}],"fixedFiles":[],"focusPath":"app/service.py","id":"spaced-string-reason-join","outcome":"reject","scenarioId":"native-token-spacing","title":"Native token spacing preserves the string reason list"},{"expectedCount":0,"files":[{"path":"app/errors.py","source":"from dataclasses import dataclass\nfrom enum import StrEnum\n\nclass ReasonCode(StrEnum):\n    MISSING_URL = 'missing_url'\n\n@dataclass(frozen=True)\nclass Reason:\n    code: ReasonCode\n    field: str | None = None\n\nclass DomainError(Exception): ...\n\nclass IncompleteError(DomainError):\n    def __init__(self, reasons: list[Reason]) -> None:\n        self.reasons = reasons\n        super().__init__('Integration is incomplete')\n"}],"fixedFiles":[],"focusPath":"app/errors.py","id":"typed-reason-list","outcome":"accept","scenarioId":"primary","title":"An exception carries nominal reason records"}],"filePatterns":[],"id":"typed-error-reasons","key":"python:typed-error-reasons","languages":["python"],"limitations":["The class name and a direct base name must end in Error or Exception; this naming convention does not prove exception inheritance or resolve shadowed bases.","The constructor must have exactly one non-self parameter and no variadic parameters, annotated exactly `list[str]`, and a literal string separator must join it inside `super().__init__(...)`.","This warning requests manual review; joining strings does not prove a finite reason domain, so contextual string lists may also match and must not automatically become enums.","Formatting delegated to another function, legacy `typing.List`, mixed constructor context, tests, and generated files are intentionally not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"Fixed reason identities benefit from stable codes for API clients, UI formatting, telemetry, and exhaustive handling. Open-ended values such as file paths remain typed context, not an enum domain.","references":[],"remediation":"If the strings are fixed reason identities, use a `StrEnum` code with typed context. Keep dynamic paths and messages as typed context; format presentation separately.","since":null,"source":"packages/python/src/sarj_python_lint/rules/typed_error_reasons.py","status":"active","summary":"Review joined exception strings for fixed reason identities versus dynamic context.","test":"packages/python/tests/rules/test_typed_error_reasons.py"},{"aliases":[],"autofix":"safe","category":"testing","code":"SARJ443","defaultLevel":"error","engine":"python","examples":[{"expectedCount":0,"files":[{"path":"tests/test_widget.py","source":"def test_widgets():\n    def _make_widget(*, size=3):\n        return Widget(size=size)\n    _make_widget()\n    _make_widget(size=4)\n"}],"fixedFiles":[],"focusPath":"tests/test_widget.py","id":"exercised-size-option","outcome":"accept","scenarioId":"primary","title":"Keep customization that a caller exercises"},{"expectedCount":1,"files":[{"path":"tests/test_widget.py","source":"def _widget(*, enabled=True):\n    return Widget(enabled=enabled)\n_widget()\n_widget(enabled=True)\n"}],"fixedFiles":[],"focusPath":"tests/test_widget.py","id":"module-invariant-singleton-option","outcome":"reject","scenarioId":"primary","title":"A private test helper's callers never vary a singleton option"}],"filePatterns":[],"id":"unused-test-factory-option","key":"python:unused-test-factory-option","languages":["python"],"limitations":["Safe fixes inline proven invariant bool/None values only in closed private nested synchronous nongenerator test factories without nested closure scopes, and remove the unannotated keyword-only option plus inert literal caller keywords. Module factories, annotations, type parameters, comments, reflection, dynamic calls, callable defaults and other values remain review-only. Known frame and metadata capabilities, their import aliases and metadata field access prevent fixes.","Containing callables must also be ordinary synchronous nongenerators with proven inert execution; unknown callouts, properties, overloads, reachable frame-exporting exceptions, and helper references crossing additional function, class, lambda, or comprehension scopes prevent fixes.","Invariant bool and None options are ERROR when the closed caller proof succeeds and the file has no visible namespace access. Other literals and callable defaults remain WARNING because identity and default binding timing may differ.","Literal findings consider private module-level helpers or _make_ and _build_ helpers nested directly inside test functions, with at least two known direct callers and straight-line construction bodies.","Callable findings consider module-level private helpers with the same construction shape and an earlier undecorated local function default, only when at least two known direct callers all omit that option.","Conftest module-level factories, explicit callable arguments, exports, decorators, escaping references, reflection, unpacking, rebinding, shadowing and ambiguous mutation or patch targets are excluded.","Same-name declarations elsewhere in the file conservatively exclude a helper. Advisory findings retain their customization contract for review.","Callable findings describe known callers in this file; cross-module and dynamic callers are not inferred. Retain intentional extension points with a reasoned exception, preserving definition-time default capture."],"messageIds":[],"optionsSchema":null,"rationale":"Unused customization obscures the values that actually distinguish test scenarios.","references":[],"remediation":"Consider keeping the invariant literal or stable local callable in the construction instead of exposing an unexercised option. Preserve intentional dependency contracts and callable default binding timing.","since":null,"source":"packages/python/src/sarj_python_lint/rules/unused_test_factory_option.py","status":"active","summary":"A private test factory exposes an invariant literal or unexercised local callable option.","test":"packages/python/tests/rules/test_unused_test_factory_option.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ123","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"supabase/migrations/001_retire_version.sql","source":"ALTER TABLE call DROP CONSTRAINT IF EXISTS call_scenario_version_fkey;\nALTER TABLE call DROP COLUMN IF EXISTS scenario_version_id;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_retire_version.sql","id":"combine-related-drops","outcome":"reject","scenarioId":"primary","title":"Drop a legacy foreign key and its column together"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_retire_version.sql","source":"ALTER TABLE call\n    DROP CONSTRAINT IF EXISTS call_scenario_version_fkey,\n    DROP COLUMN IF EXISTS scenario_version_id;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_retire_version.sql","id":"combined-drops","outcome":"accept","scenarioId":"primary","title":"One statement retains the drop order and guards"}],"filePatterns":[],"id":"combine-alter-table","key":"sql:combine-alter-table","languages":["sql"],"limitations":["Only authored PostgreSQL migrations are checked; dumps, generator-owned migrations, fixtures, and test sources are excluded.","Only adjacent statements with the same explicitly qualified relation and identical IF EXISTS, ONLY, and descendant modifiers are combined. Any intervening statement or migration-section directive ends the group.","Supported groups contain only DROP COLUMN/DROP CONSTRAINT actions, only nullable additions of recognized built-in scalar types with optional NULL and without defaults or other constraints, or only identical SET NOT NULL, DROP NOT NULL, or DROP DEFAULT actions on distinct columns.","Renames, schema and partition changes, type conversions, expressions, defaults, constraint additions/validation, repeated object targets, unsupported types, and malformed syntax remain separate. No catalog or search_path equivalence is inferred.","Analysis stops at an executable COPY FROM STDIN command because raw data framing is unsupported; later statements are not checked.","No automatic rewrite moves comments or changes migration behavior. Existing DDL event triggers may observe statement boundaries; an intentional boundary needs an exact, explained rule exception."],"messageIds":[],"optionsSchema":null,"rationale":"Repeating ALTER TABLE for compatible changes to one relation obscures a single schema operation and adds avoidable statements. PostgreSQL supports comma-separated subcommands, including multiple drops.","references":[],"remediation":"Combine the reported adjacent statements into one ALTER TABLE with comma-separated actions, preserving their order, guards, table modifiers, and drop behavior. Keep deployment checkpoints and operations whose ordering, expression evaluation, or lock contracts differ in separate statements.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/combine_alter_table.py","status":"active","summary":"Combine compatible adjacent ALTER TABLE actions.","test":"packages/sql/tests/rules/test_combine_alter_table.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ130","defaultLevel":"warning","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"supabase/migrations/001_retry.sql","source":"CREATE TABLE retry (\n  attempts INTEGER NOT NULL -- required; capped 1..10\n);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_retry.sql","id":"comment-only-range","outcome":"reject","scenarioId":"primary","title":"A comment cannot enforce a declared numeric range"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_retry.sql","source":"CREATE TABLE retry (attempts INTEGER NOT NULL CHECK (attempts BETWEEN 1 AND 10));\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_retry.sql","id":"encoded-range","outcome":"accept","scenarioId":"primary","title":"CHECK encodes the permitted range without commentary"}],"filePatterns":[],"id":"declared-column-bounds","key":"sql:declared-column-bounds","languages":["sql"],"limitations":["Checks authored PostgreSQL migration CREATE TABLE columns with an attached standalone declaration: labeled numeric a..b, explicit interval a-unit..b-unit, or positive/non-negative forms, optionally prefixed by must be. Separate required/optional semicolon clauses are accepted. Qualifying, conditional, historical and arbitrary trailing prose is excluded rather than interpreted. Names, defaults, signed amounts, offsets and undocumented limits do not imply a bound.","Understands literal numeric comparisons, BETWEEN, reversed comparisons and AND in inline/table CHECK constraints. Native SMALLINT, INT and BIGINT limits contribute to the coverage proof, and integer exclusive endpoints are normalized. Simple constant intervals use milliseconds through weeks; calendar months/years, casts on columns and computed bounds are not inferred.","Unknown CHECK expressions mentioning the column cause abstention, rather than a false claim that a complex constraint is missing. Computed columns are excluded because their expression may enforce the declared bounds; ordinary defaults and identity generators do not establish a bound. Inherited tables and files containing ALTER TABLE are excluded because inherited/staged constraints are not resolved. User-defined types, including TINYINT domains, generated migrations and dumps are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A column comment declaring a range does not reject out-of-range writes. Encoding that explicit contract in CHECK constraints keeps every writer subject to the same bounds.","references":[],"remediation":"Replace the range comment with an inline CHECK that enforces the declared bounds. Preserve units, inclusive endpoints and nullability. Do not invent a positive bound from a column name.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/declared_column_bounds.py","status":"active","summary":"Encode explicitly declared numeric and interval ranges in SQL CHECK constraints.","test":"packages/sql/tests/rules/test_declared_column_bounds.py"},{"aliases":[],"autofix":"safe","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":1,"files":[{"path":"supabase/queries/events.sql","source":"SELECT timestamp::TIMESTAMP FROM events;\n"}],"fixedFiles":[],"focusPath":"supabase/queries/events.sql","id":"timestamp-column-and-naive-cast","outcome":"reject","scenarioId":"identifier-type","title":"A column identifier does not hide a naive cast"},{"expectedCount":0,"files":[{"path":"supabase/queries/events.sql","source":"SELECT timestamp FROM events;\n"}],"fixedFiles":[],"focusPath":"supabase/queries/events.sql","id":"timestamp-column-reference","outcome":"accept","scenarioId":"identifier-type","title":"A column identifier does not declare a naive type"},{"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":["Automatic literal fixes require one literal-only SELECT or one simple CREATE TABLE with canonical typed literal defaults, with no other statements, reads, CTEs, operators, other constraints, custom casts or callouts. The external PostgreSQL built-in catalog must be canonical and unmodified; source-visible mutations are refused.","Autofix only replaces an existing canonical timezone(INTERVAL '00:00', naive ISO literal cast) or equivalent AT TIME ZONE interval expression with the same validated literal and explicit +00 timestamptz cast. A pre-existing SELECT alias or canonical timestamptz column DEFAULT must preserve its output name/type. Session-zone conversions, text timezone names, unaliased results, dynamic values, precision modifiers and comments inside the expression remain manual.","Complete parseable PostgreSQL statements distinguish identifier positions from type keywords, including exact lower-case quoted built-ins and pg_catalog qualification. Custom schemas and quoted mixed-case type names are not inferred. Fragments and unsupported procedural syntax retain unquoted lexical detection."],"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":["index-budget"],"autofix":"none","category":"performance","code":"SARJ116","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/004_indexes.sql","source":"CREATE INDEX a_idx ON event(a);\nCREATE INDEX b_idx ON event(b);\nCREATE INDEX c_idx ON event(c);\nCREATE INDEX d_idx ON event(d);\n"}],"fixedFiles":[],"focusPath":"migrations/004_indexes.sql","id":"excess-index-burst","outcome":"reject","scenarioId":"primary","title":"A fourth explicit index exceeds the table budget"},{"expectedCount":0,"files":[{"path":"migrations/004_indexes.sql","source":"CREATE INDEX a_idx ON event(a);\nCREATE INDEX b_idx ON event(b);\nCREATE INDEX c_idx ON event(c);\n"}],"fixedFiles":[],"focusPath":"migrations/004_indexes.sql","id":"within-index-budget","outcome":"accept","scenarioId":"primary","title":"Three explicit indexes stay within the table budget"}],"filePatterns":[],"id":"excess-migration-index-requires-justification","key":"sql:excess-migration-index-requires-justification","languages":["sql"],"limitations":["Only authored production migrations are checked; tests, dumps, fixtures, and recognized generator-owned migrations are excluded.","Duplicate index shapes and same-name replacement statements do not consume this budget; SARJ117 reports structural duplicates.","Index declarations inside dollar-quoted stored or anonymous program bodies are not inferred.","The policy budgets distinct explicit index definitions in one file; it does not dereference query or EXPLAIN links, estimate workload selectivity, or reconstruct fleet-wide index state.","Comment-based index justifications do not waive the budget. The historical rule ID is retained for configuration compatibility."],"messageIds":[],"optionsSchema":null,"rationale":"Every explicit index adds write amplification, storage, vacuum work, and planner surface; bursts of indexes often encode unmeasured read paths in the transactional database.","references":[],"remediation":"Keep at most three explicit indexes per table and eight per migration. Remove redundant indexes, express data uniqueness with inline UNIQUE constraints, and review the workload before adding indexes. Evaluate ClickHouse for reporting, historical analysis, and broad aggregations.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/index_budget.py","status":"active","summary":"Limit explicit indexes to three per table and eight per authored migration.","test":"packages/sql/tests/rules/test_index_budget.py"},{"aliases":["add-constraint-not-valid","add-constraint-requires-not-valid","existing-table-check-or-fk-requires-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 public.users ADD CONSTRAINT check_age CHECK (age >= 18) NOT VALID;\n"},{"path":"supabase/migrations/002_validate_age.sql","source":"ALTER TABLE public.users VALIDATE CONSTRAINT check_age;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_age.sql","id":"deferred-check-validation","outcome":"accept","scenarioId":"primary","title":"Constraint validation is deferred to a later migration"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_boundary.sql","source":"CREATE TABLE \"users\" (age integer);\nALTER TABLE users ADD CHECK (age >= 18);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_boundary.sql","id":"quoted-lowercase-fresh-table","outcome":"accept","scenarioId":"semantic-boundary","title":"Quoted lowercase names retain the fresh empty-table exemption"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_boundary.sql","source":"CREATE TABLE \"Users\" (age integer);\nALTER TABLE users ADD CHECK (age >= 18);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_boundary.sql","id":"quoted-mixed-case-distinct-table","outcome":"reject","scenarioId":"semantic-boundary","title":"A mixed-case quoted name does not exempt a different existing table"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_renamed.sql","source":"CREATE TABLE scratch (id bigint);\nALTER TABLE scratch RENAME TO scratch_empty;\nALTER TABLE old_existing RENAME TO scratch;\nALTER TABLE scratch ADD CONSTRAINT positive_id CHECK (id > 0) NOT VALID;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_renamed.sql","id":"renamed-existing-relation-defers-validation","outcome":"accept","scenarioId":"relation-rename-ownership","title":"A reused relation name safely defers constraint validation"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_renamed.sql","source":"CREATE TABLE scratch (id bigint);\nALTER TABLE scratch RENAME TO scratch_empty;\nALTER TABLE old_existing RENAME TO scratch;\nALTER TABLE scratch ADD CONSTRAINT positive_id CHECK (id > 0);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_renamed.sql","id":"renamed-existing-relation-reuses-fresh-name","outcome":"reject","scenarioId":"relation-rename-ownership","title":"An existing relation cannot inherit a former fresh spelling"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_age.sql","source":"ALTER TABLE public.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":"existing-table-check-or-foreign-key-requires-not-valid","key":"sql:existing-table-check-or-foreign-key-requires-not-valid","languages":["sql"],"limitations":["Only PostgreSQL migration files and ALTER TABLE actions that directly add CHECK or foreign-key constraints are inspected.","Foreign keys may use the referenced table's implicit primary key; NOT VALID and NOT ENFORCED constraints do not require deferred validation advice.","A table is treated as fresh only after an unconditional empty CREATE TABLE in the same migration section; ambiguous or populating statements revoke that exemption.","Migration sections, executing table or materialized-view creation, extensions, and existing-table column rewrites revoke earlier empty-table proofs. Relation-resolution settings revoke unqualified proofs while preserving explicit schema identity; nonexecuting DDL retains them.","Validation transactions are not inferred; the diagnostic explains that VALIDATE CONSTRAINT belongs in a later committed migration or transaction."],"messageIds":[],"optionsSchema":null,"rationale":"Validating a CHECK or foreign key while adding it scans existing rows while PostgreSQL holds locks that can block writes to the altered table and, for a foreign key, the referenced table.","references":[],"remediation":"Add the constraint with NOT VALID and commit that migration or transaction. Validate it in a later committed migration or transaction so the lower-lock validation scan does not inherit the ADD lock.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/add_constraint_requires_not_valid.py","status":"active","summary":"CHECK and foreign-key constraints added to existing PostgreSQL tables should defer validation.","test":"packages/sql/tests/rules/test_add_constraint_requires_not_valid.py"},{"aliases":[],"autofix":"safe","category":"correctness","code":"SARJ102","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"migrations/001_orders.sql","source":"CREATE INDEX CONCURRENTLY IF NOT EXISTS orders_total ON orders (total);\n"}],"fixedFiles":[],"focusPath":"migrations/001_orders.sql","id":"guarded-index-creation","outcome":"accept","scenarioId":"index-existence","title":"A replay-safe concurrent index creation"},{"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 INDEX CONCURRENTLY orders_total ON orders (total);\n"}],"fixedFiles":[{"path":"migrations/001_orders.sql","source":"CREATE INDEX CONCURRENTLY IF NOT EXISTS orders_total ON orders (total);\n"}],"focusPath":"migrations/001_orders.sql","id":"unguarded-index-creation","outcome":"reject","scenarioId":"index-existence","title":"An index creation that fails when replayed"},{"expectedCount":1,"files":[{"path":"migrations/001_orders.sql","source":"CREATE TABLE orders (id BIGINT PRIMARY KEY);\n"}],"fixedFiles":[{"path":"migrations/001_orders.sql","source":"CREATE TABLE IF NOT EXISTS orders (id BIGINT PRIMARY KEY);\n"}],"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":["Checks native guards on table/index/schema/extension/sequence creation, materialized views, foreign tables, statistics, collations, servers and user mappings; PostgreSQL DROP object families; and ADD/DROP COLUMN, DROP CONSTRAINT/ATTRIBUTE and DROP IDENTITY/EXPRESSION.","CREATE TYPE, CREATE DOMAIN, ADD CONSTRAINT and rename operations have no native replay guard and are excluded. CREATE OR REPLACE is not required because replacing an existing definition changes behavior. An outer ALTER TABLE IF EXISTS does not guard missing columns and is not required.","Dialect-specific DDL forms are checked only where the guard syntax is supported. Existence guards do not verify that an existing object's definition matches the intended schema.","Analysis stops at COPY FROM STDIN because raw data framing and commands after its terminator are not modeled.","Safe fixes add only supported PostgreSQL existence guards to parseable authored DDL. They intentionally make an existing or absent object a no-op on replay; dumps, generator-owned files, unsupported syntax and other dialects remain unchanged."],"messageIds":[],"optionsSchema":null,"rationale":"Supported existence guards help replay DDL after partial application. A transactional migration runner such as dbmate owns version tracking and rollback; this rule does not require every statement to be independently replayable.","references":[],"remediation":"Use the native IF NOT EXISTS or IF EXISTS form where this rule supports it. Keep ADD CONSTRAINT as plain DDL in versioned transactional migrations; do not synthesize catalog-query DO blocks or catch duplicate-object errors solely for replay.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/idempotent_ddl.py","status":"active","summary":"DDL omits a supported IF [NOT] EXISTS guard.","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"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_body.sql","source":"DO $$ BEGIN CREATE INDEX orders_idx ON orders(id); END $$;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_body.sql","id":"executable-dollar-body","outcome":"reject","scenarioId":"dollar-quoted-sql","title":"Executable dollar-quoted migration SQL remains checked"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_data.sql","source":"SELECT $data$CREATE INDEX orders_idx ON orders(id);$data$;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_data.sql","id":"scalar-dollar-data","outcome":"accept","scenarioId":"dollar-quoted-sql","title":"SQL keywords inside scalar dollar data are not executed"}],"filePatterns":[],"id":"index-concurrently","key":"sql:index-concurrently","languages":["sql"],"limitations":["The same-file CREATE TABLE exemption preserves explicit schema differences and quoted identifier case.","Explicit pg_temp and pg_temp_N targets are exempt because temporary tables cannot block writes from other sessions.","Valid catalog qualifiers use schema/relation identity; the current database is not resolved.","Mixed qualified/unqualified references with the same relation name remain potentially fresh; search_path is not resolved. UTF-8 names use ASCII-only unquoted case folding; escaped Unicode identifiers and server-side name truncation are not resolved.","Conditional creation and transaction boundaries are not re-evaluated by the same-file exemption."],"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":["insert-requires-on-conflict"],"autofix":"none","category":"correctness","code":"SARJ105","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"queries/create_plan.sql","source":"INSERT INTO live.plan SELECT id FROM pending WHERE NOT EXISTS (SELECT 1 FROM archive.plan WHERE id = pending.id);\n"}],"fixedFiles":[],"focusPath":"queries/create_plan.sql","id":"different-explicit-schema-cannot-prove-replay","outcome":"reject","scenarioId":"explicit-schema-replay","title":"A replay guard for a different explicit schema is insufficient"},{"expectedCount":0,"files":[{"path":"queries/create_plan.sql","source":"INSERT INTO plan (name) VALUES ('free') ON CONFLICT (name) DO NOTHING;\n"}],"fixedFiles":[],"focusPath":"queries/create_plan.sql","id":"insert-with-replay-policy","outcome":"accept","scenarioId":"primary","title":"Upsert declares its conflict behavior"},{"expectedCount":1,"files":[{"path":"queries/create_plan.sql","source":"INSERT INTO plan (name) VALUES ('free');\n"}],"fixedFiles":[],"focusPath":"queries/create_plan.sql","id":"insert-without-replay-policy","outcome":"reject","scenarioId":"primary","title":"Insert without declared replay behavior"},{"expectedCount":0,"files":[{"path":"queries/create_plan.sql","source":"INSERT INTO live.plan SELECT id FROM pending WHERE NOT EXISTS (SELECT 1 FROM live.plan WHERE id = pending.id);\n"}],"fixedFiles":[],"focusPath":"queries/create_plan.sql","id":"same-explicit-schema-proves-replay","outcome":"accept","scenarioId":"explicit-schema-replay","title":"A replay guard for the exact qualified target is retained"}],"filePatterns":[],"id":"insert-requires-replay-policy","key":"sql:insert-requires-replay-policy","languages":["sql"],"limitations":["Developer-authored SQL files are checked regardless of path; database dumps are excluded.","Known PostgreSQL, MySQL, and SQLite upsert forms and a target-specific NOT EXISTS replay guard are recognized.","Target aliases and OVERRIDING clauses preserve detection. Each INSERT owns its conflict policy, including data-modifying CTEs; neighboring writes do not supply another write policy."],"messageIds":[],"optionsSchema":null,"rationale":"Retries happen in migrations, jobs, commands, and application requests. An INSERT with no declared conflict behavior can duplicate state or turn a safe retry into a uniqueness failure.","references":[],"remediation":"Prefer ON CONFLICT with an explicit DO NOTHING or DO UPDATE action. If duplicate failure is the intended contract outside migrations, add an exact SARJ105 suppression on the INSERT line explaining why. Migration SQL must declare an executable replay policy.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/insert_requires_on_conflict.py","status":"active","summary":"Every INSERT must declare replay-safe upsert behavior or explicitly document intentional insert-only semantics.","test":"packages/sql/tests/rules/test_insert_requires_on_conflict.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ125","defaultLevel":"warning","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"migrations/001_index.sql","source":"-- migrate:up transaction:false\nCREATE INDEX CONCURRENTLY account_email ON account(email);\n"}],"fixedFiles":[],"focusPath":"migrations/001_index.sql","id":"nontransactional-concurrent-index","outcome":"accept","scenarioId":"primary","title":"The runner explicitly executes the concurrent index outside a transaction"},{"expectedCount":1,"files":[{"path":"migrations/001_index.sql","source":"-- migrate:up\nCREATE INDEX CONCURRENTLY account_email ON account(email);\n"}],"fixedFiles":[],"focusPath":"migrations/001_index.sql","id":"transactional-concurrent-index","outcome":"reject","scenarioId":"primary","title":"A concurrent index build cannot execute in the runner's transaction"}],"filePatterns":[],"id":"invalid-concurrent-ddl","key":"sql:invalid-concurrent-ddl","languages":["sql"],"limitations":["Only authored PostgreSQL migrations are checked; tests, fixtures, dumps and recognized generator-owned migrations are excluded.","Transaction proof comes from explicit BEGIN/START TRANSACTION or a valid dbmate up/down directive. COMMIT/END/ROLLBACK/ABORT, AND [NO] CHAIN and section boundaries are tracked; ROLLBACK TO a savepoint keeps the transaction active. Unknown caller transactions and other migration runners are not inferred.","Only parseable CREATE INDEX CONCURRENTLY statements and complete DROP INDEX or REFRESH MATERIALIZED VIEW command forms are inspected. Dollar-quoted procedural and scalar bodies are ignored; SQL-standard BEGIN ATOMIC routine bodies cause file-level abstention. Dynamic SQL, REINDEX and external catalog restrictions are not inferred.","A parsed COPY FROM STDIN command ends analysis, preserving earlier findings while ignoring inline native input and all later commands.","One finding per command combines all demonstrated restrictions. No automatic transaction or DDL rewrite is safe."],"messageIds":[],"optionsSchema":null,"rationale":"PostgreSQL rejects concurrent index creation and removal inside a transaction block. Concurrent index removal also rejects multiple targets and CASCADE, and a concurrent materialized-view refresh cannot request WITH NO DATA.","references":[],"remediation":"Run concurrent index commands outside a transaction, using transaction:false for the relevant dbmate section. Drop one index per concurrent command without CASCADE. Choose either a concurrent populated refresh or a nonconcurrent WITH NO DATA refresh. Preserve deployment and atomicity requirements when changing transaction boundaries.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/invalid_concurrent_ddl.py","status":"active","summary":"Keep concurrent PostgreSQL DDL in supported execution contexts.","test":"packages/sql/tests/rules/test_invalid_concurrent_ddl.py"},{"aliases":[],"autofix":"safe","category":"correctness","code":"SARJ126","defaultLevel":"warning","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"migrations/001_index.sql","source":"CREATE INDEX item_key ON app.item(id);\n"}],"fixedFiles":[],"focusPath":"migrations/001_index.sql","id":"fixed-explicit-index-schema","outcome":"accept","scenarioId":"same-explicit-index-schema","title":"The table supplies the schema of its index"},{"expectedCount":1,"files":[{"path":"migrations/001_index.sql","source":"CREATE INDEX app.item_key ON app.item(id);\n"}],"fixedFiles":[{"path":"migrations/001_index.sql","source":"CREATE INDEX item_key ON app.item(id);\n"}],"focusPath":"migrations/001_index.sql","id":"same-explicit-index-schema","outcome":"reject","scenarioId":"same-explicit-index-schema","title":"The table already fixes the index schema"},{"expectedCount":0,"files":[{"path":"migrations/001_index.sql","source":"CREATE UNIQUE INDEX item_key ON app.item USING btree (id) INCLUDE (label);\n"}],"fixedFiles":[],"focusPath":"migrations/001_index.sql","id":"supported-unique-btree","outcome":"accept","scenarioId":"primary","title":"A B-tree index supports uniqueness and included columns"},{"expectedCount":1,"files":[{"path":"migrations/001_index.sql","source":"CREATE UNIQUE INDEX item_key ON item USING hash (id);\n"}],"fixedFiles":[],"focusPath":"migrations/001_index.sql","id":"unsupported-unique-hash","outcome":"reject","scenarioId":"primary","title":"A built-in hash index cannot enforce uniqueness"}],"filePatterns":[],"id":"invalid-postgres-ddl","key":"sql:invalid-postgres-ddl","languages":["sql"],"limitations":["Checks authored PostgreSQL SQL and the built-in PostgreSQL 18 access methods. Explicit other dialects, dumps, generated migrations, fixtures, and test sources are excluded.","Only parsed CREATE INDEX definitions and direct ALTER TABLE ADD UNIQUE/PRIMARY KEY actions are inspected. Unknown access methods, unsupported syntax, column-level constraints, and procedural or dynamically executed SQL remain outside this rule.","INCLUDE entries must be single identifiers; quoted column names containing punctuation remain valid. Templated entries remain outside identifier validation. Included columns do not contribute to the key-count check.","Analysis stops before a proven top-level COPY FROM STDIN command; payload framing and commands following its data terminator are not modeled.","Autofix only repairs one comment-free CREATE INDEX whose explicitly qualified index name and table use the same exact PostgreSQL schema identity. It removes that index schema prefix, preserves physical lines and every other source byte, and requires one supported parsed index with identical table, basename and complete index signature. Other DDL, mismatched or unqualified schemas, multiple statements and other index errors remain manual.","The repair corrects unsupported syntax from the explicit same-schema declaration; it does not claim runtime equivalence for the invalid preimage or infer table contents, installed operator classes, function volatility, referenced constraints, partition keys or deployment transaction boundaries."],"messageIds":[],"optionsSchema":null,"rationale":"Unsupported index options and constraint validation clauses fail when PostgreSQL executes a migration. These incompatibilities follow directly from the command and known built-in access-method capabilities.","references":[],"remediation":"Use B-tree for a unique index; use a supported multicolumn method for multiple keys; list only column names with a supported method in INCLUDE; qualify the indexed table instead of the new index name; and omit NOT VALID from UNIQUE or PRIMARY KEY additions. Preserve the intended uniqueness and query-access contract.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/invalid_postgres_ddl.py","status":"active","summary":"Use supported PostgreSQL index and constraint definitions.","test":"packages/sql/tests/rules/test_invalid_postgres_ddl.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ131","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"migrations/001.sql","source":"-- timeout-justification: lock_timeout -- restore the session default after the reviewed lock override\nRESET lock_timeout;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"justified-reset","outcome":"accept","scenarioId":"reset-timeout","title":"Cleanup records the setting it restores"},{"expectedCount":0,"files":[{"path":"migrations/001.sql","source":"-- timeout-justification: statement_timeout -- bounded backfill exceeds the session default\nSET statement_timeout = '10min';\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"justified-set","outcome":"accept","scenarioId":"set-timeout","title":"The reviewed backfill records why it needs a different deadline"},{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"RESET lock_timeout;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"unjustified-reset","outcome":"reject","scenarioId":"reset-timeout","title":"A reset also changes the session's timeout behavior"},{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"SET statement_timeout = '10min';\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"unjustified-set","outcome":"reject","scenarioId":"set-timeout","title":"A timeout header has no operational reason"}],"filePatterns":[],"id":"migration-timeout-requires-justification","key":"sql:migration-timeout-requires-justification","languages":["sql"],"limitations":["Checks direct top-level SET, SET LOCAL, SET SESSION and RESET of statement_timeout, lock_timeout, idle_in_transaction_session_timeout, idle_session_timeout, transaction_timeout and deadlock_timeout in authored PostgreSQL migrations, including forward and rollback sections.","Every named timeout control needs its own immediately preceding standalone directive with a matching setting and nonempty alphanumeric reason. Blank lines, mismatched settings, ordinary prose, block comments and generic sarj-noqa do not waive it. The adequacy of a reason requires review.","Explicit dump headers/names, generator-owned migrations, queries/scripts and test fixtures are excluded. Two SET-to-zero headers alone do not establish a dump for this rule.","Dollar/string-quoted bodies and dynamic set_config calls, ALTER ROLE/DATABASE/FUNCTION policy, RESET ALL, escaped Unicode setting identifiers and SQL-standard BEGIN ATOMIC bodies are not inferred. Standalone SET FROM CURRENT is outside the documented SET grammar and abstains. Files with BEGIN ATOMIC abstain. Analysis stops before COPY FROM STDIN.","This is an explicit source-policy contract, not proof that a timeout is unsafe. Existing migrations use normal reviewed baselines at adoption; no historical migration is rewritten. No automatic SQL fix is offered because deadlines, exceptions, cancellation and session state require operational review."],"messageIds":[],"optionsSchema":null,"rationale":"Routine SET/RESET timeout headers duplicate session policy and change migration failure behavior without recording why a migration needs a different deadline or lock wait.","references":[],"remediation":"Review and remove unnecessary timeout bookkeeping, or put a standalone `-- timeout-justification: <setting> -- <operational reason>` immediately before each justified statement. The setting must match; reviewers assess the reason. Do not remove a timeout merely because its reason is missing.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/migration_timeout_requires_justification.py","status":"active","summary":"Require an explicit operational reason for migration timeout settings.","test":"packages/sql/tests/rules/test_migration_timeout_requires_justification.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ119","defaultLevel":"warning","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"migrations/004_status.sql","source":"CREATE TABLE status (id UUID PRIMARY KEY, name TEXT NOT NULL);\nINSERT INTO status VALUES ('00000000-0000-0000-0000-000000000001', 'ready');\nCREATE INDEX status_name_idx ON status(name);\n"}],"fixedFiles":[],"focusPath":"migrations/004_status.sql","id":"fresh-table-bootstrap","outcome":"accept","scenarioId":"primary","title":"A fresh lookup table is created, seeded, and indexed atomically"},{"expectedCount":1,"files":[{"path":"migrations/004_credentials.sql","source":"ALTER TABLE credential ADD COLUMN token TEXT;\nUPDATE credential SET token = legacy_token;\nALTER TABLE credential DROP COLUMN legacy_token;\n"}],"fixedFiles":[],"focusPath":"migrations/004_credentials.sql","id":"rename-backfill-contract","outcome":"reject","scenarioId":"primary","title":"An existing-table backfill immediately removes its legacy column"}],"filePatterns":[],"id":"mixed-migration-phases","key":"sql:mixed-migration-phases","languages":["sql"],"limitations":["Only authored production migration forward sections are checked; generated migrations, dumps, tests, fixtures, schemas, and rollback sections are excluded.","Fresh-table seed data, indexes, and constraints are folded into the table's expand phase.","Dollar-quoted procedure and anonymous-block bodies and dynamically executed SQL are intentionally ignored.","The rule reports BACKFILL plus CONTRACT or three distinct phases; two-phase expand/backfill and backfill/enforce migrations remain reviewable without a finding.","Header comments cannot waive phase findings; operational review belongs in the change description."],"messageIds":[],"optionsSchema":null,"rationale":"Combining data movement with destructive or enforcing schema changes removes deployment checkpoints and makes lock duration, rollback, and compatibility harder to control.","references":[],"remediation":"Split expand, backfill, enforcement, and contract work into independently deployable migrations. Review atomicity, locking, runtime and rollback requirements in the change description.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/mixed_migration_phases.py","status":"active","summary":"Review existing-table migrations that combine backfill, enforcement, and contract phases.","test":"packages/sql/tests/rules/test_mixed_migration_phases.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ121","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"supabase/migrations/001_invite.sql","source":"CREATE TABLE invite (id UUID PRIMARY KEY, email TEXT, accepted_at TIMESTAMPTZ);\nCREATE UNIQUE INDEX invite_open_email ON invite (LOWER(email)) WHERE accepted_at IS NULL;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_invite.sql","id":"inline-constraints-and-partial-index","outcome":"accept","scenarioId":"primary","title":"Inline constraints and a separate partial expression index"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_invite.sql","source":"CREATE TABLE invite (id UUID, email TEXT);\nALTER TABLE invite ADD CONSTRAINT invite_pkey PRIMARY KEY (id);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_invite.sql","id":"separate-new-table-primary-key","outcome":"reject","scenarioId":"primary","title":"The new table's primary key belongs in its definition"}],"filePatterns":[],"id":"new-table-inline-constraints","key":"sql:new-table-inline-constraints","languages":["sql"],"limitations":["Only authored PostgreSQL migrations and unconditional empty CREATE TABLE definitions are checked. Conditional creation, dumps, generated migrations, fixtures, LIKE, inheritance, and partition definitions are excluded.","Unknown SQL, data statements, transaction or migration-section boundaries, and alterations other than inline constraint candidates revoke the empty-table proof. NOT VALID, NOT ENFORCED, and USING INDEX contracts remain separate.","Foreign keys remain separate when their referenced table or matching unique key is declared after the child definition in the same section; cyclic table dependencies are not reordered. Unknown key expressions retain conservative ordering checks.","Only plain unique indexes over original columns without guards, ordering, expressions, predicates, concurrency, or physical options are candidates for an inline UNIQUE constraint. Later named index attachment, alteration, or removal preserves the standalone index contract, including Down sections. No automatic rewrite changes catalog objects or migration behavior.","Relation matching preserves explicit schemas and quoted identifier case; search_path, catalog-qualified foreign-key targets, and unsupported unquoted identifier spellings are not inferred."],"messageIds":[],"optionsSchema":null,"rationale":"Splitting an empty table's primary key, uniqueness, foreign keys, and checks into later statements hides its complete integrity contract and adds avoidable ALTER TABLE operations.","references":[],"remediation":"Move these constraints into the original CREATE TABLE, preserving their names and options. A plain unique column index can use a named UNIQUE constraint. Keep ordinary, partial, expression, and concurrent indexes separate; PostgreSQL cannot express those as equivalent table constraints.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/new_table_inline_constraints.py","status":"active","summary":"Declare new-table constraints inside CREATE TABLE.","test":"packages/sql/tests/rules/test_new_table_inline_constraints.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ122","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"queries/events.sql","source":"-- dialect: clickhouse\nSELECT team_id, COUNT(*) FROM event GROUP BY team_id;\n"}],"fixedFiles":[],"focusPath":"queries/events.sql","id":"analytical-rollup","outcome":"accept","scenarioId":"primary","title":"ClickHouse owns a reporting rollup"},{"expectedCount":1,"files":[{"path":"queries/events.sql","source":"SELECT COUNT(*) FROM event WHERE id = $1;\n"}],"fixedFiles":[],"focusPath":"queries/events.sql","id":"operational-count","outcome":"reject","scenarioId":"operational-read","title":"A transactional count still violates the datastore policy"},{"expectedCount":0,"files":[{"path":"queries/events.sql","source":"SELECT EXISTS(SELECT 1 FROM event WHERE id = $1);\n"}],"fixedFiles":[],"focusPath":"queries/events.sql","id":"operational-existence","outcome":"accept","scenarioId":"operational-read","title":"A transactional existence check uses an indexed predicate"},{"expectedCount":1,"files":[{"path":"queries/events.sql","source":"CREATE TEMP TABLE report ON COMMIT DROP AS WITH rows AS (SELECT DISTINCT team_id FROM event) SELECT team_id FROM rows;\n"}],"fixedFiles":[],"focusPath":"queries/events.sql","id":"temporary-table-deduplication","outcome":"reject","scenarioId":"temporary-table","title":"A temporary table does not exempt a transactional reporting query"},{"expectedCount":0,"files":[{"path":"queries/events.sql","source":"CREATE TEMP TABLE report ON COMMIT DROP AS WITH rows AS (SELECT team_id FROM event WHERE id = $1) SELECT team_id FROM rows;\n"}],"fixedFiles":[],"focusPath":"queries/events.sql","id":"temporary-table-operational-read","outcome":"accept","scenarioId":"temporary-table","title":"A temporary table can materialize an operational row lookup"},{"expectedCount":1,"files":[{"path":"queries/events.sql","source":"SELECT DISTINCT team_id FROM event;\n"}],"fixedFiles":[],"focusPath":"queries/events.sql","id":"transactional-deduplication","outcome":"reject","scenarioId":"primary","title":"Transactional SQL deduplicates reporting rows"}],"filePatterns":[],"id":"no-analytical-queries-in-transactional-db","key":"sql:no-analytical-queries-in-transactional-db","languages":["sql"],"limitations":["Authored SQL defaults to PostgreSQL, matching this linter's migration contract; SQLite and MySQL dialect directives and database directories select their parsers.","SELECT DISTINCT (including DISTINCT ON), deduplicating UNION/INTERSECT/EXCEPT, every GROUP BY, and built-in SQL aggregates are blocking, including CTEs, nested writes, views, windowed aggregates, identity-bounded reads, and LIMIT queries. ALL set operations remain valid.","ClickHouse, BigQuery, DuckDB, Snowflake, Redshift, Trino, Athena, Spark, and Databricks dialect directives or database directories are analytical and excluded. An explicit dialect directive takes precedence over a directory.","One finding is emitted per offending SQL statement. Comments, quoted identifiers and values, scalar dollar-quoted SQL text, and IS DISTINCT FROM do not trigger the policy.","SQLite multi-argument MIN/MAX and MySQL ANY_VALUE retain their scalar semantics; PostgreSQL ANY_VALUE remains an aggregate.","PostgreSQL pg_catalog aggregates and lowercase quoted built-ins retain native identity. Unknown schemas and distinct quoted uppercase PostgreSQL function names abstain. Executing EXPLAIN ANALYZE is analyzed; plan-only EXPLAIN and unsupported PostgreSQL EXPLAIN options abstain.","Tests, generated files, and content-identified database dumps are excluded. Malformed statements, procedural bodies, dynamic grammar, ORM expressions, and user-defined aggregates are not inferred. No automatic datastore migration is safe."],"messageIds":[],"optionsSchema":null,"rationale":"Project architecture reserves SQL deduplication, grouping, and aggregates for ClickHouse or another analytical database, preventing reporting work from competing with transactional reads. This is an explicit datastore policy, not an execution-cost estimate.","references":[],"remediation":"Move reporting queries to ClickHouse or another analytical database with an explicit freshness contract. Use EXISTS or indexed row lookups for operational reads. Preserve atomicity; a reviewed transactional invariant needs an exact, explained SARJ122 exception.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_analytical_queries_in_transactional_db.py","status":"active","summary":"Keep DISTINCT, GROUP BY, and aggregate queries in analytical databases.","test":"packages/sql/tests/rules/test_no_analytical_queries_in_transactional_db.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ118","defaultLevel":"warning","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"supabase/migrations/001_execution_plan.sql","source":"CREATE TABLE execution_plan (\n    call_id UUID PRIMARY KEY REFERENCES call (id),\n    outcomes JSONB NOT NULL\n);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_execution_plan.sql","id":"application-owned-json-shape","outcome":"accept","scenarioId":"json-shape","title":"Application schema owns the JSON document contract"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_execution_plan.sql","source":"CREATE TABLE execution_plan (\n    outcomes JSONB NOT NULL CHECK (\n        JSONB_TYPEOF(outcomes) = 'array'\n        AND JSONB_ARRAY_LENGTH(outcomes) BETWEEN 1 AND 10\n    )\n);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_execution_plan.sql","id":"json-array-shape-check","outcome":"reject","scenarioId":"json-shape","title":"Database duplicates a JSON array schema"}],"filePatterns":[],"id":"no-application-schema-check","key":"sql:no-application-schema-check","languages":["sql"],"limitations":["Only PostgreSQL CHECK expressions that call JSON_TYPEOF, JSONB_TYPEOF, JSON_ARRAY_LENGTH, or JSONB_ARRAY_LENGTH are reported.","Closed status value sets and numeric/interval bounds are allowed as database integrity constraints.","JSON operators alone are not matched. Function calls inside cross-column or mixed checks can match; application ownership is not proven and removal requires review of every writer.","Dump files are excluded; generated migrations redirect findings to their owning model when identifiable."],"messageIds":[],"optionsSchema":null,"rationale":"A database CHECK that repeats an application-owned JSON schema creates two validators that can drift and turn an otherwise valid application deployment into failed writes.","references":[],"remediation":"If the application owns the contract, validate the JSON payload with the typed application boundary before changing its CHECK. Preserve database-owned validation and checks needed by other writers; use an exact SARJ118 suppression explaining ownership. Keep relational constraints such as foreign keys, uniqueness, nullability, and cross-column invariants in the database.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_application_schema_check.py","status":"active","summary":"Review JSON shape validation duplicated in database CHECK constraints.","test":"packages/sql/tests/rules/test_no_application_schema_check.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ120","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/001_function.sql","source":"DO $outer$ BEGIN CREATE FUNCTION app.f() RETURNS integer LANGUAGE sql AS $inner$SELECT 1$inner$; END $outer$;\n"}],"fixedFiles":[],"focusPath":"migrations/001_function.sql","id":"anonymous-block-function","outcome":"reject","scenarioId":"executable-sql","title":"An executable anonymous block still creates a stored function"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001.sql","source":"ALTER TABLE batch ADD CONSTRAINT fail_counter CHECK (false) NOT VALID;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001.sql","id":"declarative-fault","outcome":"accept","scenarioId":"primary","title":"Declarative constraint in a disposable test database"},{"expectedCount":0,"files":[{"path":"migrations/001_function.sql","source":"SELECT $outer$CREATE FUNCTION app.f() RETURNS integer LANGUAGE sql AS $inner$SELECT 1$inner$;$outer$;\n"}],"fixedFiles":[],"focusPath":"migrations/001_function.sql","id":"scalar-function-text","outcome":"accept","scenarioId":"executable-sql","title":"Function DDL stored as scalar data does not execute"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001.sql","source":"CREATE FUNCTION fail_counter() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN RAISE EXCEPTION 'failure'; END $$;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001.sql","id":"stored-fault-function","outcome":"reject","scenarioId":"primary","title":"Procedural database fault fixture"}],"filePatterns":[],"id":"no-database-functions","key":"sql:no-database-functions","languages":["sql"],"limitations":["Reports PostgreSQL CREATE [OR REPLACE] FUNCTION/PROCEDURE and ALTER FUNCTION/PROCEDURE/ROUTINE, including temporary-schema routines. Calls to built-in or existing functions and DROP FUNCTION/PROCEDURE/ROUTINE remain valid.","This is an organization-specific architecture policy. Comments, quoted values (including scalar and nested dollar-quoted strings), dumps, and non-PostgreSQL dialects are excluded. Executable DO and routine bodies remain visible.","Generated migrations report against their owning model when one can be identified. Triggers are covered separately by SARJ114.","Dynamically generated DDL inside procedural bodies is not inferred.","Analysis stops at COPY FROM STDIN; raw payloads and commands following their terminator are not modeled."],"messageIds":[],"optionsSchema":null,"rationale":"Under a single-writer application architecture, stored functions can obscure application behavior. Migration behavior must remain explicit in application transactions or declarative constraints.","references":[],"remediation":"Prefer declarative constraints or explicit transactional application behavior where equivalent. Preserve transaction atomicity and database-enforced integrity; migrations cannot waive this rule with an inline suppression.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_database_functions.py","status":"active","summary":"Ban stored SQL functions and procedures in migrations.","test":"packages/sql/tests/rules/test_no_database_functions.py"},{"aliases":["no-create-trigger"],"autofix":"none","category":"architecture","code":"SARJ114","defaultLevel":"error","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-database-triggers","key":"sql:no-database-triggers","languages":["sql"],"limitations":["Reports PostgreSQL CREATE [OR REPLACE] TRIGGER, CREATE CONSTRAINT TRIGGER, CREATE EVENT TRIGGER, all ALTER TRIGGER statements, ALTER EVENT TRIGGER ENABLE/RENAME/OWNER, and ALTER TABLE ENABLE TRIGGER. DROP TRIGGER/EVENT TRIGGER and ALTER TABLE/EVENT TRIGGER DISABLE remain valid cleanup.","This is an organization-specific single-writer architecture policy, not a claim that triggers are invalid.","Dump files and non-PostgreSQL dialects are excluded.","Scalar and nested dollar-quoted values are excluded; executable DO and routine bodies remain visible.","Generated migrations report against their owning model when one can be identified.","Analysis stops at COPY FROM STDIN; raw payloads and commands following their terminator are not modeled."],"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. Migrations must express integrity through declarative constraints and explicit application transactions.","references":[],"remediation":"Use a declarative database constraint where possible or explicit transactional application behavior; migrations cannot waive the trigger ban with inline suppressions.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_create_trigger.py","status":"active","summary":"Keep behavioral logic in application code, not database triggers.","test":"packages/sql/tests/rules/test_no_create_trigger.py"},{"aliases":[],"autofix":"none","category":"performance","code":"SARJ117","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"migrations/002_indexes.sql","source":"CREATE INDEX event_open ON event(owner_id) WHERE status = 'open';\nCREATE INDEX event_closed ON event(owner_id) WHERE status = 'closed';\n"}],"fixedFiles":[],"focusPath":"migrations/002_indexes.sql","id":"distinct-index-predicates","outcome":"accept","scenarioId":"primary","title":"Different partial-index predicates remain distinct"},{"expectedCount":1,"files":[{"path":"migrations/002_indexes.sql","source":"CREATE INDEX event_owner_a ON event(owner_id DESC);\nCREATE INDEX event_owner_b ON event ( owner_id DESC );\n"}],"fixedFiles":[],"focusPath":"migrations/002_indexes.sql","id":"duplicate-index-shape","outcome":"reject","scenarioId":"primary","title":"Two active indexes repeat the same normalized definition"}],"filePatterns":[],"id":"no-duplicate-index","key":"sql:no-duplicate-index","languages":["sql"],"limitations":["Only explicit indexes in one authored production migration are checked; tests, dumps, fixtures, recognized generator-owned migrations, and indexes that exist only outside the file are excluded.","DROP INDEX operations in the same migration remove the named definition from the final active set before repeated definitions are compared.","The normalized definition includes uniqueness, table and ONLY scope, access method, ordered key expressions and operator classes, INCLUDE columns, NULLS DISTINCT treatment, storage parameters, tablespace, and WHERE predicate.","Semantically equivalent expressions with different normalized source spelling are intentionally not inferred.","Left-prefix review is limited to ordinary non-unique B-tree indexes with identical physical options and compatible INCLUDE coverage.","Only exact normalized active duplicates block; unique/nonunique access-path comparisons and strict left-prefix advice retain WARNING because query-plan value is unresolved."],"messageIds":[],"optionsSchema":null,"rationale":"Duplicate definitions, non-unique copies of unique access paths, and strict B-tree prefixes can add write amplification, storage, vacuum work, and planner choices without a distinct measured read path.","references":[],"remediation":"Remove the repeated definition or verify the exact query plan before retaining a potentially covered index. Preserve an index when uniqueness, key order, operator class, collation, predicate, included columns, partition scope, access method, tablespace, or storage parameters give it distinct behavior.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_duplicate_index.py","status":"active","summary":"Report duplicate and conservatively covered indexes that remain active in one authored migration.","test":"packages/sql/tests/rules/test_no_duplicate_index.py"},{"aliases":["excessive-commentary"],"autofix":"safe","category":"maintainability","code":"SARJ115","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"-- Create the custom_integration table used by the application.\n-- The table stores each configured integration for an organization.\n-- The next statement defines its identifier and display name.\n-- The final statement creates those columns in the database.\nCREATE TABLE IF NOT EXISTS integration (id BIGINT PRIMARY KEY, name TEXT);\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"ddl-narration","outcome":"reject","scenarioId":"primary","title":"Migration comments narrate the table declarations"},{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"-- CREATE TABLE users (id BIGINT PRIMARY KEY, name TEXT);\n-- CREATE TABLE groups (id BIGINT PRIMARY KEY, name TEXT);\n-- CREATE TABLE teams (id BIGINT PRIMARY KEY, name TEXT);\n-- CREATE TABLE accounts (id BIGINT PRIMARY KEY, name TEXT);\n"}],"fixedFiles":[{"path":"migrations/001.sql","source":"\n\n\n\n"}],"focusPath":"migrations/001.sql","id":"disabled-sql-narration","outcome":"reject","scenarioId":"disabled-sql-narration","title":"A long run consists entirely of disabled SQL declarations"},{"expectedCount":0,"files":[{"path":"migrations/001.sql","source":"CREATE TABLE users (id BIGINT PRIMARY KEY, name TEXT);\nCREATE TABLE groups (id BIGINT PRIMARY KEY, name TEXT);\nCREATE TABLE teams (id BIGINT PRIMARY KEY, name TEXT);\nCREATE TABLE accounts (id BIGINT PRIMARY KEY, name TEXT);\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"executable-sql-without-narration","outcome":"accept","scenarioId":"disabled-sql-narration","title":"Executable declarations do not narrate disabled source"},{"expectedCount":0,"files":[{"path":"migrations/001.sql","source":"-- Backfill no more than 1000 rows per transaction.\n-- Keep lock_timeout at 3 seconds while API-812 writes continuously.\n-- Wait for replica lag to return below 500 ms before the next batch.\n-- Roll back by dropping the new index concurrently.\nSET lock_timeout = '3s';\nSELECT 1;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"rollback-constraint","outcome":"accept","scenarioId":"primary","title":"Concrete operational constraints remain local"}],"filePatterns":[],"id":"no-long-migration-narration","key":"sql:no-long-migration-narration","languages":["sql"],"limitations":["Safe fixes apply only to complete supported disabled SQL-only line-comment runs, reusing the comment-cruft boundary. Narrative prose and mixed operational context remain diagnostic-only.","Only authored, non-generated migrations are checked; schema dumps and non-migration SQL are excluded.","Only contiguous standalone or block-comment narration containing at least four lines and 28 words is reported.","Directives, lists, durable references, and concrete operational constraints are excluded line by line; broader semantic judgment remains in review.","The shared SQL scanner recognizes `--` and `/* ... */` comments; MySQL `#` comments are outside this rule."],"messageIds":[],"optionsSchema":null,"rationale":"Step-by-step prose duplicates nearby DDL and drifts as statements change; migrations should be readable from SQL plus concise operational constraints.","references":[],"remediation":"Delete implementation narration. Keep only concrete rollback, locking, data-loss, compatibility, security, or externally owned constraints that the DDL cannot express.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/excessive_commentary.py","status":"active","summary":"Long migration comments narrate implementation instead of recording durable constraints.","test":"packages/sql/tests/rules/test_excessive_commentary.py"},{"aliases":["no-comment-cruft"],"autofix":"safe","category":"maintainability","code":"SARJ113","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"-- DROP TABLE legacy_events;\nSELECT 1;\n"}],"fixedFiles":[{"path":"migrations/001.sql","source":"\nSELECT 1;\n"}],"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":"Owned rollback instruction records an operational constraint"}],"filePatterns":[],"id":"no-migration-comment-cruft","key":"sql:no-migration-comment-cruft","languages":["sql"],"limitations":["Safe fixes remove standalone line-comment banners and complete supported disabled SQL-only comment runs. Debt, references, directives, rollback guidance, mixed prose and block comments remain for review.","Only authored migration files are checked; dumps and generator-owned migrations are excluded.","The scanner distinguishes SQL comments from strings, identifiers, templates, and executable dollar-quoted bodies.","Commented-out SQL is reported only for a conservative complete-statement grammar; ambiguous prose and partial statements are preserved.","Migration-tool directives, explicit rollback instructions, and debt with a durable URL or issue identifier are preserved."],"messageIds":[],"optionsSchema":null,"rationale":"Disabled statements drift while version control preserves their history, decorative dividers obscure migration structure, and debt without a durable owner silently persists.","references":[],"remediation":"Execute or delete disabled SQL, delete decorative dividers, and resolve debt or attach a durable issue reference. Retain concise operational constraints, rollback instructions, and tool directives.","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":"safe","category":"maintainability","code":"SARJ128","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"CREATE TABLE IF NOT EXISTS settings (retries INTEGER); -- retries must be positive\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"comment-only-range","outcome":"reject","scenarioId":"primary","title":"A comment cannot enforce a column range"},{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"SELECT 1/**/AS value;\n"}],"fixedFiles":[{"path":"migrations/001.sql","source":"SELECT 1    AS value;\n"}],"focusPath":"migrations/001.sql","id":"empty-comment","outcome":"reject","scenarioId":"empty-comment","title":"Remove an empty comment while keeping SQL tokens separate"},{"expectedCount":0,"files":[{"path":"migrations/001.sql","source":"SELECT 1    AS value;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"empty-comment-fixed","outcome":"accept","scenarioId":"empty-comment","title":"Whitespace preserves the executable SQL after the comment is removed"},{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"SELECT 'payload' AS label, 1/**/AS value;\n"}],"fixedFiles":[{"path":"migrations/001.sql","source":"SELECT 'payload' AS label, 1    AS value;\n"}],"focusPath":"migrations/001.sql","id":"empty-comment-with-string","outcome":"reject","scenarioId":"empty-comment-with-string","title":"Remove an empty comment beside string data"},{"expectedCount":0,"files":[{"path":"migrations/001.sql","source":"SELECT 'payload' AS label, 1    AS value;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"empty-comment-with-string-fixed","outcome":"accept","scenarioId":"empty-comment-with-string","title":"String data and SQL tokens remain unchanged"},{"expectedCount":0,"files":[{"path":"migrations/001.sql","source":"-- migrate:up\nCREATE TABLE IF NOT EXISTS settings (retries INTEGER CHECK (retries > 0));\n-- migrate:down\nDROP TABLE IF EXISTS settings;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"enforced-range","outcome":"accept","scenarioId":"primary","title":"SQL enforces the bound while runner directives remain executable"},{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"-- DROP TABLE legacy;\nSELECT 1;\n"}],"fixedFiles":[{"path":"migrations/001.sql","source":"\nSELECT 1;\n"}],"focusPath":"migrations/001.sql","id":"inert-disabled-statement","outcome":"reject","scenarioId":"inert-comment","title":"Remove a proven disabled statement without changing executable SQL"},{"expectedCount":0,"files":[{"path":"migrations/001.sql","source":"\nSELECT 1;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"inert-disabled-statement-fixed","outcome":"accept","scenarioId":"inert-comment","title":"Executable SQL remains after removing the inert comment"}],"filePatterns":[],"id":"no-migration-comments","key":"sql:no-migration-comments","languages":["sql"],"limitations":["Checks authored and generated migrations; schema dumps and non-migration SQL are excluded.","Exact standalone dbmate, Goose, sql-migrate, Liquibase, Atlas, dialect and Drizzle breakpoint directives are preserved.","PostgreSQL line and nested block comments are recognized; MySQL # comments are outside the shared scanner.","Strings and quoted identifiers are not comments, including scalar dollar-quoted data. Comments in executable bodies are checked.","COMMENT ON cleanup using unquoted NULL or an empty plain, escape, or dollar-quoted string is allowed; quoted NULL and nonempty strings still store comments.","Analysis stops before top-level COPY FROM STDIN; payloads and following commands are not inferred.","A standalone timeout-justification directive with a nonempty reason is preserved only immediately before its matching top-level timeout SET/RESET. Orphaned, mismatched and malformed directives remain comments.","Conditional safe fixes replace empty PostgreSQL comments with whitespace that preserves token boundaries and physical newlines. They also reuse the existing banner and complete supported disabled-SQL line-comment profiles only when both rules report the same location. Arbitrary prose, declared ranges, COMMENT ON statements, directives, references, rollback guidance, suppression and license comments remain manual.","Empty block comments between adjacent string literals remain manual to preserve PostgreSQL string-continuation boundaries. Empty comments elsewhere remain eligible with quoted data.","Safe fixes preserve physical newlines and refuse COPY input, dollar-quoted data and procedural sources. Direct source-string fixes refuse a visible BOM prefix; file fixes preserve the original UTF-8 BOM through the shared file adapter. Generated migrations remain model-owned. No constraint is invented from prose."],"messageIds":[],"optionsSchema":null,"rationale":"Comments that describe bounds or allowed states leave those invariants unenforced, while SQL CHECK, NOT NULL, UNIQUE and foreign-key constraints reject invalid writes from every caller.","references":[],"remediation":"Express data invariants with SQL constraints and delete explanatory line/block comments and COMMENT ON statements that store prose. COMMENT ON with NULL or an empty string may remove existing comments. Put deployment context in the change description. Retain only executable runner, dialect, statement-boundary directives, and scoped timeout-justification directives bound to a matching timeout statement. Migration sarj-noqa comments are also reported.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_migration_comments.py","status":"active","summary":"Encode migration invariants in SQL instead of explanatory comments.","test":"packages/sql/tests/rules/test_no_migration_comments.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ127","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"migrations/001.sql","source":"-- migrate:up\nCREATE TABLE IF NOT EXISTS work (id UUID PRIMARY KEY);\n-- migrate:down\nDROP TABLE IF EXISTS work;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"inverse-schema-change","outcome":"accept","scenarioId":"primary","title":"The rollback reverses the schema change"},{"expectedCount":1,"files":[{"path":"migrations/001.sql","source":"-- migrate:up\nCREATE TABLE IF NOT EXISTS work (id UUID PRIMARY KEY);\n-- migrate:down\nSELECT 1;\n"}],"fixedFiles":[],"focusPath":"migrations/001.sql","id":"literal-only-rollback","outcome":"reject","scenarioId":"primary","title":"A successful SELECT leaves the created table installed"}],"filePatterns":[],"id":"no-noop-migration-down","key":"sql:no-noop-migration-down","languages":["sql"],"limitations":["Checks explicit dbmate, Goose and sql-migrate down sections and conventional .down.sql files; forward-only migration formats are not required to add down sections.","Reports only empty bodies, literal-only SELECT statements, session settings, transaction delimiters and empty PL/pgSQL DO blocks, or combinations of these.","Calls, queries reading relations, dynamic SQL, malformed SQL and nonempty procedural blocks are preserved; inverse correctness is not inferred.","SQL strings and comments cannot introduce a down section. Schema dumps and non-migration SQL are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"An empty or SELECT 1 rollback can be recorded as successfully reverted while its schema and data changes remain installed, leaving the migration ledger inconsistent with the database.","references":[],"remediation":"Implement the inverse schema/data change using guarded DDL. If reversal cannot be supported, fail explicitly with an exception instead of recording a successful no-op rollback.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_noop_migration_down.py","status":"active","summary":"A migration down section must perform a rollback instead of silently doing nothing.","test":"packages/sql/tests/rules/test_no_noop_migration_down.py"},{"aliases":[],"autofix":"none","category":"correctness","code":"SARJ124","defaultLevel":"warning","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"queries/events.sql","source":"SELECT id FROM event WHERE archived_at IS NULL AND state IS DISTINCT FROM $1;\n"}],"fixedFiles":[],"focusPath":"queries/events.sql","id":"explicit-null-predicates","outcome":"accept","scenarioId":"primary","title":"Use predicates with explicit null semantics"},{"expectedCount":1,"files":[{"path":"queries/events.sql","source":"SELECT id FROM event WHERE archived_at = NULL;\n"}],"fixedFiles":[],"focusPath":"queries/events.sql","id":"ordinary-null-equality","outcome":"reject","scenarioId":"primary","title":"Ordinary equality does not test for missing values"}],"filePatterns":[],"id":"no-null-comparison","key":"sql:no-null-comparison","languages":["sql"],"limitations":["Checks parsed authored PostgreSQL SQL, including queries and migrations. Dumps, generated files, tests, fixtures, explicit other dialects, unsupported statements and malformed SQL are excluded.","Analysis stops before a proven COPY FROM STDIN command; inline input and following commands are not analyzed.","Only bare NULL and parenthesized NULL operands are checked. Casts, computed null values, subquery nullability and row-valued NOT IN lists are not inferred.","The rule recommends explicit predicates even when transform_null_equals is enabled. Catalog-defined non-strict comparison operators and intentional three-valued boolean results require an exact suppression; no automatic rewrite guesses their contract."],"messageIds":[],"optionsSchema":null,"rationale":"Ordinary built-in comparisons with NULL produce unknown, so a WHERE or JOIN predicate does not match rows. A scalar NOT IN list containing NULL can never be true. These expressions often silently discard the rows the query intended to retain.","references":[],"remediation":"Use IS NULL or IS NOT NULL to test for missing values, and IS [NOT] DISTINCT FROM for null-safe equality. Remove a literal NULL from a NOT IN list and express the intended null handling separately.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_null_comparison.py","status":"active","summary":"Use explicit NULL predicates instead of ordinary NULL comparisons.","test":"packages/sql/tests/rules/test_no_null_comparison.py"},{"aliases":["no-limit-offset"],"autofix":"none","category":"performance","code":"SARJ107","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":0,"files":[{"path":"queries/events.sql","source":"SELECT created_at, id FROM event\nWHERE (created_at, id) < ($1, $2)\nORDER BY created_at DESC, id DESC LIMIT $3;\n"}],"fixedFiles":[],"focusPath":"queries/events.sql","id":"cursor-pagination","outcome":"accept","scenarioId":"primary","title":"Pagination bounded by a stable cursor"},{"expectedCount":1,"files":[{"path":"queries/events.sql","source":"SELECT created_at, id FROM event ORDER BY created_at DESC, id DESC LIMIT $1 OFFSET $2;\n"}],"fixedFiles":[],"focusPath":"queries/events.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":["Only dynamic driver markers in credible SELECT statements are reported; fixed ordinal offsets are preserved.","PostgreSQL, SQLite, and common Python driver markers are recognized, including a postfix cast inside a parenthesized dynamic offset and MySQL LIMIT offset,count syntax.","Parenthesized numeric arithmetic and common numeric wrappers preserve dynamic-offset detection. Casts on later arithmetic operands, quoted/multiword type names and arbitrary function bodies remain unresolved.","Dumps, migrations, generated paths or marked files, tests, fixtures, snapshots, ORM calls, bare procedural variables, and ambiguous templates are excluded."],"messageIds":[],"optionsSchema":null,"rationale":"A parameterized offset grows with the requested page, forcing the database to scan and discard earlier rows. Concurrent inserts or deletes can also shift page boundaries, repeating or skipping rows.","references":[],"remediation":"Use keyset pagination over a stable, immutable ordering. Match the cursor predicate to the ORDER BY direction and add a unique tie-breaker. Keep dynamic OFFSET only for an intentional random-access contract or demonstrably bounded/static data, with an exact SARJ107 suppression.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_offset_pagination.py","status":"active","summary":"Dynamic OFFSET pagination in query SQL should use a stable 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_body.sql","source":"DO $$ BEGIN CREATE TYPE status AS ENUM ('ready'); END $$;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_body.sql","id":"executable-dollar-body","outcome":"reject","scenarioId":"dollar-quoted-sql","title":"Executable dollar-quoted migration SQL remains checked"},{"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_data.sql","source":"SELECT $data$CREATE TYPE status AS ENUM ('ready');$data$;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_data.sql","id":"scalar-dollar-data","outcome":"accept","scenarioId":"dollar-quoted-sql","title":"SQL keywords inside scalar dollar data are not executed"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_status.sql","source":"CREATE TABLE call (status TEXT NOT NULL);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_status.sql","id":"text-with-application-enum","outcome":"accept","scenarioId":"primary","title":"Text column whose closed values are application-owned"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_boundary.sql","source":"BEGIN;\nALTER TYPE call_status ADD VALUE 'archived';\nCOMMIT;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_boundary.sql","id":"transactional-enum-evolution","outcome":"reject","scenarioId":"semantic-boundary","title":"Transactional enum evolution still couples application values to database migrations"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_boundary.sql","source":"BEGIN;\nALTER TABLE call ADD COLUMN status TEXT;\nCOMMIT;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_boundary.sql","id":"transactional-text-column","outcome":"accept","scenarioId":"semantic-boundary","title":"A transaction adds an application-owned text value column"}],"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 validate its allowed values at the typed application boundary.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/no_pg_enum.py","status":"active","summary":"CREATE TYPE ... AS ENUM — use TEXT with an application enum instead.","test":"packages/sql/tests/rules/test_no_pg_enum.py"},{"aliases":[],"autofix":"none","category":"maintainability","code":"SARJ129","defaultLevel":"warning","engine":"sql","examples":[{"expectedCount":1,"files":[{"path":"supabase/migrations/001_job.sql","source":"CREATE TABLE job (requested BOOLEAN NOT NULL, confirmed BOOLEAN NOT NULL, CHECK (NOT confirmed OR requested));\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_job.sql","id":"dependent-lifecycle-flags","outcome":"reject","scenarioId":"primary","title":"A CHECK makes two required flags dependent"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_job.sql","source":"CREATE TABLE job (status TEXT NOT NULL CHECK (status IN ('new', 'requested', 'confirmed', 'archived')));\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_job.sql","id":"explicit-joint-status-domain","outcome":"accept","scenarioId":"joint-lifecycle","title":"A reviewed status preserves all four outcomes allowed by the implication chain"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_job.sql","source":"CREATE TABLE job (status TEXT NOT NULL CHECK (status IN ('new', 'requested', 'confirmed')), muted BOOLEAN NOT NULL);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_job.sql","id":"explicit-status-preserves-independent-flag","outcome":"accept","scenarioId":"independent-axis","title":"A reviewed status models three outcomes while muted remains independent"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_job.sql","source":"CREATE TABLE job (requested BOOLEAN NOT NULL, confirmed BOOLEAN NOT NULL, muted BOOLEAN NOT NULL, CHECK ((NOT confirmed OR requested) AND (muted OR NOT muted)));\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_job.sql","id":"independent-axis-is-preserved","outcome":"reject","scenarioId":"independent-axis","title":"A tautological flag does not join the dependent lifecycle factor"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_job.sql","source":"CREATE TABLE job (enabled BOOLEAN NOT NULL, archived BOOLEAN NOT NULL);\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_job.sql","id":"independent-flags","outcome":"accept","scenarioId":"primary","title":"Unrelated flags retain their independent values"},{"expectedCount":1,"files":[{"path":"supabase/migrations/001_job.sql","source":"CREATE TABLE job (requested BOOLEAN NOT NULL, confirmed BOOLEAN NOT NULL, archived BOOLEAN NOT NULL, CHECK (NOT confirmed OR requested), CHECK (NOT archived OR confirmed));\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_job.sql","id":"joint-lifecycle-constraints","outcome":"reject","scenarioId":"joint-lifecycle","title":"Multiple CHECKs jointly restrict three lifecycle observations"}],"filePatterns":[],"id":"prefer-constrained-status","key":"sql:prefer-constrained-status","languages":["sql"],"limitations":["Advisory only: SQL proves dependence, not that flags are semantically one lifecycle. Independent boolean names alone never produce findings.","Checks only authored PostgreSQL migration CREATE TABLE definitions with two to six NOT NULL BOOLEAN columns in each connected CHECK group. Nullable flags, unknown expressions, functions and generated columns exclude their group; duplicate columns, empty delimited identifiers, dumps and generated migrations are excluded.","Truth tables conjoin every CHECK sharing a participating column, covering AND, OR, NOT, boolean equality/inequality and IS TRUE/FALSE. Constant false CHECKs and opaque column-free CHECKs exclude the table. Tautologies, contradictions and restrictions equivalent to independent fields are excluded.","Only irreducible dependent factors are reported. Independent flags and separate dependent groups remain separate even when a tautological expression connects them syntactically. Every accepted combination must survive a reviewed migration; the analysis does not prove a lifecycle or whole-table reachability.","Tables exceeding 32 CHECKs or 512 CHECK expression nodes abstain. Connected groups exceeding six referenced columns abstain. No automatic schema rewrite is offered; DML, later ALTER statements, defaults, serialized interfaces and transition ordering require review."],"messageIds":[],"optionsSchema":null,"rationale":"A CHECK that rules out combinations of multiple required booleans exposes a shared state contract. A constrained status column can represent the permitted states explicitly and avoid invalid flag combinations.","references":[],"remediation":"Review whether these dependent booleans represent one lifecycle. Retain an authoritative Boolean when one bit determines the rest; use one constrained status for a multiway lifecycle. Preserve every valid combination, independent field, external contract and application transition.","since":null,"source":"packages/sql/src/sarj_sql_lint/rules/prefer_constrained_status.py","status":"active","summary":"Review CHECK-constrained boolean states for a single status column.","test":"packages/sql/tests/rules/test_prefer_constrained_status.py"},{"aliases":[],"autofix":"safe","category":"performance","code":"SARJ106","defaultLevel":"error","engine":"sql","examples":[{"expectedCount":2,"files":[{"path":"supabase/migrations/001_boundary.sql","source":"ALTER TABLE document ALTER COLUMN metadata TYPE json USING metadata::json;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_boundary.sql","id":"alter-json-type-with-using","outcome":"reject","scenarioId":"semantic-boundary","title":"An ALTER USING conversion still declares a plain JSON type"},{"expectedCount":0,"files":[{"path":"supabase/migrations/001_boundary.sql","source":"ALTER TABLE document ALTER COLUMN metadata TYPE jsonb USING metadata::jsonb;\n"}],"fixedFiles":[],"focusPath":"supabase/migrations/001_boundary.sql","id":"alter-jsonb-type-with-using","outcome":"accept","scenarioId":"semantic-boundary","title":"The target type and conversion both use JSONB"},{"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":["Automatic literal fixes require one literal-only SELECT or one simple CREATE TABLE with canonical typed literal defaults, with no other statements, reads, CTEs, operators, other constraints, custom casts or callouts. The external PostgreSQL built-in catalog must be canonical and unmodified; source-visible mutations are refused.","Autofix only corrects a canonical pg_catalog.json cast of the exact ordinary ASCII literal '{}' or '[]' inside an existing direct pg_catalog.jsonb cast and a canonical JSONB column DEFAULT. The outer JSONB boundary already owns normalization; schema types, other JSON documents, data expressions and missing boundaries remain manual.","PostgreSQL dump files are excluded.","JSON tokens inside comments, string literals, and longer identifiers are ignored.","Column declarations include optional COLUMN keywords, JSON arrays, and exact quoted or pg_catalog-qualified built-ins. Case-sensitive quoted custom types and other schemas remain excluded.","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":"safe","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":"queries/name.sql","source":"SELECT app.varchar(255);\n"}],"fixedFiles":[],"focusPath":"queries/name.sql","id":"qualified-function-varchar","outcome":"accept","scenarioId":"type-name-ownership","title":"A schema-qualified function keeps its own contract"},{"expectedCount":1,"files":[{"path":"migrations/001_users.sql","source":"CREATE TABLE users (name \"varchar\"(255));\n"}],"fixedFiles":[],"focusPath":"migrations/001_users.sql","id":"quoted-builtin-varchar","outcome":"reject","scenarioId":"type-name-ownership","title":"A quoted builtin still declares a bounded 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":["Automatic literal fixes require one literal-only SELECT or one simple CREATE TABLE with canonical typed literal defaults, with no other statements, reads, CTEs, operators, other constraints, custom casts or callouts. The external PostgreSQL built-in catalog must be canonical and unmodified; source-visible mutations are refused.","Autofix only replaces an exact short ordinary ASCII literal's canonical pg_catalog.varchar(n) cast when an existing direct pg_catalog.text cast already owns its result. Positive valid bounds must preserve every input character; columns, overlength strings, custom types, escaped literals and absent outer text boundaries remain manual.","MySQL and SQLite sources are excluded because their VARCHAR behavior differs.","Complete parseable PostgreSQL statements distinguish bounded builtin types from function names and custom schema types, including exact lower-case quoted varchar and pg_catalog qualification. Fragments and unsupported procedural syntax retain unquoted lexical detection."],"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) ON DELETE CASCADE);\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) ON DELETE CASCADE);\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":["Only CASCADE, SET NULL, and SET DEFAULT actions are checked; ordinary NO ACTION/RESTRICT foreign keys stay silent.","Explicit schemas and quoted identifier case remain distinct; mixed qualification is unresolved and may refer to the same relation.","Literal FALSE partial indexes cannot cover foreign keys. Other partial predicates retain conservative legacy coverage; arbitrary implication and migration-time index drops are not evaluated.","Unicode escape identifiers (U&) and server-side identifier truncation are not resolved.","Sibling discovery classifies at most 12,000 directory entries, with one lookahead to detect exhaustion, and parses at most 600 SQL candidates.","Complete discovery selects the lexicographically first 600 SQL candidates in the migration tree.","Unavailable or budget-excluded sibling evidence, including exhausted directory discovery, prevents a missing-index proof; known indexes remain usable.","Schema dumps are excluded; add the owning change to an authored migration."],"messageIds":[],"optionsSchema":null,"rationale":"PostgreSQL does not automatically index referencing columns, so cascading deletes/updates and SET NULL/DEFAULT actions may scan and lock 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":"Cascading or set-value FOREIGN KEY action lacks a child-table index.","test":"packages/sql/tests/rules/test_require_fk_index.py"},{"aliases":[],"autofix":"safe","category":"correctness","code":"SARJ315","defaultLevel":"error","engine":"text","examples":[{"expectedCount":0,"files":[{"path":"build.yaml","source":"substitutions: {_VALUE: fixture-data}\nsteps:\n- name: builder\n  entrypoint: bash\n  args: &args [scripts/check.sh, \"$_VALUE\"]\n- name: builder\n  entrypoint: bash\n  args: *args\n"}],"fixedFiles":[],"focusPath":"build.yaml","id":"alias-external-script-arguments","outcome":"accept","scenarioId":"cloudbuild-alias-location","title":"Aliased external-script arguments retain substitution as argv data"},{"expectedCount":2,"files":[{"path":"build.yaml","source":"substitutions: {_VALUE: fixture-data}\nsteps:\n- name: builder\n  entrypoint: bash\n  args: &args [-c, \"printf %s $_VALUE\"]\n- name: builder\n  entrypoint: bash\n  args: *args\n"}],"fixedFiles":[],"focusPath":"build.yaml","id":"alias-shell-source-substitution","outcome":"reject","scenarioId":"cloudbuild-alias-location","title":"An aliased shell program reports substitution at each executable use"},{"expectedCount":1,"files":[{"path":"build.yaml","source":"steps:\n- name: builder\n  waitFor: [later]\n- name: builder\n  id: later\n"}],"fixedFiles":[],"focusPath":"build.yaml","id":"forward-dependency","outcome":"reject","scenarioId":"primary","title":"A build cannot depend on a later step"},{"expectedCount":0,"files":[{"path":"build.yaml","source":"steps:\n- name: builder\n  id: first\n- name: builder\n  waitFor: [first]\n"}],"fixedFiles":[],"focusPath":"build.yaml","id":"prior-dependency","outcome":"accept","scenarioId":"primary","title":"Dependencies precede their consumers"}],"filePatterns":["**/*.yaml","**/*.yml","**/*.json"],"id":"cloudbuild-contract","key":"text:cloudbuild-contract","languages":["config"],"limitations":["In YAML or strict JSON, auto-fixing only deduplicates repeated exact '-' start sentinels in direct flow-style waitFor lists. All other parsed values and native findings must remain unchanged; aliases, anchors, tags, merges, comments, directives and escaped sentinels refuse the repair. JSON must pass strict unique-key parsing before and after the edit.","This checks step dependencies and execution semantics, not a complete Cloud Build API schema.","Only YAML or JSON documents with a Build-shaped steps sequence are selected; other configuration shapes are not guessed.","Unknown execution entrypoints, substitutions supplied by triggers, and effective service identities require prepared-output validation.","YAML aliases retain executable use-site locations, including inherited mappings and aliased argument lists; shared anchor data is not treated as an execution site.","Malformed or duplicate-key configuration raises a parser error instead of a suppressible rule finding; JSON also rejects non-finite numbers and YAML-only syntax."],"messageIds":[],"optionsSchema":null,"rationale":"Forward or duplicate dependencies invalidate a build; substitution into shell source changes executable syntax.","references":["https://docs.cloud.google.com/build/docs/configuring-builds/configure-build-step-order","https://docs.cloud.google.com/build/docs/build-config-file-schema"],"remediation":"Order uniquely identified steps before their consumers and pass substituted values through env or direct argv.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/text_rules/cloudbuild_contract.py","status":"active","summary":"Cloud Build dependencies and execution fields must have valid, safe semantics.","test":"packages/standards/tests/text_rules/test_cloudbuild_contract.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ301","defaultLevel":"error","engine":"text","examples":[{"expectedCount":1,"files":[{"path":"config.toml","source":"# timeout = 30\ntimeout = 10\n"}],"fixedFiles":[{"path":"config.toml","source":"\ntimeout = 10\n"}],"focusPath":"config.toml","id":"disabled-config-entry","outcome":"reject","scenarioId":"primary","title":"An unexplained alternative repeats the adjacent active key"},{"expectedCount":0,"files":[{"path":"config.toml","source":"# Optional local override:\n# timeout = 30\ntimeout = 10\n"}],"fixedFiles":[],"focusPath":"config.toml","id":"documented-default","outcome":"accept","scenarioId":"primary","title":"A labeled optional override remains documentation"}],"filePatterns":["**/*.{yaml,yml,toml,jsonc}"],"id":"commented-out-config","key":"text:commented-out-config","languages":["config"],"limitations":["Only YAML, TOML, and JSONC are analyzed; other configuration dialects need dedicated syntax support.","Requires two same-indent entries or one entry repeating an immediately adjacent active key.","Explanatory comment blocks, references, directives, and multiline string payloads are preserved.","YAML detection covers scalar mapping entries, not sequence entries or collection-valued mappings."],"messageIds":[],"optionsSchema":null,"rationale":"Disabled configuration becomes stale while version control already preserves its history.","references":[],"remediation":"Delete inactive settings, or explain the supported default, optional override, or constraint.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"unexplained disabled config blocks or adjacent alternatives","test":"packages/standards/tests/test_textlint.py"},{"aliases":[],"autofix":"safe","category":"maintainability","code":"SARJ300","defaultLevel":"error","engine":"text","examples":[{"expectedCount":0,"files":[{"path":"workflow.yml","source":"# Keep one worker because uploads race.\nconcurrency: 1\n# Retain three retries because the upstream API is eventually consistent.\nretries: 3\n# Set build name\nname: build\n# Set build image\nimage: app\n"}],"fixedFiles":[],"focusPath":"workflow.yml","id":"config-rationale","outcome":"accept","scenarioId":"primary","title":"Rationales keep a mixed comment cluster useful"},{"expectedCount":1,"files":[{"path":"config.yaml","source":"payload: |\n  # sarj-noqa: SARJ300\n# 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":"config.yaml","id":"literal-marker-retains-comment-wall","outcome":"reject","scenarioId":"comment-wall-suppression-provenance","title":"A scalar payload marker cannot waive a narrated comment wall"},{"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":[{"path":"workflow.yml","source":"\nname: build\n# Run build command\nrun: make build\n# Set deploy image\nimage: app\n# Run deploy command\ncommand: deploy\n"}],"focusPath":"workflow.yml","id":"narrated-config-wall","outcome":"reject","scenarioId":"primary","title":"Repeated comments restate adjacent entries"},{"expectedCount":0,"files":[{"path":"config.yaml","source":"# sarj-noqa: SARJ300\n# 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":"config.yaml","id":"real-marker-waives-comment-wall","outcome":"accept","scenarioId":"comment-wall-suppression-provenance","title":"A real exact-code comment retains its local wall waiver"}],"filePatterns":["**/*.{yaml,yml,toml,jsonc}"],"id":"config-comment-wall","key":"text:config-comment-wall","languages":["config"],"limitations":["Only valid YAML, TOML, and JSONC are analyzed; literal payloads and other configuration dialects are excluded.","Files with a leading generated-ownership marker and an explicit do-not-edit instruction are excluded.","Only clusters of at least four attached same-indent comments within one configuration owner are compared.","Clusters stop at blank lines, YAML document markers, TOML table headers, or dedentation out of the current owner.","Substantive action verbs must also appear in the adjacent entry; set, define, and configure are treated as structural narration."],"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 or more nearby configuration comments mostly repeat their entries","test":"packages/standards/tests/test_textlint.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ309","defaultLevel":"warning","engine":"text","examples":[{"expectedCount":0,"files":[{"path":".github/workflows/deploy.yml","source":"jobs:\n  deploy:\n    steps:\n      - run: gcloud run deploy api --image $IMAGE --region us\n"}],"fixedFiles":[],"focusPath":".github/workflows/deploy.yml","id":"cloud-run-application-publish","outcome":"accept","scenarioId":"cloud-run-release","title":"CI publishes a Cloud Run application artifact"},{"expectedCount":1,"files":[{"path":".github/workflows/deploy.yml","source":"jobs:\n  deploy:\n    steps:\n      - run: gcloud run services update api --memory 1Gi --region us\n"}],"fixedFiles":[],"focusPath":".github/workflows/deploy.yml","id":"cloud-run-infrastructure-update","outcome":"reject","scenarioId":"cloud-run-release","title":"A Cloud Run configuration update owns infrastructure"},{"expectedCount":0,"files":[{"path":".github/workflows/deploy.yml","source":"jobs:\n  deploy:\n    steps:\n      - run: terraform apply saved.tfplan\n"}],"fixedFiles":[],"focusPath":".github/workflows/deploy.yml","id":"saved-plan-apply","outcome":"accept","scenarioId":"primary","title":"CI applies the reviewed Terraform plan"},{"expectedCount":1,"files":[{"path":"main.tf","source":"resource \"terraform_data\" \"enable_api\" {\n  provisioner \"local-exec\" {\n    command = \"gcloud services enable example.googleapis.com\"\n  }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"terraform-local-exec-control-plane","outcome":"reject","scenarioId":"terraform-local-exec","title":"Provider resources own API enablement"},{"expectedCount":0,"files":[{"path":"main.tf","source":"resource \"terraform_data\" \"inspect_api\" {\n  provisioner \"local-exec\" {\n    command = \"gcloud services list --enabled\"\n  }\n}\n"}],"fixedFiles":[],"focusPath":"main.tf","id":"terraform-local-exec-read-only","outcome":"accept","scenarioId":"terraform-local-exec","title":"Read-only local commands do not create drift"},{"expectedCount":0,"files":[{"path":".github/workflows/deploy.yml","source":"jobs:\n  deploy:\n    steps:\n      - run: pnpm exec wrangler deploy --tag $GITHUB_SHA\n"}],"fixedFiles":[],"focusPath":".github/workflows/deploy.yml","id":"worker-application-publish","outcome":"accept","scenarioId":"worker-publish","title":"CI publishes a Worker application artifact"},{"expectedCount":1,"files":[{"path":".github/workflows/deploy.yml","source":"jobs:\n  deploy:\n    steps:\n      - run: pnpm exec wrangler d1 create app\n"}],"fixedFiles":[],"focusPath":".github/workflows/deploy.yml","id":"worker-resource-create","outcome":"reject","scenarioId":"worker-publish","title":"Keep Worker resource creation in Terraform"},{"expectedCount":1,"files":[{"path":".github/workflows/deploy.yml","source":"jobs:\n  deploy:\n    steps:\n      - run: gcloud run deploy api --image $IMAGE --memory 1Gi\n"}],"fixedFiles":[],"focusPath":".github/workflows/deploy.yml","id":"workflow-control-plane-mutation","outcome":"reject","scenarioId":"primary","title":"Keep Cloud Run infrastructure in Terraform"}],"filePatterns":[".github/workflows/*.{yaml,yml}","{cloudbuild,deploy,deployments,iac,infra,k8s,scripts,terraform,tools}/**","**/*.tf"],"id":"declarative-deployment-boundary","key":"text:declarative-deployment-boundary","languages":["config"],"limitations":["Terraform coverage extracts literal strings and heredocs owned by resource local-exec.command with the default interpreter or a literal POSIX shell -c interpreter; HCL templates, computed commands, custom interpreters, remote-exec, and Terraform JSON are intentionally unreported.","A Terraform diagnostic can be suppressed locally with an exact-code # sarj-noqa: SARJ309 comment on the preceding physical line.","The bounded scanner reports explicitly recognized commands and deployment Actions; dynamic command construction and unlisted provider surfaces are intentionally unreported.","Multiline quoted shell data is excluded and command substitutions are not evaluated; function bodies are checked for possible mutations without proving invocation.","Literal argv execution fields in supported operational configurations, including Cloud Build entrypoint plus args, preserve the command boundary. Image-default entrypoints and non-executable configuration data are not inferred.","Wrapper-indirected commands are intentionally unreported; full-tree CI scans wrapper files directly only when they live in an operational root.","Wrangler deploy and versions deploy publish application artifacts and are intentionally not treated as infrastructure mutation; Wrangler resource-creation commands remain reportable.","Cloud Run image/source-only deploys and updates publish application artifacts; configuration, identity, scaling, networking, secret, and other infrastructure flags remain reportable.","kubectl set image publishes application artifacts; other set commands and infrastructure mutations remain reportable.","Read-only diagnostics such as terraform show, gcloud describe/list, and kubectl get are allowed.","Structured configuration keys are not mutation evidence; deployment authorization policies such as plan-address allowlists remain allowed."],"messageIds":[],"optionsSchema":null,"rationale":"Imperative control-plane commands split deployment ownership between Terraform and repository-specific orchestration, so drift and safety depend on execution order. Publishing an application artifact is a release operation and remains outside this rule.","references":[],"remediation":"Model the resource, identity, and lifecycle in Terraform; let CI select inputs and apply the saved plan without maintaining a second mutation path.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"recognized control-plane commands bypass Terraform resource ownership","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.","Suspicious filenames and backup paths require execution-record evidence in prose; paths alone do not establish an artifact, and authorship is not inferred."],"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":"safe","category":"maintainability","code":"SARJ306","defaultLevel":"error","engine":"text","examples":[{"expectedCount":1,"files":[{"path":"config.yaml","source":"payload: |\n  # sarj-noqa: SARJ306\n# Retry count is 3\nretry_count: 3\n"}],"fixedFiles":[],"focusPath":"config.yaml","id":"literal-marker-does-not-waive-comment","outcome":"reject","scenarioId":"config-comment-suppression-provenance","title":"A YAML string containing a suppression marker retains the real comment diagnostic"},{"expectedCount":0,"files":[{"path":"config.yaml","source":"# sarj-noqa: SARJ306\n# Retry count is 3\nretry_count: 3\n"}],"fixedFiles":[],"focusPath":"config.yaml","id":"real-local-comment-waives-restatement","outcome":"accept","scenarioId":"config-comment-suppression-provenance","title":"A real local exact-code comment retains its waiver"},{"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":[{"path":"config.toml","source":"\nretry_count = 3\n"}],"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 valid authored YAML and TOML documents are analyzed; strong generated-file ownership headers, multiline literal payloads, and non-scalar entries are excluded.","Only an immediately adjacent standalone comment using `key is value` or `key equals value` wording is checked.","Key words may use prose spacing or common identifier separators; scalar spelling, case, punctuation, and internal whitespace must match."],"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":"safe","category":"maintainability","code":"SARJ305","defaultLevel":"error","engine":"text","examples":[{"expectedCount":1,"files":[{"path":"README.md","source":"<!--\n## Legacy setup\nUse the retired command.\n-->\n"}],"fixedFiles":[{"path":"README.md","source":"\n\n\n\n"}],"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.","Safe fixes require plain .md source without front matter, template markers, raw HTML tags or multiline inline-code spans; MDX, embedded configuration/code and ambiguous backtick contexts remain for review."],"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.","Static command and environment wrappers preserve the asserted command; command-name queries and ordinary text data remain excluded.","Heredoc bodies are data rather than independent shell commands. Variable references use complete names; local tracking does not prove all branch or alias behavior.","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":"architecture","code":"SARJ311","defaultLevel":"warning","engine":"text","examples":[{"expectedCount":1,"files":[{"path":"scripts/release.sh","source":"#!/bin/sh\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\nrun_step\n"}],"fixedFiles":[],"focusPath":"scripts/release.sh","id":"large-shell-program","outcome":"reject","scenarioId":"primary","title":"A large shell program moves typed logic to Python"},{"expectedCount":0,"files":[{"path":"scripts/release.sh","source":"#!/bin/sh\nexec python -m tools.release \"$@\"\n"}],"fixedFiles":[],"focusPath":"scripts/release.sh","id":"thin-shell-adapter","outcome":"accept","scenarioId":"primary","title":"A thin shell adapter remains at the platform boundary"}],"filePatterns":["**/*.sh","**/*.bash","**/*.zsh","extensionless shell scripts"],"id":"large-shell-program","key":"text:large-shell-program","languages":["shell"],"limitations":["Blank lines, comment-only lines, the shebang, and heredoc bodies do not count toward the threshold.","The count uses physical lines, not program complexity; continuations and multiline quoted data can count. Migration is a manual architecture decision, not an equivalent automatic translation.","Embedded shell in YAML, Makefiles, and Dockerfiles is intentionally outside this advisory."],"messageIds":[],"optionsSchema":null,"rationale":"Large shell programs lack the static type checking and structured interfaces available to application languages, making orchestration and domain logic harder to evolve safely.","references":[],"remediation":"Move substantive logic into a fully annotated Python CLI or module covered by Ruff, Sarj Python, and strict BasedPyright; retain only a thin shell adapter when the platform requires one.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"shell program contains at least 200 substantive lines","test":"packages/standards/tests/test_textlint.py"},{"aliases":[],"autofix":"none","category":"security","code":"SARJ307","defaultLevel":"error","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.","Shell comments and quoted-delimiter heredoc data are excluded. SQL keyword context is lexical, not proof of an execution sink; keywords embedded in path/name tokens are ignored."],"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":"error","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":"A wrapper requires a separate review of its validation"},{"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.","Static environment and command wrappers are normalized before recognizing the exact secret-read command prefix. Named-secret argument scopes and dynamic executables are left to their explicit permission contracts.","A clean wrapper name does not prove validation; alternate command forms and cloud IAM permissions are not audited by this rule."],"messageIds":[],"optionsSchema":null,"rationale":"A wildcard allow entry can preapprove secret-value reads beyond the particular command reviewed. Its reach depends on the wildcard position, command arguments, and available credentials.","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":"SARJ316","defaultLevel":"warning","engine":"text","examples":[{"expectedCount":0,"files":[{"path":".github/workflows/notify.yml","source":"name: Notify\non: pull_request\njobs:\n  notify:\n    runs-on: ubuntu-latest\n    steps:\n      - id: payload\n        env:\n          TITLE: ${{ github.event.pull_request.title }}\n        run: bash scripts/slack_payload.sh\n      - uses: slackapi/slack-github-action@v4\n        with:\n          webhook-type: incoming-webhook\n          webhook: ${{ secrets.SLACK_WEBHOOK }}\n          payload: ${{ steps.payload.outputs.payload }}\n"},{"path":"scripts/slack_payload.sh","source":"#!/usr/bin/env bash\npayload=$(jq -nc --arg title \"$TITLE\" '{blocks: [{type: \"section\", text: {type: \"plain_text\", text: $title}}]}')\nprintf 'payload=%s\\n' \"$payload\" >>\"$GITHUB_OUTPUT\"\n"}],"fixedFiles":[],"focusPath":".github/workflows/notify.yml","id":"plain-event-text","outcome":"accept","scenarioId":"primary","title":"A PR title remains plain text"},{"expectedCount":1,"files":[{"path":".github/workflows/notify.yml","source":"name: Notify\non: pull_request\njobs:\n  notify:\n    runs-on: ubuntu-latest\n    steps:\n      - id: payload\n        env:\n          TITLE: ${{ github.event.pull_request.title }}\n        run: bash scripts/slack_payload.sh\n      - uses: slackapi/slack-github-action@v4\n        with:\n          webhook-type: incoming-webhook\n          webhook: ${{ secrets.SLACK_WEBHOOK }}\n          payload: ${{ steps.payload.outputs.payload }}\n"},{"path":"scripts/slack_payload.sh","source":"#!/usr/bin/env bash\npayload=$(jq -nc --arg title \"$TITLE\" '{blocks: [{type: \"section\", text: {type: \"mrkdwn\", text: $title}}]}')\nprintf 'payload=%s\\n' \"$payload\" >>\"$GITHUB_OUTPUT\"\n"}],"fixedFiles":[],"focusPath":".github/workflows/notify.yml","id":"raw-event-markup","outcome":"reject","scenarioId":"primary","title":"A PR title reaches Slack markup unchanged"}],"filePatterns":["**/*.yml","**/*.yaml"],"id":"slack-event-text-in-mrkdwn","key":"text:slack-event-text-in-mrkdwn","languages":["config"],"limitations":["Requires a file immediately inside .github/workflows, an official Slack action consuming a same-job step output, and a direct pull-request title/head-ref environment binding.","Recognizes scalar, list or mapping trigger selectors using pull_request, pull_request_target, workflow_call, push and workflow_dispatch; at least one PR-capable trigger is required. Unknown, missing and review-event selectors abstain. A reusable workflow may inherit a PR caller; caller graphs are not modeled.","Literal false and whole github.event_name equality/inequality guards filter job, producer and consumer events. Other conditions are admitted only with PR-only remaining event possibilities; unresolved mixed-event guards abstain. General condition satisfiability is not modeled.","Prior inline shell references to GITHUB_ENV invalidate inherited workflow/job environment bindings; explicit producer-step bindings remain eligible. Dynamic aliases, external builders in prior steps, and action-managed environment mutations are not modeled.","Recognizes one jq constructor assignment and one unchanged printf to GITHUB_OUTPUT, optionally preceded by set -euo pipefail. Unknown shell commands, jq transformations, aliases, payload-file transport and custom Slack clients abstain.","Checks Block Kit section text and section fields. Header text only accepts plain_text and is not a mrkdwn sink. JSON serialization alone is not a Slack escape. This concerns reserved entity/link/mention controls; it does not claim to sanitize every formatting delimiter.","An external builder must be a literal argument-free bash/sh invocation, bounded to the same checkout. Only selected workflows are analyzed; selecting a builder alone does not discover its callers."],"messageIds":[],"optionsSchema":null,"rationale":"JSON quoting does not neutralize Slack markup controls in pull-request titles or branch names.","references":[],"remediation":"Use plain_text for event-controlled text, or deliberately encode reserved Slack controls before combining it with trusted markup.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/text_rules/slack_event_text_in_mrkdwn.py","status":"active","summary":"Keep proven event-controlled text out of Slack markup controls.","test":"packages/standards/tests/text_rules/test_slack_event_text_in_mrkdwn.py"},{"aliases":[],"autofix":"none","category":"architecture","code":"SARJ310","defaultLevel":"error","engine":"text","examples":[{"expectedCount":0,"files":[{"path":".github/workflows/ci.yml","source":"jobs:\n  build:\n    steps:\n    - run: bash scripts/check.sh fixture-data\n"}],"fixedFiles":[],"focusPath":".github/workflows/ci.yml","id":"bash-external-script-operand","outcome":"accept","scenarioId":"bash-stdin-source-selection","title":"The script operand selects an external Bash program"},{"expectedCount":1,"files":[{"path":".github/workflows/ci.yml","source":"jobs:\n  build:\n    steps:\n    - run: |\n        bash -s fixture-data <<'SOURCE'\n        printf fixture\n        SOURCE\n"}],"fixedFiles":[],"focusPath":".github/workflows/ci.yml","id":"bash-stdin-data-operand","outcome":"reject","scenarioId":"bash-stdin-source-selection","title":"An argument after Bash -s is data, while stdin supplies the program"},{"expectedCount":1,"files":[{"path":"compose.yaml","source":"services:\n  app:\n    image: fixture\n    healthcheck:\n      disable: false\n      test: [CMD-SHELL, \"printf first && printf second\"]\n"}],"fixedFiles":[],"focusPath":"compose.yaml","id":"compose-active-healthcheck-program","outcome":"reject","scenarioId":"compose-healthcheck-execution","title":"An enabled healthcheck runs its shell program"},{"expectedCount":0,"files":[{"path":"compose.yaml","source":"services:\n  app:\n    image: fixture\n    healthcheck:\n      disable: \"true\"\n      test: [CMD-SHELL, \"printf first && printf second\"]\n"}],"fixedFiles":[],"focusPath":"compose.yaml","id":"compose-disabled-healthcheck-data","outcome":"accept","scenarioId":"compose-healthcheck-execution","title":"A disabled healthcheck retains inert command data"},{"expectedCount":0,"files":[{"path":"Makefile","source":"CMD = python3 -c 'print(1)'\nall: CMD = python3 scripts/check.py\nall:\n\t@$(CMD)\n"}],"fixedFiles":[],"focusPath":"Makefile","id":"make-target-external-script","outcome":"accept","scenarioId":"make-target-variable-execution","title":"A target variable selects the maintained external script"},{"expectedCount":1,"files":[{"path":"Makefile","source":"CMD = python3 scripts/check.py\nall: CMD = python3 -c 'print(1)'\nall:\n\t@$(CMD)\n"}],"fixedFiles":[],"focusPath":"Makefile","id":"make-target-inline-source","outcome":"reject","scenarioId":"make-target-variable-execution","title":"A target variable selects inline interpreter source"},{"expectedCount":0,"files":[{"path":"mise.toml","source":"[vars]\ntext = '''\nrun = \"printf inert; printf data\"\n'''\n[tasks.probe]\n\"run\" = \"printf public-marker\"\n"}],"fixedFiles":[],"focusPath":"mise.toml","id":"mise-inert-command-data","outcome":"accept","scenarioId":"mise-source-attribution","title":"Literal command-shaped data does not become a mise task"},{"expectedCount":1,"files":[{"path":"mise.toml","source":"[vars]\ntext = '''\nrun = \"printf inert; printf data\"\n'''\n[tasks.probe]\n\"run\" = \"printf first && printf second\"\n"}],"fixedFiles":[],"focusPath":"mise.toml","id":"mise-program-use-site","outcome":"reject","scenarioId":"mise-source-attribution","title":"An executable mise task retains its own source location"},{"expectedCount":0,"files":[{"path":".github/workflows/ci.yml","source":"jobs:\n  test:\n    steps:\n      - run: |\n          uv run ruff check .\n          uv run pytest\n"}],"fixedFiles":[],"focusPath":".github/workflows/ci.yml","id":"workflow-command-sequence","outcome":"accept","scenarioId":"command-sequence","title":"Run existing tools directly without a wrapper script"},{"expectedCount":1,"files":[{"path":".github/workflows/ci.yml","source":"jobs:\n  test:\n    steps:\n      - run: |\n          for package in api worker; do\n            make test-package PACKAGE=\"$package\"\n          done\n"}],"fixedFiles":[],"focusPath":".github/workflows/ci.yml","id":"workflow-inline-program","outcome":"reject","scenarioId":"primary","title":"Keep procedural validation out of workflow YAML"},{"expectedCount":1,"files":[{"path":".github/workflows/ci.yml","source":"jobs:\n  test:\n    steps:\n      - run: |\n          uv run ruff check .\n          python3 -c 'print(1)'\n"}],"fixedFiles":[],"focusPath":".github/workflows/ci.yml","id":"workflow-sequence-inline-source","outcome":"reject","scenarioId":"command-sequence","title":"A later invocation still rejects inline interpreter source"},{"expectedCount":0,"files":[{"path":".github/workflows/ci.yml","source":"jobs:\n  test:\n    steps:\n      - run: |\n          MODE=test python3 scripts/check.py --target \"$TARGET\"\n"}],"fixedFiles":[],"focusPath":".github/workflows/ci.yml","id":"workflow-single-decision","outcome":"accept","scenarioId":"primary","title":"Invoke an external program with quoted arguments"}],"filePatterns":["**/*.{yaml,yml,toml}","**/Dockerfile*","**/*.Dockerfile","**/[Mm]akefile","**/GNUmakefile"],"id":"workflow-embedded-program","key":"text:workflow-embedded-program","languages":["config"],"limitations":["Only semantic execution fields are analyzed: Actions/composite steps, Cloud Build steps, Skaffold hooks and containers, Kubernetes containers/probes/hooks, Compose commands, mise tasks, Docker instructions and Make recipe units.","Compose healthchecks disabled by a constant native boolean value are inert; unresolved disable interpolation fails analysis coverage.","Inline interpreter source, jq/awk filters, shell control flow, logical chains, pipelines and command substitutions are rejected regardless of program size. Sequential direct invocations, external files/modules and recursively verified shell wrappers are allowed; every invocation is checked.","Unknown external-image ENTRYPOINT defaults and dynamic executable identities are not inferred; runtime CMD is analyzed only when its entrypoint ownership is proven. Unsupported selected interpreter option grammars or unprovable shell payloads fail analysis coverage instead of passing silently.","Literal dumb-init child forwarding is verified against its upstream source; signal-rewrite and ambiguous option forms fail analysis coverage.","Make target/pattern variables and prerequisite inheritance require statically provable names; conflicting pattern values with version-dependent precedence fail analysis coverage.","Explicit stdin ownership is native-proven for Bash. Command-string selection after options is native-proven for Bash, sh, dash, zsh and ksh; zsh/ksh retain their existing option namespaces. Ambiguous command/value clusters and unproven uppercase-O forms fail coverage.","Each ordinary Make logical recipe is a separate execution block; .ONESHELL groups contiguous recipe lines. YAML aliases are reported at each executable use site."],"messageIds":[],"optionsSchema":null,"rationale":"Programs embedded in deployment and build configuration bypass the normal language lint, type and local test boundaries. Simple sequences of existing commands are orchestration; executable logic belongs under the normal language tooling.","references":[],"remediation":"Reuse existing native commands or workflow steps. Keep control flow and inline interpreter source in maintained, linted repository code; avoid wrapper scripts solely to rearrange commands.","since":null,"source":"packages/standards/src/sarj_standards/libs/linting/textlint.py","status":"active","summary":"DevOps execution block embeds program logic","test":"packages/standards/tests/test_textlint.py"}],"schemaVersion":1}
