Skip to content

scripts(checks): check-dead-exports never sees export class/export enum, so 12 exported runtime symbols are ungraded #10047

Description

@JSONbored

⚠️ Definition of Done: this issue must be completed in full, in a single PR. Do not split this
work across multiple PRs, and do not defer any Deliverable below to a follow-up issue. A PR that
satisfies only some of the Deliverables, stubs a required test, or leaves a checkbox
partially-done does NOT resolve this issue and will be closed.

Context

scripts/check-dead-exports.ts reports "every exported runtime symbol in sourceRoots whose identifier
appears nowhere in referenceRoots outside its own declaring file" (its own JSDoc, lines 73-79). It shipped
under #9852 after 87 src/** symbols turned out to be exported with no consumer at all.

The detector is one regex:

// scripts/check-dead-exports.ts:41-44
/** `export const NAME` / `export function NAME` / `export async function NAME` at the top level. Types and
 *  interfaces are deliberately out of scope: an unused type costs nothing at runtime and TypeScript's own
 *  `noUnusedLocals` already covers the local case. */
const EXPORTED_RUNTIME_SYMBOL = /^export (?:async )?(?:const|function) ([A-Za-z_][A-Za-z0-9_]*)/gm;

The stated exclusion is types and interfaces, with a stated reason: they cost nothing at runtime. That reason
does not apply to class or enum — both are runtime values, both are emitted, both have to be read,
typechecked and maintained — yet neither is matched, and neither is mentioned as an exclusion.

src/** has 12 exported classes today (grep -rn "^export class " src --include="*.ts" | wc -l), including
RateLimiter (src/auth/rate-limit.ts), SubmissionLock (src/queue/submission-lock.ts), LoopoverMcp
(src/mcp/server.ts), the three ProjectTrackerAdapter implementations, and four Error subclasses. None of
them is graded by this check: a class exported for a consumer that never got wired up — #9852's exact
scenario, one keyword over — passes silently, and its own test keeps coverage green while it contributes
nothing.

Running the existing algorithm with the regex extended to class/enum against main reports no
violations, so closing the gap is a mechanical, non-behavioural change today; it only starts guarding from
here on.

Requirements

  • EXPORTED_RUNTIME_SYMBOL in scripts/check-dead-exports.ts must additionally match export class NAME,
    export abstract class NAME and export enum NAME at the start of a line, capturing NAME the same way.
  • The JSDoc immediately above the constant must be updated so the stated scope matches the code: it must name
    const, function, class and enum as in-scope and keep the existing "types and interfaces are out of
    scope, an unused type costs nothing at runtime" rationale for what remains excluded.
  • The internalUses-based fix hint at scripts/check-dead-exports.ts:140 (> 1 -> "drop the export
    keyword", otherwise "delete it") must keep working unchanged for the new symbol kinds.
  • findDeadExports's reference counting, its ALLOWED_EXPORTS handling, the EXCLUDED_SEGMENT filter and the
    SOURCE_PATTERN .d.ts exclusion must NOT change.
  • No entry may be added to ALLOWED_EXPORTS by this PR. If the widened regex reports a violation on the
    current tree, the correct resolution is to fix the export (drop the keyword or delete the symbol), not to
    allowlist it — the allowlist's contract is "legitimately unreferenced in-repo, with a reason".
  • npm run dead-exports:check must exit 0 on the resulting tree.

⚠️ Required pattern: extend the single existing EXPORTED_RUNTIME_SYMBOL regex in place — that constant is
the whole detector. What does NOT satisfy this issue: (a) adding a second regex and a second scan pass
alongside it; (b) replacing the textual scan with a TypeScript program, which the file's header comment
("DELIBERATELY TEXTUAL, matching its sibling") explicitly rejects; (c) also matching export type /
export interface, which reverses a deliberate, documented decision; (d) matching export default or
export { ... } re-export lists, whose fix ("drop the export keyword") does not apply and which are out of
scope here; (e) a test-only PR that asserts the desired behaviour without changing the regex.

Deliverables

  • EXPORTED_RUNTIME_SYMBOL matches all four kinds; the JSDoc above it names exactly what is in and out of
    scope.
  • findDeadExports({ sourceRoots: ["src"], referenceRoots: ["src"], listFiles, readFile }) with an
    injected single file src/x.ts whose content is export class Orphan {}\nconst y = new Orphan();\n
    returns [{ file: "src/x.ts", symbol: "Orphan", internalUses: 2 }], asserted in
    test/unit/check-dead-exports-script.test.ts.
  • The same call with export enum Kind { A } and no other reference returns a violation whose symbol
    is Kind, asserted in the same file.
  • The same call with export class Used {} in src/x.ts and new Used() in an injected src/y.ts
    returns [], asserted in the same file.
  • npm run dead-exports:check exits 0 on the resulting tree, with no new ALLOWED_EXPORTS entry.
  • A regression test at test/unit/check-dead-exports-script.test.ts named for this bug asserting that
    export abstract class is matched too (the abstract modifier must not defeat the anchor).

All Deliverables above are required in a single PR. A PR that satisfies only some of them — for example one
that widens the regex to class but not enum, or that adds an ALLOWED_EXPORTS entry to make the check
green — does not resolve this issue.

Test Coverage Requirements

This repo enforces 99%+ Codecov patch coverage, branch-counted. vitest.config.ts's coverage.include
covers src/**/*.ts, packages/loopover-engine/src/**/*.ts, packages/loopover-{miner,mcp}/{lib,bin}/**/*.ts,
packages/loopover-contract/src/**/*.ts and packages/discovery-index/src/**/*.ts. It does not cover
scripts/**, and codecov.yml's ignore: list names scripts/** explicitly — Codecov does not gate the
patch on this change.

The tests are still mandatory and still gating: the root vitest suite (include: ["test/**/*.test.ts"]) runs
test/unit/check-dead-exports-script.test.ts on every PR and a failure there is a red required check.

Both arms of every alternative the widened regex introduces must be asserted in that file: class matched and
a non-exported class Foo {} NOT matched; abstract class matched; enum matched and const enum handled
consistently with whatever the regex is written to do (state it in the test name); type/interface still NOT
matched. The pre-existing const/function/async function cases must keep their assertions. The
external > 0 early-break and the internalUses > 1 fix-hint branch must each be exercised for a
class-kind symbol as well as the existing kinds.

Expected Outcome

npm run dead-exports:check grades exported classes and enums the same way it already grades exported
consts and functions, so a class exported for a consumer that never got wired up fails CI instead of looking
like a guard while guarding nothing — closing the gap the check's own JSDoc already claims is closed.

Links & Resources

  • scripts/check-dead-exports.ts:41-44EXPORTED_RUNTIME_SYMBOL and its scope comment
  • scripts/check-dead-exports.ts:73-79 — the JSDoc claiming "every exported runtime symbol"
  • scripts/check-dead-exports.ts:113-127 — the scan loop and reference counting
  • scripts/check-dead-exports.ts:140 — the internalUses-based fix hint
  • test/unit/check-dead-exports-script.test.ts — the existing test file
  • ci: nothing detects a dead EXPORT — 87 src/ symbols have no consumer outside their own file #9852 — the issue this checker shipped under

Metadata

Metadata

Assignees

No one assigned

    Labels

    gittensor:bugGittensor-scored bug fix — scores a 0.05x multiplier.help wantedExtra attention is needed

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions