Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
514 changes: 513 additions & 1 deletion README.md

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions apps/backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,18 @@ FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./
COPY apps/backend/package*.json ./
RUN npm ci

COPY . .
COPY apps/backend/ .
RUN npm run build

FROM node:20-alpine AS runner

WORKDIR /app
ENV NODE_ENV=production

COPY package*.json ./
COPY apps/backend/package*.json ./
RUN npm ci --only=production

COPY --from=builder /app/dist ./dist
Expand Down
91 changes: 91 additions & 0 deletions apps/backend/packages/shared-types/os-finder.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
export type Difficulty = 'beginner' | 'intermediate' | 'advanced';
export type ContributionType = 'bug_fix' | 'feature' | 'documentation' | 'tests' | 'i18n' | 'performance' | 'ui_design' | 'security';
export type Domain = 'web' | 'devtools' | 'ai_ml' | 'mobile' | 'data' | 'infrastructure' | 'education' | 'games' | 'finance';
export type RepoSize = 'small' | 'medium' | 'large' | 'any';
export type LanguageMode = 'strict' | 'any_of';
export type SavedRepoStatus = 'saved' | 'contributed' | 'skipped';

export interface NCFScoreBreakdown {
total: number;
goodFirstIssue: number;
helpWanted: number;
contributingFile: number;
issueResponseTime: number;
newContribPR: number;
readmeQuality: number;
codeOfConduct: number;
prMergeRate: number;
}

export interface RepoHealthFlags {
isArchived: boolean;
isStale: boolean;
noContributing: boolean;
noReadme: boolean;
lowPRMergeRate: boolean;
forkHeavy: boolean;
noExternalContribs: boolean;
lowIssueEngagement: boolean;
slowMaintainerResp: boolean;
}

export interface OsFinderRepoResult {
githubRepoId: number;
owner: string;
name: string;
fullName: string;
description: string | null;
language: string | null;
stars: number;
forks: number;
openIssues: number;
lastCommitAt: string;
licenseType: string | null;
htmlUrl: string;
ncfScore: NCFScoreBreakdown;
langMatchScore: number; // 0.0 to 1.0
openLabels: string[];
healthFlags: RepoHealthFlags;
isSaved: boolean;
alreadyContrib: boolean;
}

export interface OsFinderFilters {
// Language
languages: string[]; // default: user's top 3 from DevPulse commit data
languageMode: 'strict' | 'any_of'; // default: 'any_of'

// Difficulty (if not set, auto-detect from user's DevPulse data)
difficulty?: 'beginner' | 'intermediate' | 'advanced';

// Contribution type (multi-select)
contributionTypes: ContributionType[];

// Domain (multi-select — maps to GitHub topic tags)
domains: Domain[];

// Repo size by stars
repoSize: RepoSize;

// Activity filters
lastCommitDays: number; // default 90 — exclude repos with no commits in N days
minOpenIssues: number; // default 3 — repo must have at least N open issues
issueFreshDays: number; // default 60 — issues must be created/updated within N days

// Health filters
hasContributing: boolean; // default true — must have CONTRIBUTING.md
hasCodeOfConduct: boolean; // default false — optional filter
licenseTypes: string[]; // default [] (any) — e.g. ['MIT', 'Apache-2.0']
prMergeRate: number; // default 30 — minimum % of PRs merged
includeAlreadyContributed?: boolean;
}

export interface OsFinderSearchResponse {
results: OsFinderRepoResult[];
total: number;
page: number;
filtersApplied: OsFinderFilters;
filtersRelaxed: Partial<OsFinderFilters> | null;
relaxationNote: string | null;
aiModeUsed: boolean;
}
2 changes: 2 additions & 0 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { DigestsModule } from './digests/digests.module';
import { CacheModule } from './common/cache/cache.module';
import { AnalyticsModule } from './analytics/analytics.module';
import { RealtimeModule } from './realtime/realtime.module';
import { OsFinderModule } from './os-finder/os-finder.module';

