Skip to content

feat: switch code quality setup to ultracite and biome#769

Open
ahmadk953 wants to merge 15 commits intodevelopmentfrom
ahmadk953/biome-ultracite
Open

feat: switch code quality setup to ultracite and biome#769
ahmadk953 wants to merge 15 commits intodevelopmentfrom
ahmadk953/biome-ultracite

Conversation

@ahmadk953
Copy link
Copy Markdown
Owner

@ahmadk953 ahmadk953 commented Mar 18, 2026

Pull Request

Summary

This pull request migrates the project from ESLint and Prettier to Ultracite (using Biome as the backend) as the standard for linting and formatting JavaScript/TypeScript code. It updates documentation, developer tooling, CI/CD workflows, and code standards to reflect this change. The main improvements include new and updated code standards, devcontainer and editor integration, workflow and hook updates, and revised project instructions.

Related issues

N/A

Type of change

  • feat (new feature)
  • fix (bug fix)
  • refactor (no functional changes)
  • docs (documentation only)
  • chore (maintenance, deps)
  • perf (performance)
  • ci (build/CI/CD)

What changed

Migration to Ultracite/Biome for Linting and Formatting:

  • Removed ESLint and Prettier in favor of Ultracite (Biome backend) for all linting and formatting, updating relevant documentation and configuration files to reference the new tools and commands. (.devcontainer/devcontainer.json, .devcontainer/README.md, .github/copilot-instructions.md, .github/pull_request_template.md, .github/PULL_REQUEST_TEMPLATE/bug_fix.md, .github/PULL_REQUEST_TEMPLATE/feature.md, .github/PULL_REQUEST_TEMPLATE/refactor.md, .github/workflows/eslint.yml, [1] [2] [3] [4] [5] [6] [7] [8]

Documentation and Code Standards:

  • Added comprehensive Ultracite documentation and code standards, including a detailed skill reference and a full code standards guide, outlining best practices for accessibility, performance, type safety, maintainability, and framework-specific conventions. (.agents/skills/ultracite/SKILL.md, .agents/skills/ultracite/references/code-standards.md, [1] [2]
  • Embedded a summary of Ultracite code standards directly into the main project instructions for quick reference. (.github/copilot-instructions.md, .github/copilot-instructions.mdL140-R266)

Developer Tooling and Editor Integration:

  • Updated the devcontainer to install and configure the Biome VSCode extension as the default formatter for all supported file types, replacing ESLint and Prettier extensions and settings. (.devcontainer/devcontainer.json, .devcontainer/devcontainer.jsonL21-R75)
  • Added a hook to automatically run yarn fix (Ultracite auto-fix) after tool usage, skipping certain correctness rules for smoother workflow. (.github/hooks/ultracite.json, .github/hooks/ultracite.jsonR1-R10)

CI/CD and Workflow Updates:

Project Scripts and PR Templates:

  • Standardized project scripts and PR checklist items to use yarn check, yarn fix, and yarn type-check in place of previous yarn lint and yarn format:fix commands. (.github/copilot-instructions.md, .github/pull_request_template.md, .github/PULL_REQUEST_TEMPLATE/bug_fix.md, .github/PULL_REQUEST_TEMPLATE/feature.md, .github/PULL_REQUEST_TEMPLATE/refactor.md, [1] [2] [3] [4] [5]

How was this tested?

  • Local run: yarn dev / yarn no-deploy
  • Lint: yarn lint
  • Format: yarn format:fix
  • Build: yarn compile
  • Manual verification (describe steps below)

Test notes:

  • Environment (OS, Node, Discord guild):
  • Steps and expected results:
  • Screenshots/logs (if applicable):

Database or cache changes

  • N/A
  • Schema changed; generated and applied migrations with drizzle-kit
  • Data migration required
  • Redis key(s) added/changed (prefix with bot:); includes graceful degradation

Details:

Breaking changes

  • No breaking changes
  • Breaking change (describe impact and migration path)

Migration notes:

Security and privacy

  • No new sensitive data handled
  • Secrets management unchanged
  • Considered abuse/spam vectors for new commands/events

Notes:

Checklist

  • I followed the contribution guidelines in CONTRIBUTING.md
  • PR title follows Conventional Commits (e.g., feat(bot): ...)
  • Branch name follows repo convention (e.g., username/feature-name)
  • Updated docs and examples where needed
  • Added or updated telemetry/logging where useful
  • For long-running operations, ensured deferReply() usage where needed

Additional context for reviewers


Summary by cubic

Switched code quality to ultracite with Biome, replacing ESLint/Prettier. Updated scripts, CI, editor/devcontainer, and docs; broad refactors improve command safety, counting validation, DB types, and logging.

  • Dependencies

    • Replaced ESLint/Prettier with ultracite (Biome) and added biome.jsonc.
    • New scripts: yarn check, yarn fix, yarn type-check, yarn setup, yarn commitlint; Husky + lint-staged now run yarn ultracite fix and type-check on staged files.
    • Replaced eslint.yml with ultracite.yml (Biome SARIF output + optional PR comments).
    • Devcontainer sets biomejs.biome as the default formatter across file types.
    • Updated PR templates and docs; moved docs URL to GitBook; added AGENTS.md and .agents/skills/ultracite.
  • Refactors

    • Standardized validateInteraction and safelyRespond across commands; improved select/menu handling and error paths.
    • Counting: stricter number/expression validation, executor tracking, clearer reactions/timestamps, safer helpers.
    • DB: updated function signatures to concrete table types, better error handling/caching, normalized moderation date fields; some functions now return IDs (e.g., addFact).
    • Logging/telemetry: improved error field extraction, standardized colors, added channel/member logging handlers.

Written for commit 93e2860. Summary will update on new commits.

@ahmadk953 ahmadk953 added this to the v1.0.0 milestone Mar 18, 2026
@ahmadk953 ahmadk953 requested a review from Copilot March 18, 2026 21:32
@ahmadk953 ahmadk953 self-assigned this Mar 18, 2026
@github-advanced-security
Copy link
Copy Markdown
Contributor

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

@ahmadk953 ahmadk953 added enhancement New feature or request dependencies Updates to dependencies github_actions Pull requests that update GitHub Actions code priority Priority issues/PRs refactor Refactors code ci Updates CI documentation Improvements or additions to documentation labels Mar 18, 2026
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 18, 2026

📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added automated code formatting and linting via Ultracite (Biome backend) with pre-commit hooks.
    • Enhanced command validation with improved error responses.
    • Expanded logging system for moderation actions including channel updates and purge operations.
    • Data retention configuration for automated cleanup.
  • Documentation

    • Updated documentation links and setup instructions.
    • Added comprehensive code standards and style guidelines.
  • Chores

    • Development tooling migration from ESLint/Prettier to Ultracite.
    • Updated development environment requirements.

Walkthrough

Replaces ESLint/Prettier with Ultracite/Biome across tooling, CI, and editor settings; adds Ultracite docs and standards; refactors DB/Redis table/type usage and many DB functions; centralizes logging handlers; introduces interaction validation/safe responses and refactors numerous commands, events, and utilities.

Changes

Cohort / File(s) Summary
Tooling & CI
package.json, .devcontainer/devcontainer.json, .devcontainer/README.md, .lintstagedrc.mjs, .husky/commit-msg, .prettierrc.yml, .prettierignore, biome.jsonc, .github/workflows/..., .github/hooks/ultracite.json, .github/*_TEMPLATE*, skills-lock.json
Remove ESLint/Prettier configs/workflows; add Ultracite/Biome tooling, VS Code formatter defaults, update scripts (yarn check/yarn fix/yarn type-check), adjust lint-staged/Husky, add Biome SARIF CI workflow and hook.
Ultracite Documentation
.agents/skills/ultracite/SKILL.md, .agents/skills/ultracite/references/code-standards.md, AGENTS.md
Add Ultracite skill docs and exhaustive code standards, commands, detection, init/CLI notes, and troubleshooting.
DB & Redis Layer
src/db/schema.ts, src/db/db.ts, src/db/redis.ts, src/db/functions/*
Switch from schema.* namespace to direct *Table/*TableTypes imports; add ensureDbInitialized() export; change Redis init to synchronous model and introduce batched SCAN/DEL helpers and restore logic; update many DB function signatures/return types.
Logging & Telemetry
src/util/logger.ts, src/util/logging/*, src/util/telemetry/otelLogTransport.ts
Reimplement logger shutdown/flush behavior; add OTEL attribute helpers; extract log construction into new handler modules (channel/member/message/moderation/purge/role), add purge-file logging and new handler APIs.
Counting & Achievement subsystem
src/util/counting/*, src/util/counting/types.ts, src/util/achievementManager.ts, src/util/achievementCardGenerator.ts, src/commands/fun/achievements.ts, src/commands/fun/counting.ts
Add bannedMeta/bannedUsers/totalCorrect, introduce math-expression validation regexes, centralize sanitize/eval and rollback/reset logic, refactor achievement awarding and command handlers, return inserted fact IDs.
Giveaways
src/util/giveaways/*, src/commands/fun/giveaway.ts
Add bonusEntries (roles/levels/messages), channelId, winnerCount; refactor embed builders, session/handler logic, requirement validation, and reorganize exports into barrels.
Commands: guards & responses
src/commands/**/* (many files)
Standardize execution: use validateInteraction at entry, centralize replies with safelyRespond, extract subcommand handlers, add pagination helpers, consistent ephemeral/error flows across moderation, util, fun, testing commands.
Events & utilities
src/events/*, src/util/*
Add message restoration helpers for counting; getPermissionTarget and permission-diff handling; FACT_MODERATION_REGEX; convert scheduleFactOfTheDay to void; refactor data-retention into helpers and switch queries to table imports; various helper API/format changes.
Types & public surface
src/types/*, src/structures/ExtendedClient.ts, src/db/functions/*, src/util/deployCommand.ts
Adjust exported types and interfaces (Config restructure, new fields, DeployStateEntry.commandCount), derive table types via InferSelectModel, make some class props readonly, and update many DB function signatures.
Minor docs & formatting
drizzle.config.ts, CONTRIBUTING.md, README.md, .github/copilot-instructions.md, LICENSE, SECURITY.md
Update contributor docs and links, bump Yarn requirement, replace ESLint references with Ultracite, minor formatting/console/log string tweaks.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant CLI as "yarn check/fix"
    participant Ultracite
    participant Biome as "Biome (engine)"
    participant Config as "biome.jsonc"

    User->>CLI: run lint/format command
    CLI->>Ultracite: invoke ultracite
    Ultracite->>Config: load biome.jsonc
    Config-->>Ultracite: return extensions/rules
    Ultracite->>Biome: delegate analysis/fixes
    Biome-->>Ultracite: return diagnostics/fixes
    Ultracite->>CLI: report/apply fixes
    CLI-->>User: display results
Loading
sequenceDiagram
    participant Git
    participant Husky
    participant LintStaged
    participant Ultracite
    participant TSC as "TypeScript (tsc)"

    Git->>Husky: pre-commit hook triggers
    Husky->>LintStaged: run lint-staged
    LintStaged->>Ultracite: run `yarn ultracite fix` on staged files
    Ultracite-->>LintStaged: apply fixes
    LintStaged->>TSC: run `yarn type-check` for staged .ts
    TSC-->>LintStaged: return type results
    LintStaged-->>Husky: finish
    Husky-->>Git: allow or block commit
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • cubic-dev-ai

Poem

🐰 I hopped through diffs both wide and deep,
Swapped lint for Ultracite, no configs to keep.
Biome hums, handlers sing, types aligned with care,
Redis, DB, and giveaways — tidied everywhere.
Hop, patch, commit — a carrot-coded affair! 🥕

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ahmadk953/biome-ultracite

@github-actions
Copy link
Copy Markdown

github-actions Bot commented Mar 18, 2026

Ultracite report

Found 0 issue(s): ERROR 0 | WARN 0 | NOTE 0

View workflow run details

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR migrates the repo’s code-quality toolchain from ESLint/Prettier to Ultracite (Biome backend), updating CI/editor/docs accordingly, and applies a large set of auto-format/refactor changes across the TypeScript codebase to match the new standards.

Changes:

  • Replace ESLint/Prettier with Ultracite/Biome (new biome.jsonc, new scripts, updated lint-staged/Husky, updated CI workflow).
  • Apply Biome-driven formatting and some small refactors across many src/ modules (logging, giveaways, counting, commands, db functions, etc.).
  • Add/expand Ultracite standards documentation and agent/editor hook configuration.

Reviewed changes

Copilot reviewed 108 out of 109 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
src/util/undeployCommands.ts Formatting-only change (logger call punctuation/line breaks).
src/util/telemetry/otelLogTransport.ts Refactors/formatting; extracts helper functions for OTEL attribute cleaning.
src/util/notificationHandler.ts Replaces enum with as const object + type alias; formatting tweaks.
src/util/logging/utils.ts Type/loop refactors (for...of), tweaks to channel field typing, import order changes.
src/util/logging/types.ts Reorders interface members/types for consistency with formatter.
src/util/logging/handlers/roleHandlers.ts New logging handler module for role-related audit log embeds.
src/util/logging/handlers/purgeHandler.ts New purge transcript generation + attachment send helper.
src/util/logging/handlers/moderationHandlers.ts New handler module for moderation/counting log embed fields.
src/util/logging/handlers/messageHandlers.ts New handler module for message delete/edit embed fields and link button.
src/util/logging/handlers/memberHandlers.ts New handler module for member join/leave + name-change embed fields.
src/util/logging/handlers/channelHandlers.ts New handler module for channel update/create/delete embed fields (incl. perm diffs).
src/util/logging/constants.ts Formatting + numeric separator style for embed colors.
src/util/logger.ts Refactors fatal error handling/transport flush logic; formatting updates.
src/util/levelingSystem.ts Imports/schema usage cleanup, formatting, minor TS typing adjustments.
src/util/giveaways/utils.ts Formatting + small control-flow refactors in requirements/session helpers.
src/util/giveaways/types.ts Reorders interface members to match formatter rules.
src/util/giveaways/modals.ts Formatting-only changes (trailing commas/line breaks).
src/util/giveaways/dropdowns.ts Formatting-only changes (trailing commas/line breaks).
src/util/giveaways/builder.ts Formatting + minor boolean expression refactors and numeric separators.
src/util/factManager.ts Changes scheduling function signature + formatting updates.
src/util/eventLoader.ts Import ordering + formatting tweaks.
src/util/deployCommand.ts Formatting + minor control-flow simplification; numeric separators; import ordering.
src/util/counting/types.ts Reorders interface members to match formatter rules.
src/util/counting/helpers.ts Formatting + adds top-level regex constants and refactors some logic structure.
src/util/counting/durationUtils.ts Formatting-only changes.
src/util/counting/constants.ts Formatting-only changes.
src/util/configLoader.ts Import ordering + formatting tweaks.
src/util/achievementManager.ts Formatting + minor control-flow refactors; safer parseInt usage.
src/util/achievementCardGenerator.ts Import ordering + formatting tweaks.
src/types/EventTypes.ts Reorders interface members.
src/types/ConfigTypes.ts Reorders interface members (no semantic change intended).
src/structures/ExtendedClient.ts Makes some properties readonly; formatting tweaks.
src/events/roleEvents.ts Import ordering tweak.
src/events/ready.ts Import ordering + removes await on sync redis connect + fact scheduling call update.
src/events/reactionEvents.ts Import ordering + early return formatting.
src/events/memberEvents.ts Import ordering + early return formatting.
src/events/interactionCreate.ts Refactors control flow, introduces constant regex, formatting updates.
src/events/channelEvents.ts Refactors permission change detection helpers and formatting.
src/discord-bot.ts Formatting and minor control-flow brace changes.
src/db/schema.ts Import ordering + interface member ordering + formatting.
src/db/functions/utils/moderationUtils.ts Formatting + == null simplification.
src/db/functions/moderationFunctions.ts Switch from schema.* namespace usage to named imports; formatting.
src/db/functions/memberFunctions.ts Switch from schema.* namespace usage to named imports; formatting.
src/db/functions/factFunctions.ts Switch from schema.* namespace usage to named imports; formatting/structure.
src/db/functions/achievementFunctions.ts Switch from schema.* namespace usage to named imports; formatting.
src/commands/util/xp.ts Formatting + early-return braces + minor string formatting changes.
src/commands/util/server.ts Formatting + early-return braces.
src/commands/util/rules.ts Formatting + numeric separators for colors.
src/commands/util/restart.ts Import ordering + early-return braces + formatting.
src/commands/util/reload-config.ts Import ordering + early-return braces + numeric separators for colors.
src/commands/util/recalculate-levels.ts Early-return braces + formatting.
src/commands/util/ping.ts Formatting-only change.
src/commands/util/members.ts Refactors pagination handler logic slightly; formatting + numeric separators.
src/commands/util/help.ts Refactors category rendering loops + changes handleSpecificCommand call style.
src/commands/util/backend-manager.ts Formatting + adds default subcommand handling; minor status color refactor.
src/commands/testing/test-leave.ts Early-return braces + formatting.
src/commands/testing/test-join.ts Early-return braces + formatting.
src/commands/moderation/warn.ts Import ordering + early-return braces + formatting.
src/commands/moderation/unmute.ts Import ordering + early-return braces + formatting.
src/commands/moderation/unban.ts Import ordering + early-return braces + formatting.
src/commands/moderation/mute.ts Import ordering + early-return braces + formatting.
src/commands/moderation/kick.ts Import ordering + early-return braces + formatting.
src/commands/moderation/ban.ts Import ordering + early-return braces + formatting.
src/commands/fun/rank.ts Import ordering + early-return braces + formatting.
src/commands/fun/leaderboard.ts Refactors pagination handling functions; formatting + numeric separators.
src/commands/fun/giveaway.ts Import ordering + adds default subcommand handling; formatting + numeric separators.
skills-lock.json Adds Ultracite “skills” lock metadata.
package.json Replaces lint/format scripts with check/fix/type-check; swaps deps to Biome/Ultracite.
eslint.config.mjs Removes ESLint flat config (migration away from ESLint).
drizzle.config.ts Formatting-only changes.
biome.jsonc Adds Biome config extending Ultracite preset + import organization settings.
README.md Updates developer commands to check/fix/type-check.
CONTRIBUTING.md Updates contribution workflow docs for Ultracite/Biome and new scripts.
AGENTS.md Adds Ultracite standards reference document.
.prettierrc.yml Removes Prettier config.
.prettierignore Removes Prettier ignore file.
.lintstagedrc.mjs Migrates lint-staged tasks to Ultracite + separate TS type-check trigger.
.husky/commit-msg Updates commit-msg hook command invocation.
.github/workflows/ultracite.yml Adds CI workflow for Ultracite/Biome check + SARIF upload + PR comment.
.github/workflows/eslint.yml Removes ESLint workflow.
.github/pull_request_template.md Updates testing checklist to new scripts.
.github/hooks/ultracite.json Adds post-tool-use hook to auto-run yarn fix (Ultracite).
.github/copilot-instructions.md Updates documented workflow commands and adds Ultracite standards reference.
.github/PULL_REQUEST_TEMPLATE/refactor.md Updates template checklists for new scripts.
.github/PULL_REQUEST_TEMPLATE/feature.md Updates template checklists for new scripts.
.github/PULL_REQUEST_TEMPLATE/bug_fix.md Updates template checklists for new scripts.
.devcontainer/devcontainer.json Switches formatter/extensions to Biome + updates save actions.
.devcontainer/README.md Updates devcontainer tool description to Ultracite/Biome.
.agents/skills/ultracite/references/code-standards.md Adds detailed Ultracite/Biome code standards reference.
.agents/skills/ultracite/SKILL.md Adds Ultracite skill documentation for agents/tools.

Comment thread src/commands/util/help.ts
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 issues found across 109 files

Confidence score: 2/5

  • There are multiple high-confidence, user-impacting risks, especially in src/commands/fun/fact.ts where using global MAX(id) is race-prone and can attach moderation buttons to the wrong fact under concurrent submissions.
  • src/events/messageEvents.ts and src/util/counting/countingManager.ts introduce concrete counting correctness issues (event-order reordering and invalid non-numeric entries being silently ignored), which can cause visible behavior regressions.
  • Operational safety is also weakened by logging raw message content in src/util/counting/countingManager.ts and allowing Biome failures to be ignored in .github/workflows/ultracite.yml, increasing privacy and quality-gate risk.
  • Pay close attention to src/commands/fun/fact.ts, src/events/messageEvents.ts, src/util/counting/countingManager.ts, and .github/workflows/ultracite.yml - race conditions, counting-state integrity, sensitive logging, and CI enforcement are the main merge risks.

Note: This PR contains a large number of files. cubic only reviews up to 75 files per PR, so some files may not have been reviewed.

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/util/counting/countingManager.ts">

<violation number="1" location="src/util/counting/countingManager.ts:197">
P1: Custom agent: **Avoid Logging Sensitive Information**

Do not log raw message content; it can contain sensitive user data. Log only non-sensitive metadata instead.</violation>

<violation number="2" location="src/util/counting/countingManager.ts:200">
P1: Non-numeric counting messages are marked as `ignored`, which bypasses the invalid-number reset path and lets invalid entries pass silently.</violation>
</file>

<file name="src/events/messageEvents.ts">

<violation number="1" location="src/events/messageEvents.ts:321">
P1: Awaiting leveling before enqueuing counting messages can reorder counting processing under concurrent `messageCreate` events. Keep leveling non-blocking so queue insertion happens immediately in event order.</violation>
</file>

<file name=".lintstagedrc.mjs">

<violation number="1" location=".lintstagedrc.mjs:2">
P2: Build the lint-staged command without manually joining raw filenames; unquoted filenames can break on spaces/special characters.</violation>
</file>

<file name="src/commands/fun/fact.ts">

<violation number="1" location="src/commands/fun/fact.ts:218">
P1: Fetching the new fact ID via global `MAX(id)` is race-prone and can link moderation buttons to the wrong fact under concurrent submissions.</violation>
</file>

<file name="package.json">

<violation number="1" location="package.json:20">
P3: `husky` is configured in both `postinstall` and `prepare`, which duplicates hook installation on dependency install.</violation>
</file>

<file name="src/commands/util/help.ts">

<violation number="1" location="src/commands/util/help.ts:50">
P2: Await the `handleSpecificCommand` promise so reply errors are caught by this command’s `try/catch` instead of becoming unhandled rejections.</violation>
</file>

<file name=".devcontainer/devcontainer.json">

<violation number="1" location=".devcontainer/devcontainer.json:70">
P2: YAML files are configured to use Biome as formatter even though Biome YAML support is not fully supported/stable; this can break format-on-save for YAML configs.</violation>
</file>

<file name=".github/workflows/ultracite.yml">

<violation number="1" location=".github/workflows/ultracite.yml:40">
P1: Do not ignore Biome CI failures; this allows the quality workflow to pass even when Biome reports error-level violations.</violation>
</file>
Architecture diagram
sequenceDiagram
    participant Dev as Developer
    participant IDE as VS Code (Biome Ext)
    participant Git as Git Hooks (Husky)
    participant Ultra as Ultracite CLI (Biome)
    participant CI as GitHub Actions
    participant GH as GitHub PR UI

    Note over Dev, IDE: Local Development Flow
    Dev->>IDE: Save changed file
    IDE->>Ultra: NEW: Format and fix (source.fixAll.biome)
    Ultra-->>IDE: Applied fixes
    IDE-->>Dev: Clean code in editor

    Note over Dev, Ultra: Pre-commit Flow (Staged Files)
    Dev->>Git: git commit
    Git->>Ultra: CHANGED: yarn ultracite fix
    Ultra->>Ultra: Format & Lint staged files
    alt NEW: Files are .ts
        Git->>Ultra: NEW: yarn type-check (tsc --noEmit)
    end
    Ultra-->>Git: Success/Fail
    Git-->>Dev: Commit accepted/rejected

    Note over Git, GH: Continuous Integration Flow
    Dev->>GH: Push to branch / Open PR
    GH->>CI: Trigger Ultracite Workflow
    CI->>Ultra: CHANGED: yarn check
    Ultra-->>CI: Lint/Format results (SARIF)
    
    alt NEW: PR Reporting
        CI->>GH: NEW: Post summary comment to PR
        CI->>GH: NEW: Upload SARIF to Code Scanning
    end

    alt CHANGED: Lint/Format Errors
        Ultra-->>CI: Exit Code 1
        CI-->>GH: Status Check: FAILED
    else Success
        Ultra-->>CI: Exit Code 0
        CI-->>GH: Status Check: PASSED
    end
Loading

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/util/counting/countingManager.ts Outdated
Comment thread src/events/messageEvents.ts
Comment thread src/commands/fun/fact.ts Outdated
Comment thread .github/workflows/ultracite.yml Outdated
Comment thread src/util/counting/countingManager.ts Outdated
Comment thread .lintstagedrc.mjs
Comment thread src/commands/util/help.ts Outdated
Comment thread .devcontainer/devcontainer.json
Comment thread package.json Outdated
@ahmadk953 ahmadk953 assigned Copilot and unassigned Copilot Mar 18, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (12)
src/util/deployCommand.ts (1)

82-99: ⚠️ Potential issue | 🟡 Minor

Validate and migrate persisted deploy-state entries before casting.

readDeployState returns parsed as DeployState without validating entry shape. Older state files (without commandCount) will deserialize successfully but violate the new DeployStateEntry contract and can leak undefined metadata.

Proposed fix
 const readDeployState = (): DeployState => {
@@
-    return parsed as DeployState;
+    const normalizedEntries = Object.entries(parsed).flatMap(([key, value]) => {
+      if (!value || typeof value !== 'object' || Array.isArray(value)) {
+        return [];
+      }
+
+      const maybe = value as Partial<DeployStateEntry>;
+      if (typeof maybe.hash !== 'string' || typeof maybe.updatedAt !== 'string') {
+        return [];
+      }
+
+      return [[
+        key,
+        {
+          hash: maybe.hash,
+          updatedAt: maybe.updatedAt,
+          commandCount:
+            typeof maybe.commandCount === 'number' ? maybe.commandCount : 0,
+        } satisfies DeployStateEntry,
+      ] as const];
+    });
+
+    return Object.fromEntries(normalizedEntries) as DeployState;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/deployCommand.ts` around lines 82 - 99, readDeployState currently
casts parsed JSON directly to DeployState, which allows older/malformed entries
(missing DeployStateEntry fields like commandCount) to slip through; update
readDeployState to iterate the parsed object entries, validate each value is an
object, ensure required fields (e.g., commandCount) are present (initialize
defaults such as commandCount = 0 and normalize metadata), discard or log
malformed entries via logger.warn, and return the reconstructed DeployState map
so the returned type conforms to DeployState/DeployStateEntry and prevents
undefined metadata from leaking.
src/commands/fun/leaderboard.ts (1)

104-123: ⚠️ Potential issue | 🟠 Major

Guard against select-menu option overflow (>25 pages).

With getLevelLeaderboard(100) and usersPerPage configurable as low as 1 (via the limit option), pages can reach 100, exceeding Discord's 25-option select menu limit. This breaks the dropdown at lines 122 and 190.

Conditionally exclude the select menu when pages.length > 25, falling back to button-only navigation:

Suggested fix
+      const shouldShowSelectMenu = pages.length > 1 && pages.length <= 25;
       const components =
-        pages.length > 1 ? [getButtonActionRow(), getSelectMenuRow()] : [];
+        pages.length > 1
+          ? shouldShowSelectMenu
+            ? [getButtonActionRow(), getSelectMenuRow()]
+            : [getButtonActionRow()]
+          : [];

@@
         await i.update({
           embeds: [pages[currentPage]],
-          components: [getButtonActionRow(), getSelectMenuRow()],
+          components: shouldShowSelectMenu
+            ? [getButtonActionRow(), getSelectMenuRow()]
+            : [getButtonActionRow()],
         });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/fun/leaderboard.ts` around lines 104 - 123, The select menu can
exceed Discord's 25-option limit when pages.length > 25; update the logic around
getSelectMenuRow, pages, and components so you only build/add the
StringSelectMenuBuilder if pages.length <= 25 (otherwise omit it and rely on
getButtonActionRow for navigation); ensure getSelectMenuRow checks pages.length
and components is computed as pages.length > 1 ? (pages.length <= 25 ?
[getButtonActionRow(), getSelectMenuRow()] : [getButtonActionRow()]) : [] and
apply the same guard wherever the select is recreated/used (e.g., the code paths
that rebuild components later such as the handler referencing getSelectMenuRow).
src/commands/util/reload-config.ts (1)

78-89: ⚠️ Potential issue | 🟠 Major

Don't send the raw reload exception back to Discord.

error can include parser internals, file paths, and config values. Keep the detailed exception in logs and return a generic admin-facing failure message here.

Suggested fix
       const errorEmbed = new EmbedBuilder()
         .setTitle('❌ Configuration Reload Failed')
         .setColor(0xff_00_00)
-        .setDescription(
-          `Failed to reload configuration from disk:\n\`\`\`${error}\`\`\``
-        )
+        .setDescription(
+          'Failed to reload configuration from disk. Check the server logs for details.'
+        )
         .setTimestamp();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/reload-config.ts` around lines 78 - 89, The current error
handling in reload-config.ts builds errorEmbed with the raw `error` and sends it
via `interaction.editReply`; instead log the full `error` details (e.g., using
your logger or console.error) and change the EmbedBuilder description to a
generic admin-facing message like "Failed to reload configuration. Check server
logs for details." before calling `interaction.editReply` so no parser
internals, file paths, or config values are exposed to Discord.
src/util/giveaways/utils.ts (1)

40-44: ⚠️ Potential issue | 🟠 Major

The current winner shuffle is biased and unfair for giveaway selection.

The comparator sort(() => 0.5 - Math.random()) does not produce a uniform permutation. This violates the comparator consistency contract and results in some participants being favored over others—a critical issue for fairness-dependent giveaway selection. Use Fisher-Yates instead.

Suggested fix
-  const shuffled = uniqueParticipants.sort(() => 0.5 - Math.random());
-  return shuffled.slice(0, actualWinnerCount);
+  const shuffled = [...uniqueParticipants];
+  for (let i = shuffled.length - 1; i > 0; i -= 1) {
+    const j = Math.floor(Math.random() * (i + 1));
+    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+  }
+  return shuffled.slice(0, actualWinnerCount);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/giveaways/utils.ts` around lines 40 - 44, The current unbiased
issue: replace the unsafe sort(() => 0.5 - Math.random()) approach (used on
uniqueParticipants to produce shuffled and then slice by actualWinnerCount) with
a proper Fisher‑Yates shuffle to produce a uniform random permutation; implement
a function or inline logic that copies uniqueParticipants (to avoid mutating
original if needed), performs an in-place Fisher‑Yates swap loop to shuffle the
array, then return the first actualWinnerCount entries from that shuffled array
(references: uniqueParticipants, actualWinnerCount, shuffled).
src/commands/util/backend-manager.ts (1)

145-166: ⚠️ Potential issue | 🟠 Major

This reconnect path can report a false failure.

ensureRedisConnection() now only starts the reconnect attempt; it does not wait for Redis to be ready. Checking isRedisConnected() immediately after it will usually still be false, so /backend-manager redis can tell admins the reconnect failed even when Redis comes back seconds later. Either wait for a concrete readiness check/ping, or phrase the response as “reconnect initiated.”

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/backend-manager.ts` around lines 145 - 166, The reconnect
handler currently calls ensureRedisConnection() then immediately checks
isRedisConnected(), which can report a false failure because
ensureRedisConnection() merely starts async reconnecting; update
handleRedisReconnect to either (A) await a concrete readiness check (e.g., call
a new or existing ping/waitForReady function on the Redis client and only then
call isRedisConnected()) so the success/failure messages reflect actual
connectivity, or (B) change the replies to indicate the reconnect was initiated
(e.g., "Reconnect initiated; will report when connection is restored") and avoid
declaring failure immediately; reference handleRedisReconnect,
ensureRedisConnection, and isRedisConnected when making the change.
src/db/functions/giveawayFunctions.ts (1)

180-198: ⚠️ Potential issue | 🟠 Major

Avoid read-modify-write on participants.

This path reads the participant array, mutates it in memory, and writes it back outside a transaction. Two users joining at the same time can read the same old array and the later update will overwrite the earlier one, silently dropping entries. Move the duplicate check + append into a transaction or an atomic database update.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/functions/giveawayFunctions.ts` around lines 180 - 198, The current
getGiveaway -> read/modify/write flow (involving getGiveaway, participants,
db.update and giveawayTable) is racy: replace it with an atomic operation or run
the duplicate check + append inside a transaction so concurrent joins cannot
clobber each other; specifically, perform the existence/status check and the
"already_entered" check and the append of `userId` (repeated `entries` times)
inside a db.transaction or use a single atomic DB update that conditionally
appends to the participants array (e.g., using the DB/ORM's array append/concat
operator with a WHERE that ensures status='active' and the user isn't already
present), and return appropriate results when the update affects zero rows to
indicate inactive/already_entered.
src/db/functions/moderationFunctions.ts (1)

73-109: ⚠️ Potential issue | 🟡 Minor

Inconsistent error handling: ensureDbInitialized() is outside the try-catch block.

In getMemberModerationHistory, ensureDbInitialized() (line 76) and the subsequent db check are outside the try-catch, unlike updateMemberModerationHistory which wraps them. If ensureDbInitialized() throws, it won't be caught and handled by handleDbError.

🛠️ Proposed fix to wrap initialization in try-catch
 export async function getMemberModerationHistory(
   discordId: string
 ): Promise<moderationTableTypes[]> {
-  await ensureDbInitialized();
-
-  if (!db) {
-    logger.error(
-      '[moderationDbFunctions] Database not initialized, cannot get member moderation history'
-    );
-    throw new Error('Database not initialized');
-  }
-
-  const cacheKey = `${discordId}-moderationHistory`;
-
   try {
+    await ensureDbInitialized();
+
+    if (!db) {
+      logger.error(
+        '[moderationDbFunctions] Database not initialized, cannot get member moderation history'
+      );
+      throw new Error('Database not initialized');
+    }
+
+    const cacheKey = `${discordId}-moderationHistory`;
+
     const moderationHistory = await withCache<moderationTableTypes[]>(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/functions/moderationFunctions.ts` around lines 73 - 109, The function
getMemberModerationHistory currently calls ensureDbInitialized() and checks db
outside of the try/catch, so initialization errors bypass handleDbError; move
the ensureDbInitialized() call and the subsequent db existence check inside the
try block that surrounds the cache/db operations (or wrap the whole function
body in a try/catch), so any errors from ensureDbInitialized() or the db check
are caught and passed to handleDbError('Failed to get moderation history',
error); keep the rest of the logic (cacheKey, withCache, withDbRetryDrizzle, and
normalizeModerationDates) unchanged and mirror the error handling pattern used
by updateMemberModerationHistory.
src/commands/moderation/ban.ts (1)

72-77: ⚠️ Potential issue | 🟡 Minor

Fix the indefinite-ban DM copy.

The string at Line 76 says "You been indefinitely banned...", which will be sent verbatim to users.

💬 Suggested copy fix
-            : `You been indefinitely banned from ${guild.name}. Reason: ${reason}.`
+            : `You have been indefinitely banned from ${guild.name}. Reason: ${reason}.`
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/moderation/ban.ts` around lines 72 - 77, In the ban DM send
logic inside src/commands/moderation/ban.ts (the member.user.send call), fix the
indefinite-ban message branch so it reads correctly (e.g., "You have been
indefinitely banned from ${guild.name}. Reason: ${reason}.") instead of the
current "You been indefinitely banned..."; update only the string used when
banDuration is falsy in the ternary so grammar is corrected and punctuation
stays consistent with the temporary-ban message.
src/discord-bot.ts (1)

9-19: ⚠️ Potential issue | 🟠 Major

Make the fallback logger non-throwing.

JSON.stringify still throws on circular objects and BigInt. If startup fails with one of those payloads, this fallback path can crash while handling the original error, which defeats the whole point of having a last-resort logger.

🛟 Suggested hardening
+const safeSerialize = (value: unknown): string => {
+  if (value instanceof Error) {
+    return value.stack ?? value.message;
+  }
+  if (typeof value === 'bigint') {
+    return value.toString();
+  }
+  if (value && typeof value === 'object') {
+    try {
+      return JSON.stringify(value);
+    } catch {
+      return '[unserializable object]';
+    }
+  }
+  return String(value);
+};
+
 const _fallbackLogger = {
   log: (level: string, message?: unknown, ...meta: unknown[]) => {
     const prefix = level ? `[${level}]` : '[log]';
-    const parts = [prefix, message];
+    const parts = [prefix];
+    if (typeof message !== 'undefined') {
+      parts.push(safeSerialize(message));
+    }
     if (meta?.length) {
-      parts.push(
-        ...meta.map((m) =>
-          typeof m === 'object' ? JSON.stringify(m) : String(m)
-        )
-      );
+      parts.push(...meta.map(safeSerialize));
     }
     process.stderr.write(`${parts.join(' ')}\n`);
   },
   error: (message?: unknown, ...meta: unknown[]) => {
-    const parts = [message];
+    const parts: string[] = [];
+    if (typeof message !== 'undefined') {
+      parts.push(safeSerialize(message));
+    }
     if (meta?.length) {
-      parts.push(
-        ...meta.map((m) =>
-          typeof m === 'object' ? JSON.stringify(m) : String(m)
-        )
-      );
+      parts.push(...meta.map(safeSerialize));
     }
     process.stderr.write(`${parts.join(' ')}\n`);
   },
 };

Also applies to: 21-30

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/discord-bot.ts` around lines 9 - 19, The fallback logger function log
currently uses JSON.stringify which can throw on circular structures or BigInt;
wrap the serialization in a try/catch inside the log function (and any sibling
fallback logger block at lines 21-30) so that if JSON.stringify fails you fall
back to a safe serializer: convert BigInt to string and replace circular
references or non-serializable values with a safe placeholder (or simply call
String(value)) and ensure the catch branch builds parts without throwing;
finally always call process.stderr.write with the assembled string so the logger
never throws while handling errors.
src/util/giveaways/handlers.ts (1)

317-330: ⚠️ Potential issue | 🟡 Minor

Inconsistent session expiry handling.

Unlike other handlers that use getSessionOrReplyExpired, this function uses getSession directly and silently returns if the session is null. This means the user won't be notified that their session expired.

🔧 Proposed fix for consistency
 export async function handlePingRoleSelect(
   interaction: StringSelectMenuInteraction
 ): Promise<void> {
   const roleId = interaction.values[0];
-  const session = await getSession(interaction.user.id);
+  const session = await getSessionOrReplyExpired(interaction);

-  if (!session) {
-    return;
-  }
+  if (!session) {
+    return;
+  }

   session.pingRoleId = roleId;
   await saveSession(interaction.user.id, session);
   await showBuilderStep(interaction, session);
 }

Note: This requires changing the function signature slightly since getSessionOrReplyExpired expects a BuilderInteraction type. You may need to extend the BuilderInteraction type alias to include StringSelectMenuInteraction.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/giveaways/handlers.ts` around lines 317 - 330, handlePingRoleSelect
currently calls getSession and silently returns on null, causing no user
feedback on expired sessions; change it to use getSessionOrReplyExpired so the
user is notified. Update the handlePingRoleSelect signature to accept the
BuilderInteraction type (or expand the BuilderInteraction type alias to include
StringSelectMenuInteraction), then replace the getSession call with await
getSessionOrReplyExpired(interaction) and early-return only when that helper
indicates expiration; preserve the existing logic that sets session.pingRoleId,
calls saveSession and then showBuilderStep.
src/util/counting/helpers.ts (1)

57-68: ⚠️ Potential issue | 🟠 Major

Backfill totalCorrect during migration.

totalCorrect was added to the migrated shape, but this function never marks the payload as changed when that field is missing, and finalData still falls back to data.totalCorrect. For legacy Redis blobs, the next successful count turns undefined += 1 into NaN, so the stat stays corrupted.

💡 Minimal fix
   if (
     !mutableData.mistakeTracker ||
     typeof mutableData.mistakeTracker !== 'object' ||
     Array.isArray(mutableData.mistakeTracker)
   ) {
     mutableData.mistakeTracker = {};
     changed = true;
   }
+
+  if (
+    typeof mutableData.totalCorrect !== 'number' ||
+    !Number.isFinite(mutableData.totalCorrect) ||
+    mutableData.totalCorrect < 0
+  ) {
+    mutableData.totalCorrect = 0;
+    changed = true;
+  }

   if (changed) {
     const finalData: CountingData = {
@@
-      totalCorrect:
-        typeof mutableData.totalCorrect === 'number'
-          ? mutableData.totalCorrect
-          : data.totalCorrect,
+      totalCorrect: mutableData.totalCorrect,

Also applies to: 101-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/counting/helpers.ts` around lines 57 - 68, The migrateData function
adds totalCorrect to the ExpectedCountingData shape but never treats a missing
totalCorrect as a migration change, leaving finalData to fall back to the
original data.totalCorrect (which can be undefined and later produce NaN).
Update migrateData so that when data.totalCorrect is missing or not a number you
set the migrated copy's totalCorrect to 0 and set the same "changed" flag used
elsewhere in this function to true so finalData is taken from the
validated/mutated copy (refer to the migrateData function, the
ExpectedCountingData interface, the local mutated copy and the
finalData/changing logic).
src/util/giveaways/builder.ts (1)

102-117: ⚠️ Potential issue | 🟠 Major

Catch block must handle deferred interactions with editReply().

After startGiveawayBuilder() defers the interaction (line 24), the error handler at line 107 checks only !interaction.replied before calling interaction.reply(). In the deferred state, this call fails silently, leaving the user without error feedback.

💡 Correct fallback
  } catch (error) {
    logger.error(
      '[GiveawayManager] Error displaying giveaway builder step',
      error
    );
+   if (interaction.deferred) {
+     try {
+       await interaction.editReply({
+         content: 'There was an error updating the giveaway builder.',
+         embeds: [],
+         components: [],
+       });
+     } catch (replyError) {
+       logger.error(
+         '[GiveawayManager] Failed to send error reply',
+         replyError
+       );
+     }
+   } else if (!interaction.replied) {
      try {
        await interaction.reply({
          content: 'There was an error updating the giveaway builder.',
          flags: ['Ephemeral'],
        });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/giveaways/builder.ts` around lines 102 - 117, The catch in
startGiveawayBuilder's error handling only checks interaction.replied and
attempts interaction.reply(), which fails for deferred interactions; change the
fallback to first check interaction.deferred (or interaction.replied) and call
interaction.editReply() when the interaction was deferred (or already replied),
otherwise call interaction.reply(); update the error branch around
logger.error('[GiveawayManager] Error displaying giveaway builder step', error)
and the nested reply attempt so it uses editReply for deferred interactions and
preserves the existing try/catch/logger.error('[GiveawayManager] Failed to send
error reply', replyError) behavior.
🟠 Major comments (23)
src/util/undeployCommands.ts-18-20 (1)

18-20: ⚠️ Potential issue | 🟠 Major

Use structured metadata in log calls.

Line 19 logs a plain string, which makes filtering and correlation harder. Please include metadata (at minimum guildId/clientId) and apply the same pattern to the other logger calls in this file.

🔧 Suggested change
-    logger.info(
-      '[UndeployCommands] Undeploying all commands from the Discord API...'
-    );
+    logger.info('[UndeployCommands] Undeploying all commands from the Discord API...', {
+      guildId,
+      clientId,
+    });

As per coding guidelines: "Use structured logging with metadata objects: logger.info('message', { userId, commandName, guildId }) and import logger from src/util/logger.js."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/undeployCommands.ts` around lines 18 - 20, The log call in
undeployCommands.ts uses a plain string; update logger calls (starting with the
logger.info('[UndeployCommands] Undeploying all commands...') invocation) to use
structured logging by passing a metadata object (at minimum { guildId, clientId
}) as the second argument and apply this pattern to every other logger.* call in
this file (e.g., any logger.info, logger.warn, logger.error occurrences); ensure
the file imports the standard logger from src/util/logger.js and include those
identifiers (guildId/clientId) available in the surrounding scope when
constructing the metadata object.
drizzle.config.ts-21-25 (1)

21-25: ⚠️ Potential issue | 🟠 Major

Fail fast in production when TLS cert loading fails.

Returning undefined for ssl silently downgrades DB connectivity to insecure mode. Keep this fallback only for local/dev, and throw in production.

Suggested hardening
       } catch (error) {
+        if (process.env.NODE_ENV === 'production') {
+          throw new Error(
+            'Missing database TLS certificate at ./certs/rootCA.pem; refusing insecure connection.'
+          );
+        }
         console.warn(
           'Failed to load certificates for database, using insecure connection:',
           error
         );
         return undefined;
       }

Based on learnings, production requires TLS certificate in certs/rootCA.pem.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@drizzle.config.ts` around lines 21 - 25, The current ssl certificate load
block (where console.warn is called and the code returns undefined) silently
falls back to insecure DB connections; update it so that when reading
certs/rootCA.pem fails you only fallback to insecure mode in local/dev (e.g.,
NODE_ENV === 'development' or a dedicated DEV flag) but in production
(process.env.NODE_ENV === 'production') throw an Error instead of returning
undefined; locate the ssl-loading logic (the block that logs "Failed to load
certificates for database, using insecure connection:" and the surrounding ssl
variable/return) and change the control flow to throw in production with a clear
message about missing certs/rootCA.pem while preserving the existing
non-throwing warning/fallback for development.
src/commands/util/recalculate-levels.ts-13-15 (1)

13-15: ⚠️ Potential issue | 🟠 Major

Use shared interaction helpers in execute (Line 13 onward).

Please replace the manual guard with await validateInteraction(interaction) and route replies through safelyRespond(...) for consistent interaction/error handling.

Suggested patch
 import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js';
 
 import type { Command } from '@/types/CommandTypes.js';
+import { safelyRespond, validateInteraction } from '@/util/helpers.js';
 import { recalculateUserLevels } from '@/util/levelingSystem.js';
 import { logger } from '@/util/logger.js';
@@
   execute: async (interaction) => {
-    if (!(interaction.isChatInputCommand() && interaction.guild)) {
+    if (!(await validateInteraction(interaction))) {
       return;
     }
 
     await interaction.deferReply({ flags: ['Ephemeral'] });
-    await interaction.editReply('Recalculating levels...');
+    await safelyRespond(interaction, 'Recalculating levels...');
@@
       await recalculateUserLevels();
-      await interaction.editReply('Levels recalculated successfully!');
+      await safelyRespond(interaction, 'Levels recalculated successfully!');
@@
-      await interaction.editReply('Failed to recalculate levels.');
+      await safelyRespond(interaction, 'Failed to recalculate levels.');
     }
   },
 };

As per coding guidelines: "Call await validateInteraction(interaction) before processing interactions" and "Use safelyRespond(interaction, content) from src/util/helpers.ts for all interaction responses."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/recalculate-levels.ts` around lines 13 - 15, Replace the
manual guard in execute with the shared helpers: call await
validateInteraction(interaction) at the start of the function to validate the
incoming interaction, and replace direct returns/responses with
safelyRespond(interaction, ...) so all replies and error messages use the
centralized handler; update any early-return logic that checked
interaction.isChatInputCommand() && interaction.guild to rely on
validateInteraction and ensure every place that previously used
interaction.reply or return now uses safelyRespond(interaction, { content:
"...", ephemeral: true }) or safelyRespond(interaction, error) as appropriate.
src/commands/util/user-info.ts-190-190 (1)

190-190: ⚠️ Potential issue | 🟠 Major

Use safelyRespond(...) instead of direct reply/editReply calls.

Line 190 and Line 238 directly call interaction.editReply(...); this bypasses the standardized response helper expected for command/event handlers.

Suggested change
+import { safelyRespond } from '@/util/helpers.js';
...
     if (!user) {
-      await interaction.editReply('User not found');
+      await safelyRespond(interaction, 'User not found');
       return;
     }
...
-    await interaction.editReply({ embeds: [embed] });
+    await safelyRespond(interaction, { embeds: [embed] });

As per coding guidelines: "Use safelyRespond(interaction, content) from src/util/helpers.ts for all interaction responses; it handles reply vs. follow-up vs. skip logic and logs Discord API errors".

Also applies to: 238-238

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/user-info.ts` at line 190, Replace direct calls to
interaction.editReply(...) in the user-info command with the standardized helper
safelyRespond(interaction, message): locate the occurrences where
interaction.editReply('User not found') and the other editReply at the other
branch are used, import safelyRespond from src/util/helpers.ts if not already
imported, and change those calls to await safelyRespond(interaction, 'User not
found') (and the corresponding message for the second editReply) so
reply/follow-up logic and error handling are handled consistently.
src/commands/util/user-info.ts-180-182 (1)

180-182: ⚠️ Potential issue | 🟠 Major

Add required validateInteraction gate before processing.

Line 180 currently does a manual guard, but this command still skips the required await validateInteraction(interaction) pre-check used across command handlers.

Suggested change
+import { validateInteraction } from '@/util/helpers.js';
...
   execute: async (interaction) => {
-    if (!(interaction.isChatInputCommand() && interaction.guild)) {
+    if (!(interaction.isChatInputCommand() && interaction.guild)) {
       return;
     }
+    if (!(await validateInteraction(interaction))) {
+      return;
+    }

     await interaction.deferReply();

As per coding guidelines: "Call await validateInteraction(interaction) before processing interactions to ensure the interaction is in-guild, channel is available, and for component interactions the original message is fetchable".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/user-info.ts` around lines 180 - 182, Replace the manual
guard that checks interaction.isChatInputCommand() && interaction.guild with the
standard pre-check: call await validateInteraction(interaction) at the start of
the handler (before any processing) and return early if it indicates the
interaction is invalid; remove or avoid duplicating the manual guard so the
interaction is validated using validateInteraction and subsequent code can
assume in-guild/channel/message availability. Ensure you reference the existing
validateInteraction function and the current interaction handling block (which
currently uses interaction.isChatInputCommand and interaction.guild) when making
the change.
src/commands/util/user-info.ts-194-194 (1)

194-194: ⚠️ Potential issue | 🟠 Major

Wrap DB access with consistent DB error handling.

Line 194 calls getMember(user.id) directly without a handleDbError(error, operation) path, which can leak inconsistent failures into the command flow.

Suggested change
-import { getMember } from '@/db/db.js';
+import { getMember, handleDbError } from '@/db/db.js';
...
-    const memberData = (await getMember(user.id)) ?? null;
+    let memberData: MemberData | null = null;
+    try {
+      memberData = (await getMember(user.id)) ?? null;
+    } catch (error) {
+      handleDbError(error, 'getMember(user.id)');
+      await safelyRespond(interaction, 'Failed to fetch user data.');
+      return;
+    }

As per coding guidelines: "Import database functions from src/db/db.ts, not direct DB access; use handleDbError(error, operation) for consistent error handling".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/user-info.ts` at line 194, The call to getMember(user.id)
in user-info command isn’t wrapped with the standardized DB error handling;
replace the direct call to getMember with a try/catch that uses
handleDbError(error, "getMember") (imported from src/db/db.ts per guidelines)
and return or rethrow/handle the error consistently so memberData is set only on
success (e.g., set memberData to null on handled DB errors); ensure you import
both getMember and handleDbError from the centralized DB module and apply this
pattern around the getMember(user.id) invocation in the file.
src/commands/fun/leaderboard.ts-32-37 (1)

32-37: ⚠️ Potential issue | 🟠 Major

Use the shared interaction guard/response helpers in this command path.

This flow still bypasses await validateInteraction(interaction) and directly replies in the collector path. Please route interaction handling through the shared helpers to match project standards and keep response/error behavior consistent.

As per coding guidelines, "Call await validateInteraction(interaction) before processing interactions" and "Use safelyRespond(interaction, content) from src/util/helpers.ts for all interaction responses."

Also applies to: 171-176

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/fun/leaderboard.ts` around lines 32 - 37, This command bypasses
the project's shared interaction helpers: call await
validateInteraction(interaction) immediately after confirming
interaction.isChatInputCommand() && interaction.guild, and replace direct uses
of interaction.deferReply()/interaction.reply() within the main flow and
collector path (including the block around lines 171-176) with the shared
helpers—use safelyRespond(interaction, { content }) for all responses and error
cases and use the standardized defer/ack flow provided by
validateInteraction/safelyRespond so responses and errors conform to project
standards; update any collector callback that replies directly to first await
validateInteraction(interaction) (or use the validated interaction) and then
call safelyRespond instead of interaction.reply/deferReply.
src/util/telemetry/otelLogTransport.ts-80-90 (1)

80-90: ⚠️ Potential issue | 🟠 Major

Avoid mapping generic name/stack metadata to exception fields unconditionally.

This currently rewrites ordinary metadata (e.g., name on non-error logs) into exception.type and drops the original key, which can corrupt telemetry dimensions.

💡 Suggested fix
-function extractCommonErrorField(
-  key: string,
-  value: unknown
-): Record<string, unknown> | null {
+function extractCommonErrorField(
+  key: string,
+  value: unknown,
+  allowErrorFieldProjection: boolean
+): Record<string, unknown> | null {
+  if (!allowErrorFieldProjection) {
+    return null;
+  }
   if (key === 'stack' && typeof value === 'string') {
     return { 'exception.stacktrace': value };
   }
   if (key === 'name' && typeof value === 'string') {
     return { 'exception.type': value };
   }
   return null;
 }
-function cleanAttributes(
-  meta: Record<string, unknown>
-): Record<string, unknown> {
+function cleanAttributes(
+  meta: Record<string, unknown>,
+  allowErrorFieldProjection = false
+): Record<string, unknown> {
   const out: Record<string, unknown> = {};
   for (const [k, v] of Object.entries(meta || {})) {
@@
-    const commonField = extractCommonErrorField(k, v);
+    const commonField = extractCommonErrorField(
+      k,
+      v,
+      allowErrorFieldProjection
+    );
-const attrs = cleanAttributes(meta as Record<string, unknown>);
+const attrs = cleanAttributes(
+  meta as Record<string, unknown>,
+  severityNumber >= 17
+);

Also applies to: 126-129

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/telemetry/otelLogTransport.ts` around lines 80 - 90, The
extractCommonErrorField function indiscriminately maps metadata keys 'stack' and
'name' to exception fields, which can corrupt non-error telemetry; update
extractCommonErrorField to only map when the value is provably an Error-like
value (e.g., for 'stack' require a string that looks like a stack trace such as
containing '\n' or ' at ', and for 'name' require the caller indicate the source
is an Error instance—add an optional parameter like isError:boolean or accept
the original value object to check instanceof Error), and when you do map
preserve the original key/value as well (return both the original key and the
mapped exception field) instead of dropping the original metadata; apply the
same guard logic to the other occurrence referenced around lines 126-129 so
non-error logs are not remapped.
src/commands/fun/fact.ts-191-196 (1)

191-196: ⚠️ Potential issue | 🟠 Major

Race condition: getLastInsertedFactId() can return wrong fact ID if concurrent submissions occur.

Between addFact() completing and getLastInsertedFactId() executing, another user could submit a fact. The MAX(id) query would then return the newer fact's ID, causing the approve/reject buttons to target the wrong submission.

Modify addFact() to return the inserted fact's ID using .returning():

export async function addFact({...}): Promise<number> {
  // ... existing code ...
  const result = await db.insert(factTable).values({...}).returning();
  return result[0].id;
}

Then use the returned ID directly instead of calling getLastInsertedFactId().

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/fun/fact.ts` around lines 191 - 196, The current flow is
vulnerable to a race because getLastInsertedFactId() can return a different
user's ID; change addFact(...) to return the newly inserted fact ID by using the
query builder's .returning() (e.g., have addFact return Promise<number> and
return result[0].id after insert into factTable), then update the caller (the
code that currently calls addFact({...}) and then calls getLastInsertedFactId())
to capture the returned id (e.g., const factId = await addFact(...)) and use
that factId for wiring approve/reject buttons instead of calling
getLastInsertedFactId().
src/commands/util/ping.ts-9-12 (1)

9-12: ⚠️ Potential issue | 🟠 Major

Use the shared interaction helpers here.

Line 10 still calls interaction.reply() directly, and this handler never invokes validateInteraction(interaction). That bypasses the repo’s guard/response flow and can break on already-acknowledged or invalid interactions. As per coding guidelines, "src/{commands,events}/**/*.ts: Use safelyRespond(interaction, content) from src/util/helpers.ts for all interaction responses" and "Call await validateInteraction(interaction) before processing interactions".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/ping.ts` around lines 9 - 12, In the execute handler
replace the direct interaction.reply call with the repo's helpers: first call
await validateInteraction(interaction) at the start of execute, then return
await safelyRespond(interaction, `🏓 Pong! Latency: ${Date.now() -
interaction.createdTimestamp}ms`); also add/import validateInteraction and
safelyRespond from src/util/helpers.ts and remove the direct interaction.reply
usage in the execute function.
src/commands/util/config.ts-121-129 (1)

121-129: ⚠️ Potential issue | 🟠 Major

Empty config sections can make /config fail.

join('\n') and rolesText can evaluate to '' for present-but-empty sections, so the ?? fallback never fires. Discord rejects empty embed field values, which will break this command on sparse configs.

Suggested fix
     embed.addFields({
       name: 'Channels',
-      value: channelsText ?? 'None configured',
+      value: channelsText || 'None configured',
     });
   }
@@
     embed.addFields({
       name: 'Roles',
-      value: rolesText ?? 'None configured',
+      value: rolesText || 'None configured',
     });
   }
@@
     embed.addFields({
       name: 'Counting',
-      value: countingText ?? 'Default settings',
+      value: countingText || 'Default settings',
     });
   }

