feat: switch code quality setup to ultracite and biome#769
feat: switch code quality setup to ultracite and biome#769ahmadk953 wants to merge 11 commits intodevelopmentfrom
Conversation
|
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:
For more information about GitHub Code Scanning, check out the documentation. |
📝 WalkthroughWalkthroughReplaces ESLint/Prettier with Ultracite/Biome (tooling, CI, editor settings) and adds Ultracite docs; refactors DB schema/type usage and Redis init; modularizes logging handlers; restructures counting, achievements, giveaways, data-retention, many commands/events, and shared helpers (validateInteraction/safelyRespond). Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant CLI as yarn check/fix
participant Ultracite as Ultracite
participant Biome as Biome (backend)
participant Config as biome.jsonc
User->>CLI: run lint/format command
CLI->>Ultracite: invoke ultracite
Ultracite->>Config: load biome.jsonc
Config-->>Ultracite: return rules/extensions
Ultracite->>Biome: defer to Biome engine
Biome-->>Ultracite: return diagnostics/fixes
Ultracite->>CLI: report/apply fixes
CLI-->>User: display results
sequenceDiagram
participant Git as Pre-commit
participant Husky
participant LintStaged
participant Ultracite
participant TSC as TypeScript
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
Poem
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
Ultracite reportFound 0 issue(s): ERROR 0 | WARN 0 | NOTE 0 |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
9 issues found across 109 files
Confidence score: 2/5
- There are multiple high-confidence, user-impacting risks, especially in
src/commands/fun/fact.tswhere using globalMAX(id)is race-prone and can attach moderation buttons to the wrong fact under concurrent submissions. src/events/messageEvents.tsandsrc/util/counting/countingManager.tsintroduce 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.tsand 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
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
There was a problem hiding this comment.
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 | 🟡 MinorValidate and migrate persisted deploy-state entries before casting.
readDeployStatereturnsparsed as DeployStatewithout validating entry shape. Older state files (withoutcommandCount) will deserialize successfully but violate the newDeployStateEntrycontract and can leakundefinedmetadata.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 | 🟠 MajorGuard against select-menu option overflow (>25 pages).
With
getLevelLeaderboard(100)andusersPerPageconfigurable as low as 1 (via thelimitoption), 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 | 🟠 MajorDon't send the raw reload exception back to Discord.
errorcan 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 | 🟠 MajorThe 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 | 🟠 MajorThis reconnect path can report a false failure.
ensureRedisConnection()now only starts the reconnect attempt; it does not wait for Redis to be ready. CheckingisRedisConnected()immediately after it will usually still befalse, so/backend-manager rediscan 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 | 🟠 MajorAvoid 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 | 🟡 MinorInconsistent error handling:
ensureDbInitialized()is outside the try-catch block.In
getMemberModerationHistory,ensureDbInitialized()(line 76) and the subsequentdbcheck are outside the try-catch, unlikeupdateMemberModerationHistorywhich wraps them. IfensureDbInitialized()throws, it won't be caught and handled byhandleDbError.🛠️ 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 | 🟡 MinorFix 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 | 🟠 MajorMake the fallback logger non-throwing.
JSON.stringifystill throws on circular objects andBigInt. 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 | 🟡 MinorInconsistent session expiry handling.
Unlike other handlers that use
getSessionOrReplyExpired, this function usesgetSessiondirectly 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
getSessionOrReplyExpiredexpects aBuilderInteractiontype. You may need to extend theBuilderInteractiontype alias to includeStringSelectMenuInteraction.🤖 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 | 🟠 MajorBackfill
totalCorrectduring migration.
totalCorrectwas added to the migrated shape, but this function never marks the payload as changed when that field is missing, andfinalDatastill falls back todata.totalCorrect. For legacy Redis blobs, the next successful count turnsundefined += 1intoNaN, 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 | 🟠 MajorCatch block must handle deferred interactions with
editReply().After
startGiveawayBuilder()defers the interaction (line 24), the error handler at line 107 checks only!interaction.repliedbefore callinginteraction.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 | 🟠 MajorUse 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 fromsrc/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 | 🟠 MajorFail fast in production when TLS cert loading fails.
Returning
undefinedforsslsilently 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 | 🟠 MajorUse shared interaction helpers in
execute(Line 13 onward).Please replace the manual guard with
await validateInteraction(interaction)and route replies throughsafelyRespond(...)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 "UsesafelyRespond(interaction, content)fromsrc/util/helpers.tsfor 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 | 🟠 MajorUse
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)fromsrc/util/helpers.tsfor 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 | 🟠 MajorAdd required
validateInteractiongate 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 | 🟠 MajorWrap DB access with consistent DB error handling.
Line 194 calls
getMember(user.id)directly without ahandleDbError(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; usehandleDbError(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 | 🟠 MajorUse 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 "UsesafelyRespond(interaction, content)fromsrc/util/helpers.tsfor 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 | 🟠 MajorAvoid mapping generic
name/stackmetadata to exception fields unconditionally.This currently rewrites ordinary metadata (e.g.,
nameon non-error logs) intoexception.typeand 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 | 🟠 MajorRace condition:
getLastInsertedFactId()can return wrong fact ID if concurrent submissions occur.Between
addFact()completing andgetLastInsertedFactId()executing, another user could submit a fact. TheMAX(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 | 🟠 MajorUse the shared interaction helpers here.
Line 10 still calls
interaction.reply()directly, and this handler never invokesvalidateInteraction(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: UsesafelyRespond(interaction, content)fromsrc/util/helpers.tsfor all interaction responses" and "Callawait 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 | 🟠 MajorEmpty config sections can make
/configfail.
join('\n')androlesTextcan 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 | 🟠 MajorThis 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 readsrootCA.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 startuses process namepoixpixel-discord-bot,yarn restartto reload); ensure TLS certificates are present atcerts/psql-ca.pemfor 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 | 🟠 MajorDon't report a successful flush after hard delete failures.
If both
DELandUNLINKfail for a batch, the helper only logs and continues, butflushRedisCache()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 | 🟠 MajorValidate 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-standardvalidateInteraction()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 | 🟠 MajorValidate 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 | 🟠 MajorAdd
awaitto handleeditReply()errors in specific-command path.
handleSpecificCommand()returns a promise frominteraction.editReply()on both code paths (lines 202–204 and 253). Line 50 drops theawait, meaning if Discord's API rejects, the error occurs outside the surroundingtry/catchblock 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 | 🟠 MajorThese hand-written row types are already out of sync with the schema definitions.
memberTableTypes.currentlyInServeranddiscordUsernameare optional in the interface despitenotNull()in the table definition (lines 26-27)levelTableTypes.lastMessageTimestampis typed as optionalDatebut should beDate | nullsince the column is nullable (line 52)moderationTableTypes.durationis required in the interface despite the column lackingnotNull()and defaulting to empty string (line 75)Downstream DB functions cast query results using
as levelTableTypes[]andas 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 | 🟠 MajorUse atomic SET with EX to ensure TTL always applies.
The
set()function currently executesSETandEXPIREas separate commands. IfEXPIREfails afterSETsucceeds, the key persists without TTL—a data reliability issue. Additionally, the truthy check skipsttl: 0. ioredis supports atomic SET-with-expiry: useredis.set(key, value, 'EX', ttl)and gate onttl !== undefinedto 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 | 🟠 MajorRun 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)beforedeferReply()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 | 🟠 MajorRun 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)beforedeferReply()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 | 🟠 MajorReuse the counting evaluator for restore checks.
The counting pipeline accepts arithmetic expressions via
sanitizeAndEval(), but this helper only doesNumber(trimmed). Deleted valid counts like1+1or3*4will 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 | 🟠 MajorDon'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 like01or1/0no 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 | 🟠 MajorEliminate duplicate audit log fetch in messageDelete handler.
The handler calls
fetchAuditLogs()twice per deleted message: once inshouldAllowRestoreCountingMessage()(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.
…mprove error handling
…e message response logic
…Respond and validateInteraction
…n and executor tracking
…ck trace handling
There was a problem hiding this comment.
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.
| ); | ||
|
|
||
| return await invalidNumberRollbackOrReset(data, message, trimmed); | ||
| return await handleRollbackOrReset('not_a_number'); |
There was a problem hiding this comment.
P1: Early return in catch exits before local variables (like expected) are initialized, risking incorrect state or runtime errors.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/util/counting/countingManager.ts, line 215:
<comment>Early return in catch exits before local variables (like `expected`) are initialized, risking incorrect state or runtime errors.</comment>
<file context>
@@ -187,17 +188,31 @@ export async function processCountingMessage(
+ message.guild?.members?.me ?? undefined
+ );
+
+ return await handleRollbackOrReset('not_a_number');
}
</file context>
| createdAt: | ||
| createdAt != null && !Number.isNaN(createdAt.getTime()) | ||
| ? createdAt | ||
| : (record.createdAt as Date), |
There was a problem hiding this comment.
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.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/db/functions/utils/moderationUtils.ts, line 16:
<comment>`createdAt` fallback uses an unsafe cast to return the original value, which can leak invalid/non-Date data instead of a normalized date value.</comment>
<file context>
@@ -4,13 +4,19 @@ export function normalizeModerationDates(
+ createdAt:
+ createdAt != null && !Number.isNaN(createdAt.getTime())
+ ? createdAt
+ : (record.createdAt as Date),
+ expiresAt:
+ expiresAt != null && !Number.isNaN(expiresAt.getTime())
</file context>
| : (record.createdAt as Date), | |
| : null, |
| } | ||
| } | ||
| } | ||
| executor = await maybeRestoreCountingMessage(message, guild); |
There was a problem hiding this comment.
P2: Moderator attribution regressed for most messageDelete logs because executor is now only derived from counting-message restoration logic.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/events/messageEvents.ts, line 267:
<comment>Moderator attribution regressed for most `messageDelete` logs because executor is now only derived from counting-message restoration logic.</comment>
<file context>
@@ -251,21 +262,16 @@ export const messageDelete: Event<typeof Events.MessageDelete> = {
+ let executor = undefined as User | undefined;
try {
- await maybeRestoreCountingMessage(message, guild);
+ executor = await maybeRestoreCountingMessage(message, guild);
} catch (error) {
logger.error(
</file context>
| * @param interaction The interaction that caused the error. | ||
| */ | ||
| function handleInteractionError(error: unknown, interaction: Interaction) { | ||
| async function handleInteractionError( |
There was a problem hiding this comment.
P2: handleInteractionError is now async, but its caller does not await it, creating a floating promise in the catch path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/events/interactionCreate.ts, line 333:
<comment>`handleInteractionError` is now async, but its caller does not await it, creating a floating promise in the catch path.</comment>
<file context>
@@ -330,7 +330,10 @@ async function handleSelectMenu(interaction: Interaction) {
* @param interaction The interaction that caused the error.
*/
-function handleInteractionError(error: unknown, interaction: Interaction) {
+async function handleInteractionError(
+ error: unknown,
+ interaction: Interaction
</file context>
| return { | ||
| ...newLevel, | ||
| id: 0, | ||
| } as levelTableTypes; |
There was a problem hiding this comment.
P2: getUserLevel returns a synthetic id: 0 instead of the actual DB row after insert, which can return invalid/stale user-level data.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/db/functions/levelFunctions.ts, line 79:
<comment>`getUserLevel` returns a synthetic `id: 0` instead of the actual DB row after insert, which can return invalid/stale user-level data.</comment>
<file context>
@@ -79,7 +76,10 @@ export async function getUserLevel(
);
- return newLevel;
+ return {
+ ...newLevel,
+ id: 0,
</file context>
| return { | |
| ...newLevel, | |
| id: 0, | |
| } as levelTableTypes; | |
| const createdLevel = await withDbRetryDrizzle( | |
| async () => { | |
| return await db | |
| .select() | |
| .from(levelTable) | |
| .where(eq(levelTable.discordId, discordId)) | |
| .then((rows) => rows[0]); | |
| }, | |
| { | |
| operationName: 'get-user-level-after-create', | |
| } | |
| ); | |
| if (!createdLevel) { | |
| throw new Error('Failed to load user level after create'); | |
| } | |
| return createdLevel as levelTableTypes; |
| throw new Error('Database not initialized'); | ||
| } | ||
|
|
||
| const { discordId, ...updateFields } = updates; |
There was a problem hiding this comment.
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.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/db/functions/memberFunctions.ts, line 238:
<comment>Guard against empty `updateFields` before calling `.set(...)`; the current signature allows `{ discordId }` only, which can produce an empty update payload and fail at runtime.</comment>
<file context>
@@ -240,17 +235,13 @@ export async function updateMember({
throw new Error('Database not initialized');
}
+ const { discordId, ...updateFields } = updates;
+
await withDbRetryDrizzle(
</file context>
| const { discordId, ...updateFields } = updates; | |
| const { discordId, ...updateFields } = updates; | |
| if (Object.keys(updateFields).length === 0) { | |
| return; | |
| } |
| if (!interaction.isChatInputCommand() || !interaction.guild) return; | ||
| if (!(await validateInteraction(interaction))) { | ||
| await safelyRespond( | ||
| interaction, |
There was a problem hiding this comment.
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.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/commands/fun/achievements.ts, line 55:
<comment>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.</comment>
<file context>
@@ -48,7 +50,12 @@ const command = {
- if (!(interaction.isChatInputCommand() && interaction.guild)) {
+ if (!(await validateInteraction(interaction))) {
+ await safelyRespond(
+ interaction,
+ 'Invalid interaction. Please try again.',
+ true
</file context>
| } as const; | ||
| }; | ||
|
|
||
| export const numericLikeRegex = /^[\d+\-*/()\s]+$/; |
There was a problem hiding this comment.
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>
|
|
||
| 1. **Install Git hooks** | ||
|
|
||
| After installing dependencies run the setup script to initialize Husky git hooks so local pre-commit checks (commitlint, lint-staged, etc.) run as expected: |
There was a problem hiding this comment.
P3: The docs incorrectly describe commitlint as a pre-commit check; it runs on the commit-msg hook.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At CONTRIBUTING.md, line 78:
<comment>The docs incorrectly describe commitlint as a pre-commit check; it runs on the commit-msg hook.</comment>
<file context>
@@ -73,6 +73,14 @@ Before you begin, ensure you have the following installed:
+1. **Install Git hooks**
+
+ After installing dependencies run the setup script to initialize Husky git hooks so local pre-commit checks (commitlint, lint-staged, etc.) run as expected:
+
+ ```bash
</file context>
| After installing dependencies run the setup script to initialize Husky git hooks so local pre-commit checks (commitlint, lint-staged, etc.) run as expected: | |
| After installing dependencies run the setup script to initialize Husky git hooks so local checks (commit-msg, pre-commit, etc.) run as expected: |
| active, | ||
| }: schema.moderationTableTypes): Promise<void> { | ||
| export async function updateMemberModerationHistory( | ||
| moderation: Omit<Partial<moderationTableTypes>, 'id'> & { |
There was a problem hiding this comment.
P3: JSDoc parameter tags no longer match the refactored function signature (single moderation object), which makes API docs misleading.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/db/functions/moderationFunctions.ts, line 27:
<comment>JSDoc parameter tags no longer match the refactored function signature (single `moderation` object), which makes API docs misleading.</comment>
<file context>
@@ -18,21 +18,18 @@ import { normalizeModerationDates } from './utils/moderationUtils.js';
- active,
-}: moderationTableTypes): Promise<void> {
+export async function updateMemberModerationHistory(
+ moderation: Omit<Partial<moderationTableTypes>, 'id'> & {
+ discordId: string;
+ moderatorDiscordId: string;
</file context>
There was a problem hiding this comment.
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 | 🟠 MajorMove moderation table deactivation before the early return check in
executeUnmute.The early return at line 162–164 skips the
UPDATE moderationTable ... active=falseblock. If the member is missing from the database or already marked unmuted, mute rows remainactive=true. SinceloadActiveMutesqueries formoderationTable.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 forcreatedAt.
${interaction.guild.createdAt}callsDate.prototype.toString(), which yields an engine/locale-dependent string like"Wed Apr 09 2025 10:30:00 GMT+0000". You already exportmsToDiscordTimestampfrom@/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 emptyupdateFields.If a caller invokes
updateMember({ discordId })with no other keys,updateFieldsis{}anddb.update(memberTable).set({})will throw at runtime (Drizzle requires at least one column inSET). 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: Misleadingas Datecast increatedAtfallback.When
createdAtis null or NaN, the fallback(record.createdAt as Date)assertsDatebut the underlying value could benull,undefined, or an invalid string (since we only reach that branch whennew Date(record.createdAt)was itself null/NaN). Downstream code that relies on theDatetype will receive a non-Datevalue.Either keep the real input type in the return type, or fall back consistently (e.g., to
nulllikeexpiresAt, or tonew Date(0)/ current time as a sentinel). At minimum the asymmetric handling betweencreatedAtandexpiresAtdeserves 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
📒 Files selected for processing (46)
.devcontainer/devcontainer.json.github/workflows/ultracite.yml.husky/commit-msgCONTRIBUTING.mdpackage.jsonsrc/commands/fun/achievements.tssrc/commands/fun/counting.tssrc/commands/fun/fact.tssrc/commands/fun/giveaway.tssrc/commands/fun/leaderboard.tssrc/commands/fun/rank.tssrc/commands/moderation/ban.tssrc/commands/moderation/kick.tssrc/commands/moderation/mute.tssrc/commands/moderation/unban.tssrc/commands/moderation/unmute.tssrc/commands/moderation/warn.tssrc/commands/testing/test-join.tssrc/commands/testing/test-leave.tssrc/commands/util/backend-manager.tssrc/commands/util/config.tssrc/commands/util/help.tssrc/commands/util/manage-achievement.tssrc/commands/util/members.tssrc/commands/util/ping.tssrc/commands/util/purge.tssrc/commands/util/recalculate-levels.tssrc/commands/util/reload-config.tssrc/commands/util/restart.tssrc/commands/util/rules.tssrc/commands/util/server.tssrc/commands/util/user-info.tssrc/commands/util/xp.tssrc/db/functions/factFunctions.tssrc/db/functions/levelFunctions.tssrc/db/functions/memberFunctions.tssrc/db/functions/moderationFunctions.tssrc/db/functions/utils/moderationUtils.tssrc/db/redis.tssrc/db/schema.tssrc/events/interactionCreate.tssrc/events/messageEvents.tssrc/util/counting/constants.tssrc/util/counting/countingManager.tssrc/util/helpers.tssrc/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 tosrc/*and imports MUST append.jsextension (e.g.,import { foo } from '@/util/helpers.js')Import database functions from
src/db/db.ts, not direct DB access. Always usehandleDbError(error, operation)for consistent error handlingAll Redis keys MUST be prefixed with
'bot:'(e.g.,bot:counting:${guildId}). Always checkisRedisConnected()before non-critical caching operationsUse
safelyRespond(interaction, content)fromsrc/util/helpers.tsfor all interaction responses instead of directly calling interaction methodsCall
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. fromsrc/util/logger.js, passing Error instances and metadata objects instead of string concatenationUse
typekeyword for imports only used in type positions (e.g.,import type { Guild } from 'discord.js')Never modify
achievementDefinitionsTabledirectly; seed once and update via admin commands only. Progress tracked inuserAchievementsTable.progressXP cooldown is stored in Redis as
bot:xp_cooldown:${userId}and enforced vialeveling.xpCooldownconfigurationUse 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 fromsrc/with.jsextensions for TypeScript filesUse
constby default,letonly when reassignment is needed, never usevarUse arrow functions for callbacks and short functions
Prefer
for...ofloops over.forEach()and indexedforloopsUse optional chaining (
?.) and nullish coalescing (??) for safer property accessPrefer template literals over string concatenation
Use destructuring for object and array assignments
Always
awaitpromises in async functions and useasync/awaitsynta...
Files:
src/commands/util/recalculate-levels.tssrc/commands/fun/rank.tssrc/commands/util/server.tssrc/commands/fun/fact.tssrc/commands/fun/counting.tssrc/commands/moderation/mute.tssrc/commands/util/config.tssrc/db/functions/moderationFunctions.tssrc/db/functions/factFunctions.tssrc/commands/util/user-info.tssrc/util/counting/countingManager.tssrc/db/functions/utils/moderationUtils.tssrc/commands/util/help.tssrc/util/telemetry/otelLogTransport.tssrc/db/functions/memberFunctions.tssrc/util/helpers.tssrc/events/messageEvents.ts
src/commands/**/*.ts
📄 CodeRabbit inference engine (.github/copilot-instructions.md)
All commands in
src/commands/**/*.tsMUST default export an object withdata(SlashCommandBuilder) andasync execute(interaction)method, satisfying Command, OptionsCommand, or SubcommandCommand typesIf a command takes >3 seconds to execute, call
interaction.deferReply()immediately to prevent interaction token expiration
Files:
src/commands/util/recalculate-levels.tssrc/commands/fun/rank.tssrc/commands/util/server.tssrc/commands/fun/fact.tssrc/commands/fun/counting.tssrc/commands/moderation/mute.tssrc/commands/util/config.tssrc/commands/util/user-info.tssrc/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
unknownoveranywhen the type is genuinely unknown in TypeScriptUse const assertions (
as const) for immutable values and literal types in TypeScriptLeverage TypeScript's type narrowing instead of type assertions
Files:
src/commands/util/recalculate-levels.tssrc/commands/fun/rank.tssrc/commands/util/server.tssrc/commands/fun/fact.tssrc/commands/fun/counting.tssrc/commands/moderation/mute.tssrc/commands/util/config.tssrc/db/functions/moderationFunctions.tssrc/db/functions/factFunctions.tssrc/commands/util/user-info.tssrc/util/counting/countingManager.tssrc/db/functions/utils/moderationUtils.tssrc/commands/util/help.tssrc/util/telemetry/otelLogTransport.tssrc/db/functions/memberFunctions.tssrc/util/helpers.tssrc/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...ofloops over.forEach()and indexedforloops in JavaScript/TypeScriptUse optional chaining (
?.) and nullish coalescing (??) for safer property access in JavaScript/TypeScriptPrefer template literals over string concatenation in JavaScript/TypeScript
Use destructuring for object and array assignments in JavaScript/TypeScript
Use
constby default,letonly when reassignment is needed, nevervarin JavaScript/TypeScriptAlways
awaitpromises in async functions - don't forget to use the return valueUse
async/awaitsyntax instead of promise chains for better readability in JavaScript/TypeScriptHandle errors appropriately in async code with try-catch blocks
Don't use async functions as Promise executors
Remove
console.log,debugger, andalertstatements from production codeThrow
Errorobjects with descriptive messages in JavaScript/TypeScript, not strings or other valuesUse
try-catchblocks meaningfully - don't catch errors just to rethrow themPrefer 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 todocument.cookiein JavaScript/TypeScriptAvoid 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.tssrc/commands/fun/rank.tssrc/commands/util/server.tssrc/commands/fun/fact.tssrc/commands/fun/counting.tssrc/commands/moderation/mute.tssrc/commands/util/config.tssrc/db/functions/moderationFunctions.tssrc/db/functions/factFunctions.tssrc/commands/util/user-info.tssrc/util/counting/countingManager.tssrc/db/functions/utils/moderationUtils.tssrc/commands/util/help.tssrc/util/telemetry/otelLogTransport.tssrc/db/functions/memberFunctions.tssrc/util/helpers.tssrc/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.tssrc/commands/fun/rank.tssrc/commands/util/server.tssrc/commands/fun/fact.tssrc/commands/fun/counting.tssrc/commands/moderation/mute.tssrc/commands/util/config.tssrc/commands/util/user-info.tssrc/commands/util/help.tssrc/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.tssrc/commands/fun/rank.tssrc/commands/util/server.tssrc/commands/fun/fact.tssrc/commands/fun/counting.tssrc/commands/moderation/mute.tssrc/commands/util/config.tssrc/commands/util/user-info.tssrc/util/counting/countingManager.tssrc/commands/util/help.tssrc/util/helpers.tssrc/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.tssrc/commands/fun/rank.tssrc/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.tssrc/commands/fun/rank.tssrc/commands/util/server.tssrc/commands/moderation/mute.tssrc/commands/util/config.tssrc/commands/util/help.tssrc/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.tssrc/commands/fun/rank.tssrc/commands/util/server.tssrc/commands/fun/fact.tssrc/commands/fun/counting.tssrc/commands/moderation/mute.tssrc/commands/util/config.tssrc/commands/util/user-info.tssrc/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.tssrc/commands/fun/rank.tssrc/commands/util/server.tssrc/commands/fun/fact.tssrc/commands/fun/counting.tssrc/commands/moderation/mute.tssrc/commands/util/config.tssrc/commands/util/user-info.tssrc/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.tssrc/commands/util/server.tssrc/commands/fun/fact.tssrc/commands/moderation/mute.tssrc/commands/util/help.tssrc/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.tssrc/commands/util/server.tssrc/commands/fun/fact.tssrc/commands/moderation/mute.tssrc/commands/util/help.tssrc/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.tssrc/commands/moderation/mute.tssrc/commands/util/help.tssrc/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.tssrc/commands/moderation/mute.tssrc/commands/util/help.tssrc/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.tssrc/db/functions/moderationFunctions.tssrc/util/counting/countingManager.tssrc/db/functions/memberFunctions.tssrc/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.tssrc/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.tssrc/commands/util/user-info.tssrc/commands/util/help.tssrc/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.tssrc/db/functions/factFunctions.tssrc/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.tssrc/db/functions/memberFunctions.tssrc/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/fixscripts and lint-staged usage.
| on: | ||
| push: | ||
| pull_request: | ||
| schedule: | ||
| - cron: "25 21 * * 1" | ||
|
|
||
| jobs: | ||
| ultracite: | ||
| name: Run ultracite scanning | ||
| runs-on: ubuntu-latest | ||
| permissions: | ||
| contents: read | ||
| security-events: write | ||
| actions: read | ||
| issues: write | ||
| pull-requests: write |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat .github/workflows/ultracite.ymlRepository: ahmadk953/poixpixel-discord-bot
Length of output: 5248
🌐 Web query:
GitHub Actions fork pull request read-only token restrict permissions security
💡 Result:
In GitHub Actions, pull requests from forks using the standard pull_request event trigger receive a read-only GITHUB_TOKEN by design for security reasons. This prevents malicious code from forks (pwn requests) from writing to the repository, accessing secrets, or exfiltrating data using the token. Permissions specified in the workflow YAML (e.g., permissions: { contents: write }) are automatically downgraded to read-only for these events, regardless of settings. Key security features and restrictions: - No access to repository secrets. - GITHUB_TOKEN scoped to read permissions across scopes like contents, metadata, etc. - Workflows may require manual approval from a repo maintainer before running (configurable in repo/org settings under Actions > General > Fork pull request workflows). To restrict permissions further (principle of least privilege): - Set default workflow permissions at repo/org level to contents: read (Actions settings > General > Workflow permissions). - Explicitly declare minimal permissions in YAML: permissions: contents: read pull-requests: read Unspecified permissions default to none. For cases needing write access or secrets (e.g., auto-posting comments, pushing fixes): - Use pull_request_target event: Runs in base repo context with full configured permissions and secrets access. However, high risk—do not checkout/run untrusted PR code. Instead, use safe patterns like artifact passing via workflow_run. - For private repos: Org/repo settings allow "Send write tokens to workflows from pull requests" (with approval), but avoid for public repos. Best practices from GitHub docs: - Always use least privilege; audit third-party actions. - Enable required approvals for fork PRs. - For read-only tasks (linting, testing), pull_request suffices securely. - Monitor for token theft attempts (e.g., curl exfiltration). This behavior remains current as of 2026 per official docs.
Citations:
- 1: https://github.blog/changelog/2021-04-20-github-actions-control-permissions-for-github_token
- 2: https://stackoverflow.com/questions/75744849/permission-to-repo-denied-to-github-actions-for-pr-coming-from-a-fork-only
- 3: https://stackoverflow.com/questions/75655770/github-actions-requiring-secrets-on-a-fork-origin-pr
- 4: https://docs.github.com/actions/reference/authentication-in-a-workflow
- 5: https://docs.github.com/en/actions/concepts/security/compromised-runners
- 6: https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization
- 7: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository
- 8: https://docs.github.com/actions/using-workflows/workflow-syntax-for-github-actions
Guard write operations for forked pull requests.
This workflow runs on pull_request while later steps write PR comments and upload code scanning results. Fork PR runs receive a read-only token by GitHub design, so those write steps will fail even with permissions declared. Add fork guards to prevent failures:
🛡️ Proposed guard for downstream write steps
- name: Comment PR with Ultracite results
- if: always() && github.event_name == 'pull_request'
+ if: >
+ always() &&
+ github.event_name == 'pull_request' &&
+ github.event.pull_request.head.repo.full_name == github.repository
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v4
- if: always()
+ if: >
+ always() &&
+ (github.event_name != 'pull_request' ||
+ github.event.pull_request.head.repo.full_name == github.repository)🤖 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 - 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.
| - name: Generate Biome SARIF report | ||
| run: yarn biome ci --reporter sarif --reporter-file biome-results.sarif ./src |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
Biome CLI --max-diagnostics flag SARIF report
💡 Result:
The Biome CLI supports the --max-diagnostics global flag, which caps the number of diagnostics (errors, warnings, info) displayed across all reporters, including SARIF. Default is 20. Use --max-diagnostics=100 to increase or --max-diagnostics=none to lift the limit entirely. To generate a SARIF report: biome lint --reporter=sarif --reporter-file=report.sarif (or biome check, biome ci). The SARIF reporter (added in v2.4) emits diagnostics in SARIF v2.1.0 format. The --max-diagnostics limit applies unless using certain reporters like summary in some cases where it's auto-lifted, but for SARIF, specify explicitly to ensure all diagnostics are included. Example for full SARIF without limit: biome check --reporter=sarif --reporter-file=report.sarif --max-diagnostics=none
Citations:
- 1: https://biomejs.dev/reference/cli/
- 2: https://next.biomejs.dev/reference/cli
- 3: feat(cli): add SARIF reporter biomejs/biome#8631
- 4: https://biomejs.dev/reference/reporters/
- 5: https://biomejs.dev/reference/reporters
🌐 Web query:
Biome diagnostic limit default cap biome ci command
💡 Result:
The default cap for the number of diagnostics displayed by Biome CLI commands, including biome ci, is 20. This is controlled by the global --max-diagnostics option, which caps the amount of diagnostics shown. Use --max-diagnostics=none to lift the limit entirely. The biome ci command, designed for CI environments, inherits this global option but historically did not enforce a strict limit in some contexts (e.g., certain reporters), though the default remains 20. To see all diagnostics in CI, explicitly set --max-diagnostics=none. Usage example: biome ci --max-diagnostics=none .
Citations:
- 1: https://biomejs.dev/reference/cli/
- 2: https://next.biomejs.dev/reference/cli
- 3: https://www.biomeconsulting.com
- 4: https://v1.biomejs.dev/uk/reference/cli
Add --max-diagnostics=none to emit the complete SARIF report.
Biome's diagnostics are capped at 20 by default, so this command will upload only the first 20 findings. Use --max-diagnostics=none to include all diagnostics in the SARIF output.
📄 Proposed fix
- run: yarn biome ci --reporter sarif --reporter-file biome-results.sarif ./src
+ run: yarn biome ci --max-diagnostics=none --reporter sarif --reporter-file biome-results.sarif ./src📝 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.
| - name: Generate Biome SARIF report | |
| run: yarn biome ci --reporter sarif --reporter-file biome-results.sarif ./src | |
| - name: Generate Biome SARIF report | |
| run: yarn biome ci --max-diagnostics=none --reporter sarif --reporter-file biome-results.sarif ./src |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/ultracite.yml around lines 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`).
| - name: Upload analysis results to GitHub | ||
| uses: github/codeql-action/upload-sarif@v4 | ||
| if: always() | ||
| with: | ||
| sarif_file: biome-results.sarif | ||
| wait-for-processing: true |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n .github/workflows/ultracite.yml | sed -n '140,155p'Repository: ahmadk953/poixpixel-discord-bot
Length of output: 411
🏁 Script executed:
cat -n .github/workflows/ultracite.yml | head -200Repository: ahmadk953/poixpixel-discord-bot
Length of output: 6291
🏁 Script executed:
# Check if there's any other handling of the biome-results.sarif file in the workflow
rg "biome-results.sarif" .github/workflows/ultracite.ymlRepository: ahmadk953/poixpixel-discord-bot
Length of output: 256
🏁 Script executed:
# Check if there are any other conditional guards or error handling around the SARIF file
rg -B 2 -A 2 "upload-sarif" .github/workflows/ultracite.ymlRepository: ahmadk953/poixpixel-discord-bot
Length of output: 209
🌐 Web query:
github codeql-action upload-sarif missing file behavior
💡 Result:
The github/codeql-action/upload-sarif action throws a ConfigurationError and fails the workflow if the specified sarif_file path does not exist or, if it is a directory, contains no SARIF files. This behavior is defined in the source code of src/upload-lib.ts in the getSarifFilePaths function: - If !fs.existsSync(sarifPath), throws ConfigurationError("Path does not exist: ${sarifPath}"). - If the path is a directory and findSarifFilesInDir(sarifPath).length === 0, throws ConfigurationError("No SARIF files found to upload in "${sarifPath}""). This is consistent across versions, including recent changes in PRs like #3123 (Sep 2025), which fixed failures in specific no-file scenarios for Code Quality but retained the general error-throwing for missing paths/files. Older issues (e.g., #2285, #1061, #7) confirm the "Path does not exist" error when the file is not generated or path is incorrect. To avoid failure, ensure the SARIF file(s) exist at the specified relative path before calling the action (e.g., use conditional steps or || true in generation commands). The action logs "Processing sarif files: [...]" only if files are found.
Citations:
- 1: https://github.com/github/codeql-action/blob/f843d941/src/upload-lib.ts
- 2: Fix
upload-sarifAction failing if there are no Code Scanning SARIF files github/codeql-action#3123 - 3: Fix
upload-sarifnot uploading non-.sariffiles github/codeql-action#3157 - 4: CodeQL Action Fails to Find eslint-results.sarif: Path Does Not Exist Error github/codeql-action#2285
- 5: Sarif file uploading results fail v2 for
@microsoft/eslint-formatter-sarifgithub/codeql-action#1061 - 6: https://github.com/github/codeql-action/blob/main/upload-sarif/action.yml
- 7: https://docs.github.com/en/code-security/how-tos/find-and-fix-code-vulnerabilities/integrate-with-existing-tools/uploading-a-sarif-file-to-github
Only upload SARIF when the file exists.
With if: always(), this step runs even after biome generation fails, and the missing biome-results.sarif file causes upload-sarif@v4 to throw a ConfigurationError. Gate the step on hashFiles(...) so the original biome failure message remains visible instead of being masked by an upload failure.
🧯 Proposed guard
- name: Upload analysis results to GitHub
uses: github/codeql-action/upload-sarif@v4
- if: always()
+ if: always() && hashFiles('biome-results.sarif') != ''
with:
sarif_file: biome-results.sarif
wait-for-processing: true🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.github/workflows/ultracite.yml around lines 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.
| try { | ||
| await setCount(count); | ||
| await safelyRespond( | ||
| interaction, | ||
| `Count has been set to **${count}**. The next number should be **${count + 1}**.` | ||
| ); | ||
| } catch (error) { | ||
| await safelyRespond(interaction, `Failed to set the count: ${error}`, true); | ||
| } |
There was a problem hiding this comment.
Do not echo raw errors back to users.
Line 103 can expose internal Redis/storage errors in Discord. Log the Error instance and send a generic failure message instead. As per coding guidelines, “Use structured logging with logger.info(), logger.error(), etc. from src/util/logger.js, passing Error instances and metadata objects instead of string concatenation.”
Suggested fix
} catch (error) {
- await safelyRespond(interaction, `Failed to set the count: ${error}`, true);
+ logger.error('[CountingCommand] Failed to set count', error);
+ await safelyRespond(interaction, 'Failed to set the count.', true);
}
}📝 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.
| try { | |
| await setCount(count); | |
| await safelyRespond( | |
| interaction, | |
| `Count has been set to **${count}**. The next number should be **${count + 1}**.` | |
| ); | |
| } catch (error) { | |
| await safelyRespond(interaction, `Failed to set the count: ${error}`, true); | |
| } | |
| try { | |
| await setCount(count); | |
| await safelyRespond( | |
| interaction, | |
| `Count has been set to **${count}**. The next number should be **${count + 1}**.` | |
| ); | |
| } catch (error) { | |
| logger.error('[CountingCommand] Failed to set count', error); | |
| await safelyRespond(interaction, 'Failed to set the count.', true); | |
| } |
🤖 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 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.
| 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(); |
There was a problem hiding this comment.
🧩 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.tsRepository: 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 5Repository: 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 10Repository: 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 -30Repository: 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.tsRepository: 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.
| 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.
| let evaluated: number | null; | ||
| try { | ||
| evaluated = sanitizeAndEval(trimmed); | ||
| } catch { | ||
| } catch (err) { | ||
| logger.debug('[CountingManager] Invalid numeric expression evaluated', { | ||
| user: message.author.id.slice(-4), | ||
| error: err, | ||
| }); | ||
|
|
||
| await handleMistake( | ||
| message.author.id, | ||
| message.guild ?? undefined, | ||
| message.guild?.members?.me ?? undefined, | ||
| message.guild?.members?.me ?? undefined | ||
| ); | ||
|
|
||
| void safeDM( | ||
| message, | ||
| '⚠️ A mistake was detected. Repeated mistakes may lead to a counting ban.', | ||
| ); | ||
|
|
||
| return await invalidNumberRollbackOrReset(data, message, trimmed); | ||
| return await handleRollbackOrReset('not_a_number'); | ||
| } | ||
|
|
||
| const count = evaluated; | ||
| const expected = data.currentCount + 1; | ||
|
|
||
| if (count !== expected) { | ||
| const reason: CountingProcessInvalidReason = | ||
| count > expected ? 'too_high' : 'too_low'; | ||
|
|
||
| const { warning, warningsCount } = await handleMistake( | ||
| message.author.id, | ||
| message.guild ?? undefined, | ||
| message.guild?.members?.me ?? undefined, | ||
| ); | ||
|
|
||
| if (warning) { | ||
| void safeDM( | ||
| message, | ||
| `⚠️ You reached ${MISTAKE_THRESHOLD} mistakes. Warning ${warningsCount}/${MAX_WARNINGS}. Warnings reset ${msToDiscordTimestamp(Date.now() + WARNING_PERIOD_MS)}.`, | ||
| ); | ||
| } | ||
|
|
||
| // Helper to handle rollback/reset logic | ||
| async function handleRollbackOrReset( | ||
| reason: string | ||
| ): Promise<CountingProcessResult> { | ||
| if (data.currentCount > 100) { | ||
| const mag = Math.pow(10, Math.floor(Math.log10(data.currentCount))); | ||
| const rollbackTo = Math.floor(data.currentCount / mag) * mag; | ||
| const rollbackStep = 100; | ||
| const rollbackTo = | ||
| Math.floor(data.currentCount / rollbackStep) * rollbackStep; | ||
| await setCount(rollbackTo); | ||
|
|
||
| logger.debug('[CountingManager] Wrong number caused rollback', { | ||
| logger.debug('[CountingManager] Invalid number caused rollback', { | ||
| user: message.author.id.slice(-4), | ||
| expected, | ||
| actual: count, | ||
| reason, | ||
| previousCount: data.currentCount, | ||
| rolledBackTo: rollbackTo, | ||
| reason, | ||
| }); | ||
|
|
||
| return { | ||
| isValid: false, | ||
| expectedCount: expected, | ||
| reason, | ||
| reason: reason as CountingProcessInvalidReason, | ||
| rolledBackTo: rollbackTo, | ||
| }; | ||
| } else { | ||
| await resetCounting(); | ||
| } | ||
|
|
||
| logger.debug('[CountingManager] Wrong number caused reset', { | ||
| user: message.author.id.slice(-4), | ||
| expected, | ||
| actual: count, | ||
| reason, | ||
| previousCount: data.currentCount, | ||
| }); | ||
| await resetCounting(); | ||
|
|
||
| return { | ||
| isValid: false, | ||
| expectedCount: expected, | ||
| reason, | ||
| rolledBackTo: 0, | ||
| }; | ||
| } | ||
| 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, | ||
| rolledBackTo: 0, | ||
| }; | ||
| } |
There was a problem hiding this comment.
Critical: TDZ crash when sanitizeAndEval throws.
handleRollbackOrReset('not_a_number') is invoked at Line 215 inside the catch block, but the const expected it closes over is not initialized until Line 219. Although the function declaration itself is hoisted, expected is a block-scoped const that remains in the temporal dead zone when the helper executes; the first access inside the helper (expectedCount: expected on Line 240) will throw ReferenceError: Cannot access 'expected' before initialization.
This path is reachable any time a numeric-looking message survives numericLikeRegex but fails arithmetic parsing (e.g. 1+, (, 3**). The outer try/catch at Line 334 will swallow it and return { isValid: false, reason: 'ignored' }, so rollback/reset and mistake tracking are effectively broken for these inputs.
Hoist expected (and any other captured state the helper needs) above the inner try/catch, or pass it in as a parameter:
Suggested fix
+ const expected = data.currentCount + 1;
+
let evaluated: number | null;
try {
evaluated = sanitizeAndEval(trimmed);
} catch (err) {
...
return await handleRollbackOrReset('not_a_number');
}
const count = evaluated;
- const expected = data.currentCount + 1;Alternatively, convert handleRollbackOrReset into a const arrow and pass expected explicitly so the dependency is visible.
🤖 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 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.
| // 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, | ||
| }); |
There was a problem hiding this comment.
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.
| if (v instanceof Error) { | ||
| Object.assign(out, extractErrorAttributes(v)); | ||
| continue; | ||
| } | ||
| if (k === 'name' && typeof v === 'string') { | ||
| out['exception.type'] = v; | ||
|
|
||
| const commonField = extractCommonErrorField(k, v, v instanceof Error); | ||
| if (commonField) { | ||
| Object.assign(out, commonField); | ||
| continue; | ||
| } |
There was a problem hiding this comment.
name → exception.type mapping is now dead code.
The Error branch on line 134 handles v instanceof Error and continues, so by the time extractCommonErrorField(k, v, v instanceof Error) runs on line 139, v can never be an Error and isError is always false. That makes the name branch in extractCommonErrorField (lines 96–101) unreachable, so Winston-flattened error records (where name arrives as a top-level string alongside a stack string — a common Winston pattern) no longer emit exception.type. Previously-documented behavior of extractCommonErrorField is silently dropped.
Either derive isError from sibling metadata (e.g., presence of an error-shaped stack) or drop the isError gate and always map a string name when combined with an error-looking stack in the same meta object.
🔧 One possible fix: compute `isError` from sibling meta once per record
const out: Record<string, unknown> = {};
- for (const [k, v] of Object.entries(meta || {})) {
+ const metaEntries = Object.entries(meta || {});
+ const stackVal = meta?.stack;
+ const hasErrorShapedStack =
+ typeof stackVal === 'string' &&
+ (stackVal.includes('\n') || stackVal.includes(' at '));
+ for (const [k, v] of metaEntries) {
if (isNumericKey(k)) {
continue;
}
if (v instanceof Error) {
Object.assign(out, extractErrorAttributes(v));
continue;
}
- const commonField = extractCommonErrorField(k, v, v instanceof Error);
+ const commonField = extractCommonErrorField(k, v, hasErrorShapedStack);
if (commonField) {
Object.assign(out, commonField);
continue;
}
out[k] = safeSerialize(v);
}🤖 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 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.
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
What changed
Migration to Ultracite/Biome for Linting and Formatting:
.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:
.agents/skills/ultracite/SKILL.md,.agents/skills/ultracite/references/code-standards.md, [1] [2].github/copilot-instructions.md, .github/copilot-instructions.mdL140-R266)Developer Tooling and Editor Integration:
.devcontainer/devcontainer.json, .devcontainer/devcontainer.jsonL21-R75)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:
.github/workflows/eslint.yml, .github/workflows/eslint.ymlL1-L47).github/copilot-instructions.md, .github/copilot-instructions.mdL140-R266)Project Scripts and PR Templates:
yarn check,yarn fix, andyarn type-checkin place of previousyarn lintandyarn format:fixcommands. (.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?
yarn dev/yarn no-deployyarn lintyarn format:fixyarn compileTest notes:
Database or cache changes
drizzle-kitbot:); includes graceful degradationDetails:
Breaking changes
Migration notes:
Security and privacy
Notes:
Checklist
CONTRIBUTING.mdfeat(bot): ...)username/feature-name)deferReply()usage where neededAdditional context for reviewers
Summary by cubic
Migrated code quality to
ultraciteusing Biome, replacing ESLint/Prettier. Updated scripts, CI, devcontainer, docs, and pre-commit hooks; refactored codebase to meet new rules and added structured logging handlers.Dependencies
ultracite(Biome) and addedbiome.jsonc.yarn check,yarn fix,yarn type-check; removed old lint/format scripts.Ultraciteworkflow; removedeslint.yml.yarn ultracite fixand type-check on staged files.Refactors
Migration
yarn install.yarn checkfor lint/format checks,yarn fixto auto-fix, andyarn type-checkfor types.lint-stagedto auto-fix staged files and type-check TS files.Written for commit 092a355. Summary will update on new commits.