Skip to content

feat: NLP search - #230

Merged
MohamadBakawi merged 3 commits into
mhmdfarhat-mhmdali-amirfrom
feat/ai_features
Aug 6, 2026
Merged

feat: NLP search#230
MohamadBakawi merged 3 commits into
mhmdfarhat-mhmdali-amirfrom
feat/ai_features

Conversation

@MohamadBakawi

@MohamadBakawi MohamadBakawi commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

api key not needed for NLP search and vectorization of projects
add openAI token if you got one to the .env in apps/api OPENAI_API_KEY="sk-your-openai-api-key-here"

Summary by CodeRabbit

  • New Features

    • Added AI-generated repository summaries to GitHub analysis previews.
    • Added semantic search for projects and developer profiles, with keyword fallback.
    • Added AI-assisted profile headline and bio enhancement.
    • Added light, dark, and system theme selection.
  • Improvements

    • Enhanced project descriptions with generated summaries.
    • Improved mobile dashboard tour behavior and responsiveness.
    • Refreshed the visual palette and improved theme consistency, including dark-mode image visibility.
    • Updated developer profile access and account-management endpoints.

feat: AI generated user profile details

feat:  AI GitHub Repository Summarizer
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

AI services now generate repository pitches, profile enhancements, and local embeddings. Projects and developer profiles persist embeddings and use semantic search with keyword fallback. The web app adds theme selection, AI profile enhancement controls, and mobile dashboard-tour updates.

AI and semantic features