Also applies to: 132-154, 177-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/config.ts` around lines 121 - 129, The embed field values
(e.g., channelsText computed from displayConfig.channels) can be an empty string
which bypasses the nullish coalescing and causes Discord to reject the embed;
update the code that builds each section (channelsText, rolesText and the other
similar variables used in embed.addFields) to treat empty strings as
absent—e.g., compute a value variable and if value === '' or value.trim() === ''
replace it with a fallback like 'None configured' before calling
embed.addFields; apply the same change where rolesText and the other sections
are assembled so no embed field value is ever an empty string.
src/db/db.ts-358-369 (1)

358-369: ⚠️ Potential issue | 🟠 Major

This CA filename no longer matches the repo's TLS setup.

The repo guidance points production PostgreSQL/Redis TLS at certs/psql-ca.pem, but this helper now reads rootCA.pem. That will push deployments into the insecure fallback path even when the expected certificate is present.

Suggested fix
 function loadDbSslOptions(): { ca: Buffer } | undefined {
   try {
     return {
-      ca: fs.readFileSync(path.resolve('./certs/rootCA.pem')),
+      ca: fs.readFileSync(path.resolve('./certs/psql-ca.pem')),
     };
   } catch (error) {

Based on learnings "Use PM2 process manager for production (yarn start uses process name poixpixel-discord-bot, yarn restart to reload); ensure TLS certificates are present at certs/psql-ca.pem for PostgreSQL and Redis".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/db.ts` around lines 358 - 369, The CA filename in loadDbSslOptions
should be changed from 'rootCA.pem' to the repo-standard 'psql-ca.pem' so the
function (loadDbSslOptions) reads the expected cert and doesn't fall back to
insecure mode; update the fs.readFileSync(path.resolve('./certs/rootCA.pem'))
call to read './certs/psql-ca.pem' (keeping the same try/catch and returned {
ca: Buffer } shape and logger.warn behavior on failure).
src/db/redis.ts-421-445 (1)

