Skip to content

feat(ci): dead-code detection gate — knip (TS) + @typescript-eslint/no-unused-vars + vulture (Python) (cairn MVG gate #6) #282

@theagenticguy

Description

@theagenticguy

This implements cairn Minimum Viable Gate Set gate #6 (Dead code detection — catches AI002, AI003), from https://github.com/krokoko/cairn (plugins/software-verification/.../detection-patterns-gates.md, "Minimum Viable Gate Set"). Mapping the gate set to the open CA issues, gate #6 is the one with no corresponding issue:

cairn MVG gate tracked by
1 Type checking #260 (CA-14)
2 Empty-catch / silent-success #257 (CA-09)
3 Mutation testing #255 (CA-04)
5 Import boundary + magic values #254 (CA-03), #258 (CA-10)
6 Dead code detection — this issue —

Component

Tooling / CI

Describe the feature

Add a dead-code detection gate covering two surfaces the existing gates miss:

  • Unused imports / locals / params — lint-level, per file.
  • Unreferenced exports / files / dead barrels — project-graph level (an exported symbol no module imports; a barrel index.ts with zero importers).

across the TypeScript packages (cdk, cli) and the Python package (agent).

Use case — the detection asymmetry this closes

The repo enforces unused-symbol detection on one language and not the other, so dead code accumulates only on the TypeScript side:

  • Python (agent/) — already enforced. ruff select includes "F" (agent/pyproject.toml), so pyflakes F401 fails the build on an unused import. A dead import in agent/src cannot reach main.
  • TypeScript is split. cli/tsconfig.json:21-22 sets "noUnusedLocals": true / "noUnusedParameters": true, so the CLI already blocks unused locals and imports. But cdk/tsconfig.json:21-22 sets both to false, and neither flat-config eslint (cdk/eslint.config.mjs, cli/eslint.config.mjs) carries a no-unused-vars error rule. So unused TS symbols pass silently in cdk/ only — which is exactly where every known instance lives. Note: this repo does NOT use projen; the eslint configs are hand-maintained flat-config files.

This is not hypothetical — three live instances exist at HEAD bb7876a (all in cdk/):

  1. Dead import: cdk/src/handlers/shared/create-task-core.ts imports DEFAULT_MAX_TURNS from ./validation and never uses it. (This file is also the subject of refactor(cdk): trim createTaskCore import graph so webhook processors don't load attachment/Cedar deps #232; an unused-import gate would have surfaced it.)
  2. Dead barrel: cdk/src/bootstrap/index.ts (added in feat(bootstrap): policies as typed TypeScript with version and hash #158) re-exports the bootstrap policy/version symbols, but nothing imports the barrel — every consumer imports the submodules (./policies, ./version) directly.
  3. Orphaned export: LINEAR_SECRET_PREFIX in cdk/src/handlers/shared/linear-verify.ts is exported but has no importer anywhere in cdk/cli/agent.

cairn's CI-integration model lists "dead code" as a fast-CI gate and tracks the metric "dead code count (should not increase)" — i.e. a ratchet, not a one-time cleanup.

Proposed solution (grounded against current tooling, mid-2026)

1. TS lint-level — @typescript-eslint/no-unused-vars (typescript-eslint v8; base no-unused-vars must be off). This is the direct parity match to the Python F401 already enforced — the same pattern CA-10 (#258) used for magic values (no-magic-numbersPLR2004).

"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", {
  "args": "all", "argsIgnorePattern": "^_",
  "varsIgnorePattern": "^_", "caughtErrors": "all",
  "caughtErrorsIgnorePattern": "^_", "ignoreRestSiblings": true,
  "destructuredArrayIgnorePattern": "^_"
}]

Apply by editing the hand-maintained flat-config files directly: cdk/eslint.config.mjs and cli/eslint.config.mjs (this repo has no projen, so there is no generated config to regenerate).

2. TS project-graph level — knip (v6.x). ts-prune is dead (archived read-only 2025-09-19; its README points new projects to knip). knip natively understands Yarn workspaces and per-workspace entry points. Minimal knip.json (the ! suffix marks production entry so test/config files aren't counted as dead):

{
  "$schema": "https://unpkg.com/knip@6/schema.json",
  "workspaces": {
    "cdk": { "entry": ["src/main.ts!"], "project": ["src/**/*.ts!"] },
    "cli": { "entry": ["src/index.ts!"], "project": ["src/**/*.ts!"] },
    "docs": { "entry": ["astro.config.{js,mjs,ts}", "src/pages/**/*.{astro,ts}"], "project": ["src/**/*.{astro,ts,tsx}"] }
  }
}

The explicit cli entry is what stops cli/src/index.ts (the declared package main/types source) from false-positiving as an unused export.

3. Python module-level — vulture (v2.16). ruff F401 covers unused imports but has no rule for unused module-level functions/classes. vulture fills that gap:

vulture agent/ --make-whitelist > agent/.vulture_allowlist.py   # once, reviewed
vulture agent/ agent/.vulture_allowlist.py --min-confidence 80   # CI

4. Rollout — advisory first, then ratchet. Land all three non-blocking; fix or baseline the existing instances (the three above plus whatever knip/vulture surface); then make blocking in the required build check. For knip, gate on the JSON issue count so the build fails only on increases (the cairn "should not increase" metric); knip has no native baseline file, so snapshot the count in CI. For vulture, ship the reviewed allowlist so the current tree passes, then flip --min-confidence 80 to a blocking exit.

Acceptance criteria

  • @typescript-eslint/no-unused-vars enabled as an error in the cdk and cli flat-config eslint files (cdk/eslint.config.mjs, cli/eslint.config.mjs), with ^_ ignore patterns. (cdk also flips noUnusedLocals/noUnusedParameters to true to match cli, closing the tsconfig half of the gap.)
  • knip added with a per-workspace knip.json covering cdk/cli/docs; cli/src/index.ts is not flagged (entry-point caveat handled).
  • vulture added for agent/ with a reviewed --make-whitelist allowlist and --min-confidence 80.
  • The three known instances are resolved: dead DEFAULT_MAX_TURNS import dropped, cdk/src/bootstrap/index.ts barrel removed (or given a real importer), LINEAR_SECRET_PREFIX removed or consumed.
  • Gate runs in fast CI (static, seconds; no build needed), advisory first.
  • Gate becomes blocking in the required build check once the baseline is clean, on a "dead-code count must not increase" basis.

Other information

(Drafted by Bonk during the ABCA review; tool currency — knip 6.x, ts-prune archived, vulture 2.16, typescript-eslint v8 — verified against current docs. Filed via Laith's account.)

Metadata

Metadata

Assignees

No one assigned

    Labels

    ci-cdBuild pipeline, deploy.yml, CI perf/caching, GitHub Actions workflowsenhancementNew feature or requesttoolingvalidation-loopTasks related to improve the validation loop for ABCA's codebase

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions