feat(ci): dead-code detection gate — knip + no-unused-vars + vulture (#282)#520
Conversation
…ws-samples#282) Closes the unused-symbol detection asymmetry (cairn MVG gate aws-samples#6): Python already fails on unused imports (ruff F401) but unused TS symbols passed silently in cdk/. Adds three detection layers plus advisory CI wiring. - TS lint-level: @typescript-eslint/no-unused-vars (error) in cdk+cli flat configs with ^_ ignore patterns; cdk tsconfig flips noUnusedLocals/noUnusedParameters to true (parity with cli). - TS project-graph: knip v6 with per-workspace knip.json (cdk/cli/docs) + a count-only ratchet (scripts/check-deadcode-ratchet.mjs, knip-baseline.json) that fails only when the count rises — the cairn "dead-code count should not increase" metric. - Python module-level: vulture 2.16 (config in pyproject [tool.vulture], min-confidence 80) with a minimal reviewed .vulture_allowlist.py for the SDK-mandated hook_context params. - CI: advisory dead-code job in security-pr.yml (continue-on-error, emits ::notice::/::warning:: annotations; not blocking yet). Resolves the three known live instances: drops the dead DEFAULT_MAX_TURNS import, removes the orphaned cdk/src/bootstrap/index.ts barrel (consumers import the submodules directly), and removes the unused LINEAR_SECRET_PREFIX export. Also clears the unused symbols the new rule surfaces across cdk/cli. The eslint half is blocking via the existing build check; knip+vulture stay advisory until the knip baseline is driven to zero (deferred AC).
) The ty typecheck step scanned .vulture_allowlist.py and failed with unresolved-reference on the bare hook_context names (the allowlist is bare-name expressions by design, not type-checkable Python). Same conflict already handled for ruff via extend-exclude; add the matching [tool.ty.src] exclude so `mise run build` passes.
…amples#282) Merging main (aws-samples#505 CLI dedupe) removed a dead symbol, dropping knip's count to 78. Lock in the gain so the ratchet holds at the lower number.
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #520 +/- ##
=======================================
Coverage ? 88.92%
=======================================
Files ? 222
Lines ? 52547
Branches ? 5440
=======================================
Hits ? 46730
Misses ? 5817
Partials ? 0 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Review
Overview
This PR closes the unused-symbol detection asymmetry between Python and TypeScript by adding three dead-code detection layers — @typescript-eslint/no-unused-vars (blocking, via the existing build check), knip + a count-only ratchet (advisory), and vulture for Python (advisory) — plus an advisory dead-code CI job. It also clears the symbols the new rules surface: dropping the dead DEFAULT_MAX_TURNS import, the orphaned bootstrap/index.ts barrel, the unused LINEAR_SECRET_PREFIX export, and a scattering of ^_-prefix renames.
The bulk of the diff is inert and safe: config, lockfile churn, and parameter/catch-variable renames. Two changes carry real logic, and one of them regresses a compile-time guard. Findings below, most-severe first.
Findings
1. cdk/src/handlers/shared/validation.ts:316 — refactor silently defeats the AttachmentType exhaustiveness check (CONFIRMED)
The change rewrote a typed assignment into a type assertion:
-const _attachmentExhaustiveCheck: _AssertAttachmentExhaustive = true;
+void (true as _AssertAttachmentExhaustive);These are not equivalent. The guard relies on _AssertAttachmentExhaustive resolving to never when AttachmentType gains a member missing from ATTACHMENT_TYPE_LIST:
- Old (assignment):
const x: never = trueis a hard compile error —trueis not assignable tonever. That error is the guard. - New (assertion):
true as neveris an assertion, and TypeScript always permitsX as Ywhen either type is assignable to the other.neveris assignable totrue, sotrue as nevernever errors.
Failure scenario: someone adds 'video' to AttachmentType without updating ATTACHMENT_TYPE_LIST. The old code fails the build; the new code compiles clean, and VALID_ATTACHMENT_TYPES silently omits the new type — the exact regression the assertion was meant to catch ships unnoticed.
Suggested fix — restore the assignment and consume the binding so it survives both noUnusedLocals (which this PR flips to true and which does not honor eslint's ^_ pattern) and @typescript-eslint/no-unused-vars:
/** Compile-time exhaustiveness check for AttachmentType. */
const ATTACHMENT_TYPE_LIST = ['image', 'file', 'url'] as const satisfies readonly AttachmentType[];
type _AssertAttachmentExhaustive = Exclude<AttachmentType, (typeof ATTACHMENT_TYPE_LIST)[number]> extends never ? true : never;
// The ASSIGNMENT is the guard: if AttachmentType gains a member missing from
// ATTACHMENT_TYPE_LIST, _AssertAttachmentExhaustive resolves to `never` and
// assigning `true` fails to compile. `void` consumes the binding so
// noUnusedLocals stays satisfied without weakening the check.
const _attachmentExhaustiveCheck: _AssertAttachmentExhaustive = true;
void _attachmentExhaustiveCheck;2. scripts/check-deadcode-ratchet.mjs:606 — classMembers is not a valid knip 6.x key; namespaceMembers is missing (low severity)
COUNTED_KEYS lists classMembers, which is not an issue key knip 6.x emits — it's always undefined, so it's silently dropped (harmless). Meanwhile the real member-issue key namespaceMembers is absent from the list, so dead namespace members never move the ratchet count, despite the script's comment claiming it counts "enum/class members." Separately, duplicates is emitted as an array-of-arrays (each inner array is one duplicate group), so issue.duplicates.length counts groups rather than individual duplicate symbols.
Impact is low because the ratchet step is continue-on-error (advisory), but it should be reconciled before the gate flips to blocking.
Suggested fix:
const COUNTED_KEYS = [
'dependencies',
'devDependencies',
'optionalPeerDependencies',
'unlisted',
'binaries',
'unresolved',
'exports',
'types',
'nsExports',
'nsTypes',
'duplicates',
'enumMembers',
'namespaceMembers', // was 'classMembers' — not a knip 6.x key
];Minor note (not a finding)
The PR description's test plan says the ratchet "holds at baseline (79)", but knip-baseline.json commits "count": 78. If the live count is 79, the gate emits a ::warning:: from day one — harmless while advisory, but reconcile the baseline before it becomes blocking.
Verdict
One change blocks: the validation.ts exhaustiveness regression (# 1) reintroduces exactly the class of silent drift this PR's tooling is meant to prevent, so it's worth fixing in-PR. The ratchet key fix (# 2) and the baseline note are low-severity cleanups that can ride along or follow before the gate goes blocking. Everything else — the eslint/tsconfig wiring, vulture setup, dead-symbol removals, and renames — looks correct and well-scoped.
…ws-samples#282) Addresses PR review (ayushtr-aws): 1. validation.ts: the `void (true as _AssertAttachmentExhaustive)` form silently defeated the AttachmentType exhaustiveness guard — a type assertion never errors, whereas the original ASSIGNMENT to a `never` type is the actual compile-time check. Restored the assignment and consume the binding with `void` so it survives noUnusedLocals and no-unused-vars without weakening the guard. Verified: tsc errors when a member is missing from ATTACHMENT_TYPE_LIST, compiles clean when exhaustive. 2. check-deadcode-ratchet.mjs: COUNTED_KEYS used non-existent knip 6.x keys (classMembers, nsExports, nsTypes) and counted unused files from a non-existent top-level report.files. Switched to the real keys (namespaceMembers, catalog) and count files from issues[].files[] so unused files actually move the ratchet. Count unchanged at 78.
|
Thanks for this — it's a clean, well-scoped implementation of the dead-code gate (#282), and the advisory-first rollout is exactly the right call. Approving with a few nits, none blocking — contingent on the fixes identified by the other reviewers in the comments above being addressed as well. What's solid:
Worth a look (non-blocking):
Everything else — docs sync, CLI/API type sync, bootstrap policies — is N/A for this diff. Nice work; happy to see this land once the above and the other reviewers' points are folded in. 🚀 |
|
Thanks for the thorough review @ayushtr-aws — all three addressed in 384efa8. #1 — const _attachmentExhaustiveCheck: _AssertAttachmentExhaustive = true;
void _attachmentExhaustiveCheck;Verified empirically: adding a #2 — ratchet Baseline note. Good catch on the description drift — the committed Everything stays advisory as designed; these were the correctness fixes needed before the gate can flip to blocking. |
… review nits) Non-blocking follow-ups from krokoko's review: - Pin knip exactly (6.20.0, drop caret) and make the ratchet fail loud instead of open: if a future knip reshapes its JSON so `issues` is not an array, countIssues now exits 2 rather than silently counting 0 and reporting a passing build (which would disable the gate). Needed before the gate flips to blocking. - COUNTED_KEYS comment: note the list is complete for the pinned knip and that nsExports/nsTypes/classMembers are not part of its schema. - validation.ts: attribute the `void` to tsconfig noUnusedLocals specifically (no-unused-vars already exempts the ^_ binding).
|
Thanks @krokoko! Folded in the non-blocking nits in 2a7555c (ayushtr-aws's three findings were already addressed in 384efa8):
Baseline is holding at 78 through the recent main merges. All checks green. Ready for another look whenever you have a moment. 🙏 |
… vulture Address PR aws-samples#520 review feedback: - Extract dead-code detection into dead-code-pr.yml (separate concern from security gate) - Use `uv run vulture` instead of `uvx vulture` for consistency with project conventions
Closes #282 (cairn MVG gate #6 — dead-code detection).
What this does
Closes the unused-symbol detection asymmetry: Python already fails on unused imports (ruff
F401), but unused TS symbols passed silently incdk/. Adds three detection layers plus advisory CI wiring.@typescript-eslint/no-unused-vars(error) incdk/+cli/flat configs with^_ignore patterns;cdk/tsconfig.jsonflipsnoUnusedLocals/noUnusedParameterstotruefor parity withcli/.knipv6 with a per-workspaceknip.json(cdk/cli/docs), plus a count-only ratchet (scripts/check-deadcode-ratchet.mjs+knip-baseline.json) that fails only when the count rises — the cairn "dead-code count should not increase" metric.vulture2.16 ([tool.vulture]inpyproject.toml,--min-confidence 80) with a minimal reviewed.vulture_allowlist.pyfor the three SDK-mandatedhook_contextparams.dead-codejob insecurity-pr.yml(continue-on-error, emits::notice::/::warning::annotations; not blocking yet).Resolves the three known live instances: drops the dead
DEFAULT_MAX_TURNSimport, removes the orphanedcdk/src/bootstrap/index.tsbarrel (consumers import the submodules directly), and removes the unusedLINEAR_SECRET_PREFIXexport. Also clears the unused symbols the new rule surfaces acrosscdk/cli.The eslint half is blocking via the existing
buildcheck; knip + vulture stay advisory until the knip baseline is driven to zero (deferred AC, per the issue's phased rollout).Acceptance criteria
@typescript-eslint/no-unused-varsas error incdk/cliflat configs with^_patterns; cdk tsconfig flags flipped totrueknipadded with per-workspaceknip.json(cdk/cli/docs);cli/src/index.tsnot flagged (entry-point caveat handled)vultureforagent/with reviewed allowlist +--min-confidence 80DEFAULT_MAX_TURNS, bootstrap barrel,LINEAR_SECRET_PREFIX)Test plan
Verified locally on a fresh checkout:
mise //cdk:build— compile, tests, eslint (no-unused-varsactive), synth all passmise //cli:build— compile, 565 tests, eslint all passyarn knip+node scripts/check-deadcode-ratchet.mjs— ratchet holds at baseline (78; ratcheted down from 79 after merging main chore: repo cleanup — roadmap removal, CLI dedupe, doc hygiene #505)uvx vulture(agent) — clean, exit 0mise //cdk:eslint,mise //cli:eslint— pass