421-445: ⚠️ Potential issue | 🟠 Major

Don't report a successful flush after hard delete failures.

If both DEL and UNLINK fail for a batch, the helper only logs and continues, but flushRedisCache() still reaches the success log. That can tell admins the cache was flushed even though keys were left behind. Bubble the failure, or aggregate batch errors and fail the flush at the end.

Also applies to: 492-494

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/redis.ts` around lines 421 - 445, The deleteKeysInBatches helper
currently swallows failures when both redis.del and redis.unlink fail, which
allows flushRedisCache to incorrectly log success; modify deleteKeysInBatches
(the function handling redis.del and redis.unlink) to propagate errors instead
of just logging: either throw the batch error immediately when both operations
fail or collect batch-level errors and throw an aggregated Error after the loop
so flushRedisCache can catch it and log/handle failure; ensure you update
flushRedisCache to catch the thrown error and avoid logging a successful flush
when deletion errors occur. Use the existing symbols: deleteKeysInBatches,
redis.del, redis.unlink, flushRedisCache, and logger.error to locate and
implement the change.
src/commands/util/backend-manager.ts-56-61 (1)

56-61: ⚠️ Potential issue | 🟠 Major

Validate the interaction before the first reply.

The command now exits on the chat-input/guild guard only, then goes straight into deferReply(). Please keep the repo-standard validateInteraction() check here as well so unavailable channels/interactions are rejected before the first Discord API call.

As per coding guidelines, "Call await validateInteraction(interaction) before processing interactions to ensure the interaction is in-guild, channel is available, and for component interactions the original message is fetchable".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/backend-manager.ts` around lines 56 - 61, The execute
handler currently checks interaction.isChatInputCommand() and interaction.guild
but calls await interaction.deferReply(...) before calling validateInteraction;
add a call to await validateInteraction(interaction) immediately after the
existing guard and before deferReply so the interaction/channel availability and
component state are validated first. Specifically, update the execute function
(the method named execute) to invoke validateInteraction(interaction) after the
guild/chat-input guard and only then call deferReply, ensuring
validateInteraction is awaited and any failed validation returns early.
src/commands/util/restart.ts-22-27 (1)

22-27: ⚠️ Potential issue | 🟠 Major

Validate the interaction before deferring.

This path still goes straight from the chat-input/guild guard into deferReply(). If the interaction is stale or its channel is unavailable, the first response call will fail instead of short-circuiting cleanly.

As per coding guidelines, "Call await validateInteraction(interaction) before processing interactions to ensure the interaction is in-guild, channel is available, and for component interactions the original message is fetchable".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/restart.ts` around lines 22 - 27, The execute handler
currently defers the reply immediately after the chat-input/guild guard; insert
a call to await validateInteraction(interaction) right after that guard and
before calling interaction.deferReply, and if validateInteraction returns false
(or throws a handled validation error) short-circuit and return early; update
the execute function to use validateInteraction(interaction) to ensure the
interaction and channel are valid before calling interaction.deferReply or
further processing.
src/commands/util/help.ts-48-50 (1)

48-50: ⚠️ Potential issue | 🟠 Major

Add await to handle editReply() errors in specific-command path.

handleSpecificCommand() returns a promise from interaction.editReply() on both code paths (lines 202–204 and 253). Line 50 drops the await, meaning if Discord's API rejects, the error occurs outside the surrounding try/catch block and surfaces as an unhandled promise rejection.

Fix
-        handleSpecificCommand(interaction, client, commandName);
+        await handleSpecificCommand(interaction, client, commandName);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/help.ts` around lines 48 - 50, The specific-command branch
drops the await when calling handleSpecificCommand after deferring the reply, so
any rejection from interaction.editReply() escapes the surrounding try/catch;
change the call in the if (commandName) branch to await
handleSpecificCommand(interaction, client, commandName) so the returned promise
is awaited and errors are caught by the enclosing try/catch that surrounds
interaction.deferReply and subsequent logic.
src/db/schema.ts-13-21 (1)

13-21: ⚠️ Potential issue | 🟠 Major

These hand-written row types are already out of sync with the schema definitions.

  • memberTableTypes.currentlyInServer and discordUsername are optional in the interface despite notNull() in the table definition (lines 26-27)
  • levelTableTypes.lastMessageTimestamp is typed as optional Date but should be Date | null since the column is nullable (line 52)
  • moderationTableTypes.duration is required in the interface despite the column lacking notNull() and defaulting to empty string (line 75)

Downstream DB functions cast query results using as levelTableTypes[] and as moderationTableTypes[] (e.g., levelFunctions.ts:535, moderationFunctions.ts:97), which masks these nullability mismatches and weakens type safety. Derive the interfaces from table definitions or align the field nullability exactly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/schema.ts` around lines 13 - 21, The handwritten row interfaces are
out-of-sync with the table schema; update the types for memberTableTypes,
levelTableTypes, and moderationTableTypes to exactly match column nullability
(or better: derive them from the table definitions) — specifically make
memberTableTypes.currentlyInServer and memberTableTypes.discordUsername
non-optional (non-null), change levelTableTypes.lastMessageTimestamp to Date |
null, and make moderationTableTypes.duration nullable/optional to reflect the
column that lacks notNull() (or reflect the actual default if you prefer
non-null empty-string); also remove or adjust unsafe casts like "as
levelTableTypes[]" and "as moderationTableTypes[]" in levelFunctions.ts and
moderationFunctions.ts and use the derived/updated types so the compiler
enforces correct nullability.
src/db/redis.ts-212-226 (1)

212-226: ⚠️ Potential issue | 🟠 Major

Use atomic SET with EX to ensure TTL always applies.

The set() function currently executes SET and EXPIRE as separate commands. If EXPIRE fails after SET succeeds, the key persists without TTL—a data reliability issue. Additionally, the truthy check skips ttl: 0. ioredis supports atomic SET-with-expiry: use redis.set(key, value, 'EX', ttl) and gate on ttl !== undefined to handle all cases atomically.

Possible fix
-    await redis.set(`bot:${key}`, value);
-    if (ttl) {
-      await redis.expire(`bot:${key}`, ttl);
-    }
+    if (ttl !== undefined) {
+      await redis.set(`bot:${key}`, value, 'EX', ttl);
+    } else {
+      await redis.set(`bot:${key}`, value);
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/redis.ts` around lines 212 - 226, The set() function currently calls
redis.set(...) then redis.expire(...), which can leave keys without TTL if
expire fails and also treats ttl: 0 as falsy; change set() (in src/db/redis.ts)
to perform an atomic SET with expiry when a TTL is provided by checking ttl !==
undefined and calling redis.set(`bot:${key}`, value, 'EX', ttl) (and remove the
separate redis.expire call); keep the ensureRedisConnection() guard and return
semantics the same.
src/commands/moderation/warn.ts-25-30 (1)

25-30: 🛠️ Refactor suggestion | 🟠 Major

Run the shared interaction validation before deferring.

This early return only checks command type and guild, so it still skips the repo's standard validation path. Please call validateInteraction(interaction) before deferReply() so invalid states are handled consistently instead of returning silently.

As per coding guidelines, "Call await validateInteraction(interaction) before processing interactions to ensure the interaction is in-guild, channel is available, and for component interactions the original message is fetchable".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/moderation/warn.ts` around lines 25 - 30, The execute handler
currently returns early before running shared validation; call await
validateInteraction(interaction) immediately after entering execute and before
await interaction.deferReply(...) so the repo's standard validation logic (in
validateInteraction) runs for all interactions; update the execute function in
the warn command to await validateInteraction(interaction) prior to deferring
the reply and proceed only if it resolves.
src/commands/moderation/ban.ts-35-40 (1)

35-40: 🛠️ Refactor suggestion | 🟠 Major

Run the shared interaction validation before deferring.

This early return only checks command type and guild, so it still skips the repo's standard validation path. Please call validateInteraction(interaction) before deferReply() so invalid states are handled consistently instead of returning silently.

As per coding guidelines, "Call await validateInteraction(interaction) before processing interactions to ensure the interaction is in-guild, channel is available, and for component interactions the original message is fetchable".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/moderation/ban.ts` around lines 35 - 40, The handler currently
returns early after checking interaction.isChatInputCommand() and
interaction.guild, skipping the repository's standard validation; update the
execute function to call await validateInteraction(interaction) before calling
interaction.deferReply({ flags: ['Ephemeral'] }) so all interactions go through
the shared validation path (keep the existing type/guild checks if desired but
ensure validateInteraction is awaited first); reference the execute function,
validateInteraction, and deferReply to locate and update the code.
src/events/messageEvents.ts-213-218 (1)

213-218: ⚠️ Potential issue | 🟠 Major

Reuse the counting evaluator for restore checks.

