You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
⚠️ 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. */constEXPORTED_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-44 — EXPORTED_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
Context
scripts/check-dead-exports.tsreports "every exported runtime symbol insourceRootswhose identifierappears nowhere in
referenceRootsoutside its own declaring file" (its own JSDoc, lines 73-79). It shippedunder #9852 after 87
src/**symbols turned out to be exported with no consumer at all.The detector is one regex:
The stated exclusion is types and interfaces, with a stated reason: they cost nothing at runtime. That reason
does not apply to
classorenum— 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), includingRateLimiter(src/auth/rate-limit.ts),SubmissionLock(src/queue/submission-lock.ts),LoopoverMcp(
src/mcp/server.ts), the threeProjectTrackerAdapterimplementations, and fourErrorsubclasses. None ofthem 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/enumagainstmainreports noviolations, so closing the gap is a mechanical, non-behavioural change today; it only starts guarding from
here on.
Requirements
EXPORTED_RUNTIME_SYMBOLinscripts/check-dead-exports.tsmust additionally matchexport class NAME,export abstract class NAMEandexport enum NAMEat the start of a line, capturingNAMEthe same way.const,function,classandenumas in-scope and keep the existing "types and interfaces are out ofscope, an unused type costs nothing at runtime" rationale for what remains excluded.
internalUses-based fix hint atscripts/check-dead-exports.ts:140(> 1-> "drop theexportkeyword", otherwise "delete it") must keep working unchanged for the new symbol kinds.
findDeadExports's reference counting, itsALLOWED_EXPORTShandling, theEXCLUDED_SEGMENTfilter and theSOURCE_PATTERN.d.tsexclusion must NOT change.ALLOWED_EXPORTSby this PR. If the widened regex reports a violation on thecurrent 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:checkmust exit 0 on the resulting tree.Deliverables
EXPORTED_RUNTIME_SYMBOLmatches all four kinds; the JSDoc above it names exactly what is in and out ofscope.
findDeadExports({ sourceRoots: ["src"], referenceRoots: ["src"], listFiles, readFile })with aninjected single file
src/x.tswhose content isexport class Orphan {}\nconst y = new Orphan();\nreturns
[{ file: "src/x.ts", symbol: "Orphan", internalUses: 2 }], asserted intest/unit/check-dead-exports-script.test.ts.export enum Kind { A }and no other reference returns a violation whosesymbolis
Kind, asserted in the same file.export class Used {}insrc/x.tsandnew Used()in an injectedsrc/y.tsreturns
[], asserted in the same file.npm run dead-exports:checkexits 0 on the resulting tree, with no newALLOWED_EXPORTSentry.test/unit/check-dead-exports-script.test.tsnamed for this bug asserting thatexport abstract classis matched too (theabstractmodifier 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
classbut notenum, or that adds anALLOWED_EXPORTSentry to make the checkgreen — does not resolve this issue.
Test Coverage Requirements
This repo enforces 99%+ Codecov patch coverage, branch-counted.
vitest.config.ts'scoverage.includecovers
src/**/*.ts,packages/loopover-engine/src/**/*.ts,packages/loopover-{miner,mcp}/{lib,bin}/**/*.ts,packages/loopover-contract/src/**/*.tsandpackages/discovery-index/src/**/*.ts. It does not coverscripts/**, andcodecov.yml'signore:list namesscripts/**explicitly — Codecov does not gate thepatch on this change.
The tests are still mandatory and still gating: the root vitest suite (
include: ["test/**/*.test.ts"]) runstest/unit/check-dead-exports-script.test.tson 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:
classmatched anda non-exported
class Foo {}NOT matched;abstract classmatched;enummatched andconst enumhandledconsistently with whatever the regex is written to do (state it in the test name);
type/interfacestill NOTmatched. The pre-existing
const/function/async functioncases must keep their assertions. Theexternal > 0early-break and theinternalUses > 1fix-hint branch must each be exercised for aclass-kind symbol as well as the existing kinds.
Expected Outcome
npm run dead-exports:checkgrades exported classes and enums the same way it already grades exportedconsts 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-44—EXPORTED_RUNTIME_SYMBOLand its scope commentscripts/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 countingscripts/check-dead-exports.ts:140— theinternalUses-based fix hinttest/unit/check-dead-exports-script.test.ts— the existing test file