Layer / File(s) Summary
AI runtime and module wiring
apps/api/src/ai/*, apps/api/src/app.module.ts, package.json
Adds OpenAI summarization, profile enhancement, and cached local MiniLM embeddings.
Vector database contract
packages/database/prisma/*, docker-compose.yml
Enables PostgreSQL vectors and adds nullable 384-dimensional embeddings.
Repository preview pitch
packages/contracts/src/github/*, apps/api/src/repository-scanner/*
Adds optional AI-generated repository pitch data to preview responses.
Project embeddings and semantic exploration
apps/api/src/projects/projects.service.ts
Persists project embeddings and applies semantic project matching with ranking and keyword fallback.
Profile enhancement and user search
apps/api/src/users/*
Adds profile enhancement routes, profile embeddings, and semantic developer exploration.
Embedded project seed data
packages/database/prisma/seeders/seedProjects.ts
Generates and persists project embeddings during seeding.

Web experience updates

Layer / File(s) Summary
Theme provider and navigation controls
apps/web/app/*, apps/web/components/theme-*, apps/web/components/site-header.tsx, apps/web/components/top-navbar.tsx
Adds class-based light, dark, and system theme selection with updated color tokens.
Mobile dashboard tour behavior
apps/web/components/dashboard-tour.tsx
Synchronizes the tour with mobile navigation and replaces polling with event and animation-frame tracking.
Profile enhancement form
apps/web/components/profile-form.tsx
Adds AI enhancement submission, field updates, status feedback, and submit protection.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ProfileForm
  participant UsersController
  participant UsersService
  participant AiService
  participant OpenAI
  ProfileForm->>UsersController: POST /users/me/enhance
  UsersController->>UsersService: enhanceProfile(userId, headline, bio)
  UsersService->>AiService: enhanceProfile(currentBio, headline, projects)
  AiService->>OpenAI: request structured profile improvements
  OpenAI-->>AiService: return bio and headline
  AiService-->>UsersService: return enhanced profile
  UsersService-->>UsersController: return enhanced profile
  UsersController-->>ProfileForm: update form fields
Loading

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: amzbg

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description mentions NLP search and OpenAI configuration but omits the required sections and testing details. Add the required Description, issue link, Steps to QA, and Screenshots sections, and document how to verify NLP search and vectorization.
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies NLP search, which is a primary change in the pull request.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/ai_features

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint install failed: one or more packages not found in the registry.


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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

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

⚠️ Outside diff range comments (3)
apps/api/src/projects/projects.service.ts (2)

790-861: 🎯 Functional Correctness | 🔴 Critical | 🏗️ Heavy lift

Semantic search breaks pagination and drops filtered results.

The vector query applies LIMIT ${take} OFFSET ${skip} before any other filter. Three defects follow.

  1. totalItems is wrong. where becomes id: { in: projectIds }, and projectIds holds at most take IDs from the current page. count({ where }) therefore returns the page size, not the number of semantic matches. totalPages collapses to 1 and hasNextPage is always false. The client cannot reach page 2 of a semantic result set.

  2. query.userId and query.technology are not applied in the vector query. They are applied afterwards against the already-paginated ID set. A search combined with a technology filter returns fewer than limit rows, or zero rows, while matching projects sit on later vector pages.

  3. query.sort is silently ignored when projectIds is set, because orderBy becomes undefined.

Apply the filters and the count in the vector stage, then paginate. Push userId and technology predicates into the raw query, run a separate COUNT(*) over the same predicate for totalItems, and keep LIMIT/OFFSET for the page slice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/projects/projects.service.ts` around lines 790 - 861, The
semantic-search path in the project listing method paginates IDs before applying
filters and counts only the current page. Update the vector-search stage to
include query.userId and query.technology predicates, run a separate COUNT over
the same semantic and filter predicates for totalItems, then apply LIMIT/OFFSET
only to the page query. Preserve query.sort ordering when loading the resulting
projects, and ensure projectIds represents the paginated slice without
corrupting totalPages or hasNextPage.

71-77: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Provide AiService in the spec before constructing ProjectsService.

The constructor now requires AiService, but every direct new ProjectsService(...) call in projects.service.spec.ts only passes four dependencies. These direct constructors do not receive the sixth required argument, so the spec failures will not come from AiModule resolution; fix the constructor calls instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/projects/projects.service.ts` around lines 71 - 77, Update every
direct ProjectsService construction in projects.service.spec.ts to pass an
AiService instance as the sixth dependency, matching the constructor signature.
Provide the mock or test fixture before constructing ProjectsService; do not
rely on AiModule resolution.
apps/api/src/repository-scanner/github-repository-snapshot.service.ts (1)

98-111: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Validate model output before returning aiPitch.

AiService.summarizeRepository uses JSON.parse on model output without Zod validation. A valid JSON object can still omit required fields or use invalid field types. This method then returns that value as aiPitch, which can violate GithubRepositoryAnalysisPreviewResponse.

Validate the generated object against the contract schema. Convert invalid output to null.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/repository-scanner/github-repository-snapshot.service.ts` around
lines 98 - 111, Validate the result of AiService.summarizeRepository before
assigning it to aiPitch in the repository analysis flow. Use the existing
GithubRepositoryAnalysisPreviewResponse contract schema to parse the generated
object, and convert validation failures to null so the returned response always
satisfies its declared shape.
🧹 Nitpick comments (2)
apps/api/src/projects/projects.service.ts (1)

933-940: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Replace the unsound cast with a type predicate.

filter(Boolean) does not narrow (T | undefined)[] to T[]. The as typeof projects cast at line 940 hides that gap. Use a Map lookup and an explicit type predicate.

♻️ Proposed refactor
-    const sortedProjects = projectIds
-      ? projectIds.map(id => projects.find(p => p.id === id)).filter(Boolean)
-      : projects;
+    const projectsById = new Map(projects.map((p) => [p.id, p]));
+    const sortedProjects = projectIds
+      ? projectIds
+          .map((id) => projectsById.get(id))
+          .filter((p): p is (typeof projects)[number] => p !== undefined)
+      : projects;
-      data: sortedProjects as typeof projects,
+      data: sortedProjects,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/projects/projects.service.ts` around lines 933 - 940, Update the
sortedProjects construction in the project-list method to use a Map-based ID
lookup and filter results with an explicit type predicate, narrowing the
collection to project values without relying on the `as typeof projects` cast.
Remove the cast from the returned data while preserving the requested projectIds
ordering and existing fallback to projects.
apps/web/components/theme-toggle.tsx (1)

26-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use dynamic primary-scale tokens for the icon colors.

text-orange-500 and text-blue-400 bypass the light and dark theme token scales. Replace them with text-primary-base or text-primary-400, as appropriate.

As per coding guidelines, use design tokens defined in globals.css instead of arbitrary color values.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/components/theme-toggle.tsx` around lines 26 - 37, Update the Sun
and Moon icon class names in the theme toggle to use the dynamic primary-scale
design tokens defined in globals.css instead of text-orange-500 and
text-blue-400. Preserve the existing theme-toggle behavior and choose
text-primary-base or text-primary-400 as appropriate for each icon.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/ai/ai.service.ts`:
- Line 39: Update apps/api/src/ai/ai.service.ts at lines 39-39 and 67-67: in the
repository-pitch and enhanced-profile response paths, parse the LLM JSON and
validate each result with its corresponding Zod schema from `@repo/contracts`
before returning it. Preserve the existing safe fallback for repository-pitch
validation failures, and throw the appropriate NestJS exception for
enhanced-profile validation failures.
- Around line 77-81: Update the extractor initialization flow around the
extractor field and transformer pipeline creation to use a shared initialization
promise: assign the promise before awaiting it so concurrent requests reuse the
same in-flight initialization, and clear the promise when initialization fails
so later requests can retry. Preserve the existing dynamic import and successful
extractor assignment behavior.
- Around line 10-14: Update AiService construction to instantiate OpenAI only
when process.env.OPENAI_API_KEY is present, and make the openai member nullable
or optional. Guard all OpenAI usage so repository-summary requests return null
without credentials, while profile-enhancement requests throw the appropriate
NestJS service exception when credentials are missing.

In `@apps/api/src/projects/projects.service.ts`:
- Around line 200-206: Move embedding generation at
apps/api/src/projects/projects.service.ts:200-206 and :528-534 before their
respective this.prisma.$transaction calls, using the already available project
input data and passing the computed vector into the transaction; at :693-699,
remove the awaited generation from the transaction and enqueue a BullMQ job
after the transaction commits, following the repository’s constants → module →
service → processor pattern.
- Line 792: Update the threshold value in the Vector Semantic Search comment
near MAX_COSINE_DISTANCE to 0.78, ensuring the documentation matches the
configured constant.

In `@apps/api/src/repository-scanner/github-repository-snapshot.service.ts`:
- Around line 62-69: Update the return type of the relevant repository snapshot
service method to use Promise<GithubRepositoryAnalysisPreviewResponse> directly.
Remove the inline aiPitch intersection because
GithubRepositoryAnalysisPreviewResponse already defines it, and ensure the
response type is imported from `@repo/contracts`.
- Around line 98-100: Update previewRepositoryAnalysis around the
aiService.summarizeRepository call so AI summarization is enqueued through the
existing BullMQ queue instead of awaited inline. Add the corresponding
job/result completion flow to populate aiPitch asynchronously, while preserving
the controller’s immediate response behavior and using the repository’s
established queue and worker symbols.

In `@apps/api/src/users/users.controller.ts`:
- Around line 23-25: Define profile-enhancement request and response Zod schemas
in `@repo/contracts` with exported inferred types. In
apps/api/src/users/users.controller.ts lines 23-25, apply
ZodValidationPipe(profileEnhanceRequestSchema) to the body and validate the
service result with the response schema before returning it. In
apps/web/components/profile-form.tsx lines 152-155, replace the inline response
shape with the inferred response type imported from `@repo/contracts`.

In `@apps/api/src/users/users.service.ts`:
- Around line 80-85: Update the semantic-search flow around the vector query and
its related total-count logic: join or filter the ranked query to confirmed
users before counting or paginating matches, compute totalItems from the full
confirmed match set, then apply take and skip to the confirmed results. Preserve
the existing relevance ordering and ensure the returned page and totalPages
reflect all confirmed semantic matches.
- Around line 26-31: UsersService is missing the NestJS Logger used by other API
services. Update the UsersService class to initialize a private readonly logger
with Logger(UsersService.name), and use that logger for non-sensitive
operational messages only while keeping the existing Prisma and AiService
dependencies unchanged.
- Around line 350-359: Update the embedding logic in updateProfile to build
combinedText from each searchable field in the merged state, using the incoming
data value when provided and user.developerProfile as the fallback. Only call
generateEmbedding and update DeveloperProfile.embedding when at least one
searchable field changed; leave the existing embedding untouched for unrelated
partial updates such as hasSeenDashboardTour.

In `@apps/web/components/profile-form.tsx`:
- Around line 160-161: Update the catch block in the profile enhancement request
to handle ApiError explicitly and pass its message to toast.error instead of
using the static fallback. Preserve the existing generic fallback for
non-ApiError failures and ensure the caught error is no longer unused.

In `@apps/web/components/theme-toggle.tsx`:
- Line 15: Update the useTheme destructuring in the theme toggle component to
bind only setTheme, removing the unused theme variable.

In `@docker-compose.yml`:
- Line 5: Update the PostgreSQL container image declaration to a pgvector
PostgreSQL 18 tag so it matches the mounted version-specific PGDATA path;
preserve the existing volume configuration.

In `@package.json`:
- Around line 30-32: Move the undeclared imports to the workspaces that actually
use them: add openai to apps/api/package.json for the code path that imports it,
and add `@xenova/transformers` to packages/database/package.json for
packages/database/prisma/seeders/seedProjects.ts. If either import is no longer
needed, remove it instead of relying on the root-level dependency, and keep the
affected workspace manifests as the source of truth.

In `@packages/database/prisma/seeders/seedProjects.ts`:
- Around line 15-18: Update generateLocalEmbedding in seedProjects so model-load
or inference errors are not silently converted into an empty vector; either
propagate the failure to stop the seed run or record it for later reporting.
Then adjust the seeding flow that consumes generateLocalEmbedding to surface the
embedding failures in the final seeding summary instead of always reporting
success, while preserving the existing update path for projects that do receive
embeddings.
- Around line 179-202: Update the project upsert in the seeding flow to populate
publishedAt in both the create and update payloads for PUBLISHED projects.
Assign distinct deterministic dates per seeded entry, reusing the same date for
that entry across create and update, so newest and oldest sorting produce
different orders.
- Around line 245-257: Update the project technology creation loop in the
seeding flow to track each slug’s index, set isPrimary only for the first
technology, and assign the index as an incrementing sortOrder. Preserve the
existing projectId, technologyId, and source values while ensuring subsequent
technologies are non-primary and deterministically ordered.

---

Outside diff comments:
In `@apps/api/src/projects/projects.service.ts`:
- Around line 790-861: The semantic-search path in the project listing method
paginates IDs before applying filters and counts only the current page. Update
the vector-search stage to include query.userId and query.technology predicates,
run a separate COUNT over the same semantic and filter predicates for
totalItems, then apply LIMIT/OFFSET only to the page query. Preserve query.sort
ordering when loading the resulting projects, and ensure projectIds represents
the paginated slice without corrupting totalPages or hasNextPage.
- Around line 71-77: Update every direct ProjectsService construction in
projects.service.spec.ts to pass an AiService instance as the sixth dependency,
matching the constructor signature. Provide the mock or test fixture before
constructing ProjectsService; do not rely on AiModule resolution.

In `@apps/api/src/repository-scanner/github-repository-snapshot.service.ts`:
- Around line 98-111: Validate the result of AiService.summarizeRepository
before assigning it to aiPitch in the repository analysis flow. Use the existing
GithubRepositoryAnalysisPreviewResponse contract schema to parse the generated
object, and convert validation failures to null so the returned response always
satisfies its declared shape.

---

Nitpick comments:
In `@apps/api/src/projects/projects.service.ts`:
- Around line 933-940: Update the sortedProjects construction in the
project-list method to use a Map-based ID lookup and filter results with an
explicit type predicate, narrowing the collection to project values without
relying on the `as typeof projects` cast. Remove the cast from the returned data
while preserving the requested projectIds ordering and existing fallback to
projects.

In `@apps/web/components/theme-toggle.tsx`:
- Around line 26-37: Update the Sun and Moon icon class names in the theme
toggle to use the dynamic primary-scale design tokens defined in globals.css
instead of text-orange-500 and text-blue-400. Preserve the existing theme-toggle
behavior and choose text-primary-base or text-primary-400 as appropriate for
each icon.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: f201ccdb-f971-4ae7-924d-e795f9565cc1

📥 Commits

Reviewing files that changed from the base of the PR and between 16eadba and 30cdd2e.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (23)
  • apps/api/src/ai/ai.module.ts
  • apps/api/src/ai/ai.service.ts
  • apps/api/src/app.module.ts
  • apps/api/src/projects/projects.service.ts
  • apps/api/src/repository-scanner/github-repository-snapshot.service.ts
  • apps/api/src/users/users.controller.ts
  • apps/api/src/users/users.module.ts
  • apps/api/src/users/users.service.ts
  • apps/web/app/globals.css
  • apps/web/app/layout.tsx
  • apps/web/components/dashboard-tour.tsx
  • apps/web/components/profile-form.tsx
  • apps/web/components/site-header.tsx
  • apps/web/components/theme-provider.tsx
  • apps/web/components/theme-toggle.tsx
  • apps/web/components/top-navbar.tsx
  • docker-compose.yml
  • package.json
  • packages/contracts/src/github/github-repository-analysis-preview.response.ts
  • packages/database/prisma/migrations/20260805093227_add_ai_features/migration.sql
  • packages/database/prisma/migrations/20260805115403_change_vector_dimension/migration.sql
  • packages/database/prisma/schema.prisma
  • packages/database/prisma/seeders/seedProjects.ts

Comment on lines +10 to +14
constructor() {
this.openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
});
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
fd -a 'ai.service.ts|package.json|package-lock.json|pnpm-lock.yaml|yarn.lock' . | sed 's#^\./##' | head -200

echo
echo "== ai.service outline =="
ast-grep outline apps/api/src/ai/ai.service.ts || true

echo
echo "== ai.service lines =="
cat - n apps/api/src/ai/ai.service.ts | sed -n '1,140p'

echo
echo "== openai usages =="
rg -n "new OpenAI|OPENAI_API_KEY|generateEmbedding|generate.*summary|enhanc|profile" apps/api/src packages deps 2>/dev/null | head -200

echo
echo "== package versions =="
cat package.json | jq -r '.dependencies["`@repo/contracts`"], .dependencies["`@repo/database`"], .dependencies["`@repo/shared`"], .dependencies["openai"]' 2>/dev/null || true
if [ -f apps/api/package.json ]; then cat apps/api/package.json | jq -r '.dependencies["`@repo/contracts`"], .dependencies["`@repo/database`"], .dependencies["`@repo/shared`"], .dependencies["openai"]' 2>/dev/null || true; fi

Repository: chrisshaddad/bootcamp-starter

Length of output: 4770


🏁 Script executed:

#!/bin/bash
set -euo pipefail

node - <<'JS'
try {
  const packageJson = require('./package.json');
  const apiKey = process.env.OPENAI_API_KEY;
  console.log('OPENAI_API_KEY=', apiKey === undefined ? '<undefined>' : apiKey);
  console.log('openai_version=', packageJson.dependencies && packageJson.dependencies.openai || 'missing');
  const OpenAI = require('openai');
  console.log('OpenAI_default=', OpenAI && OpenAI.default && typeof OpenAI.default);
  console.log('OpenAI_default_exported=', typeof module.exports && typeof require('openai').default);
  let client;
  try {
    client = new OpenAI.default({ apiKey });
    console.log('OpenAI_default_constructor_apiKey=', client.apiKey);
  } catch (err) {
    console.log('OpenAI_default_constructor_error=', err && err.message, err && err.constructor && err.constructor.name);
  }
  try {
    const OpenAINamed = require('openai').OpenAI || OpenAI.default;
    client = new OpenAINamed({ apiKey });
    console.log('OpenAI_named_constructor_apiKey=', client.apiKey);
  } catch (err) {
    console.log('OpenAI_named_constructor_error=', err && err.message, err && err.constructor && err.constructor.name);
  }
} catch (err) {
  console.log('module_probe_error=', err && err.message);
  console.log('stack=', err && err.stack);
}
JS

echo
echo "== usages context =="
rg -n "AiService|summarizeRepository|enhanceProfile|generateEmbedding|AiController|AiModule" apps/api/src -S

echo
echo "== Nest/express startup relevant snippets =="
rg -n "HttpServer|express|fastify|listen\\(|onApplicationBootstrap|onApplicationShutdown" apps/api/src apps -S | head -120

Repository: chrisshaddad/bootcamp-starter

Length of output: 8612


🌐 Web query:

OpenAI Node SDK v7 constructor apiKey undefined behavior no API key

💡 Result:

In the OpenAI Node.js SDK (v7 and other recent versions), the OpenAI client constructor enforces a credential check upon initialization [1][2][3]. If neither apiKey, adminAPIKey, nor workloadIdentity are provided to the constructor, and the corresponding environment variables (such as OPENAI_API_KEY) are not set, the SDK will immediately throw an OpenAIError with the message: "Missing credentials. Please pass an apiKey, workloadIdentity, adminAPIKey, or set the OPENAI_API_KEY or OPENAI_ADMIN_KEY environment variable." [2][3][4]. This validation occurs within the client constructor, meaning the error is thrown before any network request is attempted [1]. Key behaviors to note: - Environment Variable Fallback: If you do not pass an apiKey explicitly in the constructor options, the SDK will automatically attempt to read it from process.env['OPENAI_API_KEY'] [2][3][4]. - Empty Strings: While recent versions of the SDK (following changes in v6.x) treat an explicitly passed empty string apiKey: "" as a missing credential, subsequent fixes have been implemented to handle explicit credentials more consistently [5][1]. - Security: The SDK intentionally restricts browser usage by default to prevent secret leakage, requiring an explicit dangerouslyAllowBrowser: true flag if initialized in a browser-like environment [2][3][6]. To avoid this error, ensure that a valid key is provided in the constructor options or that the appropriate environment variable is configured in your runtime environment [2][7].

Citations:


🌐 Web query:

NestJS provider constructor error startup startup fails before first method call

💡 Result:

When a NestJS application fails at startup during provider instantiation without an explicit error message, it is often due to silent failures during the dependency injection (DI) process, such as circular dependencies or improper provider registration [1][2][3]. To troubleshoot and resolve this, follow these systematic steps: 1. Enable Debug Logging If the error is swallowed, force NestJS to output dependency resolution logs by setting the environment variable NEST_DEBUG=true [4]. This often reveals the specific point in the dependency tree where the instantiation hangs or fails. 2. Check for Circular Dependencies Circular dependencies are a common cause of silent startup hangs or "cannot resolve dependency" errors [5][6]. - Symptom: NestJS cannot determine which provider to instantiate first because they depend on each other [7][6]. - Fix: Use the forwardRef utility in both classes/modules involved [7][6]. - Note: If using circular dependencies with Request-scoped providers, the behavior is highly unpredictable; refactoring to eliminate the dependency cycle is strongly recommended over relying on workarounds [7][8]. 3. Verify Provider Registration Ensure all providers are correctly registered in the providers array of the appropriate module [5][9]. - Common Error: Placing a provider in the imports array instead of the providers array [5][10]. - Visibility: If a service is in a different module, ensure it is exported from its source module and the source module is imported into the consumer module [9][11]. 4. Inspect Constructor Logic - Self-Injection: Ensure a provider is not accidentally injecting itself, as self-injection is not supported [10]. - Logic Errors: If the application terminates silently, check for errors being thrown directly inside class constructors [1][2]. Wrapped exceptions or configuration that disables the logger (e.g., logger: false in NestFactory.create) can hide these startup errors [1]. Ensure your logger is active to see initialization exceptions [1]. 5. Scope Issues - If you use REQUEST scoped providers, be aware that these are only instantiated when a request hits the application [12]. If a provider depends on a request-scoped provider, it becomes request-scoped itself, meaning its constructor will not execute during standard application startup [12]. Top Results: [1], [5], [2], [3], [4], [7], [6]

Citations:


Do not construct OpenAI when the API key is absent.

AiService is registered during app bootstrap, so new OpenAI({}) runs even for embedding-only deployments. The OpenAI SDK requires credentials at construction time, which can prevent the local embedding path from starting. Guard construction with process.env.OPENAI_API_KEY, make openai nullable or optional, and handle optional calls: return null for repository summaries, and throw a NestJS service exception when profile enhancement is requested without credentials.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai/ai.service.ts` around lines 10 - 14, Update AiService
construction to instantiate OpenAI only when process.env.OPENAI_API_KEY is
present, and make the openai member nullable or optional. Guard all OpenAI usage
so repository-summary requests return null without credentials, while
profile-enhancement requests throw the appropriate NestJS service exception when
credentials are missing.

Comment thread apps/api/src/ai/ai.service.ts Outdated
response_format: { type: 'json_object' },
});

return JSON.parse(response.choices[0]?.message?.content || '{}');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Validate LLM output with contract schemas.

JSON.parse only validates JSON syntax. It allows {}, arrays, and wrong field types to pass through both declared response types. Parse each response with its Zod schema from @repo/contracts. Return the existing safe fallback or throw a NestJS exception when validation fails.

  • apps/api/src/ai/ai.service.ts#L39-L39: validate the repository pitch before returning it.
  • apps/api/src/ai/ai.service.ts#L67-L67: validate the enhanced profile before returning it.
📍 Affects 1 file
  • apps/api/src/ai/ai.service.ts#L39-L39 (this comment)
  • apps/api/src/ai/ai.service.ts#L67-L67
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai/ai.service.ts` at line 39, Update
apps/api/src/ai/ai.service.ts at lines 39-39 and 67-67: in the repository-pitch
and enhanced-profile response paths, parse the LLM JSON and validate each result
with its corresponding Zod schema from `@repo/contracts` before returning it.
Preserve the existing safe fallback for repository-pitch validation failures,
and throw the appropriate NestJS exception for enhanced-profile validation
failures.

Source: Coding guidelines

Comment on lines +77 to +81
if (!this.extractor) {
// Dynamic import to support NestJS CommonJS runtime
const { pipeline } = await (eval('import("@xenova/transformers")') as Promise<typeof import('@xenova/transformers')>);
this.extractor = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Make model initialization single-flight.

Concurrent first requests all observe this.extractor === null before any request assigns it. Each request can then load a separate transformer pipeline. This can duplicate model downloads and memory use during cold start. Store the initialization promise before awaiting it. Reset that promise if initialization fails.

🧰 Tools
🪛 Biome (2.5.6)

[error] 79-79: eval() exposes to security risks and performance issues.

(lint/security/noGlobalEval)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai/ai.service.ts` around lines 77 - 81, Update the extractor
initialization flow around the extractor field and transformer pipeline creation
to use a shared initialization promise: assign the promise before awaiting it so
concurrent requests reuse the same in-flight initialization, and clear the
promise when initialization fails so later requests can retry. Preserve the
existing dynamic import and successful extractor assignment behavior.

Comment on lines +200 to +206
// Save Vector Embedding
const textToEmbed = `${title} ${shortDescription || ''} ${fullDescription || ''}`;
const embedding = await this.aiService.generateEmbedding(textToEmbed);
if (embedding.length > 0) {
const vectorString = `[${embedding.join(',')}]`;
await tx.$executeRaw`UPDATE "Project" SET embedding = ${vectorString}::vector WHERE id = ${project.id}`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Embedding generation runs inside three Prisma interactive transactions. Each site awaits this.aiService.generateEmbedding while a transaction is open. The call performs local model inference or an OpenAI request. The transaction holds its written rows and a pooled database connection for that full latency. A cold model load or a slow API response can exceed the Prisma interactive transaction timeout and abort the write with P2028. Concurrent requests multiply the held connections and can exhaust the pool.

The shared fix: compute the embedding before opening the transaction and pass the vector in, or write the embedding from a BullMQ job after the transaction commits. The BullMQ route also matches the repository guideline for async work.

  • apps/api/src/projects/projects.service.ts#L200-L206: move the generateEmbedding call for the imported project above this.prisma.$transaction. The input text is already known from title, shortDescription, and fullDescription computed at lines 128-132.
  • apps/api/src/projects/projects.service.ts#L528-L534: move the generateEmbedding call above this.prisma.$transaction. The input text comes from data, which is available before the transaction starts.
  • apps/api/src/projects/projects.service.ts#L693-L699: the input text depends on the updated row, so enqueue a BullMQ job after the transaction commits instead of awaiting the model call inside it.

Based on the coding guideline "Route async work through BullMQ. Follow the pattern: constants → module → service → processor."

📍 Affects 1 file
  • apps/api/src/projects/projects.service.ts#L200-L206 (this comment)
  • apps/api/src/projects/projects.service.ts#L528-L534
  • apps/api/src/projects/projects.service.ts#L693-L699
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/projects/projects.service.ts` around lines 200 - 206, Move
embedding generation at apps/api/src/projects/projects.service.ts:200-206 and
:528-534 before their respective this.prisma.$transaction calls, using the
already available project input data and passing the computed vector into the
transaction; at :693-699, remove the awaited generation from the transaction and
enqueue a BullMQ job after the transaction commits, following the repository’s
constants → module → service → processor pattern.

Source: Coding guidelines

Comment thread apps/api/src/projects/projects.service.ts
Comment thread docker-compose.yml
services:
postgres:
image: postgres:18
image: pgvector/pgvector:pg16

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== docker-compose files =="
fd -a 'docker-compose.*|compose.*' . | sed 's#^\./##' | sort

echo
echo "== relevant compose snippets =="
for f in $(fd 'docker-compose.*|compose.*' .); do
  echo "--- $f"
  nl -ba "$f" | sed -n '1,80p'
done

echo
echo "== search for postgres image tags / volumes =="
rg -n "pgvector/pgvector|postgres:|#\?|PGDATA|/var/lib/postgresql/18|/var/lib/postgresql/16|data:|volumes:" -S .

Repository: chrisshaddad/bootcamp-starter

Length of output: 331


🌐 Web query:

pgvector pgvector image tags pg18

💡 Result:

The pgvector extension provides official Docker image tags with full support for PostgreSQL 18 [1][2]. As of the latest releases, pgvector supports PostgreSQL 18, including specific fixes for compatibility with its API changes, such as those related to vacuuming functions [3][4][5]. You can access these images on Docker Hub using tags explicitly designated for PostgreSQL 18 [1][6]. Supported variants typically include both Debian-based images, such as trixie and bookworm [1][2]. Example pull commands include: docker pull pgvector/pgvector:pg18 docker pull pgvector/pgvector:pg18-bookworm docker pull pgvector/pgvector:pg18-trixie You can also pull specific versions of the pgvector extension that support PostgreSQL 18 (e.g., version 0.8.6 or newer) by prefixing the tag: docker pull pgvector/pgvector:0.8.6-pg18 The pgvector changelogs confirm that support for PostgreSQL 18 was introduced to ensure correct functionality, including fixes for Index Searches in EXPLAIN output and performance optimizations for Hamming and Jaccard distances specific to the PostgreSQL 18 environment [3][5].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

compose="$(fd 'docker-compose\.yml$' . | head -n 1)"
echo "compose=$compose"

echo "== file section =="
awk 'NR>=1 && NR<=30 {printf "%5d  %s\n", NR, $0}' "$compose"

echo "== focused search =="
awk -v re='(pgvector/pgvector|/var/lib/postgresql/18|volumes:)' '
  $0 ~ re {printf "%5d  %s\n", NR, $0}
' "$compose"

echo "== docker compose config volume targets (read-only if available) =="
if command -v docker >/dev/null 2>&1; then
  docker compose -f "$compose" config --volumes 2>/dev/null || true
else
  echo "docker command not available"
fi

Repository: chrisshaddad/bootcamp-starter

Length of output: 1281


🌐 Web query:

Docker Hub pgvector/pgvector tags pg18 bookworm

💡 Result:

As of August 5, 2026, the official pgvector/pgvector Docker Hub repository provides explicit support and tags for PostgreSQL 18 using the Debian "bookworm" base image [1][2][3]. You can pull the relevant images using the following tags: - pgvector/pgvector:pg18-bookworm [1][2] - pgvector/pgvector:0.8.6-pg18-bookworm [1][2] - pgvector/pgvector:pg18 [1][2] - pgvector/pgvector:0.8.6-pg18 [1][2] These tags are maintained by the pgvector project, with 0.8.6 being the current version providing this support [1][2]. The project also supports "trixie" as an alternative Debian base for Postgres 18 [1][2]. Note that in addition to the standard images, there are also hardened, CIS-compliant images available through the Docker Hardened Images catalog that support PostgreSQL 18 on Debian 13 [4].

Citations:


🌐 Web query:

PostgreSQL initdb directory version mismatch major version data directory cannot be used

💡 Result:

The error message stating that your data directory is incompatible or cannot be used indicates that you are attempting to use a PostgreSQL data directory initialized by a different major version of the software [1][2][3][4]. PostgreSQL major versions have different internal storage formats and are not backward-compatible [2][5]. If you encounter this error while running initdb, it is because the utility is designed to protect existing data and will refuse to initialize a directory that is not empty [6][7][8]. To resolve this, choose one of the following approaches based on your needs: 1. If you do not need the existing data: You must clear the data directory or point your configuration to a new, empty directory [6][7][4]. PostgreSQL will then initialize a fresh database cluster compatible with the installed server version [2][9][4]. 2. If you need to keep the existing data: You cannot simply point a new PostgreSQL major version to an old data directory [1][2][5]. You must perform a formal upgrade of your data [5][10]: - Use pg_upgrade: This is the recommended tool for upgrading between major versions, as it can transfer data from the old directory to a new one efficiently [11][5][4]. - Use pg_dumpall: Perform a logical backup (export) of all your data from the old version, initialize a new, empty cluster with the new version, and restore your data using psql or the appropriate restore tools [2][5][4]. - Temporary Rollback: If you are in an emergency, you can pin your server version back to the older version to restore access to your data, then plan a formal migration path [2][8][4]. Special Note on Docker: If you are using the official PostgreSQL Docker images, note that recent versions (18+) have changed the default PGDATA path to a version-specific subdirectory (e.g., /var/lib/postgresql/18/docker) to make major version upgrades easier [11][2]. If you are migrating a legacy setup that used /var/lib/postgresql/data, you may need to adjust your volume mounts to match this new structure [11][2][4].

Citations:


🌐 Web query:

Docker PostgreSQL image 16 default Dockerfile PGDATA

💡 Result:

For the official PostgreSQL 16 Docker image, the default PGDATA directory is /var/lib/postgresql/data [1][2][3]. In the Dockerfile for PostgreSQL 16, this is explicitly set via the environment variable: ENV PGDATA /var/lib/postgresql/data [1][2][3] It is important to note that this path is specific to PostgreSQL versions 17 and below [4][5][6]. In PostgreSQL 18 and later, the PGDATA location and VOLUME structure were changed to be version-specific (e.g., /var/lib/postgresql/18/docker) to facilitate easier database upgrades using pg_upgrade --link [4][5][6]. For PostgreSQL 16, you must mount your data volume specifically at /var/lib/postgresql/data [4][7][6]. Mounting at the parent directory (/var/lib/postgresql) will not correctly persist the database files because the Dockerfile declares /var/lib/postgresql/data as a VOLUME [1][7][6]. Failure to mount at the correct path will cause data to be written to an anonymous volume that does not persist across container recreations [4][7][6].

Citations:


🌐 Web query:

pgvector/pgvector Dockerfile pg16 PGDATA /var/lib/postgresql/data

💡 Result:

In the official pgvector/pgvector Docker image for PostgreSQL 16, the PGDATA environment variable is set to /var/lib/postgresql/data [1]. This is the standard path used by the official PostgreSQL image upon which pgvector is based [1][2]. Key technical details: - Default PGDATA: The official pgvector:pg16 image explicitly defines ENV PGDATA=/var/lib/postgresql/data [1]. - Persistence: Because /var/lib/postgresql/data is declared as a VOLUME in the Dockerfile [1], you should mount a named volume or host directory to this path to ensure your database files persist across container restarts [3]. - Best Practices: While you can override PGDATA by setting the environment variable in your docker run command or docker-compose file, it is generally recommended to stick with the default /var/lib/postgresql/data unless you have a specific requirement to change it [4]. When mounting volumes, ensure you target the same path defined in the container to avoid creating anonymous volumes [3]. Please note that newer PostgreSQL versions and some specialized "hardened" images may use version-specific paths (e.g., /var/lib/postgresql/18/data) to facilitate upgrades [5][3][6], but the standard pgvector/pgvector:pg16 tag continues to use the legacy /var/lib/postgresql/data structure [1].

Citations:


Align the PostgreSQL image version with the mounted data path.

pgvector/pgvector:pg16 uses PGDATA=/var/lib/postgresql/data, but this volume is mounted at PostgreSQL 18’s version-specific path. PostgreSQL 16 also rejects a PostgreSQL 18 data directory. Use a pgvector PostgreSQL 18 image, or migrate the data and mount it at /var/lib/postgresql/data.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docker-compose.yml` at line 5, Update the PostgreSQL container image
declaration to a pgvector PostgreSQL 18 tag so it matches the mounted
version-specific PGDATA path; preserve the existing volume configuration.

Comment thread package.json
Comment on lines +30 to +32
"dependencies": {
"@xenova/transformers": "^2.17.2",
"openai": "^7.4.0"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

fd -HI '^package\.json$' . -x sh -c '
  echo "== $1 =="
  jq "{name, dependencies, devDependencies}" "$1"
' sh

rg -n -C2 --glob '*.ts' \
  "from ['\"]openai['\"]|`@xenova/transformers`" \
  apps packages

Repository: chrisshaddad/bootcamp-starter

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate package.json files (top-level workspaces only) =="
git ls-files '*/package.json' 'package.json' \
  | grep -E '^(package\.json|apps/[^/]*/package\.json|packages/[^/]*/package\.json)$' \
  || true

