Skip to content

Comments

Refactor bounty logic and data structures to align with backend schema#112

Merged
0xdevcollins merged 4 commits intoboundlessfi:mainfrom
Ekene001:feat/Align-frontend-GraphQL-schema-with-backend
Feb 23, 2026
Merged

Refactor bounty logic and data structures to align with backend schema#112
0xdevcollins merged 4 commits intoboundlessfi:mainfrom
Ekene001:feat/Align-frontend-GraphQL-schema-with-backend

Conversation

@Ekene001
Copy link
Contributor

@Ekene001 Ekene001 commented Feb 23, 2026

Closes #98


Synchronizes the entire frontend codebase with the backend's auto-generated GraphQL schema, ensuring type consistency across 33+ files spanning types, UI components, API routes, pages, mock data, and configuration.

The frontend schema had drifted significantly from the backend — using outdated enum values, referencing fields that don't exist in the backend, and employing different naming conventions. This caused potential runtime mismatches and made it impossible to run codegen cleanly.

Changes

Enum Alignment

Concept Before After
BountyType feature, bug, documentation, refactor FIXED_PRICE, MILESTONE_BASED, COMPETITION
BountyStatus open, claimed, closed OPEN, IN_PROGRESS, COMPLETED, CANCELLED, DRAFT, SUBMITTED, UNDER_REVIEW, DISPUTED

Removed Frontend-Only Fields

  • claimingModel, difficulty, tags, scope, requirements
  • claimedBy, claimedAt, claimExpiresAt
  • issueTitle, projectName, projectLogoUrl, githubRepo

Renamed Fields

  • rewardrewardAmount
  • currencyrewardCurrency

Added

  • organization / project nested objects (matching backend relations)
  • TYPE_CONFIG in lib/bounty-config.ts for the 3 bounty types
  • claim() method on bountiesApi

Files Changed

Schema & Codegen (2 files)
  • lib/graphql/schema.graphql — replaced with backend schema
  • codegen.ts — updated documents glob to lib/graphql/operations/
Type Definitions (4 files)
  • types/bounty.ts — fully rewritten for backend alignment
  • lib/types.ts — updated BountyStatus type and Bounty interface
  • lib/api/bounties.ts — Zod schemas aligned + claim method added
  • lib/bounty-config.tsSTATUS_CONFIG (8 statuses), TYPE_CONFIG (3 types); removed DIFFICULTY_CONFIG, CLAIMING_MODEL_CONFIG
UI Components (13 files)
  • components/bounty/bounty-header.tsx
  • components/bounty/github-bounty-card.tsx
  • components/bounty/bounty-card.tsx
  • components/bounty/bounty-sidebar.tsx
  • components/bounty/bounty-content.tsx
  • components/cards/bounty-card.tsx
  • components/projects/project-bounties.tsx
  • components/bounty-detail/bounty-badges.tsx
  • components/bounty-detail/bounty-detail-client.tsx
  • components/bounty-detail/bounty-detail-header-card.tsx
  • components/bounty-detail/bounty-detail-sidebar-cta.tsx
  • components/bounty-detail/bounty-detail-requirements-card.tsx
  • components/search-command.tsx
Pages & API Routes (8 files)
  • app/bounty/page.tsx — filters updated, difficulty/tags filters removed
  • app/discover/page.tsx — search/sort updated
  • app/profile/[userId]/page.tsx — removed claimedBy/issueTitle references
  • app/api/bounties/route.ts — removed difficulty/tags filters
  • app/api/bounties/[id]/claim/route.ts
  • app/api/bounties/[id]/submit/route.ts
  • app/api/bounties/[id]/join/route.ts
  • app/api/bounties/[id]/competition/join/route.ts
  • app/api/bounties/[id]/milestones/advance/route.ts
Logic & Mock Data (3 files)
  • lib/logic/bounty-logic.ts — status comparisons updated
  • lib/mock-bounty.ts — mock data aligned with backend fields
  • lib/mock-data.ts — mock data aligned with backend fields

Breaking Changes

⚠️ Any consumer of Bounty types will need to update field references:

  • Status/type enums are now UPPER_CASE
  • rewardrewardAmount, currencyrewardCurrency
  • projectNameorganization.name, projectLogoUrlorganization.logo
  • issueTitletitle
  • difficulty, tags, scope, requirements, claimingModel no longer exist

Summary by CodeRabbit

  • New Features

    • Organization-based bounty filtering and organization context shown across listings and details.
    • Expanded bounty statuses (OPEN, IN_PROGRESS, COMPLETED, CANCELLED, DRAFT, SUBMITTED, UNDER_REVIEW, DISPUTED).
  • Bug Fixes & Improvements

    • Unified type/status presentation and clearer status labels site‑wide.
    • CTA simplified: main action opens the GitHub issue; mobile/desktop flows aligned.
  • Changes

    • Search and listing now use title/description; difficulty and tag filters removed.
  • Removed

    • In‑app submission dialog and the legacy completion‑history endpoint.

- Updated BountyLogic class to simplify status processing and align with backend-driven status model.
- Modified mock bounty data to reflect new structure and status values.
- Adjusted mock data to include organization and project details.
- Aligned Bounty and related types with backend GraphQL schema, including new status values and organization/project relationships.
- Removed deprecated fields and streamlined submission interface for clarity.
@vercel
Copy link

vercel bot commented Feb 23, 2026

@Ekene001 is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Feb 23, 2026

Important

Review skipped

Review was skipped due to path filters

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json

CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including **/dist/** will override the default block on the dist directory, by removing the pattern from both the lists.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Aligns frontend bounty model and flows with backend schema: renames/enumerates BountyType/BountyStatus, restructures Bounty shape (organization/project, rewardAmount/currency, github fields), rewires API routes and GraphQL/codegen, removes client-side submission dialog, and updates many UI components and mock data.

Changes

Cohort / File(s) Summary
GraphQL & Codegen
lib/graphql/schema.graphql, lib/graphql/generated.ts, lib/graphql/client.ts, codegen.ts
Replaced schema with backend-aligned types (BountyType/BountyStatus, organization/project, submissions); fetcher now returns a thunk; narrowed codegen documents to lib/graphql/operations/**.
Core Types & API surfaces
types/bounty.ts, lib/types.ts, types/participation.ts, lib/api/bounties.ts, lib/api/reputation.ts, lib/api/transparency.ts
Bounty model reworked (title, organization/project, rewardAmount/rewardCurrency, github fields); new enums and input types; bountiesApi.claim now accepts optional contributorId; fetchCompletionHistory signature/path changed; submit removed.
API Routes (participation & management)
app/api/bounties/[id]/claim/route.ts, app/api/bounties/[id]/join/route.ts, app/api/bounties/[id]/competition/join/route.ts, app/api/bounties/[id]/milestones/advance/route.ts, app/api/bounties/[id]/submit/route.ts, app/api/bounties/route.ts, app/api/reputation/[userId]/completion-history/route.ts, app/api/reputation/[userId]/history/route.ts
Handlers updated to validate bounty.type and uppercase bounty.status; auth enforced on several endpoints; submission endpoint loosened to allow OPEN/IN_PROGRESS; old completion-history route removed and new history route added.
Frontend components — bounty detail & submission
components/bounty-detail/submission-dialog.tsx, components/bounty-detail/bounty-detail-sidebar-cta.tsx, components/bounty-detail/...
Removed SubmissionDialog; CTA simplified to open GitHub issue; claim-model UI removed; replaced DifficultyBadge with TypeBadge; trimmed Requirements/Scope rendering and related exports.
Frontend components — cards, header, sidebar
components/bounty/bounty-card.tsx, components/github-bounty-card.tsx, components/cards/bounty-card.tsx, components/bounty/bounty-header.tsx, components/bounty/bounty-content.tsx, components/bounty-detail/bounty-badges.tsx, components/bounty-detail/bounty-detail-client.tsx, components/bounty-detail/bounty-detail-header-card.tsx, components/bounty-detail/bounty-detail-requirements-card.tsx, components/bounty-detail/bounty-detail-sidebar-cta.tsx, components/bounty/bounty-sidebar.tsx
UI shifted to type/status-driven configs, use title and organization data, display rewardAmount/currency, remove tag/difficulty sections, and refactor sidebar to new rating/claim flows.
Pages & filters
app/bounty/page.tsx, app/discover/page.tsx, app/profile/[userId]/page.tsx, components/projects/project-bounties.tsx
Replaced difficulty/tags/project filters with organization-based filtering; updated option shapes to use enums/objects; search targets title/description; profile completion history usage adjusted.
Config, logic & mock data
lib/bounty-config.ts, lib/logic/bounty-logic.ts, lib/mock-bounty.ts, lib/mock-data.ts
Replaced DIFFICULTY/CLAIMING_MODEL with TYPE_CONFIG and uppercase STATUS_CONFIG; simplified processBountyStatus; updated mock bounties to new schema and statuses.
Reputation & services
lib/services/reputation.ts, app/api/reputation/[userId]/history/route.ts, lib/api/reputation.ts
Added ReputationService methods (tier, history, rate, link wallet), new history API route, and updated client wrapper for history endpoint.
Forms & schemas / types
components/bounty/forms/schemas.ts, components/bounty/forms/*, types/participation.ts
Removed submissionFormSchema and SubmissionFormValue; consolidated submission shape to a single content field.
GraphQL resolvers & generated
lib/graphql/resolvers.ts, lib/graphql/generated.ts
Added resolver guarding BountySubmissionUser.email visibility; generated types updated to match new schema (organization/project, submissions, new inputs/mutations).
Build / CI / tooling
scripts/sync-schema.js, .github/workflows/check-schema.yml, codegen.ts, package.json, README.md
Added schema sync script and CI workflow; codegen narrowed; package.json scripts/devDependency added; README docs for schema sync added (duplicated block present).
Minor / formatting
app/transparency/*, components/global-navbar.tsx, components/login/sign-in.tsx, tests...
Mostly formatting/quote standardization, small UI tweaks (Image component), and test formatting changes.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • Benjtalkshow

Poem

"I hopped through code with tidy paws,
Replaced old claims with clearer laws,
FIXED_PRICE, MILESTONE, COMPETITION gleam,
OPEN to COMPLETED—the enums now beam,
A carrot-cheer for schema dreams!" 🥕🐇

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.71% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the main change: refactoring bounty logic and data structures to align with backend schema.
Linked Issues check ✅ Passed The PR comprehensively addresses all coding requirements from issue #98: schema replacement, codegen config update, Zod schema alignment, enum updates, and UI component refactoring across 33+ files.
Out of Scope Changes check ✅ Passed All changes are directly related to aligning frontend with backend schema per issue #98. No unrelated refactoring, feature additions, or scope creep detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 20

Note

Due to the large number of review comments, Critical, Major 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 (7)
app/profile/[userId]/page.tsx (1)

57-80: ⚠️ Potential issue | 🟠 Major

createdBy filter shows bounties the user created, not bounties they claimed.

The tab is labelled "My Claims" and uses the MyClaim type, but the filter bounty.createdBy === userId selects bounties authored by this user. With claimedBy removed from the schema, there's no claim relationship to filter on—but the current behaviour is semantically wrong and will confuse users who expect to see bounties they're working on.

If the backend doesn't yet expose a claim relationship, consider either:

  1. Renaming the tab/section to "My Bounties" or "Created Bounties" to match the actual data, or
  2. Hiding the tab until a claim query is available.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 57 - 80, The "My Claims" tab
currently filters by bounty.createdBy === userId (in the myClaims useMemo that
returns MyClaim[] from bountyResponse), which shows created bounties not claimed
ones; either (A) rename the UI and types to reflect created bounties—e.g.,
change the tab label from "My Claims" to "My Bounties"/"Created Bounties",
rename myClaims/MyClaim to myBounties/MyBounty and keep the existing filter, or
(B) hide/disable the tab until a backend claim relationship exists and a
claimedBy filter can be implemented; update any references to myClaims, MyClaim,
and the tab label accordingly.
types/participation.ts (1)

16-28: ⚠️ Potential issue | 🟡 Minor

Align SubmissionStatus and ApplicationStatus casing with BountyStatus and BountyType.

BountyStatus and BountyType use UPPER_CASE enums ("OPEN", "IN_PROGRESS", "FIXED_PRICE"), but SubmissionStatus (line 16) and ApplicationStatus (line 1) use lowercase string unions ("pending", "accepted", "rejected"). The codebase shows no evidence that this difference is intentional. These should be standardized to match the UPPER_CASE pattern for consistency.

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

In `@types/participation.ts` around lines 16 - 28, Update the status string unions
to use UPPER_CASE to match the rest of the codebase: change SubmissionStatus
from "pending"|"accepted"|"rejected" to "PENDING"|"ACCEPTED"|"REJECTED" and
likewise update ApplicationStatus to its UPPER_CASE variants; then update any
related usages of Submission.status and Application.status (and any
parsing/serialization points) to use the new constants/strings so the Submission
interface and Application handling remain consistent with BountyStatus and
BountyType.
components/bounty-detail/bounty-detail-requirements-card.tsx (1)

1-23: ⚠️ Potential issue | 🟠 Major

Remove bounty-detail-requirements-card.tsx — both components are dead code.

Neither RequirementsCard nor ScopeCard are imported or used anywhere in the codebase. Since the backend no longer includes requirements/scope on the Bounty model and bounty-detail-client.tsx no longer mounts these components, the file serves no purpose and should be deleted to avoid confusion.

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

In `@components/bounty-detail/bounty-detail-requirements-card.tsx` around lines 1
- 23, This file contains two unused components (RequirementsCard and ScopeCard)
that are dead code; delete the file that defines RequirementsCard and ScopeCard
and remove any exports or imports that reference them elsewhere, then run the
repo-wide linter/TS build to ensure there are no remaining references and clean
up any leftover types/tests that mention these component names.
components/bounty-detail/bounty-badges.tsx (1)

4-14: ⚠️ Potential issue | 🟡 Minor

No runtime guard against unknown status/type values.

Both StatusBadge and TypeBadge directly dereference cfg.className without checking that the config lookup returned a value. TypeScript ensures exhaustiveness at compile time, but if the backend emits a new status or type that hasn't been added to STATUS_CONFIG/TYPE_CONFIG yet, the component will throw at render time.

🛡️ Suggested defensive guard
 export function StatusBadge({ status }: { status: BountyStatus }) {
-  const cfg = STATUS_CONFIG[status];
+  const cfg = STATUS_CONFIG[status] ?? STATUS_CONFIG["OPEN"];
   return (
 export function TypeBadge({ type }: { type: BountyType }) {
-  const cfg = TYPE_CONFIG[type];
+  const cfg = TYPE_CONFIG[type] ?? TYPE_CONFIG["FIXED_PRICE"];

Also applies to: 16-24

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

In `@components/bounty-detail/bounty-badges.tsx` around lines 4 - 14, StatusBadge
(and similarly TypeBadge) currently dereferences STATUS_CONFIG[status] (and
TYPE_CONFIG[type]) without a runtime check; add a defensive guard in the
component to handle unknown values by checking if cfg is truthy and if not, log
or report the unexpected status/type and use a safe fallback object (default
className, dot, and label) before rendering so the component never accesses
properties on undefined; update the rendering in StatusBadge and TypeBadge to
use the guarded cfg variable (or the fallback) to avoid runtime exceptions when
the backend returns new/unknown enums.
app/discover/page.tsx (1)

57-62: ⚠️ Potential issue | 🟡 Minor

Tag filter silently has no effect on the bounty tab.

filters.tags is still persisted to state (Line 59) and synced to the URL (Line 87), but Lines 150–157 no longer include a tag-based filter path for bounties. A user who selects tag filters on the bounty tab will see no results change, with no feedback that tags are ignored. Either remove tags from FilterState for the bounty context, or suppress the tag-filter UI when the bounty tab is active.

Also applies to: 87-87, 150-157

lib/graphql/generated.ts (1)

19-41: ⚠️ Potential issue | 🟠 Major

Bounty.status and Bounty.type are String instead of their respective enums — loss of type safety.

The generated types reflect the schema where status: String! and type: String! instead of status: BountyStatus! and type: BountyType!. This means GraphQL won't enforce valid enum values at the schema level. The enums BountyStatus and BountyType exist in the schema but aren't used on the Bounty type. This issue originates in schema.graphql — see comment there.

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

In `@lib/graphql/generated.ts` around lines 19 - 41, The Bounty GraphQL type
currently has fields Bounty.status and Bounty.type typed as String instead of
the schema enums BountyStatus and BountyType; fix this by updating
schema.graphql so the Bounty type declares status: BountyStatus! and type:
BountyType! (replace the String annotations), then regenerate the TypeScript
types (run your GraphQL codegen) so the generated file (generated.ts) reflects
enums for Bounty.status and Bounty.type rather than plain strings.
components/bounty-detail/bounty-detail-sidebar-cta.tsx (1)

77-87: ⚠️ Potential issue | 🔴 Critical

SidebarCTA opens GitHub instead of claiming bounty — inconsistent with BountySidebar behavior.

SidebarCTA on the bounty detail page (line 83) opens the GitHub issue URL in a new tab when "Submit to Bounty" is clicked. BountySidebar, used elsewhere, POSTs to /api/bounties/{bounty.id}/claim for the same button label. Both show "Submit to Bounty" when the bounty is OPEN, but perform fundamentally different actions. Users will expect clicking "Submit to Bounty" to claim the bounty, not navigate to GitHub. Either unify the action or change the SidebarCTA label to reflect that it opens GitHub.

Additionally, SidebarCTA.ctaLabel() (lines 25-39) and MobileCTA.label() (lines 131-143) lack explicit cases for DRAFT, SUBMITTED, UNDER_REVIEW, and DISPUTED statuses, falling back to "Not Available"—whereas BountySidebar.renderActionButton() explicitly handles all seven non-OPEN statuses (lines 54-65).

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

In `@components/bounty-detail/bounty-detail-sidebar-cta.tsx` around lines 77 - 87,
The SidebarCTA component currently opens bounty.githubIssueUrl in the Button
onClick even when ctaLabel() returns "Submit to Bounty", which is inconsistent
with BountySidebar.renderActionButton() that POSTs to
/api/bounties/{bounty.id}/claim; change SidebarCTA's Button onClick to call the
same claim flow (POST /api/bounties/{bounty.id}/claim) when ctaLabel() or
MobileCTA.label() would return "Submit to Bounty", and only open the GitHub URL
when the label explicitly indicates an external view action; also expand
SidebarCTA.ctaLabel() and MobileCTA.label() to include explicit cases for DRAFT,
SUBMITTED, UNDER_REVIEW, and DISPUTED to match the seven-status handling in
BountySidebar.renderActionButton() so labels and actions are aligned across
components.
🟡 Minor comments (17)
components/reputation/my-claims.tsx-7-13 (1)

7-13: ⚠️ Potential issue | 🟡 Minor

MyClaim.status is untyped and rewardCurrency is absent from the local type.

Two separate concerns:

  1. Loose status type — using string instead of the BountyStatus enum means invalid values can be passed in silently and normalizeStatus/section-matching bugs (see above) are not caught at compile time.

  2. Missing rewardCurrency — the PR renames currencyrewardCurrency project-wide, but MyClaim only carries rewardAmount. This forces the downstream rendering to hardcode "$" (line 79), which breaks multi-currency support.

🛡️ Proposed fix
+import { BountyStatus } from "@/types/bounty"; // adjust import path as needed
+
 export type MyClaim = {
     bountyId: string;
     title: string;
-    status: string;
+    status: BountyStatus;
     nextMilestone?: string;
     rewardAmount?: number;
+    rewardCurrency?: string;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/reputation/my-claims.tsx` around lines 7 - 13, Update the MyClaim
type so status is strongly typed to the existing BountyStatus enum and add an
optional rewardCurrency field (e.g., rewardCurrency?: string or the project's
Currency type) alongside rewardAmount; this ensures callers must supply a valid
BountyStatus (so normalizeStatus/section-matching errors are caught at compile
time) and lets rendering use claim.rewardCurrency instead of hardcoding "$".
Locate the MyClaim type definition and change status: string → status:
BountyStatus and add rewardCurrency?: <appropriate currency type>.
components/reputation/my-claims.tsx-77-80 (1)

77-80: ⚠️ Potential issue | 🟡 Minor

Hardcoded "$" currency — downstream effect of rewardCurrency missing from MyClaim.

Once rewardCurrency is added to the MyClaim type (see comment above), update this call site to use the actual value so non-USD bounties render correctly.

🐛 Proposed fix
-                                                            {formatCurrency(claim.rewardAmount, "$")}
+                                                            {formatCurrency(claim.rewardAmount, claim.rewardCurrency ?? "$")}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/reputation/my-claims.tsx` around lines 77 - 80, The call site in
components/reputation/my-claims.tsx is hardcoding USD by passing "$" to
formatCurrency; update the JSX to pass claim.rewardCurrency (from the updated
MyClaim type) instead of the string literal, e.g.,
formatCurrency(claim.rewardAmount, claim.rewardCurrency); ensure you handle a
missing rewardCurrency by falling back to a sensible default (e.g., "$") so
formatCurrency(claim.rewardAmount, claim.rewardCurrency ?? "$") remains safe.
components/login/sign-in.tsx-149-149 (1)

149-149: ⚠️ Potential issue | 🟡 Minor

Non-descriptive alt text is an accessibility issue.

alt="Image" is meaningless to screen readers. If this panel is decorative, use alt="" so assistive technology ignores it; if it eventually carries meaning (e.g. brand illustration), use a descriptive string.

♿ Proposed fix for decorative image
-  alt="Image"
+  alt=""
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/login/sign-in.tsx` at line 149, Replace the non-descriptive
alt="Image" on the <img> in components/login/sign-in.tsx with an appropriate
value: if the image is purely decorative, set alt="" so assistive tech ignores
it; if it conveys meaning (e.g., brand or explanatory illustration), replace
with a concise descriptive string such as alt="Company logo" or
alt="Illustration of sign-in process". Locate the <img> element in the SignIn
component (or top-level render) and update the alt attribute accordingly.
app/profile/[userId]/page.tsx-26-55 (1)

26-55: ⚠️ Potential issue | 🟡 Minor

Mock history entries all share the same completedAt timestamp, producing identical relative-time labels in the UI.

The CompletionHistory component calls formatDistanceToNow(new Date(record.completedAt)) for every row. Because every mock record has completedAt: "2024-01-15T12:00:00Z", users will see the exact same "X years ago" label repeated for all entries, which looks broken rather than placeholder-ish.

At minimum, spread the timestamps so rows look distinct:

Proposed fix
       .map((_, i) => ({
         id: `bounty-${i}`,
         bountyId: `b-${i}`,
         bountyTitle: `Implemented feature #${100 + i}`,
         projectName: "Drips Protocol",
         projectLogoUrl: null,
         difficulty: ["BEGINNER", "INTERMEDIATE", "ADVANCED"][i % 3] as
           | "BEGINNER"
           | "INTERMEDIATE"
           | "ADVANCED",
         rewardAmount: 500,
         rewardCurrency: "USDC",
         claimedAt: "2023-01-01T00:00:00Z",
-        completedAt: "2024-01-15T12:00:00Z",
+        completedAt: new Date(
+          Date.now() - (i + 1) * 24 * 60 * 60 * 1000
+        ).toISOString(),
         completionTimeHours: 48,
         maintainerRating: 5,
         maintainerFeedback: "Great work!",
         pointsEarned: 150,
       }));

Also, consider moving MAX_MOCK_HISTORY outside the component since it's a module-level constant.

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

In `@app/profile/`[userId]/page.tsx around lines 26 - 55, Mock history entries all
use the same completedAt timestamp, causing identical relative-time labels;
update the useMemo that builds mockHistory to assign distinct timestamps (e.g.,
subtract i hours/days from Date.now() or compute new Date(Date.now() - i * X))
for the completedAt field so each record shows a different relative time, and
move the MAX_MOCK_HISTORY constant to module scope (outside the component) to
make it truly module-level; modify the mockHistory generator (inside the useMemo
that depends on reputation) to compute completedAt per item using the index i.
lib/api/reputation.ts-14-16 (1)

14-16: ⚠️ Potential issue | 🟡 Minor

Unvalidated string interpolated into URL path may allow path traversal.

address is typed as string with no format enforcement. A value like ../../other-route would silently redirect the request to an unintended endpoint, since most HTTP clients resolve ../ segments in the path. Wallet addresses are safe in practice, but the type system doesn't enforce this.

Consider adding a lightweight guard before constructing the URL:

🛡️ Proposed fix
 fetchContributorByWallet: async (address: string): Promise<ContributorReputation> => {
+    if (!/^0x[0-9a-fA-F]{40}$/.test(address)) {
+        throw new Error(`Invalid wallet address: ${address}`);
+    }
     return get<ContributorReputation>(`${REPUTATION_ENDPOINT}/wallet/${address}`);
 },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/reputation.ts` around lines 14 - 16, The fetchContributorByWallet
function interpolates an unvalidated address into the URL and may allow path
traversal; before calling get(`${REPUTATION_ENDPOINT}/wallet/${address}`)
validate or sanitize address (e.g., enforce expected wallet format via a regex
or use encoding) to reject or normalize values containing "../", slashes, or
unexpected characters; update fetchContributorByWallet to perform this guard (or
encodeURIComponent on address) and throw or return an error for invalid
addresses so only safe, expected wallet strings are appended to
REPUTATION_ENDPOINT.
app/transparency/page.tsx-94-156 (1)

94-156: ⚠️ Potential issue | 🟡 Minor

Stats cards show "$0" fallback values alongside the error alert.

When statsError is true, stats is undefined, so the stat cards render with fallback values ("$0", "0", "0 days") while the error alert is also visible above. This gives a contradictory signal — the user sees both an error message and seemingly valid zero data.

Consider either hiding the stats grid when there's an error, or showing skeletons/placeholders instead of zero values when stats is undefined.

Suggested approach
                {/* Stats Grid */}
