Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down
147 changes: 147 additions & 0 deletions scripts/check-dead-exports.ts
Original file line number Diff line number Diff line change
@@ -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 = /(?<!\.d)\.tsx?$/;
const EXCLUDED_SEGMENT = /(?:^|\/)(?:node_modules|dist|dist-test)(?:\/|$)/;

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

/**
* Exports with no in-repo reference that are nonetheless legitimate, each with the reason.
*
* Same contract as check-dead-source-files.ts's STAGED_AHEAD_OF_CONSUMERS: an entry states WHY and, where
* it applies, what ends it. An unexplained dead export is exactly what this check exists to catch, so an
* exception has to say something.
*/
const ALLOWED_EXPORTS: ReadonlyMap<string, string> = 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<string, string>;
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<string, string>();
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();
2 changes: 1 addition & 1 deletion src/api/proof-badge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions src/auth/security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
6 changes: 3 additions & 3 deletions src/db/repositories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1892,7 +1892,7 @@ export async function persistUpstreamSourceSnapshots(env: Env, snapshots: Upstre
}
}

export async function listLatestUpstreamSourceSnapshots(env: Env, limit = 20): Promise<UpstreamSourceSnapshotRecord[]> {
async function listLatestUpstreamSourceSnapshots(env: Env, limit = 20): Promise<UpstreamSourceSnapshotRecord[]> {
const db = getDb(env.DB);
const rows = await db.select().from(upstreamSourceSnapshots).orderBy(desc(upstreamSourceSnapshots.fetchedAt)).limit(limit);
return rows.map(toUpstreamSourceSnapshotRecord);
Expand Down Expand Up @@ -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<boolean> {
async function hasRecentAuditEventForOtherTarget(env: Env, actor: string, eventType: string, currentTargetKey: string, sinceIso: string): Promise<boolean> {
const db = getDb(env.DB);
const rows = await db
.select({ id: auditEvents.id })
Expand Down Expand Up @@ -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<AgentRecommendationOutcomeRecord | null> {
async function getAgentRecommendationOutcome(env: Env, actionId: string): Promise<AgentRecommendationOutcomeRecord | null> {
const [row] = await getDb(env.DB).select().from(agentRecommendationOutcomes).where(eq(agentRecommendationOutcomes.actionId, actionId)).limit(1);
return row ? toAgentRecommendationOutcomeRecord(row) : null;
}
Expand Down
4 changes: 2 additions & 2 deletions src/github/backfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion src/github/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion src/github/webhook.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Response> {
async function enqueueVerifiedWebhook(c: Context<{ Bindings: Env }>, deliveryId: string, eventName: string, rawBody: string): Promise<Response> {
const result = await enqueueWebhookByEnv(c.env, deliveryId, eventName, rawBody, getSelfHostRequestTraceParent(c.req.raw));
switch (result) {
case "review_unavailable":
Expand Down
2 changes: 1 addition & 1 deletion src/notifications/ams-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 4 additions & 4 deletions src/notifications/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`),
Expand All @@ -62,23 +62,23 @@ 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}`),
body: sanitizePublicComment(`Your AMS miner started an attempt on ${ref}. Watch the attempt log for progress and the next decision.`),
};
}

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}`),
body: sanitizePublicComment(`Your AMS miner attempt on ${ref} did not complete successfully. Check the attempt log, then reclaim or pick the next high-fit issue.`),
};
}

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(
Expand Down
Loading
Loading