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"]); + }); +});