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
23 changes: 13 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,31 +49,34 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0

- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020
with:
node-version: 20
node-version: 22
cache: 'npm'

- name: Install dependencies
run: npm ci

- name: Static Analysis (Typecheck)
run: npm run typecheck
run: npm run typecheck && npm run typecheck:all

- name: Boundary Check (@codra/core purity)
run: npm run check:boundaries

# Lint is not cosmetic here: eslint.config.js carries the barrel guards that stop a module from
# importing a mocked barrel's sibling (which would silently void a vi.mock), plus max-lines and
# import-x/no-cycle. Without this step those guards only ever ran on a developer's machine.
- name: Static Analysis (Lint)
run: npm run lint

- name: Automated Tests
run: npm test

# Catches bundler-level breakage typecheck cannot see -- notably a client file pulling zod into
# the browser bundle through @shared/schema. `vite build` rather than `npm run build` so CI does
# not depend on the `wrangler types` step, which only regenerates a local .d.ts.
- name: Automated Tests (packages)
run: npm run test:all

- name: Build (client bundle)
run: npx vite build

- name: Build (worker bundle, dry run)
run: npx wrangler deploy --dry-run --outdir=.wrangler/dry
6 changes: 3 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0

- name: Initialize CodeQL
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/init@99df26d4f13ea111d4ec1a7dddef6063f76b97e9
with:
languages: ${{ matrix.language }}

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
uses: github/codeql-action/analyze@99df26d4f13ea111d4ec1a7dddef6063f76b97e9
23 changes: 23 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,29 @@ Before we can merge your pull request, you must sign our Contributor License Agr

---

## 📦 Repository Layout

Codra is migrating to an npm workspace monorepo. The repository is structured into `apps/` (deployable entrypoints) and `packages/` (reusable modules):

```text
packages/
├── schema/ # Shared types + zod contracts (zero dependencies)
├── core/ # Review engine (pure ports, depends on schema)
├── db/ # Postgres interactions and migrations (depends on schema, core)
├── models/ # LLM provider integrations (depends on schema, core)
├── provider-github/ # GitHub API adapter (depends on schema, core)
├── api/ # Hono router and API routes (depends on schema, core, db, models, provider-github)
└── ui/ # React design system and primitives (depends on schema)

apps/
├── worker/ # Cloudflare Worker entrypoint (wires bindings to api ports)
└── dashboard/ # React SPA frontend (depends on ui, schema)
```

**Note:** We are incrementally migrating code from the legacy `src/` directory into this workspace structure. New logic should be placed in the appropriate `packages/` or `apps/` directory when possible.

---

## 🛠️ Local Development Setup

