{
  "schema_version": 1,
  "generated_by": "cofferdam gen-docs",
  "cofferdam_version": "0.3.5",
  "checks": [
    {
      "id": "Consistency.BroadSuppression",
      "category": "Consistency",
      "base_priority": 0,
      "default_severity": "Info",
      "explanation": "Broad-form `// cofferdam-ignore` (no check id) silences every check on the next line. Tighten to a scoped form so suppression intent is auditable: `// cofferdam-ignore: <CheckId>: <reason>` (colon-separator) or `// cofferdam-ignore <CheckId> — <reason>` (space-separator, em-dash or hyphen reason).",
      "body": "---\nid: Consistency.BroadSuppression\ncategory: Consistency\nbase_priority: 0\ndefault_severity: Info\noptions: []\n---\n\n`// cofferdam-ignore` (with no check id) silences every check on the next non-blank line. That makes suppression intent invisible to reviewers — they have to read the surrounding code to guess what was being suppressed.\n\nTwo narrowed forms are accepted; both pin a check id and an optional reason:\n\n- `// cofferdam-ignore: <Check.Id>: <reason>` — colon-separator (canonical Biome-style).\n- `// cofferdam-ignore <Check.Id> — <reason>` — space-separator with em-dash, ASCII hyphen, or colon between id and reason (cd-b77).\n\nEither form binds the id and silences only the named check. Future readers (and linters) can audit it.\n\nThis diagnostic is informational — it never fails CI on its own. To suppress this nudge for a specific intentional broad form, write:\n\n```\n// cofferdam-ignore: Consistency.BroadSuppression: chosen broad scope intentionally\n// cofferdam-ignore\nsome_call_that_legitimately_needs_broad_silence();\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Consistency.QuoteStyle",
      "category": "Consistency",
      "base_priority": -5,
      "default_severity": "Info",
      "explanation": "Mixed quote styles within a file hurt scanability. Use a consistent quote character (single or double) throughout.",
      "body": "---\nid: Consistency.QuoteStyle\ncategory: Consistency\nbase_priority: -5\ndefault_severity: Info\noptions: []\n---\n\nMixed quote styles within a project hurt scannability. The full implementation is gated on the engine's two-pass mode (cd-d1y): pass 1 learns the dominant quote style across the corpus; pass 2 flags deviations. Today this is a stub that emits no findings.\n",
      "requires_types": false,
      "consistency": true,
      "autofix": false,
      "options": []
    },
    {
      "id": "Consistency.UnusedSuppression",
      "category": "Consistency",
      "base_priority": -5,
      "default_severity": "Low",
      "explanation": "A `cofferdam-ignore` directive (next-line, range, or file-wide) targets a check ID that has no current finding in scope. The underlying issue was likely fixed or the code was deleted — the directive is now dead weight.",
      "body": "---\nid: Consistency.UnusedSuppression\ncategory: Consistency\nbase_priority: -5\ndefault_severity: Low\noptions: []\n---\n\nA `cofferdam-ignore` suppression directive names a check ID but no current finding for that check exists in the scope the directive covers. The underlying issue was likely fixed, the code was deleted, or the check was renamed — the directive is now dead weight.\n\nThree forms are checked:\n\n- **Next-line** — `// cofferdam-ignore: <CheckId>[: reason]` where the next non-blank line has no matching finding.\n- **Range** — `// cofferdam-ignore-start: <CheckId>` … `// cofferdam-ignore-end` where the covered lines have no matching finding.\n- **File-wide** — `// cofferdam-ignore-file: <CheckId>` where the file has no matching finding anywhere.\n\nBroad-form directives (no check id, e.g. `// cofferdam-ignore`) are not flagged here — that's `Consistency.BroadSuppression`'s territory. Directives targeting a check ID not installed in the current engine run are also skipped — those are `Consistency.UnknownCheckId`'s territory.\n\n**Stale (flag):**\n\n```ts\n// cofferdam-ignore: Warning.TripleEquals: legacy comparator\nconst x = 1 + 1; // no == / != here — suppression is stale\n```\n\n**Stale range (flag):**\n\n```ts\n// cofferdam-ignore-start: Refactor.CyclomaticComplexity\nfunction simple() {\n  return 42;\n}\n// cofferdam-ignore-end\n```\n\n**Still valid (no finding):**\n\n```ts\n// cofferdam-ignore: Warning.TripleEquals: intentional loose comparison\nif (value == null) { /* ... */ }\n```\n\nRemove stale directives to keep the suppression list auditable and reviewable. A suppression with no finding is noise that erodes trust in suppressions that are still load-bearing.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Design.BoundaryFrozen",
      "category": "Design",
      "base_priority": 0,
      "default_severity": "Low",
      "explanation": "File lives inside an architectural boundary marked frozen=true in cofferdam.invariants.toml. New code in this area should be reviewed against the boundary's stated reason.",
      "body": "# Design.BoundaryFrozen\n\nStub-warns when a source file lives inside a boundary marked `frozen = true`\nin `cofferdam.invariants.toml`. The intent is to make a frozen architectural\narea visible during code review without blocking work.\n\n## Configuration\n\n```toml\n[boundaries]\n\"src/legacy/**\" = { frozen = true, reason = \"see ADR-0007\" }\n```\n\nWhen `frozen = true`, every source file matching the glob receives one\nfinding from this check, with the configured `reason` echoed in the\nmessage. v0 does not yet distinguish *new* additions to the area from\nexisting files — every match emits. Per-file deltas against a baseline\nare tracked under a follow-up bead.\n\n## Suppression\n\nUse a normal severity override or the inline `// cofferdam-disable-next-line`\nsuppression directive when the area is intentionally still active during\nmigration work.\n\n## Rationale\n\nA frozen boundary is an architectural intent: \"no new code here\". v0\nsurfaces the intent; v1 will turn intent into enforcement using the\nbaseline machinery already used by `cofferdam check --baseline`.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Design.DuplicateExportName",
      "category": "Design",
      "base_priority": 6,
      "default_severity": "Medium",
      "explanation": "The same name is exported from multiple files. Barrel re-exports collide silently and importers can't tell which one they got.",
      "body": "---\nid: Design.DuplicateExportName\ncategory: Design\nbase_priority: 6\ndefault_severity: Medium\noptions: []\n---\n\nThe same name is exported from multiple files. Barrel re-exports collide silently and importers can't tell which one they got. The check runs in the engine's `finalize` pass — per-file `run` collects export names into the shared corpus, then `finalize` groups by name and emits one `Issue` per duplicate set with the canonical occurrence as the primary span and the rest as `related: Vec<RelatedSpan>`.\n\n```ts\n// src/users.ts\nexport function format(u: User) { /* ... */ }   // flagged\n\n// src/posts.ts\nexport function format(p: Post) { /* ... */ }   // also flagged (related)\n```\n\n```ts\n// fix: namespace, rename, or pick one canonical home\nexport function formatUser(u: User) { /* ... */ }\nexport function formatPost(p: Post) { /* ... */ }\n```\n\n```toml\n# cofferdam.toml\n[checks.\"Design.DuplicateExportName\"]\nseverity = \"low\"   # demote to info-only if your project relies on barrel collisions\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Design.ImportCycle",
      "category": "Design",
      "base_priority": 8,
      "default_severity": "Medium",
      "explanation": "Files in this group import each other in a cycle. Cycles cause initialization-order surprises and obscure module boundaries.",
      "body": "---\nid: Design.ImportCycle\ncategory: Design\ndefault_severity: Medium\nbase_priority: 8\n---\n\n# Design.ImportCycle\n\nFlags circular import chains in the project graph. Cycles cause\ninitialization-order surprises, complicate refactoring, and usually point\nat modules that should be split.\n\n## Why\n\nWhen file A imports B and B imports A (directly or through any chain),\nthe runtime has to choose which module is \"half-loaded\" while resolving\nthe other. The result is a class of bugs where exports appear `undefined`\nat first call, work later, and disappear again under HMR. Static analysis\ncan find these cleanly — there's no reason to wait for them to bite.\n\n## What gets flagged\n\n* Direct cycles: `a.ts → b.ts → a.ts` (two-file cycle).\n* Longer cycles: `a → b → c → … → a` of any length.\n* Self-imports: a file importing itself (degenerate but always wrong).\n\nEach cycle produces one finding, anchored at the alphabetically-lowest\nmember's first import edge into the cycle. Other members appear in the\nfinding's `related` spans so editors and the SARIF formatter can\nhighlight every participant.\n\n## What's not flagged\n\n* Cycles that exist only via `import type { … }` edges. TypeScript erases\n  type-only imports during compilation, so they cause no runtime cycle.\n  Flip `ignore_type_only = false` in config to see them anyway.\n* Edges that leave the project graph (anything resolving into\n  `node_modules`, or unresolved bare specifiers). The check is\n  in-project-only by design.\n\n## Configuration\n\n```toml\n[checks.\"Design.ImportCycle\"]\nignore_type_only = true\n```\n\n## Limitations\n\n* Dynamic imports (`import('./m')`) participate in the cycle detection —\n  they're real edges. If your code uses dynamic imports specifically to\n  break a static cycle, you'll still get a finding; suppress with the\n  inline directive at the import site.\n* `require()` is not tracked, so CommonJS-only cycles slip through.\n* Re-export forwarders (`export { x } from './y'`) ARE edges because the\n  re-exporter performs the import; if barrel files form a cycle, that\n  shows up here.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": [
        {
          "id": "ignore_type_only",
          "kind": "boolean",
          "doc": "Skip cycles that exist only via `import type` edges. TS allows clean type-only cycles.",
          "default": true
        }
      ]
    },
    {
      "id": "Design.InvariantViolation",
      "category": "Design",
      "base_priority": 5,
      "default_severity": "Medium",
      "explanation": "An import edge violates a `[invariants]` rule declared in cofferdam.invariants.toml.",
      "body": "# Design.InvariantViolation\n\nGeneric forbid-imports / require-imports rule, fired per declared\ninvariant in `cofferdam.invariants.toml`. Supports declarative\narchitectural rules without writing a new check.\n\n## Configuration\n\n```toml\n[invariants]\n\"no-direct-db-access\" = { forbid_imports = [\"src/infra/db\"], from_layers = [\"app\"] }\n\"telemetry-required\"  = { require_imports = [\"src/infra/telemetry\"], from_layers = [\"app\"] }\n```\n\nEach invariant supports three keys:\n\n* `forbid_imports` — list of project-relative path prefixes (or bare\n  specifiers like `lodash`). An import edge whose resolved path or\n  source specifier starts with any prefix triggers a finding at the\n  import statement.\n* `require_imports` — list of prefixes that must be imported by every\n  file in `from_layers`. A file with no matching import receives one\n  finding at its first import statement.\n* `from_layers` — optional layer-name allowlist. When non-empty the\n  rule applies only to importing files whose path falls into one of\n  those layers (per the merged `[layers]` config). Empty means\n  \"applies to every in-project file\".\n\n## Matching semantics\n\nResolved paths are matched against the project-relative, forward-slash\nform (`src/infra/db/connection.ts`). Bare specifiers (`react`,\n`lodash`) match verbatim — a `forbid_imports = [\"lodash\"]` rule fires\non `import _ from 'lodash'` and `import { map } from 'lodash/fp'`\nalike. Prefix boundaries are honoured: `src/infra/db` matches\n`src/infra/db/x.ts` but not `src/infra/database.ts`.\n\n## Output\n\nFindings carry the invariant name, the specifier that violated it, and\nthe matched prefix. Suppress per-line with the standard inline\ndirective, or per-rule with a severity override on\n`Design.InvariantViolation` (every invariant shares one check id).\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Design.LayerViolation",
      "category": "Design",
      "base_priority": 9,
      "default_severity": "High",
      "explanation": "An import crosses a declared architectural layer in a direction not permitted by [layers].allow.",
      "body": "---\nid: Design.LayerViolation\ncategory: Design\ndefault_severity: High\nbase_priority: 9\n---\n\n# Design.LayerViolation\n\nEnforces architectural layering rules declared in\n`cofferdam.invariants.toml` (see [the invariants reference](../invariants.md)\nfor the canonical config location and field reference). Each file is\nmapped to a layer via gitignore-style globs, and every import edge is\nchecked against an explicit allow-list of cross-layer dependencies.\n\n## Why\n\nLayered architectures (hexagonal, onion, clean, n-tier) are easier to\nreason about and refactor when the dependency direction is enforced\nmechanically. Without enforcement, an `app/` file imports a `domain/`\nhelper which imports back into `app/` \"just this once,\" and within a\nquarter the layers are sand. Cofferdam can hold the line at PR time.\n\n## Configuration\n\nIn `cofferdam.invariants.toml` (the legacy single-file `cofferdam.toml`\nform is deprecated — see `docs/invariants.md`):\n\n```toml\n[layers]\ninfra  = [\"src/infra/**\"]\ndomain = [\"src/domain/**\", \"src/shared/**\"]\napp    = [\"src/app/**\"]\n\n[layers.allow]\ndomain = [\"infra\"]              # domain may import from infra\napp    = [\"domain\", \"infra\"]    # app may import from both\n# infra omitted → infra is isolated, must not import from any other layer\n```\n\nGlob patterns follow gitignore syntax. They're matched against each\nfile's path relative to the project root (where\n`cofferdam.invariants.toml` lives). When multiple layers match a file,\nthe one with the most-specific glob (longest non-glob prefix in its\ninclude patterns) wins; alphabetical layer name breaks true ties. Use\n`!pattern` within a layer's glob list to carve out subtrees explicitly:\n\n```toml\n[layers]\nui         = [\"components/ui/**\"]\ncomponents = [\"components/**\", \"!components/ui/**\"]\n```\n\n## What gets flagged\n\nEvery static or dynamic import whose source file is in layer **A** and\nwhose resolved target is in layer **B**, where **B** is not in the\n`allow` list for **A**.\n\n## What's not flagged\n\n* Files outside any declared layer — the project hasn't said how to\n  think about them yet, so the check stays silent.\n* Same-layer imports — always permitted.\n* Type-only imports (`import type { … }`) — they're erased at compile\n  time, no runtime layering implication.\n* External imports (anything in `node_modules`) — out of scope.\n\n## Limitations\n\n* No \"test layer\" sugar yet. If you want tests to import from anywhere,\n  add a `test` layer to your config and put it on every other layer's\n  `allow` list (or, simpler: exclude tests from the analysis via\n  `.cofferdamignore` since tests typically don't need their own layer).\n* Re-exports through barrel files attribute the violation to the\n  re-exporter, not the eventual consumer. If you want barrels to be\n  transparent, file an issue — the graph already records re-export\n  source paths so attribution can chain.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Design.MaxParameters",
      "category": "Design",
      "base_priority": 5,
      "default_severity": "Medium",
      "explanation": "Functions with too many parameters are hard to call correctly. Pass an options object instead.",
      "body": "---\nid: Design.MaxParameters\ncategory: Design\nbase_priority: 5\ndefault_severity: Medium\noptions: [limit]\n---\n\nFunctions with too many parameters are hard to call correctly: callers can't remember positional order, and adding a sixth parameter breaks every call site. Pass an options object instead.\n\n```ts\n// flagged: 7 parameters\nfunction createUser(\n  id: string,\n  name: string,\n  email: string,\n  role: Role,\n  team: string,\n  createdBy: string,\n  notify: boolean,\n) { /* ... */ }\n```\n\n```ts\n// fix: collapse into an options object\ninterface CreateUserInput {\n  id: string;\n  name: string;\n  email: string;\n  role: Role;\n  team: string;\n  createdBy: string;\n  notify?: boolean;\n}\nfunction createUser(input: CreateUserInput) { /* ... */ }\n```\n\n```toml\n[checks.\"Design.MaxParameters\"]\nlimit = 6           # bump to 6 if your codebase has earned it\nseverity = \"medium\"\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": [
        {
          "id": "limit",
          "kind": "integer",
          "doc": "maximum number of parameters per function signature",
          "default": 5
        }
      ]
    },
    {
      "id": "Design.OrphanExport",
      "category": "Design",
      "base_priority": 5,
      "default_severity": "Medium",
      "explanation": "An exported symbol is never imported anywhere in the project. Likely dead code left over from a refactor.",
      "body": "---\nid: Design.OrphanExport\ncategory: Design\ndefault_severity: Medium\nbase_priority: 5\n---\n\n# Design.OrphanExport\n\nFlags exports that no other file in the project imports — the most common\nform of dead code left behind by refactors.\n\n## Why\n\nSingle-file linters (like `tsc`'s `noUnusedLocals`) miss this case: an\nexport marked `export` is \"used\" from the file's perspective even if no\nconsumer ever imports it. The waste is only visible when you look at the\nproject as a whole.\n\n## What gets flagged\n\n* `export function foo() {}` / `export class Foo {}` / `export const x = ...`\n  whose name appears in zero `import { ... }` statements anywhere in the\n  project.\n* `export default ...` whose file is never the target of an\n  `import x from './that/file'`.\n* Local re-exports (`export { x }`) when neither `x` nor the file is\n  reachable from any import or namespace touch.\n\n## What's not flagged\n\n* Re-export forwarding nodes (`export { x } from './y'`,\n  `export * from './y'`) — they're routing, not endpoints. Whether the\n  underlying definition is consumed is what matters, and that's evaluated\n  on its own row.\n* Anything in test or mock files, by default. Configurable via\n  `test_file_patterns`.\n* Type-only exports (`export type X`, `export interface I`,\n  `export type { X } from './m'`), unless `include_type_only = true`.\n  Type-only consumption is harder to attribute reliably without full\n  type-aware analysis; opt in if your project's types matter as much as\n  its values.\n* Files reached via `import * as ns from './m'` — namespace imports are\n  treated as touching every named export of the target file. If you want\n  finer-grained tracking, switch the consumer to named imports.\n\n## Configuration\n\n```toml\n[checks.\"Design.OrphanExport\"]\ninclude_type_only = false  # set true to flag unused type-only exports\ntest_file_patterns = [\".test.\", \".spec.\", \"_test.\", \"_spec.\", \"/__tests__/\", \"/__mocks__/\"]\n```\n\n## Limitations\n\n* Only static `import` and `export` declarations are tracked. `require()`,\n  `module.exports = ...`, and dynamic `import('...')` calls are ignored —\n  flagged exports that are reached only through dynamic loading will be\n  false positives.\n* No `package.json` `main`/`module`/`exports` allowlist yet — public-API\n  entry points appear orphan if no in-project file imports them. Run with\n  the project's published surface in mind for now; an allowlist is a\n  planned follow-up.\n* Re-export chain attribution stops at the immediate re-exporter; if your\n  intermediate barrel re-exports `x` and only the barrel is consumed, the\n  underlying `x` will (correctly) be considered touched. If the barrel\n  itself is also unused, you'll see two findings rather than one — pick a\n  side and fix that file.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": [
        {
          "id": "include_type_only",
          "kind": "boolean",
          "doc": "Flag type-only exports (interfaces, type aliases, declared types) as orphans.",
          "default": false
        },
        {
          "id": "test_file_patterns",
          "kind": "string[]",
          "doc": "Filename substrings that mark a file as test/mocks. Exports from matching files are skipped.",
          "default": [
            ".test.",
            ".spec.",
            "_test.",
            "_spec.",
            "/__tests__/",
            "/__mocks__/"
          ]
        },
        {
          "id": "framework_entry_patterns",
          "kind": "string[]",
          "doc": "Filename substrings for framework entry-point files (Next.js App Router, Pages Router, SvelteKit, config files). Exports from matching files are skipped because the framework runtime — not application code — consumes them.",
          "default": [
            "/page.",
            "/layout.",
            "/route.",
            "/error.",
            "/loading.",
            "/not-found.",
            "/default.",
            "/template.",
            "/global-error.",
            "/middleware.",
            "/instrumentation.",
            "/manifest.",
            "/robots.",
            "/sitemap.",
            "/icon.",
            "/apple-icon.",
            "/opengraph-image.",
            "/twitter-image.",
            "/favicon.",
            "/_app.",
            "/_document.",
            "/_error.",
            "/+page.",
            "/+layout.",
            "/+server.",
            "/next.config.",
            "/vite.config.",
            "/vitest.config.",
            "/tsup.config.",
            "/tailwind.config.",
            "/postcss.config.",
            "/astro.config.",
            "/svelte.config.",
            "/remix.config.",
            "/playwright.config.",
            "/jest.config.",
            "/rollup.config.",
            "/webpack.config.",
            "/babel.config.",
            "/eslint.config.",
            "/prettier.config."
          ]
        }
      ]
    },
    {
      "id": "Design.ScriptedInvariant",
      "category": "Design",
      "base_priority": 7,
      "default_severity": "Medium",
      "explanation": "A scripted invariant declared in cofferdam.invariants.toml under [invariants.scripted] is violated for this file.",
      "body": "# Design.ScriptedInvariant\n\nEvaluates one or more `[invariants.scripted.\"rule-name\"]` blocks in\n`cofferdam.invariants.toml` using the v1 predicate DSL. Lets projects\nencode architectural rules that don't fit the flat\n`forbid_imports` / `require_imports` shape — file-level constraints,\ncross-file `imports` / `exports` predicates, layer-conditional gates.\n\n## Configuration\n\n```toml\n[invariants.scripted.\"controller-test-pair\"]\nwhen    = \"file matches 'src/controllers/**/*.ts'\"\nrequire = \"exists 'tests/' + basename(file)\"\nmessage = \"Every controller needs a test under tests/\"\n\n[invariants.scripted.\"ui-no-localstorage\"]\nwhen    = \"file matches 'ui/**'\"\nforbid  = \"imports 'localStorage'\"\nmessage = \"UI files must not touch localStorage directly\"\n```\n\nEach rule has four fields:\n\n* `when` (optional) — predicate that gates the rule. The rule only\n  fires on files where `when` evaluates true. Omit to apply\n  everywhere.\n* `require` (optional) — predicate that must hold; the rule emits a\n  finding when it evaluates false.\n* `forbid` (optional) — predicate that must NOT hold; the rule emits\n  a finding when it evaluates true.\n* `message` — the literal text surfaced on each finding.\n\nExactly one of `require` / `forbid` must be set. Setting both, or\nneither, is a config-load error.\n\n## DSL vocabulary\n\nSee `docs/dsl-grammar.md` for the full grammar. v1 surface:\n\n* Subjects: `file`, `file.path`, `file.layer`.\n* Operators: `matches` (glob), `==` / `!=`, `in '<layer>'`, `imports`,\n  `transitively imports`, `imports as type`, `imports as value`,\n  `exports`.\n* Functions: `basename(...)`, `dirname(...)`, `exists(...)`.\n* Boolean glue: `and`, `or`, `not`, parentheses.\n* String concat: `'a' + basename(file) + '.ts'`.\n\n## Validation\n\nDSL strings are parsed at config load (`cofferdam.invariants.toml`\nload). Bad scripts fail fast with an actionable error pointing at the\noffending rule + field, not at file 4000 of the run.\n\n## Span semantics (v1)\n\nFindings carry the file path with line/col 1:1. Per-edge spans for\n`imports` predicates are reserved for a future MINOR bump.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Readability.MaxFunctionLength",
      "category": "Readability",
      "base_priority": -5,
      "default_severity": "Low",
      "explanation": "Functions longer than the configured limit are hard to follow. Break them into smaller helpers.",
      "body": "---\nid: Readability.MaxFunctionLength\ncategory: Readability\nbase_priority: -5\ndefault_severity: Low\noptions: [limit]\n---\n\nFunctions longer than the configured limit are hard to follow. Break them into smaller helpers. The metric counts lines in the function body, not the whole declaration.\n\n```ts\n// flagged: body > 50 lines\nfunction processOrder(order: Order) {\n  // ... 80 lines of validation, pricing, side effects, and persistence\n}\n```\n\n```ts\n// fix: extract pure helpers with single responsibilities\nfunction processOrder(order: Order) {\n  const validated = validateOrder(order);\n  const priced = priceOrder(validated);\n  return persistOrder(priced);\n}\n```\n\n```toml\n[checks.\"Readability.MaxFunctionLength\"]\nlimit = 80\nseverity = \"low\"\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": [
        {
          "id": "limit",
          "kind": "integer",
          "doc": "maximum function body length in lines",
          "default": 50
        }
      ]
    },
    {
      "id": "Readability.MaxLineLength",
      "category": "Readability",
      "base_priority": -5,
      "default_severity": "Low",
      "explanation": "Lines longer than the configured limit are harder to scan and review.",
      "body": "---\nid: Readability.MaxLineLength\ncategory: Readability\nbase_priority: -5\ndefault_severity: Low\noptions: [limit]\n---\n\nLines longer than the configured limit are harder to scan and review. The check is a pure text-line scan — no AST traversal, so it runs even when a file fails to parse.\n\n```ts\n// flagged: > 120 columns\nconst config = { name: \"very long literal\", flags: [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"], description: \"...\" };\n```\n\n```ts\n// fix: break across lines\nconst config = {\n  name: \"very long literal\",\n  flags: [\"a\", \"b\", \"c\", \"d\", \"e\", \"f\"],\n  description: \"...\",\n};\n```\n\n```toml\n[checks.\"Readability.MaxLineLength\"]\nlimit = 100\nseverity = \"info\"\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": [
        {
          "id": "limit",
          "kind": "integer",
          "doc": "maximum line length in display columns",
          "default": 120
        }
      ]
    },
    {
      "id": "Refactor.CognitiveComplexity",
      "category": "Refactor",
      "base_priority": 10,
      "default_severity": "Medium",
      "explanation": "Sonar-style cognitive complexity. Branching breaks plus a nesting penalty — deeply nested code costs more than a long flat switch.",
      "body": "---\nid: Refactor.CognitiveComplexity\ncategory: Refactor\nbase_priority: 10\ndefault_severity: Medium\noptions:\n  - name: limit\n    kind: int\n    default: 15\n    doc: maximum cognitive complexity per function\n---\n\nSonar-style cognitive complexity per function — branching breaks plus a nesting penalty. Deeply nested code costs more than a long flat switch. Tracks `if`/`else if`, loops, ternaries, `switch`, `catch`, sequences of `&&`/`||`/`??`, and recursion-by-name. Default limit is `15`; override per-project with `[checks.\"Refactor.CognitiveComplexity\"] limit = N` in `cofferdam.toml`.\n\n```ts\n// flagged: nested branches stack a nesting penalty\nfunction classify(record: Record) {\n  if (record.kind === \"user\") {\n    if (record.active) {\n      for (const role of record.roles) {\n        if (role.permissions.includes(\"admin\")) {\n          return \"active-admin\";\n        }\n      }\n    }\n  }\n  return \"other\";\n}\n```\n\n```ts\n// fix: flatten via early returns and helpers\nfunction classify(record: Record) {\n  if (record.kind !== \"user\" || !record.active) return \"other\";\n  return hasAdmin(record.roles) ? \"active-admin\" : \"other\";\n}\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": [
        {
          "id": "limit",
          "kind": "integer",
          "doc": "maximum cognitive complexity per function",
          "default": 15
        }
      ]
    },
    {
      "id": "Refactor.CyclomaticComplexity",
      "category": "Refactor",
      "base_priority": 8,
      "default_severity": "Medium",
      "explanation": "McCabe cyclomatic complexity counts independent paths through a function. High values indicate branching that's hard to test and reason about.",
      "body": "---\nid: Refactor.CyclomaticComplexity\ncategory: Refactor\nbase_priority: 8\ndefault_severity: Medium\noptions:\n  - name: limit\n    kind: int\n    default: 10\n    doc: maximum cyclomatic complexity per function\n---\n\nMcCabe cyclomatic complexity per function — independent paths through the body. Starts at `1` and adds `1` for each branching node: `if`, each non-default `case`, each loop, ternary, `catch`, and each `&&` / `||` / `??` in conditions. Plain `else` does not add a path. Default limit is `10`; override per-project with `[checks.\"Refactor.CyclomaticComplexity\"] limit = N` in `cofferdam.toml`.\n\nCyclomatic and cognitive complexity often flag the same functions but rank them differently. Both are useful — cyclomatic captures \"how many test cases do I need\", cognitive captures \"how hard is this to read\". Run them together; the worst offenders fail both.\n\n```ts\n// flagged: 11 independent paths via &&/case/if combinations\nfunction dispatch(event: Event) {\n  switch (event.kind) {\n    case \"create\": return event.payload && event.payload.id ? handleCreate(event) : null;\n    case \"update\": return event.payload && event.payload.id ? handleUpdate(event) : null;\n    case \"delete\": return event.payload && event.payload.id ? handleDelete(event) : null;\n    case \"ping\": return null;\n    default: return null;\n  }\n}\n```\n\n```ts\n// fix: dispatch table flattens the case explosion\nconst handlers = { create: handleCreate, update: handleUpdate, delete: handleDelete } as const;\nfunction dispatch(event: Event) {\n  const handler = handlers[event.kind as keyof typeof handlers];\n  if (!handler || !event.payload?.id) return null;\n  return handler(event);\n}\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": [
        {
          "id": "limit",
          "kind": "integer",
          "doc": "maximum cyclomatic complexity per function",
          "default": 10
        }
      ]
    },
    {
      "id": "Refactor.DeadExport",
      "category": "Refactor",
      "base_priority": 4,
      "default_severity": "Low",
      "explanation": "Every importer of this export imports its local binding and never references it. The export is dead even though it appears used.",
      "body": "---\nid: Refactor.DeadExport\ncategory: Refactor\ndefault_severity: Low\nbase_priority: 4\n---\n\n# Refactor.DeadExport\n\nFlags exports that have at least one consumer but every consumer\nimports the local binding and never references it. The classic\n\"someone removed the call site but not the import\" residue.\n\n## Why\n\n`Design.OrphanExport` catches the easy case: an export with zero\nconsumers. `Refactor.DeadExport` catches the quieter case: the export\nis technically used (something imports it), but every importer's\nimport statement is itself dead. Cleaning these reduces the import\ngraph's apparent connectivity, which makes refactors and dependency\nanalysis cheaper.\n\n## What gets flagged\n\n* Named exports whose name appears in at least one `import { … }`\n  somewhere in the project, but every importer's local binding has\n  zero references after the import statement.\n* Default exports reached only by importers that never reference the\n  local default binding.\n\n## What's not flagged\n\n* Exports with no consumer at all — that's `Design.OrphanExport`.\n* Type-only exports — too many false positives without full type-\n  aware analysis (omitted argument annotations, JSX attribute types,\n  etc. don't always show up as identifier references).\n* Exports whose source file is reached via a namespace import\n  (`import * as ns`) anywhere in the project. Namespace touches are\n  opaque without member-access tracking, so we can't tell which named\n  export `ns.foo` actually used.\n* Exports whose source file is the target of any re-export\n  (`export { … } from './m'`). The re-exporter's barrel is the\n  consumer; whether the barrel itself is dead is a separate question.\n* Re-export records (forwarding nodes).\n\n## Limitations\n\n* `local_use_count` only counts `IdentifierReference` AST hits.\n  Decorator-injected references, `eval()`-tracked imports, and\n  string-key dynamic dispatch will look unused even when they're not.\n* Imports that are aliased (`import { foo as bar }`) count `bar`\n  references, not `foo` — correct, but worth knowing if a\n  refactor renames in one place but not the other.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Refactor.DuplicateBlock",
      "category": "Refactor",
      "base_priority": 12,
      "default_severity": "Medium",
      "explanation": "Runs of statements that recur (after rename canonicalisation) in multiple files. Likely copy-paste — extract a shared helper.",
      "body": "---\nid: Refactor.DuplicateBlock\ncategory: Refactor\nbase_priority: 12\ndefault_severity: Medium\noptions: [min_statements, min_chars, include_tokens, include_ast]\n---\n\nRuns of statements that recur (after rename canonicalisation) in multiple files. Likely copy-paste — extract a shared helper. Canonicalisation maps identifier tokens to per-window local indices so renamed copies still match. Minimum window is `6` consecutive statements (and `80` characters) to keep noise low. Cross-file: per-file `run` writes fingerprints into the shared corpus; `finalize` groups by hash and emits one `Issue` per duplicate set with `related` spans pointing at every other occurrence.\n\n```ts\n// src/orders.ts:42\nconst items = parseItems(input);\nconst validated = validateItems(items);\nconst priced = priceItems(validated, currency);\nconst taxed = applyTax(priced, region);\nconst total = sumItems(taxed);\nreturn { items: taxed, total };\n```\n\n```ts\n// src/quotes.ts:88 — same shape, renamed: flagged as related\nconst products = parseItems(input);\nconst checkedProducts = validateItems(products);\nconst pricedProducts = priceItems(checkedProducts, currency);\nconst taxedProducts = applyTax(pricedProducts, region);\nconst total = sumItems(taxedProducts);\nreturn { items: taxedProducts, total };\n```\n\n```ts\n// fix: extract once\nexport function pipeline(input: RawInput, currency: Currency, region: Region) {\n  const items = parseItems(input);\n  const validated = validateItems(items);\n  const priced = priceItems(validated, currency);\n  const taxed = applyTax(priced, region);\n  return { items: taxed, total: sumItems(taxed) };\n}\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": [
        {
          "id": "min_statements",
          "kind": "integer",
          "doc": "minimum number of consecutive statements required to flag a duplicate block",
          "default": 6
        },
        {
          "id": "min_chars",
          "kind": "integer",
          "doc": "minimum raw byte length of a duplicate window; prevents trivial single-liner runs from firing",
          "default": 80
        },
        {
          "id": "include_tokens",
          "kind": "boolean",
          "doc": "also run a sliding token-window pass that catches duplicates spanning non-statement boundaries",
          "default": false
        },
        {
          "id": "include_ast",
          "kind": "boolean",
          "doc": "run the AST statement-window pass (disable to use token-mode only)",
          "default": true
        }
      ]
    },
    {
      "id": "Refactor.LongAndComplex",
      "category": "Refactor",
      "base_priority": 12,
      "default_severity": "High",
      "explanation": "Functions that are both long and complex are the strongest refactor candidates. Length alone catches flat config tables; complexity alone catches deeply-branching short helpers. The intersection is almost always a real refactor target.",
      "body": "---\nid: Refactor.LongAndComplex\ncategory: Refactor\nbase_priority: 12\ndefault_severity: High\noptions: [length_limit, cyclomatic_limit]\n---\n\nFlag functions that are **both** long and cyclomatically complex. Length alone catches flat config blocks and long switch tables that may be perfectly readable; complexity alone catches deeply branching helpers that may still fit on one screen. Functions that exceed both thresholds are the strongest refactor candidates.\n\nLength is measured the same way `Readability.MaxFunctionLength` measures it — body lines excluding blanks and pure-comment lines. Cyclomatic complexity is measured the same way `Refactor.CyclomaticComplexity` measures it — McCabe count starting at `1`, adding `1` per branching node.\n\nDefaults: `length_limit = 75`, `cyclomatic_limit = 15`. These are intentionally above the standalone limits — this check is about the worst offenders, not the long tail.\n\n```ts\n// flagged: 90 effective lines AND cyclomatic 17 — both dimensions exceeded\nfunction processOrder(order: Order, config: Config, env: Env) {\n  // ... 90 lines of validation, branching, side effects ...\n}\n```\n\n```ts\n// fix: split along the seams the complexity exposes\nfunction processOrder(order: Order, config: Config, env: Env) {\n  const validated = validateOrder(order, config);\n  const priced = priceOrder(validated, env);\n  return persistOrder(priced);\n}\n```\n\n```toml\n[checks.\"Refactor.LongAndComplex\"]\nlength_limit = 100\ncyclomatic_limit = 20\nseverity = \"high\"\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": [
        {
          "id": "length_limit",
          "kind": "integer",
          "doc": "minimum non-blank-non-comment body lines for a function to be considered long",
          "default": 75
        },
        {
          "id": "cyclomatic_limit",
          "kind": "integer",
          "doc": "minimum cyclomatic complexity for a function to be considered complex",
          "default": 15
        }
      ]
    },
    {
      "id": "Refactor.PreferNullishCoalescing",
      "category": "Refactor",
      "base_priority": 3,
      "default_severity": "Low",
      "explanation": "`x || default` falls through on every falsy value (`0`, `\"\"`, `false`). Use `??` to fall through only on `null`/`undefined`.",
      "body": "---\nid: Refactor.PreferNullishCoalescing\ncategory: Refactor\nbase_priority: 3\ndefault_severity: Low\noptions: []\n---\n\n`x || default` falls through on every falsy value (`0`, `\"\"`, `false`). Use `??` to fall through only on `null`/`undefined`. Today the check ships the narrow high-confidence shape: `member-access || default-literal` (string / number / bool / `null` / bare `undefined` / array literal / object literal). Bare-identifier LHS, function-call LHS, and arithmetic LHS are deliberately not flagged — they're too often genuine alt-branch logic without type info. The rule broadens once the type-aware tier lands.\n\n```ts\n// flagged\nconst timeout = config.timeout || 5000;     // 0 should NOT fall through\nconst name = user.name || \"anonymous\";      // \"\" should NOT fall through\n```\n\n```ts\n// fix\nconst timeout = config.timeout ?? 5000;\nconst name = user.name ?? \"anonymous\";\n```\n\n```ts\n// not flagged (intentional falsy fallthrough)\nconst flag = isAdmin || isOwner;            // bare identifier — alt branch\nconst value = compute() || 0;              // call result — return type ambiguous\nconst sum = (a + b) || 1;                  // arithmetic — clearly wants falsy\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Refactor.PreferOptionalChain",
      "category": "Refactor",
      "base_priority": 5,
      "default_severity": "Low",
      "explanation": "`a && a.b && a.b.c` is more concisely written as `a?.b?.c`. The optional-chain operator (`?.`) short-circuits on null/undefined.",
      "body": "---\nid: Refactor.PreferOptionalChain\ncategory: Refactor\nbase_priority: 5\ndefault_severity: Low\noptions: []\n---\n\n`a && a.b && a.b.c` is more concisely written as `a?.b?.c`. The optional-chain operator (`?.`) short-circuits on `null`/`undefined`. The check flags `lhs && rhs` where the LHS is a \"safe to repeat\" expression (identifier, `this`, or a pure member chain — never contains a `CallExpression` or `NewExpression`) and the RHS is a member access (or call on a member access) whose object span renders to the same source bytes as the LHS span.\n\n```ts\n// flagged\nreturn user && user.profile;\nreturn user && user.profile && user.profile.name;\nreturn arr && arr[0];\n```\n\n```ts\n// fix\nreturn user?.profile;\nreturn user?.profile?.name;\nreturn arr?.[0];\n```\n\n```ts\n// not flagged (LHS is a call — rewriting would halve the call count)\nreturn get() && get().profile;\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Refactor.UnusedVariable",
      "category": "Refactor",
      "base_priority": 10,
      "default_severity": "Low",
      "explanation": "Variables declared but never read are dead code. Prefix with `_` to opt out where the binding is intentionally unused (e.g., positional function parameters).",
      "body": "---\nid: Refactor.UnusedVariable\ncategory: Refactor\nbase_priority: 10\ndefault_severity: Low\noptions: []\n---\n\nVariables declared but never read are dead code. They add noise, mislead readers, and often indicate a logic error — the value was computed but never actually used. The check covers `let`/`const`/`var` bindings, function declarations, class declarations, catch-clause variables, and function parameters.\n\n```ts\n// flagged: count is declared but never read\nfunction summarise(items: Item[]) {\n  const count = items.length;   // unused\n  return items.map(i => i.name).join(\", \");\n}\n```\n\n```ts\n// fix: remove or use the binding\nfunction summarise(items: Item[]) {\n  return items.map(i => i.name).join(\", \");\n}\n```\n\n```ts\n// opt-out: prefix with _ to signal intentionally unused\nfunction handler(_event: Event, context: Context) {\n  return context.respond(\"ok\");\n}\n```\n\nThe check skips: ESM imports (tree-shaker territory), type-only symbols (TypeAlias, Interface, Enum — deferred to the type-aware tier), rest-pattern bindings (`...rest`), and all module-scope (top-level) bindings (which may be exported without the export being visible to the AST-only pass). Use the `_` prefix convention to opt out of flagging for positional parameters you cannot remove.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Rust.MissingPubDoc",
      "category": "Design",
      "base_priority": 4,
      "default_severity": "Low",
      "explanation": "Public items in a library crate compose the published API surface. Document each `pub fn` / `pub struct` / `pub enum` / `pub trait` with a `///` doc comment so consumers can understand what to call.",
      "body": "---\nid: Rust.MissingPubDoc\ncategory: Design\nbase_priority: 4\ndefault_severity: Low\noptions: []\n---\n\nPublic items in a library crate compose the published API surface.\nConsumers discover what to call by reading the docs; an undocumented\n`pub fn` is a discoverability gap that turns into a support cost the\nfirst time someone has to read the source to figure out what it does.\n\nThe check fires on `pub` declarations of the four item kinds that make\nup the API surface: `fn`, `struct`, `enum`, `trait`. Restricted\nvisibilities (`pub(crate)`, `pub(super)`, `pub(in path)`) are exempt —\nthey're internal API, not the published contract.\n\nWhat counts as documented:\n\n* a preceding `/// ...` line doc comment,\n* a preceding `/** ... */` block doc comment,\n* a preceding `#[doc = \"...\"]` attribute,\n* or `#[doc(hidden)]` (deliberately undocumented — same opt-out as\n  clippy's `missing_docs`).\n\n## Example\n\n```rust\n// FIRES: no doc comment.\npub fn parse_id(s: &str) -> i64 {\n    s.parse::<i64>().unwrap_or(0)\n}\n\n// DOES NOT FIRE: outer doc comment.\n/// Parse `s` as an `i64`, returning 0 on failure.\npub fn parse_id_documented(s: &str) -> i64 {\n    s.parse::<i64>().unwrap_or(0)\n}\n\n// DOES NOT FIRE: restricted visibility (internal API).\npub(crate) fn helper() {}\n\n// DOES NOT FIRE: explicitly hidden.\n#[doc(hidden)]\npub fn internal_thing() {}\n```\n\n## What to do\n\n* Add a `///` doc comment naming what the item does, its arguments, and\n  any non-obvious failure modes. A one-liner is fine for self-evident\n  helpers; longer docs are valuable for anything stateful.\n* If the item shouldn't be part of the public surface, narrow the\n  visibility to `pub(crate)` or remove the `pub` entirely.\n* If the item is part of the public surface but deliberately\n  undocumented (private extension hook, deprecated alias being removed),\n  apply `#[doc(hidden)]` so the suppression is visible in source.\n\n## Suppression\n\nFor items that are temporarily undocumented (work-in-progress trait\nmethods, scaffolding), use an inline directive linked to a tracking\nissue:\n\n```rust\n// cofferdam-ignore: Rust.MissingPubDoc: docs land with <issue>\npub fn next_step() -> i32 { 0 }\n```\n\nA blanket file-wide suppression on this check tends to silently grow as\nnew public items are added; prefer per-item directives.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Rust.NoUnimplementedInNonTest",
      "category": "Warning",
      "base_priority": 14,
      "default_severity": "High",
      "explanation": "`unimplemented!()` / `todo!()` panic at runtime; calling them outside test code ships a guaranteed crash. Implement the function or move it into a `#[test]`.",
      "body": "---\nid: Rust.NoUnimplementedInNonTest\ncategory: Warning\nbase_priority: 14\ndefault_severity: High\noptions: []\n---\n\n`unimplemented!()` and `todo!()` both expand to a runtime panic. They're\nfine as scaffolding while you sketch a function or stub a trait impl\nduring development — but shipping them in library code means any consumer\nwho walks that path crashes with no useful diagnostic.\n\nThe check fires on `unimplemented!()` and `todo!()` invocations that are\n**not** inside a test context. A call counts as in test context when any\nof its ancestors is:\n\n* a function or module annotated with `#[cfg(test)]`,\n* a function annotated with `#[test]`,\n* or a module named `tests`.\n\n## Example\n\n```rust\n// FIRES: lib code, no test guard.\npub fn next_step() -> i32 {\n    todo!()  // -> Rust.NoUnimplementedInNonTest\n}\n\n// DOES NOT FIRE: enclosing function is #[test].\n#[test]\nfn placeholder_test() {\n    todo!(\"fill in once the API stabilises\");\n}\n\n// DOES NOT FIRE: enclosing module is #[cfg(test)].\n#[cfg(test)]\nmod tests {\n    fn fixture_helper() -> i32 {\n        unimplemented!()\n    }\n}\n```\n\n## What to do\n\n* Implement the function. If the implementation is genuinely deferred,\n  open an issue and link to it from the surrounding context — but\n  don't ship a panic.\n* If the function is a trait method that doesn't apply, return a\n  default value, return an error, or restructure the trait so the\n  method isn't required for this implementer.\n* If the call is during in-progress scaffolding, move it under a\n  `#[cfg(test)]` gate or behind a feature flag while you work.\n\n## Suppression\n\nInline directives narrow the suppression to the specific call:\n\n```rust\n// cofferdam-ignore: Rust.NoUnimplementedInNonTest: temporary — tracked in <issue>\npub fn next_step() -> i32 {\n    todo!()\n}\n```\n\nSuppressions on `unimplemented!()` / `todo!()` should always link to the\ntracking issue. They are by definition known incompleteness — if no issue\nexists, the suppression masks technical debt.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Rust.NoUnwrapInLib",
      "category": "Warning",
      "base_priority": 12,
      "default_severity": "Medium",
      "explanation": "Calling `.unwrap()` in library code panics on `None`/`Err(_)` with no diagnostic context. Return `Result` and propagate via `?`, or use `.expect(\"<reason>\")` when the value is provably infallible.",
      "body": "---\nid: Rust.NoUnwrapInLib\ncategory: Warning\nbase_priority: 12\ndefault_severity: Medium\noptions: []\n---\n\nCalling `.unwrap()` in library code panics on `None` or `Err(_)` with\n**no diagnostic context** — the caller gets a generic backtrace and\nnowhere to start debugging. The Rust convention is to use\n`.expect(\"<reason>\")` instead when the value is provably infallible:\nthe message describes the invariant, so when it breaks the diagnostic\ntells the next reader what assumption failed.\n\nThis check therefore flags **only `.unwrap()`** — bare, no-context\npanics. `.expect(\"<message>\")` with a descriptive message is the\ndocumented escape valve and is **not** flagged. The check matches\nclippy's `unwrap_used` (which flags `.unwrap()`) rather than its\n`expect_used` (which flags `.expect()` and is rarely enabled).\n\nThe check fires on `.unwrap()` call expressions that are **not**\ninside a test context. A call counts as in test context when any of\nits ancestors is:\n\n* a function or item annotated with `#[cfg(test)]`,\n* a function annotated with `#[test]`,\n* or a module named `tests`.\n\nCalls that hit those guards do not fire — assertions like\n`parse(input).unwrap()` inside a `#[test]` are idiomatic and the test\nharness handles the panic correctly.\n\n## Example\n\n```rust\n// FIRES: bare .unwrap() in lib code, no context.\npub fn parse_id(s: &str) -> i64 {\n    s.parse::<i64>().unwrap()  // -> Rust.NoUnwrapInLib\n}\n\n// DOES NOT FIRE: descriptive .expect() carries the proof of safety.\npub fn first_char(s: &str) -> char {\n    s.chars().next().expect(\"caller guarantees non-empty input\")\n}\n\n// DOES NOT FIRE: enclosing function is #[test].\n#[test]\nfn round_trip() {\n    assert_eq!(parse_id(\"42\"), 42);\n    let parsed: i64 = \"1\".parse().unwrap();  // ok in tests\n}\n\n// DOES NOT FIRE: enclosing module is #[cfg(test)].\n#[cfg(test)]\nmod tests {\n    use super::*;\n    fn helper() -> i64 {\n        \"7\".parse().unwrap()  // ok — inside cfg(test) module\n    }\n}\n```\n\n## What to do\n\n* In library functions, return `Result<T, E>` and propagate via `?`.\n* When the value is actually infallible (you've just checked\n  `Option::is_some` two lines up, you hold a `Mutex::lock()` and a\n  poisoned lock is unrecoverable, you've serialized an internal type\n  that can't fail), use `.expect(\"<reason>\")` — the message names\n  the invariant for the next reader when it breaks. This is **not\n  flagged**.\n* Inside `main()`, prefer `?` and let the program exit with a\n  formatted error rather than the default panic dump.\n\n## Suppression\n\nIf a particular `unwrap()` is provably safe and the refactor isn't\nworth it, narrow the suppression to the specific line:\n\n```rust\n// cofferdam-ignore: Rust.NoUnwrapInLib: provably safe — see invariant in comment above\nlet known_good = parse_input(SAFE_LITERAL).unwrap();\n```\n\nA blanket file-wide suppression masks new unwraps added later; prefer\ninline directives.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Warning.NoConsoleLog",
      "category": "Warning",
      "base_priority": -10,
      "default_severity": "Low",
      "explanation": "`console.log(...)` calls are typically debugging leftovers. Route logs through a dedicated logger or strip them in CI.",
      "body": "---\nid: Warning.NoConsoleLog\ncategory: Warning\nbase_priority: -10\ndefault_severity: Low\noptions:\n  - name: methods\n    type: string[]\n    default: [\"log\"]\n---\n\n`console.log(...)` calls are typically debugging leftovers. Route logs through a dedicated logger or strip them in CI.\n\nBy default only `console.log` is flagged. `console.warn` and `console.error` are **not** flagged by default — error logging in catch blocks is legitimate and should not produce false positives.\n\nBare-identifier match only — aliased calls (`const c = console; c.log(...)`) escape detection until the type-aware pass.\n\n```ts\n// flagged by default\nconsole.log(\"debug:\", value);\n```\n\n```ts\n// NOT flagged by default — intentional error logging\ntry {\n  riskyOp();\n} catch (err) {\n  console.error(\"operation failed:\", err);\n}\n```\n\n```ts\n// fix: route through a logger that strips in production\nimport { logger } from \"./logger\";\nlogger.debug(\"debug:\", value);\nlogger.error(\"failed:\", err);\n```\n\n## Options\n\n### `methods` (string[], default: `[\"log\"]`)\n\nThe list of `console` methods to flag. Defaults to `[\"log\"]` only.\n\nTo restore the broad pre-v0.4 behaviour and flag `console.log`, `console.warn`, and `console.error`:\n\n```toml\n[checks.\"Warning.NoConsoleLog\"]\nmethods = [\"log\", \"warn\", \"error\"]\n```\n\nTo flag only error-level calls:\n\n```toml\n[checks.\"Warning.NoConsoleLog\"]\nmethods = [\"error\"]\n```\n\nTo disable the check entirely without touching `enabled`, set an empty list:\n\n```toml\n[checks.\"Warning.NoConsoleLog\"]\nmethods = []\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": [
        {
          "id": "methods",
          "kind": "string[]",
          "doc": "console methods to flag. Defaults to [\"log\"]. Set to [\"log\", \"warn\", \"error\"] to restore the broad pre-v0.4 behaviour.",
          "default": [
            "log"
          ]
        }
      ]
    },
    {
      "id": "Warning.NoDebugger",
      "category": "Warning",
      "base_priority": 10,
      "default_severity": "Medium",
      "explanation": "`debugger` statements halt execution under attached devtools. Remove before shipping.",
      "body": "---\nid: Warning.NoDebugger\ncategory: Warning\nbase_priority: 10\ndefault_severity: Medium\noptions: []\n---\n\n`debugger` statements halt execution under attached devtools. Always a debugging leftover in shipped code — there's no benign use case in production builds. The fix is mechanical: delete the line.\n\n```ts\n// flagged\nfunction inspect(x: unknown) {\n  debugger;\n  return x;\n}\n```\n\n```ts\n// fix\nfunction inspect(x: unknown) {\n  return x;\n}\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Warning.NoEval",
      "category": "Warning",
      "base_priority": 18,
      "default_severity": "High",
      "explanation": "`eval(...)` and `new Function(...)` execute arbitrary strings as code. Universally banned for security and performance reasons.",
      "body": "---\nid: Warning.NoEval\ncategory: Warning\nbase_priority: 18\ndefault_severity: High\noptions: []\n---\n\n`eval(...)` and `new Function(...)` execute arbitrary strings as code — both are universally banned in security-conscious codebases. The check has no opt-in for a reason. If you have a vetted, isolated use, suppress per-line with an inline directive. Aliased calls (`const f = eval; f(\"...\")`) are out of scope — the AST-only pass matches bare identifiers only.\n\n```ts\n// flagged\nreturn eval(userInput);                       // direct eval\nreturn new Function(\"a\", \"b\", body);          // eval-equivalent\n```\n\n```ts\n// fix: parse what you actually need, don't execute strings\nreturn JSON.parse(userInput);                 // for JSON\nreturn template(parameters);                  // for templating, use a real engine\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Warning.TripleEquals",
      "category": "Warning",
      "base_priority": 15,
      "default_severity": "High",
      "explanation": "`==` and `!=` perform type coercion and are almost always a bug. Use `===` and `!==` instead.",
      "body": "---\nid: Warning.TripleEquals\ncategory: Warning\nbase_priority: 15\ndefault_severity: High\noptions: []\n---\n\n`==` and `!=` perform type coercion and are almost always a bug. Use `===` and `!==` instead. Walks every `BinaryExpression` and flags the equality operators.\n\n**Null exemption** (`eqeqeq: always-except-null` semantics): `x == null`, `x != null`, `null == x`, and `null != x` are intentionally exempt. The `== null` pattern is idiomatic JavaScript/TypeScript shorthand for \"value is `null` or `undefined`\" — replacing it with `=== null` changes runtime behaviour. All four shapes produce no diagnostic.\n\n```ts\n// flagged\nif (a == b) return true;\nif (a != b) return false;\nif (y == \"null\") return true;   // string literal, not the keyword\n```\n\n```ts\n// fix\nif (a === b) return true;\nif (a !== b) return false;\n```\n\n```ts\n// not flagged — null-operand exemption\nif (val == null) return \"null or undefined\";\nif (val != null) return \"defined\";\nif (null == other) return \"null or undefined\";\nif (null != other) return \"defined\";\n```\n\n```ts\n// not relevant — relational operators don't coerce in the same way\nif (a < b) return true;\nif (a >= b) return false;\n```\n\n```toml\n# The default (high) gates CI — explicit override only useful to demote.\n[checks.\"Warning.TripleEquals\"]\nseverity = \"medium\"\n```\n",
      "requires_types": false,
      "consistency": false,
      "autofix": true,
      "options": []
    },
    {
      "id": "Warning.UnusedImport",
      "category": "Warning",
      "base_priority": 7,
      "default_severity": "Low",
      "explanation": "Re-export of a symbol that no other file imports from this file. Single-file linters miss this case.",
      "body": "---\nid: Warning.UnusedImport\ncategory: Warning\ndefault_severity: Low\nbase_priority: 7\n---\n\n# Warning.UnusedImport\n\nFlags re-export passthroughs (`export { Bar } from './bar'`) when no\nother file in the project imports `Bar` from the re-exporter. The\nclassic case single-file linters miss because the file's own re-export\ncounts as \"using\" the symbol locally.\n\n## Why\n\n```ts\n// barrel.ts\nexport { Bar } from './bar';   // tsc: \"Bar is used (in the export)\"\n                               // reality: nobody imports Bar from barrel.ts\n```\n\n`tsc --noUnusedLocals` and ESLint `no-unused-vars` treat the re-export\nas a use site, so this kind of dead barrel entry survives indefinitely.\nCofferdam sees the whole project graph and can confirm whether anyone\ndownstream actually imports the re-exported name.\n\n## What gets flagged\n\n* `export { Bar } from './m'` when no other file has\n  `import { Bar } from './barrel'` (or `import Bar from './barrel'` for\n  default re-exports).\n* Default re-exports (`export { default } from './m'`,\n  `export { default as foo } from './m'`) when nobody imports the\n  default (or the renamed name) from the re-exporter.\n\n## What's not flagged\n\n* Plain unused imports inside a single file — tsc/ESLint already do\n  this well, and we deliberately don't duplicate.\n* Star re-exports (`export * from './m'`) — they have no specific\n  name to track. If the re-exporter file itself is unused, that's\n  `Design.OrphanExport`'s job.\n* Type-only re-exports — same false-positive surface as\n  `Refactor.DeadExport`. Will be enabled with type-aware analysis.\n* Re-exports from a file reached by `import * as ns` somewhere — the\n  namespace import opaquely consumes everything.\n* Side-effect imports (`import './polyfill'`) — they have no name\n  bindings.\n\n## Limitations\n\n* Re-export chain depth: we look one hop at a time. If `a.ts`\n  re-exports from `b.ts` re-exports from `c.ts` and only `c.ts`'s\n  origin export is consumed (skipping the barrel layers), each\n  re-exporter shows up separately. Walking through chains for\n  attribution is a planned enhancement.\n* Dynamic imports of the re-exporter module DO count as namespace\n  consumption (matching real runtime semantics), so a single\n  `import('./barrel').then(m => m.Bar)` keeps every re-export off the\n  list.\n",
      "requires_types": false,
      "consistency": false,
      "autofix": false,
      "options": []
    },
    {
      "id": "Warning.UnusedNullCheck",
      "category": "Warning",
      "base_priority": 15,
      "default_severity": "Low",
      "explanation": "An equality check against `null`/`undefined` whose other operand's TypeScript type already excludes that value — the guard can never change the outcome. Dead defensive code, or a hint the type annotation disagrees with reality.",
      "body": "---\nid: Warning.UnusedNullCheck\ncategory: Warning\nbase_priority: 15\ndefault_severity: Low\noptions: []\n---\n\nAn equality comparison against `null` or `undefined` where the other operand's resolved TypeScript type already excludes the value being checked for. The guard can never change the outcome — it's either dead defensive code, or a signal that the type annotation disagrees with reality.\n\nThis is cofferdam's first **type-aware** check (`requires_types: true`). It's routed through the ts-morph type host rather than the pure-Rust pipeline, so it only runs when a type host is available — a project with `tsconfig.json` and `ts-morph` installed, and `[engine] type_aware` not disabled. When no host is available the engine skips the check entirely (no false positives from missing type info).\n\nThe comparison semantics mirror JavaScript's nullish equality exactly:\n\n- `x == null` / `x != null` (**loose**) match *both* `null` and `undefined`. Redundant only when the type excludes both.\n- `x === null` / `x !== null` (**strict**) test `null` alone.\n- `x === undefined` / `x !== undefined` test `undefined` alone.\n\nThe check **bails on `any` and `unknown`** — the compiler can't prove a guard redundant against a type it knows nothing about, so flagging would be a false positive.\n\n```ts\n// flagged — `s` is `string`, which excludes null and undefined\nfunction f(s: string) {\n  if (s !== null) return s;       // redundant\n  if (s != null) return s;        // redundant (loose, excludes both)\n}\n```\n\n```ts\n// not flagged — `s` genuinely includes the value being checked\nfunction g(s: string | null) {\n  if (s !== null) return s;       // meaningful: s can be null\n}\nfunction h(s: string | undefined) {\n  if (s === undefined) return \"\";  // meaningful: s can be undefined\n}\n```\n\n```ts\n// not flagged — strict `!== null` against a type that only adds undefined\nfunction k(s: string | undefined) {\n  if (s !== null) return s;       // s can't be null, but === null is the wrong test;\n                                  // strict-null is NOT redundant only when null is possible.\n}\n```\n\n```ts\n// not flagged — `any` / `unknown` defeat the analysis\nfunction u(x: any) {\n  if (x != null) return x;\n}\n```\n\nNo autofix: removing a guard changes control flow, so the safe action is human review, not a mechanical edit.\n\n```toml\n# Raise from the default (low) if you want redundant guards to gate CI.\n[checks.\"Warning.UnusedNullCheck\"]\nseverity = \"medium\"\n```\n",
      "requires_types": true,
      "consistency": false,
      "autofix": false,
      "options": []
    }
  ]
}