echo "== root dependency entries =="
if [ -f package.json ]; then
  jq '{name, dependencies, devDependencies}' package.json
fi

echo "== workspace package dependency entries =="
for f in $(git ls-files 'apps/*/package.json' 'packages/*/package.json' | sort -u); do
  echo "-- $f --"
  jq '{name, dependencies, devDependencies, scripts}' "$f"
done

echo "== TypeScript import usages outside node_modules =="
rg -n -C1 --glob '*.ts' \
  "from ['\"]`@xenova/transformers`['\"]|import \\(['\"]`@xenova/transformers`['\"]|from ['\"]openai['\"]|import \\(['\"]openai['\"]" \
  apps packages package.json 2>/dev/null | head -n 200

Repository: chrisshaddad/bootcamp-starter

Length of output: 6106


Declare direct dependencies in the consuming workspaces.

apps/api/package.json imports openai but only declares it at the root. packages/database/package.json runs prisma db seed from packages/database/prisma/seeders/seedProjects.ts, which imports @xenova/transformers, but does not declare it. Add openai to apps/api and @xenova/transformers to packages/database, or remove the unused imports before relying on hoisted dependencies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` around lines 30 - 32, Move the undeclared imports to the
workspaces that actually use them: add openai to apps/api/package.json for the
code path that imports it, and add `@xenova/transformers` to
packages/database/package.json for
packages/database/prisma/seeders/seedProjects.ts. If either import is no longer
needed, remove it instead of relying on the root-level dependency, and keep the
affected workspace manifests as the source of truth.

Comment on lines +15 to +18
} catch (error) {
console.warn('Could not generate local vector embedding:', error);
return [];
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Do not swallow embedding failures silently.

If the model load or the inference fails, generateLocalEmbedding returns []. The caller at line 207 then skips the UPDATE, so the project keeps embedding = NULL. A project with a NULL embedding never matches the cosine-distance query in exploreProjects. The seeder still prints 5 Distinct projects seeded with vectors. at line 261, so the operator has no signal that semantic search will not work on the seeded data.

Track the failures and report them in the final message, or fail the seed run.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/database/prisma/seeders/seedProjects.ts` around lines 15 - 18,
Update generateLocalEmbedding in seedProjects so model-load or inference errors
are not silently converted into an empty vector; either propagate the failure to
stop the seed run or record it for later reporting. Then adjust the seeding flow
that consumes generateLocalEmbedding to surface the embedding failures in the
final seeding summary instead of always reporting success, while preserving the
existing update path for projects that do receive embeddings.