@Module({
imports: [
Expand All @@ -35,6 +36,7 @@ import { RealtimeModule } from './realtime/realtime.module';
DigestsModule,
AnalyticsModule,
RealtimeModule,
OsFinderModule,
],
controllers: [AppController],
providers: [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class CreateOsFinderTables1779200000000 implements MigrationInterface {
name = 'CreateOsFinderTables1779200000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE "saved_repos" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"user_id" uuid NOT NULL,
"github_repo_id" bigint NOT NULL,
"owner" character varying NOT NULL,
"name" character varying NOT NULL,
"full_name" character varying NOT NULL,
"description" text,
"language" character varying,
"stars" integer NOT NULL DEFAULT 0,
"forks" integer NOT NULL DEFAULT 0,
"open_issues" integer NOT NULL DEFAULT 0,
"ncf_score" double precision,
"lang_match_score" double precision,
"last_commit_at" TIMESTAMP,
"has_contributing" boolean DEFAULT false,
"license_type" character varying,
"html_url" character varying NOT NULL,
"saved_at" TIMESTAMP NOT NULL DEFAULT now(),
"notes" text,
"status" character varying NOT NULL DEFAULT 'saved',
CONSTRAINT "PK_saved_repos_id" PRIMARY KEY ("id"),
CONSTRAINT "UQ_saved_repos_user_github" UNIQUE ("user_id", "github_repo_id"),
CONSTRAINT "FK_saved_repos_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE
)
`);

await queryRunner.query(`
CREATE INDEX "IDX_saved_repos_user" ON "saved_repos" ("user_id")
`);

await queryRunner.query(`
CREATE TABLE "os_finder_searches" (
"id" uuid NOT NULL DEFAULT uuid_generate_v4(),
"user_id" uuid NOT NULL,
"query_text" text,
"filters_applied" jsonb NOT NULL,
"result_count" integer,
"ai_query_used" boolean NOT NULL DEFAULT false,
"github_query" text,
"created_at" TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT "PK_os_finder_searches_id" PRIMARY KEY ("id"),
CONSTRAINT "FK_os_finder_searches_user" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE
)
`);

await queryRunner.query(`
CREATE INDEX "idx_os_finder_searches_user" ON "os_finder_searches" ("user_id", "created_at" DESC)
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS "idx_os_finder_searches_user"`);
await queryRunner.query(`DROP TABLE IF EXISTS "os_finder_searches"`);
await queryRunner.query(`DROP INDEX IF EXISTS "IDX_saved_repos_user"`);
await queryRunner.query(`DROP TABLE IF EXISTS "saved_repos"`);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

export class AlterSavedReposNcfScore1779200000001 implements MigrationInterface {
name = 'AlterSavedReposNcfScore1779200000001';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "saved_repos"
ALTER COLUMN "ncf_score" TYPE jsonb
USING CASE
WHEN ncf_score IS NULL THEN NULL
ELSE jsonb_build_object('total', ncf_score)
END
`);
}

public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "saved_repos"
ALTER COLUMN "ncf_score" TYPE double precision
USING (ncf_score->>'total')::double precision
`);
}
}
15 changes: 14 additions & 1 deletion apps/backend/src/database/typeorm.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,26 @@ import { PullRequest } from '../github-sync/entities/pull-request.entity';
import { Post } from '../posts/entities/post.entity';
import { Comment } from '../comments/entities/comment.entity';
import { Digest } from '../digests/entities/digest.entity';
import { SavedRepo } from '../os-finder/entities/saved-repo.entity';
import { OsFinderSearch } from '../os-finder/entities/os-finder-search.entity';

export const createTypeOrmOptions = (): TypeOrmModuleOptions => ({
type: 'postgres',
url: process.env.DATABASE_URL,
entities: [User, Repository, Commit, PullRequest, Post, Comment, Digest],
entities: [
User,
Repository,
Commit,
PullRequest,
Post,
Comment,
Digest,
SavedRepo,
OsFinderSearch,
],
migrations: [join(__dirname, './migrations/*{.ts,.js}')],
migrationsRun: true,
synchronize: process.env.DATABASE_SYNCHRONIZE === 'true',
logging: process.env.DATABASE_LOGGING === 'true',
});

Loading
Loading