diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..da7fd6b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,97 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-typecheck: + name: Lint & Type Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Prisma client + run: pnpm --filter db db:generate + env: + DATABASE_URL: "postgresql://localhost:5432/test" + + - name: Type check + run: pnpm check-types + + - name: Lint + run: pnpm lint + + - name: Format check + run: pnpm format --check + + build: + name: Build + runs-on: ubuntu-latest + needs: lint-and-typecheck + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Generate Prisma client + run: pnpm --filter db db:generate + env: + DATABASE_URL: "postgresql://localhost:5432/test" + + - name: Build + run: pnpm build + + validate-schema: + name: Validate Prisma Schema + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Validate Prisma schema + run: pnpm --filter db db:validate + env: + DATABASE_URL: "postgresql://localhost:5432/test" diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..019d9d2 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,14 @@ +# Dependencies +node_modules/ +pnpm-lock.yaml + +# Build outputs +dist/ +.next/ +out/ + +# Generated files +packages/db/generated/ + +# Package manager +.pnpm-store/ diff --git a/README.md b/README.md index 2132f71..1ed93c2 100644 --- a/README.md +++ b/README.md @@ -1,159 +1,122 @@ -# Turborepo starter +# Cortex -This Turborepo starter is maintained by the Turborepo core team. +A TypeScript monorepo for an MCP-first context, policy, and audit platform. -## Using this example +## Getting Started -Run the following command: +### Prerequisites -```sh -npx create-turbo@latest -``` - -## What's inside? - -This Turborepo includes the following packages/apps: - -### Apps and Packages - -- `docs`: a [Next.js](https://nextjs.org/) app -- `web`: another [Next.js](https://nextjs.org/) app -- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications -- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`) -- `@repo/typescript-config`: `tsconfig.json`s used throughout the monorepo - -Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). - -### Utilities +- Node.js >= 18 +- pnpm 9.x +- Docker (for local development with PostgreSQL and Redis) -This Turborepo has some additional tools already setup for you: - -- [TypeScript](https://www.typescriptlang.org/) for static type checking -- [ESLint](https://eslint.org/) for code linting -- [Prettier](https://prettier.io) for code formatting - -### Build - -To build all apps and packages, run the following command: - -With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed (recommended): +### Installation ```sh -cd my-turborepo -turbo build -``` - -Without global `turbo`, use your package manager: +# Install dependencies +pnpm install -```sh -cd my-turborepo -npx turbo build -pnpm dlx turbo build -pnpm exec turbo build +# Generate Prisma client (requires DATABASE_URL) +DATABASE_URL="postgresql://cortex:cortex@localhost:5432/cortex" pnpm --filter db db:generate ``` -You can build a specific package by using a [filter](https://turborepo.dev/docs/crafting-your-repository/running-tasks#using-filters): +### Development -With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed: +Start the development environment: ```sh -turbo build --filter=docs -``` +# Start PostgreSQL and Redis +docker-compose up -d -Without global `turbo`: - -```sh -npx turbo build --filter=docs -pnpm exec turbo build --filter=docs -pnpm exec turbo build --filter=docs +# Run all apps in development mode +pnpm dev ``` -### Develop +## Project Structure -To develop all apps and packages, run the following command: +### Apps -With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed (recommended): +- `apps/web` - Admin Web (Next.js) +- `apps/docs` - Documentation site (Next.js) +- `apps/api` - API + MCP entrypoint (NestJS) -```sh -cd my-turborepo -turbo dev -``` +### Packages -Without global `turbo`, use your package manager: +- `packages/db` - Prisma schema and database client +- `packages/ui` - Shared React component library (`@cortex/ui`) +- `packages/shared` - Shared types and utilities (`@cortex/shared`) +- `packages/eslint-config` - ESLint configurations (`@cortex/eslint-config`) +- `packages/typescript-config` - TypeScript configurations (`@cortex/typescript-config`) + +## Commands ```sh -cd my-turborepo -npx turbo dev -pnpm exec turbo dev -pnpm exec turbo dev -``` +# Install dependencies +pnpm install -You can develop a specific package by using a [filter](https://turborepo.dev/docs/crafting-your-repository/running-tasks#using-filters): +# Build all workspaces +pnpm build -With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed: +# Type-check all workspaces +pnpm check-types -```sh -turbo dev --filter=web -``` +# Lint all workspaces +pnpm lint -Without global `turbo`: +# Format code +pnpm format -```sh -npx turbo dev --filter=web -pnpm exec turbo dev --filter=web -pnpm exec turbo dev --filter=web +# Run dev servers +pnpm dev ``` -### Remote Caching - -> [!TIP] -> Vercel Remote Cache is free for all plans. Get started today at [vercel.com](https://vercel.com/signup?utm_source=remote-cache-sdk&utm_campaign=free_remote_cache). +### Database Commands -Turborepo can use a technique known as [Remote Caching](https://turborepo.dev/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines. +```sh +# Generate Prisma client +pnpm --filter db db:generate -By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup?utm_source=turborepo-examples), then enter the following commands: +# Validate Prisma schema +pnpm --filter db db:validate -With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed (recommended): +# Create a new migration +pnpm --filter db db:migrate -```sh -cd my-turborepo -turbo login +# Apply pending migrations (production) +pnpm --filter db db:migrate:deploy ``` -Without global `turbo`, use your package manager: +## Docker Services -```sh -cd my-turborepo -npx turbo login -pnpm exec turbo login -pnpm exec turbo login -``` +The `docker-compose.yml` provides: -This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview). +- **PostgreSQL 16** - Available at `localhost:5432` (user: `cortex`, password: `cortex`, database: `cortex`) +- **Redis 7** - Available at `localhost:6379` -Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo: - -With [global `turbo`](https://turborepo.dev/docs/getting-started/installation#global-installation) installed: +Start services: ```sh -turbo link +docker-compose up -d ``` -Without global `turbo`: +Stop services: ```sh -npx turbo link -pnpm exec turbo link -pnpm exec turbo link +docker-compose down ``` -## Useful Links +## Tech Stack + +- **Package Manager**: pnpm (workspace monorepo) +- **Task Runner**: Turborepo +- **Language**: TypeScript +- **Web Framework**: Next.js 16 +- **API Framework**: NestJS +- **Database**: PostgreSQL with Prisma ORM +- **Cache**: Redis +- **Linting**: ESLint +- **Formatting**: Prettier -Learn more about the power of Turborepo: +## License -- [Tasks](https://turborepo.dev/docs/crafting-your-repository/running-tasks) -- [Caching](https://turborepo.dev/docs/crafting-your-repository/caching) -- [Remote Caching](https://turborepo.dev/docs/core-concepts/remote-caching) -- [Filtering](https://turborepo.dev/docs/crafting-your-repository/running-tasks#using-filters) -- [Configuration Options](https://turborepo.dev/docs/reference/configuration) -- [CLI Usage](https://turborepo.dev/docs/reference/command-line-reference) +ISC diff --git a/apps/api/eslint.config.js b/apps/api/eslint.config.js new file mode 100644 index 0000000..7f23796 --- /dev/null +++ b/apps/api/eslint.config.js @@ -0,0 +1,4 @@ +import { config as baseConfig } from "@cortex/eslint-config/base"; + +/** @type {import("eslint").Linter.Config[]} */ +export default baseConfig; diff --git a/apps/api/package.json b/apps/api/package.json index 38d1da4..e009194 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,25 +1,37 @@ { - "name": "@cortex/api", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "start": "nest start", - "dev": "nest start --watch" - }, - "keywords": [], - "author": "", - "license": "ISC", - "dependencies": { - "@modelcontextprotocol/sdk": "^1.29.0", - "@nestjs/common": "^11.1.21", - "@nestjs/core": "^11.1.21", - "@nestjs/platform-express": "^11.1.21", - "express": "^5.2.1", - "zod": "^4.4.3" - }, - "devDependencies": { - "@nestjs/cli": "^11.0.21" - } + "name": "@cortex/api", + "version": "1.0.0", + "description": "NestJS API + MCP entrypoint for Cortex", + "main": "dist/main.js", + "scripts": { + "build": "nest build", + "check-types": "tsc --noEmit", + "dev": "nest start --watch", + "start": "nest start", + "start:prod": "node dist/main", + "lint": "eslint . --max-warnings 0", + "test": "echo \"Error: no test specified\" && exit 1" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "@cortex/shared": "workspace:*", + "@modelcontextprotocol/sdk": "^1.29.0", + "@nestjs/common": "^11.1.21", + "@nestjs/core": "^11.1.21", + "@nestjs/platform-express": "^11.1.21", + "db": "workspace:*", + "express": "^5.2.1", + "zod": "^4.4.3" + }, + "devDependencies": { + "@cortex/eslint-config": "workspace:*", + "@cortex/typescript-config": "workspace:*", + "@nestjs/cli": "^11.0.21", + "@types/express": "^5.0.3", + "@types/node": "^22.15.3", + "eslint": "^9.39.1", + "typescript": "5.9.2" + } } diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 7db8513..af15bcc 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -1,7 +1,7 @@ -import { Module } from '@nestjs/common'; -import { MCPModule } from './mcp/mcp.module'; +import { Module } from "@nestjs/common"; +import { MCPModule } from "./mcp/mcp.module"; @Module({ - imports: [MCPModule], + imports: [MCPModule], }) -export class AppModule { } \ No newline at end of file +export class AppModule {} diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index f22b184..bd20e80 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -1,12 +1,12 @@ -import { NestFactory } from '@nestjs/core'; -import { AppModule } from './app.module'; +import { NestFactory } from "@nestjs/core"; +import { AppModule } from "./app.module"; async function bootstrap() { - const app = await NestFactory.create(AppModule); + const app = await NestFactory.create(AppModule); - app.enableCors(); + app.enableCors(); - await app.listen(4000); + await app.listen(4000); } -bootstrap(); \ No newline at end of file +bootstrap(); diff --git a/apps/api/src/mcp/mcp.controller.ts b/apps/api/src/mcp/mcp.controller.ts index fceb155..c0cea08 100644 --- a/apps/api/src/mcp/mcp.controller.ts +++ b/apps/api/src/mcp/mcp.controller.ts @@ -1,34 +1,36 @@ -import { Controller, Post, Body } from '@nestjs/common'; +import { Controller, Post, Body } from "@nestjs/common"; +import type { MCPRequest } from "@cortex/shared"; -@Controller('mcp') -export class MCPController { - @Post() - async handleMCP(@Body() body: any) { - const { query, userId } = body; +interface MCPResponse { + context: string[]; + policy: { + rules: string[]; + }; + answer: string; +} - // STEP 1: mock identity resolution (Day 1 simplified) - const user = { - id: userId, - department: 'engineering', - organizationId: 'org_1', - }; +@Controller("mcp") +export class MCPController { + @Post() + async handleMCP(@Body() body: MCPRequest): Promise { + const { query } = body; - // STEP 2: mock policy - const policy = { - rules: ['Use TypeScript', 'Follow clean architecture'], - }; + // STEP 2: mock policy + const policy = { + rules: ["Use TypeScript", "Follow clean architecture"], + }; - // STEP 3: mock context retrieval - const context = [ - 'Company uses modular monolith architecture', - 'No direct DB access from controllers', - ]; + // STEP 3: mock context retrieval + const context = [ + "Company uses modular monolith architecture", + "No direct DB access from controllers", + ]; - // STEP 4: response assembly - return { - context, - policy, - answer: `Based on company standards: ${query}`, - }; - } -} \ No newline at end of file + // STEP 4: response assembly + return { + context, + policy, + answer: `Based on company standards: ${query}`, + }; + } +} diff --git a/apps/api/src/mcp/mcp.module.ts b/apps/api/src/mcp/mcp.module.ts index d5cc4c2..b19fc22 100644 --- a/apps/api/src/mcp/mcp.module.ts +++ b/apps/api/src/mcp/mcp.module.ts @@ -1,7 +1,7 @@ -import { Module } from '@nestjs/common'; -import { MCPController } from './mcp.controller'; +import { Module } from "@nestjs/common"; +import { MCPController } from "./mcp.controller"; @Module({ - controllers: [MCPController], + controllers: [MCPController], }) -export class MCPModule { } \ No newline at end of file +export class MCPModule {} diff --git a/apps/docs/app/page.tsx b/apps/docs/app/page.tsx index efb86f0..bf4c11e 100644 --- a/apps/docs/app/page.tsx +++ b/apps/docs/app/page.tsx @@ -1,5 +1,5 @@ import Image, { type ImageProps } from "next/image"; -import { Button } from "@repo/ui/button"; +import { Button } from "@cortex/ui/button"; import styles from "./page.module.css"; type Props = Omit & { diff --git a/apps/docs/eslint.config.js b/apps/docs/eslint.config.js index 47b0670..8eb0e5a 100644 --- a/apps/docs/eslint.config.js +++ b/apps/docs/eslint.config.js @@ -1,4 +1,4 @@ -import { nextJsConfig } from "@repo/eslint-config/next-js"; +import { nextJsConfig } from "@cortex/eslint-config/next-js"; /** @type {import("eslint").Linter.Config[]} */ export default nextJsConfig; diff --git a/apps/docs/tsconfig.json b/apps/docs/tsconfig.json index c0346be..b2602ea 100644 --- a/apps/docs/tsconfig.json +++ b/apps/docs/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@repo/typescript-config/nextjs.json", + "extends": "@cortex/typescript-config/nextjs.json", "compilerOptions": { "plugins": [ { diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 593833b..20f6f61 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,5 +1,5 @@ import Image, { type ImageProps } from "next/image"; -import { Button } from "@repo/ui/button"; +import { Button } from "@cortex/ui/button"; import styles from "./page.module.css"; type Props = Omit & { diff --git a/apps/web/eslint.config.js b/apps/web/eslint.config.js index 47b0670..8eb0e5a 100644 --- a/apps/web/eslint.config.js +++ b/apps/web/eslint.config.js @@ -1,4 +1,4 @@ -import { nextJsConfig } from "@repo/eslint-config/next-js"; +import { nextJsConfig } from "@cortex/eslint-config/next-js"; /** @type {import("eslint").Linter.Config[]} */ export default nextJsConfig; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index c0346be..b2602ea 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@repo/typescript-config/nextjs.json", + "extends": "@cortex/typescript-config/nextjs.json", "compilerOptions": { "plugins": [ { diff --git a/packages/db/package.json b/packages/db/package.json index 3ecb11b..cc45130 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,16 +1,39 @@ { "name": "db", "version": "1.0.0", - "description": "", - "main": "index.js", + "type": "module", + "description": "Prisma schema and DB client for Cortex", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./client": { + "types": "./generated/prisma/index.d.ts", + "default": "./generated/prisma/index.js" + } + }, "scripts": { + "build": "prisma generate && tsc", + "check-types": "tsc --noEmit", + "db:generate": "prisma generate", + "db:push": "prisma db push", + "db:migrate": "prisma migrate dev", + "db:migrate:deploy": "prisma migrate deploy", + "db:validate": "prisma validate", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { - "@prisma/client": "^7.8.0", - "prisma": "^7.8.0" + "@prisma/client": "^7.8.0" + }, + "devDependencies": { + "@cortex/typescript-config": "workspace:*", + "prisma": "^7.8.0", + "typescript": "5.9.2" } } diff --git a/packages/db/prisma/migrations/0001_initial_schema/migration.sql b/packages/db/prisma/migrations/0001_initial_schema/migration.sql new file mode 100644 index 0000000..353b6a4 --- /dev/null +++ b/packages/db/prisma/migrations/0001_initial_schema/migration.sql @@ -0,0 +1,202 @@ +-- Initial Cortex Schema Migration +-- Organization, User, Department, UserDepartment models with full multi-tenant shape + +-- CreateTable: organizations +CREATE TABLE "organizations" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "organizations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: users +CREATE TABLE "users" ( + "id" TEXT NOT NULL, + "email" TEXT NOT NULL, + "google_sub" TEXT NOT NULL, + "role" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "organization_id" TEXT NOT NULL, + + CONSTRAINT "users_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: departments +CREATE TABLE "departments" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "organization_id" TEXT NOT NULL, + + CONSTRAINT "departments_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: user_departments (many-to-many join table) +CREATE TABLE "user_departments" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "department_id" TEXT NOT NULL, + "is_primary" BOOLEAN NOT NULL DEFAULT false, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "user_departments_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: sources +CREATE TABLE "sources" ( + "id" TEXT NOT NULL, + "name" TEXT NOT NULL, + "type" TEXT NOT NULL, + "config" JSONB NOT NULL DEFAULT '{}', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "organization_id" TEXT NOT NULL, + + CONSTRAINT "sources_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: documents +CREATE TABLE "documents" ( + "id" TEXT NOT NULL, + "source_id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "metadata" JSONB NOT NULL DEFAULT '{}', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "documents_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: chunks +CREATE TABLE "chunks" ( + "id" TEXT NOT NULL, + "document_id" TEXT NOT NULL, + "content" TEXT NOT NULL, + "embedding" BYTEA, + "metadata" JSONB NOT NULL DEFAULT '{}', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "chunks_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: policies +CREATE TABLE "policies" ( + "id" TEXT NOT NULL, + "version" INTEGER NOT NULL DEFAULT 1, + "rules_json" JSONB NOT NULL, + "is_active" BOOLEAN NOT NULL DEFAULT true, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + "organization_id" TEXT NOT NULL, + "department_id" TEXT NOT NULL, + + CONSTRAINT "policies_pkey" PRIMARY KEY ("id") +); + +-- CreateTable: query_logs +CREATE TABLE "query_logs" ( + "id" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "query" TEXT NOT NULL, + "context_bundle" JSONB, + "policy_decision" JSONB, + "response" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "query_logs_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex: unique email +CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); + +-- CreateIndex: unique google_sub +CREATE UNIQUE INDEX "users_google_sub_key" ON "users"("google_sub"); + +-- CreateIndex: users by organization +CREATE INDEX "users_organization_id_idx" ON "users"("organization_id"); + +-- CreateIndex: users by email +CREATE INDEX "users_email_idx" ON "users"("email"); + +-- CreateIndex: unique department name per organization +CREATE UNIQUE INDEX "departments_organization_id_name_key" ON "departments"("organization_id", "name"); + +-- CreateIndex: departments by organization +CREATE INDEX "departments_organization_id_idx" ON "departments"("organization_id"); + +-- CreateIndex: unique user-department pair +CREATE UNIQUE INDEX "user_departments_user_id_department_id_key" ON "user_departments"("user_id", "department_id"); + +-- CreateIndex: user_departments by user +CREATE INDEX "user_departments_user_id_idx" ON "user_departments"("user_id"); + +-- CreateIndex: user_departments by department +CREATE INDEX "user_departments_department_id_idx" ON "user_departments"("department_id"); + +-- CreateIndex: enforce exactly one primary department per user (business rule from UBIQUITOUS_LANGUAGE.md) +-- "A User is mapped to one or more Departments, with one Primary Department" +CREATE UNIQUE INDEX "user_departments_one_primary_per_user" + ON "user_departments"("user_id") + WHERE "is_primary" = true; + +-- CreateIndex: sources by organization +CREATE INDEX "sources_organization_id_idx" ON "sources"("organization_id"); + +-- CreateIndex: documents by source +CREATE INDEX "documents_source_id_idx" ON "documents"("source_id"); + +-- CreateIndex: chunks by document +CREATE INDEX "chunks_document_id_idx" ON "chunks"("document_id"); + +-- CreateIndex: unique policy per org/dept/version +CREATE UNIQUE INDEX "policies_organization_id_department_id_version_key" ON "policies"("organization_id", "department_id", "version"); + +-- CreateIndex: policies by organization +CREATE INDEX "policies_organization_id_idx" ON "policies"("organization_id"); + +-- CreateIndex: policies by department +CREATE INDEX "policies_department_id_idx" ON "policies"("department_id"); + +-- CreateIndex: policies by active status +CREATE INDEX "policies_is_active_idx" ON "policies"("is_active"); + +-- CreateIndex: query_logs by user +CREATE INDEX "query_logs_user_id_idx" ON "query_logs"("user_id"); + +-- CreateIndex: query_logs by created time +CREATE INDEX "query_logs_createdAt_idx" ON "query_logs"("createdAt"); + +-- AddForeignKey: users -> organizations +ALTER TABLE "users" ADD CONSTRAINT "users_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: departments -> organizations +ALTER TABLE "departments" ADD CONSTRAINT "departments_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: user_departments -> users +ALTER TABLE "user_departments" ADD CONSTRAINT "user_departments_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: user_departments -> departments +ALTER TABLE "user_departments" ADD CONSTRAINT "user_departments_department_id_fkey" FOREIGN KEY ("department_id") REFERENCES "departments"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: sources -> organizations +ALTER TABLE "sources" ADD CONSTRAINT "sources_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: documents -> sources +ALTER TABLE "documents" ADD CONSTRAINT "documents_source_id_fkey" FOREIGN KEY ("source_id") REFERENCES "sources"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: chunks -> documents +ALTER TABLE "chunks" ADD CONSTRAINT "chunks_document_id_fkey" FOREIGN KEY ("document_id") REFERENCES "documents"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: policies -> organizations +ALTER TABLE "policies" ADD CONSTRAINT "policies_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: policies -> departments +ALTER TABLE "policies" ADD CONSTRAINT "policies_department_id_fkey" FOREIGN KEY ("department_id") REFERENCES "departments"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey: query_logs -> users +ALTER TABLE "query_logs" ADD CONSTRAINT "query_logs_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 09e7ac9..9bd2b2d 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -1,7 +1,5 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -// Get a free hosted Postgres database in seconds: `npx create-db` +// Prisma schema for Cortex - MCP-first context, policy, and audit platform +// Learn more: https://pris.ly/d/prisma-schema generator client { provider = "prisma-client" @@ -12,47 +10,188 @@ datasource db { provider = "postgresql" } +// ============================================================================ +// Core Identity Models +// ============================================================================ + +/// A tenant boundary representing one customer company. model Organization { id String @id @default(uuid()) name String - users User[] createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + users User[] + departments Department[] + sources Source[] + policies Policy[] + + @@map("organizations") } +/// An authenticated human identity belonging to one Organization. model User { - id String @id @default(uuid()) - email String @unique - googleSub String @unique + id String @id @default(uuid()) + email String @unique + googleSub String @unique @map("google_sub") role String - departmentId String? - organizationId String - organization Organization @relation(fields: [organizationId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String @map("organization_id") + + // Relations + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + userDepartments UserDepartment[] + queryLogs QueryLog[] + + // Indexes for common query patterns + @@index([organizationId]) + @@index([email]) + @@map("users") } +/// A policy scope within an Organization that groups users by function. model Department { - id String @id @default(uuid()) + id String @id @default(uuid()) + name String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String @map("organization_id") + + // Relations + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + userDepartments UserDepartment[] + policies Policy[] + + // Unique department name per organization + @@unique([organizationId, name]) + @@index([organizationId]) + @@map("departments") +} + +/// Join table mapping Users to Departments with primary department designation. +/// A User can belong to multiple Departments, with exactly one marked as primary. +model UserDepartment { + id String @id @default(uuid()) + userId String @map("user_id") + departmentId String @map("department_id") + isPrimary Boolean @default(false) @map("is_primary") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + department Department @relation(fields: [departmentId], references: [id], onDelete: Cascade) + + // Ensure unique user-department pairs + @@unique([userId, departmentId]) + @@index([userId]) + @@index([departmentId]) + // Note: Unique partial index "user_departments_one_primary_per_user" enforces + // one primary department per user (see migration SQL) + @@map("user_departments") +} + +// ============================================================================ +// Knowledge and Retrieval Models +// ============================================================================ + +/// A configured upstream knowledge origin (upload, Slack, Confluence, etc.) +model Source { + id String @id @default(uuid()) name String - organizationId String + type String + config Json @default("{}") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String @map("organization_id") + + // Relations + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + documents Document[] + + @@index([organizationId]) + @@map("sources") } +/// A normalized unit of source content stored for retrieval. model Document { id String @id @default(uuid()) - source String + sourceId String @map("source_id") content String + metadata Json @default("{}") createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + + // Relations + source Source @relation(fields: [sourceId], references: [id], onDelete: Cascade) + chunks Chunk[] + + @@index([sourceId]) + @@map("documents") +} + +/// A retrievable segment of a Document used in similarity search. +model Chunk { + id String @id @default(uuid()) + documentId String @map("document_id") + content String + embedding Bytes? + metadata Json @default("{}") + createdAt DateTime @default(now()) + + // Relations + document Document @relation(fields: [documentId], references: [id], onDelete: Cascade) + + @@index([documentId]) + @@map("chunks") } +// ============================================================================ +// Policy and Governance Models +// ============================================================================ + +/// A versioned department-scoped rule set that controls query and response behavior. model Policy { - id String @id @default(uuid()) - organizationId String - departmentId String - rulesJson Json + id String @id @default(uuid()) + version Int @default(1) + rulesJson Json @map("rules_json") + isActive Boolean @default(true) @map("is_active") + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + organizationId String @map("organization_id") + departmentId String @map("department_id") + + // Relations + organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade) + department Department @relation(fields: [departmentId], references: [id], onDelete: Cascade) + + @@unique([organizationId, departmentId, version]) + @@index([organizationId]) + @@index([departmentId]) + @@index([isActive]) + @@map("policies") } +// ============================================================================ +// Audit Models +// ============================================================================ + +/// An immutable record of query input, context usage, policy decisions, and output metadata. model QueryLog { - id String @id @default(uuid()) - userId String - query String - response String - createdAt DateTime @default(now()) + id String @id @default(uuid()) + userId String @map("user_id") + query String + contextBundle Json? @map("context_bundle") + policyDecision Json? @map("policy_decision") + response String? + createdAt DateTime @default(now()) + + // Relations + user User @relation(fields: [userId], references: [id], onDelete: Cascade) + + @@index([userId]) + @@index([createdAt]) + @@map("query_logs") } diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts new file mode 100644 index 0000000..6ce41af --- /dev/null +++ b/packages/db/src/index.ts @@ -0,0 +1,13 @@ +// Re-export Prisma client and types from generated directory +export { PrismaClient, Prisma } from "../generated/prisma/client.js"; +export type { + Organization, + User, + Department, + UserDepartment, + Source, + Document, + Chunk, + Policy, + QueryLog, +} from "../generated/prisma/client.js"; diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json new file mode 100644 index 0000000..c9a8196 --- /dev/null +++ b/packages/db/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@cortex/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "declaration": true, + "declarationMap": true, + "module": "NodeNext", + "moduleResolution": "NodeNext" + }, + "include": ["src", "generated"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/shared/package.json b/packages/shared/package.json new file mode 100644 index 0000000..af2500b --- /dev/null +++ b/packages/shared/package.json @@ -0,0 +1,27 @@ +{ + "name": "@cortex/shared", + "version": "1.0.0", + "description": "Shared types and utilities for Cortex", + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + }, + "./types": { + "types": "./src/types.ts", + "default": "./src/types.ts" + } + }, + "scripts": { + "check-types": "tsc --noEmit" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "@cortex/typescript-config": "workspace:*", + "typescript": "5.9.2" + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts new file mode 100644 index 0000000..eea524d --- /dev/null +++ b/packages/shared/src/index.ts @@ -0,0 +1 @@ +export * from "./types"; diff --git a/packages/shared/src/types.ts b/packages/shared/src/types.ts index 64feed1..67bb61a 100644 --- a/packages/shared/src/types.ts +++ b/packages/shared/src/types.ts @@ -1,6 +1,6 @@ export type MCPRequest = { - userId: string; - organizationId: string; - departmentId: string; - query: string; -}; \ No newline at end of file + userId: string; + organizationId: string; + departmentId: string; + query: string; +}; diff --git a/packages/shared/tsconfig.json b/packages/shared/tsconfig.json new file mode 100644 index 0000000..714fdde --- /dev/null +++ b/packages/shared/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@cortex/typescript-config/base.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "declaration": true, + "declarationMap": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/ui/eslint.config.mjs b/packages/ui/eslint.config.mjs index 19170f8..6fb3147 100644 --- a/packages/ui/eslint.config.mjs +++ b/packages/ui/eslint.config.mjs @@ -1,4 +1,4 @@ -import { config } from "@repo/eslint-config/react-internal"; +import { config } from "@cortex/eslint-config/react-internal"; /** @type {import("eslint").Linter.Config} */ export default config; diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index ed023ce..58d0d66 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@repo/typescript-config/react-library.json", + "extends": "@cortex/typescript-config/react-library.json", "compilerOptions": { "outDir": "dist", "strictNullChecks": true diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 74188f8..3ed4188 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: apps/api: dependencies: + '@cortex/shared': + specifier: workspace:* + version: link:../../packages/shared '@modelcontextprotocol/sdk': specifier: ^1.29.0 version: 1.29.0(zod@4.4.3) @@ -32,6 +35,9 @@ importers: '@nestjs/platform-express': specifier: ^11.1.21 version: 11.1.21(@nestjs/common@11.1.21(reflect-metadata@0.2.2)(rxjs@7.8.2))(@nestjs/core@11.1.21) + db: + specifier: workspace:* + version: link:../../packages/db express: specifier: ^5.2.1 version: 5.2.1 @@ -39,9 +45,27 @@ importers: specifier: ^4.4.3 version: 4.4.3 devDependencies: + '@cortex/eslint-config': + specifier: workspace:* + version: link:../../packages/eslint-config + '@cortex/typescript-config': + specifier: workspace:* + version: link:../../packages/typescript-config '@nestjs/cli': specifier: ^11.0.21 version: 11.0.21(@types/node@22.15.3)(prettier@3.7.4) + '@types/express': + specifier: ^5.0.3 + version: 5.0.6 + '@types/node': + specifier: ^22.15.3 + version: 22.15.3 + eslint: + specifier: ^9.39.1 + version: 9.39.1(jiti@2.7.0) + typescript: + specifier: 5.9.2 + version: 5.9.2 apps/docs: dependencies: @@ -122,9 +146,16 @@ importers: '@prisma/client': specifier: ^7.8.0 version: 7.8.0(prisma@7.8.0(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.2))(typescript@5.9.2) + devDependencies: + '@cortex/typescript-config': + specifier: workspace:* + version: link:../typescript-config prisma: specifier: ^7.8.0 version: 7.8.0(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(typescript@5.9.2) + typescript: + specifier: 5.9.2 + version: 5.9.2 packages/eslint-config: devDependencies: @@ -162,6 +193,15 @@ importers: specifier: ^8.50.0 version: 8.50.0(eslint@9.39.1(jiti@2.7.0))(typescript@5.9.2) + packages/shared: + devDependencies: + '@cortex/typescript-config': + specifier: workspace:* + version: link:../typescript-config + typescript: + specifier: 5.9.2 + version: 5.9.2 + packages/typescript-config: {} packages/ui: @@ -930,6 +970,12 @@ packages: cpu: [arm64] os: [win32] + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/eslint-scope@3.7.7': resolution: {integrity: sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==} @@ -939,12 +985,27 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/node@22.15.3': resolution: {integrity: sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==} + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/react-dom@19.2.2': resolution: {integrity: sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==} peerDependencies: @@ -953,6 +1014,12 @@ packages: '@types/react@19.2.2': resolution: {integrity: sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@typescript-eslint/eslint-plugin@8.50.0': resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} @@ -3720,6 +3787,15 @@ snapshots: '@turbo/windows-arm64@2.9.14': optional: true + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 22.15.3 + + '@types/connect@3.4.38': + dependencies: + '@types/node': 22.15.3 + '@types/eslint-scope@3.7.7': dependencies: '@types/eslint': 9.6.1 @@ -3732,12 +3808,31 @@ snapshots: '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 22.15.3 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + + '@types/http-errors@2.0.5': {} + '@types/json-schema@7.0.15': {} '@types/node@22.15.3': dependencies: undici-types: 6.21.0 + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + '@types/react-dom@19.2.2(@types/react@19.2.2)': dependencies: '@types/react': 19.2.2 @@ -3746,6 +3841,15 @@ snapshots: dependencies: csstype: 3.1.3 + '@types/send@1.2.1': + dependencies: + '@types/node': 22.15.3 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 22.15.3 + '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.1(jiti@2.7.0))(typescript@5.9.2))(eslint@9.39.1(jiti@2.7.0))(typescript@5.9.2)': dependencies: '@eslint-community/regexpp': 4.12.2 diff --git a/turbo.json b/turbo.json index 452ba54..ce0cc5a 100644 --- a/turbo.json +++ b/turbo.json @@ -5,7 +5,7 @@ "build": { "dependsOn": ["^build"], "inputs": ["$TURBO_DEFAULT$", ".env*"], - "outputs": [".next/**", "!.next/cache/**"] + "outputs": [".next/**", "!.next/cache/**", "dist/**"] }, "lint": { "dependsOn": ["^lint"] @@ -16,6 +16,12 @@ "dev": { "cache": false, "persistent": true + }, + "db:generate": { + "cache": false + }, + "db:migrate": { + "cache": false } } }