Codra is a monorepo-style project built with **Hono** (Worker), **React** (Vite), and **Cloudflare Workers**.
Expand Down
1 change: 1 addition & 0 deletions apps/worker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
5 changes: 5 additions & 0 deletions apps/worker/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "@codra/worker",
"version": "0.9.4",
"private": true
Comment thread
devarshishimpi marked this conversation as resolved.
}
86 changes: 74 additions & 12 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescrip
export default tseslint.config(
{
ignores: [
'dist/**',
'node_modules/**',
// `**/` matters: a bare `dist/**` only covers the root build output, so emitted .d.ts under
// packages/*/dist was being linted as source.
'**/dist/**',
'**/node_modules/**',
// Generated by `wrangler types`.
'src/server/worker-env.d.ts',
'worker-configuration.d.ts',
Expand All @@ -22,8 +24,6 @@ export default tseslint.config(
files: ['**/*.{ts,tsx,js,mjs}'],
plugins: { 'import-x': importX, 'react-hooks': reactHooks },
settings: {
// The resolver has to understand the @server/@client/@shared aliases from tsconfig, or every
// internal import reads as unresolved and no-cycle/no-self-import are silently useless.
'import-x/resolver-next': [
createTypeScriptImportResolver({ project: './tsconfig.json' }),
],
Expand Down Expand Up @@ -72,6 +72,18 @@ export default tseslint.config(
files: ['src/client/**/*.{ts,tsx}'],
rules: {
'react-hooks/rules-of-hooks': 'error',

// The zone block at the bottom of this file cannot express this direction: its `files` is
// packages/** + apps/**, so a violation living in src/client is never linted by it.
'import-x/no-restricted-paths': ['error', {
zones: [
{
target: 'src/client/**/*',
from: ['packages/core/**/*', 'src/server/**/*'],
message: 'The review engine and the Worker tree are server-only. Importing either pulls zod/jsonrepair/picomatch into the browser bundle -- exactly what the `vite build` CI step exists to catch. (@codra/schema/review-limits is the sanctioned client-side import.)'
}
]
}],
},
},

Expand All @@ -98,10 +110,10 @@ export default tseslint.config(
{ group: ['**/core/github/http', '**/core/github/app-auth', '**/core/github/types', '**/core/github/diff-fetch', '**/core/github/review-post', '**/core/github/labels', '@server/core/github/http', '@server/core/github/app-auth', '@server/core/github/types', '@server/core/github/diff-fetch', '@server/core/github/review-post', '@server/core/github/labels'], message: 'Import from @server/core/github, not a sibling. One spec vi.mocks that specifier. (core/github/oauth is deliberately NOT listed: it is the dashboard OAuth flow, not part of the GitHubClient barrel, and routes/auth.ts imports it directly.)' },
// Covers every sibling in the family, including the three the barrel re-exports publicly
// (budget, diff-cache, request) which were previously unprotected.
{ group: ['**/core/review/*', '@server/core/review/*'], message: 'Import from @server/core/review, not a sibling. One spec vi.mocks that specifier and workflows/review.ts imports only runReviewJob from it.' },
{ group: ['**/core/model-output/*', '@server/core/model-output/*'], message: 'Import from @server/core/model-output, not a sibling.' },
{ group: ['**/core/diff/position', '@server/core/diff/position'], message: 'Import from @server/core/diff, not a sibling.' },
{ group: ['**/shared/schema-claims', '**/shared/schema-repo-config', '**/shared/schema-enums', '@shared/schema-claims', '@shared/schema-repo-config', '@shared/schema-enums'], message: 'Import from @shared/schema, not a sibling. (@shared/review-limits is exempt: the client imports it directly to keep zod out of the browser bundle.)' },
{ group: ['**/core/review/*', '@server/core/review/*', '@codra/core/review/*'], message: 'Import from @server/core/review, not a sibling. One spec vi.mocks that specifier and workflows/review.ts imports only runReviewJob from it.' },
{ group: ['**/core/model-output/*', '@server/core/model-output/*', '@codra/core/model-output/*'], message: 'Import from @codra/core/model-output, not a sibling. (The package exports map already refuses to resolve these; the lint rule gives the error at edit time.)' },
{ group: ['**/core/diff/position', '@server/core/diff/position', '@codra/core/diff/position'], message: 'Import from @codra/core/diff, not a sibling.' },
{ group: ['**/schema-claims', '**/schema-repo-config', '**/schema-enums', '@codra/schema/schema-claims', '@codra/schema/schema-repo-config', '@codra/schema/schema-enums'], message: 'Import from @codra/schema, not a sibling. (@codra/schema/review-limits is exempt: the client imports it directly to keep zod out of the browser bundle.)' },
],
}],
},
Expand All @@ -126,10 +138,10 @@ export default tseslint.config(
'src/server/db/file-reviews.ts',
'src/server/services/model.ts',
'src/server/core/github/index.ts',
'src/server/core/review/index.ts',
'src/server/core/diff/index.ts',
'src/server/core/model-output/index.ts',
'src/shared/schema.ts',
// core/review, core/diff and core/model-output are gone from here: they moved to @codra/core and
// what is left at those paths is a re-export shim with no sibling imports to exempt. ESLint does
// not warn about `files` patterns that match nothing, so a stale entry would just rot quietly.
'packages/schema/src/schema.ts',
],
rules: {
'no-restricted-imports': 'off',
Expand All @@ -152,4 +164,54 @@ export default tseslint.config(
},
},
},

{
files: ['packages/**/*.{ts,tsx}', 'apps/**/*.{ts,tsx}'],
rules: {
'import-x/no-restricted-paths': ['error', {
zones: [
{
// `src/**` in `from` is what actually holds the extraction in place. The zones below
// only ever described packages -> packages traffic, so nothing stopped a moved file from
// keeping its old `@server/db/jobs` import and quietly re-coupling the package to the
// Worker tree. Traffic goes src -> packages, through src/server/adapters, never back.
target: 'packages/schema/**/*',
from: ['src/**/*', 'test/**/*', 'scripts/**/*', 'packages/core/**/*', 'packages/provider-github/**/*', 'packages/db/**/*', 'packages/models/**/*', 'packages/api/**/*', 'packages/ui/**/*', 'apps/worker/**/*', 'apps/dashboard/**/*']
},
{
target: 'packages/core/**/*',
from: ['src/**/*', 'test/**/*', 'scripts/**/*', 'packages/provider-github/**/*', 'packages/db/**/*', 'packages/models/**/*', 'packages/api/**/*', 'packages/ui/**/*', 'apps/worker/**/*', 'apps/dashboard/**/*']
},
{
target: 'packages/db/**/*',
from: ['packages/provider-github/**/*', 'packages/models/**/*', 'packages/api/**/*', 'packages/ui/**/*', 'apps/worker/**/*', 'apps/dashboard/**/*']
},
{
target: 'packages/provider-github/**/*',
from: ['packages/db/**/*', 'packages/models/**/*', 'packages/api/**/*', 'packages/ui/**/*', 'apps/worker/**/*', 'apps/dashboard/**/*']
},
{
target: 'packages/models/**/*',
from: ['packages/db/**/*', 'packages/provider-github/**/*', 'packages/api/**/*', 'packages/ui/**/*', 'apps/worker/**/*', 'apps/dashboard/**/*']
},
{
target: 'packages/api/**/*',
from: ['packages/ui/**/*', 'apps/worker/**/*', 'apps/dashboard/**/*']
},
{
target: 'packages/ui/**/*',
from: ['packages/core/**/*', 'packages/provider-github/**/*', 'packages/db/**/*', 'packages/models/**/*', 'packages/api/**/*', 'apps/worker/**/*', 'apps/dashboard/**/*']
},
{
target: 'apps/dashboard/**/*',
from: ['packages/core/**/*', 'packages/provider-github/**/*', 'packages/db/**/*', 'packages/models/**/*', 'packages/api/**/*', 'apps/worker/**/*']
},
{
target: 'apps/worker/**/*',
from: ['packages/ui/**/*', 'apps/dashboard/**/*']
}
]
}]
}
}
);
44 changes: 42 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 13 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,37 @@
"bugs": {
"url": "https://github.com/devarshishimpi/codra/issues"
},
"workspaces": [
"packages/*",
"apps/*"
],
"scripts": {
"build": "vite build && npm run cf-typegen",
"build:all": "npm run build --workspaces --if-present",
"cf-typegen": "wrangler types ./src/server/worker-env.d.ts",
"deploy": "npm run build && npm run migrate && wrangler deploy",
"dev": "concurrently -k -n CLIENT,WORKER -c cyan,green \"npm:dev:client\" \"npm:dev:worker\"",
"dev:client": "vite build --watch --mode development",
"dev:worker": "wrangler dev --local",
"lint": "eslint src test scripts",
"lint": "eslint src test scripts packages apps",
"lint:all": "npm run lint --workspaces --if-present",
"check:boundaries": "node scripts/check-core-boundary.mjs",
"density": "node scripts/comment-density.mjs --top",
"start": "npm run dev",
"setup:cloudflare": "node scripts/setup-cloudflare.js",
"migrate": "node scripts/migrate.mjs",
"test": "node scripts/test.mjs",
"test:all": "npm run test --workspaces --if-present",
"test:watch": "vitest",
"typecheck": "tsc --noEmit"
"typecheck": "tsc --noEmit",
"typecheck:all": "npm run typecheck --workspaces --if-present"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@tailwindcss/vite": "^4.2.2",
"@testing-library/dom": "^10.4.1",
"@testing-library/react": "^16.3.2",
"@types/node": "^25.6.0",
"@types/picomatch": "^4.0.3",
"@types/react": "^19.2.18",
"@types/react-dom": "^19.2.4",
"@vitejs/plugin-react": "^6.0.5",
Expand All @@ -56,6 +64,8 @@
"wrangler": "^4.114.0"
},
"dependencies": {
"@codra/core": "*",
"@codra/schema": "*",
"@base-ui/react": "^1.6.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
Expand All @@ -64,7 +74,6 @@
"lenis": "^1.3.26",
"lucide-react": "^1.8.0",
"motion": "^12.42.2",
"picomatch": "^4.0.5",
"postgres": "^3.4.9",
"react": "^19.2.8",
"react-dom": "^19.2.8",
Expand Down
Loading