Comment thread packages/database/prisma/seeders/seedProjects.ts
Comment on lines +245 to +257
for (const slug of item.techSlugs) {
const techId = techMap[slug];
if (techId) {
await prisma.projectTechnology.create({
data: {
projectId: project.id,
technologyId: techId,
source: 'MANUAL',
isPrimary: true,
},
});
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Do not mark every technology as primary.

Each projectTechnology row is created with isPrimary: true and no sortOrder. exploreProjects orders technologies by [{ isPrimary: 'desc' }, { sortOrder: 'asc' }]. With every row primary and every sortOrder at the same default, the displayed technology order is arbitrary and the primary badge loses meaning in the seeded data.

Mark only the first slug as primary and assign an incrementing sortOrder.

🐛 Proposed fix
-    for (const slug of item.techSlugs) {
-      const techId = techMap[slug];
-      if (techId) {
-        await prisma.projectTechnology.create({
-          data: {
-            projectId: project.id,
-            technologyId: techId,
-            source: 'MANUAL',
-            isPrimary: true,
-          },
-        });
-      }
-    }
+    for (const [sortOrder, slug] of item.techSlugs.entries()) {
+      const techId = techMap[slug];
+      if (techId) {
+        await prisma.projectTechnology.create({
+          data: {
+            projectId: project.id,
+            technologyId: techId,
+            source: 'MANUAL',
+            isPrimary: sortOrder === 0,
+            sortOrder,
+          },
+        });
+      }
+    }
📝 Committable suggestion

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

Suggested change
for (const slug of item.techSlugs) {
const techId = techMap[slug];
if (techId) {
await prisma.projectTechnology.create({
data: {
projectId: project.id,
technologyId: techId,
source: 'MANUAL',
isPrimary: true,
},
});
}
}
for (const [sortOrder, slug] of item.techSlugs.entries()) {
const techId = techMap[slug];
if (techId) {
await prisma.projectTechnology.create({
data: {
projectId: project.id,
technologyId: techId,
source: 'MANUAL',
isPrimary: sortOrder === 0,
sortOrder,
},
});
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/database/prisma/seeders/seedProjects.ts` around lines 245 - 257,
Update the project technology creation loop in the seeding flow to track each
slug’s index, set isPrimary only for the first technology, and assign the index
as an incrementing sortOrder. Preserve the existing projectId, technologyId, and
source values while ensuring subsequent technologies are non-primary and
deterministically ordered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (3)
apps/api/src/users/users.controller.ts (1)

14-20: 🔒 Security & Privacy | 🟡 Minor | ⚡ Quick win

Remove dashboard state from the public explore response.

GET /users/explore is public and returns getSafeSelect(). That selection includes hasSeenDashboardTour. Use a public response select and schema that excludes this user-specific state. Keep it only in the authenticated current-user response.

As per coding guidelines, define every wire response shape as a Zod schema in packages/contracts/src/<resource>/.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/users/users.controller.ts` around lines 14 - 20, Update the
public explore response flow around UsersController.exploreUsers and
UsersService.exploreUsers to use a dedicated public user select and Zod response
schema that excludes hasSeenDashboardTour; define the schema under
packages/contracts/src/<resource>/ and retain that field only for the
authenticated current-user response.

Source: Coding guidelines

apps/api/src/projects/projects.service.ts (2)

697-703: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Clear a stale embedding when regeneration fails.

If generateEmbedding returns [], this path keeps the old embedding after title or description changes. Semantic search can then rank the project by obsolete content.

Set embedding to NULL when generation fails, or mark it stale before an asynchronous regeneration job runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/projects/projects.service.ts` around lines 697 - 703, Update the
embedding persistence logic after aiService.generateEmbedding so an empty result
clears the existing Project.embedding value by setting it to NULL. Preserve the
current vector update for non-empty embeddings and ensure both operations target
updated.id within the existing transaction.

803-810: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Apply active filters before vector pagination.

When query.userId or query.technology is set, this query ranks and paginates all published projects first. The later Prisma where clause can remove every selected ID even when matching projects exist beyond this page.

Add the user and technology restrictions to the vector query before LIMIT and OFFSET.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/projects/projects.service.ts` around lines 803 - 810, Update the
raw vector query in the projects search method to apply the active query.userId
and query.technology restrictions alongside the published and embedding
conditions before ORDER BY, LIMIT, and OFFSET. Reuse the existing filter
semantics and parameterization so vector pagination operates only on eligible
projects, while preserving unrestricted behavior when either filter is unset.
🧹 Nitpick comments (1)
apps/api/src/ai/ai.service.ts (1)

103-105: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Remove both eval() wrappers.

These imports do not depend on dynamic code derivation, so use direct dynamic import instead.

  • apps/api/src/ai/ai.service.ts#L103-L105: replace the eval() expression with await import('@xenova/transformers').
  • packages/database/prisma/seeders/seedProjects.ts#L20-L22: replace the matching eval() expression with await import('@xenova/transformers').
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai/ai.service.ts` around lines 103 - 105, Remove the eval()
wrappers around the transformers imports: in apps/api/src/ai/ai.service.ts lines
103-105 and packages/database/prisma/seeders/seedProjects.ts lines 20-22, update
each matching import expression to use await import('`@xenova/transformers`')
directly while preserving the existing pipeline destructuring and behavior.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/ai/ai.service.ts`:
- Around line 93-95: Update the catch block in enhanceProfile() to import and
throw ServiceUnavailableException after logging, instead of rethrowing the raw
error. Preserve the existing error log and ensure failures from OpenAI, missing
response content, and JSON parsing all map to this NestJS exception.

---

Outside diff comments:
In `@apps/api/src/projects/projects.service.ts`:
- Around line 697-703: Update the embedding persistence logic after
aiService.generateEmbedding so an empty result clears the existing
Project.embedding value by setting it to NULL. Preserve the current vector
update for non-empty embeddings and ensure both operations target updated.id
within the existing transaction.
- Around line 803-810: Update the raw vector query in the projects search method
to apply the active query.userId and query.technology restrictions alongside the
published and embedding conditions before ORDER BY, LIMIT, and OFFSET. Reuse the
existing filter semantics and parameterization so vector pagination operates
only on eligible projects, while preserving unrestricted behavior when either
filter is unset.

In `@apps/api/src/users/users.controller.ts`:
- Around line 14-20: Update the public explore response flow around
UsersController.exploreUsers and UsersService.exploreUsers to use a dedicated
public user select and Zod response schema that excludes hasSeenDashboardTour;
define the schema under packages/contracts/src/<resource>/ and retain that field
only for the authenticated current-user response.

---

Nitpick comments:
In `@apps/api/src/ai/ai.service.ts`:
- Around line 103-105: Remove the eval() wrappers around the transformers
imports: in apps/api/src/ai/ai.service.ts lines 103-105 and
packages/database/prisma/seeders/seedProjects.ts lines 20-22, update each
matching import expression to use await import('`@xenova/transformers`') directly
while preserving the existing pipeline destructuring and behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8aeedd8d-eaa3-4d76-adac-38b671ffe3f2

📥 Commits

Reviewing files that changed from the base of the PR and between 30cdd2e and 316c6bb.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (13)
  • apps/api/src/ai/ai.module.ts
  • apps/api/src/ai/ai.service.ts
  • apps/api/src/app.module.ts
  • apps/api/src/projects/projects.service.ts
  • apps/api/src/repository-scanner/github-repository-snapshot.service.ts
  • apps/api/src/users/users.controller.ts
  • apps/api/src/users/users.module.ts
  • apps/api/src/users/users.service.ts
  • apps/web/components/profile-form.tsx
  • apps/web/components/theme-toggle.tsx
  • package.json
  • packages/contracts/src/github/github-repository-analysis-preview.response.ts
  • packages/database/prisma/seeders/seedProjects.ts
🚧 Files skipped from review as they are similar to previous changes (8)
  • apps/api/src/ai/ai.module.ts
  • apps/api/src/app.module.ts
  • apps/web/components/theme-toggle.tsx
  • apps/api/src/users/users.module.ts
  • packages/contracts/src/github/github-repository-analysis-preview.response.ts
  • package.json
  • apps/api/src/repository-scanner/github-repository-snapshot.service.ts
  • apps/web/components/profile-form.tsx

Comment on lines +93 to +95
} catch (error) {
this.logger.error('Failed to enhance profile with OpenAI', error);
throw error;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate ai.service.ts =="
fd -a 'ai\.service\.ts$' . || true

echo "== file outline =="
file="$(fd 'ai\.service\.ts$' . | head -n 1)"
if [ -n "${file:-}" ]; then
  ast-grep outline "$file" --view expanded || true
  echo "== relevant lines 1-150 =="
  sed -n '1,150p' "$file" | cat -n
fi

echo "== NestJS exception imports/usages in api services/controllers if available =="
rg -n "ServiceUnavailableException|Catch|`@Catch`|`@Throws`|throw new .*Exception|console\.log|new Logger" apps/api/src packages/contracts 2>/dev/null | head -n 200 || true

echo "== package nest deps =="
for f in package.json apps/api/package.json; do
  if [ -f "$f" ]; then
    echo "-- $f --"
    jq -r '.dependencies += .devDependencies? // {} | with_entries(select(.key|test("`@nestjs/`"))) | . as $x | {dependencies: $x}' "$f" 2>/dev/null || cat "$f"
  fi
done

Repository: chrisshaddad/bootcamp-starter

Length of output: 27059


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== package JSON files mentioning `@nestjs/common` =="
rg -n '"`@nestjs/common`"|"`@nestjs/microservices`"|bullmq' -g 'package.json' .

echo "== lockfile relevant entries, if any =="
for f in package-lock.json pnpm-lock.yaml yarn.lock bun.lockb; do
  if [ -e "$f" ]; then
    echo "-- $f --"
    if [ "$f" = "bun.lockb" ]; then
      echo "binary lockfile"
    else
      rg -n "(`@nestjs/common`|ServiceUnavailableException)" "$f" | head -n 40 || true
    fi
  fi
done

echo "== structural catch/import verifier =="
python3 - <<'PY'
from pathlib import Path
p = Path('apps/api/src/ai/ai.service.ts')
text = p.read_text()
expected_in_file = [
    'import { Injectable, Logger }',
    'async enhanceProfile',
    '} catch (error)',
    'this.logger.error(\'Failed to enhance profile with OpenAI\', error);',
    'throw error;',
]
for segment in expected_in_file:
    print(f'MISSING={segment!r}' if segment not in text else f'FOUND={segment!r}')
# Basic semantic check: every bare throw inside enhanceProfile catch is not a Nestjs exception constructor.
import re
m = re.search(r'async enhanceProfile[\s\S]*?^[}\s]*$', text, re.M)
if m:
    block = m.group(0)
    catches = re.findall(r'}\ caught error[\s\S]*?'}\ caught error', block)
    print('enhanceProfile catch blocks:', re.findall(r'\}\s*catch\s*\(([^)]+)\)[\s\S]*?^\s*\}', block, re.M))
    throws = re.findall(r'throw\s+([^;\n]+)', block)
    print('enhanceProfile throw statements:', throws)
else:
    print('enhanceProfile block not found')
PY

Repository: chrisshaddad/bootcamp-starter

Length of output: 1275


Map OpenAI and parsing errors to a NestJS exception.

enhanceProfile() catches OpenAI failures, missing response content, and JSON.parse failures, then rethrows them as raw errors. Import ServiceUnavailableException and throw it after logging instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/ai/ai.service.ts` around lines 93 - 95, Update the catch block
in enhanceProfile() to import and throw ServiceUnavailableException after
logging, instead of rethrowing the raw error. Preserve the existing error log
and ensure failures from OpenAI, missing response content, and JSON parsing all
map to this NestJS exception.

Source: Coding guidelines

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
apps/api/src/users/users.service.ts (1)

76-109: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Move profile enhancement off the HTTP request path.

Line 109 waits for an external OpenAI request before the controller can respond. A slow provider response keeps the HTTP request open and reduces API capacity.

Queue this work through BullMQ. Use the required constants, module, service, and processor pattern. Return a job result through a defined contract.

As per coding guidelines, “Route async work through BullMQ. Follow the pattern: constants → module → service → processor. See src/mail/ for the canonical example.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/users/users.service.ts` around lines 76 - 109, Refactor
enhanceProfile so the controller no longer awaits aiService.enhanceProfile:
enqueue the profile data and projectsForAi through a BullMQ queue using the
required queue constants, module, service, and processor pattern established by
src/mail. Define and use a job payload/result contract, return the queued job
result or identifier through that contract, and move the OpenAI call into the
processor.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/api/src/users/users.service.ts`:
- Around line 207-238: Remove the 100-row caps from the keyword and vector
candidate queries used by the search method, and calculate pagination from the
complete confirmed union in combinedIds. Preserve keyword precedence and vector
relevance while applying query.limit and offset only after deduplication, using
a database-side CTE or equivalent approach to avoid unbounded application-memory
materialization.

---

Outside diff comments:
In `@apps/api/src/users/users.service.ts`:
- Around line 76-109: Refactor enhanceProfile so the controller no longer awaits
aiService.enhanceProfile: enqueue the profile data and projectsForAi through a
BullMQ queue using the required queue constants, module, service, and processor
pattern established by src/mail. Define and use a job payload/result contract,
return the queued job result or identifier through that contract, and move the
OpenAI call into the processor.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 50ff8259-ce77-49e4-8fa4-54e9827080b3

📥 Commits

Reviewing files that changed from the base of the PR and between 316c6bb and 8640c99.

📒 Files selected for processing (3)
  • apps/api/src/ai/ai.service.ts
  • apps/api/src/users/users.controller.ts
  • apps/api/src/users/users.service.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • apps/api/src/ai/ai.service.ts
  • apps/api/src/users/users.controller.ts

Comment on lines +207 to +238
take: 100,
});

const keywordIds = keywordMatches.map((m) => m.id);

// 3. AI Vector Matches (Lower Priority)
let vectorIds: string[] = [];
if (cleanedText.length > 0) {
try {
const embedding = await this.aiService.generateEmbedding(cleanedText);
if (embedding.length > 0) {
const vectorString = `[${embedding.join(',')}]`;
const matches = await this.prisma.$queryRaw<{ userId: string }[]>`
SELECT dp."userId" FROM "DeveloperProfile" dp
JOIN "User" u ON dp."userId" = u.id
WHERE dp.embedding IS NOT NULL
AND u."isConfirmed" = true
AND (dp.embedding <=> ${vectorString}::vector) < ${MAX_COSINE_DISTANCE}
ORDER BY dp.embedding <=> ${vectorString}::vector
LIMIT 100
`;
vectorIds = matches.map((m) => m.userId);
}
} catch (error) {
// Fallback gracefully
}
}

// Combine IDs (Keywords first, then broader vector matches)
const combinedIds = Array.from(new Set([...keywordIds, ...vectorIds]));
const totalItems = combinedIds.length;
const totalPages = Math.ceil(totalItems / query.limit);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Do not cap candidate IDs before calculating pagination.

Line 207 limits keyword matches to 100. Line 226 limits vector matches to 100. Lines 236-238 then calculate totalItems from those truncated sets.

For example, 101 keyword matches report only 100 results, so the last result is inaccessible. Count and rank the complete confirmed match set, then apply pagination after the union. A database CTE can preserve keyword precedence and vector relevance without materializing an unbounded result set in application memory.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/users/users.service.ts` around lines 207 - 238, Remove the
100-row caps from the keyword and vector candidate queries used by the search
method, and calculate pagination from the complete confirmed union in
combinedIds. Preserve keyword precedence and vector relevance while applying
query.limit and offset only after deduplication, using a database-side CTE or
equivalent approach to avoid unbounded application-memory materialization.

@MohamadBakawi
MohamadBakawi merged commit eed0175 into mhmdfarhat-mhmdali-amir Aug 6, 2026
2 checks passed
@MohamadBakawi
MohamadBakawi deleted the feat/ai_features branch August 6, 2026 18:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants