From 51a4852e5fa294773842d929c7fd27a420f267bf Mon Sep 17 00:00:00 2001 From: JSONbored <49853598+JSONbored@users.noreply.github.com> Date: Wed, 29 Jul 2026 07:23:39 -0700 Subject: [PATCH] ci: detect a dead EXPORT, and make the 84 existing ones honest (#9852) `check-dead-source-files.ts` catches a dead FILE and its own header names what it does not catch: "no detection of a file that's imported but whose EXPORTS are all unused". That gap was populated -- 84 `src/**` symbols were exported and referenced nowhere outside the file declaring them, not by another module, not by a script, not even by their own test. Each was one of three things and nothing told them apart: a missing wire-up (#9492's class one level down), a safety net with nothing behind it (#9851 exactly -- two route-spec tables exported "for the meta-test", test never written), or dead surface that still has to be read and maintained. Coverage cannot catch any of them, because the declaring file's own tests exercise the symbol directly and report green while it contributes nothing to the running system. The fix is almost never deletion. 82 of the 84 are used INSIDE their file, so only the `export` keyword was untrue -- dropping it makes the surface honest, and tsc proves the safety of every one (if any had really been reachable from elsewhere, the build would say so; it is clean). The two exceptions, each decided by reading rather than by rule: - `DEFAULT_REVIEW_CONFIDENCE` is deleted. Its doc claims it is "shared by the parser and the combiners so the fallback is identical everywhere" -- nothing references it, and the constant declared directly below it (#8833) replaced the fallback it describes. Stale, and its comment actively misleads. - `impactMapQueryCache` is allowlisted with the reason: check-schema-drift.ts PARSES this file's sqliteTable declarations rather than importing the symbol, and the table's reads are raw SQL, so deleting the declaration would blind that check to the table. The check is wired into test:ci and mirrors its sibling's conventions exactly: pure over injectable seams, and an exception must state WHY rather than being a bare name on a list. --- package.json | 3 +- scripts/check-dead-exports.ts | 147 ++++++++++++++++++ src/api/proof-badge.ts | 2 +- src/auth/security.ts | 4 +- src/db/repositories.ts | 6 +- src/github/backfill.ts | 4 +- src/github/commands.ts | 2 +- src/github/webhook.ts | 2 +- src/notifications/ams-events.ts | 2 +- src/notifications/service.ts | 8 +- src/notifications/stranded-delivery-sweep.ts | 4 +- src/openapi/schemas.ts | 6 +- src/orb/apr-repo-transfer.ts | 2 +- src/queue/patchless-secret-scan.ts | 2 +- src/queue/processors.ts | 2 +- src/review/ai-slop-cache-input.ts | 2 +- src/review/ams-reputation-bridge.ts | 2 +- src/review/auto-apply.ts | 2 +- src/review/content-lane/orchestrator.ts | 2 +- src/review/content-lane/registry-logic.ts | 4 +- src/review/decision-audit.ts | 2 +- src/review/decision-record.ts | 2 +- .../linked-issue-satisfaction-cache-input.ts | 2 +- src/review/pending-closure-watchdog.ts | 4 +- src/review/rag.ts | 2 +- src/review/repo-culture-profile.ts | 4 +- src/review/review-diff.ts | 2 +- src/review/review-eligibility.ts | 2 +- src/review/risk-control-wire.ts | 2 +- src/review/rule-repeat-alarm-wire.ts | 2 +- src/review/stats.ts | 2 +- src/review/unified-comment.ts | 4 +- src/review/visual/actions-fallback.ts | 2 +- src/review/visual/screenshot-table-vision.ts | 2 +- src/review/visual/shot-render-token.ts | 2 +- src/review/visual/shot.ts | 6 +- src/scenarios/input-model.ts | 10 +- src/selfhost/redaction-scrub.ts | 18 +-- src/services/ai-review.ts | 7 +- src/services/contributor-evidence-graph.ts | 2 +- src/services/contributor-issue-draft.ts | 2 +- src/services/decision-pack.ts | 2 +- src/services/issue-plan-draft.ts | 4 +- src/services/linked-issue-satisfaction.ts | 2 +- src/services/mcp-compatibility.ts | 2 +- src/services/notify-pagerduty.ts | 4 +- src/services/public-quality-metrics.ts | 4 +- src/services/queue-trends.ts | 2 +- src/services/recommendation-outcomes.ts | 4 +- src/settings/agent-execution.ts | 2 +- src/signals/settings-preview.ts | 4 +- test/unit/check-dead-exports-script.test.ts | 83 ++++++++++ 52 files changed, 314 insertions(+), 88 deletions(-) create mode 100644 scripts/check-dead-exports.ts create mode 100644 test/unit/check-dead-exports-script.test.ts diff --git a/package.json b/package.json index d07cc66b20..874759237d 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "ui-derived-types:check": "node --experimental-strip-types scripts/check-ui-derived-types.ts", "server-manifest:check": "node --experimental-strip-types scripts/check-server-manifest.ts", "dead-source-files:check": "node --experimental-strip-types scripts/check-dead-source-files.ts", + "dead-exports:check": "node --experimental-strip-types scripts/check-dead-exports.ts", "regate-sort-key:check": "node --experimental-strip-types scripts/check-regate-sort-key.ts", "command-redelivery-guards:check": "node --experimental-strip-types scripts/check-command-redelivery-guards.ts", "dispatch-gate-reasons:check": "node --experimental-strip-types scripts/check-dispatch-gate-reasons.ts", @@ -138,7 +139,7 @@ "test:smoke:browser:install": "playwright install chromium", "test:smoke:browser": "node --experimental-strip-types scripts/smoke-ui-browser.ts", "pretest:ci": "npm run check-node-version", - "test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:migrations:immutable:check && npm run workspace-dep-ranges:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run build:mcp && npm run build:miner && npm run build --workspace @loopover/ui-kit && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run validate:mcp && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:contract-pack && npm run test:miner-pack && npm run test:engine-pack && npm run test:ui-kit-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run control-plane:contract:check && npm run control-plane:openapi:check && npm run ui:version-audit && npm run docs:drift-check && npm run coverage-boltons:check && npm run import-specifiers:check && npm run ui-derived-types:check && npm run server-manifest:check && npm run dead-source-files:check && npm run publishable-deps:check && npm run regate-sort-key:check && npm run command-redelivery-guards:check && npm run dispatch-gate-reasons:check && npm run validate:no-hand-written-js && npm run replay-runner-manifest:check && npm run coco-dev-versions:check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run release-linked-versions:check && npm run command-reference:check && npm run mcp:tool-reference:check && npm run mcp:client-config:check && npm run contract:api-schemas:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build", + "test:ci": "git diff --check && npm run actionlint && npm run lint:composite-actions && npm run db:migrations:check && npm run db:migrations:immutable:check && npm run workspace-dep-ranges:check && npm run db:schema-drift:check && npm run selfhost:env-reference:check && npm run miner:env-reference:check && npm run selfhost:validate-observability && npm run cf-typegen:check && npm run build --workspace @loopover/engine && npm run build --workspace @loopover/discovery-index && npm run build:mcp && npm run build:miner && npm run build --workspace @loopover/ui-kit && npm run typecheck && npm run test:coverage && npm run test:engine-parity && npm run test:live-gate-parity && npm run test:driver-parity && npm run validate:mcp && npm run test --workspace @loopover/engine && npm run test:workers && npm run test:mcp-pack && npm run test:contract-pack && npm run test:miner-pack && npm run test:engine-pack && npm run test:ui-kit-pack && npm run test:miner-deployment-docs-audit && npm run rees:test && npm run ui:openapi:check && npm run control-plane:contract:check && npm run control-plane:openapi:check && npm run ui:version-audit && npm run docs:drift-check && npm run coverage-boltons:check && npm run import-specifiers:check && npm run ui-derived-types:check && npm run server-manifest:check && npm run dead-source-files:check && npm run dead-exports:check && npm run publishable-deps:check && npm run regate-sort-key:check && npm run command-redelivery-guards:check && npm run dispatch-gate-reasons:check && npm run validate:no-hand-written-js && npm run replay-runner-manifest:check && npm run coco-dev-versions:check && npm run branding-drift:check && npm run manifest:drift-check && npm run engine-parity:drift-check && npm run engines-nvmrc:check && npm run release-manifest:sync:check && npm run release-linked-versions:check && npm run command-reference:check && npm run mcp:tool-reference:check && npm run mcp:client-config:check && npm run contract:api-schemas:check && npm run ui:lint && npm run ui:typecheck && npm run ui:test && npm run ui:build", "test:release": "npm run test:ci && npm run changelog:check", "test:release:mcp": "npm run test:ci", "test:watch": "vitest", diff --git a/scripts/check-dead-exports.ts b/scripts/check-dead-exports.ts new file mode 100644 index 0000000000..84ac4c37ac --- /dev/null +++ b/scripts/check-dead-exports.ts @@ -0,0 +1,147 @@ +#!/usr/bin/env node +// Guards against a silently dead EXPORT (#9852) — the gap check-dead-source-files.ts names in its own header: +// +// > DELIBERATELY NARROW. This is not a general dead-code/knip-style analysis (no re-export chasing, no +// > detection of a file that's imported but whose EXPORTS are all unused) +// +// That gap was populated: 87 `src/**` symbols were exported and referenced nowhere outside the file that +// declared them — not by another module, not by a script, not even by their own test. Each one is one of +// three things, and nothing told them apart: +// +// 1. A missing wire-up — #9492's class one level down. An export with no caller is a feature that was +// built and never connected. +// 2. A safety net with nothing behind it — #9851 exactly: two route-spec tables exported "for the +// meta-test that asserts every entry's declared auth matches the middleware", test never written. +// 3. Genuinely dead surface that still has to be read, typechecked and maintained. +// +// Coverage cannot catch any of them: the declaring file's own tests exercise the symbol directly and report +// green while it contributes nothing to the running system. +// +// THE FIX IS USUALLY NOT DELETION. Most flagged symbols are used inside their own file — the export keyword +// is the only untrue part. Dropping `export` makes the surface honest and is provably safe (tsc fails if the +// symbol really was reached from elsewhere). Deletion is for a symbol with no uses at all. +// +// DELIBERATELY TEXTUAL, matching its sibling: identifier occurrences, not a TypeScript program. A symbol +// reached only through a namespace import (`import * as m`) or a dynamic string would be a false positive — +// which is what ALLOWED_EXPORTS is for, and why it demands a reason rather than a bare name. +import { readFileSync, readdirSync } from "node:fs"; +import { fileURLToPath } from "node:url"; + +/** Where exports are CHECKED. Scoped to the Worker's own source; the published packages are a separate + * problem (their exports are consumed outside this repo, so absence of an in-repo reference proves + * nothing). */ +const SOURCE_ROOTS = ["src"] as const; + +/** Where a reference COUNTS from. Everything that could legitimately consume `src/**`. */ +const REFERENCE_ROOTS = ["src", "packages", "test", "scripts", "apps/loopover-ui/src"] as const; + +const SOURCE_PATTERN = /(? = new Map([ + [ + "src/db/schema.ts:impactMapQueryCache", + "Consumed by scripts/check-schema-drift.ts, which PARSES this file's sqliteTable declarations rather than importing the symbol — the table's reads/writes are raw SQL (`FROM impact_map_query_cache`). Deleting the declaration would blind the drift check to the table.", + ], +]); + +function defaultListFiles(root: string, pattern: RegExp): string[] { + try { + return readdirSync(root, { recursive: true }) + .map(String) + .filter((entry) => pattern.test(entry) && !EXCLUDED_SEGMENT.test(entry)) + .map((entry) => `${root}/${entry}`); + } catch { + return []; + } +} + +export type DeadExportViolation = { file: string; symbol: string; internalUses: number }; + +/** + * Pure over its inputs: reports every exported runtime symbol in `sourceRoots` whose identifier appears + * nowhere in `referenceRoots` outside its own declaring file. + * + * `internalUses` is carried through because it decides the fix: greater than one means the symbol is used + * inside its file and only the `export` keyword is wrong; one means the declaration is the sole occurrence + * and the symbol is dead outright. + */ +export function findDeadExports( + options: { + sourceRoots?: readonly string[]; + referenceRoots?: readonly string[]; + allowedExports?: ReadonlyMap; + listFiles?: (root: string, pattern: RegExp) => string[]; + readFile?: (file: string) => string; + } = {}, +): DeadExportViolation[] { + const { + sourceRoots = SOURCE_ROOTS, + referenceRoots = REFERENCE_ROOTS, + allowedExports = ALLOWED_EXPORTS, + listFiles = defaultListFiles, + readFile = (file: string) => readFileSync(file, "utf8"), + } = options; + + const referenceFiles = [...new Set(referenceRoots.flatMap((root) => listFiles(root, SOURCE_PATTERN)))]; + const contents = new Map(); + for (const file of referenceFiles) { + try { + contents.set(file, readFile(file)); + } catch { + // A file that vanished between listing and reading contributes no references; skip it rather than + // failing the whole check on a race. + } + } + + const violations: DeadExportViolation[] = []; + for (const file of sourceRoots.flatMap((root) => listFiles(root, SOURCE_PATTERN))) { + const own = contents.get(file); + if (own === undefined) continue; + for (const match of own.matchAll(EXPORTED_RUNTIME_SYMBOL)) { + const symbol = match[1]; + if (symbol === undefined || allowedExports.has(`${file}:${symbol}`)) continue; + const pattern = new RegExp(`\\b${symbol}\\b`, "g"); + let external = 0; + let internalUses = 0; + for (const [candidate, text] of contents) { + const hits = text.match(pattern)?.length ?? 0; + if (hits === 0) continue; + if (candidate === file) internalUses = hits; + else external += hits; + if (external > 0) break; + } + if (external === 0) violations.push({ file, symbol, internalUses }); + } + } + return violations.sort((a, b) => a.file.localeCompare(b.file) || a.symbol.localeCompare(b.symbol)); +} + +function main(): void { + const violations = findDeadExports(); + if (violations.length === 0) { + process.stdout.write("check-dead-exports: no exported symbol is unreferenced outside its own file.\n"); + return; + } + process.stderr.write(`check-dead-exports found ${violations.length} exported symbol(s) with no reference outside their own file:\n`); + for (const { file, symbol, internalUses } of violations) { + const fix = internalUses > 1 ? "used internally — drop the `export` keyword" : "no uses at all — delete it, or wire up the consumer it was written for"; + process.stderr.write(` ${file}: ${symbol} (${fix})\n`); + } + process.stderr.write("\nIf an export is legitimately unreferenced in-repo, add it to ALLOWED_EXPORTS with the reason.\n"); + process.exit(1); +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) main(); diff --git a/src/api/proof-badge.ts b/src/api/proof-badge.ts index 549712bfac..817a041746 100644 --- a/src/api/proof-badge.ts +++ b/src/api/proof-badge.ts @@ -4,7 +4,7 @@ import { escapeXml } from "./badge"; import { buildProofBadgeColor, buildProofBadgeMessage, type ProofSummary } from "../review/proof-summary"; -export const PROOF_BADGE_LABEL = "loopover proof"; +const PROOF_BADGE_LABEL = "loopover proof"; const UNAVAILABLE_COLOR = "#9e9e9e"; /** `null` renders the neutral unavailable badge — used for both the flag-off 404 and the error 503, since diff --git a/src/auth/security.ts b/src/auth/security.ts index 709ddcfb2b..b8e7af3bde 100644 --- a/src/auth/security.ts +++ b/src/auth/security.ts @@ -18,8 +18,8 @@ export type AuthIdentity = | { kind: "static"; actor: "api" | "mcp" | "mcp-admin" | "internal" } | { kind: "session"; actor: string; session: AuthSessionRecord }; -export const SESSION_TTL_SECONDS = 7 * 24 * 60 * 60; -export const BROWSER_SESSION_COOKIE = "loopover_session"; +const SESSION_TTL_SECONDS = 7 * 24 * 60 * 60; +const BROWSER_SESSION_COOKIE = "loopover_session"; export const GITHUB_OAUTH_STATE_COOKIE = "loopover_oauth_state"; export const GITHUB_OAUTH_STATE_TTL_SECONDS = 10 * 60; diff --git a/src/db/repositories.ts b/src/db/repositories.ts index e217eea487..075b521182 100644 --- a/src/db/repositories.ts +++ b/src/db/repositories.ts @@ -1892,7 +1892,7 @@ export async function persistUpstreamSourceSnapshots(env: Env, snapshots: Upstre } } -export async function listLatestUpstreamSourceSnapshots(env: Env, limit = 20): Promise { +async function listLatestUpstreamSourceSnapshots(env: Env, limit = 20): Promise { const db = getDb(env.DB); const rows = await db.select().from(upstreamSourceSnapshots).orderBy(desc(upstreamSourceSnapshots.fetchedAt)).limit(limit); return rows.map(toUpstreamSourceSnapshotRecord); @@ -3090,7 +3090,7 @@ export async function hasRecentAuditEvent(env: Env, actor: string, eventType: st return rows.length > 0; } -export async function hasRecentAuditEventForOtherTarget(env: Env, actor: string, eventType: string, currentTargetKey: string, sinceIso: string): Promise { +async function hasRecentAuditEventForOtherTarget(env: Env, actor: string, eventType: string, currentTargetKey: string, sinceIso: string): Promise { const db = getDb(env.DB); const rows = await db .select({ id: auditEvents.id }) @@ -6363,7 +6363,7 @@ export async function upsertAgentRecommendationOutcome(env: Env, outcome: AgentR return (await getAgentRecommendationOutcome(env, outcome.actionId))!; } -export async function getAgentRecommendationOutcome(env: Env, actionId: string): Promise { +async function getAgentRecommendationOutcome(env: Env, actionId: string): Promise { const [row] = await getDb(env.DB).select().from(agentRecommendationOutcomes).where(eq(agentRecommendationOutcomes.actionId, actionId)).limit(1); return row ? toAgentRecommendationOutcomeRecord(row) : null; } diff --git a/src/github/backfill.ts b/src/github/backfill.ts index 89d991d2a6..e60b0dab31 100644 --- a/src/github/backfill.ts +++ b/src/github/backfill.ts @@ -910,9 +910,9 @@ export const REQUIRED_INSTALLATION_EVENTS = [ * `status` in particular matters more than its optional standing suggests: codecov/patch is a commit status, * and it is the gate's hardest required check. */ -export const DIAGNOSED_INSTALLATION_EVENTS = ["pull_request_review_thread", "status", "workflow_run", "deployment_status", "push"] as const; +const DIAGNOSED_INSTALLATION_EVENTS = ["pull_request_review_thread", "status", "workflow_run", "deployment_status", "push"] as const; -export const OPTIONAL_VISIBLE_INSTALLATION_EVENTS = ["installation_target", "installation_repositories"] as const; +const OPTIONAL_VISIBLE_INSTALLATION_EVENTS = ["installation_target", "installation_repositories"] as const; /** * The complete event set a new App should subscribe to: everything required, plus everything diagnosed. diff --git a/src/github/commands.ts b/src/github/commands.ts index ba1e843736..0dd3f63f81 100644 --- a/src/github/commands.ts +++ b/src/github/commands.ts @@ -405,7 +405,7 @@ export function isAuthorizedCommandActor(args: { /** Fixed, non-LLM disclaimer stamped on every `@loopover chat` answer card (#4595 req 9). Deliberately NOT run * through neutralizePublicMarkdownText so the `@loopover review` code span renders; it carries no forbidden * terms, so the whole-body sanitizePublicComment pass leaves it byte-for-byte intact. */ -export const CHAT_QA_DISCLAIMER = +const CHAT_QA_DISCLAIMER = "Read-only informational reply — cannot change review outcomes, gate state, or trigger a re-review. To retrigger a review, comment `@loopover review`."; export function buildPublicAgentCommandComment(args: { diff --git a/src/github/webhook.ts b/src/github/webhook.ts index 18209075e6..c89c3c97b9 100644 --- a/src/github/webhook.ts +++ b/src/github/webhook.ts @@ -123,7 +123,7 @@ export async function handleGitHubWebhook(c: Context<{ Bindings: Env }>): Promis /** Shared post-verification path: parse → dedup → record → enqueue to the WEBHOOKS lane → 202. Used by the GitHub * webhook receiver above AND the Orb relay receiver below (they verify the body differently — GitHub's HMAC vs the * Orb relay HMAC — then share everything after). */ -export async function enqueueVerifiedWebhook(c: Context<{ Bindings: Env }>, deliveryId: string, eventName: string, rawBody: string): Promise { +async function enqueueVerifiedWebhook(c: Context<{ Bindings: Env }>, deliveryId: string, eventName: string, rawBody: string): Promise { const result = await enqueueWebhookByEnv(c.env, deliveryId, eventName, rawBody, getSelfHostRequestTraceParent(c.req.raw)); switch (result) { case "review_unavailable": diff --git a/src/notifications/ams-events.ts b/src/notifications/ams-events.ts index 536f0feff8..4d7744c2d8 100644 --- a/src/notifications/ams-events.ts +++ b/src/notifications/ams-events.ts @@ -5,7 +5,7 @@ import type { DetectedNotificationEvent, NotificationEventType } from "../types"; import { nowIso } from "../utils/json"; -export const AMS_NOTIFICATION_EVENT_TYPES = [ +const AMS_NOTIFICATION_EVENT_TYPES = [ "ams_attempt_started", "ams_attempt_failed", "ams_governor_paused", diff --git a/src/notifications/service.ts b/src/notifications/service.ts index 58d4cad800..e502652df5 100644 --- a/src/notifications/service.ts +++ b/src/notifications/service.ts @@ -43,7 +43,7 @@ export function buildChangesRequestedNotification(event: DetectedNotificationEve // Post-merge self-attribution (#702): the miner's OWN outcome record for a merged PR. Public-safe — frames // what merged work does for the contributor's standing, never raw reward $/trust/score. -export function buildMergedOutcomeNotification(event: DetectedNotificationEvent): { title: string; body: string } { +function buildMergedOutcomeNotification(event: DetectedNotificationEvent): { title: string; body: string } { const ref = `${event.repoFullName}#${event.pullNumber}`; return { title: sanitizePublicComment(`Merged: ${ref}`), @@ -62,7 +62,7 @@ export function buildIssueWatchNotification(event: DetectedNotificationEvent): { } // AMS (#7657): attempt lifecycle — `pullNumber` carries the ISSUE number. Public-safe; no reward/trust figures. -export function buildAmsAttemptStartedNotification(event: DetectedNotificationEvent): { title: string; body: string } { +function buildAmsAttemptStartedNotification(event: DetectedNotificationEvent): { title: string; body: string } { const ref = `${event.repoFullName}#${event.pullNumber}`; return { title: sanitizePublicComment(`Attempt started on ${ref}`), @@ -70,7 +70,7 @@ export function buildAmsAttemptStartedNotification(event: DetectedNotificationEv }; } -export function buildAmsAttemptFailedNotification(event: DetectedNotificationEvent): { title: string; body: string } { +function buildAmsAttemptFailedNotification(event: DetectedNotificationEvent): { title: string; body: string } { const ref = `${event.repoFullName}#${event.pullNumber}`; return { title: sanitizePublicComment(`Attempt failed on ${ref}`), @@ -78,7 +78,7 @@ export function buildAmsAttemptFailedNotification(event: DetectedNotificationEve }; } -export function buildAmsGovernorPausedNotification(_event: DetectedNotificationEvent): { title: string; body: string } { +function buildAmsGovernorPausedNotification(_event: DetectedNotificationEvent): { title: string; body: string } { return { title: sanitizePublicComment("AMS governor paused"), body: sanitizePublicComment( diff --git a/src/notifications/stranded-delivery-sweep.ts b/src/notifications/stranded-delivery-sweep.ts index 0d51de5e6c..e87c5ed509 100644 --- a/src/notifications/stranded-delivery-sweep.ts +++ b/src/notifications/stranded-delivery-sweep.ts @@ -19,7 +19,7 @@ import { errorMessage } from "../utils/json"; * re-enqueued, rather than relying solely on that downstream no-op. */ -export const STRANDED_NOTIFICATION_REQUEUED_EVENT = "notification.delivery.requeued"; +const STRANDED_NOTIFICATION_REQUEUED_EVENT = "notification.delivery.requeued"; /** Grace past a row's creation before the sweep steps in, so a delivery merely waiting its turn behind a busy * queue is never mistaken for a lost one. */ @@ -34,7 +34,7 @@ export const STRANDED_NOTIFICATION_LOOKBACK_MS = 3 * 24 * 60 * 60 * 1000; export const STRANDED_NOTIFICATION_REQUEUE_INTERVAL_MS = 60 * 60 * 1000; /** Bounded rows-per-tick, so one sweep can never fan out an unbounded number of re-enqueues. */ -export const STRANDED_NOTIFICATION_SCAN_LIMIT = 500; +const STRANDED_NOTIFICATION_SCAN_LIMIT = 500; export type StrandedNotificationSweepResult = { scanned: number; requeued: number }; diff --git a/src/openapi/schemas.ts b/src/openapi/schemas.ts index b501322764..46db716376 100644 --- a/src/openapi/schemas.ts +++ b/src/openapi/schemas.ts @@ -1404,7 +1404,7 @@ export const SignalFidelitySchema = z }) .openapi("SignalFidelity"); -export const CoreSignalFidelitySchema = z +const CoreSignalFidelitySchema = z .object({ status: z.enum(["complete", "degraded", "blocked", "unknown"]), repoCount: z.number(), @@ -1418,7 +1418,7 @@ export const CoreSignalFidelitySchema = z }) .openapi("CoreSignalFidelity"); -export const RepoGithubTotalsSnapshotSchema = z +const RepoGithubTotalsSnapshotSchema = z .object({ id: z.string(), repoFullName: z.string(), @@ -1550,7 +1550,7 @@ const RegistryHyperparameterDriftFieldSchema = z.enum([ const RegistryDriftSurfaceSchema = z.enum(["allocation", "lane_fit", "scoreability_assumptions", "maintainer_economics", "issue_discovery_behavior", "label_policy"]); -export const RegistryHyperparameterDriftSummarySchema = z +const RegistryHyperparameterDriftSummarySchema = z .object({ totalEvents: z.number(), omittedEvents: z.number(), diff --git a/src/orb/apr-repo-transfer.ts b/src/orb/apr-repo-transfer.ts index a8182ca296..8114060e0e 100644 --- a/src/orb/apr-repo-transfer.ts +++ b/src/orb/apr-repo-transfer.ts @@ -76,7 +76,7 @@ export type AprRepoTransferBindingRejection = | "installation_not_bound"; /** #9490: pure tenant-binding check, separated from the completion gate so each is independently testable. */ -export function evaluateAprRepoTransferBinding( +function evaluateAprRepoTransferBinding( input: Pick, binding: AprRepoBinding | null, ): { allowed: true } | { allowed: false; reason: AprRepoTransferBindingRejection } { diff --git a/src/queue/patchless-secret-scan.ts b/src/queue/patchless-secret-scan.ts index a5e2f8a85e..03eb210a04 100644 --- a/src/queue/patchless-secret-scan.ts +++ b/src/queue/patchless-secret-scan.ts @@ -22,7 +22,7 @@ const SECRET_SCAN_PATCH_FALLBACK_MAX_CONCURRENT = 4; /** Aggregate Contents API read budget for one PR secret-scan fallback pass. */ export const SECRET_SCAN_PATCH_FALLBACK_MAX_FETCHES = 100; /** Max patch-less paths listed in the fail-closed advisory detail (title still reports the full count). */ -export const INCOMPLETE_PATCH_LESS_PATH_DETAIL_MAX = 5; +const INCOMPLETE_PATCH_LESS_PATH_DETAIL_MAX = 5; /** Lines present in `head` but not in `base` (multiset), for scanning only the additions on a modified file. */ export function addedLinesForSecretScan(base: string, head: string): string[] { diff --git a/src/queue/processors.ts b/src/queue/processors.ts index 25db7015e3..d6e3202059 100644 --- a/src/queue/processors.ts +++ b/src/queue/processors.ts @@ -8037,7 +8037,7 @@ export async function resolveLinkedIssueAuthorLogins( // than risking only some remembering to add it. The live open-reference fetch is skipped entirely (resolves // `true` with no network call) unless `linkedIssueGateMode` is actually "block" -- the only mode where // whether a citation is open can change the gate's outcome. -export async function resolveLinkedIssueAdvisoryContext( +async function resolveLinkedIssueAdvisoryContext( env: Env, installationId: number | null | undefined, repoFullName: string, diff --git a/src/review/ai-slop-cache-input.ts b/src/review/ai-slop-cache-input.ts index 9358824611..25835321c2 100644 --- a/src/review/ai-slop-cache-input.ts +++ b/src/review/ai-slop-cache-input.ts @@ -4,7 +4,7 @@ import { sha256Hex } from "../utils/crypto"; // (title/body) plus the currently-built diff and deterministic band. Those can drift for the same head when a PR // is edited, retargeted, or re-evaluated under changed settings, so they are hashed alongside the provider // identity to avoid replaying an advisory written for a different prompt. -export const AI_SLOP_CACHE_INPUT_VERSION = "ai-slop-input:v2"; +const AI_SLOP_CACHE_INPUT_VERSION = "ai-slop-input:v2"; export type AiSlopCacheInput = { title?: string | null | undefined; diff --git a/src/review/ams-reputation-bridge.ts b/src/review/ams-reputation-bridge.ts index 390532b484..f0164a2851 100644 --- a/src/review/ams-reputation-bridge.ts +++ b/src/review/ams-reputation-bridge.ts @@ -28,7 +28,7 @@ export const AMS_TRACK_RECORD_TIMEOUT_MS = 400; export const AMS_BRIDGE_TRUSTED_MIN_MERGED = 3; /** …AND a merge rate at/above this share of that submitter's terminal AMS outcomes (0–1). */ -export const AMS_BRIDGE_TRUSTED_MIN_MERGE_RATE = 0.6; +const AMS_BRIDGE_TRUSTED_MIN_MERGE_RATE = 0.6; export type AmsTrackRecordFetch = (url: string, init: RequestInit) => Promise; diff --git a/src/review/auto-apply.ts b/src/review/auto-apply.ts index 4c7313b99d..d1776aea67 100644 --- a/src/review/auto-apply.ts +++ b/src/review/auto-apply.ts @@ -143,7 +143,7 @@ export function isStrictlyTightening(o: TunableOverride, liveFloor?: number, liv export const SHADOW_PROMOTION_MIN_DECIDED = 10; /** How long a shadow override must SOAK before the cron may promote it to live (a transient-blip guard). (#276) */ -export const SHADOW_SOAK_MS = 24 * 60 * 60 * 1000; +const SHADOW_SOAK_MS = 24 * 60 * 60 * 1000; /** PURE promotion gate: promote a SHADOW override → LIVE only when it is (1) strictly tightening vs the live * config, (2) backed by >= SHADOW_PROMOTION_MIN_DECIDED decided samples, (3) SOAKED past validated_until, and diff --git a/src/review/content-lane/orchestrator.ts b/src/review/content-lane/orchestrator.ts index 5c52e0d4ee..c741a06ea1 100644 --- a/src/review/content-lane/orchestrator.ts +++ b/src/review/content-lane/orchestrator.ts @@ -86,7 +86,7 @@ export function diffAppendedSurfaceEntries(headRaw: string | null, baseRaw: stri * exactly what it is -- zero appended entries. Arrays keep their order (order IS meaning there, same rule as * decision-record.ts's canonicalJson). */ -export function canonicalEntryKey(entry: unknown): string { +function canonicalEntryKey(entry: unknown): string { return JSON.stringify(sortKeysDeep(entry)); } diff --git a/src/review/content-lane/registry-logic.ts b/src/review/content-lane/registry-logic.ts index 16b3b1df86..921b018b2e 100644 --- a/src/review/content-lane/registry-logic.ts +++ b/src/review/content-lane/registry-logic.ts @@ -57,7 +57,7 @@ const OBSERVED_STATE_KEYS = new Set([ "last_checked", ]); /** Base-layer chain endpoints (wss/ws JSON-RPC). One-shot: probed + dual-AI-verified like any other candidate. */ -export const REVIEWER_BASE_LAYER_KINDS = new Set(["archive", "subtensor-rpc", "subtensor-wss"]); +const REVIEWER_BASE_LAYER_KINDS = new Set(["archive", "subtensor-rpc", "subtensor-wss"]); export function isBaseLayerKind(kind: unknown): boolean { return REVIEWER_BASE_LAYER_KINDS.has(String(kind)); } @@ -438,7 +438,7 @@ export function isNonEmptyStructuredBody(contentType: unknown, body: unknown): b // ── Repository freshness (hardening) ────────────────────────────────────────────────────────── /** A source repo untouched for longer than this (days), or archived, is not "live truth" → manual. */ -export const STALE_REPO_DAYS = 365; +const STALE_REPO_DAYS = 365; export interface FreshnessSignals { known: boolean; archived: boolean; diff --git a/src/review/decision-audit.ts b/src/review/decision-audit.ts index 73c7e7765d..053f548def 100644 --- a/src/review/decision-audit.ts +++ b/src/review/decision-audit.ts @@ -24,7 +24,7 @@ export const DECISION_AUDIT_SAMPLE_SIZE = 30; /** Stratum quotas over the sample size: merges oversampled (a wrong merge is the costly error), a dedicated * first-time-author slice (weakest priors), remainder to closes. Shortfalls in any stratum spill into the * others rather than shrinking the week's sample. */ -export const DECISION_AUDIT_STRATA = { merge_arm: 0.5, close_arm: 0.3, first_time_author: 0.2 } as const; +const DECISION_AUDIT_STRATA = { merge_arm: 0.5, close_arm: 0.3, first_time_author: 0.2 } as const; export type AuditStratum = keyof typeof DECISION_AUDIT_STRATA; diff --git a/src/review/decision-record.ts b/src/review/decision-record.ts index bc8a1dfbbf..a2e9fac907 100644 --- a/src/review/decision-record.ts +++ b/src/review/decision-record.ts @@ -423,7 +423,7 @@ export type LedgerBreak = * minutes is orders of magnitude beyond any healthy insert-to-append latency while still bounding how long a * genuinely failed append (its own error-level alarm fires at the moment it happens) can hide from verify. */ -export const LEDGER_APPEND_GRACE_MS = 5 * 60 * 1000; +const LEDGER_APPEND_GRACE_MS = 5 * 60 * 1000; /** * Verify a window of the chain, resumable via `afterSeq` (0 = genesis). Reports the FIRST break with its diff --git a/src/review/linked-issue-satisfaction-cache-input.ts b/src/review/linked-issue-satisfaction-cache-input.ts index c08b0f771b..fe091a859f 100644 --- a/src/review/linked-issue-satisfaction-cache-input.ts +++ b/src/review/linked-issue-satisfaction-cache-input.ts @@ -5,7 +5,7 @@ import { sha256Hex } from "../utils/crypto"; // and primary linked issue number; this fingerprint handles reviewer configuration plus mutable GitHub text // (issue title/body and PR title/body) so edits cannot replay a verdict for an older prompt. Diff text is // included defensively too, keeping the cache tied to exactly the model prompt that produced the opinion. -export const LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSION = "linked-issue-satisfaction-input:v2"; +const LINKED_ISSUE_SATISFACTION_CACHE_INPUT_VERSION = "linked-issue-satisfaction-input:v2"; export type LinkedIssueSatisfactionCacheInput = { byok: boolean; diff --git a/src/review/pending-closure-watchdog.ts b/src/review/pending-closure-watchdog.ts index eeeef4d177..32f03dcbf5 100644 --- a/src/review/pending-closure-watchdog.ts +++ b/src/review/pending-closure-watchdog.ts @@ -22,8 +22,8 @@ import { errorMessage } from "../utils/json"; * that re-derives everything from live state. */ -export const PENDING_CLOSURE_FLAGGED_EVENT = "agent.linked_issue.pending_closure_flagged"; -export const PENDING_CLOSURE_REQUEUED_EVENT = "agent.linked_issue.pending_closure_requeued"; +const PENDING_CLOSURE_FLAGGED_EVENT = "agent.linked_issue.pending_closure_flagged"; +const PENDING_CLOSURE_REQUEUED_EVENT = "agent.linked_issue.pending_closure_requeued"; /** Grace beyond the flag's own deadline before the watchdog steps in, so a job merely waiting its turn behind a * busy queue is never mistaken for a lost one. */ diff --git a/src/review/rag.ts b/src/review/rag.ts index 0fb85b5788..a6521b1d74 100644 --- a/src/review/rag.ts +++ b/src/review/rag.ts @@ -98,7 +98,7 @@ export type RagRetrievalResult = { * model id; the self-host embed path (`createOpenAiCompatibleAi` in src/selfhost/ai.ts) discards any * `@cf/`-prefixed id and substitutes its own configured/default embed model (`AI_EMBED_MODEL`), so this * constant only matters for a genuine Cloudflare Workers AI inference binding. */ -export const EMBED_MODEL = "@cf/baai/bge-m3"; +const EMBED_MODEL = "@cf/baai/bge-m3"; /** Default bge-m3 output dimension. Self-host can override this when QDRANT_DIM selects another model width. */ export const RAG_DIMENSIONS = 1024; diff --git a/src/review/repo-culture-profile.ts b/src/review/repo-culture-profile.ts index 66cb834551..e879e1a12b 100644 --- a/src/review/repo-culture-profile.ts +++ b/src/review/repo-culture-profile.ts @@ -38,11 +38,11 @@ export const REPO_CULTURE_PROFILE_SCHEMA_VERSION = 1; export const MIN_SAMPLE_PULL_REQUESTS = 5; /** Signal type this profile is cached under in `signal_snapshots` (mirrors REPO_FOCUS_MANIFEST_SIGNAL's naming). */ -export const REPO_CULTURE_PROFILE_SIGNAL = "repo-culture-profile"; +const REPO_CULTURE_PROFILE_SIGNAL = "repo-culture-profile"; /** Default cache freshness window -- matches REPO_FOCUS_MANIFEST_MAX_AGE_MS's order of magnitude (a repo's merge * norms drift slowly; there is no need to re-derive this on every review). */ -export const REPO_CULTURE_PROFILE_MAX_AGE_MS = 6 * 60 * 60 * 1000; +const REPO_CULTURE_PROFILE_MAX_AGE_MS = 6 * 60 * 60 * 1000; export type RepoCulturePrSizeBand = "tiny" | "small" | "medium" | "large"; diff --git a/src/review/review-diff.ts b/src/review/review-diff.ts index 9126cb35bc..8736ba3b90 100644 --- a/src/review/review-diff.ts +++ b/src/review/review-diff.ts @@ -13,7 +13,7 @@ import type { listPullRequestFiles } from "../db/repositories"; /** Char budget of the diff fed to the review models. The 120B review models have ~128k-token context, so * even a large PR fits in ONE coherent pass (accuracy over speed). Only a genuinely huge PR truncates — * and then SOURCE survives via priority ordering. */ -export const DEFAULT_DIFF_BUDGET = 80_000; +const DEFAULT_DIFF_BUDGET = 80_000; /** Review priority for diff ordering. When the budget is tight, SOURCE survives and * lockfiles/generated/docs/tests are dropped first (least useful to a code reviewer). Lower = kept. diff --git a/src/review/review-eligibility.ts b/src/review/review-eligibility.ts index 464177be9c..0c5b5f1bd3 100644 --- a/src/review/review-eligibility.ts +++ b/src/review/review-eligibility.ts @@ -19,7 +19,7 @@ export type ReviewEligibilityDecision = matchedPattern: string; }; -export const REVIEW_ELIGIBLE: ReviewEligibilityDecision = { +const REVIEW_ELIGIBLE: ReviewEligibilityDecision = { eligible: true, skipReason: null, matchedPattern: null, diff --git a/src/review/risk-control-wire.ts b/src/review/risk-control-wire.ts index 016827aaa4..89c581ea38 100644 --- a/src/review/risk-control-wire.ts +++ b/src/review/risk-control-wire.ts @@ -71,7 +71,7 @@ export function scheduledAlpha(arm: "close" | "merge", usablePairs: number): num if (usablePairs >= 350) return 0.025; return 0.05; } -export function riskControlDelta(env: Env): number { +function riskControlDelta(env: Env): number { return parseBudget(env.LOOPOVER_RISK_CONTROL_DELTA, 0.05, 0.2); } diff --git a/src/review/rule-repeat-alarm-wire.ts b/src/review/rule-repeat-alarm-wire.ts index f8ce0bf353..17e2192f80 100644 --- a/src/review/rule-repeat-alarm-wire.ts +++ b/src/review/rule-repeat-alarm-wire.ts @@ -26,7 +26,7 @@ import { createSignalStore } from "./signal-tracking-wire"; /** How far back to look for repeat fires. Matches #7983's own "e.g. 1-24h, tunable" proposal — chosen at the * wide end so a slow-burn (not just a fast-burst) repeat still gets caught. */ -export const RULE_REPEAT_ALARM_WINDOW_MS = 24 * 60 * 60 * 1000; +const RULE_REPEAT_ALARM_WINDOW_MS = 24 * 60 * 60 * 1000; /** Distinct-target count that trips the alarm. #7983's own proposal ("e.g. >= 3 within 24h") and its own * validation bar ("should have alerted after the 2nd or 3rd occurrence" against the real incident replay). */ export const RULE_REPEAT_ALARM_THRESHOLD = 3; diff --git a/src/review/stats.ts b/src/review/stats.ts index 3bfe23331e..da497d9193 100644 --- a/src/review/stats.ts +++ b/src/review/stats.ts @@ -113,7 +113,7 @@ const EMPTY_EVAL: GateEvalReport = { rows: [], hasSignal: false }; const emptyParity = (authoritative = "reviewbot", shadow = "loopover"): GateParityReport => ({ authoritative, shadow, rows: [], hasSignal: false }); /** Default deps: no-signal eval, no recommendations, empty parity. Keeps the payload shape with no engine. */ -export const defaultStatsEvalDeps: StatsEvalDeps = { +const defaultStatsEvalDeps: StatsEvalDeps = { computeGateEval: async () => EMPTY_EVAL, computeTuningRecommendations: () => [], computeGateParity: async (_env, opts) => emptyParity(opts.authoritative, opts.shadow), diff --git a/src/review/unified-comment.ts b/src/review/unified-comment.ts index c9c7695fae..f05363a2fc 100644 --- a/src/review/unified-comment.ts +++ b/src/review/unified-comment.ts @@ -132,7 +132,7 @@ function dedupeConcerns(items: string[]): string[] { return out.slice(0, 20); } -export function extractReviewSummary(reviews: DualReviewNote[]): ExtractedReviewSummary { +function extractReviewSummary(reviews: DualReviewNote[]): ExtractedReviewSummary { const valid = reviews.filter((r) => r.notes); const failedCount = reviews.length - valid.length; const recommendations = valid.map((r) => (r.notes as ReviewNotes).recommendation); @@ -515,7 +515,7 @@ function dedupeLines(items: string[], cap = Number.POSITIVE_INFINITY): string[] // The human-scannable display cap applied when a repo's maxFindingsCaps supplies nothing. Moved here from // dedupeLines' old default so the cap is now disclosed truncation (with a "+N more" footer) rather than silent // data loss upstream of the AI-context block and the more-footer (#9670). -export const DEFAULT_FINDINGS_DISPLAY_CAP = 12; +const DEFAULT_FINDINGS_DISPLAY_CAP = 12; export function truncateFindingsForDisplay( items: string[], diff --git a/src/review/visual/actions-fallback.ts b/src/review/visual/actions-fallback.ts index 306c97ebc4..76fbf03602 100644 --- a/src/review/visual/actions-fallback.ts +++ b/src/review/visual/actions-fallback.ts @@ -42,7 +42,7 @@ const DEFAULT_TIMEOUT_MS = 20_000; const API_VERSION = "2022-11-28"; /** The workflow file this module dispatches and whose completions it listens for. */ -export const FALLBACK_WORKFLOW_FILE = "visual-capture-fallback.yml"; +const FALLBACK_WORKFLOW_FILE = "visual-capture-fallback.yml"; /** The workflow's declared `name:` -- cross-checked against `workflow_run.name` before acting on a completion. */ export const FALLBACK_WORKFLOW_NAME = "LoopOver Visual Capture Fallback"; /** The artifact name the dispatched workflow uploads its captured PNGs under. */ diff --git a/src/review/visual/screenshot-table-vision.ts b/src/review/visual/screenshot-table-vision.ts index 793e052fa7..2cdf2b0311 100644 --- a/src/review/visual/screenshot-table-vision.ts +++ b/src/review/visual/screenshot-table-vision.ts @@ -79,7 +79,7 @@ export function evaluateScreenshotTableVisionGate(input: { * deterministic screenshot-table gate with a duplicated/unrelated image — so skipping it with no trace at * all reproduces #9015's "suspicion buys less scrutiny" shape. `severity: "warning"` (never a blocker — this * module stays STRICTLY ADVISORY, see the file header) puts it in `gate.warnings`. */ -export const SCREENSHOT_TABLE_VISION_SKIPPED_LOW_REPUTATION_FINDING_CODE = "screenshot_table_vision_skipped_low_reputation"; +const SCREENSHOT_TABLE_VISION_SKIPPED_LOW_REPUTATION_FINDING_CODE = "screenshot_table_vision_skipped_low_reputation"; /** Build the compensating advisory finding for a low-reputation gaming-check skip (#9136). `imagePairCount` is * how many real (different-bytes) pairs survived the caller's byte pre-check — the pairs vision would have diff --git a/src/review/visual/shot-render-token.ts b/src/review/visual/shot-render-token.ts index 0045f777de..9c794eaad1 100644 --- a/src/review/visual/shot-render-token.ts +++ b/src/review/visual/shot-render-token.ts @@ -18,7 +18,7 @@ import { hmacHex, timingSafeEqualHex } from "../../utils/crypto"; // window. A comment's embedded link is minted once and never re-signed, so this also bounds how long a // stale/never-viewed on-demand link stays renderable at all -- an explicit tradeoff, not an oversight (see // this repo's own PR discussion for #9044). -export const SHOT_RENDER_TOKEN_TTL_MS = 24 * 60 * 60 * 1000; +const SHOT_RENDER_TOKEN_TTL_MS = 24 * 60 * 60 * 1000; async function signShotRenderToken(env: Env, url: string, expiresAtMs: number): Promise { return hmacHex(env.INTERNAL_JOB_TOKEN, `${url}:${expiresAtMs}`); diff --git a/src/review/visual/shot.ts b/src/review/visual/shot.ts index 593d1d033e..b155634212 100644 --- a/src/review/visual/shot.ts +++ b/src/review/visual/shot.ts @@ -85,7 +85,7 @@ export const MAX_SCREENSHOT_HEIGHT = 20000; // The real cost ceiling: width × height, so a narrow-tall page is judged by what it actually costs to // render rather than by height alone. export const MAX_SCREENSHOT_PIXELS = 14_400_000; // 1440 × 10000 — one full desktop-width page. -export const MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024; +const MAX_SCREENSHOT_BYTES = 5 * 1024 * 1024; const SCREENSHOT_TIMEOUT_MS = 10000; const SCREENSHOT_HEIGHT_PROBE_TIMEOUT_MS = 2_000; const THEME_STORAGE_WRITE_TIMEOUT_MS = 2_000; @@ -139,7 +139,7 @@ const AUTH_SVG = ` { +async function renderScreenshot(env: Env, url: string, viewport: Viewport = VIEWPORT, opts: CaptureShotOptions = {}): Promise { return (await captureShot(env, url, viewport, opts)).png; } diff --git a/src/scenarios/input-model.ts b/src/scenarios/input-model.ts index 9ba6cc2206..468e39ab6a 100644 --- a/src/scenarios/input-model.ts +++ b/src/scenarios/input-model.ts @@ -6,16 +6,16 @@ import { sanitizePublicComment } from "../github/commands"; // of a weaker env-flag-only check. Re-exported here to keep this module's public surface unchanged. export { assertScenarioLocalBranchInputSafe } from "@loopover/engine"; -export const SCENARIO_INPUT_VERSION = 1 as const; +const SCENARIO_INPUT_VERSION = 1 as const; export const SCENARIO_MAX_REPO_FULL_NAME_CHARS = 200; export const SCENARIO_MAX_BRANCH_REF_CHARS = 200; export const SCENARIO_MAX_LINKED_ISSUE_NUMBERS = 50; export const SCENARIO_MAX_SIGNAL_DETAIL_CHARS = 2000; -export const scenarioInputKinds = ["fact", "assumption", "estimate", "unavailable"] as const; +const scenarioInputKinds = ["fact", "assumption", "estimate", "unavailable"] as const; export type ScenarioInputKind = (typeof scenarioInputKinds)[number]; -export const scenarioTypes = [ +const scenarioTypes = [ "open_pr_pressure", "pending_pr_resolution", "branch_preflight", @@ -24,7 +24,7 @@ export const scenarioTypes = [ ] as const; export type ScenarioType = (typeof scenarioTypes)[number]; -export const scenarioSignalSources = [ +const scenarioSignalSources = [ "github_observed", "user_supplied", "local_metadata", @@ -93,7 +93,7 @@ const scenarioBranchStateSchema = z export type ScenarioBranchState = z.infer; -export const agentScenarioInputSchema = z +const agentScenarioInputSchema = z .object({ version: z.literal(SCENARIO_INPUT_VERSION), scenarioType: z.enum(scenarioTypes), diff --git a/src/selfhost/redaction-scrub.ts b/src/selfhost/redaction-scrub.ts index 8ecec9b4f7..2a0433e877 100644 --- a/src/selfhost/redaction-scrub.ts +++ b/src/selfhost/redaction-scrub.ts @@ -12,7 +12,7 @@ import { export const SECRET_KEY = /(token|secret|key|password|passwd|authorization|auth|dsn|cookie|bearer|credential|private)/i; -export const PAYLOAD_KEY = +const PAYLOAD_KEY = /(^|[_-])(body|payload|patch|diff|prompt|rubric|guardrail|headers?|cookies?|title|config|review[-_]?text|review[-_]?content|comment[-_]?text|comment[-_]?body)([_-]|$)|^(body|payload|patch|diff|prompt|rubric|guardrail|headers?|cookies?|title|config|review[-_]?text|review[-_]?content|comment[-_]?text|comment[-_]?body)$/i; export const SECRET_VALUE = new RegExp( [ @@ -30,10 +30,10 @@ export const SECRET_VALUE = new RegExp( ].join("|"), "gi", ); -export const JWT_VALUE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g; -export const QUERY_SECRET_VALUE = +const JWT_VALUE = /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g; +const QUERY_SECRET_VALUE = /([?&;][^=\s&#;]*(?:token|secret|key|password|passwd|authorization|auth|dsn|cookie|bearer|credential|private)[^=\s&#;]*=)[^&#\s;]+/gi; -export const PRIVATE_TEXT = +const PRIVATE_TEXT = /\b(raw[-_\s]?score|scoring context|private rubric|gate prompt|review prompt|guardrail paths?|pull request body|pr body|pr title|raw diff)\b/gi; export const PUBLIC_UNSAFE_SCRUB = new RegExp(String.raw`\b(${PUBLIC_UNSAFE_TERMS})\b`, "gi"); export const REDACTED = "[redacted]"; @@ -106,14 +106,14 @@ function normalizeInstallationId(value: unknown): string | undefined { /** Hash a raw installation id into a short, non-reversible tag -- undefined when the hasher hasn't been * loaded yet ({@link loadNodeHasher}) or the value isn't a real installation id. */ -export function installationIdHash(value: unknown): string | undefined { +function installationIdHash(value: unknown): string | undefined { if (!digestHexSync) return undefined; const normalized = normalizeInstallationId(value); if (!normalized) return undefined; return digestHexSync(`${INSTALLATION_HASH_SEED}${normalized}`).slice(0, 16); } -export function isInstallationIdKey(key: string): boolean { +function isInstallationIdKey(key: string): boolean { return key.replace(/[^A-Za-z0-9]/g, "").toLowerCase() === "installationid"; } @@ -130,7 +130,7 @@ export function hashedInstallationContext(context: Record): Rec return safe; } -export function shouldRedactKey(key: string): boolean { +function shouldRedactKey(key: string): boolean { const compact = key.replace(/[^A-Za-z0-9]/g, "").toLowerCase(); return ( SECRET_KEY.test(key) || @@ -149,11 +149,11 @@ export function scrubString(value: string): string { .replace(PRIVATE_TEXT, "private context"); } -export function isUrlKey(key: string): boolean { +function isUrlKey(key: string): boolean { return key.replace(/[^A-Za-z0-9]/g, "").toLowerCase().endsWith("url"); } -export function isQueryKey(key: string): boolean { +function isQueryKey(key: string): boolean { const compact = key.replace(/[^A-Za-z0-9]/g, "").toLowerCase(); return compact === "query" || compact === "querystring"; } diff --git a/src/services/ai-review.ts b/src/services/ai-review.ts index 36f5d3ba42..417aa9e25e 100644 --- a/src/services/ai-review.ts +++ b/src/services/ai-review.ts @@ -857,11 +857,6 @@ export function extractLastJsonObject(text: string): string | null { return last; } -/** Default reviewer confidence when the model omits a usable `confidence` (#8) — 1.0, so an absent/garbage value - * degrades to EXACTLY the historical hardcoded `confidence: 1` (a defect always cleared the floor). Shared by the - * parser and the combiners so the fallback is identical everywhere. */ -export const DEFAULT_REVIEW_CONFIDENCE = 1; - /** #8833: the fallback when a model states NO usable confidence at all. The old fallback was 1.0 — "the * model said nothing" read as MAXIMUM certainty, so a review missing the field skipped every low-confidence * safeguard (#4603's disposition, the close-confidence floor) and drove a straight close. 0.5 sits below @@ -1042,7 +1037,7 @@ export function demoteStaleBaseClaimBlockers(review: ModelReview): { review: Mod * hallucination pattern lives in (a symbol/import/guard/handler the model merely could not SEE). Every * other kind remains free-form judgment; this list is deliberately closed so a novel kind string can never * smuggle a claim past verification (unknown kinds are treated as plain judgment, not as absence). */ -export const ABSENCE_CLAIM_KINDS = new Set(["missing_symbol", "missing_import", "missing_guard", "missing_handling", "missing_registration"]); +const ABSENCE_CLAIM_KINDS = new Set(["missing_symbol", "missing_import", "missing_guard", "missing_handling", "missing_registration"]); /** #8833: the minimum usable evidence quote. Shorter fragments ("x", ") {") appear in virtually any diff, so * they would verify vacuously — a quote must be long enough to plausibly identify ONE breaking line. */ diff --git a/src/services/contributor-evidence-graph.ts b/src/services/contributor-evidence-graph.ts index 7ea7ea3a38..af72760c39 100644 --- a/src/services/contributor-evidence-graph.ts +++ b/src/services/contributor-evidence-graph.ts @@ -11,7 +11,7 @@ import type { import { nowIso } from "../utils/json"; export const CONTRIBUTOR_EVIDENCE_GRAPH_SIGNAL = "contributor-evidence-graph"; -export const CONTRIBUTOR_EVIDENCE_GRAPH_VERSION = 1; +const CONTRIBUTOR_EVIDENCE_GRAPH_VERSION = 1; export const CONTRIBUTOR_EVIDENCE_GRAPH_MAX_REPOS = 50; export const CONTRIBUTOR_EVIDENCE_GRAPH_MAX_LABELS = 80; export const CONTRIBUTOR_EVIDENCE_GRAPH_MAX_PATHS = 80; diff --git a/src/services/contributor-issue-draft.ts b/src/services/contributor-issue-draft.ts index b3d8bac82c..cd68dfa314 100644 --- a/src/services/contributor-issue-draft.ts +++ b/src/services/contributor-issue-draft.ts @@ -185,7 +185,7 @@ export function findDuplicateContributorDraft( return null; } -export const CONTRIBUTOR_ISSUE_DRAFT_DECLINED_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000; +const CONTRIBUTOR_ISSUE_DRAFT_DECLINED_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000; const DECLINED_DRAFT_WONTFIX_LABELS = new Set(["wontfix", "wont-fix", "invalid", "duplicate", "not-planned"]); /** diff --git a/src/services/decision-pack.ts b/src/services/decision-pack.ts index a8d4183e9a..5750b969e1 100644 --- a/src/services/decision-pack.ts +++ b/src/services/decision-pack.ts @@ -78,7 +78,7 @@ function resolveOssEmissionShare(constants: Record | undefined): const value = constants?.OSS_EMISSION_SHARE; return typeof value === "number" && Number.isFinite(value) ? value : DEFAULT_OSS_EMISSION_SHARE; } -export const DECISION_PACK_REBUILD_DEBOUNCE_MS = 15 * 1000; +const DECISION_PACK_REBUILD_DEBOUNCE_MS = 15 * 1000; const pendingDecisionPackRebuilds = new Map>(); async function loadContributorPullRequestFilePaths( diff --git a/src/services/issue-plan-draft.ts b/src/services/issue-plan-draft.ts index 49e0e0b8f0..bcc3fd9f22 100644 --- a/src/services/issue-plan-draft.ts +++ b/src/services/issue-plan-draft.ts @@ -45,7 +45,7 @@ import type { IssueRecord, RepositorySettings } from "../types"; import { sha256Hex } from "../utils/crypto"; import { errorMessage, nowIso } from "../utils/json"; -export const ISSUE_PLAN_DRAFT_MARKER_PREFIX = "loopover-issue-plan-draft"; +const ISSUE_PLAN_DRAFT_MARKER_PREFIX = "loopover-issue-plan-draft"; export function issuePlanDraftMarker(fingerprint: string): string { return ``; @@ -90,7 +90,7 @@ export function findDuplicateIssuePlanDraft( return null; } -export const ISSUE_PLAN_DRAFT_DECLINED_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000; +const ISSUE_PLAN_DRAFT_DECLINED_COOLDOWN_MS = 30 * 24 * 60 * 60 * 1000; const DECLINED_ISSUE_PLAN_WONTFIX_LABELS = new Set(["wontfix", "wont-fix", "invalid", "duplicate", "not-planned"]); /** Mirrors contributor-issue-draft.ts's findDeclinedContributorDraft exactly (same wontfix/cooldown/maintainer- diff --git a/src/services/linked-issue-satisfaction.ts b/src/services/linked-issue-satisfaction.ts index 1a584e0981..623b0bc7b4 100644 --- a/src/services/linked-issue-satisfaction.ts +++ b/src/services/linked-issue-satisfaction.ts @@ -28,7 +28,7 @@ import { toPublicSafe } from "./ai-review"; /** The three verdicts this advisory can reach about a single linked issue. */ -export const LINKED_ISSUE_SATISFACTION_STATUSES = ["addressed", "partial", "unaddressed"] as const; +const LINKED_ISSUE_SATISFACTION_STATUSES = ["addressed", "partial", "unaddressed"] as const; export type LinkedIssueSatisfactionStatus = (typeof LINKED_ISSUE_SATISFACTION_STATUSES)[number]; /** Below this calibrated confidence, an "unaddressed" verdict is too uncertain to publish (see module doc) — diff --git a/src/services/mcp-compatibility.ts b/src/services/mcp-compatibility.ts index 99a4aa0dc2..a9803ff5bb 100644 --- a/src/services/mcp-compatibility.ts +++ b/src/services/mcp-compatibility.ts @@ -5,7 +5,7 @@ // before the npm package itself is actually tagged/published. import loopoverMcpPackageJson from "../../packages/loopover-mcp/package.json"; -export const LOOPOVER_API_VERSION = "0.1.0"; +const LOOPOVER_API_VERSION = "0.1.0"; export const LOOPOVER_MCP_PACKAGE_NAME = "@loopover/mcp"; export const MINIMUM_SUPPORTED_MCP_VERSION = "0.5.0"; export const LATEST_RECOMMENDED_MCP_VERSION: string = loopoverMcpPackageJson.version; diff --git a/src/services/notify-pagerduty.ts b/src/services/notify-pagerduty.ts index 9f9ebcf610..8b95751fa8 100644 --- a/src/services/notify-pagerduty.ts +++ b/src/services/notify-pagerduty.ts @@ -25,8 +25,8 @@ const PAGERDUTY_EVENTS_URL = "https://events.pagerduty.com/v2/enqueue"; // The audit `detail` written for a real page vs an auto-resolve. Both share outcome "completed", so the cooldown // must key off `detail` to count only rows that ACTUALLY paged (#9695). Exported + used at both the write and // the read site so the two spellings can never drift. -export const PAGERDUTY_AUDIT_DETAIL_TRIGGERED = "triggered"; -export const PAGERDUTY_AUDIT_DETAIL_RESOLVED = "resolved"; +const PAGERDUTY_AUDIT_DETAIL_TRIGGERED = "triggered"; +const PAGERDUTY_AUDIT_DETAIL_RESOLVED = "resolved"; // PagerDuty routing/integration keys are 32 lowercase hex characters. const ROUTING_KEY_RE = /^[a-f0-9]{32}$/i; const DEFAULT_MIN_SEVERITY: PagerDutySeverity = "error"; diff --git a/src/services/public-quality-metrics.ts b/src/services/public-quality-metrics.ts index f4bac5b561..7be92982f2 100644 --- a/src/services/public-quality-metrics.ts +++ b/src/services/public-quality-metrics.ts @@ -14,7 +14,7 @@ import { closedAtMs } from "./review-recap"; export const PUBLIC_QUALITY_TREND_WEEKS = 8; /** Below this per-week gate-block sample the weekly false-positive rate is too noisy to publish. */ -export const MIN_GATE_TREND_SAMPLE = 3; +const MIN_GATE_TREND_SAMPLE = 3; export type PublicQualityTrendWeek = { /** UTC Monday (YYYY-MM-DD) that starts the bucket. */ @@ -199,7 +199,7 @@ function topPublicGateTypes(gatePrecision: GatePrecisionReport): PublicQualityGa } /** Assemble the public-safe per-repo quality payload from existing telemetry. Pure. */ -export function buildPublicQualityMetrics(args: { +function buildPublicQualityMetrics(args: { repoFullName: string; generatedAt: string; gatePrecision: GatePrecisionReport; diff --git a/src/services/queue-trends.ts b/src/services/queue-trends.ts index 41ffb1a7fe..cea3116a39 100644 --- a/src/services/queue-trends.ts +++ b/src/services/queue-trends.ts @@ -2,7 +2,7 @@ import type { JsonValue, RepoGithubTotalsSnapshotRecord, SignalSnapshotRecord } import type { QueueHealth } from "../signals/engine"; import { nowIso } from "../utils/json"; -export const QUEUE_TREND_WINDOWS_DAYS = [7, 14, 30] as const; +const QUEUE_TREND_WINDOWS_DAYS = [7, 14, 30] as const; export const QUEUE_TREND_HISTORY_DAYS = 35; export type QueueTrendWindow = { diff --git a/src/services/recommendation-outcomes.ts b/src/services/recommendation-outcomes.ts index f50c51c295..dff7d9cc02 100644 --- a/src/services/recommendation-outcomes.ts +++ b/src/services/recommendation-outcomes.ts @@ -17,8 +17,8 @@ import type { } from "../types"; import { nowIso } from "../utils/json"; -export const DEFAULT_RECOMMENDATION_OUTCOME_STALE_DAYS = 14; -export const DEFAULT_RECOMMENDATION_OUTCOME_IGNORED_DAYS = 7; +const DEFAULT_RECOMMENDATION_OUTCOME_STALE_DAYS = 14; +const DEFAULT_RECOMMENDATION_OUTCOME_IGNORED_DAYS = 7; export type RecommendationOutcomeEvaluationResult = { login: string; diff --git a/src/settings/agent-execution.ts b/src/settings/agent-execution.ts index 79444bfc03..ce4a1b5626 100644 --- a/src/settings/agent-execution.ts +++ b/src/settings/agent-execution.ts @@ -138,7 +138,7 @@ export function requiredAgentActionPermissions( return requirements; } -export function missingAgentActionPermissions(input: { +function missingAgentActionPermissions(input: { autonomy: AutonomyPolicy | null | undefined; installationPermissions: Record | null | undefined; actionClass?: AgentActionClass | null | undefined; diff --git a/src/signals/settings-preview.ts b/src/signals/settings-preview.ts index f89ae4d23e..ba962c65a8 100644 --- a/src/signals/settings-preview.ts +++ b/src/signals/settings-preview.ts @@ -24,11 +24,11 @@ import { buildUnifiedCommentBody } from "../review/unified-comment-bridge"; import { requiredAgentActionPermissions } from "../settings/agent-execution"; import { isAgentConfigured } from "../settings/autonomy"; -export function hasVisiblePrSurface(settings: RepositorySettings): boolean { +function hasVisiblePrSurface(settings: RepositorySettings): boolean { return settings.publicSurface !== "off" || settings.checkRunMode === "enabled" || shouldPublishReviewCheck(settings.reviewCheckMode); } -export function shouldPublishPrComment(settings: RepositorySettings, minerStatus: PublicSurfaceMinerStatus = "not_checked"): boolean { +function shouldPublishPrComment(settings: RepositorySettings, minerStatus: PublicSurfaceMinerStatus = "not_checked"): boolean { if (settings.commentMode === "off") return false; if (settings.publicSurface !== "comment_and_label" && settings.publicSurface !== "comment_only") return false; if (settings.commentMode === "detected_contributors_only") return minerStatus === "confirmed"; diff --git a/test/unit/check-dead-exports-script.test.ts b/test/unit/check-dead-exports-script.test.ts new file mode 100644 index 0000000000..8633c58a46 --- /dev/null +++ b/test/unit/check-dead-exports-script.test.ts @@ -0,0 +1,83 @@ +// The dead-export check's own behaviour (#9852), driven through its injectable seams so no fixture is +// written into the real tree — same pattern as check-dead-source-files-script.test.ts. +import { describe, expect, it } from "vitest"; +import { findDeadExports } from "../../scripts/check-dead-exports"; + +/** A tiny two-root world: `src` is checked, `test` only contributes references. */ +function world(files: Record) { + return { + listFiles: (root: string) => Object.keys(files).filter((path) => path.startsWith(`${root}/`)), + readFile: (file: string) => files[file] ?? "", + }; +} + +describe("findDeadExports (#9852)", () => { + it("flags an export nothing outside its own file references", () => { + const found = findDeadExports({ + sourceRoots: ["src"], + referenceRoots: ["src", "test"], + ...world({ + "src/lonely.ts": "export const LONELY = 1;\nconst used = LONELY + 1;\nexport function keep() { return used; }\n", + "src/consumer.ts": "import { keep } from './lonely';\nkeep();\n", + "test/x.test.ts": "", + }), + }); + expect(found.map((v) => v.symbol)).toEqual(["LONELY"]); + }); + + it("reports internalUses, because that decides the fix", () => { + // >1 means the symbol is used inside the file and only `export` is wrong; 1 means it is dead outright. + const found = findDeadExports({ + sourceRoots: ["src"], + referenceRoots: ["src"], + ...world({ "src/a.ts": "export const USED_INSIDE = 1;\nconst x = USED_INSIDE;\nexport const NEVER = 2;\nvoid x;\n" }), + }); + expect(found.find((v) => v.symbol === "USED_INSIDE")?.internalUses).toBeGreaterThan(1); + expect(found.find((v) => v.symbol === "NEVER")?.internalUses).toBe(1); + }); + + it("counts a reference from ANY reference root, including tests and scripts", () => { + const found = findDeadExports({ + sourceRoots: ["src"], + referenceRoots: ["src", "test", "scripts"], + ...world({ + "src/a.ts": "export const FROM_TEST = 1;\nexport const FROM_SCRIPT = 2;\n", + "test/a.test.ts": "FROM_TEST;", + "scripts/a.ts": "FROM_SCRIPT;", + }), + }); + expect(found).toEqual([]); + }); + + it("honours an allowlist entry, which is keyed file:symbol", () => { + const files = { "src/a.ts": "export const ALLOWED = 1;\n" }; + expect(findDeadExports({ sourceRoots: ["src"], referenceRoots: ["src"], ...world(files) }).map((v) => v.symbol)).toEqual(["ALLOWED"]); + expect( + findDeadExports({ + sourceRoots: ["src"], + referenceRoots: ["src"], + allowedExports: new Map([["src/a.ts:ALLOWED", "because"]]), + ...world(files), + }), + ).toEqual([]); + }); + + it("ignores exported TYPES — an unused type costs nothing at runtime", () => { + const found = findDeadExports({ + sourceRoots: ["src"], + referenceRoots: ["src"], + ...world({ "src/a.ts": "export type Unused = { a: 1 };\nexport interface AlsoUnused { b: 2 }\n" }), + }); + expect(found).toEqual([]); + }); + + it("does not confuse a substring for a reference", () => { + // `FOO` must not be considered referenced by `FOOBAR` — the whole point of the word boundary. + const found = findDeadExports({ + sourceRoots: ["src"], + referenceRoots: ["src"], + ...world({ "src/a.ts": "export const FOO = 1;\n", "src/b.ts": "const FOOBAR = 2;\nvoid FOOBAR;\n" }), + }); + expect(found.map((v) => v.symbol)).toEqual(["FOO"]); + }); +});