Refactor bounty logic and data structures to align with backend schema#112
Conversation
- 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.
|
@Ekene001 is attempting to deploy a commit to the Threadflow Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedReview was skipped due to path filters ⛔ Files ignored due to path filters (1)
CodeRabbit blocks several paths by default. You can override this behavior by explicitly including those paths in the path filters. For example, including You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughAligns 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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
createdByfilter shows bounties the user created, not bounties they claimed.The tab is labelled "My Claims" and uses the
MyClaimtype, but the filterbounty.createdBy === userIdselects bounties authored by this user. WithclaimedByremoved 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:
- Renaming the tab/section to "My Bounties" or "Created Bounties" to match the actual data, or
- 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 | 🟡 MinorAlign
SubmissionStatusandApplicationStatuscasing withBountyStatusandBountyType.
BountyStatusandBountyTypeuse UPPER_CASE enums ("OPEN","IN_PROGRESS","FIXED_PRICE"), butSubmissionStatus(line 16) andApplicationStatus(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 | 🟠 MajorRemove
bounty-detail-requirements-card.tsx— both components are dead code.Neither
RequirementsCardnorScopeCardare imported or used anywhere in the codebase. Since the backend no longer includes requirements/scope on the Bounty model andbounty-detail-client.tsxno 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 | 🟡 MinorNo runtime guard against unknown status/type values.
Both
StatusBadgeandTypeBadgedirectly dereferencecfg.classNamewithout 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 toSTATUS_CONFIG/TYPE_CONFIGyet, 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 | 🟡 MinorTag filter silently has no effect on the bounty tab.
filters.tagsis 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 removetagsfromFilterStatefor 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.statusandBounty.typeareStringinstead of their respective enums — loss of type safety.The generated types reflect the schema where
status: String!andtype: String!instead ofstatus: BountyStatus!andtype: BountyType!. This means GraphQL won't enforce valid enum values at the schema level. The enumsBountyStatusandBountyTypeexist in the schema but aren't used on theBountytype. This issue originates inschema.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 | 🔴 CriticalSidebarCTA opens GitHub instead of claiming bounty — inconsistent with BountySidebar behavior.
SidebarCTAon 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}/claimfor 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) andMobileCTA.label()(lines 131-143) lack explicit cases for DRAFT, SUBMITTED, UNDER_REVIEW, and DISPUTED statuses, falling back to "Not Available"—whereasBountySidebar.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.statusis untyped andrewardCurrencyis absent from the local type.Two separate concerns:
Loose
statustype — usingstringinstead of theBountyStatusenum means invalid values can be passed in silently andnormalizeStatus/section-matching bugs (see above) are not caught at compile time.Missing
rewardCurrency— the PR renamescurrency→rewardCurrencyproject-wide, butMyClaimonly carriesrewardAmount. 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 | 🟡 MinorHardcoded
"$"currency — downstream effect ofrewardCurrencymissing fromMyClaim.Once
rewardCurrencyis added to theMyClaimtype (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 | 🟡 MinorNon-descriptive alt text is an accessibility issue.
alt="Image"is meaningless to screen readers. If this panel is decorative, usealt=""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 | 🟡 MinorMock history entries all share the same
completedAttimestamp, producing identical relative-time labels in the UI.The
CompletionHistorycomponent callsformatDistanceToNow(new Date(record.completedAt))for every row. Because every mock record hascompletedAt: "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_HISTORYoutside 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 | 🟡 MinorUnvalidated string interpolated into URL path may allow path traversal.
addressis typed asstringwith no format enforcement. A value like../../other-routewould 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 | 🟡 MinorStats cards show "$0" fallback values alongside the error alert.
When
statsErroris true,statsisundefined, 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
statsis 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 | 🟡 MinorPII exposure:
BountySubmissionUser.emailfield.The
BountySubmissionUsercarries 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 | 🟡 MinorRemove unused
DifficultyLeveltype definition.The type is not referenced anywhere in the codebase. Since
difficultyhas 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 | 🟡 MinorFloating-point comparison
total === 100may 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.99999999999999instead of100, causing the refinement to reject valid input. If only integer percentages are expected, consider adding.int()to thepercentagefield inmilestoneSchema. 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.COMPLETEDis 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 | 🟡 MinorIssue number
0would be suppressed by the&&short-circuit.
bounty.githubIssueNumber && ...evaluates to0(falsy) when the issue number is0, 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 | 🟡 MinorUnchecked null return from
updateMilestoneParticipation.
BountyStore.updateMilestoneParticipationreturnsnullwhen 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 === 0is falsy, returning a misleading 500 error.If a participation record has
totalMilestones: 0, the!totalMilestonescheck treats it the same asundefined/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 | 🟡 MinorRaw 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_CONFIGinlib/bounty-config.tsandtypeConfigincomponents/bounty/bounty-header.tsx). ImportTYPE_CONFIGfromlib/bounty-config.tsand useTYPE_CONFIG[bounty.type]?.labelfor 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_CONFIGfrom@/lib/bounty-configandBountyTypefrom 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 | 🟡 MinorToast 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) andtoast()(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 | 🟡 MinorUnsafe double-cast
as unknown as (...)bypasses type safety and is unnecessary in graphql-request v7.The
graphql-requestv7 API natively supports passing(document, variables?, requestHeaders?)as positional arguments via theVariablesAndRequestHeadersArgstuple 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.
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
lib/api/reputation.ts (1)
50-54: Nit: cachequery.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.
⛔ Files ignored due to path filters (2)
package-lock.jsonis excluded by!**/package-lock.jsonpnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (2)
lib/api/reputation.tspackage.json
There was a problem hiding this comment.
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 | 🔴 CriticalUpdate
@next/swc-wasm-nodejsversion to match available releases.
nextat^16.1.6is a valid stable release (published Jan 27, 2026). However,@next/swc-wasm-nodejsdoes not have a 16.1.6 release. The latest available version as of February 2026 is15.5.3. Either downgrade@next/swc-wasm-nodejsto^15.5.3or 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 forIN_PROGRESSandCANCELLEDlook correct.The previous review flagged that
CANCELLEDmapped to"completed"andIN_PROGRESSto"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 | 🟡 MinorMissing mapping for
UNDER_REVIEWstatus.The backend schema includes
UNDER_REVIEWas a validBountyStatusvalue (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 andcontributorIdis 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:defaultTypeInfofallback addresses the prior concern nicely.The defensive fallback for unknown
bounty.typevalues 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
Bountyinterface and thetype: stringissue are eliminated by re-exporting canonical types fromtypes/bounty.ts. Consumers oflib/types.tsnow get full type safety includingBountyTypeandBountyStatusenums.,
🤖 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.statusandBounty.typeare stillString!rather than the defined enumsBountyStatus/BountyType; same applies toBountySubmissionType.status(line 82),ReviewSubmissionInput.status(line 179), andUpdateBountyInput.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.emailPII exposure was previously flagged;lib/graphql/resolvers.tshas 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 inlib/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 tobounty.createdBystill targets the wrong person for rating.The primary path via
submission?.submittedByis a good fix, but the fallback chainsubmission?.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:rateContributorstub: consider validating theratingrange.The method accepts any number but the
BountyCompletionRecord.maintainerRatingtype 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 forlimit/offsetare duplicated between the route and the service.Lines 21–22 apply
?? 10/?? 0, butReputationService.getCompletionHistoryalready defaultslimit = 10, offset = 0. Consider passinglimitandoffsetdirectly (possiblyundefined) 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.gqlin the error message, but the actual source could be theCANONICAL_SCHEMAenv var or the in-repo pathboundless-nestjs/src/schema.gql. Consider using the resolvedsrcvariable 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.tshandler logsconsole.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 singleDateinstance 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 outercatchreturns 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.rewardAmountis displayed raw (e.g.,1000 USDC). Other components likemy-claims.tsxuseformatCurrency(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 togithubIssueUrlandrewardAmount.
z.string()accepts any string including empty or non-URL values forgithubIssueUrl, andz.number()accepts zero or negatives forrewardAmount. 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: Preferrouter.refresh()overwindow.location.reload(), and usetoastconsistently for feedback.Three issues in this handler:
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.alert(message)on error (line 70) while usingtoast()on success (line 63) is inconsistent UX. Prefertoast.error(message)so all feedback flows through the same notification system.- Generic success message
"Action completed successfully"— should be"Bounty claimed successfully"or similar.♻️ Proposed fix
Add
useRouterimport 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: emptyclassName=""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.
📒 Files selected for processing (22)
.github/workflows/check-schema.ymlREADME.mdapp/api/bounties/[id]/claim/route.tsapp/api/bounties/[id]/join/route.tsapp/api/bounties/[id]/milestones/advance/route.tsapp/api/bounties/[id]/submit/route.tsapp/api/reputation/[userId]/history/route.tsapp/profile/[userId]/page.tsxcodegen.tscomponents/bounty/bounty-sidebar.tsxcomponents/bounty/github-bounty-card.tsxcomponents/cards/bounty-card.tsxcomponents/reputation/my-claims.tsxlib/api/bounties.tslib/api/reputation.tslib/graphql/resolvers.tslib/graphql/schema.graphqllib/mock-data.tslib/services/reputation.tslib/types.tspackage.jsonscripts/sync-schema.js
🚧 Files skipped from review as they are similar to previous changes (1)
- codegen.ts
| - 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." |
There was a problem hiding this comment.
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.
| 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 }, | ||
| ); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
| const limit = searchParams.get("limit") | ||
| ? Number(searchParams.get("limit")) | ||
| : undefined; | ||
| const offset = searchParams.get("offset") | ||
| ? Number(searchParams.get("offset")) | ||
| : undefined; |
There was a problem hiding this comment.
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.
| 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.
| return bounties | ||
| .filter((bounty) => bounty.claimedBy === userId) | ||
| .filter((bounty) => bounty.createdBy === userId) |
There was a problem hiding this comment.
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.
| 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; |
There was a problem hiding this comment.
🧩 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 10Repository: 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 3Repository: 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.tsRepository: 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.
| 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); |
There was a problem hiding this comment.
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).
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
BountyTypefeature,bug,documentation,refactorFIXED_PRICE,MILESTONE_BASED,COMPETITIONBountyStatusopen,claimed,closedOPEN,IN_PROGRESS,COMPLETED,CANCELLED,DRAFT,SUBMITTED,UNDER_REVIEW,DISPUTEDRemoved Frontend-Only Fields
claimingModel,difficulty,tags,scope,requirementsclaimedBy,claimedAt,claimExpiresAtissueTitle,projectName,projectLogoUrl,githubRepoRenamed Fields
reward→rewardAmountcurrency→rewardCurrencyAdded
organization/projectnested objects (matching backend relations)TYPE_CONFIGinlib/bounty-config.tsfor the 3 bounty typesclaim()method onbountiesApiFiles Changed
Schema & Codegen (2 files)
lib/graphql/schema.graphql— replaced with backend schemacodegen.ts— updateddocumentsglob tolib/graphql/operations/Type Definitions (4 files)
types/bounty.ts— fully rewritten for backend alignmentlib/types.ts— updatedBountyStatustype andBountyinterfacelib/api/bounties.ts— Zod schemas aligned +claimmethod addedlib/bounty-config.ts—STATUS_CONFIG(8 statuses),TYPE_CONFIG(3 types); removedDIFFICULTY_CONFIG,CLAIMING_MODEL_CONFIGUI Components (13 files)
components/bounty/bounty-header.tsxcomponents/bounty/github-bounty-card.tsxcomponents/bounty/bounty-card.tsxcomponents/bounty/bounty-sidebar.tsxcomponents/bounty/bounty-content.tsxcomponents/cards/bounty-card.tsxcomponents/projects/project-bounties.tsxcomponents/bounty-detail/bounty-badges.tsxcomponents/bounty-detail/bounty-detail-client.tsxcomponents/bounty-detail/bounty-detail-header-card.tsxcomponents/bounty-detail/bounty-detail-sidebar-cta.tsxcomponents/bounty-detail/bounty-detail-requirements-card.tsxcomponents/search-command.tsxPages & API Routes (8 files)
app/bounty/page.tsx— filters updated, difficulty/tags filters removedapp/discover/page.tsx— search/sort updatedapp/profile/[userId]/page.tsx— removedclaimedBy/issueTitlereferencesapp/api/bounties/route.ts— removeddifficulty/tagsfiltersapp/api/bounties/[id]/claim/route.tsapp/api/bounties/[id]/submit/route.tsapp/api/bounties/[id]/join/route.tsapp/api/bounties/[id]/competition/join/route.tsapp/api/bounties/[id]/milestones/advance/route.tsLogic & Mock Data (3 files)
lib/logic/bounty-logic.ts— status comparisons updatedlib/mock-bounty.ts— mock data aligned with backend fieldslib/mock-data.ts— mock data aligned with backend fieldsBreaking Changes
Summary by CodeRabbit
New Features
Bug Fixes & Improvements
Changes
Removed