-                <section>
+                {!statsError && <section>
                    <h2 className="text-xl font-semibold text-foreground mb-4">
                        Platform Overview
                    </h2>
                    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
                        {statCards.map((card) => (
                            <StatCard
                                key={card.title}
                                title={card.title}
                                value={card.value}
                                icon={card.icon}
                                isLoading={statsLoading}
                            />
                        ))}
                    </div>
-                </section>
+                </section>}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/transparency/page.tsx` around lines 94 - 156, The stat cards currently
render fallback zero values when stats is undefined while statsError is true,
causing contradictory UI; update the rendering logic so the stats grid (the
statCards mapping that uses statCards) is only rendered when stats is defined
and statsError is false (or alternatively render skeletons/placeholders when
stats is undefined and not render zeros), and ensure statCards is built or
accessed only when stats exists (referencing statCards, stats, and statsError)
so the error alert displays alone and the zero-value cards are not shown; keep
the Try Again button and refetchStats behavior unchanged.
types/bounty.ts-40-45 (1)

40-45: ⚠️ Potential issue | 🟡 Minor

PII exposure: BountySubmissionUser.email field.

The email field on BountySubmissionUser carries user PII. Ensure this field isn't rendered in UI components or logged without appropriate protections (masking, access control). If the frontend doesn't need the email for display or functionality, consider omitting it from the type and the GraphQL query to minimize PII surface.

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

In `@types/bounty.ts` around lines 40 - 45, The BountySubmissionUser.email
property exposes PII; either remove it from the public type or restrict its use:
update the BountySubmissionUser interface by deleting the email field (or
replace it with a separate internal type like BountySubmissionUserPrivate that
includes email), change any GraphQL queries/mutations to stop requesting email
for UI/public endpoints, and update UI components and logging to never render or
log the email (if email is needed server-side keep it only in server/internal
types and add masking/access-control where used). Ensure references to
BountySubmissionUser.email in code are removed or swapped to the
private/internal type and add a comment indicating why email was excluded.
types/bounty.ts-17-17 (1)

17-17: ⚠️ Potential issue | 🟡 Minor

Remove unused DifficultyLevel type definition.

The type is not referenced anywhere in the codebase. Since difficulty has been removed from the Bounty model, this type definition should also be removed.

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

In `@types/bounty.ts` at line 17, Remove the unused type alias DifficultyLevel
from types/bounty.ts; it is no longer referenced (difficulty was removed from
the Bounty model), so delete the export "export type DifficultyLevel =
'beginner' | 'intermediate' | 'advanced';" and ensure no other modules import or
reference DifficultyLevel before committing.
components/bounty/forms/schemas.ts-54-60 (1)

54-60: ⚠️ Potential issue | 🟡 Minor

Floating-point comparison total === 100 may fail for fractional percentages.

If a user enters fractional percentages (e.g., 33.33 + 33.33 + 33.34), floating-point arithmetic can produce a result like 99.99999999999999 instead of 100, causing the refinement to reject valid input. If only integer percentages are expected, consider adding .int() to the percentage field in milestoneSchema. Otherwise, use a tolerance-based comparison.

Proposed fix (tolerance-based)
  .refine(
    (milestones) => {
      const total = milestones.reduce((sum, m) => sum + m.percentage, 0)
-     return total === 100
+     return Math.abs(total - 100) < 0.01
    },
    { message: 'Milestone percentages must total 100%' }
  )

Or enforce integers at the field level:

  percentage: z
    .number({ ... })
+   .int('Percentage must be a whole number')
    .min(1, 'Percentage must be at least 1%')
    .max(100, 'Percentage cannot exceed 100%'),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/forms/schemas.ts` around lines 54 - 60, The refinement
currently checks total === 100 which can fail due to floating-point precision;
update the check in the .refine callback to use a tolerance-based comparison
(e.g., Math.abs(total - 100) < 1e-6) to accept near-equal sums, or if only
integer percentages are allowed, enforce integers by adding .int() to the
percentage field in milestoneSchema; modify the reduce-based total comparison in
the refine call accordingly and ensure any error message still reads 'Milestone
percentages must total 100%'.
components/bounty/bounty-card.tsx-78-78 (1)

78-78: ⚠️ Potential issue | 🟡 Minor

statusConfig.COMPLETED is a misleading fallback for unknown statuses.

An unknown status (e.g., a new backend status not yet handled on the frontend) will render the card as "Completed" with a gray dot — indistinguishable from a genuinely completed bounty. A neutral fallback is less deceptive.

-  const status = statusConfig[bounty.status] || statusConfig.COMPLETED;
+  const status = statusConfig[bounty.status] ?? {
+    variant: "outline" as const,
+    label: bounty.status,
+    dotColor: "bg-gray-400",
+  };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/bounty-card.tsx` at line 78, The current line sets status =
statusConfig[bounty.status] || statusConfig.COMPLETED which incorrectly treats
unknown backend statuses as "COMPLETED"; change the fallback to a neutral entry
(e.g., statusConfig.UNKNOWN or statusConfig.DEFAULT) and ensure that neutral key
exists in statusConfig so unknown bounty.status values render a non-deceptive
neutral state; update any rendering logic that consumes the status variable to
handle the neutral entry appropriately (references: statusConfig, bounty.status,
status).
components/bounty-detail/bounty-detail-header-card.tsx-32-34 (1)

32-34: ⚠️ Potential issue | 🟡 Minor

Issue number 0 would be suppressed by the && short-circuit.

bounty.githubIssueNumber && ... evaluates to 0 (falsy) when the issue number is 0, so nothing renders. While rare, a strict check is safer:

Proposed fix
-          {bounty.githubIssueNumber && `#${bounty.githubIssueNumber}`}
+          {bounty.githubIssueNumber != null && `#${bounty.githubIssueNumber}`}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty-detail/bounty-detail-header-card.tsx` around lines 32 - 34,
The conditional rendering suppresses a valid issue number 0 because it uses a
falsy check; change the check in the JSX around bounty.githubIssueNumber (the
fragment that currently uses "bounty.githubIssueNumber &&
`#${bounty.githubIssueNumber}`") to explicitly test for null/undefined (e.g.,
"bounty.githubIssueNumber != null" or "typeof bounty.githubIssueNumber !==
'undefined'") so that 0 renders correctly, keeping the ExternalLink and
surrounding markup intact.
app/api/bounties/[id]/milestones/advance/route.ts-87-92 (1)

87-92: ⚠️ Potential issue | 🟡 Minor

Unchecked null return from updateMilestoneParticipation.

BountyStore.updateMilestoneParticipation returns null when the participation ID isn't found. The result is passed directly into the success response without a null check.

Proposed fix
     const updated = BountyStore.updateMilestoneParticipation(
       participation.id,
       updates,
     );
 
-    return NextResponse.json({ success: true, data: updated });
+    if (!updated) {
+      return NextResponse.json(
+        { error: "Failed to update participation" },
+        { status: 500 },
+      );
+    }
+
+    return NextResponse.json({ success: true, data: updated });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/milestones/advance/route.ts around lines 87 - 92,
BountyStore.updateMilestoneParticipation can return null; after calling
updateMilestoneParticipation(participation.id, updates) in route.ts, check if
updated is null and if so return an error response (e.g., NextResponse.json({
success: false, error: 'Participation not found' }, { status: 404 })) instead of
unconditionally returning success; keep the positive path returning
NextResponse.json({ success: true, data: updated }) when updated is non-null.
app/api/bounties/[id]/milestones/advance/route.ts-58-63 (1)

58-63: ⚠️ Potential issue | 🟡 Minor

totalMilestones === 0 is falsy, returning a misleading 500 error.

If a participation record has totalMilestones: 0, the !totalMilestones check treats it the same as undefined/null, returning a 500 "Cannot determine total milestones". Use an explicit nullish check instead.

Proposed fix
-      if (!totalMilestones) {
+      if (totalMilestones == null) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/milestones/advance/route.ts around lines 58 - 63, The
current check treats totalMilestones === 0 as falsy and returns a 500; change
the conditional to an explicit nullish check so only null/undefined trigger the
error. Locate the totalMilestones variable in
app/api/bounties/[id]/milestones/advance/route.ts and replace the if
(!totalMilestones) check with a nullish check (e.g., totalMilestones == null or
totalMilestones === null || totalMilestones === undefined) so zero is allowed,
and keep the same NextResponse.json error path for the nullish case.
components/cards/bounty-card.tsx-119-121 (1)

119-121: ⚠️ Potential issue | 🟡 Minor

Raw uppercase type label — "FIXED PRICE" instead of "Fixed Price".

bounty.type.replace(/_/g, " ") yields all-caps strings like "FIXED PRICE" and "MILESTONE BASED", which is inconsistent with the proper-case labels used elsewhere (e.g., TYPE_CONFIG in lib/bounty-config.ts and typeConfig in components/bounty/bounty-header.tsx). Import TYPE_CONFIG from lib/bounty-config.ts and use TYPE_CONFIG[bounty.type]?.label for a consistent display.

Proposed fix
-            <Badge variant="outline" className="bg-primary/10 text-primary">
-              {bounty.type.replace(/_/g, " ")}
-            </Badge>
+            <Badge variant="outline" className="bg-primary/10 text-primary">
+              {TYPE_CONFIG[bounty.type as BountyType]?.label ?? bounty.type}
+            </Badge>

This requires importing TYPE_CONFIG from @/lib/bounty-config and BountyType from the appropriate type module.

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

In `@components/cards/bounty-card.tsx` around lines 119 - 121, Replace the raw
uppercase label usage in the Badge (currently using bounty.type.replace(/_/g, "
")) with the canonical label from TYPE_CONFIG: import TYPE_CONFIG from
"@/lib/bounty-config" and use TYPE_CONFIG[bounty.type]?.label (casting
bounty.type to BountyType if needed) inside the Badge so the displayed text
matches other components (e.g., components/bounty/bounty-header.tsx) and handles
missing config safely.
components/bounty/bounty-sidebar.tsx-125-133 (1)

125-133: ⚠️ Potential issue | 🟡 Minor

Toast message perspective is wrong — message is shown to the maintainer but reads as if directed to the contributor.

The maintainer sees: "You have been rated 4 stars and gained +40 reputation!" — but the maintainer is the one giving the rating, not receiving it. The description "Congratulations on your contribution!" further reinforces the wrong perspective.

Proposed fix
     toast.success(
-      `You have been rated ${rating} star${rating > 1 ? "s" : ""} and gained +${rating * 10} reputation!`,
-      {
-        description: "Congratulations on your contribution!",
-      },
+      `Rating submitted: ${rating} star${rating > 1 ? "s" : ""}. Contributor gained +${rating * 10} reputation.`,
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/bounty-sidebar.tsx` around lines 125 - 133, The toast shown
after rating uses the wrong perspective; update the message in the toast.success
call (the block that calls setHasRated, setShowRating and toast.success,
referencing rating) so it addresses the maintainer action rather than the
contributor receiving the rating—for example change the title to something like
"You rated the contributor X star(s) — they gained +Y reputation" or
"Contributor rated X star(s); +Y reputation awarded" and update the description
from "Congratulations on your contribution!" to a maintainer-facing confirmation
such as "Rating submitted successfully" or "The contributor has been notified."
Ensure you adjust pluralization logic (rating > 1 ? "s" : "") and reputation
calculation (rating * 10) are preserved.
components/bounty/bounty-sidebar.tsx-56-84 (1)

56-84: ⚠️ Potential issue | 🟡 Minor

res.json() on error response can throw if body is not valid JSON.

Line 69: if the server returns a non-JSON error body (e.g., plain text 500), res.json() will throw an unhandled exception that falls to the outer catch — which shows a generic "Something went wrong" alert. While not catastrophic, the original error context is lost.

Also, the function mixes alert() (lines 70, 79) and toast() (line 74) for user feedback, which is inconsistent UX.

Proposed fix: wrap res.json() and unify feedback
       if (!res.ok) {
-        const error = await res.json();
-        alert(error.error || "Action failed");
+        const error = await res.json().catch(() => ({ error: "Action failed" }));
+        toast.error(error.error || "Action failed");
         return false;
       }
 
       toast("Action completed successfully");
       window.location.reload();
       return true;
     } catch (error) {
       console.error("Action error:", error);
-      alert("Something went wrong");
+      toast.error("Something went wrong");
       return false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/bounty-sidebar.tsx` around lines 56 - 84, The error
handling in handleAction currently calls await res.json() on non-OK responses
which can throw if the response body is not JSON; wrap the res.json() call in a
safe parse (try/catch or check Content-Type) and fall back to reading text
(res.text()) or a default message so the original error context is preserved,
and replace alert(...) usages with the same toast(...) UI used for success so
feedback is consistent (update the error branch to toast(errorMessage) and the
catch block to toast with the caught error message); keep setLoading(false) in
the finally as-is and continue including CURRENT_USER_ID in the request body.
lib/graphql/client.ts-59-65 (1)

59-65: ⚠️ Potential issue | 🟡 Minor

Unsafe double-cast as unknown as (...) bypasses type safety and is unnecessary in graphql-request v7.

The graphql-request v7 API natively supports passing (document, variables?, requestHeaders?) as positional arguments via the VariablesAndRequestHeadersArgs tuple type. Removing this cast entirely and calling directly will preserve full type safety:

return await graphQLClient.request<TData>(query, variables, headers);

Alternatively, use the object form for explicit clarity:

Object form (also valid in v7)
-      return await (
-        graphQLClient.request as unknown as (
-          q: string,
-          v?: TVariables,
-          h?: Record<string, string>,
-        ) => Promise<TData>
-      )(query, variables, headers);
+      return await graphQLClient.request<TData>({
+        document: query,
+        variables,
+        requestHeaders: headers,
+      });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/graphql/client.ts` around lines 59 - 65, The return uses an unsafe
double-cast on graphQLClient.request which bypasses TypeScript safety; remove
the "as unknown as" cast and call graphQLClient.request with the generic TData
and the positional arguments (query, variables, headers) or use the object form
(document and variables/requestHeaders) so the native
VariablesAndRequestHeadersArgs typing is preserved; update the call site where
graphQLClient.request is invoked to use the typed request signature instead of
the double-cast.

- Aligned enums and removed outdated fields to ensure type consistency across 33+ files.
- Updated type definitions, UI components, pages, and API routes to reflect backend changes.
- Added new nested objects and methods to enhance functionality.
- Conducted thorough testing to ensure successful builds and zero warnings.
- Note: Breaking changes introduced for consumers of Bounty types.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/api/reputation.ts (1)

50-54: Nit: cache query.toString() to avoid the duplicate call.

query.toString() is evaluated twice on line 53. Storing it in a local const is marginally cleaner.

♻️ Suggested tweak
     const query = new URLSearchParams();
     if (params?.limit != null) query.set("limit", String(params.limit));
     if (params?.offset != null) query.set("offset", String(params.offset));
-    const url = `${REPUTATION_ENDPOINT}/${userId}/history${query.toString() ? `?${query.toString()}` : ""}`;
+    const qs = query.toString();
+    const url = `${REPUTATION_ENDPOINT}/${userId}/history${qs ? `?${qs}` : ""}`;
     return get<ReputationHistoryResponse>(url);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/reputation.ts` around lines 50 - 54, The URL-building code calls
query.toString() twice; cache the result in a local constant (e.g., const q =
query.toString()) after constructing the URLSearchParams and use that constant
when composing url and the conditional query suffix so the logic in the
reputation endpoint assembly (the const query, the URL template that creates
url, and the final call to get<ReputationHistoryResponse>(url)) uses the cached
string instead of calling query.toString() twice.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/api/reputation.ts`:
- Line 6: Remove the unused ReputationHistoryParams import from the top of
lib/api/reputation.ts; locate the import list that includes
ReputationHistoryParams and delete that identifier so the file only imports
types actually used (the function that takes userId and the inline pagination
type), ensuring no other code references ReputationHistoryParams (if referenced,
update those places to use the inline type or correct parameter shape).
- Around line 46-55: The client method fetchCompletionHistory constructs
requests to `${REPUTATION_ENDPOINT}/${userId}/history` but there is no server
route to handle that path; add a new GET route handler (e.g., create
app/api/reputation/[userId]/history/route.ts) that accepts the same query params
(limit, offset) and returns the ReputationHistoryResponse shape, mirroring the
logic in the existing [userId]/route.ts but scoped to the /history sub-path so
fetchCompletionHistory can succeed; ensure route exports a GET handler that
parses userId from params and query params limit/offset and uses the same data
access functions as the existing reputation endpoint.

---

Nitpick comments:
In `@lib/api/reputation.ts`:
- Around line 50-54: The URL-building code calls query.toString() twice; cache
the result in a local constant (e.g., const q = query.toString()) after
constructing the URLSearchParams and use that constant when composing url and
the conditional query suffix so the logic in the reputation endpoint assembly
(the const query, the URL template that creates url, and the final call to
get<ReputationHistoryResponse>(url)) uses the cached string instead of calling
query.toString() twice.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ebbc724 and 8125eb4.

⛔ Files ignored due to path filters (2)
  • package-lock.json is excluded by !**/package-lock.json
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • lib/api/reputation.ts
  • package.json

@0xdevcollins 0xdevcollins merged commit 0d3f3ae into boundlessfi:main Feb 23, 2026
1 of 2 checks passed
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
package.json (1)

61-61: ⚠️ Potential issue | 🔴 Critical

Update @next/swc-wasm-nodejs version to match available releases.

next at ^16.1.6 is a valid stable release (published Jan 27, 2026). However, @next/swc-wasm-nodejs does not have a 16.1.6 release. The latest available version as of February 2026 is 15.5.3. Either downgrade @next/swc-wasm-nodejs to ^15.5.3 or verify and use the correct 16.x version if available from the package maintainer.

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

In `@package.json` at line 61, package.json lists "next": "^16.1.6" but
`@next/swc-wasm-nodejs` does not have a matching 16.1.6 release; update
dependencies so versions are compatible by either changing the
`@next/swc-wasm-nodejs` entry to the latest published 15.x release (e.g., ^15.5.3)
or aligning both packages to a valid 16.x set if the maintainer provides a
matching `@next/swc-wasm-nodejs` 16.x; modify the package.json dependency for
"@next/swc-wasm-nodejs" accordingly and run your package manager lockfile update
(npm/yarn/pnpm) to regenerate the lockfile.
♻️ Duplicate comments (9)
app/profile/[userId]/page.tsx (2)

43-57: Status mapping fixes for IN_PROGRESS and CANCELLED look correct.

The previous review flagged that CANCELLED mapped to "completed" and IN_PROGRESS to "in-review". Both are now correctly mapped to "cancelled" and "in-progress" respectively, and additional statuses (DRAFT, SUBMITTED, DISPUTED, OPEN) are handled.

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

In `@app/profile/`[userId]/page.tsx around lines 43 - 57, Previously bounty.status
values were mapped incorrectly; ensure the code sets the local variable status
based on bounty.status for all known cases (COMPLETED, IN_PROGRESS, CANCELLED,
DRAFT, SUBMITTED, DISPUTED, OPEN) as shown (bounty.status -> status) and add a
final default branch so status is never left undefined (e.g., set status =
"unknown" or "other"); locate the conditional handling of bounty.status in
page.tsx and either keep the fixed if/else chain or replace it with a switch on
bounty.status to make the mappings explicit and maintainable.

41-57: ⚠️ Potential issue | 🟡 Minor

Missing mapping for UNDER_REVIEW status.

The backend schema includes UNDER_REVIEW as a valid BountyStatus value (per the PR objectives), but it's not handled here. It would fall through to the "unknown" default.

Proposed fix
         } else if (bounty.status === "DISPUTED") {
           status = "disputed";
+        } else if (bounty.status === "UNDER_REVIEW") {
+          status = "under-review";
         } else if (bounty.status === "OPEN") {
           status = "open";
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/profile/`[userId]/page.tsx around lines 41 - 57, The status mapping
misses the BACKEND value "UNDER_REVIEW", causing it to default to "unknown";
update the conditional chain in page.tsx where bounty.status is inspected (the
block that assigns to the local variable status) to handle bounty.status ===
"UNDER_REVIEW" and set status to "under-review" (place it alongside the other
else-if cases so it maps consistently with "in-progress"/"completed"/etc.).
components/reputation/my-claims.tsx (1)

25-56: CLAIM_SECTIONS now covers all backend statuses — previous concern addressed.

The five missing normalized statuses (open, submitted, draft, cancelled, disputed) have been added to appropriate sections. This resolves the earlier gap where claims with those statuses would silently disappear.

One minor UX note: "draft" grouped under "In Review" may confuse users since drafts haven't been submitted yet. Consider whether a separate "Drafts" section (or placing it in "Active Claims") better communicates the claim's lifecycle stage. Not blocking.

,

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

In `@components/reputation/my-claims.tsx` around lines 25 - 56, CLAIM_SECTIONS
currently includes the previously-missing normalized statuses (open, submitted,
draft, cancelled, disputed) which fixes disappearing claims; to address the UX
concern move "draft" out of the "In Review" statuses (the CLAIM_SECTIONS
constant) into either a new "Drafts" section or into the "Active Claims"
statuses array so drafts are represented clearly in the UI—update the
CLAIM_SECTIONS entry for "In Review" to remove "draft" and add a new object {
title: "Drafts", statuses: ["draft"] } or add "draft" to the "Active Claims"
statuses as appropriate.
app/api/bounties/[id]/submit/route.ts (1)

14-18: Authentication restored — previous critical concern addressed.

getCurrentUser() is now called and contributorId is derived from the authenticated user, preventing the spoofing issue flagged in the prior review.

,

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

In `@app/api/bounties/`[id]/submit/route.ts around lines 14 - 18, The auth fix
needs one final enforcement: ensure contributorId is derived from the
authenticated user and any client-sent contributorId is ignored—use the result
of getCurrentUser() (e.g., const user = await getCurrentUser(); const
contributorId = user.id) and replace any use of request.body.contributorId or
similar in submit handler code (e.g., in route.ts functions that create the
submission) so the server-authoritative contributorId is used for DB writes and
permission checks.
components/bounty/github-bounty-card.tsx (1)

64-73: defaultTypeInfo fallback addresses the prior concern nicely.

The defensive fallback for unknown bounty.type values prevents the runtime crash flagged in the previous review.

,

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

In `@components/bounty/github-bounty-card.tsx` around lines 64 - 73, Keep the
defensive fallback as implemented: in BountyCard ensure defaultTypeInfo is used
when bounty.type is missing or unknown by keeping the expression const typeInfo
= typeConfig[bounty.type as BountyType] ?? defaultTypeInfo; and retain
defaultTypeInfo (with label, icon, className) to avoid runtime crashes; also
ensure typeConfig is typed to accept BountyType keys so the lookup remains safe.
lib/types.ts (1)

17-28: Clean consolidation — both previous concerns resolved.

The duplicate Bounty interface and the type: string issue are eliminated by re-exporting canonical types from types/bounty.ts. Consumers of lib/types.ts now get full type safety including BountyType and BountyStatus enums.

,

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

In `@lib/types.ts` around lines 17 - 28, Consolidate type usage by removing any
local duplicate interfaces and relying on the re-exported canonical types:
Bounty, BountyType, BountyStatus, BountyOrganization, BountyProject,
BountySubmission, BountyWindowType, BountySubmissionUser, and BountyCount;
update code that previously declared a local Bounty or used an untyped `type:
string` to import/use the re-exported Bounty and enum types instead so callers
get full enum/type safety, and ensure there are no remaining local definitions
or conflicting imports shadowing these exported symbols.
lib/graphql/schema.graphql (2)

23-27: Bounty.status and Bounty.type are still String! rather than the defined enums BountyStatus/BountyType; same applies to BountySubmissionType.status (line 82), ReviewSubmissionInput.status (line 179), and UpdateBountyInput.status (line 191).

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

In `@lib/graphql/schema.graphql` around lines 23 - 27, The schema fields are using
raw String! instead of the enum types; update Bounty.status to use BountyStatus!
and Bounty.type to use BountyType!, and likewise change
BountySubmissionType.status, ReviewSubmissionInput.status, and
UpdateBountyInput.status from String! to the appropriate enum type
(BountyStatus! or BountyType! as applicable), ensuring the enum definitions
BountyStatus and BountyType are present in the file and referenced where these
fields are declared.

88-93: BountySubmissionUser.email PII exposure was previously flagged; lib/graphql/resolvers.ts has been added to gate this field, which is an improvement — but the resolver must be wired into the GraphQL server to take effect (see comment in lib/graphql/resolvers.ts).

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

In `@lib/graphql/schema.graphql` around lines 88 - 93, The
BountySubmissionUser.email field resolver implemented in
lib/graphql/resolvers.ts is not yet wired into the GraphQL server, so the PII
gate isn't applied; import the resolver (the exported resolver object/function
in lib/graphql/resolvers.ts) into the server bootstrap where you assemble
GraphQL schema/resolvers (e.g., the object passed to makeExecutableSchema or
ApolloServer) and merge it into the top-level resolver map so that the
BountySubmissionUser.email resolver overrides the schema field; ensure the
import is used instead of or merged with any existing resolvers map so the
field-level gating logic is executed at runtime.
components/bounty/bounty-sidebar.tsx (1)

99-111: Fallback to bounty.createdBy still targets the wrong person for rating.

The primary path via submission?.submittedBy is a good fix, but the fallback chain submission?.submittedBy ?? bounty.createdBy ?? "" still rates the bounty creator (maintainer) when no submissions exist. If there's no submission to rate, it's likely better to skip the rating flow entirely rather than rate the wrong person.

♻️ Suggested guard
      const submission =
        bounty.submissions && bounty.submissions.length > 0
          ? bounty.submissions[0]
          : null;
-     const contributorId = submission?.submittedBy ?? bounty.createdBy ?? "";
-     const contributorName =
-       submission?.submittedByUser?.name ?? "Contributor";
-     setRatingTarget({
-       id: contributorId,
-       name: contributorName,
-       reputation: 100 + (reputationGain || 0),
-     });
-     setShowRating(true);
+     if (!submission) {
+       toast.info("No submissions to rate.");
+       return;
+     }
+     setRatingTarget({
+       id: submission.submittedBy,
+       name: submission.submittedByUser?.name ?? "Contributor",
+       reputation: 100 + (reputationGain || 0),
+     });
+     setShowRating(true);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/bounty-sidebar.tsx` around lines 99 - 111, The current
contributor selection uses submission?.submittedBy ?? bounty.createdBy which
incorrectly falls back to the bounty creator when no submission exists; change
the logic in the component around submission, contributorId and setRatingTarget
so that if submission is null you do not set a rating target (e.g., skip calling
setRatingTarget or set it to null/undefined) instead of using bounty.createdBy,
and ensure any UI/flow that expects a rating target handles the absence
accordingly.
🧹 Nitpick comments (11)
lib/api/reputation.ts (1)

52-52: Nit: query.toString() is evaluated twice.

Trivial, but you could store the result in a local variable for clarity.

Suggested tweak
-    const url = `${REPUTATION_ENDPOINT}/${userId}/history${query.toString() ? `?${query.toString()}` : ""}`;
+    const qs = query.toString();
+    const url = `${REPUTATION_ENDPOINT}/${userId}/history${qs ? `?${qs}` : ""}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/reputation.ts` at line 52, The URL construction currently calls
query.toString() twice; assign const qs = query.toString() (or similar) and use
qs in the template literal when building url (using REPUTATION_ENDPOINT, userId,
and query) so the string conversion happens once and the expression is clearer
and more efficient.
lib/services/reputation.ts (1)

153-163: rateContributor stub: consider validating the rating range.

The method accepts any number but the BountyCompletionRecord.maintainerRating type indicates a 1–5 scale. Even for a mock/stub, a bounds check would prevent garbage data from propagating if upstream callers pass invalid values.

Optional guard
   static async rateContributor(
     maintainerId: string,
     contributorId: string,
     rating: number,
   ): Promise<boolean> {
+    if (rating < 1 || rating > 5) {
+      console.warn(`Invalid rating ${rating} — must be 1-5`);
+      return false;
+    }
     console.log(
       `Maintainer ${maintainerId} rated ${contributorId} with ${rating}`,
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/services/reputation.ts` around lines 153 - 163, The rateContributor stub
accepts any number and should validate that rating is an integer between 1 and 5
(matching BountyCompletionRecord.maintainerRating) before proceeding; update the
static method rateContributor to check the rating range (and optionally
integer-ness), log or return false/error when out of bounds, and only perform
the success path (console.log and return true) when the value is valid so
invalid upstream values are rejected early.
app/api/reputation/[userId]/history/route.ts (1)

19-23: Default values for limit/offset are duplicated between the route and the service.

Lines 21–22 apply ?? 10 / ?? 0, but ReputationService.getCompletionHistory already defaults limit = 10, offset = 0. Consider passing limit and offset directly (possibly undefined) to avoid the defaults drifting apart.

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

In `@app/api/reputation/`[userId]/history/route.ts around lines 19 - 23, The route
currently applies duplicate defaults before calling
ReputationService.getCompletionHistory (see the resp assignment), causing
default drift; remove the inline fallbacks (?? 10 / ?? 0) and pass limit and
offset directly (allowing undefined) to ReputationService.getCompletionHistory
so the service's own defaults (limit = 10, offset = 0) are authoritative; update
the call in route.ts to use userId, limit, offset without providing hardcoded
defaults.
scripts/sync-schema.js (1)

44-44: Hardcoded path in error message doesn't reflect actual source location.

Line 44 always references ../boundless-nestjs/src/schema.gql in the error message, but the actual source could be the CANONICAL_SCHEMA env var or the in-repo path boundless-nestjs/src/schema.gql. Consider using the resolved src variable instead.

Proposed fix
     console.error(
-      "lib/graphql/schema.graphql is out of sync with ../boundless-nestjs/src/schema.gql",
+      `lib/graphql/schema.graphql is out of sync with ${src}`,
     );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync-schema.js` at line 44, The error message currently hardcodes
"../boundless-nestjs/src/schema.gql"; update the code in scripts/sync-schema.js
to use the resolved source variable (src) instead of the literal string so the
message reflects the actual source (whether from CANONICAL_SCHEMA or the in-repo
path). Locate the string that reads "lib/graphql/schema.graphql is out of sync
with ../boundless-nestjs/src/schema.gql" and replace it with a message that
interpolates src (e.g., using a template literal or string concatenation) so the
log shows the real resolved path/value.
app/api/bounties/[id]/join/route.ts (2)

64-69: No error logging in the catch block — inconsistent with sibling routes.

The claim/route.ts handler logs console.error("Error claiming bounty:", error) in its catch block, but this route silently swallows exceptions. For operational debuggability, consider binding and logging the error.

Proposed fix
-  } catch {
+  } catch (error) {
+    console.error("Error joining bounty:", error);
     return NextResponse.json(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/join/route.ts around lines 64 - 69, The catch block in
the join route silently swallows exceptions; change it to catch the error (e.g.,
catch (error)) and log it before returning the 500 response—mirror the sibling
handler by adding console.error("Error joining bounty:", error) in the catch of
the function handling the join request (the catch in
app/api/bounties/[id]/join/route.ts) and then return the existing
NextResponse.json({ error: "Internal Server Error" }, { status: 500 }).

51-59: Minor: use a single Date instance for consistent timestamps.

The two separate new Date().toISOString() calls (lines 57-58) can produce slightly different timestamp values. Capture the timestamp once and reuse it to ensure both fields are identical.

Proposed optimization
+    const now = new Date().toISOString();
     const participation: MilestoneParticipation = {
       id: generateId(),
       bountyId,
       contributorId,
       currentMilestone: 1, // Start at milestone 1
       status: "active",
-      joinedAt: new Date().toISOString(),
-      lastUpdatedAt: new Date().toISOString(),
+      joinedAt: now,
+      lastUpdatedAt: now,
     };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/join/route.ts around lines 51 - 59, The two separate
new Date().toISOString() calls in the MilestoneParticipation object can yield
slightly different timestamps; capture a single timestamp (e.g., const now = new
Date().toISOString()) before building the participation object and reuse that
value for both joinedAt and lastUpdatedAt so they are identical when creating
the participation (reference: MilestoneParticipation, participation, joinedAt,
lastUpdatedAt).
app/api/bounties/[id]/submit/route.ts (1)

20-28: Malformed JSON body will surface as a 500 instead of a 400.

If the client sends a non-JSON body, request.json() throws and the outer catch returns a generic 500. A targeted try/catch around the parse (or a content-type check) would give callers a more actionable 400 response.

Proposed fix
+    let body;
+    try {
+      body = await request.json();
+    } catch {
+      return NextResponse.json(
+        { error: "Invalid JSON body" },
+        { status: 400 },
+      );
+    }
-    const body = await request.json();
     const { content } = body;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/submit/route.ts around lines 20 - 28, Wrap the JSON
parse so malformed bodies return 400: guard the call to request.json() (the line
assigning const body = await request.json()) with a try/catch (or first check
request.headers.get('content-type') for application/json), catch the
parse/SyntaxError, and return NextResponse.json({ error: "Invalid JSON body" },
{ status: 400 }); after successful parse continue to destructure const { content
} = body and keep the existing missing-field 400 handling.
components/bounty/github-bounty-card.tsx (1)

161-163: Reward amount rendered without number formatting.

bounty.rewardAmount is displayed raw (e.g., 1000 USDC). Other components like my-claims.tsx use formatCurrency(claim.rewardAmount, "$") for consistent presentation (e.g., $1,000.00). Consider applying a formatter here for visual consistency.

Proposed fix
+import { formatCurrency } from "@/helpers/format.helper";
 ...
             <span className="font-bold text-primary">
-              {bounty.rewardAmount} {bounty.rewardCurrency}
+              {formatCurrency(bounty.rewardAmount, bounty.rewardCurrency)}
             </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/github-bounty-card.tsx` around lines 161 - 163, Replace the
raw display of bounty.rewardAmount and bounty.rewardCurrency with the same
formatter used elsewhere (e.g., formatCurrency) to ensure consistent
number/currency formatting; import formatCurrency from the same util used in
my-claims.tsx and call it with bounty.rewardAmount and the appropriate currency
symbol/identifier so the <span className="font-bold text-primary"> shows a
formatted string like "$1,000.00" instead of "1000 USDC".
lib/api/bounties.ts (1)

47-51: Consider adding field-level validation to githubIssueUrl and rewardAmount.

z.string() accepts any string including empty or non-URL values for githubIssueUrl, and z.number() accepts zero or negatives for rewardAmount. Neither will be caught before the API call.

♻️ Proposed improvement
-  githubIssueUrl: z.string(),
-  githubIssueNumber: z.number().nullable(),
+  githubIssueUrl: z.string().url(),
+  githubIssueNumber: z.number().int().positive().nullable(),
 
-  rewardAmount: z.number(),
+  rewardAmount: z.number().positive(),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/api/bounties.ts` around lines 47 - 51, The schema currently allows any
string for githubIssueUrl and any number (including zero/negative) for
rewardAmount; update the Zod schema in lib/api/bounties.ts so githubIssueUrl
uses z.string().nonempty().url() (to reject empty and non-URL values) and make
rewardAmount use z.number().positive() or z.number().min(1) (to reject
zero/negative amounts); modify the githubIssueUrl and rewardAmount declarations
accordingly while keeping githubIssueNumber nullable as-is.
components/bounty/bounty-sidebar.tsx (2)

59-75: Prefer router.refresh() over window.location.reload(), and use toast consistently for feedback.

Three issues in this handler:

  1. window.location.reload() performs a full hard navigation, discarding all client state. In Next.js App Router, useRouter().refresh() re-fetches server components and preserves client state — much lighter and avoids a flash of blank page.
  2. alert(message) on error (line 70) while using toast() on success (line 63) is inconsistent UX. Prefer toast.error(message) so all feedback flows through the same notification system.
  3. Generic success message "Action completed successfully" — should be "Bounty claimed successfully" or similar.
♻️ Proposed fix

Add useRouter import and hook:

import { useRouter } from "next/navigation";
// inside the component:
const router = useRouter();

Then update handleClaim:

 const handleClaim = async (): Promise<boolean> => {
   setLoading(true);
   try {
     await bountiesApi.claim(bounty.id, CURRENT_USER_ID);
-    toast("Action completed successfully");
-    window.location.reload();
+    toast.success("Bounty claimed successfully");
+    router.refresh();
     return true;
   } catch (error) {
     console.error("Claim error:", error);
     const message = error instanceof Error ? error.message : "Action failed";
-    alert(message);
+    toast.error(message);
     return false;
   } finally {
     setLoading(false);
   }
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@components/bounty/bounty-sidebar.tsx` around lines 59 - 75, The handleClaim
handler should use Next.js routing and consistent toasts: import and call
useRouter() to get router, replace window.location.reload() with
router.refresh() after a successful bountiesApi.claim(bounty.id,
CURRENT_USER_ID), change the success toast text to "Bounty claimed
successfully", and replace alert(message) in the catch with
toast.error(message); keep setLoading and the try/catch/finally structure
intact.

242-242: Nit: empty className="" on Separator.

This has no effect and can be removed for cleanliness, or given the same styling as line 229 (bg-gray-800).

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

In `@components/bounty/bounty-sidebar.tsx` at line 242, Remove the redundant empty
className attribute from the Separator component usage (Separator) in
bounty-sidebar.tsx; either delete className="" entirely or set it to the same
styling used earlier (e.g., "bg-gray-800") to keep markup clean and consistent
with the Separator at line ~229.
🤖 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/check-schema.yml:
- Around line 17-29: The workflow incorrectly uses secrets in step-level if
conditions (steps "Checkout canonical schema repo (private)" and "Note about
private repo") which GitHub disallows; change to expose BOUNDLESS_NESTJS_TOKEN
as a job-level environment variable (e.g., under the job's env) and then update
the steps to use that environment variable in their if conditions (e.g., if:
env.BOUNDLESS_NESTJS_TOKEN != '' and if: env.BOUNDLESS_NESTJS_TOKEN == '') so
actionlint/GitHub will evaluate them properly while still using the secret
stored as BOUNDLESS_NESTJS_TOKEN at the job level.

In `@app/api/bounties/`[id]/milestones/advance/route.ts:
- Around line 22-31: The request currently authorizes before validating body
fields so a missing contributorId yields a 403; move the presence check for
contributorId and action before the authorization check. Specifically, in
route.ts ensure the if (!contributorId || !action) { return
NextResponse.json(... 400) } block runs before the if (contributorId !==
user.id) { return NextResponse.json(... 403) } block (referencing contributorId,
action, and user.id) so missing fields return 400 and only valid payloads reach
the authorization comparison.

In `@app/api/reputation/`[userId]/history/route.ts:
- Around line 12-17: limit and offset currently use
Number(searchParams.get(...)) which produces NaN for non-numeric input and
silently breaks pagination; change the parsing to use parseInt (or parseFloat if
fractional allowed) and guard with isNaN so invalid values fall back to
undefined/defaults: update the code that sets limit and offset (the const limit
and const offset assignments in route.ts) to parse the query string with
parseInt(searchParams.get("limit") ?? "", 10) /
parseInt(searchParams.get("offset") ?? "", 10), then if isNaN(parsed) set the
variable to undefined (or your default page size/0), ensuring downstream slice
calls use valid numbers.

In `@app/profile/`[userId]/page.tsx:
- Around line 38-39: The "My Claims" list is using bounty.createdBy === userId
which returns bounties the user posted, not those they claimed; update the
filter to use the appropriate assignment field (e.g., bounty.claimedBy or
bounty.assignedTo) instead of createdBy, or if that field doesn't exist yet add
a TODO and change the section label to "My Posts"; locate the filter expression
in the function that maps/returns bounties (the variable bounties, the
parameter/userId and the createdBy field) and replace createdBy with the correct
claim/assignment property or add a TODO comment and relabel the UI string.

In `@lib/graphql/resolvers.ts`:
- Around line 1-30: The BountySubmissionUser.email resolver is defined but not
registered with any GraphQL schema and also depends on getCurrentUser which uses
Next.js cookies() (not safe outside a Next request); fix by importing and
attaching the exported resolvers object (resolvers, BountySubmissionUser.email)
into your GraphQL schema/server initialization (e.g., when calling
makeExecutableSchema/createSchema or ApolloServer) so it is actually used, and
refactor getCurrentUser in lib/server-auth.ts to accept an explicit
request/context (or provide a context-aware wrapper) instead of calling
next/headers cookies() directly so the resolver can call getCurrentUser(context)
from the GraphQL context when resolving requests.

In `@scripts/sync-schema.js`:
- Around line 36-50: The --check branch is comparing raw srcContents to
destContents while the write path writes header + srcContents, causing a
permanent mismatch; move the header declaration so it exists before the --check
block and change the comparison to build expectedContents = header + srcContents
(or otherwise prepend header to srcContents) and compare expectedContents to
destContents (use the existing srcContents and destContents variables and keep
the same exit codes/log messages).

---

Outside diff comments:
In `@package.json`:
- Line 61: package.json lists "next": "^16.1.6" but `@next/swc-wasm-nodejs` does
not have a matching 16.1.6 release; update dependencies so versions are
compatible by either changing the `@next/swc-wasm-nodejs` entry to the latest
published 15.x release (e.g., ^15.5.3) or aligning both packages to a valid 16.x
set if the maintainer provides a matching `@next/swc-wasm-nodejs` 16.x; modify the
package.json dependency for "@next/swc-wasm-nodejs" accordingly and run your
package manager lockfile update (npm/yarn/pnpm) to regenerate the lockfile.

---

Duplicate comments:
In `@app/api/bounties/`[id]/submit/route.ts:
- Around line 14-18: The auth fix needs one final enforcement: ensure
contributorId is derived from the authenticated user and any client-sent
contributorId is ignored—use the result of getCurrentUser() (e.g., const user =
await getCurrentUser(); const contributorId = user.id) and replace any use of
request.body.contributorId or similar in submit handler code (e.g., in route.ts
functions that create the submission) so the server-authoritative contributorId
is used for DB writes and permission checks.

In `@app/profile/`[userId]/page.tsx:
- Around line 43-57: Previously bounty.status values were mapped incorrectly;
ensure the code sets the local variable status based on bounty.status for all
known cases (COMPLETED, IN_PROGRESS, CANCELLED, DRAFT, SUBMITTED, DISPUTED,
OPEN) as shown (bounty.status -> status) and add a final default branch so
status is never left undefined (e.g., set status = "unknown" or "other"); locate
the conditional handling of bounty.status in page.tsx and either keep the fixed
if/else chain or replace it with a switch on bounty.status to make the mappings
explicit and maintainable.
- Around line 41-57: The status mapping misses the BACKEND value "UNDER_REVIEW",
causing it to default to "unknown"; update the conditional chain in page.tsx
where bounty.status is inspected (the block that assigns to the local variable
status) to handle bounty.status === "UNDER_REVIEW" and set status to
"under-review" (place it alongside the other else-if cases so it maps
consistently with "in-progress"/"completed"/etc.).

In `@components/bounty/bounty-sidebar.tsx`:
- Around line 99-111: The current contributor selection uses
submission?.submittedBy ?? bounty.createdBy which incorrectly falls back to the
bounty creator when no submission exists; change the logic in the component
around submission, contributorId and setRatingTarget so that if submission is
null you do not set a rating target (e.g., skip calling setRatingTarget or set
it to null/undefined) instead of using bounty.createdBy, and ensure any UI/flow
that expects a rating target handles the absence accordingly.

In `@components/bounty/github-bounty-card.tsx`:
- Around line 64-73: Keep the defensive fallback as implemented: in BountyCard
ensure defaultTypeInfo is used when bounty.type is missing or unknown by keeping
the expression const typeInfo = typeConfig[bounty.type as BountyType] ??
defaultTypeInfo; and retain defaultTypeInfo (with label, icon, className) to
avoid runtime crashes; also ensure typeConfig is typed to accept BountyType keys
so the lookup remains safe.

In `@components/reputation/my-claims.tsx`:
- Around line 25-56: CLAIM_SECTIONS currently includes the previously-missing
normalized statuses (open, submitted, draft, cancelled, disputed) which fixes
disappearing claims; to address the UX concern move "draft" out of the "In
Review" statuses (the CLAIM_SECTIONS constant) into either a new "Drafts"
section or into the "Active Claims" statuses array so drafts are represented
clearly in the UI—update the CLAIM_SECTIONS entry for "In Review" to remove
"draft" and add a new object { title: "Drafts", statuses: ["draft"] } or add
"draft" to the "Active Claims" statuses as appropriate.

In `@lib/graphql/schema.graphql`:
- Around line 23-27: The schema fields are using raw String! instead of the enum
types; update Bounty.status to use BountyStatus! and Bounty.type to use
BountyType!, and likewise change BountySubmissionType.status,
ReviewSubmissionInput.status, and UpdateBountyInput.status from String! to the
appropriate enum type (BountyStatus! or BountyType! as applicable), ensuring the
enum definitions BountyStatus and BountyType are present in the file and
referenced where these fields are declared.
- Around line 88-93: The BountySubmissionUser.email field resolver implemented
in lib/graphql/resolvers.ts is not yet wired into the GraphQL server, so the PII
gate isn't applied; import the resolver (the exported resolver object/function
in lib/graphql/resolvers.ts) into the server bootstrap where you assemble
GraphQL schema/resolvers (e.g., the object passed to makeExecutableSchema or
ApolloServer) and merge it into the top-level resolver map so that the
BountySubmissionUser.email resolver overrides the schema field; ensure the
import is used instead of or merged with any existing resolvers map so the
field-level gating logic is executed at runtime.

In `@lib/types.ts`:
- Around line 17-28: Consolidate type usage by removing any local duplicate
interfaces and relying on the re-exported canonical types: Bounty, BountyType,
BountyStatus, BountyOrganization, BountyProject, BountySubmission,
BountyWindowType, BountySubmissionUser, and BountyCount; update code that
previously declared a local Bounty or used an untyped `type: string` to
import/use the re-exported Bounty and enum types instead so callers get full
enum/type safety, and ensure there are no remaining local definitions or
conflicting imports shadowing these exported symbols.

---

Nitpick comments:
In `@app/api/bounties/`[id]/join/route.ts:
- Around line 64-69: The catch block in the join route silently swallows
exceptions; change it to catch the error (e.g., catch (error)) and log it before
returning the 500 response—mirror the sibling handler by adding
console.error("Error joining bounty:", error) in the catch of the function
handling the join request (the catch in app/api/bounties/[id]/join/route.ts) and
then return the existing NextResponse.json({ error: "Internal Server Error" }, {
status: 500 }).
- Around line 51-59: The two separate new Date().toISOString() calls in the
MilestoneParticipation object can yield slightly different timestamps; capture a
single timestamp (e.g., const now = new Date().toISOString()) before building
the participation object and reuse that value for both joinedAt and
lastUpdatedAt so they are identical when creating the participation (reference:
MilestoneParticipation, participation, joinedAt, lastUpdatedAt).

In `@app/api/bounties/`[id]/submit/route.ts:
- Around line 20-28: Wrap the JSON parse so malformed bodies return 400: guard
the call to request.json() (the line assigning const body = await
request.json()) with a try/catch (or first check
request.headers.get('content-type') for application/json), catch the
parse/SyntaxError, and return NextResponse.json({ error: "Invalid JSON body" },
{ status: 400 }); after successful parse continue to destructure const { content
} = body and keep the existing missing-field 400 handling.

In `@app/api/reputation/`[userId]/history/route.ts:
- Around line 19-23: The route currently applies duplicate defaults before
calling ReputationService.getCompletionHistory (see the resp assignment),
causing default drift; remove the inline fallbacks (?? 10 / ?? 0) and pass limit
and offset directly (allowing undefined) to
ReputationService.getCompletionHistory so the service's own defaults (limit =
10, offset = 0) are authoritative; update the call in route.ts to use userId,
limit, offset without providing hardcoded defaults.

In `@components/bounty/bounty-sidebar.tsx`:
- Around line 59-75: The handleClaim handler should use Next.js routing and
consistent toasts: import and call useRouter() to get router, replace
window.location.reload() with router.refresh() after a successful
bountiesApi.claim(bounty.id, CURRENT_USER_ID), change the success toast text to
"Bounty claimed successfully", and replace alert(message) in the catch with
toast.error(message); keep setLoading and the try/catch/finally structure
intact.
- Line 242: Remove the redundant empty className attribute from the Separator
component usage (Separator) in bounty-sidebar.tsx; either delete className=""
entirely or set it to the same styling used earlier (e.g., "bg-gray-800") to
keep markup clean and consistent with the Separator at line ~229.

In `@components/bounty/github-bounty-card.tsx`:
- Around line 161-163: Replace the raw display of bounty.rewardAmount and
bounty.rewardCurrency with the same formatter used elsewhere (e.g.,
formatCurrency) to ensure consistent number/currency formatting; import
formatCurrency from the same util used in my-claims.tsx and call it with
bounty.rewardAmount and the appropriate currency symbol/identifier so the <span
className="font-bold text-primary"> shows a formatted string like "$1,000.00"
instead of "1000 USDC".

In `@lib/api/bounties.ts`:
- Around line 47-51: The schema currently allows any string for githubIssueUrl
and any number (including zero/negative) for rewardAmount; update the Zod schema
in lib/api/bounties.ts so githubIssueUrl uses z.string().nonempty().url() (to
reject empty and non-URL values) and make rewardAmount use z.number().positive()
or z.number().min(1) (to reject zero/negative amounts); modify the
githubIssueUrl and rewardAmount declarations accordingly while keeping
githubIssueNumber nullable as-is.

In `@lib/api/reputation.ts`:
- Line 52: The URL construction currently calls query.toString() twice; assign
const qs = query.toString() (or similar) and use qs in the template literal when
building url (using REPUTATION_ENDPOINT, userId, and query) so the string
conversion happens once and the expression is clearer and more efficient.

In `@lib/services/reputation.ts`:
- Around line 153-163: The rateContributor stub accepts any number and should
validate that rating is an integer between 1 and 5 (matching
BountyCompletionRecord.maintainerRating) before proceeding; update the static
method rateContributor to check the rating range (and optionally integer-ness),
log or return false/error when out of bounds, and only perform the success path
(console.log and return true) when the value is valid so invalid upstream values
are rejected early.

In `@scripts/sync-schema.js`:
- Line 44: The error message currently hardcodes
"../boundless-nestjs/src/schema.gql"; update the code in scripts/sync-schema.js
to use the resolved source variable (src) instead of the literal string so the
message reflects the actual source (whether from CANONICAL_SCHEMA or the in-repo
path). Locate the string that reads "lib/graphql/schema.graphql is out of sync
with ../boundless-nestjs/src/schema.gql" and replace it with a message that
interpolates src (e.g., using a template literal or string concatenation) so the
log shows the real resolved path/value.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Disabled knowledge base sources:

  • Linear integration is disabled

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 8125eb4 and e54dd16.

📒 Files selected for processing (22)
  • .github/workflows/check-schema.yml
  • README.md
  • app/api/bounties/[id]/claim/route.ts
  • app/api/bounties/[id]/join/route.ts
  • app/api/bounties/[id]/milestones/advance/route.ts
  • app/api/bounties/[id]/submit/route.ts
  • app/api/reputation/[userId]/history/route.ts
  • app/profile/[userId]/page.tsx
  • codegen.ts
  • components/bounty/bounty-sidebar.tsx
  • components/bounty/github-bounty-card.tsx
  • components/cards/bounty-card.tsx
  • components/reputation/my-claims.tsx
  • lib/api/bounties.ts
  • lib/api/reputation.ts
  • lib/graphql/resolvers.ts
  • lib/graphql/schema.graphql
  • lib/mock-data.ts
  • lib/services/reputation.ts
  • lib/types.ts
  • package.json
  • scripts/sync-schema.js
🚧 Files skipped from review as they are similar to previous changes (1)
  • codegen.ts

Comment on lines +17 to +29
- name: Checkout canonical schema repo (private)
if: ${{ secrets.BOUNDLESS_NESTJS_TOKEN != '' }}
uses: actions/checkout@v4
with:
repository: boundlessfi/boundless-nestjs
path: boundless-nestjs
token: ${{ secrets.BOUNDLESS_NESTJS_TOKEN }}

- name: Note about private repo
if: ${{ secrets.BOUNDLESS_NESTJS_TOKEN == '' }}
run: |
echo "No secret BOUNDLESS_NESTJS_TOKEN provided; skipping checkout of private repo."
echo "If the canonical schema is in the private repo, add a repository secret named BOUNDLESS_NESTJS_TOKEN with a PAT that has access to boundlessfi/boundless-nestjs."
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

secrets context is not permitted in step if conditions — both conditional steps will never evaluate correctly.

The GitHub Actions documentation explicitly states that "Secrets cannot be directly referenced in if: conditionals" and recommends "setting secrets as job-level environment variables, then referencing the environment variables to conditionally run steps in the job." The actionlint tool confirms this — lines 18 and 26 will produce "Unrecognized named-value: 'secrets'" errors at parse time, causing the checkout step to never run regardless of whether the token is set.

🔧 Proposed fix — expose the secret as a job-level env var
 jobs:
   check-schema:
     name: Check schema is in sync
     runs-on: ubuntu-latest
+    env:
+      NESTJS_TOKEN: ${{ secrets.BOUNDLESS_NESTJS_TOKEN }}
     steps:
       - name: Checkout repository
         uses: actions/checkout@v4

       - name: Checkout canonical schema repo (private)
-        if: ${{ secrets.BOUNDLESS_NESTJS_TOKEN != '' }}
+        if: ${{ env.NESTJS_TOKEN != '' }}
         uses: actions/checkout@v4
         with:
           repository: boundlessfi/boundless-nestjs
           path: boundless-nestjs
           token: ${{ secrets.BOUNDLESS_NESTJS_TOKEN }}

       - name: Note about private repo
-        if: ${{ secrets.BOUNDLESS_NESTJS_TOKEN == '' }}
+        if: ${{ env.NESTJS_TOKEN == '' }}
         run: |
🧰 Tools
🪛 actionlint (1.7.11)

[error] 18-18: context "secrets" is not allowed here. available contexts are "env", "github", "inputs", "job", "matrix", "needs", "runner", "steps", "strategy", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details

(expression)


[error] 26-26: context "secrets" is not allowed here. available contexts are "env", "github", "inputs", "job", "matrix", "needs", "runner", "steps", "strategy", "vars". see https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability for more details

(expression)

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

In @.github/workflows/check-schema.yml around lines 17 - 29, The workflow
incorrectly uses secrets in step-level if conditions (steps "Checkout canonical
schema repo (private)" and "Note about private repo") which GitHub disallows;
change to expose BOUNDLESS_NESTJS_TOKEN as a job-level environment variable
(e.g., under the job's env) and then update the steps to use that environment
variable in their if conditions (e.g., if: env.BOUNDLESS_NESTJS_TOKEN != '' and
if: env.BOUNDLESS_NESTJS_TOKEN == '') so actionlint/GitHub will evaluate them
properly while still using the secret stored as BOUNDLESS_NESTJS_TOKEN at the
job level.

Comment on lines +22 to +31
if (contributorId !== user.id) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}

if (!participation) {
return NextResponse.json({ error: 'Participation not found' }, { status: 404 });
}
if (!contributorId || !action) {
return NextResponse.json(
{ error: "Missing required fields" },
{ status: 400 },
);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validation runs after authorization — missing contributorId returns 403 instead of 400.

When contributorId is absent from the body, undefined !== user.id is always true, so the 403 at line 23 fires before the !contributorId guard at line 26 is ever reached. Swap the two blocks.

🐛 Proposed fix
-    // Ensure the authenticated user matches the contributor being modified
-    if (contributorId !== user.id) {
-      return NextResponse.json({ error: "Forbidden" }, { status: 403 });
-    }
-
     if (!contributorId || !action) {
       return NextResponse.json(
         { error: "Missing required fields" },
         { status: 400 },
       );
     }
+
+    // Ensure the authenticated user matches the contributor being modified
+    if (contributorId !== user.id) {
+      return NextResponse.json({ error: "Forbidden" }, { status: 403 });
+    }
📝 Committable suggestion

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

Suggested change
if (contributorId !== user.id) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
if (!participation) {
return NextResponse.json({ error: 'Participation not found' }, { status: 404 });
}
if (!contributorId || !action) {
return NextResponse.json(
{ error: "Missing required fields" },
{ status: 400 },
);
}
if (!contributorId || !action) {
return NextResponse.json(
{ error: "Missing required fields" },
{ status: 400 },
);
}
// Ensure the authenticated user matches the contributor being modified
if (contributorId !== user.id) {
return NextResponse.json({ error: "Forbidden" }, { status: 403 });
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/bounties/`[id]/milestones/advance/route.ts around lines 22 - 31, The
request currently authorizes before validating body fields so a missing
contributorId yields a 403; move the presence check for contributorId and action
before the authorization check. Specifically, in route.ts ensure the if
(!contributorId || !action) { return NextResponse.json(... 400) } block runs
before the if (contributorId !== user.id) { return NextResponse.json(... 403) }
block (referencing contributorId, action, and user.id) so missing fields return
400 and only valid payloads reach the authorization comparison.

Comment on lines +12 to +17
const limit = searchParams.get("limit")
? Number(searchParams.get("limit"))
: undefined;
const offset = searchParams.get("offset")
? Number(searchParams.get("offset"))
: undefined;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Non-numeric query params silently produce empty results instead of falling back to defaults.

Number("abc") returns NaN, which passes the truthiness check but then NaN ?? 10 still yields NaN (the ?? operator only catches null/undefined). This propagates to Array.slice(offset, offset + NaN), returning an empty array instead of the default page.

Proposed fix — validate with parseInt + isNaN guard
-    const limit = searchParams.get("limit")
-      ? Number(searchParams.get("limit"))
-      : undefined;
-    const offset = searchParams.get("offset")
-      ? Number(searchParams.get("offset"))
-      : undefined;
+    const rawLimit = searchParams.get("limit");
+    const rawOffset = searchParams.get("offset");
+    const limit = rawLimit != null && !Number.isNaN(Number(rawLimit))
+      ? Number(rawLimit)
+      : undefined;
+    const offset = rawOffset != null && !Number.isNaN(Number(rawOffset))
+      ? Number(rawOffset)
+      : undefined;
📝 Committable suggestion

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

Suggested change
const limit = searchParams.get("limit")
? Number(searchParams.get("limit"))
: undefined;
const offset = searchParams.get("offset")
? Number(searchParams.get("offset"))
: undefined;
const rawLimit = searchParams.get("limit");
const rawOffset = searchParams.get("offset");
const limit = rawLimit != null && !Number.isNaN(Number(rawLimit))
? Number(rawLimit)
: undefined;
const offset = rawOffset != null && !Number.isNaN(Number(rawOffset))
? Number(rawOffset)
: undefined;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/api/reputation/`[userId]/history/route.ts around lines 12 - 17, limit and
offset currently use Number(searchParams.get(...)) which produces NaN for
non-numeric input and silently breaks pagination; change the parsing to use
parseInt (or parseFloat if fractional allowed) and guard with isNaN so invalid
values fall back to undefined/defaults: update the code that sets limit and
offset (the const limit and const offset assignments in route.ts) to parse the
query string with parseInt(searchParams.get("limit") ?? "", 10) /
parseInt(searchParams.get("offset") ?? "", 10), then if isNaN(parsed) set the
variable to undefined (or your default page size/0), ensuring downstream slice
calls use valid numbers.

Comment on lines 38 to +39
return bounties
.filter((bounty) => bounty.claimedBy === userId)
.filter((bounty) => bounty.createdBy === userId)
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

createdBy filter inverts the "My Claims" semantics — shows bounties the user posted, not claimed.

The section is labeled "My Claims" (implying bounties the user has claimed / is working on), but filtering on bounty.createdBy === userId returns bounties the user created. The old field claimedBy was removed in this schema alignment, but the replacement should reflect who is assigned/working on the bounty, not who created it. If no equivalent field exists yet, consider adding a TODO or adjusting the section label to avoid misleading users.

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

In `@app/profile/`[userId]/page.tsx around lines 38 - 39, The "My Claims" list is
using bounty.createdBy === userId which returns bounties the user posted, not
those they claimed; update the filter to use the appropriate assignment field
(e.g., bounty.claimedBy or bounty.assignedTo) instead of createdBy, or if that
field doesn't exist yet add a TODO and change the section label to "My Posts";
locate the filter expression in the function that maps/returns bounties (the
variable bounties, the parameter/userId and the createdBy field) and replace
createdBy with the correct claim/assignment property or add a TODO comment and
relabel the UI string.

Comment on lines +1 to +30
import { getCurrentUser } from "@/lib/server-auth";

/**
* Minimal resolver overrides for GraphQL fields that require
* authorization checks before returning PII.
*
* This file only implements gating for `BountySubmissionUser.email`.
* If you have a GraphQL server that loads resolvers, include this
* object when creating the executable schema.
*/
export const resolvers = {
BountySubmissionUser: {
email: async (parent: { id: string; email?: string }) => {
// Only return email to the owner (same id) or in development.
// This is intentionally conservative — expand checks (roles/admin)
// when your auth model supports them.
try {
const user = await getCurrentUser();
if (!user) return null;
if (process.env.NODE_ENV === "development") return parent.email ?? null;
if (user.id === parent.id) return parent.email ?? null;
return null;
} catch {
return null;
}
},
},
};

export default resolvers;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for imports of resolvers.ts
echo "=== Searching for imports of resolvers.ts ==="
rg -n "from.*resolvers|import.*resolvers" -t ts -t tsx

echo ""
echo "=== Searching for GraphQL server setup patterns ==="
rg -n "ApolloServer|createSchema|makeExecutableSchema|createYoga|buildSchema" -t ts -t tsx -B 2 -A 5

echo ""
echo "=== Searching for getCurrentUser implementation ==="
rg -n "export.*getCurrentUser|function.*getCurrentUser" -t ts -t tsx -A 10

Repository: boundlessfi/bounties

Length of output: 316


🏁 Script executed:

#!/bin/bash
# Check what file types exist
echo "=== File types in repo ==="
find . -type f \( -name "*.ts" -o -name "*.js" -o -name "*.tsx" -o -name "*.jsx" \) | head -20

echo ""
echo "=== Searching for imports of resolvers ==="
rg -n "from.*resolvers|import.*resolvers" --type-list | head -20

rg -n "resolvers" | grep -i "import\|from"

echo ""
echo "=== Searching for GraphQL setup ==="
rg -n "ApolloServer|createSchema|makeExecutableSchema|createYoga|buildSchema"

echo ""
echo "=== Searching for getCurrentUser ==="
rg -n "getCurrentUser" -A 3

Repository: boundlessfi/bounties

Length of output: 10578


🏁 Script executed:

#!/bin/bash
# Get full implementation of getCurrentUser from lib/server-auth.ts
cat -n lib/server-auth.ts

Repository: boundlessfi/bounties

Length of output: 1132


The email resolver is not wired into any GraphQL server, making it completely ineffective.

No imports of resolvers.ts exist anywhere in the codebase, and no GraphQL server setup (ApolloServer, createSchema, etc.) was found. The resolver is defined but never used. Without integration into the server's schema initialization, the email field falls back to the GraphQL runtime's default resolver, which would expose the raw value to all callers.

Additionally, getCurrentUser() at lib/server-auth.ts:16 calls await cookies() from next/headers, which only works inside a Next.js request context. If these resolvers were ever moved to a standalone GraphQL server, this call would throw at runtime.

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

In `@lib/graphql/resolvers.ts` around lines 1 - 30, The BountySubmissionUser.email
resolver is defined but not registered with any GraphQL schema and also depends
on getCurrentUser which uses Next.js cookies() (not safe outside a Next
request); fix by importing and attaching the exported resolvers object
(resolvers, BountySubmissionUser.email) into your GraphQL schema/server
initialization (e.g., when calling makeExecutableSchema/createSchema or
ApolloServer) so it is actually used, and refactor getCurrentUser in
lib/server-auth.ts to accept an explicit request/context (or provide a
context-aware wrapper) instead of calling next/headers cookies() directly so the
resolver can call getCurrentUser(context) from the GraphQL context when
resolving requests.

Comment on lines +36 to +50
if (process.argv.includes("--check")) {
if (!fs.existsSync(dest)) {
console.error(`Destination schema missing: ${dest}`);
process.exit(1);
}
const destContents = fs.readFileSync(dest, "utf8");
if (srcContents !== destContents) {
console.error(
"lib/graphql/schema.graphql is out of sync with ../boundless-nestjs/src/schema.gql",
);
console.error("Run `npm run sync-schema` to update the copied schema.");
process.exit(2);
}
console.log("Schema is in sync.");
process.exit(0);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: --check mode always fails after a successful sync.

The write path (line 57) prepends a header to srcContents, so the destination file contains header + srcContents. However, the --check path on line 42 compares raw srcContents (no header) against destContents (which includes the header). They will never be equal, so npm run check-schema will always report the schema as out-of-sync — breaking the CI workflow.

🐛 Proposed fix — compare with the header included
   const destContents = fs.readFileSync(dest, "utf8");
-  if (srcContents !== destContents) {
+  if (header + srcContents !== destContents) {
     console.error(

This requires moving the header declaration before the --check block:

+const header = `# ------------------------------------------------------\n# GENERATED FILE — DO NOT EDIT DIRECTLY\n# Source: ../boundless-nestjs/src/schema.gql\n# To update, run: npm run sync-schema\n# ------------------------------------------------------\n\n`;
+
 if (process.argv.includes("--check")) {
   if (!fs.existsSync(dest)) {
     console.error(`Destination schema missing: ${dest}`);
     process.exit(1);
   }
   const destContents = fs.readFileSync(dest, "utf8");
-  if (srcContents !== destContents) {
+  if (header + srcContents !== destContents) {
     console.error(
       "lib/graphql/schema.graphql is out of sync with ../boundless-nestjs/src/schema.gql",
     );
     console.error("Run `npm run sync-schema` to update the copied schema.");
     process.exit(2);
   }
   console.log("Schema is in sync.");
   process.exit(0);
 }
 
-// Write header and then the source contents
-const header = `# ...`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/sync-schema.js` around lines 36 - 50, The --check branch is comparing
raw srcContents to destContents while the write path writes header +
srcContents, causing a permanent mismatch; move the header declaration so it
exists before the --check block and change the comparison to build
expectedContents = header + srcContents (or otherwise prepend header to
srcContents) and compare expectedContents to destContents (use the existing
srcContents and destContents variables and keep the same exit codes/log
messages).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Align frontend GraphQL schema with backend

2 participants