The counting pipeline accepts arithmetic expressions via sanitizeAndEval(), but this helper only does Number(trimmed). Deleted valid counts like 1+1 or 3*4 will never be restored even though they are accepted everywhere else in the counting flow.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/events/messageEvents.ts` around lines 213 - 218, The restore-check code
currently converts the message with Number(trimmed) which rejects valid
arithmetic like "1+1"; replace the Number(trimmed) usage with the shared
evaluator sanitizeAndEval(trimmed) (the same function used by the counting
pipeline), then validate the result with Number.isInteger(result) before
returning; if sanitizeAndEval can throw or return non-numeric values, guard it
with a try/catch or NaN check so only integer results proceed (refer to
sanitizeAndEval and the parsed/result variable in this message handling block).
src/util/counting/countingManager.ts-191-200 (1)

191-200: ⚠️ Potential issue | 🟠 Major

Don't collapse every eval error into ignored.

sanitizeAndEval() throws for invalid count attempts too — division by zero, leading zeros, empty parens, malformed operator sequences, etc. Catching everything here means inputs like 01 or 1/0 no longer trigger the warning/reset path and can be used to bypass counting enforcement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/counting/countingManager.ts` around lines 191 - 200, The current
catch around sanitizeAndEval() treats all errors as "ignored", hiding malformed
numeric inputs (e.g., leading zeros, division by zero) that should trigger the
invalid/warn/reset path; change the logic so non-numeric messages are filtered
before calling sanitizeAndEval() (e.g., a regex test on trimmed) and only map
true non-numeric errors to the ignored path, while any errors thrown by
sanitizeAndEval() (identify via the thrown error or by calling sanitizeAndEval()
only for inputs that pass the numeric-ish test) should be handled as invalid
counting attempts (return { isValid: false, reason: 'invalid' } or rethrow to
let the CountingManager's invalid/warn/reset logic run). Use the sanitizeAndEval
and CountingManager context and update the catch branch to distinguish
non-numeric vs evaluation/parsing errors accordingly.
src/events/messageEvents.ts-151-198 (1)

151-198: ⚠️ Potential issue | 🟠 Major

Eliminate duplicate audit log fetch in messageDelete handler.

The handler calls fetchAuditLogs() twice per deleted message: once in shouldAllowRestoreCountingMessage() (line 158) to validate restore eligibility against author/channel, and again at line 263 to retrieve the executor for logging. During bulk deletes this becomes a significant performance issue. Reuse the matched entry from the first lookup or cache the result to avoid the second fetch.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/events/messageEvents.ts` around lines 151 - 198, The audit log is being
fetched twice; update shouldAllowRestoreCountingMessage to return the matched
audit-log entry (or its executor) along with the boolean decision so the caller
can reuse it instead of calling fetchAuditLogs() again: locate the matching
variable inside shouldAllowRestoreCountingMessage (currently assigned from
entries.find) and return an object like { allowed: boolean, matchingEntry?:
AuditLogEntry } or { allowed: boolean, executor?: User } (or a tuple), update
the call sites in the messageDelete handler to consume the returned
executor/matchingEntry (use its executor for logging) and remove the second
fetchAuditLogs() call; keep existing error handling and fallback behavior.

Comment thread .husky/commit-msg Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Apr 12, 2026
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

11 issues found across 46 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/events/messageEvents.ts">

<violation number="1" location="src/events/messageEvents.ts:267">
P2: Moderator attribution regressed for most `messageDelete` logs because executor is now only derived from counting-message restoration logic.</violation>
</file>

<file name="src/util/counting/countingManager.ts">

<violation number="1" location="src/util/counting/countingManager.ts:215">
P1: Early return in catch exits before local variables (like `expected`) are initialized, risking incorrect state or runtime errors.</violation>
</file>

<file name="src/db/functions/utils/moderationUtils.ts">

<violation number="1" location="src/db/functions/utils/moderationUtils.ts:16">
P1: `createdAt` fallback uses an unsafe cast to return the original value, which can leak invalid/non-Date data instead of a normalized date value.</violation>
</file>

<file name="src/events/interactionCreate.ts">

<violation number="1" location="src/events/interactionCreate.ts:333">
P2: `handleInteractionError` is now async, but its caller does not await it, creating a floating promise in the catch path.</violation>
</file>

<file name="CONTRIBUTING.md">

<violation number="1" location="CONTRIBUTING.md:78">
P3: The docs incorrectly describe commitlint as a pre-commit check; it runs on the commit-msg hook.</violation>
</file>

<file name="src/db/functions/levelFunctions.ts">

<violation number="1" location="src/db/functions/levelFunctions.ts:79">
P2: `getUserLevel` returns a synthetic `id: 0` instead of the actual DB row after insert, which can return invalid/stale user-level data.</violation>
</file>

<file name="src/util/telemetry/otelLogTransport.ts">

<violation number="1" location="src/util/telemetry/otelLogTransport.ts:96">
P2: The `isError` check makes the `'name'` branch unreachable because this function is only called after non-Error values are filtered, so `isError` is always false.</violation>
</file>

<file name="src/db/functions/memberFunctions.ts">

<violation number="1" location="src/db/functions/memberFunctions.ts:238">
P2: Guard against empty `updateFields` before calling `.set(...)`; the current signature allows `{ discordId }` only, which can produce an empty update payload and fail at runtime.</violation>
</file>

<file name="src/commands/fun/achievements.ts">

<violation number="1" location="src/commands/fun/achievements.ts:55">
P2: Use the component interaction (`i`) for unauthorized collector responses; responding with the original command interaction can overwrite the main reply and leave the clicked interaction unacknowledged.</violation>
</file>

<file name="src/db/functions/moderationFunctions.ts">

<violation number="1" location="src/db/functions/moderationFunctions.ts:27">
P3: JSDoc parameter tags no longer match the refactored function signature (single `moderation` object), which makes API docs misleading.</violation>
</file>

<file name="src/util/counting/constants.ts">

<violation number="1" location="src/util/counting/constants.ts:43">
P2: This introduces a duplicate math-validation regex. Keep a single shared regex source so `processCountingMessage` and `sanitizeAndEval` cannot diverge over time.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread src/util/counting/countingManager.ts
Comment thread src/db/functions/utils/moderationUtils.ts
Comment thread src/events/messageEvents.ts Outdated
Comment thread src/events/interactionCreate.ts
Comment thread src/db/functions/levelFunctions.ts Outdated
Comment thread src/db/functions/memberFunctions.ts
Comment thread src/commands/fun/achievements.ts
Comment thread src/util/counting/constants.ts Outdated
} as const;
};

export const numericLikeRegex = /^[\d+\-*/()\s]+$/;
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This introduces a duplicate math-validation regex. Keep a single shared regex source so processCountingMessage and sanitizeAndEval cannot diverge over time.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/util/counting/constants.ts, line 43:

<comment>This introduces a duplicate math-validation regex. Keep a single shared regex source so `processCountingMessage` and `sanitizeAndEval` cannot diverge over time.</comment>

<file context>
@@ -38,4 +38,6 @@ export const MILESTONE_REACTIONS = {
-} as const;
+};
+
+export const numericLikeRegex = /^[\d+\-*/()\s]+$/;
</file context>
Fix with Cubic

Comment thread CONTRIBUTING.md Outdated
Comment thread src/db/functions/moderationFunctions.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 19

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/util/helpers.ts (1)

162-175: ⚠️ Potential issue | 🟠 Major

Move moderation table deactivation before the early return check in executeUnmute.

The early return at line 162–164 skips the UPDATE moderationTable ... active=false block. If the member is missing from the database or already marked unmuted, mute rows remain active=true. Since loadActiveMutes queries for moderationTable.active = true, these stale rows re-trigger on the next startup, causing phantom unmutes or duplicate scheduling.

Deactivate the mute rows before checking currentlyMuted, or restructure the logic to ensure moderation rows are always cleaned up.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/helpers.ts` around lines 162 - 175, In executeUnmute, the current
early return after checking getMember(userId)?.currentlyMuted prevents running
the db.update(...) that sets moderationTable.active = false; move or copy the
moderationTable deactivation (the db.update(moderationTable).set({ active: false
})... where(...) call) to run before the early return so that moderation rows
for action 'mute' and discordId userId are always marked inactive (use the same
where clause with eq(moderationTable.discordId, userId),
eq(moderationTable.action, 'mute'), eq(moderationTable.active, true));
optionally keep the currentlyMuted check afterward to skip the Discord unmute
call but ensure the DB cleanup happens first.
🧹 Nitpick comments (3)
src/commands/util/server.ts (1)

29-32: Use a Discord timestamp for createdAt.

${interaction.guild.createdAt} calls Date.prototype.toString(), which yields an engine/locale-dependent string like "Wed Apr 09 2025 10:30:00 GMT+0000". You already export msToDiscordTimestamp from @/util/helpers.js; using it gives users a localized, hover-for-relative timestamp:

Suggested tweak
-    await safelyRespond(
-      interaction,
-      `The server **${interaction.guild.name}** has **${interaction.guild.memberCount}** members and was created on **${interaction.guild.createdAt}**. It is **${new Date().getFullYear() - interaction.guild.createdAt.getFullYear()}** years old.`
-    );
+    const { guild } = interaction;
+    await safelyRespond(
+      interaction,
+      `The server **${guild.name}** has **${guild.memberCount}** members and was created on ${msToDiscordTimestamp(guild.createdTimestamp)}. It is **${new Date().getFullYear() - guild.createdAt.getFullYear()}** years old.`
+    );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/server.ts` around lines 29 - 32, The message uses
interaction.guild.createdAt directly which prints a locale-dependent Date
string; replace that with the exported helper msToDiscordTimestamp to produce a
Discord-formatted timestamp. Update the template in the safelyRespond call
(where interaction is used) to call
msToDiscordTimestamp(interaction.guild.createdAt.getTime()) (and keep the
existing memberCount and age calculation), ensuring you import
msToDiscordTimestamp from "@/util/helpers.js" if not already imported.
src/db/functions/memberFunctions.ts (1)

225-251: Guard against empty updateFields.

If a caller invokes updateMember({ discordId }) with no other keys, updateFields is {} and db.update(memberTable).set({}) will throw at runtime (Drizzle requires at least one column in SET). Current callers look safe, but a one-line guard makes this robust against future misuse:

Proposed guard
     const { discordId, ...updateFields } = updates;
+    if (Object.keys(updateFields).length === 0) {
+      return;
+    }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/functions/memberFunctions.ts` around lines 225 - 251, The updateMember
function must guard against an empty updateFields object before calling
db.update(memberTable).set(updateFields); check the destructured updateFields
(from updates) and if Object.keys(updateFields).length === 0, log an appropriate
message via logger (e.g., "[memberDbFunctions] no update fields provided for
discordId: ...") and either throw a clear Error('No update fields provided') or
return early—make the behavior consistent with other DB error handling; update
the branch in updateMember that reaches db.update(memberTable).set(...) to
perform this check and avoid calling Drizzle with an empty SET.
src/db/functions/utils/moderationUtils.ts (1)

13-20: Misleading as Date cast in createdAt fallback.

When createdAt is null or NaN, the fallback (record.createdAt as Date) asserts Date but the underlying value could be null, undefined, or an invalid string (since we only reach that branch when new Date(record.createdAt) was itself null/NaN). Downstream code that relies on the Date type will receive a non-Date value.

Either keep the real input type in the return type, or fall back consistently (e.g., to null like expiresAt, or to new Date(0) / current time as a sentinel). At minimum the asymmetric handling between createdAt and expiresAt deserves an inline note.

Possible normalization
   return {
     ...record,
     createdAt:
       createdAt != null && !Number.isNaN(createdAt.getTime())
         ? createdAt
-        : (record.createdAt as Date),
+        : (record.createdAt ?? null),
     expiresAt:
       expiresAt != null && !Number.isNaN(expiresAt.getTime())
         ? expiresAt
         : null,
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/functions/utils/moderationUtils.ts` around lines 13 - 20, The
createdAt fallback in moderationUtils.ts currently forces (record.createdAt as
Date) which can be null/undefined/invalid; make it consistent with expiresAt by
returning null when createdAt is missing/NaN (i.e., change the createdAt branch
to yield null instead of casting record.createdAt), and update any related
return type/interface to allow createdAt to be Date | null (or choose a clear
sentinel Date and apply it to both createdAt and expiresAt consistently); ensure
references to createdAt, expiresAt and record.createdAt in the moderationUtils
functions reflect the chosen nullable/sentinel contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ultracite.yml:
- Around line 38-39: The Biome SARIF generation step ("Generate Biome SARIF
report") is limited to 20 diagnostics; update the run command that currently
calls `yarn biome ci --reporter sarif --reporter-file biome-results.sarif ./src`
to include the `--max-diagnostics=none` flag so the SARIF output contains all
findings (i.e., change the step's run invocation to add
`--max-diagnostics=none`).
- Around line 3-18: The workflow triggers on pull_request but later steps in the
ultracite job perform write operations and will fail for forked PRs because the
token is read-only; add a guard to skip write steps for forked PRs by checking
the pull_request head repo fork flag (e.g., use an if condition such as
"github.event.pull_request == null || github.event.pull_request.head.repo.fork
== false") on the write steps (or the ultracite job) so writes only run for
non-fork PRs and other events; update any step that posts comments or uploads
results to include this condition.
- Around line 144-149: The workflow step named "Upload analysis results to
GitHub" currently uses if: always() and tries to upload sarif_file:
biome-results.sarif even when the file doesn't exist; change the step's
condition to only run when the SARIF file exists by replacing the if: always()
guard with a hashFiles check (e.g., test that hashFiles('biome-results.sarif')
is not empty) so the upload-sarif@v4 action is executed only when
biome-results.sarif is present and the original biome failure isn't masked by a
ConfigurationError.

In `@package.json`:
- Line 19: Add a "prepare" lifecycle script to package.json so Husky hooks are
installed automatically during install: update the "scripts" object that
currently contains "setup": "husky" by adding "prepare": "husky install" (you
can keep "setup": "husky" for manual runs if desired) so that running npm/yarn
install will run the Husky installation step without requiring contributors to
run "setup" manually.

In `@src/commands/fun/counting.ts`:
- Around line 96-104: The catch block currently echoes raw error details to the
user; instead log the Error instance using the project's logger (e.g.,
logger.error(error, { action: 'setCount', count, interactionId: interaction.id
})) and send a generic failure message via safelyRespond (for example: "Failed
to set the count. Please try again later.") without including the error string;
update the catch around setCount/ safelyRespond to follow this pattern and
ensure you import/ use the logger from src/util/logger.js.
- Around line 313-323: The select menu construction in getSelectRow creates one
option per page using StringSelectMenuBuilder and doesn't guard Discord's
25-option limit, so add a check on pages.length and only build/add the
StringSelectMenu when pages.length <= 25 (otherwise omit the select component
but still return the pagination buttons); update both the getSelectRow used in
the listbans handler and the analogous select construction in the listwarnings
handler to follow this pattern, referencing pages and currentPage to preserve
default selection when the select is present and ensuring the returned
ActionRow(s) still include the navigation buttons even if the select is omitted.

In `@src/commands/fun/fact.ts`:
- Around line 132-136: Several places in src/commands/fun/fact.ts are calling
interaction.reply / i.reply / interaction.editReply directly for text-only
responses (e.g., the guard that checks i.user.id !== interaction.user.id and
similar blocks around the ranges noted); replace those direct calls with the
shared helper safelyRespond(interaction, { content: '...', flags: [...] } or
safelyRespond(interaction, '...') as appropriate so you reuse centralized
response/error handling from src/util/helpers.ts, while keeping direct
interaction.update / interaction.editReply only where embeds or component
updates are actually required (do the replacements for the occurrences around
lines 132-136, 259-270, 280-290, 301-312, and 374-383).
- Around line 192-249: The code currently calls addFact(...) (factId) before
verifying the approval channel, which can persist a pending fact that never gets
moderator buttons; change the flow in the non-admin branch so you first locate
and validate approvalChannel (check
interaction.guild?.channels.cache.get(config.channels.factApproval) and
approvalChannel?.isTextBased()) and only then call addFact(...) to create the
pending fact and send the embed/buttons; if the channel is missing or not
text-based, do not call addFact — instead log the error
(logger.error('[FactCommand] Fact approval channel not found or is not a text
channel')) and respond to the user that submission failed or to contact staff,
ensuring addFact, approvalChannel, and interaction.editReply are updated
accordingly.
- Around line 94-114: The subcommand dispatch in the switch (handling subcommand
variable and calling handleSubmitFact, handleApproveFact, handleDeleteFact,
handlePendingFacts, handlePostFact) must be wrapped in a try/catch so that if
any handler throws after the interaction was deferred the user still gets a
final response; update the code to call the handlers inside a try block and in
the catch log the error and call safelyRespond(interaction, { content: 'An error
occurred while processing your request.' }) (or similar contextual message) to
send a final reply, mirroring the pattern used in counting.ts.

In `@src/commands/fun/rank.ts`:
- Line 5: The import for safelyRespond and validateInteraction currently uses
the path alias without the required extension; update the import of
"@/util/helpers" so it includes the .js extension (i.e., import safelyRespond
and validateInteraction from "@/util/helpers.js") to comply with ESM/project
rules and match other imports in this file.

In `@src/commands/util/config.ts`:
- Around line 30-35: The current masking logic only hides one DB secret because
it uses "else if" between output.database.poolingDbConnectionString and
output.database.directDbConnectionString; change that to two independent checks
so both output.database.poolingDbConnectionString and
output.database.directDbConnectionString are masked regardless of the other’s
presence (i.e., replace the "else if" with a separate "if" for
directDbConnectionString) so both secrets are redacted before posting the
config.

In `@src/commands/util/user-info.ts`:
- Around line 128-155: The current code uses muteModerations[0] and
banModerations[0] which may be the newest row but not the currently active
moderation; update buildCurrentMuteField and buildCurrentBanField to search for
the active moderation row (e.g., const currentMute = muteModerations.find(m =>
m.active) and const currentBan = banModerations.find(m => m.active)), and if no
active row exists fall back to the first entry (muteModerations[0] /
banModerations[0]) or return null as appropriate so the embed shows the true
active reason/duration; keep the rest of the field construction in
buildCurrentMuteField/buildCurrentBanField unchanged.

In `@src/db/functions/factFunctions.ts`:
- Around line 37-52: The insert currently returns result[0]?.id ?? 0 which
silently coerces a missing returning row to 0; instead, in the function that
inserts into factTable (the add fact logic that calls db.insert(...).returning({
id: factTable.id })), detect when result[0] is undefined and throw or call
handleDbError with a clear message (e.g., "No row returned after insert") so
callers don't receive a bogus factId of 0; keep the
invalidateCache('unused-facts') call when insert succeeds and only return the
numeric id when result[0].id is present.

In `@src/events/messageEvents.ts`:
- Around line 321-335: The execute function should be declared async and must
await the async work so the event handler doesn't resolve early: change the
execute: (message: Message): Promise<void> => { ... } to an async function,
await handleLevelingMessage(message), and replace any Promise.resolve() return
statements with simple returns (e.g., return;). Also ensure you still enqueue
counting messages (countingQueue.push) and call processCountingQueue() as
before; if processCountingQueue is async consider awaiting it as well. Use the
identifiers execute, handleLevelingMessage, countingQueue, and
processCountingQueue to locate and update the code.

In `@src/util/counting/countingManager.ts`:
- Around line 200-260: The catch invokes the inner helper handleRollbackOrReset
while it closes over const expected which is declared after the try/catch,
causing a TDZ ReferenceError when sanitizeAndEval throws; fix by moving the
expected initialization (const expected = data.currentCount + 1) above the
try/catch so it's defined before handleRollbackOrReset can run, or refactor
handleRollbackOrReset to accept expected as a parameter (or convert it to a
const arrow function) and pass expected in; update references inside
handleRollbackOrReset (expectedCount, logging) accordingly and keep use of
setCount, resetCounting and handleMistake unchanged.

In `@src/util/helpers.ts`:
- Around line 564-586: In safelyRespond, the code paths for interaction.deferred
and interaction.replied call interaction.editReply but the comments state they
should send a follow-up (ephemeral by default); fix this by replacing the
editReply calls with interaction.followUp({ content, flags: ephemeral ?
MessageFlags.Ephemeral : undefined }) for the deferred and replied branches to
match the documented behavior, or if editReply was intended, update the comments
to state that the original reply is edited and remove any reference to
follow-up/ephemeral semantics; reference interaction.deferred,
interaction.replied, safelyRespond, followUp, editReply, and
MessageFlags.Ephemeral when making the change.

In `@src/util/telemetry/otelLogTransport.ts`:
- Around line 134-143: The code marks Error objects early in the loop (the v
instanceof Error branch in otelLogTransport.ts) and continues, so
extractCommonErrorField(k, v, v instanceof Error) never sees isError=true and
the "name" → "exception.type" mapping in extractCommonErrorField is effectively
dead; fix by making isError derived from sibling metadata instead of v
instanceof Error — e.g., before the per-key logic detect an error-shaped record
(presence of a string "stack" and/or "message") and set a local isErrorForRecord
flag, then call extractCommonErrorField(k, v, isErrorForRecord) or remove the
isError gate inside extractCommonErrorField so that when a top-level "name"
string appears alongside an error-looking "stack" it maps to exception.type;
update extractErrorAttributes usage accordingly so true Error instances still
prefer extractErrorAttributes but flattened Winston-style records get the
name→exception.type mapping.
- Around line 59-61: isNumericKey currently treats empty or whitespace-only
strings as numeric because Number('') === 0; update isNumericKey to only return
true for non-empty, trimmed strings that consist entirely of digits (i.e.,
integer indices) so whitespace or empty keys are not considered numeric. Locate
the isNumericKey function and replace the Number(...) check with a stricter
validation such as trimming the key and matching it against an all-digits
pattern (e.g., /^\d+$/) to ensure only true numeric splat indices pass.

---

Outside diff comments:
In `@src/util/helpers.ts`:
- Around line 162-175: In executeUnmute, the current early return after checking
getMember(userId)?.currentlyMuted prevents running the db.update(...) that sets
moderationTable.active = false; move or copy the moderationTable deactivation
(the db.update(moderationTable).set({ active: false })... where(...) call) to
run before the early return so that moderation rows for action 'mute' and
discordId userId are always marked inactive (use the same where clause with
eq(moderationTable.discordId, userId), eq(moderationTable.action, 'mute'),
eq(moderationTable.active, true)); optionally keep the currentlyMuted check
afterward to skip the Discord unmute call but ensure the DB cleanup happens
first.

---

Nitpick comments:
In `@src/commands/util/server.ts`:
- Around line 29-32: The message uses interaction.guild.createdAt directly which
prints a locale-dependent Date string; replace that with the exported helper
msToDiscordTimestamp to produce a Discord-formatted timestamp. Update the
template in the safelyRespond call (where interaction is used) to call
msToDiscordTimestamp(interaction.guild.createdAt.getTime()) (and keep the
existing memberCount and age calculation), ensuring you import
msToDiscordTimestamp from "@/util/helpers.js" if not already imported.

In `@src/db/functions/memberFunctions.ts`:
- Around line 225-251: The updateMember function must guard against an empty
updateFields object before calling db.update(memberTable).set(updateFields);
check the destructured updateFields (from updates) and if
Object.keys(updateFields).length === 0, log an appropriate message via logger
(e.g., "[memberDbFunctions] no update fields provided for discordId: ...") and
either throw a clear Error('No update fields provided') or return early—make the
behavior consistent with other DB error handling; update the branch in
updateMember that reaches db.update(memberTable).set(...) to perform this check
and avoid calling Drizzle with an empty SET.

In `@src/db/functions/utils/moderationUtils.ts`:
- Around line 13-20: The createdAt fallback in moderationUtils.ts currently
forces (record.createdAt as Date) which can be null/undefined/invalid; make it
consistent with expiresAt by returning null when createdAt is missing/NaN (i.e.,
change the createdAt branch to yield null instead of casting record.createdAt),
and update any related return type/interface to allow createdAt to be Date |
null (or choose a clear sentinel Date and apply it to both createdAt and
expiresAt consistently); ensure references to createdAt, expiresAt and
record.createdAt in the moderationUtils functions reflect the chosen
nullable/sentinel contract.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d89e5123-3b11-4345-9a9e-12cfafef280b

📥 Commits

Reviewing files that changed from the base of the PR and between 092a355 and 594551b.

📒 Files selected for processing (46)
  • .devcontainer/devcontainer.json
  • .github/workflows/ultracite.yml
  • .husky/commit-msg
  • CONTRIBUTING.md
  • package.json
  • src/commands/fun/achievements.ts
  • src/commands/fun/counting.ts
  • src/commands/fun/fact.ts
  • src/commands/fun/giveaway.ts
  • src/commands/fun/leaderboard.ts
  • src/commands/fun/rank.ts
  • src/commands/moderation/ban.ts
  • src/commands/moderation/kick.ts
  • src/commands/moderation/mute.ts
  • src/commands/moderation/unban.ts
  • src/commands/moderation/unmute.ts
  • src/commands/moderation/warn.ts
  • src/commands/testing/test-join.ts
  • src/commands/testing/test-leave.ts
  • src/commands/util/backend-manager.ts
  • src/commands/util/config.ts
  • src/commands/util/help.ts
  • src/commands/util/manage-achievement.ts
  • src/commands/util/members.ts
  • src/commands/util/ping.ts
  • src/commands/util/purge.ts
  • src/commands/util/recalculate-levels.ts
  • src/commands/util/reload-config.ts
  • src/commands/util/restart.ts
  • src/commands/util/rules.ts
  • src/commands/util/server.ts
  • src/commands/util/user-info.ts
  • src/commands/util/xp.ts
  • src/db/functions/factFunctions.ts
  • src/db/functions/levelFunctions.ts
  • src/db/functions/memberFunctions.ts
  • src/db/functions/moderationFunctions.ts
  • src/db/functions/utils/moderationUtils.ts
  • src/db/redis.ts
  • src/db/schema.ts
  • src/events/interactionCreate.ts
  • src/events/messageEvents.ts
  • src/util/counting/constants.ts
  • src/util/counting/countingManager.ts
  • src/util/helpers.ts
  • src/util/telemetry/otelLogTransport.ts
🚧 Files skipped from review as they are similar to previous changes (27)
  • .husky/commit-msg
  • src/commands/util/ping.ts
  • src/commands/testing/test-join.ts
  • src/util/counting/constants.ts
  • src/commands/util/rules.ts
  • src/commands/moderation/unmute.ts
  • src/commands/testing/test-leave.ts
  • src/commands/moderation/unban.ts
  • src/commands/util/restart.ts
  • src/commands/util/members.ts
  • src/commands/util/reload-config.ts
  • .devcontainer/devcontainer.json
  • src/commands/moderation/kick.ts
  • src/commands/util/manage-achievement.ts
  • src/commands/moderation/ban.ts
  • src/commands/util/xp.ts
  • CONTRIBUTING.md
  • src/commands/moderation/warn.ts
  • src/commands/fun/leaderboard.ts
  • src/events/interactionCreate.ts
  • src/commands/fun/achievements.ts
  • src/commands/util/backend-manager.ts
  • src/db/functions/levelFunctions.ts
  • src/commands/fun/giveaway.ts
  • src/db/schema.ts
  • src/db/redis.ts
  • src/commands/util/purge.ts
📜 Review details
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: cubic · AI code reviewer
  • GitHub Check: Seer Code Review
🧰 Additional context used
📓 Path-based instructions (4)
src/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Path alias @/* maps to src/* and imports MUST append .js extension (e.g., import { foo } from '@/util/helpers.js')

Import database functions from src/db/db.ts, not direct DB access. Always use handleDbError(error, operation) for consistent error handling

All Redis keys MUST be prefixed with 'bot:' (e.g., bot:counting:${guildId}). Always check isRedisConnected() before non-critical caching operations

Use safelyRespond(interaction, content) from src/util/helpers.ts for all interaction responses instead of directly calling interaction methods

Call await validateInteraction(interaction) before processing interactions to ensure the interaction is still valid (in-guild, channel available, message fetchable for components)

Use structured logging with logger.info(), logger.error(), etc. from src/util/logger.js, passing Error instances and metadata objects instead of string concatenation

Use type keyword for imports only used in type positions (e.g., import type { Guild } from 'discord.js')

Never modify achievementDefinitionsTable directly; seed once and update via admin commands only. Progress tracked in userAchievementsTable.progress

XP cooldown is stored in Redis as bot:xp_cooldown:${userId} and enforced via leveling.xpCooldown configuration

Use modal UI for role input in giveaways when guild has >25 eligible roles (Discord's 25-option limit for select menus)

Don't import from target/ directory; always import from src/ with .js extensions for TypeScript files

Use const by default, let only when reassignment is needed, never use var

Use arrow functions for callbacks and short functions

Prefer for...of loops over .forEach() and indexed for loops

Use optional chaining (?.) and nullish coalescing (??) for safer property access

Prefer template literals over string concatenation

Use destructuring for object and array assignments

Always await promises in async functions and use async/await synta...

Files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/fun/rank.ts
  • src/commands/util/server.ts
  • src/commands/fun/fact.ts
  • src/commands/fun/counting.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/config.ts
  • src/db/functions/moderationFunctions.ts
  • src/db/functions/factFunctions.ts
  • src/commands/util/user-info.ts
  • src/util/counting/countingManager.ts
  • src/db/functions/utils/moderationUtils.ts
  • src/commands/util/help.ts
  • src/util/telemetry/otelLogTransport.ts
  • src/db/functions/memberFunctions.ts
  • src/util/helpers.ts
  • src/events/messageEvents.ts
src/commands/**/*.ts

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

All commands in src/commands/**/*.ts MUST default export an object with data (SlashCommandBuilder) and async execute(interaction) method, satisfying Command, OptionsCommand, or SubcommandCommand types

If a command takes >3 seconds to execute, call interaction.deferReply() immediately to prevent interaction token expiration

Files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/fun/rank.ts
  • src/commands/util/server.ts
  • src/commands/fun/fact.ts
  • src/commands/fun/counting.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/config.ts
  • src/commands/util/user-info.ts
  • src/commands/util/help.ts
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use explicit types for function parameters and return values in TypeScript when they enhance clarity

Prefer unknown over any when the type is genuinely unknown in TypeScript

Use const assertions (as const) for immutable values and literal types in TypeScript

Leverage TypeScript's type narrowing instead of type assertions

Files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/fun/rank.ts
  • src/commands/util/server.ts
  • src/commands/fun/fact.ts
  • src/commands/fun/counting.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/config.ts
  • src/db/functions/moderationFunctions.ts
  • src/db/functions/factFunctions.ts
  • src/commands/util/user-info.ts
  • src/util/counting/countingManager.ts
  • src/db/functions/utils/moderationUtils.ts
  • src/commands/util/help.ts
  • src/util/telemetry/otelLogTransport.ts
  • src/db/functions/memberFunctions.ts
  • src/util/helpers.ts
  • src/events/messageEvents.ts
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use meaningful variable names instead of magic numbers - extract constants with descriptive names

Use arrow functions for callbacks and short functions in JavaScript/TypeScript

Prefer for...of loops over .forEach() and indexed for loops in JavaScript/TypeScript

Use optional chaining (?.) and nullish coalescing (??) for safer property access in JavaScript/TypeScript

Prefer template literals over string concatenation in JavaScript/TypeScript

Use destructuring for object and array assignments in JavaScript/TypeScript

Use const by default, let only when reassignment is needed, never var in JavaScript/TypeScript

Always await promises in async functions - don't forget to use the return value

Use async/await syntax instead of promise chains for better readability in JavaScript/TypeScript

Handle errors appropriately in async code with try-catch blocks

Don't use async functions as Promise executors

Remove console.log, debugger, and alert statements from production code

Throw Error objects with descriptive messages in JavaScript/TypeScript, not strings or other values

Use try-catch blocks meaningfully - don't catch errors just to rethrow them

Prefer early returns over nested conditionals for error cases in JavaScript/TypeScript

Extract complex conditions into well-named boolean variables in JavaScript/TypeScript

Use early returns to reduce nesting in JavaScript/TypeScript

Prefer simple conditionals over nested ternary operators in JavaScript/TypeScript

Don't use eval() or assign directly to document.cookie in JavaScript/TypeScript

Avoid spread syntax in accumulators within loops in JavaScript/TypeScript for performance

Use top-level regex literals instead of creating them in loops in JavaScript/TypeScript for performance

Prefer specific imports over namespace imports in JavaScript/TypeScript

Files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/fun/rank.ts
  • src/commands/util/server.ts
  • src/commands/fun/fact.ts
  • src/commands/fun/counting.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/config.ts
  • src/db/functions/moderationFunctions.ts
  • src/db/functions/factFunctions.ts
  • src/commands/util/user-info.ts
  • src/util/counting/countingManager.ts
  • src/db/functions/utils/moderationUtils.ts
  • src/commands/util/help.ts
  • src/util/telemetry/otelLogTransport.ts
  • src/db/functions/memberFunctions.ts
  • src/util/helpers.ts
  • src/events/messageEvents.ts
🧠 Learnings (21)
📓 Common learnings
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:09.737Z
Learning: Use `yarn ultracite fix` to automatically format and lint code before committing
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:09.737Z
Learning: Run `yarn type-check` to validate TypeScript types before committing
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:09.737Z
Learning: Use `yarn dev` to start the bot in watch mode with auto-checking of command deployment
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:09.737Z
Learning: Use `FORCE_COMMAND_DEPLOY=true yarn dev` only when necessary to force command re-registration; normally `yarn dev` handles this automatically
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:09.737Z
Learning: Bot only registers commands in configured `config.guildId` (guild-specific), not global slash commands
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:09.737Z
Learning: Gracefully degrade when Redis is unavailable; check `isRedisConnected()` before non-critical caching operations
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:09.737Z
Learning: Database migrations use `npx drizzle-kit generate` to generate SQL and `npx drizzle-kit migrate` to apply pending migrations
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:09.737Z
Learning: Production uses TLS certificate at `certs/psql-ca.pem` for PostgreSQL connections; `drizzle.config.ts` uses `database.directDbConnectionString` for DDL operations
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:09.737Z
Learning: Use PM2 process manager for production with `yarn start` and `yarn restart` commands; process name is `poixpixel-discord-bot`
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:09.737Z
Learning: Pre-commit hooks via Husky run `yarn ultracite fix` on staged JS/TS/JSON files and `yarn type-check` when staged `.ts` files are present
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:31.694Z
Learning: Keep functions focused and under reasonable cognitive complexity limits
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:31.694Z
Learning: Group related code together and separate concerns in code organization
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:31.694Z
Learning: Validate and sanitize user input for security
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:31.694Z
Learning: Use descriptive names for functions, variables, and types - write self-documenting code
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:31.694Z
Learning: Write code that is accessible, performant, type-safe, and maintainable - focus on clarity and explicit intent over brevity
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot

Timestamp: 2026-04-19T05:49:31.694Z
Learning: Add comments for complex logic, but prefer self-documenting code
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Pre-commit hooks run ESLint and Prettier via Husky and lint-staged

Applied to files:

  • package.json
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Use PM2 process manager for production (`yarn start` uses process name `poixpixel-discord-bot`, `yarn restart` to reload); ensure TLS certificates are present at `certs/psql-ca.pem` for PostgreSQL and Redis

Applied to files:

  • package.json
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/{commands,events}/**/*.ts : Call `await validateInteraction(interaction)` before processing interactions to ensure the interaction is in-guild, channel is available, and for component interactions the original message is fetchable

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/fun/rank.ts
  • src/commands/util/server.ts
  • src/commands/fun/fact.ts
  • src/commands/fun/counting.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/config.ts
  • src/commands/util/user-info.ts
  • src/commands/util/help.ts
  • src/util/helpers.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/{commands,events}/**/*.ts : Use `safelyRespond(interaction, content)` from `src/util/helpers.ts` for all interaction responses; it handles reply vs. follow-up vs. skip logic and logs Discord API errors

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/fun/rank.ts
  • src/commands/util/server.ts
  • src/commands/fun/fact.ts
  • src/commands/fun/counting.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/config.ts
  • src/commands/util/user-info.ts
  • src/util/counting/countingManager.ts
  • src/commands/util/help.ts
  • src/util/helpers.ts
  • src/events/messageEvents.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/events/interactionCreate.ts : Call `processCommandAchievements()` and `processMessageAchievements()` only once per event (in `src/events/interactionCreate.ts` and `src/util/levelingSystem.ts` respectively) to batch-process all relevant achievements efficiently

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/fun/rank.ts
  • src/events/messageEvents.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/commands/**/*.ts : If a command takes longer than 3 seconds, call `interaction.deferReply()` immediately to prevent interaction token expiration

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/fun/rank.ts
  • src/commands/util/server.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/config.ts
  • src/commands/util/help.ts
  • src/util/helpers.ts
📚 Learning: 2025-10-05T20:23:31.976Z
Learnt from: ahmadk953
Repo: ahmadk953/poixpixel-discord-bot PR: 568
File: src/commands/util/help.ts:38-43
Timestamp: 2025-10-05T20:23:31.976Z
Learning: In the Poixpixel Discord Bot codebase, processCommandAchievements() is called centrally in the interactionCreate event handler (src/events/interactionCreate.ts) after command execution for all commands. Individual command files should NOT call processCommandAchievements() directly, as this would duplicate the achievement processing logic.

Applied to files:

  • src/commands/util/recalculate-levels.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/commands/**/*.ts : All commands in `src/commands/**/*.ts` MUST default export an object with `data` (SlashCommandBuilder) and `async execute(interaction)` properties, satisfying the `Command` type

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/fun/rank.ts
  • src/commands/util/server.ts
  • src/commands/fun/fact.ts
  • src/commands/fun/counting.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/config.ts
  • src/commands/util/user-info.ts
  • src/commands/util/help.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/commands/**/*.ts : Use `SlashCommandBuilder` for command definitions with required `setName()` and `setDescription()` calls

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/fun/rank.ts
  • src/commands/util/server.ts
  • src/commands/fun/fact.ts
  • src/commands/fun/counting.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/config.ts
  • src/commands/util/user-info.ts
  • src/commands/util/help.ts
📚 Learning: 2025-04-17T05:13:22.634Z
Learnt from: ahmadk953
Repo: ahmadk953/poixpixel-discord-bot PR: 343
File: src/commands/util/xp.ts:80-82
Timestamp: 2025-04-17T05:13:22.634Z
Learning: In Discord.js v14, the `ephemeral: true` option is deprecated and should be replaced with `flags: ['Ephemeral']` when using interaction methods like `deferReply()` and `reply()`.

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/util/server.ts
  • src/commands/fun/fact.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/help.ts
  • src/util/helpers.ts
📚 Learning: 2025-04-17T05:13:22.634Z
Learnt from: ahmadk953
Repo: ahmadk953/poixpixel-discord-bot PR: 343
File: src/commands/util/xp.ts:80-82
Timestamp: 2025-04-17T05:13:22.634Z
Learning: In Discord.js v14, the `ephemeral: true` option is deprecated and should be replaced with `flags: ['Ephemeral']` when using methods like `interaction.deferReply()` and `interaction.reply()`.

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/util/server.ts
  • src/commands/fun/fact.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/help.ts
  • src/util/helpers.ts
📚 Learning: 2025-04-17T05:13:52.029Z
Learnt from: ahmadk953
Repo: ahmadk953/poixpixel-discord-bot PR: 343
File: src/commands/moderation/ban.ts:36-37
Timestamp: 2025-04-17T05:13:52.029Z
Learning: In Discord.js v14, `ephemeral: true` is deprecated and should be replaced with `flags: ['Ephemeral']` when making messages ephemeral in interaction replies and deferred replies.

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/help.ts
  • src/util/helpers.ts
📚 Learning: 2025-04-17T05:13:52.029Z
Learnt from: ahmadk953
Repo: ahmadk953/poixpixel-discord-bot PR: 343
File: src/commands/moderation/ban.ts:36-37
Timestamp: 2025-04-17T05:13:52.029Z
Learning: In Discord.js v14.17+, `ephemeral: true` is deprecated and should be replaced with `flags: ['Ephemeral']` when making messages ephemeral in interaction replies and deferred replies.

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/commands/moderation/mute.ts
  • src/commands/util/help.ts
  • src/util/helpers.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/**/*.ts : Use structured logging with metadata objects: `logger.info('message', { userId, commandName, guildId })` and import logger from `src/util/logger.js`

Applied to files:

  • src/commands/util/recalculate-levels.ts
  • src/db/functions/moderationFunctions.ts
  • src/util/counting/countingManager.ts
  • src/db/functions/memberFunctions.ts
  • src/events/messageEvents.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/events/interactionCreate.ts : Use handler maps (object literals) in `src/events/interactionCreate.ts` for routing buttons, modals, and select menus instead of switch statements; add new handlers to `giveawayHandlers`, `modalHandlers`, `selectHandlers` objects with customId prefixes

Applied to files:

  • src/commands/fun/fact.ts
  • src/commands/util/help.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/{commands,util/giveaways}/**/*.ts : If a guild has more than 25 eligible roles for giveaway configuration, use a modal for role input instead of select menu due to Discord's 25-option limit

Applied to files:

  • src/commands/fun/counting.ts
  • src/commands/util/user-info.ts
  • src/commands/util/help.ts
  • src/util/helpers.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/**/*.ts : Import database functions from `src/db/db.ts`, not direct DB access; use `handleDbError(error, operation)` for consistent error handling

Applied to files:

  • src/db/functions/moderationFunctions.ts
  • src/db/functions/factFunctions.ts
  • src/db/functions/memberFunctions.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/**/*.ts : Use `type` keyword for imports only used in type positions: `import type { Guild } from 'discord.js'`

Applied to files:

  • src/util/counting/countingManager.ts
  • src/db/functions/memberFunctions.ts
  • src/util/helpers.ts
📚 Learning: 2025-12-25T06:04:00.591Z
Learnt from: CR
Repo: ahmadk953/poixpixel-discord-bot PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-25T06:04:00.591Z
Learning: Applies to src/**/*.ts : All Redis keys MUST be prefixed with `'bot:'` (e.g., `bot:counting:${guildId}`); always check `isRedisConnected()` before non-critical Redis operations

Applied to files:

  • src/util/counting/countingManager.ts
📚 Learning: 2025-03-01T05:51:01.720Z
Learnt from: ahmadk953
Repo: ahmadk953/poixpixel-discord-bot PR: 294
File: src/events/channelEvents.ts:152-177
Timestamp: 2025-03-01T05:51:01.720Z
Learning: The `channelUpdate` function in `src/events/channelEvents.ts` intentionally skips logging when the only change is to a channel's position (using the condition `oldChannel.position !== newChannel.position` in the early-return check). This is by design to prevent log spam from channel position changes, which can be frequent during server reorganizations.

Applied to files:

  • src/events/messageEvents.ts
🔇 Additional comments (1)
package.json (1)

39-52: LGTM — dependency migration is aligned.

Biome and Ultracite are present as dev dependencies, matching the new check/fix scripts and lint-staged usage.

Comment thread .github/workflows/ultracite.yml
Comment thread .github/workflows/ultracite.yml Outdated
Comment thread .github/workflows/ultracite.yml
Comment thread package.json
Comment thread src/commands/fun/counting.ts
Comment thread src/events/messageEvents.ts Outdated
Comment on lines +321 to +335
execute: (message: Message): Promise<void> => {
try {
if (message.author.bot || !message.guild) return;
if (message.author.bot || !message.guild) {
return Promise.resolve();
}

void handleLevelingMessage(message);
handleLevelingMessage(message);

const countingChannelId = config.channels.counting;
if (message.channel.id === countingChannelId) {
countingQueue.push(message);
processCountingQueue();
}

return Promise.resolve();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify no fire-and-forget leveling call remains in messageEvents.ts.
rg -n -C 2 'handleLevelingMessage\(message\);' --iglob 'messageEvents.ts'

Repository: ahmadk953/poixpixel-discord-bot

Length of output: 340


🏁 Script executed:

# Find and examine the handleLevelingMessage function definition
fd -t f 'levelingSystem.ts' --exec grep -l 'handleLevelingMessage' {} \;

Repository: ahmadk953/poixpixel-discord-bot

Length of output: 57


🏁 Script executed:

# Also check the imports in messageEvents.ts to understand where it comes from
head -50 src/events/messageEvents.ts | grep -n 'import\|from'

Repository: ahmadk953/poixpixel-discord-bot

Length of output: 573


🏁 Script executed:

# Get the full execute function to understand context
sed -n '321,340p' src/events/messageEvents.ts

Repository: ahmadk953/poixpixel-discord-bot

Length of output: 629


🏁 Script executed:

# Find handleLevelingMessage definition in levelingSystem.ts
rg -n 'export.*handleLevelingMessage|function handleLevelingMessage|const handleLevelingMessage' src/util/levelingSystem.ts -A 5

Repository: ahmadk953/poixpixel-discord-bot

Length of output: 57


🏁 Script executed:

# Get full context of levelingSystem.ts around handleLevelingMessage
rg -n 'handleLevelingMessage' src/util/levelingSystem.ts -B 2 -A 10

Repository: ahmadk953/poixpixel-discord-bot

Length of output: 57


🏁 Script executed:

# Search entire codebase for handleLevelingMessage
rg -n 'handleLevelingMessage' src/

Repository: ahmadk953/poixpixel-discord-bot

Length of output: 233


🏁 Script executed:

# Check what's exported from levelingSystem.ts
rg -n 'export' src/util/levelingSystem.ts | head -30

Repository: ahmadk953/poixpixel-discord-bot

Length of output: 499


🏁 Script executed:

# Get the full definition of handleLevelingMessage in messageEvents.ts
sed -n '115,150p' src/events/messageEvents.ts

Repository: ahmadk953/poixpixel-discord-bot

Length of output: 1044


Make the execute function async and await handleLevelingMessage(message).

The function at line 115 is async and contains await statements for XP processing, role assignment, and level-up achievements. Calling it without await at line 327 causes the event to resolve before this work completes. Update the executor to be async, await the call, and replace Promise.resolve() returns with simple return statements.

Suggested fix
-  execute: (message: Message): Promise<void> => {
+  execute: async (message: Message): Promise<void> => {
     try {
       if (message.author.bot || !message.guild) {
-        return Promise.resolve();
+        return;
       }
 
-      handleLevelingMessage(message);
+      await handleLevelingMessage(message);
 
       const countingChannelId = config.channels.counting;
       if (message.channel.id === countingChannelId) {
         countingQueue.push(message);
         processCountingQueue();
       }
 
-      return Promise.resolve();
+      return;
     } catch (error) {
       logger.error('[MessageEvents] Error handling message create', error);
-      return Promise.resolve();
     }
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
execute: (message: Message): Promise<void> => {
try {
if (message.author.bot || !message.guild) return;
if (message.author.bot || !message.guild) {
return Promise.resolve();
}
void handleLevelingMessage(message);
handleLevelingMessage(message);
const countingChannelId = config.channels.counting;
if (message.channel.id === countingChannelId) {
countingQueue.push(message);
processCountingQueue();
}
return Promise.resolve();
execute: async (message: Message): Promise<void> => {
try {
if (message.author.bot || !message.guild) {
return;
}
await handleLevelingMessage(message);
const countingChannelId = config.channels.counting;
if (message.channel.id === countingChannelId) {
countingQueue.push(message);
processCountingQueue();
}
return;
} catch (error) {
logger.error('[MessageEvents] Error handling message create', error);
}
},
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/events/messageEvents.ts` around lines 321 - 335, The execute function
should be declared async and must await the async work so the event handler
doesn't resolve early: change the execute: (message: Message): Promise<void> =>
{ ... } to an async function, await handleLevelingMessage(message), and replace
any Promise.resolve() return statements with simple returns (e.g., return;).
Also ensure you still enqueue counting messages (countingQueue.push) and call
processCountingQueue() as before; if processCountingQueue is async consider
awaiting it as well. Use the identifiers execute, handleLevelingMessage,
countingQueue, and processCountingQueue to locate and update the code.

Comment thread src/util/counting/countingManager.ts
Comment thread src/util/helpers.ts Outdated
Comment on lines +564 to +586
// If the interaction was deferred, send an ephemeral follow-up instead of
// editing the original reply, since its visibility (ephemeral/public)
// cannot be changed after deferReply.
if (interaction.deferred) {
await interaction.followUp({ content, flags: ['Ephemeral'] });
await interaction.editReply({
content,
});
return;
}

// If we've already replied, send a follow-up message (ephemeral by default)
if (interaction.replied) {
await interaction.followUp({ content, flags: ['Ephemeral'] });
await interaction.editReply({
content,
});
return;
}

// Fresh interaction: send the initial reply (ephemeral by default)
await interaction.reply({ content, flags: ['Ephemeral'] });
await interaction.reply({
content,
flags: ephemeral ? MessageFlags.Ephemeral : undefined,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Comments contradict behavior in safelyRespond.

The comments above both branches claim a follow-up ("send an ephemeral follow-up", "send a follow-up message (ephemeral by default)"), but both branches call interaction.editReply({ content }) — editing the original reply, not following up, and without any ephemeral flag. Either update the code to use followUp({ content, flags: MessageFlags.Ephemeral }) when that is the intended semantics, or correct the comments to match the current edit-reply behavior. As-is, this will confuse future maintainers.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/helpers.ts` around lines 564 - 586, In safelyRespond, the code paths
for interaction.deferred and interaction.replied call interaction.editReply but
the comments state they should send a follow-up (ephemeral by default); fix this
by replacing the editReply calls with interaction.followUp({ content, flags:
ephemeral ? MessageFlags.Ephemeral : undefined }) for the deferred and replied
branches to match the documented behavior, or if editReply was intended, update
the comments to state that the original reply is edited and remove any reference
to follow-up/ephemeral semantics; reference interaction.deferred,
interaction.replied, safelyRespond, followUp, editReply, and
MessageFlags.Ephemeral when making the change.

Comment thread src/util/telemetry/otelLogTransport.ts
Comment thread src/util/telemetry/otelLogTransport.ts
Copilot AI review requested due to automatic review settings April 26, 2026 09:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Migrates the repo’s JS/TS code quality tooling from ESLint/Prettier to Ultracite (Biome backend), updating scripts, docs, and CI, plus applying broadly consistent formatting/lint-driven refactors across the codebase.

Changes:

  • Replaced ESLint/Prettier tooling with Ultracite/Biome (new biome.jsonc, updated scripts, removed ESLint config).
  • Updated devcontainer, hooks/lint-staged, and GitHub Actions workflow to run Biome/Ultracite checks and publish SARIF.
  • Refactored and reorganized a number of bot utilities/handlers to match new lint/format rules (imports, control flow, constants, logging helpers).

Reviewed changes

Copilot reviewed 110 out of 111 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/util/undeployCommands.ts Formatting-only adjustments consistent with new formatter.
src/util/telemetry/otelLogTransport.ts Refactors log attribute cleaning and OTEL transport setup for Biome style.
src/util/notificationHandler.ts Replaces enum with as const object + union type per Ultracite standards.
src/util/logging/utils.ts Logging helper refactors and type tweaks for embed fields.
src/util/logging/types.ts Type reordering/cleanup to align with formatting and handler refactors.
src/util/logging/handlers/roleHandlers.ts New role audit log embed field builders.
src/util/logging/handlers/purgeHandler.ts New purge transcript generation and attachment logging.
src/util/logging/handlers/moderationHandlers.ts New moderation log embed field builder.
src/util/logging/handlers/messageHandlers.ts New message delete/edit log embed field builders.
src/util/logging/handlers/memberHandlers.ts New member join/leave and name update embed field builders.
src/util/logging/handlers/channelHandlers.ts New channel create/update/delete embed field builders.
src/util/logging/constants.ts Numeric literal formatting updates for action colors.
src/util/logger.ts Refactors global error handling and transport flushing/shutdown.
src/util/levelingSystem.ts Schema import refactor + formatting and minor typing refinements.
src/util/giveaways/utils.ts Formatting and small logic reshapes for requirement checking and sessions.
src/util/giveaways/types.ts Type/property order adjustments matching new code style.
src/util/giveaways/modals.ts Formatting-only tweaks to modal builders.
src/util/giveaways/dropdowns.ts Formatting-only tweaks to select menu builders.
src/util/factManager.ts Minor scheduling signature refactor + formatting updates.
src/util/eventLoader.ts Import reordering and small formatting changes.
src/util/deployCommand.ts Formatting and small control-flow adjustments for lint compliance.
src/util/dataRetention.ts Refactor into helper functions for readability/maintainability with new style.
src/util/counting/types.ts Type/property order adjustments.
src/util/counting/helpers.ts Adds regex constants and refactors validation/eval flow for lint rules.
src/util/counting/durationUtils.ts Small formatting/early-return tweaks.
src/util/counting/constants.ts Formatting plus new exported regex constant.
src/util/configLoader.ts Import reordering and formatting in error output.
src/util/achievementManager.ts Control-flow simplification and formatting; numeric literal formatting.
src/util/achievementCardGenerator.ts Import ordering and formatting for Biome.
src/types/EventTypes.ts Member order formatting/consistency changes.
src/types/ConfigTypes.ts Reorders config fields; no semantic change intended.
src/structures/ExtendedClient.ts Marks properties readonly and formatting updates.
src/events/roleEvents.ts Import ordering/formatting adjustments.
src/events/ready.ts Refactors init sequence usage (Redis/Fact scheduling call sites updated).
src/events/reactionEvents.ts Formatting and early-return block style updates.
src/events/memberEvents.ts Import ordering and interaction/logging formatting updates.
src/events/interactionCreate.ts Refactors handlers and adds constants for regex; formatting and error handling tweaks.
src/events/channelEvents.ts Refactors permission change extraction and adds helper for readability.
src/discord-bot.ts Formatting and small control-flow adjustments for lint compliance.
src/db/schema.ts Switches table “Types” interfaces to InferSelectModel type aliases for some tables.
src/db/functions/utils/moderationUtils.ts Changes moderation date normalization logic.
src/db/functions/moderationFunctions.ts Refactors to use new schema table exports/types and updates signatures.
src/db/functions/memberFunctions.ts Refactors to use new schema table exports/types and updates signatures.
src/db/functions/factFunctions.ts Refactors to use new schema table exports/types; addFact now returns inserted id.
src/db/functions/achievementFunctions.ts Refactors to use new schema table exports/types and formatting adjustments.
src/commands/util/xp.ts Updates interaction validation/response handling and formatting.
src/commands/util/server.ts Uses shared validate/respond helpers and adds guild-only messaging.
src/commands/util/rules.ts Uses shared validate/respond helpers and formatting updates.
src/commands/util/restart.ts Uses shared validate/respond helpers and formatting adjustments.
src/commands/util/reload-config.ts Uses shared validate/respond helpers and formatting adjustments.
src/commands/util/recalculate-levels.ts Uses shared validate/respond helpers and formatting adjustments.
src/commands/util/ping.ts Uses shared validate/respond helpers and formatting updates.
src/commands/util/members.ts Refactors pagination interaction handling with shared helpers and formatting.
src/commands/util/help.ts Refactors category iteration/collector callbacks and uses shared helpers.
src/commands/testing/test-leave.ts Uses shared validate/respond helpers and adds guild-only messaging.
src/commands/testing/test-join.ts Uses shared validate/respond helpers and adds guild-only messaging.
src/commands/moderation/warn.ts Uses shared validate/respond helpers and formatting updates.
src/commands/moderation/unmute.ts Uses shared validate/respond helpers and formatting updates.
src/commands/moderation/unban.ts Uses shared validate/respond helpers and formatting updates.
src/commands/moderation/mute.ts Uses shared validate/respond helpers and formatting updates.
src/commands/moderation/kick.ts Uses shared validate/respond helpers and formatting updates.
src/commands/moderation/ban.ts Uses shared validate/respond helpers and formatting updates.
src/commands/fun/rank.ts Refactors to shared validate/respond helpers; import ordering changes.
src/commands/fun/leaderboard.ts Refactors pagination interaction handling with shared helpers and formatting.
skills-lock.json Adds Ultracite skill lock metadata.
package.json Replaces lint/format scripts with Ultracite/Biome scripts and adds dependencies.
eslint.config.mjs Removes ESLint flat config (migration away from ESLint).
drizzle.config.ts Formatting adjustments for Biome.
biome.jsonc Adds Biome config extending Ultracite preset.
README.md Updates developer commands from ESLint/Prettier to Ultracite/Biome.
CONTRIBUTING.md Updates contributor workflow/docs for Ultracite/Biome and Husky setup.
AGENTS.md Adds Ultracite code standards documentation for agents.
.prettierrc.yml Removes Prettier config (migration away from Prettier).
.prettierignore Removes Prettier ignore file (migration away from Prettier).
.lintstagedrc.mjs Moves staged formatting/linting to yarn ultracite fix + type-check on TS.
.husky/commit-msg Updates commit-msg hook to run via package script.
.github/workflows/ultracite.yml Adds Ultracite/Biome workflow producing SARIF + PR comment.
.github/workflows/eslint.yml Removes ESLint SARIF workflow (migration away from ESLint).
.github/pull_request_template.md Updates checklist commands to yarn check/fix/type-check.
.github/hooks/ultracite.json Adds PostToolUse hook to run yarn fix with a skip list.
.github/copilot-instructions.md Updates repo instructions to Ultracite/Biome commands and standards.
.github/PULL_REQUEST_TEMPLATE/refactor.md Updates checklist commands to new scripts.
.github/PULL_REQUEST_TEMPLATE/feature.md Updates checklist commands to new scripts.
.github/PULL_REQUEST_TEMPLATE/bug_fix.md Updates checklist commands to new scripts.
.devcontainer/devcontainer.json Switches devcontainer editor defaults/extensions to Biome.
.devcontainer/README.md Updates devcontainer docs to reference Ultracite/Biome.
.agents/skills/ultracite/references/code-standards.md Adds Ultracite standards reference doc.
.agents/skills/ultracite/SKILL.md Adds Ultracite “skill” metadata/documentation.

Comment thread src/util/logger.ts
Comment on lines +223 to +237
const transportsList: ClosableTransport[] =
(logger as unknown as { transports?: ClosableTransport[] }).transports ??
[];
for (const transport of transportsList) {
const flushResult = invokeSafely(transport.flush);
if (isPromiseLike(flushResult)) {
closePromises.push(flushResult);
continue;
}

const closeResult = invokeSafely(transport.close);
if (isPromiseLike(closeResult)) {
closePromises.push(closeResult);
continue;
}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling invokeSafely(transport.flush) / invokeSafely(transport.close) drops the method this binding, which can break transports whose flush/close rely on instance state. Wrap these calls so they’re invoked as methods on the transport (e.g., call through a lambda or use .call(transport)), ensuring correct behavior during fatal shutdown.

Copilot uses AI. Check for mistakes.
Comment on lines +134 to 143
const hasMessage =
typeof (meta as Record<string, unknown>).message === 'string';
const hasName = typeof (meta as Record<string, unknown>).name === 'string';
const isErrorShapedRecord =
hasStack || hasMessage || (hasName && (hasStack || hasMessage));

// Handle Error instances explicitly
if (v instanceof Error) {
out['exception.type'] = v.name;
out['exception.message'] = v.message;
if (v.stack) out['exception.stacktrace'] = v.stack;
for (const [k, v] of Object.entries(meta || {})) {
if (isNumericKey(k)) {
continue;
}
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extractCommonErrorField is passed v instanceof Error, but v instanceof Error is always false here due to the earlier continue. This causes regressions for common fields like name/stack when log metadata contains plain { name: 'TypeError', stack: '...' }. Consider removing the isError parameter (or deriving it from the overall metadata shape) and handling name/stack independently of Error instances.

Copilot uses AI. Check for mistakes.
Comment thread src/events/ready.ts
Comment thread src/commands/fun/rank.ts
Comment thread .lintstagedrc.mjs
'*.{js,mjs,ts,mts}': [prettierCommand, buildEslintCommand],
'*.json': [prettierCommand],
'*.{js,mjs,ts,mts,json}': (filenames) =>
`yarn ultracite fix ${filenames.join(' ')}`,
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building the command via filenames.join(' ') does not properly escape filenames (spaces, quotes, backslashes), which can cause lint-staged failures or incorrect file targeting. Prefer shell-escaping/quoting each filename (e.g., JSON.stringify per filename) or returning an array form where supported so filenames are passed safely.

Suggested change
`yarn ultracite fix ${filenames.join(' ')}`,
`yarn ultracite fix ${filenames.map((filename) => JSON.stringify(filename)).join(' ')}`,

Copilot uses AI. Check for mistakes.
Comment on lines 6 to +20
const createdAt =
record.createdAt != null ? new Date(record.createdAt) : undefined;
record.createdAt == null ? null : new Date(record.createdAt);
const expiresAt =
record.expiresAt != null ? new Date(record.expiresAt) : undefined;
record.expiresAt == null ? null : new Date(record.expiresAt);

return {
...record,
createdAt: Number.isNaN(createdAt?.getTime()) ? undefined : createdAt,
expiresAt: Number.isNaN(expiresAt?.getTime()) ? undefined : expiresAt,
createdAt:
createdAt != null && !Number.isNaN(createdAt.getTime())
? createdAt
: (record.createdAt as Date),
expiresAt:
expiresAt != null && !Number.isNaN(expiresAt.getTime())
? expiresAt
: null,
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The fallback createdAt: (record.createdAt as Date) can return a non-Date value at runtime (e.g., a cached/serialized string), which defeats the purpose of normalization and can break downstream code expecting Date. Prefer guaranteeing createdAt is always a valid Date (e.g., parse then fall back to a safe default or throw), and avoid type casts that don’t enforce runtime validity.

Copilot uses AI. Check for mistakes.
Comment thread package.json
Comment on lines 14 to +20
"undeploy-commands": "tsx src/util/undeployCommands.ts",
"lint": "eslint ./src && tsc --noEmit",
"type-check": "tsc --noEmit",
"clean": "rimraf ./target",
"format": "prettier --check --ignore-path .prettierignore .",
"format:fix": "prettier --write --ignore-path .prettierignore .",
"postinstall": "husky"
"check": "ultracite check",
"fix": "ultracite fix",
"setup": "husky",
"commitlint": "commitlint --edit"
Copy link

Copilot AI Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description’s 'How was this tested?' section indicates checks were run with yarn lint / yarn format:fix, but those scripts are removed in favor of yarn check / yarn fix / yarn type-check. Update the PR description/checklist to match the new commands so reviewers can trust the reported verification steps.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🧹 Nitpick comments (19)
src/util/telemetry/otelLogTransport.ts (1)

129-138: Simplify isErrorShapedRecordhasName is dead code, and the casts are redundant.

The expression on line 137-138 simplifies to just hasStack || hasMessage: the third disjunct (hasName && (hasStack || hasMessage)) can never flip the result because hasStack || hasMessage is already evaluated as the first term. So hasName is computed but never affects the outcome, which is almost certainly not what was intended.

Also, meta is already typed Record<string, unknown>, so the as Record<string, unknown> casts on lines 130–133 (and line 135–136) are no-ops.

If the intent was "treat a record with name plus any error-ish hint as error-shaped," then hasName is already subsumed; if the intent was something stricter (e.g., require both name and stack together), the predicate needs to be rewritten. Either way, the current form is misleading.

♻️ Suggested simplification
-  const hasStack =
-    (typeof (meta as Record<string, unknown>).stack === 'string' &&
-      ((meta as Record<string, unknown>).stack as string).includes('\n')) ||
-    (typeof (meta as Record<string, unknown>).stack === 'string' &&
-      ((meta as Record<string, unknown>).stack as string).includes(' at '));
-  const hasMessage =
-    typeof (meta as Record<string, unknown>).message === 'string';
-  const hasName = typeof (meta as Record<string, unknown>).name === 'string';
-  const isErrorShapedRecord =
-    hasStack || hasMessage || (hasName && (hasStack || hasMessage));
+  const stackVal = meta.stack;
+  const hasErrorShapedStack =
+    typeof stackVal === 'string' &&
+    (stackVal.includes('\n') || stackVal.includes(' at '));
+  const hasMessage = typeof meta.message === 'string';
+  const isErrorShapedRecord = hasErrorShapedStack || hasMessage;

As per coding guidelines: "Extract complex conditions into well-named boolean variables" and "Leverage TypeScript's type narrowing instead of type assertions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/telemetry/otelLogTransport.ts` around lines 129 - 138, The
isErrorShapedRecord predicate is needlessly complex: remove the redundant casts
on meta and drop hasName (dead code) and set isErrorShapedRecord to simply
hasStack || hasMessage; specifically, replace the current
hasStack/hasMessage/hasName definitions to use meta (already typed) directly
(e.g., typeof meta.stack === 'string' && checks for '\n' or ' at ') and then
assign isErrorShapedRecord = hasStack || hasMessage, removing hasName and its
use; if you intended a stricter rule instead, explicitly change the predicate to
require hasName && (hasStack || hasMessage) so the intent is clear.
.github/workflows/ultracite.yml (1)

3-7: Both push and pull_request triggers will run the job twice on PR branches in this repo.

For branches that exist in this repo and have an open PR, both events fire and the workflow runs twice (uploading SARIF twice as well, which CodeQL deduplicates by commit but still costs minutes). Consider scoping push: to specific branches (e.g., main, development) so PR runs are exclusively driven by pull_request. Not blocking, just noting the double-run cost.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ultracite.yml around lines 3 - 7, The workflow currently
triggers on both push and pull_request which causes duplicate runs for PR
branches; update the on: block in ultracite.yml to scope push to specific
branches (e.g., change push: to push: branches: [main, development]) while
keeping pull_request: and schedule: unchanged so PRs trigger only via
pull_request and pushes only on your selected branches (refer to the on: push,
on: pull_request, and on: schedule keys in the diff).
CONTRIBUTING.md (1)

76-83: Code-block indentation under numbered items is inconsistent with the rest of the file.

The new items at lines 76-82, 187-192, and 491-494 indent their nested paragraph and fenced code block by 2 spaces, while every other numbered item in this file (e.g., lines 60-62, 67, 73, 178) uses 3 spaces to align with the content position after 1. . Strict CommonMark parsers (and some doc renderers like GitBook, where these docs are now hosted) treat 2-space-indented content as a sibling block, breaking the list. GitHub itself is lenient, but the inconsistency is best avoided.

📄 Proposed fix (lines 76-82)
-1. **Install Git hooks**
-
-  After installing dependencies run the setup script to initialize Husky git hooks so local checks (commit-msg, pre-commit, etc.) run as expected:
-
-  ```bash
-  yarn setup
-  ```
+1. **Install Git hooks**
+
+   After installing dependencies run the setup script to initialize Husky git hooks so local checks (commit-msg, pre-commit, etc.) run as expected:
+
+   ```bash
+   yarn setup
+   ```

Apply the same 3-space adjustment to lines 187-192 and 491-494.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CONTRIBUTING.md` around lines 76 - 83, List items under numbered entries use
2-space indentation causing list breakage; fix by increasing indentation for the
nested paragraph and fenced code blocks to 3 spaces so they align under the
numeral. In the "1. **Install Git hooks**" block change the two-space indents
before the paragraph and the ```bash fenced block and its content to three
spaces, and make the same 3-space adjustment for the other affected numbered
entries (the blocks currently at the other two locations mentioned in the
review). Ensure the paragraph and the opening/closing ``` lines as well as the
code line ("yarn setup") are indented with three spaces to match the rest of the
file.
src/db/functions/factFunctions.ts (1)

21-26: Overly permissive parameter type for addFact.

Typing the input as factTableTypes (the full select model) means callers can pass server-managed columns like id, usedOn, or any timestamp fields, which are then silently ignored by destructuring. Consider narrowing to the input shape you actually accept, e.g. Pick<factTableTypes, 'content' | 'source' | 'addedBy'> & { approved?: boolean }, to make the contract explicit and prevent accidental misuse.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/functions/factFunctions.ts` around lines 21 - 26, The addFact function
accepts the entire factTableTypes which allows callers to pass server-managed
fields (e.g., id, usedOn, timestamps) that are ignored; change its parameter
type to a narrow input shape such as Pick<factTableTypes, 'content' | 'source' |
'addedBy'> & { approved?: boolean } (or define an explicit AddFactInput type)
and update the function signature for addFact to use that type so the contract
explicitly disallows server-managed columns.
src/commands/util/members.ts (1)

63-66: Extract Discord's 25-option limit as a named constant.

25 is Discord's hard limit on StringSelectMenu options and is replicated in at least src/commands/util/members.ts and src/commands/fun/leaderboard.ts. Per guidelines on magic numbers, lifting this into a shared MAX_SELECT_MENU_OPTIONS = 25 as const (e.g., in src/util/constants.ts or alongside the helpers) would make both call sites self-documenting and easier to update if Discord ever changes the cap.

As per coding guidelines: "Use meaningful variable names instead of magic numbers — extract constants with descriptive names".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/members.ts` around lines 63 - 66, Extract the magic number
25 into a named constant (e.g., MAX_SELECT_MENU_OPTIONS = 25 as const) and use
it where Discord select-menu option limits are checked; update the condition in
getSelectMenuRow (replace pages.length > 25) to pages.length >
MAX_SELECT_MENU_OPTIONS and do the same replacement in
src/commands/fun/leaderboard.ts (or wherever the same 25 is used) so the limit
is self-documenting and centrally maintained.
src/commands/util/config.ts (1)

305-305: Style nit: prefer MessageFlags.Ephemeral for consistency.

Other files touched in this PR (e.g., src/util/helpers.ts, src/commands/fun/rank.ts) import MessageFlags from discord.js and use MessageFlags.Ephemeral. The string-literal form flags: ['Ephemeral'] is equivalent and supported, but mixing styles makes greps and refactors slightly harder. Consider standardizing on MessageFlags.Ephemeral here (and at line 253).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/config.ts` at line 305, The defer reply call uses a string
literal for flags—change await interaction.deferReply({ flags: ['Ephemeral'] })
(and the similar call at the other occurrence around line 253) to use the enum
constant from discord.js by importing MessageFlags and passing flags:
[MessageFlags.Ephemeral]; update the imports if needed and ensure both places
consistently use MessageFlags.Ephemeral for style and easier refactors.
src/db/functions/levelFunctions.ts (1)

79-94: Verify the post-insert SELECT runs against the same connection/transaction context.

The new pattern of INSERT ... ON CONFLICT DO NOTHING followed by an independent SELECT (each through its own withDbRetryDrizzle call) is susceptible to a brief race window: a concurrent deleteUserLevel or pool failover between the two retries can cause the SELECT to return no row and trigger Failed to load user level after create. For a Discord bot this is extremely unlikely in practice, but if you want fully atomic semantics consider using INSERT ... ON CONFLICT DO UPDATE SET <noop> RETURNING * (or wrapping both calls in a single db.transaction) so the row is guaranteed to be returned in one round trip.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/functions/levelFunctions.ts` around lines 79 - 94, The post-insert
SELECT (inside withDbRetryDrizzle that assigns createdLevel) can observe a race
because the INSERT ON CONFLICT DO NOTHING and the subsequent SELECT run as
separate calls; update the logic in the function using either a single INSERT
... ON CONFLICT DO UPDATE SET <noop> RETURNING * to get the row in the same
statement, or execute both statements inside a single db.transaction so the
SELECT runs on the same connection/transaction; locate the code around
withDbRetryDrizzle, createdLevel, and levelTable (and consider interactions with
deleteUserLevel) and replace the two-call pattern with one of these atomic
approaches.
src/commands/util/user-info.ts (1)

132-154: Use ?? instead of || for fallback to align with coding guidelines.

Array.prototype.find returns either an object or undefined, so || happens to work, but the codebase guidelines prefer nullish coalescing for safer property access.

♻️ Proposed refactor
-  const currentMute =
-    muteModerations.find((m) => m.active) || muteModerations[0];
+  const currentMute =
+    muteModerations.find((m) => m.active) ?? muteModerations[0];
@@
-  const currentBan = banModerations.find((m) => m.active) || banModerations[0];
+  const currentBan = banModerations.find((m) => m.active) ?? banModerations[0];

As per coding guidelines: "Use optional chaining (?.) and nullish coalescing (??) for safer property access."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/user-info.ts` around lines 132 - 154, The fallback logic
uses || which should be replaced with nullish coalescing (??) to follow
guidelines: change the assignment of currentMute (currently "const currentMute =
muteModerations.find((m) => m.active) || muteModerations[0];") to use ?? instead
of ||, and likewise change currentBan in buildCurrentBanField (currently "const
currentBan = banModerations.find((m) => m.active) || banModerations[0];") to use
??; this ensures undefined results from .find are handled safely without
treating falsy values like 0 or '' as missing.
src/events/messageEvents.ts (1)

344-363: Drop Promise.resolve() in the async execute.

Now that execute is async, returning Promise.resolve() is redundant — a bare return; resolves the same promise. Tightens the code without behavior change.

♻️ Proposed refactor
   execute: async (message: Message): Promise<void> => {
     try {
       if (message.author.bot || !message.guild) {
-        return Promise.resolve();
+        return;
       }
@@
-      return Promise.resolve();
+      return;
     } catch (error) {
       logger.error('[MessageEvents] Error handling message create', error);
-      return Promise.resolve();
+      return;
     }
   },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/events/messageEvents.ts` around lines 344 - 363, In the execute async
function in messageEvents (execute: async (message: Message): Promise<void>),
remove the redundant Promise.resolve() usages and replace them with bare
returns: use plain return; after early exits (when message.author.bot ||
!message.guild) and at the end of the try block, and replace the
Promise.resolve() in the catch block with a simple return; so the async function
resolves naturally without unnecessary Promise.resolve() calls.
src/commands/fun/achievements.ts (1)

580-590: Avoid the as Date assertion via local narrowing.

isAchievementEarned already proves earnedAt != null, but TypeScript can't propagate the narrowing across the function boundary, so the cast remains. Capturing earnedAt in a local and narrowing inline removes the assertion.

♻️ Proposed refactor
-  if (isAchievementEarned(achievement)) {
-    const earnedDate = new Date(achievement.earnedAt as Date);
-    fieldValue += `\n✅ **Completed**: <t:${Math.floor(
-      earnedDate.getTime() / 1000
-    )}:R>`;
-  } else {
+  const { earnedAt } = achievement;
+  if (earnedAt != null && new Date(earnedAt).getTime() > 0) {
+    const earnedDate = new Date(earnedAt);
+    fieldValue += `\n✅ **Completed**: <t:${Math.floor(
+      earnedDate.getTime() / 1000
+    )}:R>`;
+  } else {

As per coding guidelines: "Leverage TypeScript's type narrowing instead of type assertions."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/fun/achievements.ts` around lines 580 - 590, Replace the type
assertion "achievement.earnedAt as Date" by first capturing and narrowing the
property locally: inside the isAchievementEarned(achievement) branch read const
earnedAt = achievement.earnedAt and rely on the local truthiness check (or the
knowledge from isAchievementEarned) to create earnedDate = new Date(earnedAt);
remove the "as Date" cast and use earnedDate where currently referenced (this
touches the block that builds fieldValue when isAchievementEarned returns true,
alongside symbols achievement.earnedAt and earnedDate).
src/util/counting/constants.ts (1)

36-41: Restore as const on MILESTONE_REACTIONS.

The trailing as const was dropped in this change. Without it, the emoji values widen from string literals to string, so consumers lose the literal type information that const assertions provide. The keyof typeof MILESTONE_REACTIONS usage in helpers.ts (deriveMilestone) still works, but immutability and value-literal narrowing are lost.

♻️ Proposed refactor
 export const MILESTONE_REACTIONS = {
   normal: '✅',
   multiples25: '✨',
   multiples50: '⭐',
   multiples100: '🎉',
-};
+} as const;

As per coding guidelines: "Use const assertions (as const) for immutable values and literal types."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/counting/constants.ts` around lines 36 - 41, Restore the const
assertion on MILESTONE_REACTIONS by appending "as const" to the exported object
literal so the emoji values remain string literal types and the object is
readonly; update the exported constant MILESTONE_REACTIONS declaration (the
object with keys normal, multiples25, multiples50, multiples100) to use a const
assertion to preserve literal types used by helpers like deriveMilestone.
src/commands/util/help.ts (2)

211-215: Use safelyRespond for the "command not found" reply.

This text-only response bypasses the centralized response/error helper used throughout this command. Prefer safelyRespond(interaction, ...) to keep reply/follow-up logic and Discord error logging consistent.

♻️ Proposed refactor
   if (!cmd) {
-    return interaction.editReply({
-      content: `Command \`${commandName}\` not found.`,
-    });
+    await safelyRespond(interaction, `Command \`${commandName}\` not found.`);
+    return;
   }

As per coding guidelines: "Use safelyRespond(interaction, content) from src/util/helpers.ts for all interaction responses."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/help.ts` around lines 211 - 215, Replace the direct
interaction.editReply call in the branch where cmd is falsy with the centralized
helper: call safelyRespond(interaction, `Command \`${commandName}\` not found.`)
(or safelyRespond(interaction, { content: `Command \`${commandName}\` not
found.` }) if your helper expects an options object); this changes the code
around the cmd check (variables: cmd, commandName, interaction) to use
safelyRespond imported from src/util/helpers.ts so reply/follow-up handling and
error logging remain consistent.

143-151: Don't silently swallow invalid component interactions inside the collector.

When validateInteraction(i) returns false, the handler returns without acknowledging the component interaction, leaving the clicking user with a hanging "thinking…" / failed-interaction state. Mirror the pattern used at the top of execute (Lines 34-41) and respond ephemerally before exiting.

🛠️ Suggested fix
         collector.on('collect', async (i) => {
           if (!(await validateInteraction(i))) {
+            await safelyRespond(i, 'Invalid interaction. Please try again.', true);
             return;
           }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/help.ts` around lines 143 - 151, When the collector
callback returns early because validateInteraction(i) is false it currently
leaves the component interaction unacknowledged; update the
collector.on('collect', async (i) => { ... }) handler to call safelyRespond(i,
'Invalid interaction.', true) (or mirror the same ephemeral response used at the
top of execute) before returning so the component is acknowledged; specifically
change the branch that checks validateInteraction(i) to send an ephemeral
response via safelyRespond and then return, referencing validateInteraction,
safelyRespond, the collector.on('collect' handler, and the interaction/i
variables to locate the code.
src/commands/fun/fact.ts (1)

129-186: Drop redundant createPaginationButtons parameter.

createPaginationButtons is already imported at the top of the file (Line 17), so passing it as a parameter to handlePaginationCollect is unnecessary indirection. Inline the import usage and simplify the signature.

♻️ Proposed refactor
 async function handlePaginationCollect(
   i: MessageComponentInteraction,
   interaction: ChatInputCommandInteraction,
   pages: EmbedBuilder[],
-  createPaginationButtons: (
-    totalPages: number,
-    currentPage: number
-  ) => ActionRowBuilder<ButtonBuilder>,
   currentPage: number,
   setCurrentPage: (newPage: number) => void
 ): Promise<void> {
@@
-  collector.on('collect', async (i) => {
-    await handlePaginationCollect(
-      i,
-      interaction,
-      pages,
-      createPaginationButtons,
-      currentPage,
-      (newPage) => {
-        currentPage = newPage;
-      }
-    );
-  });
+  collector.on('collect', async (i) => {
+    await handlePaginationCollect(
+      i,
+      interaction,
+      pages,
+      currentPage,
+      (newPage) => {
+        currentPage = newPage;
+      }
+    );
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/fun/fact.ts` around lines 129 - 186, The function
handlePaginationCollect currently accepts createPaginationButtons as a parameter
even though that helper is already imported; remove the createPaginationButtons
parameter from handlePaginationCollect's signature and calls, and inside the
function call the imported createPaginationButtons directly (i.e., replace
createPaginationButtons(pages.length, newPage) with the imported function).
Update all call sites of handlePaginationCollect to stop passing
createPaginationButtons and ensure the components array still uses .toJSON() on
the ActionRowBuilder returned by the imported createPaginationButtons; keep the
rest of the parameter list and logic in handlePaginationCollect unchanged.
src/util/counting/countingManager.ts (3)

191-198: Log message wording is slightly off.

VALID_MATH_EXPR_RE permits digits, operators, parentheses, and whitespace, so the rejected content isn't strictly "non-numeric" — it's "non-arithmetic". Consider tweaking to e.g. 'Ignored non-arithmetic message' to keep debug logs accurate.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/counting/countingManager.ts` around lines 191 - 198, The debug log
message in CountingManager uses misleading wording ("Ignored non-numeric
message") even though VALID_MATH_EXPR_RE allows operators and parentheses;
update the logger.debug call to use a clearer message such as "Ignored
non-arithmetic message" (keep the existing metadata: user and contentLength) so
logs accurately reflect rejections of non-arithmetic input; locate the check
around VALID_MATH_EXPR_RE.test(trimmed) and change only the string passed to
logger.debug.

226-244: Extract the 100 rollback threshold/step into a named constant.

Both the threshold check (data.currentCount > 100) and the rollback step (Math.floor(data.currentCount / rollbackStep) * rollbackStep) hard-code 100. The intent (rollback to nearest hundred when above 100, full reset otherwise) is non-obvious from a bare literal, and the two uses can drift if one is ever tweaked.

Move into src/util/counting/constants.ts, e.g. ROLLBACK_THRESHOLD = 100 / ROLLBACK_STEP = 100, and import here.

As per coding guidelines: "Use meaningful variable names instead of magic numbers - extract constants with descriptive names".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/counting/countingManager.ts` around lines 226 - 244, Extract the
magic number 100 into named constants by adding ROLLBACK_THRESHOLD and
ROLLBACK_STEP to src/util/counting/constants.ts and importing them into
src/util/counting/countingManager.ts; then replace the hard-coded checks and
calculations (the conditional data.currentCount > 100, the const rollbackStep =
100, and Math.floor(data.currentCount / rollbackStep) * rollbackStep) with the
imported ROLLBACK_THRESHOLD and ROLLBACK_STEP so setCount(rollbackTo), the
logger.debug call, and the returned rolledBackTo use the computed rollback based
on those constants while preserving existing behavior; refer to symbols
data.currentCount, setCount, logger.debug, rollbackTo and rolledBackTo to locate
the changes.

222-261: Type the reason parameter directly to drop the casts.

handleRollbackOrReset accepts reason: string and then asserts reason as CountingProcessInvalidReason at both return sites. Typing the parameter as CountingProcessInvalidReason eliminates the assertions, enables compile-time validation at every call site, and aligns with the guideline to "leverage type narrowing instead of type assertions".

♻️ Suggested change
-    async function handleRollbackOrReset(
-      reason: string
-    ): Promise<CountingProcessResult> {
+    async function handleRollbackOrReset(
+      reason: CountingProcessInvalidReason
+    ): Promise<CountingProcessResult> {
       if (data.currentCount > 100) {
         const rollbackStep = 100;
         const rollbackTo =
           Math.floor(data.currentCount / rollbackStep) * rollbackStep;
         await setCount(rollbackTo);

         logger.debug('[CountingManager] Invalid number caused rollback', {
           user: message.author.id.slice(-4),
           previousCount: data.currentCount,
           rolledBackTo: rollbackTo,
           reason,
         });

         return {
           isValid: false,
           expectedCount: expected,
-          reason: reason as CountingProcessInvalidReason,
+          reason,
           rolledBackTo: rollbackTo,
         };
       }

       await resetCounting();

       logger.debug('[CountingManager] Invalid number caused reset', {
         user: message.author.id.slice(-4),
         previousCount: data.currentCount,
         reason,
       });

       return {
         isValid: false,
         expectedCount: expected,
-        reason: reason as CountingProcessInvalidReason,
+        reason,
         rolledBackTo: 0,
       };
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/counting/countingManager.ts` around lines 222 - 261, Change the
handleRollbackOrReset function signature so its reason parameter is typed as
CountingProcessInvalidReason instead of string (function:
handleRollbackOrReset), then remove the redundant casts of reason as
CountingProcessInvalidReason in both returned objects; update any call sites to
pass a valid CountingProcessInvalidReason value (or refine their types) so
callers compile without assertions. This ensures compile-time validation and
eliminates the need for type assertions in the return objects (rolledBackTo
branches) while keeping behavior unchanged.
src/commands/fun/counting.ts (2)

270-506: Extract the duplicated pagination pipeline into a shared helper.

handleListBans and handleListWarnings share ~120 lines of essentially identical code: the getSelectRow builder, components composition, the message-component collector, the user-id filter, the getUpdatedPageFromComponent update step, and the safeRemoveComponents cleanup. The only differences are the customId prefix (counting_listbans_select vs counting_listwarnings_select) and the embed array.

Consolidate into a single helper (e.g., in src/util/helpers.ts) like runEmbedPaginator(interaction, pages, { selectCustomId, timeMs }) that handles getSelectRow, components, and the collector lifecycle. This will also let you fix the duplicated magic number 25 (lines 315, 433) once.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/fun/counting.ts` around lines 270 - 506, handleListBans and
handleListWarnings duplicate the entire pagination pipeline (getSelectRow,
components composition, message creation, collector handling, user filter, page
updates, and safeRemoveComponents); extract this into a shared helper like
runEmbedPaginator(interaction, pages, { selectCustomId, timeMs }) placed in
src/util/helpers.ts which implements the select/menu builder (replacing the
hardcoded 25 with a constant), composes components using
createPaginationButtons, wires the MessageComponentCollector, calls
getUpdatedPageFromComponent to update currentPage, filters by
interaction.user.id, defers/edits replies, and calls safeRemoveComponents on
end; update both handlers to call runEmbedPaginator with their pages and their
customId (e.g., 'counting_listbans_select'/'counting_listwarnings_select') and
remove the duplicated blocks (references: handleListBans, handleListWarnings,
getSelectRow, createPaginationButtons, getUpdatedPageFromComponent,
safeRemoveComponents).

313-342: Replace the literal 25 with a named constant.

Lines 315 and 433 both hard-code 25, which is Discord's StringSelectMenu option cap. Extract a module-level (or shared) constant such as MAX_SELECT_MENU_OPTIONS = 25 to make the intent explicit and ensure both list views stay in sync if the limit ever changes.

As per coding guidelines: "Use meaningful variable names instead of magic numbers - extract constants with descriptive names".

♻️ Suggested change
+const MAX_SELECT_MENU_OPTIONS = 25;
+
 // ...
   const getSelectRow = () => {
-    if (pages.length > 25) {
+    if (pages.length > MAX_SELECT_MENU_OPTIONS) {
       return null;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/fun/counting.ts` around lines 313 - 342, The code uses the magic
number 25 when building the select menu in getSelectRow (and elsewhere) which is
Discord's option cap; introduce a shared module-level constant such as
MAX_SELECT_MENU_OPTIONS = 25 and replace the literal 25 occurrences with that
constant (e.g., update the conditional in getSelectRow that checks pages.length
> 25 and any other hard-coded 25 checks) so the limit is explicit and consistent
across uses (references: getSelectRow, pages.length,
StringSelectMenuBuilder/select creation).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In @.github/workflows/ultracite.yml:
- Around line 78-90: Wrap the SARIF file read/parse around
JSON.parse(fs.readFileSync(reportPath, 'utf8')) in a try/catch to handle
partial/malformed SARIF; if parsing fails, catch the error, log a concise
message referencing reportPath and the error, and set runs/results to empty so
subsequent code that computes levelCounts ({ error, warning, note }) still runs
safely; ensure you keep the existing logic that defaults level to 'warning' and
updates levelCounts so the step doesn't throw and still produces a meaningful
result when parsing fails.

In `@src/commands/fun/achievements.ts`:
- Around line 341-349: The unauthorized-button handler inside
buttonCollector.on('collect', async (i: ButtonInteraction) => { ... }) sends the
ephemeral warning to the original interaction; change the call to safelyRespond
so it responds to the button interaction `i` (i.e., use safelyRespond(i, 'You
cannot use these buttons.', true)) so the clicking user receives the ephemeral
message (mirror the select-menu behavior used in the select collector).

In `@src/commands/fun/counting.ts`:
- Around line 117-120: The guard that checks for a missing guild (e.g., the
block with "const guild = interaction.guild; if (!guild) { return; }") returns
silently after interaction.deferReply() was already called in execute(), leaving
the interaction hung; update that guard (and the three other similar guards) to
call safelyRespond(interaction, "An error occurred") or a short generic error
message before returning so the deferred reply is fulfilled; ensure you use the
existing safelyRespond helper, and keep the guard early-return behavior after
calling safelyRespond to avoid further execution.

In `@src/commands/util/config.ts`:
- Around line 249-256: Replace the direct i.reply call in the collector's
'collect' handler with the project's interaction-guard pattern: first call await
validateInteraction(i) to ensure the interaction/channel/message is valid, then
call await safelyRespond(i, 'You cannot use this pagination.', true) (true =
ephemeral) instead of i.reply; keep the same early return behavior when
i.user.id !== interaction.user.id so the handler exits after safelyRespond.
Target the collector.on('collect', async (i: MessageComponentInteraction) => {
... }) block and update the non-invoker branch to use validateInteraction and
safelyRespond.

In `@src/db/functions/factFunctions.ts`:
- Around line 126-142: The branch in getRandomUnusedFact that resets usedOn then
unconditionally recurses can infinite-loop if there are no approved facts;
change it so after calling withDbRetryDrizzle(... update(factTable).set({
usedOn: null }).where(eq(factTable.approved, true))) you check the update result
(e.g., affected rows/count returned by the DB driver) or immediately re-query
facts (using the same logic that produced `facts`) and if zero rows were
affected or the re-query is still empty, do not recurse — instead return a
sentinel (null) or throw a clear error; only recurse when the reset actually
changed rows (or the re-query returns >0). Ensure you reference the existing
symbols factTable, withDbRetryDrizzle, invalidateCache, cacheKey, and
getRandomUnusedFact when making this change.

In `@src/db/functions/moderationFunctions.ts`:
- Around line 30-32: Update the inconsistent log message in
moderationFunctions.ts: in the function that logs "[moderationDbFunctions]
Database not initialized, update member moderation history" (the update member
moderation history path), change the message to include "cannot" so it matches
other messages (e.g., "[moderationDbFunctions] Database not initialized, cannot
update member moderation history").

In `@src/events/messageEvents.ts`:
- Around line 268-296: The code currently calls
shouldAllowRestoreCountingMessage twice causing duplicate guild.fetchAuditLogs
calls; change the flow to fetch audit-log once and reuse its result by either
(A) removing the standalone shouldAllowRestoreCountingMessage call and awaiting
maybeRestoreCountingMessage(...) and assign executor from its returned value, or
(B) perform a single shouldAllowRestoreCountingMessage(...) at the top, capture
{ allowed, executor } and pass those values into
maybeRestoreCountingMessage(message, guild, { allowed, executor }) (or adjust
maybeRestoreCountingMessage signature to accept them) so that
guild.fetchAuditLogs is only invoked once per delete event; update logger
handling to use the single fetched executor and preserve existing error
handling.

In `@src/util/helpers.ts`:
- Around line 246-252: The zero/negative-delay branch in schedule() invokes cb()
via setTimeout without awaiting or catching rejections, causing unhandled
promise rejections; change that branch to run the callback inside the same async
try/catch error-handling used by the main schedule path (e.g., wrap the
setTimeout handler in an async IIFE or call Promise.resolve(cb()).catch(...))
and forward errors to the same logger used by schedule (the same error logging
call used in the main await cb() catch block) so async rejections are logged
consistently for delayMs <= 0.

---

Nitpick comments:
In @.github/workflows/ultracite.yml:
- Around line 3-7: The workflow currently triggers on both push and pull_request
which causes duplicate runs for PR branches; update the on: block in
ultracite.yml to scope push to specific branches (e.g., change push: to push:
branches: [main, development]) while keeping pull_request: and schedule:
unchanged so PRs trigger only via pull_request and pushes only on your selected
branches (refer to the on: push, on: pull_request, and on: schedule keys in the
diff).

In `@CONTRIBUTING.md`:
- Around line 76-83: List items under numbered entries use 2-space indentation
causing list breakage; fix by increasing indentation for the nested paragraph
and fenced code blocks to 3 spaces so they align under the numeral. In the "1.
**Install Git hooks**" block change the two-space indents before the paragraph
and the ```bash fenced block and its content to three spaces, and make the same
3-space adjustment for the other affected numbered entries (the blocks currently
at the other two locations mentioned in the review). Ensure the paragraph and
the opening/closing ``` lines as well as the code line ("yarn setup") are
indented with three spaces to match the rest of the file.

In `@src/commands/fun/achievements.ts`:
- Around line 580-590: Replace the type assertion "achievement.earnedAt as Date"
by first capturing and narrowing the property locally: inside the
isAchievementEarned(achievement) branch read const earnedAt =
achievement.earnedAt and rely on the local truthiness check (or the knowledge
from isAchievementEarned) to create earnedDate = new Date(earnedAt); remove the
"as Date" cast and use earnedDate where currently referenced (this touches the
block that builds fieldValue when isAchievementEarned returns true, alongside
symbols achievement.earnedAt and earnedDate).

In `@src/commands/fun/counting.ts`:
- Around line 270-506: handleListBans and handleListWarnings duplicate the
entire pagination pipeline (getSelectRow, components composition, message
creation, collector handling, user filter, page updates, and
safeRemoveComponents); extract this into a shared helper like
runEmbedPaginator(interaction, pages, { selectCustomId, timeMs }) placed in
src/util/helpers.ts which implements the select/menu builder (replacing the
hardcoded 25 with a constant), composes components using
createPaginationButtons, wires the MessageComponentCollector, calls
getUpdatedPageFromComponent to update currentPage, filters by
interaction.user.id, defers/edits replies, and calls safeRemoveComponents on
end; update both handlers to call runEmbedPaginator with their pages and their
customId (e.g., 'counting_listbans_select'/'counting_listwarnings_select') and
remove the duplicated blocks (references: handleListBans, handleListWarnings,
getSelectRow, createPaginationButtons, getUpdatedPageFromComponent,
safeRemoveComponents).
- Around line 313-342: The code uses the magic number 25 when building the
select menu in getSelectRow (and elsewhere) which is Discord's option cap;
introduce a shared module-level constant such as MAX_SELECT_MENU_OPTIONS = 25
and replace the literal 25 occurrences with that constant (e.g., update the
conditional in getSelectRow that checks pages.length > 25 and any other
hard-coded 25 checks) so the limit is explicit and consistent across uses
(references: getSelectRow, pages.length, StringSelectMenuBuilder/select
creation).

In `@src/commands/fun/fact.ts`:
- Around line 129-186: The function handlePaginationCollect currently accepts
createPaginationButtons as a parameter even though that helper is already
imported; remove the createPaginationButtons parameter from
handlePaginationCollect's signature and calls, and inside the function call the
imported createPaginationButtons directly (i.e., replace
createPaginationButtons(pages.length, newPage) with the imported function).
Update all call sites of handlePaginationCollect to stop passing
createPaginationButtons and ensure the components array still uses .toJSON() on
the ActionRowBuilder returned by the imported createPaginationButtons; keep the
rest of the parameter list and logic in handlePaginationCollect unchanged.

In `@src/commands/util/config.ts`:
- Line 305: The defer reply call uses a string literal for flags—change await
interaction.deferReply({ flags: ['Ephemeral'] }) (and the similar call at the
other occurrence around line 253) to use the enum constant from discord.js by
importing MessageFlags and passing flags: [MessageFlags.Ephemeral]; update the
imports if needed and ensure both places consistently use MessageFlags.Ephemeral
for style and easier refactors.

In `@src/commands/util/help.ts`:
- Around line 211-215: Replace the direct interaction.editReply call in the
branch where cmd is falsy with the centralized helper: call
safelyRespond(interaction, `Command \`${commandName}\` not found.`) (or
safelyRespond(interaction, { content: `Command \`${commandName}\` not found.` })
if your helper expects an options object); this changes the code around the cmd
check (variables: cmd, commandName, interaction) to use safelyRespond imported
from src/util/helpers.ts so reply/follow-up handling and error logging remain
consistent.
- Around line 143-151: When the collector callback returns early because
validateInteraction(i) is false it currently leaves the component interaction
unacknowledged; update the collector.on('collect', async (i) => { ... }) handler
to call safelyRespond(i, 'Invalid interaction.', true) (or mirror the same
ephemeral response used at the top of execute) before returning so the component
is acknowledged; specifically change the branch that checks
validateInteraction(i) to send an ephemeral response via safelyRespond and then
return, referencing validateInteraction, safelyRespond, the
collector.on('collect' handler, and the interaction/i variables to locate the
code.

In `@src/commands/util/members.ts`:
- Around line 63-66: Extract the magic number 25 into a named constant (e.g.,
MAX_SELECT_MENU_OPTIONS = 25 as const) and use it where Discord select-menu
option limits are checked; update the condition in getSelectMenuRow (replace
pages.length > 25) to pages.length > MAX_SELECT_MENU_OPTIONS and do the same
replacement in src/commands/fun/leaderboard.ts (or wherever the same 25 is used)
so the limit is self-documenting and centrally maintained.

In `@src/commands/util/user-info.ts`:
- Around line 132-154: The fallback logic uses || which should be replaced with
nullish coalescing (??) to follow guidelines: change the assignment of
currentMute (currently "const currentMute = muteModerations.find((m) =>
m.active) || muteModerations[0];") to use ?? instead of ||, and likewise change
currentBan in buildCurrentBanField (currently "const currentBan =
banModerations.find((m) => m.active) || banModerations[0];") to use ??; this
ensures undefined results from .find are handled safely without treating falsy
values like 0 or '' as missing.

In `@src/db/functions/factFunctions.ts`:
- Around line 21-26: The addFact function accepts the entire factTableTypes
which allows callers to pass server-managed fields (e.g., id, usedOn,
timestamps) that are ignored; change its parameter type to a narrow input shape
such as Pick<factTableTypes, 'content' | 'source' | 'addedBy'> & { approved?:
boolean } (or define an explicit AddFactInput type) and update the function
signature for addFact to use that type so the contract explicitly disallows
server-managed columns.

In `@src/db/functions/levelFunctions.ts`:
- Around line 79-94: The post-insert SELECT (inside withDbRetryDrizzle that
assigns createdLevel) can observe a race because the INSERT ON CONFLICT DO
NOTHING and the subsequent SELECT run as separate calls; update the logic in the
function using either a single INSERT ... ON CONFLICT DO UPDATE SET <noop>
RETURNING * to get the row in the same statement, or execute both statements
inside a single db.transaction so the SELECT runs on the same
connection/transaction; locate the code around withDbRetryDrizzle, createdLevel,
and levelTable (and consider interactions with deleteUserLevel) and replace the
two-call pattern with one of these atomic approaches.

In `@src/events/messageEvents.ts`:
- Around line 344-363: In the execute async function in messageEvents (execute:
async (message: Message): Promise<void>), remove the redundant Promise.resolve()
usages and replace them with bare returns: use plain return; after early exits
(when message.author.bot || !message.guild) and at the end of the try block, and
replace the Promise.resolve() in the catch block with a simple return; so the
async function resolves naturally without unnecessary Promise.resolve() calls.

In `@src/util/counting/constants.ts`:
- Around line 36-41: Restore the const assertion on MILESTONE_REACTIONS by
appending "as const" to the exported object literal so the emoji values remain
string literal types and the object is readonly; update the exported constant
MILESTONE_REACTIONS declaration (the object with keys normal, multiples25,
multiples50, multiples100) to use a const assertion to preserve literal types
used by helpers like deriveMilestone.

In `@src/util/counting/countingManager.ts`:
- Around line 191-198: The debug log message in CountingManager uses misleading
wording ("Ignored non-numeric message") even though VALID_MATH_EXPR_RE allows
operators and parentheses; update the logger.debug call to use a clearer message
such as "Ignored non-arithmetic message" (keep the existing metadata: user and
contentLength) so logs accurately reflect rejections of non-arithmetic input;
locate the check around VALID_MATH_EXPR_RE.test(trimmed) and change only the
string passed to logger.debug.
- Around line 226-244: Extract the magic number 100 into named constants by
adding ROLLBACK_THRESHOLD and ROLLBACK_STEP to src/util/counting/constants.ts
and importing them into src/util/counting/countingManager.ts; then replace the
hard-coded checks and calculations (the conditional data.currentCount > 100, the
const rollbackStep = 100, and Math.floor(data.currentCount / rollbackStep) *
rollbackStep) with the imported ROLLBACK_THRESHOLD and ROLLBACK_STEP so
setCount(rollbackTo), the logger.debug call, and the returned rolledBackTo use
the computed rollback based on those constants while preserving existing
behavior; refer to symbols data.currentCount, setCount, logger.debug, rollbackTo
and rolledBackTo to locate the changes.
- Around line 222-261: Change the handleRollbackOrReset function signature so
its reason parameter is typed as CountingProcessInvalidReason instead of string
(function: handleRollbackOrReset), then remove the redundant casts of reason as
CountingProcessInvalidReason in both returned objects; update any call sites to
pass a valid CountingProcessInvalidReason value (or refine their types) so
callers compile without assertions. This ensures compile-time validation and
eliminates the need for type assertions in the return objects (rolledBackTo
branches) while keeping behavior unchanged.

In `@src/util/telemetry/otelLogTransport.ts`:
- Around line 129-138: The isErrorShapedRecord predicate is needlessly complex:
remove the redundant casts on meta and drop hasName (dead code) and set
isErrorShapedRecord to simply hasStack || hasMessage; specifically, replace the
current hasStack/hasMessage/hasName definitions to use meta (already typed)
directly (e.g., typeof meta.stack === 'string' && checks for '\n' or ' at ') and
then assign isErrorShapedRecord = hasStack || hasMessage, removing hasName and
its use; if you intended a stricter rule instead, explicitly change the
predicate to require hasName && (hasStack || hasMessage) so the intent is clear.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a6c9a348-fc4e-41ab-8ec1-0f58f4f384a0

📥 Commits

Reviewing files that changed from the base of the PR and between 594551b and 93e2860.

📒 Files selected for processing (25)
  • .github/workflows/ultracite.yml
  • CONTRIBUTING.md
  • LICENSE
  • README.md
  • SECURITY.md
  • src/commands/fun/achievements.ts
  • src/commands/fun/counting.ts
  • src/commands/fun/fact.ts
  • src/commands/fun/leaderboard.ts
  • src/commands/fun/rank.ts
  • src/commands/util/config.ts
  • src/commands/util/help.ts
  • src/commands/util/members.ts
  • src/commands/util/user-info.ts
  • src/db/functions/factFunctions.ts
  • src/db/functions/levelFunctions.ts
  • src/db/functions/memberFunctions.ts
  • src/db/functions/moderationFunctions.ts
  • src/events/interactionCreate.ts
  • src/events/messageEvents.ts
  • src/util/counting/constants.ts
  • src/util/counting/countingManager.ts
  • src/util/counting/helpers.ts
  • src/util/helpers.ts
  • src/util/telemetry/otelLogTransport.ts
✅ Files skipped from review due to trivial changes (3)
  • SECURITY.md
  • LICENSE
  • README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/events/interactionCreate.ts

Comment on lines +78 to +90
const sarif = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
const runs = sarif.runs ?? [];
const results = runs.flatMap((run) => run.results ?? []);

const levelCounts = { error: 0, warning: 0, note: 0 };
for (const result of results) {
const level = result.level ?? 'warning';
if (levelCounts[level] === undefined) {
levelCounts.note += 1;
} else {
levelCounts[level] += 1;
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Wrap SARIF parsing in try/catch.

If biome ci writes a partial/malformed SARIF (e.g., when killed mid-write or hitting an internal error), JSON.parse throws and this whole step fails with a stack trace instead of a useful PR comment. Since the upload-sarif step is gated by hashFiles, a parse failure here would also leave reviewers with no signal at all.

🛡️ Proposed fix
-            } else {
-              const sarif = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
-              const runs = sarif.runs ?? [];
-              const results = runs.flatMap((run) => run.results ?? []);
+            } else {
+              let runs = [];
+              try {
+                const sarif = JSON.parse(fs.readFileSync(reportPath, 'utf8'));
+                runs = sarif.runs ?? [];
+              } catch (err) {
+                core.warning(`Failed to parse SARIF report: ${err.message}`);
+                body += `Could not parse SARIF report: \`${err.message}\`.`;
+                runs = null;
+              }
+              if (runs !== null) {
+              const results = runs.flatMap((run) => run.results ?? []);

(close the new conditional before the trailing } at line 115)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ultracite.yml around lines 78 - 90, Wrap the SARIF file
read/parse around JSON.parse(fs.readFileSync(reportPath, 'utf8')) in a try/catch
to handle partial/malformed SARIF; if parsing fails, catch the error, log a
concise message referencing reportPath and the error, and set runs/results to
empty so subsequent code that computes levelCounts ({ error, warning, note })
still runs safely; ensure you keep the existing logic that defaults level to
'warning' and updates levelCounts so the step doesn't throw and still produces a
meaningful result when parsing fails.

Comment on lines 341 to 349
buttonCollector.on('collect', async (i: ButtonInteraction) => {
if (i.user.id !== interaction.user.id) {
await i.reply({
content: 'You cannot use these buttons.',
flags: ['Ephemeral'],
});
await safelyRespond(
interaction,
'You cannot use these buttons.',
true
);
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Send the unauthorized-button warning to the clicking user, not the original interaction.

When a non-invoker clicks a pagination button, the response should target i (the button interaction), but this passes interaction (the original chat-input). Compare with the select-menu collector at Line 302, which correctly uses safelyRespond(i, ...). As written, the editReply goes to the original interaction instead of replying ephemerally to the user who clicked.

🐛 Suggested fix
       buttonCollector.on('collect', async (i: ButtonInteraction) => {
         if (i.user.id !== interaction.user.id) {
-          await safelyRespond(
-            interaction,
-            'You cannot use these buttons.',
-            true
-          );
+          await safelyRespond(i, 'You cannot use these buttons.', true);
           return;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
buttonCollector.on('collect', async (i: ButtonInteraction) => {
if (i.user.id !== interaction.user.id) {
await i.reply({
content: 'You cannot use these buttons.',
flags: ['Ephemeral'],
});
await safelyRespond(
interaction,
'You cannot use these buttons.',
true
);
return;
}
buttonCollector.on('collect', async (i: ButtonInteraction) => {
if (i.user.id !== interaction.user.id) {
await safelyRespond(i, 'You cannot use these buttons.', true);
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/fun/achievements.ts` around lines 341 - 349, The
unauthorized-button handler inside buttonCollector.on('collect', async (i:
ButtonInteraction) => { ... }) sends the ephemeral warning to the original
interaction; change the call to safelyRespond so it responds to the button
interaction `i` (i.e., use safelyRespond(i, 'You cannot use these buttons.',
true)) so the clicking user receives the ephemeral message (mirror the
select-menu behavior used in the select collector).

Comment on lines +117 to +120
const guild = interaction.guild;
if (!guild) {
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Silent return after deferReply() leaves the interaction hanging.

Lines 117-120 (and similarly at 176-179, 211-214, 249-252) bail out without any reply. Because execute() already called interaction.deferReply() (line 650), the user is stuck on "thinking..." until the token expires. validateInteraction should make this branch unreachable, but if it ever fires it produces a hung interaction. Send a generic response via safelyRespond before returning.

♻️ Suggested fix (apply to all four guard sites)
-  const guild = interaction.guild;
-  if (!guild) {
-    return;
-  }
+  const { guild } = interaction;
+  if (!guild) {
+    await safelyRespond(interaction, 'This command can only be used in a server.');
+    return;
+  }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const guild = interaction.guild;
if (!guild) {
return;
}
const { guild } = interaction;
if (!guild) {
await safelyRespond(interaction, 'This command can only be used in a server.');
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/fun/counting.ts` around lines 117 - 120, The guard that checks
for a missing guild (e.g., the block with "const guild = interaction.guild; if
(!guild) { return; }") returns silently after interaction.deferReply() was
already called in execute(), leaving the interaction hung; update that guard
(and the three other similar guards) to call safelyRespond(interaction, "An
error occurred") or a short generic error message before returning so the
deferred reply is fulfilled; ensure you use the existing safelyRespond helper,
and keep the guard early-return behavior after calling safelyRespond to avoid
further execution.

Comment on lines +249 to 256
collector.on('collect', async (i: MessageComponentInteraction) => {
if (i.user.id !== interaction.user.id) {
await i.reply({
content: 'You cannot use this pagination.',
flags: ['Ephemeral'],
});
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Inconsistent non-invoker reply pattern — should use validateInteraction + safelyRespond like the rest of the PR.

Other paginated commands updated in this PR (e.g., src/commands/util/members.ts and src/commands/fun/leaderboard.ts) gate the non-invoker response behind validateInteraction(i) and dispatch through safelyRespond(i, '...', true). Here i.reply(...) is called directly, which (a) bypasses the safe-respond error logging if the interaction is no longer repliable, and (b) skips the channel/message validity check. Aligning this site with the project pattern keeps behavior uniform.

♻️ Suggested change
   collector.on('collect', async (i: MessageComponentInteraction) => {
     if (i.user.id !== interaction.user.id) {
-      await i.reply({
-        content: 'You cannot use this pagination.',
-        flags: ['Ephemeral'],
-      });
+      if (await validateInteraction(i)) {
+        await safelyRespond(i, 'You cannot use this pagination.', true);
+      }
       return;
     }

As per coding guidelines: "Use safelyRespond(interaction, content) from src/util/helpers.ts for all interaction responses" and "Call await validateInteraction(interaction) before processing".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/commands/util/config.ts` around lines 249 - 256, Replace the direct
i.reply call in the collector's 'collect' handler with the project's
interaction-guard pattern: first call await validateInteraction(i) to ensure the
interaction/channel/message is valid, then call await safelyRespond(i, 'You
cannot use this pagination.', true) (true = ephemeral) instead of i.reply; keep
the same early return behavior when i.user.id !== interaction.user.id so the
handler exits after safelyRespond. Target the collector.on('collect', async (i:
MessageComponentInteraction) => { ... }) block and update the non-invoker branch
to use validateInteraction and safelyRespond.

Comment on lines 126 to 142
if (facts.length === 0) {
await withDbRetryDrizzle(
async () => {
return await db
.update(schema.factTable)
.update(factTable)
.set({ usedOn: null })
.where(eq(schema.factTable.approved, true));
.where(eq(factTable.approved, true));
},
{
operationName: 'reset-used-facts',
forceRetry: true,
},
}
);

await invalidateCache(cacheKey);
return await getRandomUnusedFact();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Possible infinite recursion when no approved facts exist.

If there are zero approved facts in the table, the empty-facts branch resets usedOn for eq(approved, true) (matching nothing), invalidates the cache, and recurses. The next call again finds an empty facts array and recurses again — a stack-overflowing infinite loop. Add a base case that bails when the reset affected no rows (or when the post-reset re-query is still empty), and return a sentinel/throw rather than recursing unconditionally.

🛡️ Suggested guard
     if (facts.length === 0) {
-      await withDbRetryDrizzle(
+      const reset = await withDbRetryDrizzle(
         async () => {
           return await db
             .update(factTable)
             .set({ usedOn: null })
             .where(eq(factTable.approved, true));
         },
         {
           operationName: 'reset-used-facts',
           forceRetry: true,
         }
       );
 
       await invalidateCache(cacheKey);
+      // If nothing was reset, there are no approved facts at all — avoid recursing forever.
+      if (!reset || (reset as { rowCount?: number }).rowCount === 0) {
+        throw new Error('No approved facts available');
+      }
       return await getRandomUnusedFact();
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/functions/factFunctions.ts` around lines 126 - 142, The branch in
getRandomUnusedFact that resets usedOn then unconditionally recurses can
infinite-loop if there are no approved facts; change it so after calling
withDbRetryDrizzle(... update(factTable).set({ usedOn: null
}).where(eq(factTable.approved, true))) you check the update result (e.g.,
affected rows/count returned by the DB driver) or immediately re-query facts
(using the same logic that produced `facts`) and if zero rows were affected or
the re-query is still empty, do not recurse — instead return a sentinel (null)
or throw a clear error; only recurse when the reset actually changed rows (or
the re-query returns >0). Ensure you reference the existing symbols factTable,
withDbRetryDrizzle, invalidateCache, cacheKey, and getRandomUnusedFact when
making this change.

Comment on lines 30 to 32
logger.error(
'[moderationDbFunctions] Database not initialized, update member moderation history',
'[moderationDbFunctions] Database not initialized, update member moderation history'
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Minor: missing "cannot" in log message.

The other "Database not initialized" log messages in this PR use the form "... Database not initialized, cannot <action>". This one reads "... Database not initialized, update member moderation history" (verb missing). Tiny inconsistency.

-        '[moderationDbFunctions] Database not initialized, update member moderation history'
+        '[moderationDbFunctions] Database not initialized, cannot update member moderation history'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/db/functions/moderationFunctions.ts` around lines 30 - 32, Update the
inconsistent log message in moderationFunctions.ts: in the function that logs
"[moderationDbFunctions] Database not initialized, update member moderation
history" (the update member moderation history path), change the message to
include "cannot" so it matches other messages (e.g., "[moderationDbFunctions]
Database not initialized, cannot update member moderation history").

Comment on lines 268 to 296
const { guild } = message;

let executor = undefined as User | undefined;
try {
const audit = await shouldAllowRestoreCountingMessage(
guild,
message.author?.id,
message.channelId,
message.client?.user?.id
);
executor = audit.executor;
} catch (error) {
logger.warn(
'[MessageEvents] Could not determine audit-log executor for deleted message',
error
);
}

try {
const countingChannelId = config.channels.counting;
if (
message.channelId === countingChannelId &&
message.content &&
message.author
) {
const { author } = message;
const trimmed = message.content.trim();
const parsed = Number(trimmed);
if (Number.isInteger(parsed)) {
const data = await getCountingData();

let allowRestore = true;
try {
const logs = await guild.fetchAuditLogs({
type: AuditLogEvent.MessageDelete,
limit: 5,
});
const entries = Array.from(logs.entries.values());

const matching = entries.find((e) => {
const target = e.target as { id?: string } | null;
const targetId =
target?.id ?? (e as { targetId?: string }).targetId;
const extra = e.extra as {
channel?: { id?: string };
channelId?: string;
} | null;
const channelId = extra?.channel?.id ?? extra?.channelId;
if (!targetId) return false;
if (targetId !== author?.id) return false;
if (channelId && channelId !== message.channelId) return false;
return true;
});

const executor = matching?.executor;
if (
executor &&
author &&
executor.id !== author.id &&
executor.id !== message.client?.user?.id
) {
allowRestore = false;
}
} catch (error) {
logger.warn(
'[MessageEvents] Could not fetch audit logs when checking message delete; allowing restore by fallback',
error,
);
allowRestore = true;
}

if (data.currentCount === parsed && allowRestore) {
const countingChannel =
guild.channels.cache.get(countingChannelId);
if (countingChannel?.isTextBased()) {
await countingChannel.send(
`🔁 Restoring deleted counting message: **${trimmed}** (originally by <@${message.author.id}>)`,
);
}
}
}
}
// Still attempt to restore counting messages when appropriate.
// We intentionally ignore the return value here to avoid overwriting
// the executor derived from audit logs for general moderator attribution.
await maybeRestoreCountingMessage(message, guild);
} catch (error) {
logger.error(
'[MessageEvents] Error attempting to restore deleted counting message',
error,
error
);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Audit log is fetched twice per message delete; reuse the executor.

shouldAllowRestoreCountingMessage is invoked once at Line 272 to derive the moderator and again indirectly inside maybeRestoreCountingMessage at Line 290 (which itself calls shouldAllowRestoreCountingMessage). Both paths run the same guild.fetchAuditLogs(...) request. The comment at Lines 287-289 implies the duplication exists to avoid “overwriting” the executor, but both calls compute the same matching entry, so the second fetch is redundant Discord API traffic for every deletion event.

Either:

  1. Use the executor returned from maybeRestoreCountingMessage(...) and skip the standalone shouldAllowRestoreCountingMessage call, or
  2. Fetch the audit-log result once at the top of the handler and pass { allowed, executor } into maybeRestoreCountingMessage.
♻️ Suggested refactor (option 1)
-      let executor = undefined as User | undefined;
-      try {
-        const audit = await shouldAllowRestoreCountingMessage(
-          guild,
-          message.author?.id,
-          message.channelId,
-          message.client?.user?.id
-        );
-        executor = audit.executor;
-      } catch (error) {
-        logger.warn(
-          '[MessageEvents] Could not determine audit-log executor for deleted message',
-          error
-        );
-      }
-
-      try {
-        // Still attempt to restore counting messages when appropriate.
-        // We intentionally ignore the return value here to avoid overwriting
-        // the executor derived from audit logs for general moderator attribution.
-        await maybeRestoreCountingMessage(message, guild);
-      } catch (error) {
-        logger.error(
-          '[MessageEvents] Error attempting to restore deleted counting message',
-          error
-        );
-      }
+      let executor: User | undefined;
+      try {
+        executor = await maybeRestoreCountingMessage(message, guild);
+      } catch (error) {
+        logger.error(
+          '[MessageEvents] Error attempting to restore deleted counting message',
+          error
+        );
+      }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/events/messageEvents.ts` around lines 268 - 296, The code currently calls
shouldAllowRestoreCountingMessage twice causing duplicate guild.fetchAuditLogs
calls; change the flow to fetch audit-log once and reuse its result by either
(A) removing the standalone shouldAllowRestoreCountingMessage call and awaiting
maybeRestoreCountingMessage(...) and assign executor from its returned value, or
(B) perform a single shouldAllowRestoreCountingMessage(...) at the top, capture
{ allowed, executor } and pass those values into
maybeRestoreCountingMessage(message, guild, { allowed, executor }) (or adjust
maybeRestoreCountingMessage signature to accept them) so that
guild.fetchAuditLogs is only invoked once per delete event; update logger
handling to use the single fetched executor and preserve existing error
handling.

Comment thread src/util/helpers.ts
Comment on lines 246 to 252
if (delayMs <= 0) {
// Execute immediately (next tick) for zero / negative values
setTimeout(() => {
void cb();
cb();
}, 0);
return;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Async errors from cb() are swallowed in the zero/negative-delay path.

The main schedule() path wraps await cb() in try/catch and logs failures, but the delayMs <= 0 branch invokes cb() without await/.catch(). If cb is async and rejects, this surfaces as an unhandled promise rejection rather than going through the same logger path. Make this branch behave consistently with schedule:

♻️ Suggested fix
   if (delayMs <= 0) {
     // Execute immediately (next tick) for zero / negative values
-    setTimeout(() => {
-      cb();
-    }, 0);
+    setTimeout(async () => {
+      try {
+        await cb();
+      } catch (error) {
+        logger.error('[scheduleLargeTimeout] Callback error', error);
+      }
+    }, 0);
     return;
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (delayMs <= 0) {
// Execute immediately (next tick) for zero / negative values
setTimeout(() => {
void cb();
cb();
}, 0);
return;
}
if (delayMs <= 0) {
// Execute immediately (next tick) for zero / negative values
setTimeout(async () => {
try {
await cb();
} catch (error) {
logger.error('[scheduleLargeTimeout] Callback error', error);
}
}, 0);
return;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/util/helpers.ts` around lines 246 - 252, The zero/negative-delay branch
in schedule() invokes cb() via setTimeout without awaiting or catching
rejections, causing unhandled promise rejections; change that branch to run the
callback inside the same async try/catch error-handling used by the main
schedule path (e.g., wrap the setTimeout handler in an async IIFE or call
Promise.resolve(cb()).catch(...)) and forward errors to the same logger used by
schedule (the same error logging call used in the main await cb() catch block)
so async rejections are logged consistently for delayMs <= 0.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 issues found across 25 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/util/telemetry/otelLogTransport.ts">

<violation number="1" location="src/util/telemetry/otelLogTransport.ts:137">
P2: `hasMessage` alone makes records look like errors, so normal `name` fields can be incorrectly exported as `exception.type`.</violation>
</file>

<file name="src/commands/fun/fact.ts">

<violation number="1" location="src/commands/fun/fact.ts:117">
P2: Log the caught exception as structured metadata (`{ error }`) so stack/details are preserved by the logger formatter.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment on lines +137 to +138
const isErrorShapedRecord =
hasStack || hasMessage || (hasName && (hasStack || hasMessage));
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: hasMessage alone makes records look like errors, so normal name fields can be incorrectly exported as exception.type.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/util/telemetry/otelLogTransport.ts, line 137:

<comment>`hasMessage` alone makes records look like errors, so normal `name` fields can be incorrectly exported as `exception.type`.</comment>

<file context>
@@ -126,6 +126,17 @@ function cleanAttributes(
+  const hasMessage =
+    typeof (meta as Record<string, unknown>).message === 'string';
+  const hasName = typeof (meta as Record<string, unknown>).name === 'string';
+  const isErrorShapedRecord =
+    hasStack || hasMessage || (hasName && (hasStack || hasMessage));
+
</file context>
Suggested change
const isErrorShapedRecord =
hasStack || hasMessage || (hasName && (hasStack || hasMessage));
const isErrorShapedRecord = hasStack || (hasName && hasMessage);
Fix with Cubic

Comment thread src/commands/fun/fact.ts
return;
}
} catch (error) {
logger.error('[FactCommand] Error handling subcommand', error);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot Apr 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Log the caught exception as structured metadata ({ error }) so stack/details are preserved by the logger formatter.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/fun/fact.ts, line 117:

<comment>Log the caught exception as structured metadata (`{ error }`) so stack/details are preserved by the logger formatter.</comment>

<file context>
@@ -91,26 +91,34 @@ const command: SubcommandCommand = {
+          });
+      }
+    } catch (error) {
+      logger.error('[FactCommand] Error handling subcommand', error);
+      await safelyRespond(
+        interaction,
</file context>
Suggested change
logger.error('[FactCommand] Error handling subcommand', error);
logger.error('[FactCommand] Error handling subcommand', { error });
Fix with Cubic

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci Updates CI dependencies Updates to dependencies documentation Improvements or additions to documentation enhancement New feature or request github_actions Pull requests that update GitHub Actions code priority Priority issues/PRs refactor Refactors code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants