diff --git a/.changeset/README.md b/.changeset/README.md new file mode 100644 index 00000000..697e6f70 --- /dev/null +++ b/.changeset/README.md @@ -0,0 +1,33 @@ +# Changesets + +This folder is managed by [Changesets](https://github.com/changesets/changesets), used here only for +**local, manual** versioning of the public `@codraoss/*` packages. There is no CI-based publishing. + +The seven publishable packages (`schema`, `core`, `db`, `models`, `provider-github`, `api`, `ui`) +are a **fixed group** — they version and release in lockstep (currently `0.9.4`). The worker app +(`@codraoss/worker`) is private and never published. + +## Recording a change + +```bash +npx changeset # pick the bump, write a summary; commit the generated file +``` + +## Cutting a release (run locally, then publish by hand) + +```bash +npm run version:packages # applies pending changesets: bumps all @codraoss/* in lockstep + changelog +npm run release # builds dist and runs `changeset publish` for you +``` + +`npm run release` builds every package to `dist/` and publishes. Publishing is also possible per +package with plain npm — the `prepack` hook rewrites `exports` to the compiled `dist/` paths in the +tarball automatically: + +```bash +npm run build:packages +npm publish -w @codraoss/schema # ...repeat bottom-up: schema → core → db/models/provider-github → api → ui +``` + +You must be logged in to npm (`npm whoami`) and own the `@codraoss` scope. First publish of each +package needs public access, which `publishConfig.access` already sets. diff --git a/.changeset/config.json b/.changeset/config.json new file mode 100644 index 00000000..eec496e2 --- /dev/null +++ b/.changeset/config.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@3.1.1/schema.json", + "changelog": "@changesets/cli/changelog", + "commit": false, + "fixed": [ + [ + "@codraoss/schema", + "@codraoss/core", + "@codraoss/db", + "@codraoss/models", + "@codraoss/provider-github", + "@codraoss/api", + "@codraoss/ui" + ] + ], + "linked": [], + "access": "public", + "baseBranch": "main", + "updateInternalDependencies": "patch", + "ignore": ["@codraoss/worker"] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a300965..97818689 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -60,6 +60,17 @@ jobs: - name: Install dependencies run: npm ci + - name: Build packages + run: npm run build:packages + + - name: Validate package exports + run: | + npm run check:exports + for dir in packages/*/; do + echo "publint $dir" + (cd "$dir" && npx --no-install publint) || exit 1 + done + - name: Static Analysis (Typecheck) run: npm run typecheck && npm run typecheck:all diff --git a/.github/workflows/cla-check.yml b/.github/workflows/cla-check.yml index 0cca46ce..4050c151 100644 --- a/.github/workflows/cla-check.yml +++ b/.github/workflows/cla-check.yml @@ -15,40 +15,82 @@ jobs: contents: read pull-requests: read steps: - - name: Verify contributor CLA signature + - name: Verify CLA signatures for the PR author and every commit author env: CLA_CHECK_SECRET: ${{ secrets.CLA_CHECK_SECRET }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REPO: ${{ github.repository }} + PR_NUMBER: ${{ github.event.pull_request.number }} PR_AUTHOR: ${{ github.event.pull_request.user.login }} run: | node -e " (async () => { - const author = process.env.PR_AUTHOR; const secret = process.env.CLA_CHECK_SECRET; - - if (!author) throw new Error('Missing PR author.'); + const token = process.env.GITHUB_TOKEN; + const repo = process.env.REPO; + const prNumber = process.env.PR_NUMBER; + const prAuthor = process.env.PR_AUTHOR; + if (!secret) throw new Error('Missing CLA_CHECK_SECRET.'); + if (!token) throw new Error('Missing GITHUB_TOKEN.'); + if (!repo || !prNumber || !prAuthor) throw new Error('Missing PR context.'); + + const isBot = (login) => login.endsWith('[bot]'); + const logins = new Set(); + if (!isBot(prAuthor)) logins.add(prAuthor); - console.log('Checking CLA signature for @' + author + '...'); + // Walk every commit in the PR, not just the opener: cherry-picked or + // applied patches carry other people's copyright and need a signature too. + const headers = { + authorization: 'Bearer ' + token, + accept: 'application/vnd.github+json', + 'x-github-api-version': '2022-11-28', + }; + const unmatched = []; + for (let page = 1; page <= 3; page++) { + const url = 'https://api.github.com/repos/' + repo + '/pulls/' + prNumber + '/commits?per_page=100&page=' + page; + const res = await fetch(url, { headers }); + if (!res.ok) throw new Error('GitHub API error ' + res.status + ': ' + (await res.text())); + const commits = await res.json(); + for (const c of commits) { + if (c.author && c.author.login) { + if (c.author.type !== 'Bot' && !isBot(c.author.login)) logins.add(c.author.login); + } else { + unmatched.push(c.sha.slice(0, 7) + ' (' + c.commit.author.name + ' <' + c.commit.author.email + '>)'); + } + } + if (commits.length < 100) break; + } - const response = await fetch('https://codra.run/api/internal/check-cla?user=' + encodeURIComponent(author), { - headers: { - 'x-cla-check-secret': secret, - }, - }); + if (unmatched.length > 0) { + console.error('❌ Some commits are not linked to a GitHub account, so their CLA status cannot be verified:'); + for (const line of unmatched) console.error(' ' + line); + console.error('Each author must add their commit email to their GitHub account, or the commits must be re-authored.'); + process.exit(1); + } - if (!response.ok) { - const body = await response.text(); - throw new Error('API Error ' + response.status + ': ' + body); + const unsigned = []; + for (const login of [...logins].sort()) { + console.log('Checking CLA signature for @' + login + '...'); + const res = await fetch('https://codra.run/api/internal/check-cla?user=' + encodeURIComponent(login), { + headers: { 'x-cla-check-secret': secret }, + }); + if (!res.ok) throw new Error('API Error ' + res.status + ': ' + (await res.text())); + const payload = await res.json(); + if (payload.signed) { + console.log('✅ CLA confirmed for @' + login + '.'); + } else { + unsigned.push(login); + } } - const payload = await response.json(); - if (!payload.signed) { - console.error('❌ @' + author + ' has not signed the CLA.'); + if (unsigned.length > 0) { + for (const login of unsigned) console.error('❌ @' + login + ' has not signed the CLA.'); console.error('Please visit https://codra.run/cla to sign.'); process.exit(1); } - console.log('✅ CLA confirmed for @' + author + '.'); + console.log('✅ CLA confirmed for all ' + logins.size + ' author(s).'); })().catch(err => { console.error(err.message); process.exit(1); diff --git a/.gitignore b/.gitignore index 435ede5f..e22ce89b 100644 --- a/.gitignore +++ b/.gitignore @@ -144,3 +144,7 @@ vite.config.ts.timestamp-* .agent +*.prepack-bak +tsup.config.bundled_*.mjs +*.tgz +.npmrc diff --git a/apps/worker/package.json b/apps/worker/package.json index 3db124b9..fbd91f5e 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -1,12 +1,15 @@ { - "name": "@codra/worker", + "name": "@codraoss/worker", "version": "0.9.4", "private": true, "type": "module", "dependencies": { - "@codra/core": "*", - "@codra/db": "*", - "@codra/schema": "*", + "@codraoss/api": "*", + "@codraoss/core": "*", + "@codraoss/db": "*", + "@codraoss/models": "*", + "@codraoss/provider-github": "*", + "@codraoss/schema": "*", "hono": "^4.12.25" }, "devDependencies": { diff --git a/apps/worker/src/api-deps.ts b/apps/worker/src/api-deps.ts index d10b664b..be76b9e9 100644 --- a/apps/worker/src/api-deps.ts +++ b/apps/worker/src/api-deps.ts @@ -1,17 +1,18 @@ -import type { ApiRouterDeps } from '@codra/api'; +import type { ApiRouterDeps } from '@codraoss/api'; import type { AppBindings } from './env'; -import * as dbAccounts from '@codra/db/accounts'; -import * as dbJobs from '@codra/db/jobs'; -import * as dbFileReviews from '@codra/db/file-reviews'; -import * as dbCommentFeedback from '@codra/db/comment-feedback'; -import * as dbModelConfigs from '@codra/db/model-configs'; -import * as dbRepoConfigs from '@codra/db/repo-configs'; -import * as dbAppSettings from '@codra/db/app-settings'; -import * as dbStats from '@codra/db/stats'; -import * as dbWebhookDeliveries from '@codra/db/webhook-deliveries'; +import * as dbAccounts from '@codraoss/db/accounts'; +import * as dbJobs from '@codraoss/db/jobs'; +import * as dbFileReviews from '@codraoss/db/file-reviews'; +import * as dbCommentFeedback from '@codraoss/db/comment-feedback'; +import * as dbModelConfigs from '@codraoss/db/model-configs'; +import * as dbRepoConfigs from '@codraoss/db/repo-configs'; +import * as dbAppSettings from '@codraoss/db/app-settings'; +import * as dbStats from '@codraoss/db/stats'; +import * as dbWebhookDeliveries from '@codraoss/db/webhook-deliveries'; -import { GitHubClient, normalizeGitHubWebhook } from '@codra/provider-github'; +import { GitHubClient, normalizeGitHubWebhook } from '@codraoss/provider-github'; +import { GitHubIdentityProvider } from '@codraoss/provider-github/oauth'; import { getGlobalConfig, updateGlobalConfig, loadRepoConfig, invalidateRepoConfigCache } from '../../../src/server/core/config'; import { getUpdatesEmailPreference, syncUpdatesEmail } from '../../../src/server/core/updates-email'; @@ -23,11 +24,12 @@ import { createOAuthState, consumeOAuthState } from '../../../src/server/core/oa import { verifyGitHubWebhookSignature } from '../../../src/server/core/verify'; import { CloudflareSessionStore } from './sessions'; +import { makeKvStore } from '../../../src/server/adapters/platform'; import { logger } from '../../../src/server/core/logger'; // model sync dependencies -import { listLlmProviderSecrets, upsertDiscoveredModelConfigs, createLlmProvider, updateLlmProvider, getResolvedModelConfig, getLlmProvider } from '@codra/db/model-configs'; -import { encryptLlmApiKey, decryptLlmApiKey, listProviderModels, reviewWithCloudflare, reviewWithGoogle, reviewWithVertex, reviewWithOpenAI, reviewWithAnthropic, ProviderRequestError } from '@codra/models'; +import { listLlmProviderSecrets, upsertDiscoveredModelConfigs, createLlmProvider, updateLlmProvider, getResolvedModelConfig, getLlmProvider } from '@codraoss/db/model-configs'; +import { encryptLlmApiKey, decryptLlmApiKey, listProviderModels, reviewWithCloudflare, reviewWithGoogle, reviewWithVertex, reviewWithOpenAI, reviewWithAnthropic, ProviderRequestError } from '@codraoss/models'; import { buildReviewResponseSchema } from '../../../src/server/prompts/file-review'; function getSecretStore(env: AppBindings) { @@ -43,6 +45,12 @@ function optionalEnv(value: () => string) { } } +// `IDENTITY_PROVIDER` is a test-only seam; production has no such binding. +const githubIdentity = new GitHubIdentityProvider(); +function identityProvider(env: AppBindings) { + return ((env as any).IDENTITY_PROVIDER as any) ?? githubIdentity; +} + export function createApiRouterDeps(env: AppBindings, _ctx: ExecutionContext): ApiRouterDeps { return { repositories: { @@ -232,9 +240,7 @@ export function createApiRouterDeps(env: AppBindings, _ctx: ExecutionContext): A ); } catch (e) { /* ignore */ } }, - createReviewRuntime: () => { - throw new Error('Not implemented for this context'); - }, + createReviewRuntime: () => ({ kv: makeKvStore(env) } as any), getUpdatesEmailPreference: async (githubUserId: number) => await getUpdatesEmailPreference(env as any, githubUserId), syncUpdatesEmail: async (githubUserId: number, email: string | null | undefined) => await syncUpdatesEmail(env as any, githubUserId, email), terminateJobWorkflow: async (job: { id: string; workflowInstanceId?: string | null }) => { @@ -256,8 +262,10 @@ export function createApiRouterDeps(env: AppBindings, _ctx: ExecutionContext): A authProvider: { createOAuthState: async () => await createOAuthState(env as any), consumeOAuthState: async (state: string) => await consumeOAuthState(env as any, state), - beginAuthorization: async (callbackUrl: string, state: string) => await ((env as any).IDENTITY_PROVIDER as any).beginAuthorization(callbackUrl, state, env), - completeAuthorization: async (code: string, state: string, expectedState: string) => await ((env as any).IDENTITY_PROVIDER as any).completeAuthorization(code, state, expectedState, env), + beginAuthorization: async (callbackUrl: string, state: string) => + await identityProvider(env).beginAuthorization(callbackUrl, state, env), + completeAuthorization: async (code: string, state: string, expectedState: string) => + await identityProvider(env).completeAuthorization(code, state, expectedState, env), }, webhook: { verifySignature: async (signature: string | null, body: string) => await verifyGitHubWebhookSignature(env.GITHUB_APP_WEBHOOK_SECRET, signature, body), diff --git a/apps/worker/src/env.ts b/apps/worker/src/env.ts index 03238748..b3160b61 100644 --- a/apps/worker/src/env.ts +++ b/apps/worker/src/env.ts @@ -1,5 +1,5 @@ -import type { ReviewJobMessage } from '@codra/schema'; -import type { DashboardSessionUser, SessionStore } from '@codra/core'; +import type { ReviewJobMessage } from '@codraoss/schema'; +import type { DashboardSessionUser, SessionStore } from '@codraoss/core'; export interface WorkersAiBinding { run(model: string, input: Record, options?: { signal?: AbortSignal }): Promise; diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index 9bd6f940..f63f12bf 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -1,12 +1,12 @@ -import { createApiRouter } from '@codra/api'; +import { createApiRouter } from '@codraoss/api'; import { createApiRouterDeps } from './api-deps'; import { ReviewWorkflow } from './workflows/review'; import type { AppBindings } from './env'; -import { reviewJobMessageSchema } from '@codra/schema'; +import { reviewJobMessageSchema } from '@codraoss/schema'; import { logger } from '@server/core/logger'; import { disposeRpc } from '@server/core/rpc'; -import { runWithDb } from '@codra/db/client'; -import { failJob, hasPendingMaintenanceWork, clearSystemActive } from '@codra/db/jobs'; +import { runWithDb } from '@codraoss/db/client'; +import { failJob, hasPendingMaintenanceWork, clearSystemActive } from '@codraoss/db/jobs'; import { runBestEffortJobMaintenance } from '@server/core/job-recovery'; const app = createApiRouter(); @@ -23,10 +23,7 @@ export default { }, async scheduled(_controller: ScheduledController, env: AppBindings, _ctx: ExecutionContext) { - // The cron fires every 2 minutes but only does maintenance (recovering stuck jobs, finishing - // check runs). Touching Postgres every tick would keep the serverless DB awake 24/7, so gate on - // a KV flag set whenever a job is created/claimed and cleared once nothing is left to maintain - // -- when it's absent we return without ever opening a DB connection. + // Gate on KV flag: avoids waking the serverless DB every 2min tick when nothing's pending. try { const active = await env.APP_KV.get('system:active_jobs'); if (!active) { @@ -38,8 +35,7 @@ export default { return runWithDb(env, async () => { await runBestEffortJobMaintenance(env); - // Drop the flag as soon as nothing is left to maintain, so the next tick skips Postgres - // instead of waiting out the 20-minute TTL; a new job re-sets it on insert/claim. + // Clear flag early so next tick skips DB instead of waiting for TTL. try { if (!(await hasPendingMaintenanceWork(env))) { await clearSystemActive(env); @@ -58,9 +54,7 @@ export default { logger.error('Pre-batch maintenance task failed', error instanceof Error ? error : new Error(String(error))); } - // Sequential by design: each iteration creates a Workflow instance (a subrequest), and a - // batch can carry enough messages that fanning out would breach the Workers simultaneous- - // subrequest cap on the Free plan. + // Sequential: parallel fan-out could breach the Free plan subrequest cap. for (const message of batch.messages) { const parseResult = reviewJobMessageSchema.safeParse(message.body); @@ -69,9 +63,7 @@ export default { body: message.body, error: parseResult.error.flatten(), }); - // A malformed message can't be processed and retrying won't help, so ack it -- but if it - // still carries a recognizable jobId, fail that job so it doesn't sit 'queued' forever - // (lease recovery only revives 'running' rows). + // Ack (retry won't help); fail the job too, since lease recovery only revives 'running' rows. const strandedId = (message.body as { jobId?: unknown })?.jobId; if (typeof strandedId === 'string' && /^[0-9a-f-]{36}$/i.test(strandedId)) { try { @@ -87,10 +79,7 @@ export default { const { jobId, deliveryId, forceFreshInstance } = parseResult.data; try { - // Recovery re-enqueues a stuck job under its original jobId; keying the instance on jobId - // would collide with the dead instance (instance.already_exists), so recovery sets - // forceFreshInstance to key the new instance on the (fresh) deliveryId -- a UUID, - // matching workflow_instance_id's column type. + // forceFreshInstance keys on deliveryId (UUID) to avoid instance.already_exists on the dead jobId-keyed instance. const id = forceFreshInstance ? deliveryId : (jobId ?? deliveryId); if (!id) { logger.error('Message missing identifiers; dropping', { body: message.body }); diff --git a/apps/worker/src/ports/cloudflare-kv.ts b/apps/worker/src/ports/cloudflare-kv.ts index 0c2af4b4..e8151ae3 100644 --- a/apps/worker/src/ports/cloudflare-kv.ts +++ b/apps/worker/src/ports/cloudflare-kv.ts @@ -1,4 +1,4 @@ -import type { KeyValueStore } from '@codra/core'; +import type { KeyValueStore } from '@codraoss/core'; export class CloudflareKV implements KeyValueStore { constructor(private readonly kv: KVNamespace) {} diff --git a/apps/worker/src/ports/cloudflare-orchestrator.ts b/apps/worker/src/ports/cloudflare-orchestrator.ts index ca8d28ea..ed27767d 100644 --- a/apps/worker/src/ports/cloudflare-orchestrator.ts +++ b/apps/worker/src/ports/cloudflare-orchestrator.ts @@ -1,9 +1,9 @@ -import type { JobOrchestrator } from '@codra/core'; -import type { ReviewJobMessage } from '@codra/schema'; -import { FRESH_INVOCATION_YIELD_SECONDS } from '@codra/core'; +import type { JobOrchestrator } from '@codraoss/core'; +import type { ReviewJobMessage } from '@codraoss/schema'; +import { FRESH_INVOCATION_YIELD_SECONDS } from '@codraoss/core'; import { runReviewJob } from '@server/core/review'; -import { setJobWorkflowInstance } from '@codra/db/jobs'; -import { logger } from '@codra/core/logger'; +import { setJobWorkflowInstance } from '@codraoss/db/jobs'; +import { logger } from '@codraoss/core/logger'; import { runBestEffortJobMaintenance } from '@server/core/job-recovery'; import type { AppBindings } from '../env'; import type { WorkflowStep } from 'cloudflare:workers'; diff --git a/apps/worker/src/sessions.ts b/apps/worker/src/sessions.ts index c6ecc3a7..b50375f7 100644 --- a/apps/worker/src/sessions.ts +++ b/apps/worker/src/sessions.ts @@ -1,4 +1,4 @@ -import type { DashboardSessionUser, SessionStore } from '@codra/core'; +import type { DashboardSessionUser, SessionStore } from '@codraoss/core'; export class CloudflareSessionStore implements SessionStore { constructor(private readonly kv: KVNamespace) {} diff --git a/apps/worker/src/workflows/review.ts b/apps/worker/src/workflows/review.ts index 01d55f99..0a3dc46b 100644 --- a/apps/worker/src/workflows/review.ts +++ b/apps/worker/src/workflows/review.ts @@ -1,7 +1,7 @@ import { WorkflowEntrypoint, type WorkflowEvent, type WorkflowStep } from 'cloudflare:workers'; import type { AppBindings } from '../env'; -import { type ReviewJobMessage } from '@codra/schema'; -import { runWithDb } from '@codra/db/client'; +import { type ReviewJobMessage } from '@codraoss/schema'; +import { runWithDb } from '@codraoss/db/client'; import { CloudflareOrchestrator } from '../ports/cloudflare-orchestrator'; export class ReviewWorkflow extends WorkflowEntrypoint { diff --git a/eslint.config.js b/eslint.config.js index 0396e8ec..d2cd9241 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -7,11 +7,9 @@ import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescrip export default tseslint.config( { ignores: [ - // `**/` matters: a bare `dist/**` only covers the root build output, so emitted .d.ts under - // packages/*/dist was being linted as source. + // `**/` needed: bare `dist/**` misses packages/*/dist, linting emitted .d.ts as source. '**/dist/**', - '**/node_modules/**','**/.wrangler/**', - // Generated by `wrangler types`. + '**/node_modules/**','**/.wrangler/**', 'apps/worker/src/worker-env.d.ts', 'worker-configuration.d.ts', ], @@ -29,41 +27,29 @@ export default tseslint.config( ], }, rules: { - // TypeScript resolves every identifier already, and does it correctly for types, `declare`, - // and the Worker/DOM lib globals. Leaving this on means re-declaring hundreds of ambient - // globals in ESLint just to get a worse version of a check `npm run typecheck` already runs. + // TS resolves identifiers correctly (incl. types, `declare`, lib globals); this is redundant and worse. 'no-undef': 'off', - // The base rule cannot see TypeScript's type-only positions; the TS one can. 'no-unused-vars': 'off', '@typescript-eslint/no-unused-vars': ['error', { - // `catch {}` is the preferred form, but an unused binding is not worth an error. caughtErrors: 'none', argsIgnorePattern: '^_', varsIgnorePattern: '^_', }], - // `import-x/no-duplicates` and NOT the core `no-duplicate-imports`: the core rule is type-blind - // and flags the deliberate `import { Hono }` + `import type { Context }` split as a duplicate. + // Not core `no-duplicate-imports`: it's type-blind, flags `import {X}` + `import type {Y}` split as dupe. 'import-x/no-duplicates': 'error', 'import-x/no-self-import': 'error', 'import-x/no-cycle': 'error', - // 400 lines, counting neither blanks nor comments, so adding an explanation never pushes a file - // over. One file carries an explicit override below, for a stated reason -- a second should - // be a split, not a second override. 'max-lines': ['error', { max: 400, skipBlankLines: true, skipComments: true }], - // An error, not a warning: the three places whose dependency array is deliberately narrower - // than their closure now carry a line-level disable stating why. A new violation should fail. 'react-hooks/exhaustive-deps': 'error', - // Fires on the finding-title normalizer, which strips emoji and variation selectors from model - // output. Those combining characters are the point of it, and its behaviour is pinned by tests. + // Fires on emoji/variation-selector stripping in finding-title normalizer; that's intentional, test-pinned. 'no-misleading-character-class': 'off', - // `any` is used deliberately at the provider and DB boundaries, where the shape is genuinely - // unknown until it is parsed. Turning this on would mean ~100 suppressions, not better types. + // Deliberate at provider/DB boundaries with unknown shape until parsed; ~100 suppressions otherwise. '@typescript-eslint/no-explicit-any': 'off', }, }, @@ -73,14 +59,13 @@ export default tseslint.config( 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. + // Zone block at file bottom only covers packages/**+apps/**, so it can't catch this direction. '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.)' + 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. (@codraoss/schema/review-limits is the sanctioned client-side import.)' } ] }], @@ -88,43 +73,25 @@ export default tseslint.config( }, { - // These specifiers are intercepted BY STRING in test mocks (`vi.mock('@server/db/jobs', ...)`). - // Each one is free to split into sibling files internally, but every other module must keep - // importing the barrel path -- importing a sibling directly bypasses whichever spec mocks the - // barrel, and the test keeps passing while asserting nothing. - // - // Every group MUST list the `@alias/...` form, not just `**/dir/...`. Under the tsconfig paths - // (`@shared/*` -> src/shared/*) the specifier a consumer actually writes is `@shared/schema-claims`, - // whose segments are ["@shared", "schema-claims"] -- there is no literal `shared` segment for - // `**/shared/` to match, so that pattern silently matched nothing at all. `@server/*` groups - // happen to work because `db`/`core`/`review` survive as real segments, but spell both forms out - // rather than relying on that. Probe any new pattern with: - // echo "import '';" | npx eslint --stdin --stdin-filename src/server/probe.ts + // vi.mock() intercepts these specifiers by string; list both `@alias/...` and `**/dir/...` forms since sibling imports and tsconfig-alias imports otherwise bypass it. files: ['src/**/*.{ts,tsx}', 'test/**/*.{ts,tsx}'], rules: { 'no-restricted-imports': ['error', { patterns: [ { group: ['**/db/jobs-*', '@server/db/jobs-*'], message: 'Import from @server/db/jobs, not a sibling. Eight specs vi.mock that specifier; a direct sibling import silently bypasses the mock.' }, { group: ['**/db/file-reviews-*', '@server/db/file-reviews-*'], message: 'Import from @server/db/file-reviews, not a sibling. (No spec mocks this one today; the rule keeps the barrel the single entry point.)' }, - { group: ['**/services/model-review-*', '**/services/model-rate-limits', '**/services/model-chain-runner', '**/services/model-support', '@codra/models-*'], message: 'Import from @codra/models, not a sibling. Four specs vi.mock that specifier.' }, + { group: ['**/services/model-review-*', '**/services/model-rate-limits', '**/services/model-chain-runner', '**/services/model-support', '@codraoss/models-*'], message: 'Import from @codraoss/models, not a sibling. Four specs vi.mock that specifier.' }, { 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/*', '@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.)' }, + { group: ['**/core/review/*', '@server/core/review/*', '@codraoss/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/*', '@codraoss/core/model-output/*'], message: 'Import from @codraoss/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', '@codraoss/core/diff/position'], message: 'Import from @codraoss/core/diff, not a sibling.' }, + { group: ['**/schema-claims', '**/schema-repo-config', '**/schema-enums', '@codraoss/schema/schema-claims', '@codraoss/schema/schema-repo-config', '@codraoss/schema/schema-enums'], message: 'Import from @codraoss/schema, not a sibling. (@codraoss/schema/review-limits is exempt: the client imports it directly to keep zod out of the browser bundle.)' }, ], }], }, }, { - // The one file still over the limit, for a stated reason. Known work, not a permanent - // carve-out -- delete the entry rather than raising `max` when it is split. - // - // test/api/auth.spec.ts (422): the review-settings suites here read-modify-write the same - // singleton `global_settings` row set and race across files once `fileParallelism` is on. See - // the DO-NOT-SPLIT header on the file itself. + // auth.spec.ts (422 lines): suites read-modify-write singleton global_settings, racing under fileParallelism (see DO-NOT-SPLIT header there). Delete entry, don't raise max, once split. files: ['test/api/auth.spec.ts'], rules: { 'max-lines': 'off', @@ -132,15 +99,11 @@ export default tseslint.config( }, { - // The barrel files themselves are the one place allowed to import their own siblings. files: [ 'src/server/db/jobs.ts', 'src/server/db/file-reviews.ts', 'src/server/services/model.ts', 'src/server/core/github/index.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: { @@ -149,7 +112,6 @@ export default tseslint.config( }, { - // Plain-JS scripts are not covered by tsconfig, so they need their globals declared. files: ['scripts/**/*.{js,mjs}'], languageOptions: { globals: { @@ -171,10 +133,7 @@ export default tseslint.config( '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. + // `src/**` in `from` catches a moved file that kept its old `@server/*` import (re-coupling). target: 'packages/schema/**/*', from: ['src/**/*', 'test/**/*', 'scripts/**/*', 'packages/core/**/*', 'packages/provider-github/**/*', 'packages/db/**/*', 'packages/models/**/*', 'packages/api/**/*', 'packages/ui/**/*', 'apps/worker/**/*', 'apps/dashboard/**/*'] }, diff --git a/package-lock.json b/package-lock.json index d74001b2..13532691 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,9 +14,13 @@ ], "dependencies": { "@base-ui/react": "^1.6.0", - "@codra/core": "*", - "@codra/schema": "*", - "@codra/ui": "*", + "@codraoss/api": "*", + "@codraoss/core": "*", + "@codraoss/db": "*", + "@codraoss/models": "*", + "@codraoss/provider-github": "*", + "@codraoss/schema": "*", + "@codraoss/ui": "*", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "hono": "^4.12.25", @@ -39,6 +43,8 @@ "zod": "^4.3.6" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.18.5", + "@changesets/cli": "^3.0.0", "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.2.2", "@testing-library/dom": "^10.4.1", @@ -56,7 +62,9 @@ "jsdom": "^29.0.2", "ora": "^9.4.1", "prompts": "^2.4.2", + "publint": "^0.3.23", "tailwindcss": "^4.2.2", + "tsup": "^8.5.1", "typescript": "^6.0.2", "typescript-eslint": "^8.66.0", "vite": "^8.0.8", @@ -65,12 +73,15 @@ } }, "apps/worker": { - "name": "@codra/worker", + "name": "@codraoss/worker", "version": "0.9.4", "dependencies": { - "@codra/core": "*", - "@codra/db": "*", - "@codra/schema": "*", + "@codraoss/api": "*", + "@codraoss/core": "*", + "@codraoss/db": "*", + "@codraoss/models": "*", + "@codraoss/provider-github": "*", + "@codraoss/schema": "*", "hono": "^4.12.25" }, "devDependencies": { @@ -85,6 +96,115 @@ "dev": true, "license": "MIT OR Apache-2.0" }, + "node_modules/@andrewbranch/untar.js": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@andrewbranch/untar.js/-/untar.js-1.0.4.tgz", + "integrity": "sha512-pVXSwPsLuw8IGLo2Di0EaOfsk+ntVvpkk942J/sHYIkwvtKUakEcPh7HBgZ6tuimgzKSEHgCvO4XgQ05DEbwDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@arethetypeswrong/cli": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/cli/-/cli-0.18.5.tgz", + "integrity": "sha512-gM+8vRsQOD/Uc7EnBedUhkG5OCsDWE4uoak5QvomGpMpaky0Eh41p04nIMgrWb8EOmqZUJGc6zz9hsP6E56R7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@arethetypeswrong/core": "0.18.5", + "chalk": "^4.1.2", + "cli-table3": "^0.6.3", + "commander": "^10.0.1", + "marked": "^9.1.2", + "marked-terminal": "^7.1.0", + "semver": "^7.5.4" + }, + "bin": { + "attw": "dist/index.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@arethetypeswrong/cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@arethetypeswrong/cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@arethetypeswrong/cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@arethetypeswrong/core": { + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/@arethetypeswrong/core/-/core-0.18.5.tgz", + "integrity": "sha512-9ytjzGwxjm9Uz7I9avfbt5vlQt6uk9uRRESzJjqrznl6WKvI6dwYTo+vJ3U02Wrq/mR3iql/PzhvHhKdJIAjDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@andrewbranch/untar.js": "^1.0.3", + "@loaderkit/resolve": "^1.0.2", + "cjs-module-lexer": "^1.2.3", + "fflate": "^0.8.3", + "lru-cache": "^11.0.1", + "semver": "^7.5.4", + "typescript": "5.6.1-rc", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@arethetypeswrong/core/node_modules/typescript": { + "version": "5.6.1-rc", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.1-rc.tgz", + "integrity": "sha512-E3b2+1zEFu84jB0YQi9BORDjz9+jGbwwy1Zi3G0LUNw7a7cePUrHMRNy8aPh53nXpkFGVHSxIZo5vKTfYaFiBQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "5.1.11", "dev": true, @@ -461,6 +581,13 @@ } } }, + "node_modules/@braidai/lang": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@braidai/lang/-/lang-1.1.2.tgz", + "integrity": "sha512-qBcknbBufNHlui137Hft8xauQMTZDKdophmLFv05r2eNmdIv/MlPuP4TdUknHG68UdWLgVZwgxVe735HzJNIwA==", + "dev": true, + "license": "ISC" + }, "node_modules/@bramus/specificity": { "version": "2.4.2", "dev": true, @@ -472,6 +599,289 @@ "specificity": "bin/cli.js" } }, + "node_modules/@changesets/apply-release-plan": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-8.0.0.tgz", + "integrity": "sha512-kUd2pbf1w5/AYmBMb0Tt+rkIPCjFJdT0SZMrkOjJT/WV/QbtmvkyB5jkV0oaNPheavprZk+SfUiozUti7TIL2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/config": "^4.0.0", + "@changesets/format": "^0.1.1", + "@changesets/git": "^4.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "jsonc-parser": "^3.3.1", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/assemble-release-plan": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-7.0.0.tgz", + "integrity": "sha512-oEW8BxdA604kGGtDSCiHr5w9Tv4UWe9I2k61IBNZzCOE1kbYaJj4v+lFQNgcEZFkUc2pV/+hASErGDvpJOZCTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/changelog-git": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/changelog-git/-/changelog-git-1.0.0.tgz", + "integrity": "sha512-3Dst2Ime2Op5nd4XmWJLPIgp11ZFqJqSkVug9izK6TDcIV4YlhPS4ECbEVR+eGI0bk0r1ItogD4j2Oli87bJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/cli": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/cli/-/cli-3.0.0.tgz", + "integrity": "sha512-V7Gm+GP5OT3mJinMI2YcJD/JyO/a4WChnaMCCzTRhWHgu1zhtsyY7zCjP6n5W6zsgSjJKs+yPmvtREvE0F2g8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/apply-release-plan": "^8.0.0", + "@changesets/assemble-release-plan": "^7.0.0", + "@changesets/changelog-git": "^1.0.0", + "@changesets/config": "^4.0.0", + "@changesets/errors": "^1.0.0", + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/git": "^4.0.0", + "@changesets/pre": "^3.0.0", + "@changesets/read": "^1.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@changesets/write": "^1.0.0", + "@clack/prompts": "^1.7.0", + "@manypkg/get-packages": "^3.1.0", + "@pnpm/deps.graph-sequencer": "^1100.0.1", + "cac": "^7.0.0", + "import-meta-resolve": "^4.2.0", + "launch-editor": "^2.14.1", + "package-manager-detector": "^1.6.0", + "semver": "^7.8.1", + "tinyexec": "^1.3.0" + }, + "bin": { + "changeset": "bin.js" + }, + "engines": { + "node": "^22.11 || ^24 || >=26", + "npm": ">=10.9.0", + "pnpm": ">=10.0.0", + "yarn": ">=4.5.2" + } + }, + "node_modules/@changesets/cli/node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@changesets/config": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@changesets/config/-/config-4.0.0.tgz", + "integrity": "sha512-mw95/YrkOuhZZxfnVAA4bSXOFUi+KlhzOBTM8C4x777NhUU6HWIl9Z+K+nME+E4PVsv5NQVQwTfiHihAS1A/ow==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/get-dependents-graph": "^3.0.0", + "@changesets/should-skip-package": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/errors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/errors/-/errors-1.0.0.tgz", + "integrity": "sha512-ElN/mEzn6zmETgjwf5MclCMa9ef59sAR0lfO8VSYIsiRvbC2FbLB/92EoYw10Sl0kGixxHFiJZUSv7dA+YpR8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/format": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@changesets/format/-/format-0.1.2.tgz", + "integrity": "sha512-Caez5XtNXCFS/G5bwyav3wuXL0tMxVd2ZGbaumWbzN08tyzO21asCw7JZhNtVsAZDCvDRUzZN+Iit9SyRITSYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.8.0", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/get-dependents-graph": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-3.0.0.tgz", + "integrity": "sha512-ji/t5wFA1zREKXRUePE6Qi+Qu2UgxCeSSGQrphezwvQZrp49B7sJ+8+wvM0tA7zPeSxYKCojDy3WWgrl+s+awg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0", + "semver": "^7.8.1" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/git": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@changesets/git/-/git-4.0.0.tgz", + "integrity": "sha512-uIEswpPUgzBBqrC0qg13byNaPorzhf05LE2T+gRizEKCXvMsJC6NPJp5iNDSV/gYj4Viqh09UiOF5E7XWeH5lQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0", + "picomatch": "^4.0.4", + "tinyexec": "^1.3.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/parse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/parse/-/parse-1.0.0.tgz", + "integrity": "sha512-P0iaMb9p9CRYZiTgAllEIF9AUMQHIy1G72tKlcIqJp61icZSsKQNiOPdxAMZG8m/DvZwt/oz5xEbTpike//dWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0", + "yaml": "^2.9.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/pre": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@changesets/pre/-/pre-3.0.0.tgz", + "integrity": "sha512-Zm/6YliV/a2oeWTqHJf6KxLrQwgcK1i/BRDl2m0EKZvbnxV5fG9QRhwJJGshjsZTUTS6dkfURQ2K6aAvgNw/3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/errors": "^1.0.0", + "@changesets/types": "^7.0.0", + "@manypkg/get-packages": "^3.1.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/read": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/read/-/read-1.0.0.tgz", + "integrity": "sha512-8TdE2PwG6yArPt5Ozej83Z6iHz1G8BDBKvIEKZ455MccR4K3GNbLR2XqjBxa+5yLFnEd3xAOPXYboBg1pt8stQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/git": "^4.0.0", + "@changesets/parse": "^1.0.0", + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/should-skip-package": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/should-skip-package/-/should-skip-package-1.0.0.tgz", + "integrity": "sha512-pwqoJmbONn1XgXmZXPEExgAaT+HdZLjALFTDgIm+PnS5KeO2nLtzA2/Q+4aMFY14kFMuXKG30MObhvDzWgzDgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/types": "^7.0.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/types": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@changesets/types/-/types-7.0.0.tgz", + "integrity": "sha512-c5GoiQyt3pxiXjrWSNoP8/GRf4kG+VnKzovx1OQM8dYYALlSwgedmkPmJ+ZqGxqwg9D3Bkj85Uo4KLd5BN3A0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@changesets/write": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@changesets/write/-/write-1.0.0.tgz", + "integrity": "sha512-xCK3/4C7Z7muQB/wguE6HcbjtY9iOtaK9orZKE+7xi573cMGD8rLZz7qlo6IvK7s+SIUIKajzj1l+F1QuP97tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@changesets/format": "^0.1.1", + "@changesets/types": "^7.0.0", + "human-id": "^4.2.0" + }, + "engines": { + "node": "^22.11 || ^24 || >=26" + } + }, + "node_modules/@clack/core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.4.3.tgz", + "integrity": "sha512-/kr3UWNtdJfxZtPgDqUOmG2pvwlmcLGheex5yiZKdwbzZJxhV+HMNR9QNmyY5cGwTNV6LrR7Jtp+KjhUAP1qBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.7.0.tgz", + "integrity": "sha512-y7/yvZ2TPAnR9+jnc00klvNNLkJiXFFrQA/hlLCcxA9a2A4zQIOimyFQ9XfwYKiGD1fb5GY8vbKIIgO8d5Tb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@clack/core": "1.4.3", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, "node_modules/@cloudflare/kv-asset-handler": { "version": "0.5.0", "dev": true, @@ -579,38 +989,49 @@ "node": ">=16" } }, - "node_modules/@codra/api": { + "node_modules/@codraoss/api": { "resolved": "packages/api", "link": true }, - "node_modules/@codra/core": { + "node_modules/@codraoss/core": { "resolved": "packages/core", "link": true }, - "node_modules/@codra/db": { + "node_modules/@codraoss/db": { "resolved": "packages/db", "link": true }, - "node_modules/@codra/models": { + "node_modules/@codraoss/models": { "resolved": "packages/models", "link": true }, - "node_modules/@codra/provider-github": { + "node_modules/@codraoss/provider-github": { "resolved": "packages/provider-github", "link": true }, - "node_modules/@codra/schema": { + "node_modules/@codraoss/schema": { "resolved": "packages/schema", "link": true }, - "node_modules/@codra/ui": { + "node_modules/@codraoss/ui": { "resolved": "packages/ui", "link": true }, - "node_modules/@codra/worker": { + "node_modules/@codraoss/worker": { "resolved": "apps/worker", "link": true }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -2092,45 +2513,130 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", - "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "node_modules/@loaderkit/resolve": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@loaderkit/resolve/-/resolve-1.0.6.tgz", + "integrity": "sha512-G8FdIoF5CypfwmD9rl8BXod5HDn8JqB0CCNBXDTaRZ+yRYhARrrSToX1zg1zy9jX3zLqigsELwhT4gNtkdQAUg==", + "dev": true, + "license": "ISC", + "dependencies": { + "@braidai/lang": "^1.0.0" + } + }, + "node_modules/@manypkg/find-root": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/find-root/-/find-root-3.1.0.tgz", + "integrity": "sha512-BcSqCyKhBVZ5YkSzOiheMCV41kqAFptW6xGqYSTjkVTl9XQpr+pqHhwgGCOHQtjDCv7Is6EFyA14Sm5GVbVABA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@tybys/wasm-util": "^0.10.3" + "@manypkg/tools": "^2.1.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + "node": ">=20.0.0" } }, - "node_modules/@oxc-project/types": { - "version": "0.137.0", + "node_modules/@manypkg/get-packages": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@manypkg/get-packages/-/get-packages-3.1.0.tgz", + "integrity": "sha512-0TbBVyvPrP7xGYBI/cP8UP+yl/z+HtbTttAD7FMAJgn/kXOTwh5/60TsqP9ZYY710forNfyV0N8P/IE/ujGZJg==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "dependencies": { + "@manypkg/find-root": "^3.1.0", + "@manypkg/tools": "^2.1.0" + }, + "engines": { + "node": ">=20.0.0" } }, - "node_modules/@poppinss/colors": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", - "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "node_modules/@manypkg/tools": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@manypkg/tools/-/tools-2.1.2.tgz", + "integrity": "sha512-6QEf6yqFbETdwGITKq57aYoPfX/3K8XFNwsAlx0C1M7o8cb79sv1M3w+tWuWvIcSbNqrLF7OD7YpZMVVz335hQ==", "dev": true, "license": "MIT", "dependencies": { - "kleur": "^4.1.5" - } + "jju": "^1.4.0", + "tinyglobby": "^0.2.13", + "yaml": "^2.9.0" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", + "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pnpm/deps.graph-sequencer": { + "version": "1100.0.1", + "resolved": "https://registry.npmjs.org/@pnpm/deps.graph-sequencer/-/deps.graph-sequencer-1100.0.1.tgz", + "integrity": "sha512-pOr5+q1fLYKwFN3LAJuGZEnfXDcQ73zqgDHMtGy+K+uIoUqyY+6MeDCWFwfu+4EFuq76I5EPFofoNAI+Bmmq4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.13" + }, + "funding": { + "url": "https://opencollective.com/pnpm" + } + }, + "node_modules/@poppinss/colors": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@poppinss/colors/-/colors-4.1.6.tgz", + "integrity": "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^4.1.5" + } }, "node_modules/@poppinss/colors/node_modules/kleur": { "version": "4.1.5", @@ -2174,6 +2680,22 @@ "dev": true, "license": "MIT" }, + "node_modules/@publint/pack": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.6.tgz", + "integrity": "sha512-3uVNyGcVplhPZSLVyeIpL7+cIRn1YCSNHLG/rUIlBQMVH8YuN9++YF+5+UDIIO9RW98dujiUoTltO7RDB5bFJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyexec": "^1.2.4" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, "node_modules/@reduxjs/toolkit": { "version": "2.12.0", "license": "MIT", @@ -2484,74 +3006,24 @@ "dev": true, "license": "MIT" }, - "node_modules/@sindresorhus/is": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", - "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@speed-highlight/core": { - "version": "1.2.23", - "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.23.tgz", - "integrity": "sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg==", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/@standard-schema/utils": { - "version": "0.3.0", - "license": "MIT" - }, - "node_modules/@tailwindcss/node": { - "version": "4.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/remapping": "^2.3.5", - "enhanced-resolve": "5.21.6", - "jiti": "^2.7.0", - "lightningcss": "1.32.0", - "magic-string": "^0.30.21", - "source-map-js": "^1.2.1", - "tailwindcss": "4.3.2" - } - }, - "node_modules/@tailwindcss/oxide": { - "version": "4.3.2", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "engines": { - "node": ">= 20" - }, - "optionalDependencies": { - "@tailwindcss/oxide-android-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-arm64": "4.3.2", - "@tailwindcss/oxide-darwin-x64": "4.3.2", - "@tailwindcss/oxide-freebsd-x64": "4.3.2", - "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", - "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", - "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", - "@tailwindcss/oxide-linux-x64-musl": "4.3.2", - "@tailwindcss/oxide-wasm32-wasi": "4.3.2", - "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", - "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@tailwindcss/oxide-android-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", - "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", "cpu": [ "arm64" ], @@ -2560,15 +3032,12 @@ "optional": true, "os": [ "android" - ], - "engines": { - "node": ">= 20" - } + ] }, - "node_modules/@tailwindcss/oxide-darwin-arm64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", - "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", "cpu": [ "arm64" ], @@ -2577,15 +3046,12 @@ "optional": true, "os": [ "darwin" - ], - "engines": { - "node": ">= 20" - } + ] }, - "node_modules/@tailwindcss/oxide-darwin-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", - "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", "cpu": [ "x64" ], @@ -2594,15 +3060,26 @@ "optional": true, "os": [ "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" ], - "engines": { - "node": ">= 20" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/@tailwindcss/oxide-freebsd-x64": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", - "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", "cpu": [ "x64" ], @@ -2611,32 +3088,46 @@ "optional": true, "os": [ "freebsd" - ], - "engines": { - "node": ">= 20" - } + ] }, - "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", - "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", "cpu": [ "arm" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" ], - "engines": { - "node": ">= 20" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", - "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", "cpu": [ "arm64" ], @@ -2648,15 +3139,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 20" - } + ] }, - "node_modules/@tailwindcss/oxide-linux-arm64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", - "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", "cpu": [ "arm64" ], @@ -2668,17 +3156,14 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 20" - } + ] }, - "node_modules/@tailwindcss/oxide-linux-x64-gnu": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", - "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", "cpu": [ - "x64" + "loong64" ], "dev": true, "libc": [ @@ -2688,17 +3173,14 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 20" - } + ] }, - "node_modules/@tailwindcss/oxide-linux-x64-musl": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", - "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", "cpu": [ - "x64" + "loong64" ], "dev": true, "libc": [ @@ -2708,3146 +3190,4907 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": ">= 20" - } + ] }, - "node_modules/@tailwindcss/oxide-wasm32-wasi": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", - "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", - "bundleDependencies": [ - "@napi-rs/wasm-runtime", - "@emnapi/core", - "@emnapi/runtime", - "@tybys/wasm-util", - "@emnapi/wasi-threads", - "tslib" + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", "cpu": [ - "wasm32" + "ppc64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/core": "^1.11.1", - "@emnapi/runtime": "^1.11.1", - "@emnapi/wasi-threads": "^1.2.2", - "@napi-rs/wasm-runtime": "^1.1.4", - "@tybys/wasm-util": "^0.10.2", - "tslib": "^2.8.1" - }, - "engines": { - "node": ">=14.0.0" - } + "os": [ + "linux" + ] }, - "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", - "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", "cpu": [ - "arm64" + "riscv64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" ], - "engines": { - "node": ">= 20" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@tailwindcss/oxide-win32-x64-msvc": { - "version": "4.3.2", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", "cpu": [ "x64" ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ - "win32" + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" ], - "engines": { - "node": ">= 20" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@tailwindcss/vite": { - "version": "4.3.2", + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@tailwindcss/node": "4.3.2", - "@tailwindcss/oxide": "4.3.2", - "tailwindcss": "4.3.2" - }, - "peerDependencies": { - "vite": "^5.2.0 || ^6 || ^7 || ^8" - } + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/@testing-library/dom": { - "version": "10.4.1", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.3.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "picocolors": "1.1.1", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=18" - } + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/@testing-library/react": { - "version": "16.3.2", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.5" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@testing-library/dom": "^10.0.0", - "@types/react": "^18.0.0 || ^19.0.0", - "@types/react-dom": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } + "os": [ + "win32" + ] }, - "node_modules/@types/aria-query": { - "version": "5.0.4", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@types/chai": { - "version": "5.2.3", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@sindresorhus/is": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.2.0.tgz", + "integrity": "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@types/d3-array": { - "version": "3.2.2", - "license": "MIT" + "node_modules/@speed-highlight/core": { + "version": "1.2.23", + "resolved": "https://registry.npmjs.org/@speed-highlight/core/-/core-1.2.23.tgz", + "integrity": "sha512-iRoq6i6JDJP6Mt2A5JaPvzw0pgYHH6k92ij+yXiTrB7T2y9N789aWE3EHWj/5ztlJBokcCBja3iYLVdu5wgnkg==", + "dev": true, + "license": "CC0-1.0" }, - "node_modules/@types/d3-color": { - "version": "3.1.3", + "node_modules/@standard-schema/spec": { + "version": "1.1.0", "license": "MIT" }, - "node_modules/@types/d3-ease": { - "version": "3.0.2", + "node_modules/@standard-schema/utils": { + "version": "0.3.0", "license": "MIT" }, - "node_modules/@types/d3-interpolate": { - "version": "3.0.4", + "node_modules/@tailwindcss/node": { + "version": "4.3.2", + "dev": true, "license": "MIT", "dependencies": { - "@types/d3-color": "*" + "@jridgewell/remapping": "^2.3.5", + "enhanced-resolve": "5.21.6", + "jiti": "^2.7.0", + "lightningcss": "1.32.0", + "magic-string": "^0.30.21", + "source-map-js": "^1.2.1", + "tailwindcss": "4.3.2" } }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "license": "MIT" - }, - "node_modules/@types/d3-scale": { - "version": "4.0.9", + "node_modules/@tailwindcss/oxide": { + "version": "4.3.2", + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-time": "*" + "engines": { + "node": ">= 20" + }, + "optionalDependencies": { + "@tailwindcss/oxide-android-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-arm64": "4.3.2", + "@tailwindcss/oxide-darwin-x64": "4.3.2", + "@tailwindcss/oxide-freebsd-x64": "4.3.2", + "@tailwindcss/oxide-linux-arm-gnueabihf": "4.3.2", + "@tailwindcss/oxide-linux-arm64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-arm64-musl": "4.3.2", + "@tailwindcss/oxide-linux-x64-gnu": "4.3.2", + "@tailwindcss/oxide-linux-x64-musl": "4.3.2", + "@tailwindcss/oxide-wasm32-wasi": "4.3.2", + "@tailwindcss/oxide-win32-arm64-msvc": "4.3.2", + "@tailwindcss/oxide-win32-x64-msvc": "4.3.2" } }, - "node_modules/@types/d3-shape": { - "version": "3.1.8", + "node_modules/@tailwindcss/oxide-android-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.3.2.tgz", + "integrity": "sha512-WHxqIuHpvZ5VtdX6GTl1Ik/Vp2YuN42Et+0CdeaVd/frQ9jAvGmvR8vLT+jk3e8/Q3x8kECB9+R17pgpp2BulA==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/d3-path": "*" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/@types/d3-time": { - "version": "3.0.4", - "license": "MIT" - }, - "node_modules/@types/d3-timer": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/@types/debug": { - "version": "4.1.13", + "node_modules/@tailwindcss/oxide-darwin-arm64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.3.2.tgz", + "integrity": "sha512-GZypeUY/IDJW3877KeM+O67vbXr3MBnbtEL4aYhNErv/JWZhye2vGSWWG9tB6iiqR2MqRNkY8IOUy4NdSZV26w==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/ms": "*" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "node_modules/@tailwindcss/oxide-darwin-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.3.2.tgz", + "integrity": "sha512-UIIzmefR6KO1sDU7MzRqAxC8iBpft/VhkGjTjnhoS6k7Z3rQ9wEgA1ODSiyH/tcSYssulNm4Ci3hOeK1jH7ccQ==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", "license": "MIT", - "dependencies": { - "@types/estree": "*" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/@types/hast": { - "version": "3.0.4", + "node_modules/@tailwindcss/oxide-freebsd-x64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.3.2.tgz", + "integrity": "sha512-GN+uAmcI6DNspnCDwtOAZrTz6oukJnp337qZvxqCGLd3BHBzJpO0ZbTLRvJNdztOeAmTzewewGIMPb0tk2R4WA==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "@types/unist": "*" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.3.2.tgz", + "integrity": "sha512-4ABn7qSbdHRwTiDiuWNegCyb5+2FJ4vKIKc3DmKrvAFw7MU1Lm11dIkTPwUaFdTzc7IsOpDbqBrlh0x6y36U/w==", + "cpu": [ + "arm" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", "license": "MIT", - "dependencies": { - "@types/unist": "*" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/@types/ms": { - "version": "2.1.0", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "25.9.4", + "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.3.2.tgz", + "integrity": "sha512-wDgEIGwoM8w8pufh9LVt1PahDgNdKXrLC2qfAnV3vAmococ9RWbxeAw4pxPttd/TsJfwjyLf90Dg1y9y8I6Emw==", + "cpu": [ + "arm64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "undici-types": ">=7.24.0 <7.24.7" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/@types/picomatch": { - "version": "4.0.3", + "node_modules/@tailwindcss/oxide-linux-arm64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.3.2.tgz", + "integrity": "sha512-J5Nuk0uZQIiMTJj3LEx4sAA9tMFUoXQZFv1J6An+QGYe53HKRJuFDi0rpq/tuouCZeAbOBY3kQ6g8qeD4TUjtA==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.2.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", - "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "libc": [ + "musl" + ], "license": "MIT", - "dependencies": { - "csstype": "^3.2.2" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/@types/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "node_modules/@tailwindcss/oxide-linux-x64-gnu": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.3.2.tgz", + "integrity": "sha512-kqCZpSKOBEJO4mz7OqWoofBZeXTAwaVGPj0ErAj7CojmhKpWVWVOnrt9dE8odoIraZq4oj3ausM37kXi+Tow8w==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "peerDependencies": { - "@types/react": "^19.2.0" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 20" } }, - "node_modules/@types/unist": { - "version": "3.0.3", - "license": "MIT" - }, - "node_modules/@types/use-sync-external-store": { - "version": "0.0.6", - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", - "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", + "node_modules/@tailwindcss/oxide-linux-x64-musl": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.3.2.tgz", + "integrity": "sha512-cixpqbh2toJDmkuCRI68nXA8ZxNmdK9Y+9v5h3MC3ZQKy/0BO8AWzlkWyRM7JAFSGBlfig4YVTPsK6MVgqz1uw==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/type-utils": "8.66.0", - "@typescript-eslint/utils": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.5.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.66.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">= 20" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", - "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", - "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", + "node_modules/@tailwindcss/oxide-wasm32-wasi": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.3.2.tgz", + "integrity": "sha512-4ec2Z/LOmRsAgU23CS4xeJfcJlmRg94A/XrbGRCF1gyU/zdDfRLYDVsS+ynSZCmGNxQ1jQriQOKMQeQxBA3Isw==", + "bundleDependencies": [ + "@napi-rs/wasm-runtime", + "@emnapi/core", + "@emnapi/runtime", + "@tybys/wasm-util", + "@emnapi/wasi-threads", + "tslib" + ], + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", - "debug": "^4.4.3" + "@emnapi/core": "^1.11.1", + "@emnapi/runtime": "^1.11.1", + "@emnapi/wasi-threads": "^1.2.2", + "@napi-rs/wasm-runtime": "^1.1.4", + "@tybys/wasm-util": "^0.10.2", + "tslib": "^2.8.1" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "node": ">=14.0.0" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", - "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", + "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.3.2.tgz", + "integrity": "sha512-Zyr/M0+XcYZu3bZrUytc7TXvrk0ftWfl8gN2MwekNDzhqhKRUucMPSeOzM0o0wH5AWOU49BsKRrfKxI2atCPMQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.66.0", - "@typescript-eslint/types": "^8.66.0", - "debug": "^4.4.3" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "node": ">= 20" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", - "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", + "node_modules/@tailwindcss/oxide-win32-x64-msvc": { + "version": "4.3.2", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0" - }, + "optional": true, + "os": [ + "win32" + ], "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">= 20" } }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", - "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", + "node_modules/@tailwindcss/vite": { + "version": "4.3.2", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "dependencies": { + "@tailwindcss/node": "4.3.2", + "@tailwindcss/oxide": "4.3.2", + "tailwindcss": "4.3.2" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", - "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", + "node_modules/@testing-library/dom": { + "version": "10.4.1", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0", - "@typescript-eslint/utils": "8.66.0", - "debug": "^4.4.3", - "ts-api-utils": "^2.5.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" - } - }, - "node_modules/@typescript-eslint/types": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", - "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", - "dev": true, - "license": "MIT", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=18" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", - "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", + "node_modules/@testing-library/react": { + "version": "16.3.2", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.66.0", - "@typescript-eslint/tsconfig-utils": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/visitor-keys": "8.66.0", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.5.0" + "@babel/runtime": "^7.12.5" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=18" }, "peerDependencies": { - "typescript": ">=4.8.4 <6.1.0" + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", - "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", "dev": true, "license": "MIT", + "optional": true, "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.66.0", - "@typescript-eslint/types": "8.66.0", - "@typescript-eslint/typescript-estree": "8.66.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.1.0" + "tslib": "^2.4.0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.66.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", - "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", + "node_modules/@types/aria-query": { + "version": "5.0.4", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.66.0", - "eslint-visitor-keys": "^5.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.2", - "license": "ISC" + "node_modules/@types/d3-array": { + "version": "3.2.2", + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-android-arm-eabi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", - "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "node_modules/@types/d3-color": { + "version": "3.1.3", + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-android-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", - "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@types/d3-color": "*" + } }, - "node_modules/@unrs/resolver-binding-darwin-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", - "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", - "cpu": [ - "arm64" - ], - "dev": true, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@types/d3-time": "*" + } }, - "node_modules/@unrs/resolver-binding-darwin-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", - "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@types/d3-shape": { + "version": "3.1.8", "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] + "dependencies": { + "@types/d3-path": "*" + } }, - "node_modules/@unrs/resolver-binding-freebsd-x64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", - "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", - "cpu": [ - "x64" - ], - "dev": true, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "dependencies": { + "@types/ms": "*" + } }, - "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", - "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", - "cpu": [ - "arm" - ], + "node_modules/@types/deep-eql": { + "version": "4.0.2", "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", - "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", - "cpu": [ - "arm" - ], + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@types/estree": "*" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", - "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], + "node_modules/@types/hast": { + "version": "3.0.4", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@types/unist": "*" + } }, - "node_modules/@unrs/resolver-binding-linux-arm64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", - "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", - "cpu": [ - "arm64" - ], + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, - "libc": [ - "musl" - ], + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@types/unist": "*" + } }, - "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", - "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", - "cpu": [ - "loong64" - ], + "node_modules/@types/ms": { + "version": "2.1.0", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.4", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } }, - "node_modules/@unrs/resolver-binding-linux-loong64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", - "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", - "cpu": [ - "loong64" - ], + "node_modules/@types/picomatch": { + "version": "4.0.3", "dev": true, - "libc": [ - "musl" - ], + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "csstype": "^3.2.2" + } }, - "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", - "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", - "cpu": [ - "ppc64" - ], + "node_modules/@types/react-dom": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "peerDependencies": { + "@types/react": "^19.2.0" + } }, - "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", - "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", - "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", - "cpu": [ - "riscv64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@types/unist": { + "version": "3.0.3", + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", - "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", - "cpu": [ - "s390x" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "license": "MIT" }, - "node_modules/@unrs/resolver-binding-linux-x64-gnu": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", - "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.66.0.tgz", + "integrity": "sha512-p088eaGrzYz1s+7cov0aMOCkNGTJlVxF4jgubf28c8L0Cv9Rloj8YBHnv4hXLq6IIEE1AsjNWavO+k+8kP2Y0A==", "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/type-utils": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.66.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/@unrs/resolver-binding-linux-x64-musl": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", - "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", - "cpu": [ - "x64" - ], + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", "dev": true, - "libc": [ - "musl" - ], "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": ">= 4" + } }, - "node_modules/@unrs/resolver-binding-openharmony-arm64": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", - "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/parser": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.66.0.tgz", + "integrity": "sha512-X6ypGChaWYk6PBtUg2BwuTZEFFcHJAtGTVJ9/lCTOufhZ4i9fNolQNnktq+kkMCwMj7V8Svsq7+TxSDslmhE0g==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ] + "dependencies": { + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", - "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", - "cpu": [ - "wasm32" - ], + "node_modules/@typescript-eslint/project-service": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.66.0.tgz", + "integrity": "sha512-7MthGPTt4BP69lSryqpqq8HQqxuzynssckL/jyDyk3+TNMQ3y2jFWkptCrktWvBrP+EH787Nl5N5Qpw7WZg+5g==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "1.10.0", - "@emnapi/runtime": "1.10.0", - "@napi-rs/wasm-runtime": "^1.1.4" + "@typescript-eslint/tsconfig-utils": "^8.66.0", + "@typescript-eslint/types": "^8.66.0", + "debug": "^4.4.3" }, "engines": { - "node": ">=14.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", - "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.66.0.tgz", + "integrity": "sha512-8TGcH25j9zqJ/IULB/ppyhRvxA8QYfFEZ7nfbg6/BN9spDgb8fPWQXlE5l8TWBL50EtUx007uZ1o9VOwrq2/9g==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.1", - "tslib": "^2.4.0" + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", - "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.66.0.tgz", + "integrity": "sha512-9D5gLYZG4rOjcoag8MQ/fWI8WqA9wcPDyOGyWtWFhvM1lHRbliqUSPIY5J3zqCU1tvSwzXxnnjhQhz5Ne7mJ4g==", "dev": true, "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", - "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.66.0.tgz", + "integrity": "sha512-LG2dWfjZQQp0ADtAu/EWJVayefGL2UEZ3CDeI44D9v3rXB/WYUqE/jpO28KrEKul5AySrmI+Zh1v6v+xW2U9+g==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0", + "@typescript-eslint/utils": "8.66.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", - "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", - "cpu": [ - "arm64" - ], + "node_modules/@typescript-eslint/types": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.66.0.tgz", + "integrity": "sha512-H6gcYaSDOyvL3AD/jHUtUFo2jqGgn/F6nuyuZSu0QTesxL+cP4dQoIMrODRofuJC09g64+WgZ6tE19Y1N2YIFQ==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", - "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@unrs/resolver-binding-win32-x64-msvc": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", - "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@vitejs/plugin-react": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", - "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@rolldown/pluginutils": "^1.0.1" - }, "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "peerDependencies": { - "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", - "babel-plugin-react-compiler": "^1.0.0", - "vite": "^8.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "peerDependenciesMeta": { - "@rolldown/plugin-babel": { - "optional": true - }, - "babel-plugin-react-compiler": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.66.0.tgz", + "integrity": "sha512-8/x4INiiQb10jGgXYD7116/zQ+OL84ZIFn0za68wwFHCanT/VLbBEroWht8RV8fn0/ZCAoazHLQgwUC0UQcDfg==", "dev": true, "license": "MIT", "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" + "@typescript-eslint/project-service": "8.66.0", + "@typescript-eslint/tsconfig-utils": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/visitor-keys": "8.66.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "node_modules/@typescript-eslint/utils": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.66.0.tgz", + "integrity": "sha512-jasearZPolBw5NJNYGMwxzHMF83niVWmMU1VdHzG1CyfI2VS7f7nZltnKtHcg20hW+7Uo5GfK4MeDPoU3qI8EA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.66.0", + "@typescript-eslint/types": "8.66.0", + "@typescript-eslint/typescript-estree": "8.66.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" } }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.66.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.66.0.tgz", + "integrity": "sha512-dkKR8q+lKciskj1Y3vthHktl+3cMLWGyVUP23bRiPZ5O9BRT++4EqDDV+TVeIKBL1VXVEqrJlz8MYbcnvJcAlg==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^3.1.0" + "@typescript-eslint/types": "8.66.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/vitest" + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "node_modules/@ungap/structured-clone": { + "version": "1.3.2", + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.12.2.tgz", + "integrity": "sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.12.2.tgz", + "integrity": "sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "optional": true, + "os": [ + "android" + ] }, - "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.12.2.tgz", + "integrity": "sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.12.2.tgz", + "integrity": "sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/acorn": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", - "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.12.2.tgz", + "integrity": "sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } + "optional": true, + "os": [ + "freebsd" + ] }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.12.2.tgz", + "integrity": "sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.12.2.tgz", + "integrity": "sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/ansi-regex": { - "version": "5.0.1", + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.12.2.tgz", + "integrity": "sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==", + "cpu": [ + "arm64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "engines": { - "node": ">=8" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/ansi-styles": { - "version": "5.2.0", + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.12.2.tgz", + "integrity": "sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==", + "cpu": [ + "arm64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/aria-query": { - "version": "5.3.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "dequal": "^2.0.3" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/assertion-error": { - "version": "2.0.1", + "node_modules/@unrs/resolver-binding-linux-loong64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-gnu/-/resolver-binding-linux-loong64-gnu-1.12.2.tgz", + "integrity": "sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==", + "cpu": [ + "loong64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "engines": { - "node": ">=12" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/bail": { - "version": "2.0.2", + "node_modules/@unrs/resolver-binding-linux-loong64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-loong64-musl/-/resolver-binding-linux-loong64-musl-1.12.2.tgz", + "integrity": "sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.12.2.tgz", + "integrity": "sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==", + "cpu": [ + "ppc64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/baseline-browser-mapping": { - "version": "2.11.12", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", - "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.12.2.tgz", + "integrity": "sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.cjs" - }, - "engines": { - "node": ">=6.0.0" - } + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/bidi-js": { - "version": "1.0.3", + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.12.2.tgz", + "integrity": "sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==", + "cpu": [ + "riscv64" + ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/blake3-wasm": { - "version": "2.1.5", + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.12.2.tgz", + "integrity": "sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==", + "cpu": [ + "s390x" + ], "dev": true, - "license": "MIT" + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.12.2.tgz", + "integrity": "sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==", + "cpu": [ + "x64" + ], "dev": true, + "libc": [ + "glibc" + ], "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "20 || >=22" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/browserslist": { - "version": "4.28.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", - "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.12.2.tgz", + "integrity": "sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==", + "cpu": [ + "x64" + ], "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "libc": [ + "musl" ], "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.10.44", - "caniuse-lite": "^1.0.30001806", - "electron-to-chromium": "^1.5.393", - "node-releases": "^2.0.51", - "update-browserslist-db": "^1.2.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/caniuse-lite": { - "version": "1.0.30001806", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", - "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "node_modules/@unrs/resolver-binding-openharmony-arm64": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-openharmony-arm64/-/resolver-binding-openharmony-arm64-1.12.2.tgz", + "integrity": "sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==", + "cpu": [ + "arm64" ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "optional": true, + "os": [ + "openharmony" + ] }, - "node_modules/chai": { - "version": "6.2.2", + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.12.2.tgz", + "integrity": "sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/chalk": { - "version": "5.6.2", + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" } }, - "node_modules/character-entities": { - "version": "2.0.2", + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/character-entities-html4": { - "version": "2.1.0", + "node_modules/@unrs/resolver-binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.12.2.tgz", + "integrity": "sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==", + "cpu": [ + "arm64" + ], + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.12.2.tgz", + "integrity": "sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/class-variance-authority": { - "version": "0.7.1", - "license": "Apache-2.0", - "dependencies": { - "clsx": "^2.1.1" - }, - "funding": { - "url": "https://polar.sh/cva" - } + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.12.2.tgz", + "integrity": "sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/cli-cursor": { - "version": "5.0.0", + "node_modules/@vitejs/plugin-react": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.5.tgz", + "integrity": "sha512-BOVzne/NL162sMdResB25mUv+vWMF5NoAjNf09TeGlE7ZpszZWSD3winycicLJw72yeVsoCn/2kOhEuCvEShMA==", "dev": true, "license": "MIT", "dependencies": { - "restore-cursor": "^5.0.0" + "@rolldown/pluginutils": "^1.0.1" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } } }, - "node_modules/cli-spinners": { - "version": "3.4.0", + "node_modules/@vitest/expect": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", + "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=18.20" + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.10", + "@vitest/utils": "4.1.10", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/vitest" } }, - "node_modules/cliui": { - "version": "8.0.1", + "node_modules/@vitest/mocker": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", + "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" + "@vitest/spy": "4.1.10", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" }, - "engines": { - "node": ">=12" + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } } }, - "node_modules/cliui/node_modules/string-width": { - "version": "4.2.3", + "node_modules/@vitest/pretty-format": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", + "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", "dev": true, "license": "MIT", "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" + "tinyrainbow": "^3.1.0" }, - "engines": { - "node": ">=8" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/cliui/node_modules/strip-ansi": { - "version": "6.0.1", - "dev": true, + "node_modules/@vitest/runner": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", + "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1" + "@vitest/utils": "4.1.10", + "pathe": "^2.0.3" }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=6" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/color-convert": { - "version": "2.0.1", + "node_modules/@vitest/snapshot": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", + "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", "dev": true, "license": "MIT", "dependencies": { - "color-name": "~1.1.4" + "@vitest/pretty-format": "4.1.10", + "@vitest/utils": "4.1.10", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, - "engines": { - "node": ">=7.0.0" + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/color-name": { - "version": "1.1.4", + "node_modules/@vitest/spy": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", + "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", "dev": true, - "license": "MIT" - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", "license": "MIT", "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://opencollective.com/vitest" } }, - "node_modules/comment-parser": { - "version": "1.4.7", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.7.tgz", - "integrity": "sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==", + "node_modules/@vitest/utils": { + "version": "4.1.10", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", + "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 12.0.0" + "dependencies": { + "@vitest/pretty-format": "4.1.10", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" } }, - "node_modules/concurrently": { - "version": "9.2.4", - "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", - "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", "dev": true, "license": "MIT", - "dependencies": { - "chalk": "4.1.2", - "rxjs": "7.8.2", - "shell-quote": "1.9.0", - "supports-color": "8.1.1", - "tree-kill": "1.2.2", - "yargs": "17.7.2" - }, "bin": { - "conc": "dist/bin/concurrently.js", - "concurrently": "dist/bin/concurrently.js" + "acorn": "bin/acorn" }, "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" + "node": ">=0.4.0" } }, - "node_modules/concurrently/node_modules/ansi-styles": { - "version": "4.3.0", + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", "dev": true, "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/concurrently/node_modules/chalk": { - "version": "4.1.2", + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", "dev": true, "license": "MIT", "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "environment": "^1.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { - "version": "7.2.0", + "node_modules/ansi-regex": { + "version": "5.0.1", "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, "engines": { "node": ">=8" } }, - "node_modules/convert-source-map": { - "version": "2.0.0", + "node_modules/ansi-styles": { + "version": "5.2.0", "dev": true, - "license": "MIT" - }, - "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } + "license": "MIT" }, - "node_modules/css-tree": { - "version": "3.2.1", + "node_modules/aria-query": { + "version": "5.3.0", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + "dequal": "^2.0.3" } }, - "node_modules/csstype": { - "version": "3.2.3", - "license": "MIT" - }, - "node_modules/d3-array": { - "version": "3.2.4", - "license": "ISC", - "dependencies": { - "internmap": "1 - 2" - }, + "node_modules/assertion-error": { + "version": "2.0.1", + "dev": true, + "license": "MIT", "engines": { "node": ">=12" } }, - "node_modules/d3-color": { - "version": "3.1.0", - "license": "ISC", - "engines": { - "node": ">=12" + "node_modules/bail": { + "version": "2.0.2", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/d3-ease": { - "version": "3.0.1", - "license": "BSD-3-Clause", + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": "18 || 20 || >=22" } }, - "node_modules/d3-format": { - "version": "3.1.2", - "license": "ISC", + "node_modules/baseline-browser-mapping": { + "version": "2.11.12", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.12.tgz", + "integrity": "sha512-r7WnVImvVCeFpf2DOXfy41aPWzeNg3H/A2X4dKmy1QL0MSyyk/e7z8ihJ3N6Nn2PsdhkVlqnEfnUE4a05P2aTA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, "engines": { - "node": ">=12" + "node": ">=6.0.0" } }, - "node_modules/d3-interpolate": { - "version": "3.0.1", - "license": "ISC", + "node_modules/bidi-js": { + "version": "1.0.3", + "dev": true, + "license": "MIT", "dependencies": { - "d3-color": "1 - 3" - }, - "engines": { - "node": ">=12" + "require-from-string": "^2.0.2" } }, - "node_modules/d3-path": { - "version": "3.1.0", - "license": "ISC", + "node_modules/blake3-wasm": { + "version": "2.1.5", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, "engines": { - "node": ">=12" + "node": "20 || >=22" } }, - "node_modules/d3-scale": { - "version": "4.0.2", - "license": "ISC", + "node_modules/browserslist": { + "version": "4.28.7", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz", + "integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", "dependencies": { - "d3-array": "2.10.0 - 3", - "d3-format": "1 - 3", - "d3-interpolate": "1.2.0 - 3", - "d3-time": "2.1.1 - 3", - "d3-time-format": "2 - 4" + "baseline-browser-mapping": "^2.10.44", + "caniuse-lite": "^1.0.30001806", + "electron-to-chromium": "^1.5.393", + "node-releases": "^2.0.51", + "update-browserslist-db": "^1.2.3" }, - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" + "bin": { + "browserslist": "cli.js" }, "engines": { - "node": ">=12" + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/d3-time": { - "version": "3.1.0", - "license": "ISC", + "node_modules/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", "dependencies": { - "d3-array": "2 - 3" + "load-tsconfig": "^0.2.3" }, "engines": { - "node": ">=12" + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" } }, - "node_modules/d3-time-format": { - "version": "4.1.0", - "license": "ISC", - "dependencies": { - "d3-time": "1 - 3" - }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=8" } }, - "node_modules/d3-timer": { - "version": "3.0.1", - "license": "ISC", - "engines": { - "node": ">=12" + "node_modules/caniuse-lite": { + "version": "1.0.30001806", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz", + "integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/ccount": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/data-urls": { - "version": "7.0.0", + "node_modules/chai": { + "version": "6.2.2", "dev": true, "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + "node": ">=18" } }, - "node_modules/debug": { - "version": "4.4.3", + "node_modules/chalk": { + "version": "5.6.2", + "dev": true, "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, "engines": { - "node": ">=6.0" + "node": "^12.17.0 || ^14.13 || >=16.0.0" }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/decimal.js": { - "version": "10.6.0", + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, - "license": "MIT" - }, - "node_modules/decimal.js-light": { - "version": "2.5.1", - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=10" + } }, - "node_modules/decode-named-character-reference": { - "version": "1.3.0", + "node_modules/character-entities": { + "version": "2.0.2", "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" + "node_modules/character-entities-html4": { + "version": "2.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "node_modules/dequal": { - "version": "2.0.3", + "node_modules/character-entities-legacy": { + "version": "3.0.0", "license": "MIT", - "engines": { - "node": ">=6" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/detect-libc": { - "version": "2.1.2", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/devlop": { - "version": "1.1.0", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, "license": "MIT", "dependencies": { - "dequal": "^2.0.0" + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://paulmillr.com/funding/" } }, - "node_modules/dom-accessibility-api": { - "version": "0.5.16", + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", "dev": true, "license": "MIT" }, - "node_modules/electron-to-chromium": { - "version": "1.5.400", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz", - "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==", - "dev": true, - "license": "ISC" + "node_modules/class-variance-authority": { + "version": "0.7.1", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } }, - "node_modules/emoji-regex": { - "version": "8.0.0", + "node_modules/cli-cursor": { + "version": "5.0.0", "dev": true, - "license": "MIT" + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/enhanced-resolve": { - "version": "5.21.6", + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.3.3" + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" }, "engines": { - "node": ">=10.13.0" + "node": ">=8.0.0", + "npm": ">=5.0.0" } }, - "node_modules/entities": { - "version": "8.0.0", + "node_modules/cli-highlight/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, "engines": { - "node": ">=20.19.0" + "node": ">=8" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/error-stack-parser-es": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", - "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "node_modules/cli-highlight/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, "funding": { - "url": "https://github.com/sponsors/antfu" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/es-module-lexer": { - "version": "2.2.0", + "node_modules/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", "dev": true, - "license": "MIT" - }, - "node_modules/es-toolkit": { - "version": "1.49.0", - "license": "MIT", - "workspaces": [ - "docs", - "benchmarks" - ] + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } }, - "node_modules/esbuild": { - "version": "0.28.1", + "node_modules/cli-highlight/node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.1", - "@esbuild/android-arm": "0.28.1", - "@esbuild/android-arm64": "0.28.1", - "@esbuild/android-x64": "0.28.1", - "@esbuild/darwin-arm64": "0.28.1", - "@esbuild/darwin-x64": "0.28.1", - "@esbuild/freebsd-arm64": "0.28.1", - "@esbuild/freebsd-x64": "0.28.1", - "@esbuild/linux-arm": "0.28.1", - "@esbuild/linux-arm64": "0.28.1", - "@esbuild/linux-ia32": "0.28.1", - "@esbuild/linux-loong64": "0.28.1", - "@esbuild/linux-mips64el": "0.28.1", - "@esbuild/linux-ppc64": "0.28.1", - "@esbuild/linux-riscv64": "0.28.1", - "@esbuild/linux-s390x": "0.28.1", - "@esbuild/linux-x64": "0.28.1", - "@esbuild/netbsd-arm64": "0.28.1", - "@esbuild/netbsd-x64": "0.28.1", - "@esbuild/openbsd-arm64": "0.28.1", - "@esbuild/openbsd-x64": "0.28.1", - "@esbuild/openharmony-arm64": "0.28.1", - "@esbuild/sunos-x64": "0.28.1", - "@esbuild/win32-arm64": "0.28.1", - "@esbuild/win32-ia32": "0.28.1", - "@esbuild/win32-x64": "0.28.1" - } + "license": "MIT" }, - "node_modules/escalade": { - "version": "3.2.0", + "node_modules/cli-highlight/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/escape-string-regexp": { - "version": "5.0.0", + "node_modules/cli-highlight/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12" + "dependencies": { + "ansi-regex": "^5.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8" } }, - "node_modules/eslint": { - "version": "10.8.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", - "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", + "node_modules/cli-highlight/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", - "workspaces": [ - "packages/*" - ], "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.7.0", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.2", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.5", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" + "has-flag": "^4.0.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } + "node": ">=8" } }, - "node_modules/eslint-import-context": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", - "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.2.tgz", + "integrity": "sha512-Nt9ZJjXTv5R8MHbqby/wXQ6Gi0Bb3TcYZkR1bzuL4yB2OxWPkXknz513gEF0GoA6tn00UpbPvERW8rzCuWCA6w==", "dev": true, "license": "MIT", "dependencies": { - "get-tsconfig": "^4.10.1", - "stable-hash-x": "^0.2.0" + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-import-context" - }, - "peerDependencies": { - "unrs-resolver": "^1.0.0" - }, - "peerDependenciesMeta": { - "unrs-resolver": { - "optional": true - } + "node": ">=10" } }, - "node_modules/eslint-import-resolver-typescript": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.5.tgz", - "integrity": "sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==", + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", "dev": true, "license": "ISC", - "dependencies": { - "debug": "^4.4.1", - "eslint-import-context": "^0.1.8", - "get-tsconfig": "^4.10.1", - "is-bun-module": "^2.0.0", - "stable-hash-x": "^0.2.0", - "tinyglobby": "^0.2.14", - "unrs-resolver": "^1.7.11" - }, "engines": { - "node": "^16.17.0 || >=18.6.0" + "node": ">=10" + } + }, + "node_modules/cli-spinners": { + "version": "3.4.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" }, "funding": { - "url": "https://opencollective.com/eslint-import-resolver-typescript" - }, - "peerDependencies": { - "eslint": "*", - "eslint-plugin-import": "*", - "eslint-plugin-import-x": "*" - }, - "peerDependenciesMeta": { - "eslint-plugin-import": { - "optional": true - }, - "eslint-plugin-import-x": { - "optional": true - } + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-import-x": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.17.1.tgz", - "integrity": "sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==", + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "^8.56.0", - "comment-parser": "^1.4.1", - "debug": "^4.4.1", - "eslint-import-context": "^0.1.9", - "is-glob": "^4.0.3", - "minimatch": "^9.0.3 || ^10.1.2", - "semver": "^7.7.2", - "stable-hash-x": "^0.2.0", - "unrs-resolver": "^1.9.2" + "string-width": "^4.2.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint-plugin-import-x" - }, - "peerDependencies": { - "@typescript-eslint/utils": "^8.56.0", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "eslint-import-resolver-node": "*" + "node": "10.* || >= 12.*" }, - "peerDependenciesMeta": { - "@typescript-eslint/utils": { - "optional": true - }, - "eslint-import-resolver-node": { - "optional": true - } + "optionalDependencies": { + "@colors/colors": "1.5.0" } }, - "node_modules/eslint-plugin-react-hooks": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", - "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", + "node_modules/cli-table3/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "hermes-parser": "^0.25.1", - "zod": "^3.25.0 || ^4.0.0", - "zod-validation-error": "^3.5.0 || ^4.0.0" + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, "engines": { - "node": ">=18" - }, - "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" + "node": ">=8" } }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "node_modules/cli-table3/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "ansi-regex": "^5.0.1" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=8" } }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "node_modules/cliui": { + "version": "8.0.1", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" }, - "funding": { - "url": "https://opencollective.com/eslint" + "engines": { + "node": ">=12" } }, - "node_modules/eslint/node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", "dev": true, "license": "MIT", - "engines": { - "node": ">=10" + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8" } }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" + "ansi-regex": "^5.0.1" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=8" } }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, + "node_modules/clsx": { + "version": "2.1.1", + "license": "MIT", "engines": { - "node": ">=0.10" + "node": ">=6" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/color-convert": { + "version": "2.0.1", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "estraverse": "^5.2.0" + "color-name": "~1.1.4" }, "engines": { - "node": ">=4.0" + "node": ">=7.0.0" } }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/color-name": { + "version": "1.1.4", "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } + "license": "MIT" }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", + "node_modules/comma-separated-tokens": { + "version": "2.0.3", "license": "MIT", "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/estree-walker": { - "version": "3.0.3", + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", "engines": { - "node": ">=0.10.0" + "node": ">=14" } }, - "node_modules/eventemitter3": { - "version": "5.0.4", - "license": "MIT" - }, - "node_modules/expect-type": { - "version": "1.4.0", + "node_modules/comment-parser": { + "version": "1.4.7", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.7.tgz", + "integrity": "sha512-0h+uSNtQGW3D98eQt3jJ8L06Fves8hncB4V/PKdw/Qb8Hnk19VaKuTr55UNRYiSoVa7WwrFls+rh3ux9agmkeQ==", "dev": true, - "license": "Apache-2.0", + "license": "MIT", "engines": { - "node": ">=12.0.0" + "node": ">= 12.0.0" } }, - "node_modules/extend": { - "version": "3.0.2", - "license": "MIT" - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", + "node_modules/concurrently": { + "version": "9.2.4", + "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.4.tgz", + "integrity": "sha512-TZ0CEhyzvFjgtAvHTusDMgj7wNdihCh7LLLrzdUOXIhdlnL2JBBGA9eJxR24rtqgmdjh3OA3hrN1rCHj6HM8qA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=12.0.0" + "dependencies": { + "chalk": "4.1.2", + "rxjs": "7.8.2", + "shell-quote": "1.9.0", + "supports-color": "8.1.1", + "tree-kill": "1.2.2", + "yargs": "17.7.2" }, - "peerDependencies": { - "picomatch": "^3 || ^4" + "bin": { + "conc": "dist/bin/concurrently.js", + "concurrently": "dist/bin/concurrently.js" }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/open-cli-tools/concurrently?sponsor=1" } }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "node_modules/concurrently/node_modules/ansi-styles": { + "version": "4.3.0", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" + "color-convert": "^2.0.1" }, "engines": { - "node": ">=16.0.0" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/concurrently/node_modules/chalk": { + "version": "4.1.2", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" }, "engines": { "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "node_modules/concurrently/node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" + "has-flag": "^4.0.0" }, "engines": { - "node": ">=16" + "node": ">=8" } }, - "node_modules/flatted": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", - "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", "dev": true, - "license": "ISC" - }, - "node_modules/framer-motion": { - "version": "12.42.2", - "license": "MIT", - "dependencies": { - "motion-dom": "^12.42.2", - "motion-utils": "^12.39.0", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } + "license": "MIT" }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", "dev": true, "license": "MIT", "engines": { - "node": ">=6.9.0" + "node": "^14.18.0 || >=16.10.0" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", + "node_modules/convert-source-map": { + "version": "2.0.0", "dev": true, - "license": "ISC", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } + "license": "MIT" }, - "node_modules/get-east-asian-width": { - "version": "1.6.0", - "dev": true, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", "license": "MIT", "engines": { "node": ">=18" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "node_modules/get-tsconfig": { - "version": "4.14.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.1.tgz", - "integrity": "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==", + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "license": "MIT", "dependencies": { - "resolve-pkg-maps": "^1.0.0" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" }, - "funding": { - "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + "engines": { + "node": ">= 8" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/css-tree": { + "version": "3.2.1", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "is-glob": "^4.0.3" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" }, "engines": { - "node": ">=10.13.0" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "dev": true, - "license": "ISC" + "node_modules/csstype": { + "version": "3.2.3", + "license": "MIT" }, - "node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", + "node_modules/d3-array": { + "version": "3.2.4", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/d3-color": { + "version": "3.1.0", + "license": "ISC", + "engines": { + "node": ">=12" } }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "node_modules/d3-ease": { + "version": "3.0.1", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" } }, - "node_modules/hast-util-raw": { - "version": "9.1.0", - "license": "MIT", + "node_modules/d3-format": { + "version": "3.1.2", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "license": "ISC", "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" + "d3-color": "1 - 3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=12" } }, - "node_modules/hast-util-raw/node_modules/entities": { - "version": "6.0.1", - "license": "BSD-2-Clause", + "node_modules/d3-path": { + "version": "3.1.0", + "license": "ISC", "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "node": ">=12" } }, - "node_modules/hast-util-raw/node_modules/parse5": { - "version": "7.3.0", - "license": "MIT", + "node_modules/d3-scale": { + "version": "4.0.2", + "license": "ISC", "dependencies": { - "entities": "^6.0.0" + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "engines": { + "node": ">=12" } }, - "node_modules/hast-util-sanitize": { - "version": "5.0.2", - "license": "MIT", + "node_modules/d3-shape": { + "version": "3.2.0", + "license": "ISC", "dependencies": { - "@types/hast": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "unist-util-position": "^5.0.0" + "d3-path": "^3.1.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=12" } }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "license": "MIT", + "node_modules/d3-time": { + "version": "3.1.0", + "license": "ISC", "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" + "d3-array": "2 - 3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=12" } }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.1", - "license": "MIT", + "node_modules/d3-time-format": { + "version": "4.1.0", + "license": "ISC", "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" + "d3-time": "1 - 3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=12" } }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", + "node_modules/d3-timer": { + "version": "3.0.1", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/data-urls": { + "version": "7.0.0", + "dev": true, "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, - "node_modules/hastscript": { - "version": "9.0.1", + "node_modules/debug": { + "version": "4.4.3", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" + "ms": "^2.1.3" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/hermes-estree": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", - "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "node_modules/decimal.js": { + "version": "10.6.0", "dev": true, "license": "MIT" }, - "node_modules/hermes-parser": { - "version": "0.25.1", - "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", - "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", - "dev": true, - "license": "MIT", - "dependencies": { - "hermes-estree": "0.25.1" - } - }, - "node_modules/hono": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", - "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } + "node_modules/decimal.js-light": { + "version": "2.5.1", + "license": "MIT" }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "dev": true, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", "license": "MIT", "dependencies": { - "@exodus/bytes": "^1.6.0" + "character-entities": "^2.0.0" }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/html-url-attributes": { - "version": "3.0.1", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/immer": { - "version": "10.2.0", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/immer" - } - }, - "node_modules/imurmurhash": { + "node_modules/deep-is": { "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/inline-style-parser": { - "version": "0.2.7", "license": "MIT" }, - "node_modules/internmap": { + "node_modules/dequal": { "version": "2.0.3", - "license": "ISC", + "license": "MIT", "engines": { - "node": ">=12" + "node": ">=6" } }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "node_modules/detect-libc": { + "version": "2.1.2", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" } }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", + "node_modules/devlop": { + "version": "1.1.0", "license": "MIT", "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" + "dequal": "^2.0.0" }, "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-bun-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", - "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "node_modules/dom-accessibility-api": { + "version": "0.5.16", "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.7.1" - } + "license": "MIT" }, - "node_modules/is-decimal": { - "version": "2.0.1", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } + "node_modules/electron-to-chromium": { + "version": "1.5.400", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.400.tgz", + "integrity": "sha512-96EWDNjM59SYflgeV5Ylsf4EMiq1a25YjCnJH7cxn/AF2H3pILRweaUnoLax0yKHWdpOzY6JKEu45e8irqZIHA==", + "dev": true, + "license": "ISC" }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/emoji-regex": { + "version": "8.0.0", "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", + "node_modules/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } + "license": "MIT" }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/enhanced-resolve": { + "version": "5.21.6", "dev": true, "license": "MIT", "dependencies": { - "is-extglob": "^2.1.1" + "graceful-fs": "^4.2.4", + "tapable": "^2.3.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=10.13.0" } }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "license": "MIT", + "node_modules/entities": { + "version": "8.0.0", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/is-interactive": { - "version": "2.0.0", + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, "license": "MIT", "engines": { - "node": ">=12" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-plain-obj": { - "version": "4.1.0", + "node_modules/error-stack-parser-es": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", + "integrity": "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA==", + "dev": true, "license": "MIT", - "engines": { - "node": ">=12" - }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/antfu" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", + "node_modules/es-module-lexer": { + "version": "2.2.0", "dev": true, "license": "MIT" }, - "node_modules/is-unicode-supported": { - "version": "2.1.0", + "node_modules/es-toolkit": { + "version": "1.49.0", + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, + "node_modules/esbuild": { + "version": "0.28.1", "dev": true, + "hasInstallScript": true, "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { "node": ">=18" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/jiti": { - "version": "2.7.0", + "node_modules/escalade": { + "version": "3.2.0", "dev": true, "license": "MIT", - "bin": { - "jiti": "lib/jiti-cli.mjs" + "engines": { + "node": ">=6" } }, - "node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/jsdom": { - "version": "29.1.1", + "node_modules/eslint": { + "version": "10.8.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.8.1.tgz", + "integrity": "sha512-wqA7W2jbsC/BnV9Iv1UZpKVFkO1AdNoSmYW8NWG4HNOBbkAMvIqDZ27pI2f07dqn583NcIC44ckjAcOXDL1QbQ==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", - "xml-name-validator": "^5.0.0" + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.2", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" }, "peerDependencies": { - "canvas": "^3.0.0" + "jiti": "*" }, "peerDependenciesMeta": { - "canvas": { + "jiti": { "optional": true } } }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", "dev": true, "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" }, "engines": { - "node": ">=6" + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.5.tgz", + "integrity": "sha512-nbE5XLph6TLtGYcu/U6e6ZVXyKBhbDWK5cLGk76eJ7NdZpwf1P9EFkpt1Z01mNZNrrilsAYWKH6zUkL4reoXbw==", "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" }, "engines": { - "node": ">=6" - } - }, - "node_modules/jsonrepair": { - "version": "3.15.0", - "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.15.0.tgz", - "integrity": "sha512-wy8OTjwsJwQRnQJkKnMJJ9vcytRdBPAgIF/Hy6+s1dAj42BHMKiyL8JzEieIl3JY7idt8eyHwBWTO8mh/+mtwA==", - "license": "ISC", - "bin": { - "jsonrepair": "bin/cli.js" + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "node_modules/eslint-plugin-import-x": { + "version": "4.17.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import-x/-/eslint-plugin-import-x-4.17.1.tgz", + "integrity": "sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==", "dev": true, "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "dev": true, - "license": "MIT", + "@typescript-eslint/types": "^8.56.0", + "comment-parser": "^1.4.1", + "debug": "^4.4.1", + "eslint-import-context": "^0.1.9", + "is-glob": "^4.0.3", + "minimatch": "^9.0.3 || ^10.1.2", + "semver": "^7.7.2", + "stable-hash-x": "^0.2.0", + "unrs-resolver": "^1.9.2" + }, "engines": { - "node": ">=6" - } - }, - "node_modules/lenis": { - "version": "1.3.26", - "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.26.tgz", - "integrity": "sha512-s/xTCZCxTFvHbAN1OzuhNaN5YPJH2ail0XAkctKW1b+RUAG4nUL5UHLXwNko1h8aEeT2jspBXegMgPJd8zcuag==", - "license": "MIT", - "workspaces": [ - "packages/*", - "playground", - "playground/*" - ], + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, "funding": { - "type": "github", - "url": "https://github.com/sponsors/darkroomengineering" + "url": "https://opencollective.com/eslint-plugin-import-x" }, "peerDependencies": { - "@nuxt/kit": ">=3.0.0", - "react": ">=17.0.0", - "vue": ">=3.0.0" + "@typescript-eslint/utils": "^8.56.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint-import-resolver-node": "*" }, "peerDependenciesMeta": { - "@nuxt/kit": { - "optional": true - }, - "react": { + "@typescript-eslint/utils": { "optional": true }, - "vue": { + "eslint-import-resolver-node": { "optional": true } } }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/eslint-plugin-react-hooks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz", + "integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==", "dev": true, "license": "MIT", "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" }, "engines": { - "node": ">= 0.8.0" + "node": ">=18" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, - "node_modules/lightningcss": { - "version": "1.32.0", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">= 12.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" + "url": "https://opencollective.com/eslint" } }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], + "license": "Apache-2.0", "engines": { - "node": ">= 12.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/eslint" } }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, "engines": { - "node": ">= 12.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/eslint" } }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, "engines": { - "node": ">= 12.0.0" + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">=4.0" } }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "BSD-2-Clause", "engines": { - "node": ">= 12.0.0" - }, + "node": ">=4.0" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "license": "MIT", "funding": { "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://opencollective.com/unified" } }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], + "node_modules/estree-walker": { + "version": "3.0.3", "dev": true, - "libc": [ - "glibc" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=0.10.0" } }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], + "node_modules/eventemitter3": { + "version": "5.0.4", + "license": "MIT" + }, + "node_modules/expect-type": { + "version": "1.4.0", "dev": true, - "libc": [ - "musl" - ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], + "license": "Apache-2.0", "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "node": ">=12.0.0" } }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], + "node_modules/extend": { + "version": "3.0.2", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true, - "libc": [ - "glibc" + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fflate": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz", + "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/framer-motion": { + "version": "12.42.2", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.42.2", + "motion-utils": "^12.39.0", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.1.tgz", + "integrity": "sha512-Dz/6HxkrxgNehhxLVeyv8sad9UzF2xBVeaKBQNDfJ5XiSXmp2gTR0eO0RWiT2NCKS5aGP9jjkOMggTN90qU50A==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw": { + "version": "9.1.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-from-parse5": "^8.0.0", + "hast-util-to-parse5": "^8.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "parse5": "^7.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-raw/node_modules/entities": { + "version": "6.0.1", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/hast-util-raw/node_modules/parse5": { + "version": "7.3.0", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/hast-util-sanitize": { + "version": "5.0.2", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@ungap/structured-clone": "^1.0.0", + "unist-util-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-parse5": { + "version": "8.0.1", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "web-namespaces": "^2.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/hono": { + "version": "4.13.1", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", + "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", + "license": "MIT", + "engines": { + "node": ">=16.9.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/human-id": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/human-id/-/human-id-4.2.0.tgz", + "integrity": "sha512-K3GbkIWqyvvlpfhBPlbEvD97TtqBpAYA4kt+cn2lD2x2HuohzZCibcA2nOlnJT6exqvJLggoB5nv2dNf192nEA==", + "dev": true, + "license": "MIT", + "bin": { + "human-id": "dist/cli.js" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immer": { + "version": "10.2.0", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "license": "MIT" + }, + "node_modules/internmap": { + "version": "2.0.3", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-interactive": { + "version": "2.0.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/jiti": { + "version": "2.7.0", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/jju": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/jju/-/jju-1.4.0.tgz", + "integrity": "sha512-8wb9Yw966OSxApiCt0K3yNJL8pnNeIv+OEq2YMidz4FKP6nonSRoOXc80iXY4JaN2FC11B9qsNmDsm+ZOfMROA==", + "dev": true, + "license": "MIT" + }, + "node_modules/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "29.1.1", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonrepair": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/jsonrepair/-/jsonrepair-3.15.0.tgz", + "integrity": "sha512-wy8OTjwsJwQRnQJkKnMJJ9vcytRdBPAgIF/Hy6+s1dAj42BHMKiyL8JzEieIl3JY7idt8eyHwBWTO8mh/+mtwA==", + "license": "ISC", + "bin": { + "jsonrepair": "bin/cli.js" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/launch-editor": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.14.1.tgz", + "integrity": "sha512-QWBrQsMpH7gPr965dsKD/3cKWiNoTjpATQf++Xq63N6sKRGMwlVXz41O1IZTMfZQgBctD/K5Zt06+/I6pP6+HA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picocolors": "^1.1.1", + "shell-quote": "^1.8.4" + } + }, + "node_modules/lenis": { + "version": "1.3.26", + "resolved": "https://registry.npmjs.org/lenis/-/lenis-1.3.26.tgz", + "integrity": "sha512-s/xTCZCxTFvHbAN1OzuhNaN5YPJH2ail0XAkctKW1b+RUAG4nUL5UHLXwNko1h8aEeT2jspBXegMgPJd8zcuag==", + "license": "MIT", + "workspaces": [ + "packages/*", + "playground", + "playground/*" + ], + "funding": { + "type": "github", + "url": "https://github.com/sponsors/darkroomengineering" + }, + "peerDependencies": { + "@nuxt/kit": ">=3.0.0", + "react": ">=17.0.0", + "vue": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "react": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols": { + "version": "7.0.1", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.5.1", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lucide-react": { + "version": "1.22.0", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "dev": true, + "license": "MIT", + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/marked": { + "version": "9.1.6", + "resolved": "https://registry.npmjs.org/marked/-/marked-9.1.6.tgz", + "integrity": "sha512-jcByLnIFkd5gSXZmjNvS1TlmRhCXZjIzHYlaGkPlLIekG55JDR2Z4va9tZwCiP+/RDERiNhMOFu01xd6O5ct1Q==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 16" + } + }, + "node_modules/marked-terminal": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", + "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "ansi-regex": "^6.1.0", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.2.0", + "supports-hyperlinks": "^3.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <16" + } + }, + "node_modules/marked-terminal/node_modules/ansi-regex": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz", + "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "dev": true, - "libc": [ - "musl" + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" } }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" } }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], "license": "MIT", "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" } }, - "node_modules/log-symbols": { - "version": "7.0.1", + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/mimic-function": { + "version": "5.0.1", "dev": true, "license": "MIT", - "dependencies": { - "is-unicode-supported": "^2.0.0", - "yoctocolors": "^2.1.1" - }, "engines": { "node": ">=18" }, @@ -5855,959 +8098,881 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/longest-streak": { - "version": "3.1.0", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lru-cache": { - "version": "11.5.1", + "node_modules/miniflare": { + "version": "5.20260801.1-alpha", + "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260801.1-alpha.tgz", + "integrity": "sha512-BHPVzIDA6mbx7LefxpvkXW7DHx9FKB9GorZatbnrrFTt3CVMU8zuUpbgyCuebwDKcTTOZos43ta8GQ0eMVEpxA==", "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, - "node_modules/lucide-react": { - "version": "1.22.0", - "license": "ISC", - "peerDependencies": { - "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "0.8.1", + "sharp": "0.35.2", + "undici": "7.29.0", + "workerd": "1.20260801.1", + "ws": "8.21.0", + "youch": "4.1.0-beta.10" + }, + "engines": { + "node": ">=22.0.0" } }, - "node_modules/lz-string": { - "version": "1.5.0", + "node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", "dev": true, - "license": "MIT", - "bin": { - "lz-string": "bin/bin.js" + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/magic-string": { - "version": "0.30.21", + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" } }, - "node_modules/markdown-table": { - "version": "3.0.4", + "node_modules/motion": { + "version": "12.42.2", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "dependencies": { + "framer-motion": "^12.42.2", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } } }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", + "node_modules/motion-dom": { + "version": "12.42.2", "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "motion-utils": "^12.39.0" } }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", + "node_modules/motion-utils": { + "version": "12.39.0", + "license": "MIT" + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=4" } }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", + "node_modules/ms": { + "version": "2.1.3", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" } }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" + "bin": { + "nanoid": "bin/nanoid.cjs" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://opencollective.com/napi-postinstall" } }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "dev": true, "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=18" } }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", + "node_modules/node-emoji/node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", + "node_modules/node-releases": { + "version": "2.0.52", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", + "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=18" } }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", + "node_modules/obug": { + "version": "2.1.3", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=12.20.0" } }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", + "node_modules/onetime": { + "version": "7.0.0", + "dev": true, "license": "MIT", "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.1", + "node_modules/ora": { + "version": "9.4.1", + "dev": true, "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" + "chalk": "^5.6.2", + "cli-cursor": "^5.0.0", + "cli-spinners": "^3.2.0", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.1.0", + "log-symbols": "^7.0.1", + "stdin-discarder": "^0.3.2", + "string-width": "^8.1.0" + }, + "engines": { + "node": ">=20" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0" + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mdn-data": { - "version": "2.27.1", + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", "dev": true, - "license": "CC0-1.0" + "license": "MIT" }, - "node_modules/micromark": { + "node_modules/parse-entities": { "version": "4.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], "license": "MIT", "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "license": "MIT" }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", + "node_modules/parse5": { + "version": "8.0.1", + "dev": true, "license": "MIT", "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" + "entities": "^8.0.0" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, "license": "MIT", "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "parse5": "^6.0.1" } }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=8" } }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">=8" } }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", + "node_modules/path-to-regexp": { + "version": "6.3.0", + "dev": true, + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "engines": { + "node": ">=12" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 6" } }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, "license": "MIT", "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" } }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, "funding": [ { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" }, { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", + "node_modules/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, "funding": [ { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "type": "opencollective", + "url": "https://opencollective.com/postcss/" }, { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "license": "MIT", "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } - ], + } + }, + "node_modules/postgres": { + "version": "3.4.9", + "license": "Unlicense", + "engines": { + "node": ">=12" + }, + "funding": { + "type": "individual", + "url": "https://github.com/sponsors/porsager" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" + "engines": { + "node": ">= 0.8.0" } }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/pretty-format": { + "version": "27.5.1", + "dev": true, "license": "MIT", "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/pretty-format/node_modules/react-is": { + "version": "17.0.2", + "dev": true, + "license": "MIT" + }, + "node_modules/prompts": { + "version": "2.4.2", + "dev": true, "license": "MIT", "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" } }, - "node_modules/micromark-util-character": { - "version": "2.1.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/property-information": { + "version": "7.2.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/publint": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.23.tgz", + "integrity": "sha512-5MQipUPcB7MWw84zLUkHrg/H/UBtk3LL+A0GngTTBSsiNJLQurMUaSIRG3edlOrRz4UFe0AOKK9TZdIWviV+jQ==", + "dev": true, "license": "MIT", "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "@publint/pack": "^0.1.6", + "package-manager-detector": "^1.7.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" } }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", "license": "MIT", "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" } }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/react-is": { + "version": "19.2.7", "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } + "peer": true }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/react-markdown": { + "version": "10.1.0", "license": "MIT", "dependencies": { - "micromark-util-symbol": "^2.0.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" } }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/react-redux": { + "version": "9.3.0", "license": "MIT", "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" + "redux": { + "optional": true } - ], - "license": "MIT" + } }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/react-router": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", + "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", "license": "MIT", "dependencies": { - "micromark-util-symbol": "^2.0.0" + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } } }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/react-router-dom": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", + "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", "license": "MIT", "dependencies": { - "micromark-util-types": "^2.0.0" + "react-router": "7.18.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" } }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/recharts": { + "version": "3.9.0", "license": "MIT", + "workspaces": [ + "www" + ], "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.2.0", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/micromark-util-symbol": { - "version": "2.0.1", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], + "node_modules/redux": { + "version": "5.0.1", "license": "MIT" }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" + "node_modules/redux-thunk": { + "version": "3.1.0", + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } }, - "node_modules/mimic-function": { - "version": "5.0.1", - "dev": true, + "node_modules/rehype-raw": { + "version": "7.0.0", "license": "MIT", - "engines": { - "node": ">=18" + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-raw": "^9.0.0", + "vfile": "^6.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/miniflare": { - "version": "5.20260801.1-alpha", - "resolved": "https://registry.npmjs.org/miniflare/-/miniflare-5.20260801.1-alpha.tgz", - "integrity": "sha512-BHPVzIDA6mbx7LefxpvkXW7DHx9FKB9GorZatbnrrFTt3CVMU8zuUpbgyCuebwDKcTTOZos43ta8GQ0eMVEpxA==", - "dev": true, + "node_modules/rehype-sanitize": { + "version": "6.0.0", "license": "MIT", "dependencies": { - "@cspotcode/source-map-support": "0.8.1", - "sharp": "0.35.2", - "undici": "7.29.0", - "workerd": "1.20260801.1", - "ws": "8.21.0", - "youch": "4.1.0-beta.10" + "@types/hast": "^3.0.0", + "hast-util-sanitize": "^5.0.0" }, - "engines": { - "node": ">=22.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/minimatch": { - "version": "10.2.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", - "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", - "dev": true, - "license": "BlueOak-1.0.0", + "node_modules/remark-gfm": { + "version": "4.0.1", + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.8" - }, - "engines": { - "node": "18 || 20 || >=22" + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/motion": { - "version": "12.42.2", + "node_modules/remark-parse": { + "version": "11.0.0", "license": "MIT", "dependencies": { - "framer-motion": "^12.42.2", - "tslib": "^2.4.0" - }, - "peerDependencies": { - "@emotion/is-prop-valid": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" }, - "peerDependenciesMeta": { - "@emotion/is-prop-valid": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/motion-dom": { - "version": "12.42.2", + "node_modules/remark-rehype": { + "version": "11.1.2", "license": "MIT", "dependencies": { - "motion-utils": "^12.39.0" + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" } }, - "node_modules/motion-utils": { - "version": "12.39.0", - "license": "MIT" - }, - "node_modules/ms": { - "version": "2.1.3", - "license": "MIT" + "node_modules/remark-stringify": { + "version": "11.0.0", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } }, - "node_modules/nanoid": { - "version": "3.3.18", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", - "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "node_modules/require-directory": { + "version": "2.1.1", "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">=0.10.0" } }, - "node_modules/napi-postinstall": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", - "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "node_modules/require-from-string": { + "version": "2.0.2", "dev": true, "license": "MIT", - "bin": { - "napi-postinstall": "lib/cli.js" - }, "engines": { - "node": "^12.20.0 || ^14.18.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/napi-postinstall" + "node": ">=0.10.0" } }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, + "node_modules/reselect": { + "version": "5.2.0", "license": "MIT" }, - "node_modules/node-releases": { - "version": "2.0.52", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.52.tgz", - "integrity": "sha512-MRlTqhAfoMx/4mhEbPo3Hi02g9LJZaJkka69V6h67Cb1gjrAG0jsTE4CZX1eptNx+VCAwJmfpnDIF4P0Nh1A7A==", + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18" + "node": ">=8" } }, - "node_modules/obug": { - "version": "2.1.3", + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], "license": "MIT", - "engines": { - "node": ">=12.20.0" + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "node_modules/onetime": { - "version": "7.0.0", + "node_modules/restore-cursor": { + "version": "5.1.0", "dev": true, "license": "MIT", "dependencies": { - "mimic-function": "^5.0.0" + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" }, "engines": { "node": ">=18" @@ -6816,994 +8981,1192 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "node_modules/rolldown": { + "version": "1.1.3", "dev": true, "license": "MIT", "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" + "@oxc-project/types": "=0.137.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">= 0.8.0" + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" } - }, - "node_modules/ora": { - "version": "9.4.1", + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "^5.6.2", - "cli-cursor": "^5.0.0", - "cli-spinners": "^3.2.0", - "is-interactive": "^2.0.0", - "is-unicode-supported": "^2.1.0", - "log-symbols": "^7.0.1", - "stdin-discarder": "^0.3.2", - "string-width": "^8.1.0" + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" }, "engines": { - "node": ">=20" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" } }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "node_modules/rxjs": { + "version": "7.8.2", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", "dev": true, "license": "MIT", "dependencies": { - "yocto-queue": "^0.1.0" + "mri": "^1.1.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "node_modules/saxes": { + "version": "6.0.0", "dev": true, - "license": "MIT", + "license": "ISC", "dependencies": { - "p-limit": "^3.0.2" + "xmlchars": "^2.2.0" }, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=v12.22.7" } }, - "node_modules/parse-entities": { - "version": "4.0.2", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" + "node_modules/scheduler": { + "version": "0.27.0", + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "engines": { + "node": ">=10" } }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "license": "MIT" }, - "node_modules/parse5": { - "version": "8.0.1", + "node_modules/sharp": { + "version": "0.35.2", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", + "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "dependencies": { - "entities": "^8.0.0" + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.4" + }, + "engines": { + "node": ">=20.9.0" }, "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.2", + "@img/sharp-darwin-x64": "0.35.2", + "@img/sharp-freebsd-wasm32": "0.35.2", + "@img/sharp-libvips-darwin-arm64": "1.3.1", + "@img/sharp-libvips-darwin-x64": "1.3.1", + "@img/sharp-libvips-linux-arm": "1.3.1", + "@img/sharp-libvips-linux-arm64": "1.3.1", + "@img/sharp-libvips-linux-ppc64": "1.3.1", + "@img/sharp-libvips-linux-riscv64": "1.3.1", + "@img/sharp-libvips-linux-s390x": "1.3.1", + "@img/sharp-libvips-linux-x64": "1.3.1", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", + "@img/sharp-libvips-linuxmusl-x64": "1.3.1", + "@img/sharp-linux-arm": "0.35.2", + "@img/sharp-linux-arm64": "0.35.2", + "@img/sharp-linux-ppc64": "0.35.2", + "@img/sharp-linux-riscv64": "0.35.2", + "@img/sharp-linux-s390x": "0.35.2", + "@img/sharp-linux-x64": "0.35.2", + "@img/sharp-linuxmusl-arm64": "0.35.2", + "@img/sharp-linuxmusl-x64": "0.35.2", + "@img/sharp-webcontainers-wasm32": "0.35.2", + "@img/sharp-win32-arm64": "0.35.2", + "@img/sharp-win32-ia32": "0.35.2", + "@img/sharp-win32-x64": "0.35.2" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, "license": "MIT", "engines": { "node": ">=8" } }, - "node_modules/path-to-regexp": { - "version": "6.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", + "node_modules/shell-quote": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", + "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", "license": "MIT", "engines": { - "node": ">=12" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/jonschlinkert" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/postcss": { - "version": "8.5.26", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", - "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "node_modules/siginfo": { + "version": "2.0.0", "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.17", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } + "license": "ISC" }, - "node_modules/postgres": { - "version": "3.4.9", - "license": "Unlicense", + "node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "license": "ISC", "engines": { - "node": ">=12" + "node": ">=14" }, "funding": { - "type": "individual", - "url": "https://github.com/sponsors/porsager" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "node_modules/sisteransi": { + "version": "1.0.5", "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } + "license": "MIT" }, - "node_modules/pretty-format": { - "version": "27.5.1", + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^5.0.1", - "ansi-styles": "^5.0.0", - "react-is": "^17.0.1" + "unicode-emoji-modifier-base": "^1.0.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": ">=8" } }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "dev": true, - "license": "MIT" + "node_modules/sonner": { + "version": "2.0.7", + "license": "MIT", + "peerDependencies": { + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + } }, - "node_modules/prompts": { - "version": "2.4.2", + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "dev": true, + "license": "BSD-3-Clause", "engines": { - "node": ">= 6" + "node": ">=0.10.0" } }, - "node_modules/property-information": { - "version": "7.2.0", + "node_modules/space-separated-tokens": { + "version": "2.0.2", "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/punycode": { - "version": "2.3.1", + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", "dev": true, "license": "MIT", "engines": { - "node": ">=6" - } - }, - "node_modules/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" + "node": ">=12.0.0" } }, - "node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.8" - } + "node_modules/stackback": { + "version": "0.0.2", + "dev": true, + "license": "MIT" }, - "node_modules/react-is": { - "version": "19.2.7", - "license": "MIT", - "peer": true + "node_modules/std-env": { + "version": "4.1.0", + "dev": true, + "license": "MIT" }, - "node_modules/react-markdown": { - "version": "10.1.0", + "node_modules/stdin-discarder": { + "version": "0.3.2", + "dev": true, "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "html-url-attributes": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "unified": "^11.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" + "engines": { + "node": ">=18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=18", - "react": ">=18" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-redux": { - "version": "9.3.0", + "node_modules/string-width": { + "version": "8.2.1", + "dev": true, "license": "MIT", "dependencies": { - "@types/use-sync-external-store": "^0.0.6", - "use-sync-external-store": "^1.4.0" + "get-east-asian-width": "^1.5.0", + "strip-ansi": "^7.1.2" }, - "peerDependencies": { - "@types/react": "^18.2.25 || ^19", - "react": "^18.0 || ^19", - "redux": "^5.0.0" + "engines": { + "node": ">=20" }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "redux": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/react-router": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.18.2.tgz", - "integrity": "sha512-aUVMjFm3GAPTTZL7oYr5E7ETiqfQCHRLH+B+5afnICvf0r7kkK4eR6SMuwbSTJw/7t+12khT/Kahij49fqOCIg==", + "node_modules/stringify-entities": { + "version": "4.0.4", "license": "MIT", "dependencies": { - "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0" - }, - "engines": { - "node": ">=20.0.0" - }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" }, - "peerDependenciesMeta": { - "react-dom": { - "optional": true - } + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/react-router-dom": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.18.2.tgz", - "integrity": "sha512-AIKJ/jgGlFb3EbfCXk5Gzshiwt+l3mqbCrNjmEWMMjqQxNJ3svBa6bgzFyCC2Sw3RA0VWF1kg3uQf2OFhxb8hw==", + "node_modules/strip-ansi": { + "version": "7.2.0", + "dev": true, "license": "MIT", "dependencies": { - "react-router": "7.18.2" + "ansi-regex": "^6.2.2" }, "engines": { - "node": ">=20.0.0" + "node": ">=12" }, - "peerDependencies": { - "react": ">=18", - "react-dom": ">=18" + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/recharts": { - "version": "3.9.0", + "node_modules/strip-ansi/node_modules/ansi-regex": { + "version": "6.2.2", + "dev": true, "license": "MIT", - "workspaces": [ - "www" - ], - "dependencies": { - "@reduxjs/toolkit": "^1.9.0 || 2.x.x", - "clsx": "^2.1.1", - "decimal.js-light": "^2.5.1", - "es-toolkit": "^1.39.3", - "eventemitter3": "^5.0.1", - "immer": "^10.1.1", - "react-redux": "8.x.x || 9.x.x", - "reselect": "5.2.0", - "tiny-invariant": "^1.3.3", - "use-sync-external-store": "^1.2.2", - "victory-vendor": "^37.0.2" - }, "engines": { - "node": ">=18" + "node": ">=12" }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/redux": { - "version": "5.0.1", - "license": "MIT" - }, - "node_modules/redux-thunk": { - "version": "3.1.0", - "license": "MIT", - "peerDependencies": { - "redux": "^5.0.0" + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/rehype-raw": { - "version": "7.0.0", + "node_modules/style-to-js": { + "version": "1.1.21", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-raw": "^9.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "style-to-object": "1.0.14" } }, - "node_modules/rehype-sanitize": { - "version": "6.0.0", + "node_modules/style-to-object": { + "version": "1.0.14", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-sanitize": "^5.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "inline-style-parser": "0.2.7" } }, - "node_modules/remark-gfm": { - "version": "4.0.1", + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" } }, - "node_modules/remark-parse": { - "version": "11.0.0", + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "engines": { + "node": ">= 6" } }, - "node_modules/remark-rehype": { - "version": "11.1.2", + "node_modules/sugar-high": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sugar-high/-/sugar-high-2.0.0.tgz", + "integrity": "sha512-qZQaa+3vbmKSx00G465bY1LqVUU0RaUkY/7xDV63cQzezD6dF/DJCthztRQqYLGA7HXdObvCEtcCRpvoIrrbCg==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "8.1.1", + "dev": true, "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/remark-stringify": { - "version": "11.0.0", + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, "license": "MIT", "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" } }, - "node_modules/require-directory": { - "version": "2.1.1", + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=0.10.0" + "node": ">=8" } }, - "node_modules/require-from-string": { - "version": "2.0.2", + "node_modules/symbol-tree": { + "version": "3.2.4", "dev": true, + "license": "MIT" + }, + "node_modules/tailwind-merge": { + "version": "3.6.0", "license": "MIT", - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" } }, - "node_modules/reselect": { - "version": "5.2.0", + "node_modules/tailwindcss": { + "version": "4.3.2", + "dev": true, "license": "MIT" }, - "node_modules/resolve-pkg-maps": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "node_modules/tapable": { + "version": "2.3.3", "dev": true, "license": "MIT", + "engines": { + "node": ">=6" + }, "funding": { - "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/restore-cursor": { - "version": "5.1.0", + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "license": "MIT", "dependencies": { - "onetime": "^7.0.0", - "signal-exit": "^4.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "any-promise": "^1.0.0" } }, - "node_modules/rolldown": { - "version": "1.1.3", + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.137.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" + "thenify": ">= 3.1.0 < 4" }, "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.3", - "@rolldown/binding-darwin-arm64": "1.1.3", - "@rolldown/binding-darwin-x64": "1.1.3", - "@rolldown/binding-freebsd-x64": "1.1.3", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", - "@rolldown/binding-linux-arm64-gnu": "1.1.3", - "@rolldown/binding-linux-arm64-musl": "1.1.3", - "@rolldown/binding-linux-ppc64-gnu": "1.1.3", - "@rolldown/binding-linux-s390x-gnu": "1.1.3", - "@rolldown/binding-linux-x64-gnu": "1.1.3", - "@rolldown/binding-linux-x64-musl": "1.1.3", - "@rolldown/binding-openharmony-arm64": "1.1.3", - "@rolldown/binding-wasm32-wasi": "1.1.3", - "@rolldown/binding-win32-arm64-msvc": "1.1.3", - "@rolldown/binding-win32-x64-msvc": "1.1.3" + "node": ">=0.8" } }, - "node_modules/rxjs": { - "version": "7.8.2", + "node_modules/tiny-invariant": { + "version": "1.3.3", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", "dev": true, - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.1.0" + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" } }, - "node_modules/saxes": { - "version": "6.0.0", + "node_modules/tinyglobby": { + "version": "0.2.17", "dev": true, - "license": "ISC", + "license": "MIT", "dependencies": { - "xmlchars": "^2.2.0" + "fdir": "^6.5.0", + "picomatch": "^4.0.4" }, "engines": { - "node": ">=v12.22.7" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/scheduler": { - "version": "0.27.0", - "license": "MIT" + "node_modules/tinyrainbow": { + "version": "3.1.0", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } }, - "node_modules/semver": { - "version": "7.8.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", - "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "node_modules/tldts": { + "version": "7.4.5", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.5" }, - "engines": { - "node": ">=10" + "bin": { + "tldts": "bin/cli.js" } }, - "node_modules/set-cookie-parser": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", - "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "node_modules/tldts-core": { + "version": "7.4.5", + "dev": true, "license": "MIT" }, - "node_modules/sharp": { - "version": "0.35.2", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.2.tgz", - "integrity": "sha512-FVtFjtBCMiJS6yb5CX7Sop45WFMpeGw6oRKuJnXYgf/f1ms/D7LE/ZUSNxnW7rZ/dbslQWYkoqFHGPaDBtaK4w==", + "node_modules/tough-cookie": { + "version": "6.0.1", "dev": true, - "license": "Apache-2.0", + "license": "BSD-3-Clause", "dependencies": { - "@img/colour": "^1.1.0", - "detect-libc": "^2.1.2", - "semver": "^7.8.4" + "tldts": "^7.0.5" }, "engines": { - "node": ">=20.9.0" - }, - "funding": { - "url": "https://opencollective.com/libvips" - }, - "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.2", - "@img/sharp-darwin-x64": "0.35.2", - "@img/sharp-freebsd-wasm32": "0.35.2", - "@img/sharp-libvips-darwin-arm64": "1.3.1", - "@img/sharp-libvips-darwin-x64": "1.3.1", - "@img/sharp-libvips-linux-arm": "1.3.1", - "@img/sharp-libvips-linux-arm64": "1.3.1", - "@img/sharp-libvips-linux-ppc64": "1.3.1", - "@img/sharp-libvips-linux-riscv64": "1.3.1", - "@img/sharp-libvips-linux-s390x": "1.3.1", - "@img/sharp-libvips-linux-x64": "1.3.1", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.1", - "@img/sharp-libvips-linuxmusl-x64": "1.3.1", - "@img/sharp-linux-arm": "0.35.2", - "@img/sharp-linux-arm64": "0.35.2", - "@img/sharp-linux-ppc64": "0.35.2", - "@img/sharp-linux-riscv64": "0.35.2", - "@img/sharp-linux-s390x": "0.35.2", - "@img/sharp-linux-x64": "0.35.2", - "@img/sharp-linuxmusl-arm64": "0.35.2", - "@img/sharp-linuxmusl-x64": "0.35.2", - "@img/sharp-webcontainers-wasm32": "0.35.2", - "@img/sharp-win32-arm64": "0.35.2", - "@img/sharp-win32-ia32": "0.35.2", - "@img/sharp-win32-x64": "0.35.2" + "node": ">=16" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "node_modules/tr46": { + "version": "6.0.0", "dev": true, "license": "MIT", "dependencies": { - "shebang-regex": "^3.0.0" + "punycode": "^2.3.1" }, "engines": { - "node": ">=8" + "node": ">=20" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/tree-kill": { + "version": "1.2.2", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" + "bin": { + "tree-kill": "cli.js" } }, - "node_modules/shell-quote": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.9.0.tgz", - "integrity": "sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==", - "dev": true, + "node_modules/trim-lines": { + "version": "3.0.1", "license": "MIT", - "engines": { - "node": ">= 0.4" - }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/siginfo": { - "version": "2.0.0", - "dev": true, - "license": "ISC" + "node_modules/trough": { + "version": "2.2.0", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } }, - "node_modules/signal-exit": { - "version": "4.1.0", + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, - "license": "ISC", + "license": "MIT", "engines": { - "node": ">=14" + "node": ">=18.12" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependencies": { + "typescript": ">=4.8.4" } }, - "node_modules/sisteransi": { - "version": "1.0.5", + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/tslib": { + "version": "2.8.1", + "license": "0BSD" + }, + "node_modules/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", "dev": true, - "license": "MIT" - }, - "node_modules/sonner": { - "version": "2.0.7", "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, "peerDependencies": { - "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", - "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "optional": true + } } }, - "node_modules/source-map-js": { - "version": "1.2.1", + "node_modules/tsup/node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "BSD-3-Clause", + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", + "node_modules/tsup/node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/stable-hash-x": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", - "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", + "node_modules/tsup/node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=12.0.0" + "node": ">=18" } }, - "node_modules/stackback": { - "version": "0.0.2", + "node_modules/tsup/node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/std-env": { - "version": "4.1.0", + "node_modules/tsup/node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/stdin-discarder": { - "version": "0.3.2", + "node_modules/tsup/node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/string-width": { - "version": "8.2.1", + "node_modules/tsup/node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "get-east-asian-width": "^1.5.0", - "strip-ansi": "^7.1.2" - }, + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=18" } }, - "node_modules/stringify-entities": { - "version": "4.0.4", + "node_modules/tsup/node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/strip-ansi": { - "version": "7.2.0", + "node_modules/tsup/node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", - "dependencies": { - "ansi-regex": "^6.2.2" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": ">=18" } }, - "node_modules/strip-ansi/node_modules/ansi-regex": { - "version": "6.2.2", + "node_modules/tsup/node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" + "node": ">=18" } }, - "node_modules/style-to-js": { - "version": "1.1.21", + "node_modules/tsup/node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "dependencies": { - "style-to-object": "1.0.14" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/style-to-object": { - "version": "1.0.14", + "node_modules/tsup/node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.7" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/sugar-high": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sugar-high/-/sugar-high-2.0.0.tgz", - "integrity": "sha512-qZQaa+3vbmKSx00G465bY1LqVUU0RaUkY/7xDV63cQzezD6dF/DJCthztRQqYLGA7HXdObvCEtcCRpvoIrrbCg==", - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "8.1.1", + "node_modules/tsup/node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], "dev": true, "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" + "node": ">=18" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", + "node_modules/tsup/node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], "dev": true, - "license": "MIT" - }, - "node_modules/tailwind-merge": { - "version": "3.6.0", "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" } }, - "node_modules/tailwindcss": { - "version": "4.3.2", + "node_modules/tsup/node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/tapable": { - "version": "2.3.3", + "node_modules/tsup/node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" + "node": ">=18" } }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "license": "MIT" - }, - "node_modules/tinybench": { - "version": "2.9.0", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.2.4", + "node_modules/tsup/node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { "node": ">=18" } }, - "node_modules/tinyglobby": { - "version": "0.2.17", + "node_modules/tsup/node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" + "node": ">=18" } }, - "node_modules/tinyrainbow": { - "version": "3.1.0", + "node_modules/tsup/node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], "engines": { - "node": ">=14.0.0" + "node": ">=18" } }, - "node_modules/tldts": { - "version": "7.4.5", + "node_modules/tsup/node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "tldts-core": "^7.4.5" - }, - "bin": { - "tldts": "bin/cli.js" + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, - "node_modules/tldts-core": { - "version": "7.4.5", + "node_modules/tsup/node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/tough-cookie": { - "version": "6.0.1", + "node_modules/tsup/node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "tldts": "^7.0.5" - }, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], "engines": { - "node": ">=16" + "node": ">=18" } }, - "node_modules/tr46": { - "version": "6.0.0", + "node_modules/tsup/node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, + "optional": true, + "os": [ + "sunos" + ], "engines": { - "node": ">=20" + "node": ">=18" } }, - "node_modules/tree-kill": { - "version": "1.2.2", + "node_modules/tsup/node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "bin": { - "tree-kill": "cli.js" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/trim-lines": { - "version": "3.0.1", + "node_modules/tsup/node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/trough": { - "version": "2.2.0", + "node_modules/tsup/node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" } }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "node_modules/tsup/node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", "dev": true, + "hasInstallScript": true, "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=18.12" + "node": ">=18" }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "license": "0BSD" + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/tsup/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" }, "node_modules/type-check": { "version": "0.4.0", @@ -7854,6 +10217,13 @@ "typescript": ">=4.8.4 <6.1.0" } }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", @@ -7877,6 +10247,16 @@ "pathe": "^2.0.3" } }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/unified": { "version": "11.0.5", "license": "MIT", @@ -8038,6 +10418,16 @@ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/vfile": { "version": "6.0.3", "license": "MIT", @@ -8170,21 +10560,6 @@ } } }, - "node_modules/vite/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/vitest": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", @@ -8421,21 +10796,6 @@ } } }, - "node_modules/wrangler/node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/wrap-ansi": { "version": "7.0.0", "dev": true, @@ -8540,6 +10900,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "dev": true, @@ -8667,19 +11043,21 @@ } }, "packages/api": { - "name": "@codra/api", + "name": "@codraoss/api", "version": "0.9.4", + "license": "AGPL-3.0-only", "dependencies": { - "@codra/core": "*", - "@codra/db": "*", - "@codra/models": "*", - "@codra/provider-github": "*", - "@codra/schema": "*", + "@codraoss/core": "^0.9.4", + "@codraoss/db": "^0.9.4", + "@codraoss/models": "^0.9.4", + "@codraoss/provider-github": "^0.9.4", + "@codraoss/schema": "^0.9.4", "hono": "^4.12.25", "zod": "^4.3.6" }, "devDependencies": { - "@types/node": "^22.0.0" + "@types/node": "^22.0.0", + "tsup": "^8.0.0" } }, "packages/api/node_modules/@types/node": { @@ -8700,27 +11078,32 @@ "license": "MIT" }, "packages/core": { - "name": "@codra/core", + "name": "@codraoss/core", "version": "0.9.4", + "license": "AGPL-3.0-only", "dependencies": { - "@codra/schema": "*", + "@codraoss/schema": "^0.9.4", "jsonrepair": "^3.15.0", "picomatch": "^4.0.5", "zod": "^4.3.6" }, "devDependencies": { - "@types/picomatch": "^4.0.3" + "@types/picomatch": "^4.0.3", + "tsup": "^8.0.0" } }, "packages/db": { - "name": "@codra/db", + "name": "@codraoss/db", "version": "0.9.4", + "license": "AGPL-3.0-only", "dependencies": { - "@codra/schema": "*", + "@codraoss/core": "^0.9.4", + "@codraoss/schema": "^0.9.4", "postgres": "^3.4.9" }, "devDependencies": { - "@types/node": "^22.0.0" + "@types/node": "^22.0.0", + "tsup": "^8.0.0" } }, "packages/db/node_modules/@types/node": { @@ -8741,39 +11124,55 @@ "license": "MIT" }, "packages/models": { - "name": "@codra/models", + "name": "@codraoss/models", "version": "0.9.4", + "license": "AGPL-3.0-only", "dependencies": { - "@codra/core": "*", - "@codra/schema": "*" + "@codraoss/core": "^0.9.4", + "@codraoss/schema": "^0.9.4" }, - "devDependencies": {} + "devDependencies": { + "tsup": "^8.0.0" + } }, "packages/provider-github": { - "name": "@codra/provider-github", + "name": "@codraoss/provider-github", "version": "0.9.4", + "license": "AGPL-3.0-only", "dependencies": { - "@codra/core": "*", - "@codra/schema": "*" + "@codraoss/core": "^0.9.4", + "@codraoss/schema": "^0.9.4" + }, + "devDependencies": { + "tsup": "^8.0.0" } }, "packages/schema": { - "name": "@codra/schema", + "name": "@codraoss/schema", "version": "0.9.4", + "license": "AGPL-3.0-only", "dependencies": { "zod": "^4.3.6" + }, + "devDependencies": { + "tsup": "^8.0.0" } }, "packages/ui": { - "name": "@codra/ui", + "name": "@codraoss/ui", "version": "0.9.4", + "license": "AGPL-3.0-only", "dependencies": { "@base-ui/react": "^1.6.0", + "@codraoss/schema": "^0.9.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "sugar-high": "^2.0.0", "tailwind-merge": "^3.5.0" }, + "devDependencies": { + "tsup": "^8.0.0" + }, "peerDependencies": { "lenis": ">=1.0.0", "lucide-react": ">=1.0.0", diff --git a/package.json b/package.json index cf83099f..e3a84df0 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,7 @@ ], "scripts": { "build": "vite build && npm run cf-typegen", + "build:packages": "npm run build -w @codraoss/schema && npm run build -w @codraoss/core && npm run build -w @codraoss/provider-github && npm run build -w @codraoss/db && npm run build -w @codraoss/models && npm run build -w @codraoss/api && npm run build -w @codraoss/ui", "build:all": "npm run build --workspaces --if-present", "cf-typegen": "cd apps/worker && wrangler types ./src/worker-env.d.ts", "deploy": "npm run build && npm run migrate && cd apps/worker && wrangler deploy", @@ -34,9 +35,15 @@ "test:all": "npm run test --workspaces --if-present", "test:watch": "vitest", "typecheck": "tsc --noEmit", - "typecheck:all": "npm run typecheck --workspaces --if-present" + "typecheck:all": "npm run typecheck --workspaces --if-present", + "changeset": "changeset", + "version:packages": "changeset version", + "release": "npm run build:packages && changeset publish", + "check:exports": "node scripts/check-package-exports.mjs" }, "devDependencies": { + "@arethetypeswrong/cli": "^0.18.5", + "@changesets/cli": "^3.0.0", "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.2.2", "@testing-library/dom": "^10.4.1", @@ -54,7 +61,9 @@ "jsdom": "^29.0.2", "ora": "^9.4.1", "prompts": "^2.4.2", + "publint": "^0.3.23", "tailwindcss": "^4.2.2", + "tsup": "^8.5.1", "typescript": "^6.0.2", "typescript-eslint": "^8.66.0", "vite": "^8.0.8", @@ -62,9 +71,14 @@ "wrangler": "^4.114.0" }, "dependencies": { - "@codra/core": "*", - "@codra/schema": "*", "@base-ui/react": "^1.6.0", + "@codraoss/api": "*", + "@codraoss/core": "*", + "@codraoss/db": "*", + "@codraoss/models": "*", + "@codraoss/provider-github": "*", + "@codraoss/schema": "*", + "@codraoss/ui": "*", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "hono": "^4.12.25", @@ -84,12 +98,11 @@ "sonner": "^2.0.7", "sugar-high": "^2.0.0", "tailwind-merge": "^3.5.0", - "zod": "^4.3.6", - "@codra/ui": "*" + "zod": "^4.3.6" }, "allowScripts": { "esbuild": true, "unrs-resolver@1.12.2": true, "workerd@1.20260801.1": true } -} \ No newline at end of file +} diff --git a/packages/api/LICENSE b/packages/api/LICENSE new file mode 100644 index 00000000..024299ea --- /dev/null +++ b/packages/api/LICENSE @@ -0,0 +1,625 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 2007 Free Software Foundation, Inc. <>_ + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### 0. Definitions +“This License” refers to version 3 of the GNU Affero General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based +on the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that **(1)** displays an appropriate copyright notice, and **(2)** +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code +The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other +than the work as a whole, that **(a)** is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and **(b)** serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +### 2. Basic Permissions +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +### 4. Conveying Verbatim Copies +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified +it, and giving a relevant date. +* **b)** The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. +This requirement modifies the requirement in section 4 to +“keep intact all notices”. +* **c)** You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. +* **d)** If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either **(1)** a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or **(2)** access to copy the +Corresponding Source from a network server at no charge. +* **c)** Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. +* **d)** Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A “User Product” is either **(1)** a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or **(2)** anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install + +### 6. Conveying Non-Source Forms +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or +authors of the material; or +* **e)** Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +### 8. Termination +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated **(a)** +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and **(b)** permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either **(1)** cause the Corresponding Source to be so +available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license **(a)** in connection with copies of the covered work +conveyed by you (or copies made from those copies), or **(b)** primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found. + + Codra: Open source PR review infrastructure for Cloudflare Workers. + Copyright (C) 2026 Devarshi Shimpi + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source” link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<>. diff --git a/packages/api/README.md b/packages/api/README.md new file mode 100644 index 00000000..a44d77eb --- /dev/null +++ b/packages/api/README.md @@ -0,0 +1,15 @@ +# @codraoss/api + +Codra's HTTP surface as a mountable Hono router, wired through ports. + +Part of [Codra](https://codra.run), an open-source code review engine. See the [monorepo](https://github.com/devarshishimpi/codra) for development, and [CONTRIBUTING](https://github.com/devarshishimpi/codra/blob/main/CONTRIBUTING.md) for the dual-licensing / CLA details. + +## Install + +```bash +npm install @codraoss/api +``` + +## License + +[AGPL-3.0-only](./LICENSE) © Devarshi Shimpi diff --git a/packages/api/package.json b/packages/api/package.json index 8795350e..58c3b8cd 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -1,24 +1,57 @@ { - "name": "@codra/api", + "name": "@codraoss/api", "version": "0.9.4", - "private": true, + "description": "Codra's HTTP surface as a mountable Hono router, wired through ports.", + "author": "Devarshi Shimpi", + "license": "AGPL-3.0-only", + "homepage": "https://codra.run", + "repository": { + "type": "git", + "url": "git+https://github.com/devarshishimpi/codra.git", + "directory": "packages/api" + }, + "bugs": { + "url": "https://github.com/devarshishimpi/codra/issues" + }, "type": "module", + "sideEffects": false, "exports": { ".": "./src/index.ts" }, + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + } + }, "scripts": { - "typecheck": "tsc --noEmit" + "build": "tsup", + "typecheck": "tsc -p tsconfig.json", + "prepack": "node ../../scripts/swap-publish-exports.mjs promote", + "postpack": "node ../../scripts/swap-publish-exports.mjs restore" }, "dependencies": { "hono": "^4.12.25", "zod": "^4.3.6", - "@codra/schema": "*", - "@codra/core": "*", - "@codra/db": "*", - "@codra/provider-github": "*", - "@codra/models": "*" + "@codraoss/schema": "^0.9.4", + "@codraoss/core": "^0.9.4", + "@codraoss/db": "^0.9.4", + "@codraoss/provider-github": "^0.9.4", + "@codraoss/models": "^0.9.4" }, "devDependencies": { - "@types/node": "^22.0.0" + "@types/node": "^22.0.0", + "tsup": "^8.0.0" } } diff --git a/packages/api/src/logger.ts b/packages/api/src/logger.ts index 74a6744e..34a3a12d 100644 --- a/packages/api/src/logger.ts +++ b/packages/api/src/logger.ts @@ -1,10 +1,10 @@ import { AsyncLocalStorage } from 'node:async_hooks'; -import { formatLogRecord, setLoggerSink } from '@codra/core/logger'; +import { formatLogRecord, setLoggerSink } from '@codraoss/core/logger'; -// The request-context half of the logger. Scrubbing and record shaping live in @codra/core/logger; +// The request-context half of the logger. Scrubbing and record shaping live in @codraoss/core/logger; // this file owns everything platform-bound -- AsyncLocalStorage and the console sink -- so that // node:async_hooks never enters the engine package. Importing this module installs it as the sink -// that @codra/core's `logger` facade delegates to (see the bottom of the file). +// that @codraoss/core's `logger` facade delegates to (see the bottom of the file). const storage = new AsyncLocalStorage>(); class Logger { @@ -58,6 +58,6 @@ class Logger { export const logger = new Logger(); -// Wired at import scope so engine code logging through @codra/core's facade lands here, with request +// Wired at import scope so engine code logging through @codraoss/core's facade lands here, with request // context attached, rather than in core's bare console fallback. setLoggerSink(logger); diff --git a/packages/api/src/ports.ts b/packages/api/src/ports.ts index 40aec3d5..72d0f05e 100644 --- a/packages/api/src/ports.ts +++ b/packages/api/src/ports.ts @@ -1,16 +1,16 @@ -import type { DashboardSessionUser, SessionStore, ReviewRuntime } from '@codra/core/ports'; +import type { DashboardSessionUser, SessionStore, ReviewRuntime } from '@codraoss/core/ports'; // Type stubs that represent what the API layer requires. -// By importing types from @codra/db, we avoid a runtime dependency while retaining type safety. -import type * as dbAccounts from '@codra/db/accounts'; -import type * as dbJobs from '@codra/db/jobs'; -import type * as dbFileReviews from '@codra/db/file-reviews'; -import type * as dbCommentFeedback from '@codra/db/comment-feedback'; -import type * as dbModelConfigs from '@codra/db/model-configs'; -import type * as dbRepoConfigs from '@codra/db/repo-configs'; -import type * as dbAppSettings from '@codra/db/app-settings'; -import type * as dbStats from '@codra/db/stats'; -import type * as dbWebhookDeliveries from '@codra/db/webhook-deliveries'; +// By importing types from @codraoss/db, we avoid a runtime dependency while retaining type safety. +import type * as dbAccounts from '@codraoss/db/accounts'; +import type * as dbJobs from '@codraoss/db/jobs'; +import type * as dbFileReviews from '@codraoss/db/file-reviews'; +import type * as dbCommentFeedback from '@codraoss/db/comment-feedback'; +import type * as dbModelConfigs from '@codraoss/db/model-configs'; +import type * as dbRepoConfigs from '@codraoss/db/repo-configs'; +import type * as dbAppSettings from '@codraoss/db/app-settings'; +import type * as dbStats from '@codraoss/db/stats'; +import type * as dbWebhookDeliveries from '@codraoss/db/webhook-deliveries'; export interface RepositoriesPort { accounts: typeof dbAccounts; diff --git a/packages/api/src/router.ts b/packages/api/src/router.ts index 7a4b523e..6b583704 100644 --- a/packages/api/src/router.ts +++ b/packages/api/src/router.ts @@ -15,11 +15,12 @@ import { createSettingsRouter } from './routes/api/settings'; async function serveIndex(c: Context) { // If the host platform passes an ASSETS binding via `c.env`, use it (e.g., Cloudflare Workers). - const assetsFetch = (c.env as any).ASSETS?.fetch; - if (typeof assetsFetch === 'function') { - return assetsFetch(new URL('/index.html', c.req.url)); + const assets = (c.env as any).ASSETS; + if (assets && typeof assets.fetch === 'function') { + // Method call, and `/` not `/index.html`: detaching throws, and `/index.html` 307s into a loop. + return assets.fetch(new Request(new URL('/', c.req.url), c.req.raw)); } - + return c.text('Not Found: Please mount UI static assets handler here.', 404); } diff --git a/packages/api/src/routes/api/auth.ts b/packages/api/src/routes/api/auth.ts index 0069ad2f..0c6327aa 100644 --- a/packages/api/src/routes/api/auth.ts +++ b/packages/api/src/routes/api/auth.ts @@ -1,4 +1,4 @@ -import { isSupportedTimeZone } from '@codra/schema/timezone'; +import { isSupportedTimeZone } from '@codraoss/schema/timezone'; import { Hono } from 'hono'; import { z } from 'zod'; import { jsonError } from '../../http'; diff --git a/packages/api/src/routes/api/jobs.ts b/packages/api/src/routes/api/jobs.ts index 31ee97d5..721d9859 100644 --- a/packages/api/src/routes/api/jobs.ts +++ b/packages/api/src/routes/api/jobs.ts @@ -1,17 +1,14 @@ import { Hono } from 'hono'; import type { Context } from 'hono'; -import { defaultRepoConfig, findingLabelSchema, jobsQuerySchema } from '@codra/schema'; +import { defaultRepoConfig, findingLabelSchema, jobsQuerySchema } from '@codraoss/schema'; import { jsonError } from '../../http'; -import { parseUnifiedDiff } from '@codra/core/diff'; -import { buildFileReviewPrompts } from '@codra/core/prompts/file-review'; +import { parseUnifiedDiff } from '@codraoss/core/diff'; +import { buildFileReviewPrompts, changelogExcerptFromDiff, wantsFileContext } from '@codraoss/core/prompts/file-review'; import type { ApiEnv } from '../../ports'; -// Best-effort terminate; .get() throws if the instance is gone and .terminate() if already terminal, both non-fatal. +// Best-effort: .get()/.terminate() throw if instance is gone/already terminal; both non-fatal. async function terminateJobWorkflow(c: Context, job: { id: string; workflowInstanceId?: string | null }) { - // This interacts with bindings. The plan says "No binding access in a handler". - // So we need to put workflow termination behind a platform port. - // Wait! The user plan says: "call a packages/core use case". - // Let's add it to `platform` port as well. + // Goes through platform port; handlers must not touch bindings directly. await c.env.deps.platform.terminateJobWorkflow(job); } @@ -67,7 +64,7 @@ export function createJobsRouter() { return response; }); - // diff_input is not persisted; rebuilt on demand from the job's own base/head commits (not the live PR), using the KV cache while warm. + // diff_input isn't persisted; rebuilt from the job's own base/head commits (not the live PR), via KV cache. app.get('/:id/diffs', async (c) => { const job = await c.env.deps.repositories.jobs.getJobDetail(c.env as any, c.req.param('id')); if (!job) { @@ -85,31 +82,37 @@ export function createJobsRouter() { github, ); } catch (error) { - // Need logger from deps or imported directly since we moved it c.env.deps.platform.logger.warn(`Could not reconstruct diff for job ${job.id}`, error instanceof Error ? error : new Error(String(error))); return c.json({ diffs: {} }); } - // Must include the PR description: this reconstructs the prompt the model actually saw. + // Reconstructs the prompt the model actually saw, so it needs the PR description too. let prDescription: string | null = null; try { prDescription = (await github.getPullRequest(job.owner, job.repo, job.prNumber)).body ?? null; } catch (error) { - // Best-effort: a missing description degrades fidelity, never fails the view. + // Best-effort: missing description degrades fidelity, doesn't fail the view. c.env.deps.platform.logger.warn(`Could not load the PR body for job ${job.id}; prompts will omit the description`, error instanceof Error ? error : new Error(String(error))); } - // The ENTIRE PR diff, not just files with a review row, so Files-changed matches GitHub mid-review. + // Full PR diff, not just reviewed files, so file count matches GitHub mid-review. const diffs: Record = {}; - for (const file of parseUnifiedDiff(rawDiff, config.review)) { + const parsedFiles = parseUnifiedDiff(rawDiff, config.review); + const changelogExcerpt = changelogExcerptFromDiff(parsedFiles); + for (const file of parsedFiles) { if (file.isDeleted || file.isBinary || !file.path) continue; - diffs[file.path] = buildFileReviewPrompts({ + const { userPrompt } = buildFileReviewPrompts({ file, prTitle: job.prTitle, prDescription, + changelogExcerpt, config: config.review, - }).userPrompt; + }); + const hadContext = wantsFileContext(file, config.review.full_file_context); + diffs[file.path] = hadContext + ? `${userPrompt}\n\n[The full file at the reviewed commit was included here at review time; it is omitted from this preview.]` + : userPrompt; } const response = c.json({ diffs }); @@ -117,7 +120,7 @@ export function createJobsRouter() { return response; }); - // Shared by re-run and rerun-from-start; inherit=true links retryOfJobId and reuses `done` file reviews, false reviews everything. + // inherit=true links retryOfJobId and reuses done file reviews; false reviews everything. async function startReplacementJob(c: Context, rawSource: any, options: { inherit: boolean }) { const jobs = c.env.deps.repositories.jobs; const source = jobs.mapJob(rawSource); @@ -167,7 +170,6 @@ export function createJobsRouter() { return job; } - // Re-run: reuse the parent's completed reviews where the model strategy still matches. app.post('/:id/retry', async (c) => { const jobs = c.env.deps.repositories.jobs; const rawSource = await jobs.getJobForProcessing(c.env as any, c.req.param('id')); @@ -178,7 +180,7 @@ export function createJobsRouter() { return c.json({ job }, 202); }); - // Rerun from start: no inheritance. Stops the current run so two workflows cannot race. + // No inheritance; stops the current run first so two workflows can't race. app.post('/:id/rerun', async (c) => { const jobs = c.env.deps.repositories.jobs; const rawSource = await jobs.getJobForProcessing(c.env as any, c.req.param('id')); @@ -210,7 +212,7 @@ export function createJobsRouter() { return c.json({ job: updated ? jobs.mapJob(updated) : job }, 200); }); - // Human verdict on one finding: WRONG suppresses repository-wide, RIGHT suppresses nothing and is purely measurement. + // "wrong" suppresses this finding repo-wide; "right" is measurement only. app.put('/:id/findings/:fingerprint/label', async (c) => { const jobId = c.req.param('id'); const fingerprint = c.req.param('fingerprint'); @@ -226,7 +228,7 @@ export function createJobsRouter() { prNumber: target.pr_number, fingerprint, anchorHash: target.anchor_hash, - // Carried so a rejection survives the model rewording its title. + // Keeps rejection valid even if the model rewords the title. fingerprintV2: target.fingerprint_v2, jobId, labelledBy: c.get('sessionUser')?.providerUserId ? Number(c.get('sessionUser')?.providerUserId) : null, @@ -236,7 +238,7 @@ export function createJobsRouter() { return c.json({ label: parsed.data.label }, 200); }); - // Undo a label, scoped to dashboard rows so a real GitHub deletion stays recorded. + // Scoped to dashboard rows; a real GitHub deletion still stays recorded. app.delete('/:id/findings/:fingerprint/label', async (c) => { const jobId = c.req.param('id'); const fingerprint = c.req.param('fingerprint'); diff --git a/packages/api/src/routes/api/models.ts b/packages/api/src/routes/api/models.ts index a9e3708f..c5727d39 100644 --- a/packages/api/src/routes/api/models.ts +++ b/packages/api/src/routes/api/models.ts @@ -1,7 +1,7 @@ import { Hono } from 'hono'; import { z } from 'zod'; import { jsonError } from '../../http'; -import { llmApiFormats } from '@codra/schema'; +import { llmApiFormats } from '@codraoss/schema'; import type { ApiEnv } from '../../ports'; const apiFormatSchema = z.enum(llmApiFormats); diff --git a/packages/api/src/routes/api/repos.ts b/packages/api/src/routes/api/repos.ts index be66a68f..c10bc2a3 100644 --- a/packages/api/src/routes/api/repos.ts +++ b/packages/api/src/routes/api/repos.ts @@ -2,7 +2,7 @@ import { Hono } from 'hono'; import { z } from 'zod'; import type { ApiEnv } from '../../ports'; import { jsonError } from '../../http'; -import { repoConfigSchema } from '@codra/schema'; +import { repoConfigSchema } from '@codraoss/schema'; const repoConfigPatchSchema = z .strictObject({ diff --git a/packages/api/src/routes/api/settings.ts b/packages/api/src/routes/api/settings.ts index d81f622d..774f95ce 100644 --- a/packages/api/src/routes/api/settings.ts +++ b/packages/api/src/routes/api/settings.ts @@ -2,7 +2,7 @@ import { Hono } from 'hono'; import { z } from 'zod'; import type { ApiEnv } from '../../ports'; import { jsonError } from '../../http'; -import { reviewConcurrencyLevels, reviewMaxCommentsOptions, reviewMaxFilesRange, reviewSettingsSchema } from '@codra/schema'; +import { reviewConcurrencyLevels, reviewMaxCommentsOptions, reviewMaxFilesRange, reviewSettingsSchema } from '@codraoss/schema'; const reviewSettingsPatchSchema = z.strictObject({ concurrencyLevel: z.enum(reviewConcurrencyLevels).optional(), diff --git a/packages/api/src/routes/webhook.ts b/packages/api/src/routes/webhook.ts index 1bca3089..2daad75a 100644 --- a/packages/api/src/routes/webhook.ts +++ b/packages/api/src/routes/webhook.ts @@ -5,7 +5,7 @@ import { type FeedbackWebhookPayload, type GitHubReviewCommentPayload, type GitHubWebhookPayload, -} from '@codra/schema/github'; +} from '@codraoss/schema/github'; import type { ApiEnv } from '../ports'; import { jsonError } from '../http'; diff --git a/packages/api/src/sessions.ts b/packages/api/src/sessions.ts index d622f8ab..ae7fae0b 100644 --- a/packages/api/src/sessions.ts +++ b/packages/api/src/sessions.ts @@ -1,7 +1,7 @@ import { deleteCookie, getCookie, setCookie } from 'hono/cookie'; import type { Context } from 'hono'; import type { ApiEnv } from './ports'; -import type { DashboardSessionUser } from '@codra/core/ports'; +import type { DashboardSessionUser } from '@codraoss/core/ports'; const SESSION_COOKIE_NAME = 'codra_session'; const SESSION_TTL_SECONDS = 60 * 60 * 24 * 7; diff --git a/packages/api/tsconfig.json b/packages/api/tsconfig.json index 347926ac..6e673328 100644 --- a/packages/api/tsconfig.json +++ b/packages/api/tsconfig.json @@ -3,9 +3,11 @@ "compilerOptions": { "module": "ESNext", "moduleResolution": "Bundler", - "outDir": "./dist", - "rootDir": "./src", - "types": ["node"] + "types": ["node"], + "composite": false, + "declaration": false, + "emitDeclarationOnly": false, + "noEmit": true }, "include": ["src/**/*"] } diff --git a/packages/api/tsup.config.json b/packages/api/tsup.config.json new file mode 100644 index 00000000..bc7ed045 --- /dev/null +++ b/packages/api/tsup.config.json @@ -0,0 +1,9 @@ +{ + "entry": ["src/index.ts"], + "format": "esm", + "dts": true, + "splitting": true, + "sourcemap": true, + "clean": true, + "outDir": "dist" +} diff --git a/packages/core/LICENSE b/packages/core/LICENSE new file mode 100644 index 00000000..024299ea --- /dev/null +++ b/packages/core/LICENSE @@ -0,0 +1,625 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 2007 Free Software Foundation, Inc. <>_ + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### 0. Definitions +“This License” refers to version 3 of the GNU Affero General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based +on the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that **(1)** displays an appropriate copyright notice, and **(2)** +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code +The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other +than the work as a whole, that **(a)** is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and **(b)** serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +### 2. Basic Permissions +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +### 4. Conveying Verbatim Copies +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified +it, and giving a relevant date. +* **b)** The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. +This requirement modifies the requirement in section 4 to +“keep intact all notices”. +* **c)** You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. +* **d)** If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either **(1)** a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or **(2)** access to copy the +Corresponding Source from a network server at no charge. +* **c)** Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. +* **d)** Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A “User Product” is either **(1)** a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or **(2)** anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install + +### 6. Conveying Non-Source Forms +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or +authors of the material; or +* **e)** Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +### 8. Termination +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated **(a)** +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and **(b)** permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either **(1)** cause the Corresponding Source to be so +available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license **(a)** in connection with copies of the covered work +conveyed by you (or copies made from those copies), or **(b)** primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found. + + Codra: Open source PR review infrastructure for Cloudflare Workers. + Copyright (C) 2026 Devarshi Shimpi + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source” link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<>. diff --git a/packages/core/README.md b/packages/core/README.md new file mode 100644 index 00000000..2c46cfab --- /dev/null +++ b/packages/core/README.md @@ -0,0 +1,15 @@ +# @codraoss/core + +The Codra review engine: pure, transport- and platform-agnostic code review logic behind ports. + +Part of [Codra](https://codra.run), an open-source code review engine. See the [monorepo](https://github.com/devarshishimpi/codra) for development, and [CONTRIBUTING](https://github.com/devarshishimpi/codra/blob/main/CONTRIBUTING.md) for the dual-licensing / CLA details. + +## Install + +```bash +npm install @codraoss/core +``` + +## License + +[AGPL-3.0-only](./LICENSE) © Devarshi Shimpi diff --git a/packages/core/package.json b/packages/core/package.json index 13cbd73f..e2a4434a 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,8 +1,20 @@ { - "name": "@codra/core", + "name": "@codraoss/core", "version": "0.9.4", - "private": true, + "description": "The Codra review engine: pure, transport- and platform-agnostic code review logic behind ports.", + "author": "Devarshi Shimpi", + "license": "AGPL-3.0-only", + "homepage": "https://codra.run", + "repository": { + "type": "git", + "url": "git+https://github.com/devarshishimpi/codra.git", + "directory": "packages/core" + }, + "bugs": { + "url": "https://github.com/devarshishimpi/codra/issues" + }, "type": "module", + "sideEffects": false, "exports": { ".": "./src/index.ts", "./ports": "./src/ports/index.ts", @@ -21,17 +33,98 @@ "./prompts/summary": "./src/prompts/summary.ts", "./prompts/verify": "./src/prompts/verify.ts" }, + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./ports": { + "types": "./dist/ports/index.d.ts", + "import": "./dist/ports/index.js" + }, + "./logger": { + "types": "./dist/logger.d.ts", + "import": "./dist/logger.js" + }, + "./diff": { + "types": "./dist/diff/index.d.ts", + "import": "./dist/diff/index.js" + }, + "./model-output": { + "types": "./dist/model-output/index.d.ts", + "import": "./dist/model-output/index.js" + }, + "./rules/detect": { + "types": "./dist/rules/detect.d.ts", + "import": "./dist/rules/detect.js" + }, + "./rules/table": { + "types": "./dist/rules/table.d.ts", + "import": "./dist/rules/table.js" + }, + "./claim-checks": { + "types": "./dist/claim-checks.d.ts", + "import": "./dist/claim-checks.js" + }, + "./verify": { + "types": "./dist/verify.d.ts", + "import": "./dist/verify.js" + }, + "./fingerprint": { + "types": "./dist/fingerprint.d.ts", + "import": "./dist/fingerprint.js" + }, + "./timeout": { + "types": "./dist/timeout.d.ts", + "import": "./dist/timeout.js" + }, + "./token-tracker": { + "types": "./dist/token-tracker.d.ts", + "import": "./dist/token-tracker.js" + }, + "./prompts/file-review": { + "types": "./dist/prompts/file-review.d.ts", + "import": "./dist/prompts/file-review.js" + }, + "./prompts/languages": { + "types": "./dist/prompts/languages.d.ts", + "import": "./dist/prompts/languages.js" + }, + "./prompts/summary": { + "types": "./dist/prompts/summary.d.ts", + "import": "./dist/prompts/summary.js" + }, + "./prompts/verify": { + "types": "./dist/prompts/verify.d.ts", + "import": "./dist/prompts/verify.js" + } + } + }, "scripts": { + "build": "tsup", "typecheck": "tsc -p tsconfig.json", - "test": "vitest run" + "test": "vitest run", + "prepack": "node ../../scripts/swap-publish-exports.mjs promote", + "postpack": "node ../../scripts/swap-publish-exports.mjs restore" }, "dependencies": { - "@codra/schema": "*", + "@codraoss/schema": "^0.9.4", "jsonrepair": "^3.15.0", "picomatch": "^4.0.5", "zod": "^4.3.6" }, "devDependencies": { - "@types/picomatch": "^4.0.3" + "@types/picomatch": "^4.0.3", + "tsup": "^8.0.0" } } diff --git a/packages/core/src/claim-checks.ts b/packages/core/src/claim-checks.ts index 1f991068..0eecd04f 100644 --- a/packages/core/src/claim-checks.ts +++ b/packages/core/src/claim-checks.ts @@ -1,5 +1,5 @@ // SOUNDNESS, binding on every change: `refuted` asserts only that "X does not appear" is FALSE. There is no `confirmed` verdict, since a check that can confirm findings manufactures them. Losing a refutation is free; a wrong one silences a real defect. -import type { DiffLine, FileDiff } from './diff'; +import type { FileDiff } from './diff'; import { normalizeDiffText } from './fingerprint'; const PROXIMITY_WINDOW_LINES = 25; @@ -35,7 +35,7 @@ const VERSION_CLAIM_PATTERNS: readonly RegExp[] = [ /\bis not (?:exposed|exported|available) (?:by|from|in)\b/i, ]; -// Same soundness rule as the absence checker above: a refutation asserts only that the claim cannot be +// Same soundness rule as the absence checker above. const CROSS_FILE_SUBJECT = /\b(?:other|another|external|downstream|consuming|importing|dependent|calling)\s+(?:module|file|component|caller|package|consumer|import)s?\b/i; const CROSS_FILE_CONSEQUENCE = /\b(?:break|breaks|breaking|broken|fail|fails|failing|error|errors|cannot import|can't import|unable to|compilation|compile|prevent|prevents|preventing|block|blocks|blocking)\b/i; @@ -49,14 +49,7 @@ const CALLEE_UNHANDLED_OUTCOME = /\bunhandled\b|\bunhandled promise\b|\bnot (?:c export type UndecidableClaimReason = 'cross-file' | 'environment' | 'callee-errors'; -/** - * Refutes a claim whose truth lives outside the diff, returning the family it belongs to or null. - * - * Deliberately requires TWO independent signals per family -- a subject and a consequence -- because - * either alone is ordinary review language. "This breaks the build" is a normal thing to say about - * code in the diff; "other modules import this" is a normal aside. Only together do they describe a - * consequence in a file nobody showed the model. - */ +/** Refutes a claim whose truth lives outside the diff; two signals per family, since one is ordinary. */ export function refuteUndecidableClaim(input: { title: string; body: string }): UndecidableClaimReason | null { const text = `${input.title}\n${input.body}`; @@ -81,7 +74,8 @@ export function isVersionClaimRefutedByPin(input: { title: string; body: string; return FULL_SHA_PATTERN.test(input.anchorContent); } -type PresenceEntry = { line: DiffLine; hunkIndex: number; code: string }; +/** One line the identifier could be found on; `hunkIndex` is null for lines from the post-image. */ +type PresenceEntry = { newLineNumber: number | undefined; hunkIndex: number | null; code: string }; export type PresenceIndex = { byToken: Map; @@ -100,16 +94,37 @@ export type AbsenceClaimVerdict = | 'not_present' | 'out_of_window'; } - | { status: 'refuted'; identifier: string; line: DiffLine }; + | { status: 'refuted'; identifier: string; line: number | undefined }; type CommentSyntax = { line: readonly string[]; block: boolean }; +// Must stay complete: a misclassified file keeps comment text as code, refuting real absence claims. +const HASH_COMMENT_EXTENSIONS = new Set([ + 'py', 'pyi', 'rb', 'sh', 'bash', 'zsh', 'fish', 'ps1', 'yaml', 'yml', 'toml', 'ini', 'cfg', 'conf', + 'tf', 'tfvars', 'hcl', 'pl', 'pm', 'r', 'jl', 'nim', 'cr', 'ex', 'exs', 'elixir', 'gemspec', + 'dockerfile', 'containerfile', 'mk', 'cmake', 'gradle', 'properties', 'env', 'gitignore', + 'dockerignore', 'editorconfig', +]); + +const HASH_COMMENT_FILENAMES = new Set([ + 'dockerfile', 'containerfile', 'makefile', 'gnumakefile', 'rakefile', 'gemfile', 'brewfile', + 'procfile', 'vagrantfile', 'justfile', 'cmakelists.txt', '.gitignore', '.dockerignore', '.env', +]); + export function commentSyntaxFor(path: string): CommentSyntax { - const ext = path.toLowerCase().split('.').pop() ?? ''; - if (ext === 'py' || ext === 'rb' || ext === 'sh' || ext === 'yaml' || ext === 'yml' || ext === 'toml') { - return { line: ['#'], block: false }; - } + const name = path.toLowerCase().split('/').pop() ?? ''; + if (HASH_COMMENT_FILENAMES.has(name)) return { line: ['#'], block: false }; + + const ext = name.includes('.') ? name.split('.').pop() ?? '' : ''; + if (HASH_COMMENT_EXTENSIONS.has(ext)) return { line: ['#'], block: false }; + if (ext === 'sql') return { line: ['--'], block: true }; + if (ext === 'lua') return { line: ['--'], block: true }; + if (ext === 'hs' || ext === 'elm' || ext === 'ada') return { line: ['--'], block: false }; + if (ext === 'vim') return { line: ['"'], block: false }; + if (ext === 'clj' || ext === 'cljs' || ext === 'edn' || ext === 'lisp' || ext === 'scm') { + return { line: [';'], block: false }; + } return { line: ['//'], block: true }; } @@ -195,14 +210,29 @@ function scanTemplateLiteral(input: string, start: number): { code: string; next return null; } +// MEASURED AND REJECTED: a call-site/reachability gate. The withheld slice had precision 27.3% vs an +// 18.7% pooled baseline, and "mentions callers" scored -0.3 on the codra-only subset. Any gate proposed +// from this corpus must be re-checked on the codra-only subset -- pooled signals do not survive it. + const TOKEN_PATTERN = /[A-Za-z_$][\w$]*/g; -export function buildPresenceIndex(file: FileDiff): PresenceIndex { +/** Where each identifier appears after the change; without a post-image the index sees only the diff. */ +export function buildPresenceIndex(file: FileDiff, fileContent?: string | null): PresenceIndex { const syntax = commentSyntaxFor(file.path); const byToken = new Map(); const entries: PresenceEntry[] = []; const hunkByLine = new Map(); + const add = (entry: PresenceEntry) => { + entries.push(entry); + for (const match of entry.code.matchAll(TOKEN_PATTERN)) { + const token = match[0]; + const existing = byToken.get(token); + if (existing) existing.push(entry); + else byToken.set(token, [entry]); + } + }; + file.hunks.forEach((hunk, hunkIndex) => { for (const line of hunk.lines) { if (line.newLineNumber !== undefined) hunkByLine.set(line.newLineNumber, hunkIndex); @@ -212,18 +242,23 @@ export function buildPresenceIndex(file: FileDiff): PresenceIndex { const code = stripCommentsAndStrings(normalizeDiffText(line.content), syntax); if (code === null) continue; - const entry: PresenceEntry = { line, hunkIndex, code }; - entries.push(entry); - - for (const match of code.matchAll(TOKEN_PATTERN)) { - const token = match[0]; - const existing = byToken.get(token); - if (existing) existing.push(entry); - else byToken.set(token, [entry]); - } + add({ newLineNumber: line.newLineNumber, hunkIndex, code }); } }); + if (fileContent) { + const lines = fileContent.split('\n'); + for (let i = 0; i < lines.length; i++) { + const newLineNumber = i + 1; + if (hunkByLine.has(newLineNumber)) continue; + + const code = stripCommentsAndStrings(normalizeDiffText(lines[i]), syntax); + if (code === null) continue; + + add({ newLineNumber, hunkIndex: null, code }); + } + } + return { byToken, entries, hunkByLine }; } @@ -286,11 +321,11 @@ export function checkAbsenceClaim(input: { const anchorHunk = input.anchorLine !== undefined ? input.index.hunkByLine.get(input.anchorLine) : undefined; const nearby = occurrences.find((entry) => { - if (anchorHunk !== undefined && entry.hunkIndex === anchorHunk) return true; - if (input.anchorLine === undefined || entry.line.newLineNumber === undefined) return false; - return Math.abs(entry.line.newLineNumber - input.anchorLine) <= PROXIMITY_WINDOW_LINES; + if (anchorHunk !== undefined && entry.hunkIndex !== null && entry.hunkIndex === anchorHunk) return true; + if (input.anchorLine === undefined || entry.newLineNumber === undefined) return false; + return Math.abs(entry.newLineNumber - input.anchorLine) <= PROXIMITY_WINDOW_LINES; }); if (!nearby) return { status: 'unknown', reason: 'out_of_window' }; - return { status: 'refuted', identifier, line: nearby.line }; + return { status: 'refuted', identifier, line: nearby.newLineNumber }; } diff --git a/packages/core/src/constants.ts b/packages/core/src/constants.ts index c3d9456f..daade712 100644 --- a/packages/core/src/constants.ts +++ b/packages/core/src/constants.ts @@ -3,6 +3,9 @@ export const PACKABLE_MAX_DIFF_LINES = 150; export const BIN_TARGET_DIFF_LINES = 300; export const BIN_MAX_FILES = 4; export const BIN_DIFF_CHAR_BUDGET = 24_000; +// Files with many scattered hunks get whole-file context; the line floor excludes trivial files. +export const FRAGMENTED_HUNK_THRESHOLD = 5; +export const FRAGMENTED_MIN_LINES = 60; export const DIFF_CACHE_TTL_SECONDS = 6 * 60 * 60; // Phase Control & Timers @@ -37,3 +40,9 @@ export const MAX_RULE_SCAN_ADDED_LINES = 600; // Prompts export const EXEMPLAR_BLOCK_CHARS = 700; export const PR_DESCRIPTION_CHARS = 2_000; +// Changelog entries are short; keep well under the description budget. +export const CHANGELOG_EXCERPT_CHARS = 600; +// Whole-file context budget when full_file_context is on; half the bin diff budget. +export const FILE_CONTEXT_CHAR_BUDGET = 8_000; +// Per-chunk window, not a repeated whole-file block, to stay within the budget above. +export const FILE_CONTEXT_WINDOW_LINES = 120; diff --git a/packages/core/src/diff/index.ts b/packages/core/src/diff/index.ts index 2c246bbf..ab20686e 100644 --- a/packages/core/src/diff/index.ts +++ b/packages/core/src/diff/index.ts @@ -1,5 +1,5 @@ import picomatch from 'picomatch'; -import type { RepoConfig } from '@codra/schema'; +import type { RepoConfig } from '@codraoss/schema'; import { type DiffLineKind, type DiffLine, @@ -279,8 +279,23 @@ export function filterReviewableFiles( } reviewable.sort((left, right) => Number(left.isNew) - Number(right.isNew) || left.path.localeCompare(right.path)); + const withinFileLimit = reviewable.slice(0, maxFiles); + + // Job-level input ceiling. Files are kept or dropped whole; half a file's hunks would lie. + const kept: FileDiff[] = []; + let totalChars = 0; + for (const file of withinFileLimit) { + const fileChars = file.hunks.reduce( + (sum, hunk) => sum + hunk.lines.reduce((lineSum, line) => lineSum + line.content.length + 1, 0), + 0, + ); + if (kept.length > 0 && totalChars + fileChars > config.max_total_diff_chars) break; + kept.push(file); + totalChars += fileChars; + } + return { - files: reviewable.slice(0, maxFiles), - skipped: Math.max(0, reviewable.length - maxFiles), + files: kept, + skipped: Math.max(0, reviewable.length - kept.length), }; } diff --git a/packages/core/src/finding-gates.ts b/packages/core/src/finding-gates.ts index 19b69ddc..712b1631 100644 --- a/packages/core/src/finding-gates.ts +++ b/packages/core/src/finding-gates.ts @@ -1,8 +1,9 @@ -import type { FindingDisposition, ParsedReviewComment, RepoConfig } from '@codra/schema'; +import type { FindingDisposition, ParsedReviewComment, RepoConfig } from '@codraoss/schema'; import type { FileDiff } from './diff'; import type { ReviewModel } from './ports'; import { renderDiffSnippet, parseVerifyResponse, type VerifyCandidate } from './prompts/verify'; import { logger } from './logger'; +import { reviewBreadth } from './prompts/file-review'; type VerifiableJob = { id: string }; @@ -24,8 +25,8 @@ export function shadowEvaluate(candidates: ParsedReviewComment[], posted: Parsed }; } -function verifyCandidateLimit(effectiveMaxComments: number) { - return Math.min(40, Math.max(10, effectiveMaxComments * 3)); +function verifyCandidateLimit(breadth: number) { + return Math.min(40, Math.max(10, breadth * 3)); } import { VERIFY_MIN_ANSWER_RATIO } from './constants'; @@ -36,10 +37,14 @@ export type VerifyDrop = { reason?: string; }; +/** `null` means verification ran; any other value means findings were posted unverified. */ +export type VerifySkipReason = 'no_verifiable_candidates' | 'low_answer_ratio' | 'verify_call_failed'; + export type VerifyOutcome = { comments: ParsedReviewComment[]; dropped: VerifyDrop[]; reasons: Map; + skipped: VerifySkipReason | null; }; export async function verifyFindings(params: { @@ -52,11 +57,16 @@ export async function verifyFindings(params: { }): Promise { const { comments, files, model, config, job } = params; - const keepAll = (): VerifyOutcome => ({ comments, dropped: [], reasons: new Map() }); + const keepAll = (skipped: VerifySkipReason | null): VerifyOutcome => ({ + comments, + dropped: [], + reasons: new Map(), + skipped, + }); - if (comments.length === 0) return keepAll(); + if (comments.length === 0) return keepAll(null); - const limit = verifyCandidateLimit(params.maxCandidates ?? config.review.max_comments); + const limit = verifyCandidateLimit(params.maxCandidates ?? reviewBreadth(config.review)); const toVerify = comments.slice(0, limit); const fileByPath = new Map(files.map((file) => [file.path, file])); @@ -66,7 +76,7 @@ export async function verifyFindings(params: { })); const verifiable = prepared.filter((entry) => entry.snippet !== '' || entry.comment.evidence); - if (verifiable.length === 0) return keepAll(); + if (verifiable.length === 0) return keepAll('no_verifiable_candidates'); const candidates: VerifyCandidate[] = verifiable.map((entry, index) => ({ index, @@ -101,7 +111,7 @@ export async function verifyFindings(params: { logger.warn('Verification did not answer enough indices; keeping all findings', { jobId: job.id, candidates: candidates.length, answered, }); - return keepAll(); + return keepAll('low_answer_ratio'); } const dropped: VerifyDrop[] = []; @@ -133,12 +143,12 @@ export async function verifyFindings(params: { topReasons: dropped.slice(0, 5).map((drop) => drop.reason), }); - return { comments: comments.filter((comment) => !droppedSet.has(comment)), dropped, reasons }; + return { comments: comments.filter((comment) => !droppedSet.has(comment)), dropped, reasons, skipped: null }; } catch (error) { logger.warn('Verification pass failed; posting pre-verification findings', { jobId: job.id, error: error instanceof Error ? error.message : String(error), }); - return keepAll(); + return keepAll('verify_call_failed'); } } diff --git a/packages/core/src/model-output/batch.ts b/packages/core/src/model-output/batch.ts index 83931689..1fa5e459 100644 --- a/packages/core/src/model-output/batch.ts +++ b/packages/core/src/model-output/batch.ts @@ -1,4 +1,4 @@ -import type { ClaimType } from '@codra/schema'; +import type { ClaimType } from '@codraoss/schema'; import type { FileDiff } from '../diff'; import { generatorFindingCap } from '../prompts/file-review'; import { logger } from '../logger'; diff --git a/packages/core/src/model-output/dedupe.ts b/packages/core/src/model-output/dedupe.ts index 0d3e080d..a5804f0a 100644 --- a/packages/core/src/model-output/dedupe.ts +++ b/packages/core/src/model-output/dedupe.ts @@ -1,4 +1,4 @@ -import type { ParsedReviewComment } from '@codra/schema'; +import type { ParsedReviewComment } from '@codraoss/schema'; import { normalizeFindingTitle } from '../fingerprint'; const SEVERITY_RANK: Record = { P0: 0, P1: 1, P2: 2, P3: 3, nit: 4 }; @@ -8,13 +8,16 @@ const NUL = String.fromCharCode(0); export function dedupeFindings(comments: ParsedReviewComment[]): ParsedReviewComment[] { const best = new Map(); for (const comment of comments) { - const key = comment.source === 'rule' - ? `rule${NUL}${comment.ruleId ?? ''}${NUL}${comment.path}${NUL}${comment.anchorHash ?? ''}` - : normalizeFindingTitle(comment.title); - if (!key) { + // Union, keyed per location. Never weight by agreement: 7 configs agreeing measured 7% correct, 1 config 20%. + const normalizedTitle = comment.source === 'rule' ? '' : normalizeFindingTitle(comment.title); + if (comment.source !== 'rule' && !normalizedTitle) { best.set(`__unique__${best.size}`, comment); continue; } + + const key = comment.source === 'rule' + ? `rule${NUL}${comment.ruleId ?? ''}${NUL}${comment.path}${NUL}${comment.anchorHash ?? ''}` + : `llm${NUL}${comment.path}${NUL}${comment.anchorHash ?? comment.line ?? ''}${NUL}${normalizedTitle}`; const existing = best.get(key); if (!existing) { best.set(key, comment); diff --git a/packages/core/src/model-output/evidence.ts b/packages/core/src/model-output/evidence.ts index c7f5cb0b..1f4f71a0 100644 --- a/packages/core/src/model-output/evidence.ts +++ b/packages/core/src/model-output/evidence.ts @@ -3,14 +3,17 @@ import type { DiffLine, FileDiff } from '../diff'; import { MIN_DISCRIMINATING_EVIDENCE_CHARS } from '../constants'; +/** `anchor` is where a quoting finding gets posted; `sourceKind` is the text's kind pre-re-anchoring. */ +export type IndexedLine = { anchor: DiffLine; sourceKind: DiffLine['kind'] }; + export type EvidenceIndex = { - byContent: Map; - lines: { normalized: string; line: DiffLine }[]; + byContent: Map; + lines: { normalized: string; line: IndexedLine }[]; }; export function buildEvidenceIndex(file: FileDiff): EvidenceIndex { - const byContent = new Map(); - const lines: { normalized: string; line: DiffLine }[] = []; + const byContent = new Map(); + const lines: { normalized: string; line: IndexedLine }[] = []; for (const hunk of file.hunks) { const postable = hunk.lines.filter((line) => line.kind !== 'del' && line.newLineNumber !== undefined); @@ -27,10 +30,11 @@ export function buildEvidenceIndex(file: FileDiff): EvidenceIndex { ?? postable[0]; } - lines.push({ normalized, line: anchor }); + const entry: IndexedLine = { anchor, sourceKind: line.kind }; + lines.push({ normalized, line: entry }); const existing = byContent.get(normalized); - if (existing) existing.push(anchor); - else byContent.set(normalized, [anchor]); + if (existing) existing.push(entry); + else byContent.set(normalized, [entry]); }); } @@ -69,7 +73,8 @@ export function buildBinAmbiguityIndex(files: readonly FileDiff[]): BinAmbiguity export type EvidenceResolution = | { status: 'absent' } | { status: 'weak' } - | { status: 'matched'; line: DiffLine } + // `touched` is false only when every occurrence of the quoted text is an untouched context line. + | { status: 'matched'; line: DiffLine; touched: boolean } | { status: 'unmatched' }; export function resolveEvidence( @@ -83,24 +88,35 @@ export function resolveEvidence( if (!firstLine) return { status: 'absent' }; if (firstLine.length < MIN_DISCRIMINATING_EVIDENCE_CHARS) return { status: 'weak' }; - const nearest = (candidates: DiffLine[]) => { - if (reportedLine === undefined) return candidates[0]; - return candidates.reduce((best, candidate) => - Math.abs((candidate.newLineNumber ?? 0) - reportedLine) < Math.abs((best.newLineNumber ?? 0) - reportedLine) + // Judged over the whole candidate set, so one context occurrence cannot refuse a changed one. + const anyTouched = (candidates: IndexedLine[]) => candidates.some((c) => c.sourceKind !== 'context'); + + const nearest = (candidates: IndexedLine[]) => { + const preferred = candidates.some((c) => c.sourceKind !== 'context') + ? candidates.filter((c) => c.sourceKind !== 'context') + : candidates; + if (reportedLine === undefined) return preferred[0].anchor; + return preferred.reduce((best, candidate) => + Math.abs((candidate.anchor.newLineNumber ?? 0) - reportedLine) + < Math.abs((best.anchor.newLineNumber ?? 0) - reportedLine) ? candidate : best, - ); + ).anchor; }; const exact = index.byContent.get(firstLine); - if (exact && exact.length > 0) return { status: 'matched', line: nearest(exact) }; + if (exact && exact.length > 0) { + return { status: 'matched', line: nearest(exact), touched: anyTouched(exact) }; + } const contained = index.lines.flatMap(({ normalized, line }) => normalized.length >= MIN_DISCRIMINATING_EVIDENCE_CHARS && (normalized.includes(firstLine) || firstLine.includes(normalized)) ? [line] : []); - if (contained.length > 0) return { status: 'matched', line: nearest(contained) }; + if (contained.length > 0) { + return { status: 'matched', line: nearest(contained), touched: anyTouched(contained) }; + } return { status: 'unmatched' }; } diff --git a/packages/core/src/model-output/index.ts b/packages/core/src/model-output/index.ts index 9d0ad5a4..631ee913 100644 --- a/packages/core/src/model-output/index.ts +++ b/packages/core/src/model-output/index.ts @@ -6,7 +6,7 @@ import { type ClaimType, type ParsedReviewComment, reviewSeverities, -} from '@codra/schema'; +} from '@codraoss/schema'; import { renderDiffSnippet } from '../prompts/verify'; import { logger } from '../logger'; import { z } from 'zod'; @@ -87,7 +87,7 @@ function formatWithheld(w: Withheld): string { function groundFindingInEvidence( finding: RawFinding, evidenceIndex: EvidenceIndex, - evidenceStats: { total: number; matched: number; unmatched: number; weak: number; absent: number }, + evidenceStats: { total: number; matched: number; unmatched: number; weak: number; absent: number; contextOnly: number }, ambiguity?: BinAmbiguity, ): { diffLine: DiffLine } | { withheld: Withheld } { const reportedLine = finding.code_location.line || finding.code_location.line_range?.start; @@ -103,6 +103,15 @@ function groundFindingInEvidence( return { withheld: { title: finding.title, body: finding.body, tag: `unverified:${evidence.status}` } }; } + // The evidence exists, but only on lines this pull request did not touch. That is a review of the + // repository, not of the change -- and it is the enforcement half of whole-file context: the prompt + // says the context block is not evidence, and this is what makes that true. Deletions count as + // touched: a finding about removed code is a finding about the change. + if (!evidence.touched) { + evidenceStats.contextOnly += 1; + return { withheld: { title: finding.title, body: finding.body, tag: 'unverified:context-only' } }; + } + if (ambiguity) { const firstLine = foldFirstEvidenceLine(finding.evidence); const claimedPath = finding.code_location.absolute_file_path?.trim(); @@ -219,22 +228,8 @@ function buildParsedComment(params: { claimType: ClaimType; anchorContent: string; finding: RawFinding; - presenceIndex: ReturnType; - absenceCheckStats: { absenceShaped: number; identifierExtracted: number; refuted: number }; }): ParsedReviewComment { - const { file, line, position, severity, title, body, claimType, anchorContent, finding, presenceIndex, absenceCheckStats } = params; - - const absence = checkAbsenceClaim({ title, body, anchorLine: line, index: presenceIndex }); - if (absence.status === 'refuted') { - absenceCheckStats.absenceShaped += 1; - absenceCheckStats.identifierExtracted += 1; - absenceCheckStats.refuted += 1; - } else if (absence.reason !== 'not_absence_shaped') { - absenceCheckStats.absenceShaped += 1; - if (absence.reason !== 'no_identifier' && absence.reason !== 'ambiguous_identifier') { - absenceCheckStats.identifierExtracted += 1; - } - } + const { file, line, position, severity, title, body, claimType, anchorContent, finding } = params; const confidenceScore = typeof finding.confidence_score === 'number' ? finding.confidence_score @@ -272,6 +267,10 @@ export type FileReviewPayload = z.infer; export type GroundingOptions = { deniedClaimTypes?: readonly ClaimType[]; ambiguity?: BinAmbiguity; + // The file's validated post-change content, when `full_file_context` fetched one. Only widens the + // absence check: an identifier the diff never showed is still present in the file, and a claim that + // it is missing is refutable by looking. Evidence stays diff-anchored regardless. + fileContent?: string | null; }; export type GroundedFileReview = { @@ -280,7 +279,7 @@ export type GroundedFileReview = { fileSummary: string; overallCorrectness?: string; confidenceScore?: number; - evidenceStats: { total: number; matched: number; unmatched: number; weak: number; absent: number }; + evidenceStats: { total: number; matched: number; unmatched: number; weak: number; absent: number; contextOnly: number }; claimTypeCounts: Record; deniedClaimCounts: Record; absenceCheckStats: { absenceShaped: number; identifierExtracted: number; refuted: number }; @@ -293,11 +292,11 @@ export function groundParsedFindings( ): GroundedFileReview { const validPositions = getValidPositions(file); const evidenceIndex = buildEvidenceIndex(file); - const evidenceStats = { total: 0, matched: 0, unmatched: 0, weak: 0, absent: 0 }; + const evidenceStats = { total: 0, matched: 0, unmatched: 0, weak: 0, absent: 0, contextOnly: 0 }; const claimTypeCounts: Record = {}; const deniedClaimCounts: Record = {}; const deniedClaimTypes = new Set(options?.deniedClaimTypes ?? []); - const presenceIndex = buildPresenceIndex(file); + const presenceIndex = buildPresenceIndex(file, options?.fileContent); const absenceCheckStats = { absenceShaped: 0, identifierExtracted: 0, refuted: 0 }; const orphanedComments: string[] = []; @@ -327,6 +326,39 @@ export function groundParsedFindings( return null; } + // "X is missing / was removed / is never awaited", answered by looking. This verdict was computed + // and then thrown away for a long time -- the finding was posted regardless -- so the machinery + // was there and simply had no teeth. + const absence = checkAbsenceClaim({ title, body, anchorLine: anchored.line, index: presenceIndex }); + if (absence.status === 'refuted') { + absenceCheckStats.absenceShaped += 1; + absenceCheckStats.identifierExtracted += 1; + absenceCheckStats.refuted += 1; + + // Not at P0. The matcher is a literal search over stripped source: it cannot tell a call from a + // definition, or a live path from a dead one. Silencing a wrong nit is cheap; silencing a + // correct P0 is not, and `claim-checks.ts` is written to be sound in exactly this direction. + if (severity !== 'P0') { + logger.info(`Refuted an absence claim in ${file.path}`, { + identifier: absence.identifier, + foundAtLine: absence.line, + title, + }); + orphanedComments.push(formatWithheld({ + title, + body, + tag: `refuted:absence:${absence.identifier}`, + })); + return null; + } + } else if (absence.reason !== 'not_absence_shaped') { + absenceCheckStats.absenceShaped += 1; + if (absence.reason !== 'no_identifier' && absence.reason !== 'ambiguous_identifier') { + absenceCheckStats.identifierExtracted += 1; + } + } + + try { return buildParsedComment({ file, @@ -338,8 +370,6 @@ export function groundParsedFindings( claimType: gated.claimType, anchorContent, finding, - presenceIndex, - absenceCheckStats, }); } catch (error) { if (!(error instanceof z.ZodError)) throw error; diff --git a/packages/core/src/model-output/json-batch.ts b/packages/core/src/model-output/json-batch.ts index a02b47c5..013b448d 100644 --- a/packages/core/src/model-output/json-batch.ts +++ b/packages/core/src/model-output/json-batch.ts @@ -1,4 +1,4 @@ -import { batchReviewModelOutputSchema, fileReviewModelOutputSchema } from '@codra/schema'; +import { batchReviewModelOutputSchema, fileReviewModelOutputSchema } from '@codraoss/schema'; import { jsonrepair } from 'jsonrepair'; import { z } from 'zod'; import { logger } from '../logger'; diff --git a/packages/core/src/model-output/json.ts b/packages/core/src/model-output/json.ts index b53dbd31..bec5d0e7 100644 --- a/packages/core/src/model-output/json.ts +++ b/packages/core/src/model-output/json.ts @@ -1,4 +1,4 @@ -import { fileReviewModelOutputSchema } from '@codra/schema'; +import { fileReviewModelOutputSchema } from '@codraoss/schema'; import { jsonrepair } from 'jsonrepair'; import { z } from 'zod'; import { logger } from '../logger'; diff --git a/packages/core/src/ports/file-reviews.ts b/packages/core/src/ports/file-reviews.ts index 742a1e88..aaa752ae 100644 --- a/packages/core/src/ports/file-reviews.ts +++ b/packages/core/src/ports/file-reviews.ts @@ -1,4 +1,4 @@ -import type { ParsedReviewComment } from '@codra/schema'; +import type { ParsedReviewComment } from '@codraoss/schema'; export type FileReviewRow = { @@ -23,7 +23,7 @@ export type FileReviewRow = { transient_error_count: number; async_request_id: string | null; async_model: string | null; - withheld_counts: { evidence?: number; claimDenied?: number }; + withheld_counts: { evidence?: number; claimDenied?: number; contextOnly?: number; absenceRefuted?: number }; batch_size: number | null; }; @@ -50,7 +50,10 @@ export type BulkFileReviewInput = { overallCorrectness?: string | null; confidenceScore?: number | null; errorMessage: string | null; - withheldCounts?: { evidence: number; claimDenied: number } | null; + withheldCounts?: { evidence: number; claimDenied: number; contextOnly?: number; absenceRefuted?: number } | null; + // The call answered, but not cleanly: it ran without a response grammar, or its output was cut off + // and salvaged. Persisted rather than logged so "how often did this happen" is a query. + degraded?: string | null; batchSize: number; }; @@ -72,7 +75,8 @@ export interface FileReviewStore { overallCorrectness?: string | null; confidenceScore?: number | null; errorMessage: string | null; - withheldCounts?: { evidence: number; claimDenied: number } | null; + withheldCounts?: { evidence: number; claimDenied: number; contextOnly?: number; absenceRefuted?: number } | null; + degraded?: string | null; asyncRequestId?: string | null; asyncModel?: string | null; }): Promise; diff --git a/packages/core/src/ports/formatter.ts b/packages/core/src/ports/formatter.ts index e8fe4eb1..222bbe3d 100644 --- a/packages/core/src/ports/formatter.ts +++ b/packages/core/src/ports/formatter.ts @@ -1,8 +1,19 @@ -import type { ParsedReviewComment } from '@codra/schema'; +import type { ParsedReviewComment } from '@codraoss/schema'; export interface ReviewFormatter { toReviewEvent(verdict: 'approve' | 'comment'): 'APPROVE' | 'COMMENT'; summarizeVerdict(comments: ParsedReviewComment[], hasFailures: boolean): { verdict: 'approve' | 'comment'; errors: number; warnings: number }; formatInlineComment(comment: ParsedReviewComment): string; - formatReviewOverview(commitSha: string, botUsername: string): string; + formatReviewOverview(input: ReviewOverviewInput): string; } + +export type ReviewOverviewInput = { + commitSha: string; + /** Comments actually posted. Zero means the header must not promise suggestions. */ + postedFindings: number; + filesReviewed: number; + linesReviewed: number; + /** Candidates the gates dropped; on a clean review this is what "nothing to report" cost. */ + withheldFindings: number; + filesFailed: number; +}; diff --git a/packages/core/src/ports/git-provider.ts b/packages/core/src/ports/git-provider.ts index 457df8bd..223b49ae 100644 --- a/packages/core/src/ports/git-provider.ts +++ b/packages/core/src/ports/git-provider.ts @@ -23,6 +23,8 @@ export interface ReviewGitProvider { getPullRequest(owner: string, repo: string, prNumber: number): Promise; getPullRequestDiff(owner: string, repo: string, prNumber: number): Promise; getCompareDiff(owner: string, repo: string, base: string, head: string): Promise; + /** File content at `ref`, or null if unavailable. Optional: backs opt-in file-context enrichment. */ + getRepoFile?(owner: string, repo: string, path: string, ref?: string): Promise; createCheckRun(owner: string, repo: string, params: { headSha: string; title: string; summary: string }): Promise<{ id: number }>; updateCheckRun(owner: string, repo: string, checkRunId: number, params: { title: string; @@ -39,6 +41,8 @@ export interface ReviewGitProvider { findBotReviewForCommit(owner: string, repo: string, prNumber: number, commitSha: string, botLogin: string): Promise<{ id: number } | null>; ensureLabel(owner: string, repo: string, name: string, color: string): Promise; addIssueLabels(owner: string, repo: string, prNumber: number, labels: string[]): Promise; + /** Optional: a provider or test double without it simply does not react. */ + addIssueReaction?(owner: string, repo: string, prNumber: number, content: '+1'): Promise; removeIssueLabelsIfPresent(owner: string, repo: string, prNumber: number, labels: string[]): Promise; } diff --git a/packages/core/src/ports/in-memory.ts b/packages/core/src/ports/in-memory.ts index c5b1e413..57533d64 100644 --- a/packages/core/src/ports/in-memory.ts +++ b/packages/core/src/ports/in-memory.ts @@ -2,7 +2,7 @@ import type { KeyValueStore } from './kv'; import type { QueueProducer } from './queue'; import type { JobOrchestrator } from './orchestrator'; import type { SessionStore, DashboardSessionUser } from './session-store'; -import type { ReviewJobMessage } from '@codra/schema'; +import type { ReviewJobMessage } from '@codraoss/schema'; export class InMemoryKV implements KeyValueStore { private store = new Map(); diff --git a/packages/core/src/ports/index.ts b/packages/core/src/ports/index.ts index 1002c116..3aab552e 100644 --- a/packages/core/src/ports/index.ts +++ b/packages/core/src/ports/index.ts @@ -5,7 +5,7 @@ export type { BulkFileReviewInput, FileReviewRow, FileReviewStore, SuppressedFin export type { LearningStore, ModelConfigReader, RepoConfigLoader, ReviewSettingsReader, WebhookDeliveryReader } from './settings'; export type { GitProviderFactory, ReviewComment, PullRequestRecord, ReviewGitProvider } from './git-provider'; export type { FileReviewOutcome, ModelErrorClassifier, ModelResponse, ModelResponseSchema, ReviewModel } from './model'; -export type { ReviewFormatter } from './formatter'; +export type { ReviewFormatter, ReviewOverviewInput } from './formatter'; export type { ReviewTelemetryEvent, TelemetrySink } from './telemetry'; export type { ReviewRuntime } from './runtime'; export type { RepoConfigStore } from './repo-config'; diff --git a/packages/core/src/ports/jobs.ts b/packages/core/src/ports/jobs.ts index 8535b52b..f93fb717 100644 --- a/packages/core/src/ports/jobs.ts +++ b/packages/core/src/ports/jobs.ts @@ -1,4 +1,4 @@ -import type { JobSummary, RepoConfig } from '@codra/schema'; +import type { JobSummary, RepoConfig } from '@codraoss/schema'; export type PersistedReviewJob = JobSummary; diff --git a/packages/core/src/ports/model.ts b/packages/core/src/ports/model.ts index 1118e578..200d75e1 100644 --- a/packages/core/src/ports/model.ts +++ b/packages/core/src/ports/model.ts @@ -1,4 +1,4 @@ -import type { RepoConfig } from '@codra/schema'; +import type { RepoConfig } from '@codraoss/schema'; import type { FileDiff } from '../diff'; import type { BatchReviewResult, parseFileReviewResponse } from '../model-output'; import type { RejectedExemplar } from '../prompts/file-review'; @@ -12,7 +12,8 @@ export type ModelResponse = { outputTokens: number; modelUsed: string; provider: string; - degraded?: 'schema-dropped'; + // schema-dropped: model refused the grammar. truncated: parsed prefix of a cut-off answer, may be incomplete. + degraded?: 'schema-dropped' | 'schema-dropped-catchall' | 'truncated'; }; export type ModelResponseSchema = { @@ -30,8 +31,10 @@ export type FileReviewOutcome = ModelResponse & { export interface ReviewModel { reviewFile(params: { file: FileDiff; + fileContext?: string | null; prTitle: string | null; prDescription: string | null; + changelogExcerpt?: string | null; config: RepoConfig; totalLineCount: number; compactPrompt?: boolean; @@ -42,6 +45,7 @@ export interface ReviewModel { files: readonly FileDiff[]; prTitle: string | null; prDescription: string | null; + changelogExcerpt?: string | null; config: RepoConfig; totalLineCount: number; rejectedExemplars?: readonly RejectedExemplar[]; @@ -49,8 +53,10 @@ export interface ReviewModel { submitReviewBatch(params: { file: FileDiff; + fileContext?: string | null; prTitle: string | null; prDescription: string | null; + changelogExcerpt?: string | null; config: RepoConfig; totalLineCount: number; compactPrompt?: boolean; diff --git a/packages/core/src/ports/orchestrator.ts b/packages/core/src/ports/orchestrator.ts index 0e6d3f04..6b62a3e2 100644 --- a/packages/core/src/ports/orchestrator.ts +++ b/packages/core/src/ports/orchestrator.ts @@ -1,4 +1,4 @@ -import type { ReviewJobMessage } from '@codra/schema'; +import type { ReviewJobMessage } from '@codraoss/schema'; export interface JobOrchestrator { startReviewJob(id: string, params: ReviewJobMessage): Promise; diff --git a/packages/core/src/ports/repo-config.ts b/packages/core/src/ports/repo-config.ts index ca3c02cf..8ba3a895 100644 --- a/packages/core/src/ports/repo-config.ts +++ b/packages/core/src/ports/repo-config.ts @@ -1,4 +1,4 @@ -import type { RepoConfig } from '@codra/schema'; +import type { RepoConfig } from '@codraoss/schema'; export interface RepoConfigStore { getRepoConfigRecord(owner: string, repo: string): Promise<{ diff --git a/packages/core/src/ports/settings.ts b/packages/core/src/ports/settings.ts index ca658c62..8cdac300 100644 --- a/packages/core/src/ports/settings.ts +++ b/packages/core/src/ports/settings.ts @@ -1,4 +1,4 @@ -import type { ClaimType, RepoConfig, ReviewSettings } from '@codra/schema'; +import type { ClaimType, RepoConfig, ReviewSettings } from '@codraoss/schema'; export interface ReviewSettingsReader { getReviewSettings(): Promise; diff --git a/packages/core/src/prompts/file-review.ts b/packages/core/src/prompts/file-review.ts index 10d0057e..17211b60 100644 --- a/packages/core/src/prompts/file-review.ts +++ b/packages/core/src/prompts/file-review.ts @@ -1,16 +1,28 @@ -import { claimTypes, type RepoConfig } from '@codra/schema'; +import { claimTypes, type RepoConfig } from '@codraoss/schema'; import type { FileDiff } from '../diff'; import type { ModelResponseSchema } from '../ports/model'; import { getLanguageForFile } from './languages'; +import { + INTENT_CHECK_INSTRUCTION, + renderFileContext, + renderIntentBlock, +} from './review-context'; import { EXEMPLAR_BLOCK_CHARS, - PR_DESCRIPTION_CHARS, } from '../constants'; +export { changelogExcerptFromDiff, wantsFileContext } from './review-context'; + +// Pre-review_breadth fallback: generator was allowed ~2x the posted cap. export function generatorFindingCap(maxComments: number): number { return Math.max(1, maxComments * 2); } +/** Internal candidate cap upstream of posting; falls back for job snapshots queued before this field existed. */ +export function reviewBreadth(config: Pick & { review_breadth?: number }): number { + return config.review_breadth ?? generatorFindingCap(config.max_comments); +} + function findingItemSchema() { return { type: 'object', @@ -48,7 +60,7 @@ function findingItemSchema() { }; } -export function buildReviewResponseSchema(maxComments: number): ModelResponseSchema { +export function buildReviewResponseSchema(findingCap: number): ModelResponseSchema { return { name: 'codra_file_review', schema: { @@ -58,7 +70,7 @@ export function buildReviewResponseSchema(maxComments: number): ModelResponseSch properties: { findings: { type: 'array', - maxItems: generatorFindingCap(maxComments), + maxItems: Math.max(1, findingCap), items: findingItemSchema(), }, overall_explanation: { type: 'string' }, @@ -69,7 +81,7 @@ export function buildReviewResponseSchema(maxComments: number): ModelResponseSch }; } -export function buildBatchReviewResponseSchema(maxComments: number, fileCount: number): ModelResponseSchema { +export function buildBatchReviewResponseSchema(findingCap: number, fileCount: number): ModelResponseSchema { return { name: 'codra_batch_review', schema: { @@ -144,13 +156,17 @@ const MULTI_FILE_SCHEMA_FORMAT = `{ "overall_confidence_score": number (0 to 1) }`; -export function buildFileReviewSystemPromptBase(opts?: { multiFile?: boolean }): string { +export function buildFileReviewSystemPromptBase(opts?: { multiFile?: boolean; fileContext?: boolean }): string { const multi = opts?.multiFile === true; + const singleFileScope = opts?.fileContext === true + ? '- You can see the diff below and, after it, the full content of that one file. You cannot see the rest of the repository. Findings must still be about lines the diff CHANGED; the file content is there to tell you what the surrounding code does, not to be reviewed.' + : '- You can see ONLY the diff below, not the whole file or the rest of the repository.'; + const contextScope = multi ? `- You can see ONLY the diffs below, not the whole files or the rest of the repository. - Each file below is INDEPENDENT. A finding about one file must be grounded in a line from THAT file's diff, and must be reported inside that file's entry. Never carry a claim from one file to another, and never assume two files interact unless both diffs show it.` - : '- You can see ONLY the diff below, not the whole file or the rest of the repository.'; + : singleFileScope; const evidenceSource = multi ? `the single line of code the finding is about, copied VERBATIM from that file's diff below.` @@ -160,7 +176,6 @@ export function buildFileReviewSystemPromptBase(opts?: { multiFile?: boolean }): ? '4. Return at most {{MAX_COMMENTS}} findings PER FILE, most severe first. Keep each body under 160 words.' : '4. Return at most {{MAX_COMMENTS}} findings, most severe first. Keep each body under 160 words.'; - // presupposed clean files in every bin and reintroduced exactly the restraint language the note above const emptyRule = multi ? `5. Return exactly one entry per file listed below, in the same order, and never omit a file. Review each file's diff with the same care you would give it if it were the only file in front of you. An empty findings array is a positive claim that this diff introduces no defect, so return one only when that is true. Do not pad, and do not withhold.` : '5. If the diff genuinely introduces no defect, return an empty findings array and a short explanation. Do not pad, and do not withhold.'; @@ -211,11 +226,11 @@ export const fileReviewSystemPromptBase = buildFileReviewSystemPromptBase(); export function buildFileReviewSystemPrompt( config: RepoConfig['review'], languagePersona?: string, - opts?: { multiFile?: boolean }, + opts?: { multiFile?: boolean; fileContext?: boolean }, ) { const persona = languagePersona ? ` as ${languagePersona}` : ''; const prompt = buildFileReviewSystemPromptBase(opts) - .replace('{{MAX_COMMENTS}}', generatorFindingCap(config.max_comments).toString()); + .replace('{{MAX_COMMENTS}}', reviewBreadth(config).toString()); return `You are a world-class professional senior code reviewer${persona}. ${prompt}`; } @@ -239,15 +254,6 @@ function renderExemplars(exemplars: readonly RejectedExemplar[] | undefined): st const heading = 'Findings a reviewer on THIS repository has already rejected. Do not report things like these:'; return [heading, ...lines].join('\n'); } - - - -function renderPrContext(prDescription: string | null): string | null { - const trimmed = prDescription?.trim(); - if (!trimmed) return null; - return `PR description (author intent - use to judge whether a change is deliberate):\n${trimmed.slice(0, PR_DESCRIPTION_CHARS)}${trimmed.length > PR_DESCRIPTION_CHARS ? '…' : ''}`; -} - function renderCustomRules(config: RepoConfig['review']): string { const rules = config.custom_rules.length > 0 ? config.custom_rules.map((rule) => `- ${rule}`).join('\n') : '- None'; return `Custom rules:\n${rules}`; @@ -263,30 +269,35 @@ function renderLanguageGuidelines(path: string): string { export function buildFileReviewPrompts(input: { file: FileDiff; + fileContext?: string | null; prTitle: string | null; prDescription: string | null; + changelogExcerpt?: string | null; config: RepoConfig['review']; rejectedExemplars?: readonly RejectedExemplar[]; }) { const languageInfo = getLanguageForFile(input.file.path); const rules = input.config.custom_rules.length > 0 ? input.config.custom_rules.map((rule) => `- ${rule}`).join('\n') : '- None'; - const systemPrompt = buildFileReviewSystemPrompt(input.config, languageInfo?.persona); - const languageGuidelines = renderLanguageGuidelines(input.file.path); + const intentBlock = renderIntentBlock(input); + const fileContext = input.fileContext ? renderFileContext(input.file, input.fileContext) : null; - const prContext = renderPrContext(input.prDescription); + const systemPrompt = buildFileReviewSystemPrompt(input.config, languageInfo?.persona, { + fileContext: fileContext !== null, + }); + const languageGuidelines = renderLanguageGuidelines(input.file.path); const exemplars = renderExemplars(input.rejectedExemplars); const userPrompt = [ - `PR title: ${input.prTitle ?? 'Untitled PR'}`, - ...(prContext ? [prContext] : []), + intentBlock, ...(exemplars ? [exemplars] : []), `File path: ${input.file.path}`, languageGuidelines, `Custom rules:\n${rules}`, 'Review ONLY the diff shown below. You cannot see the rest of the file or repository - do not report something as undefined, unimported, unused, or missing just because it is not in the diff. If the diff note says it was truncated, do not infer issues from omitted lines.', 'Line numbers: every diff line below is prefixed with two columns - the OLD file line number, then the NEW file line number. Always report `line` (and `line_range`) using the NEW (second, right-hand) number, and only ever cite a line that appears in the diff. For a removed line, cite the nearest NEW line number shown next to it.', - 'Evidence: every finding must carry an `evidence` string containing the exact code of the line it is about, copied character-for-character from the diff below. Strip the two leading line-number columns and the +/-/space marker - quote only the code itself. A finding whose evidence does not appear in the diff will be discarded.', + `Evidence: every finding must carry an \`evidence\` string containing the exact code of the line it is about, copied character-for-character from the UNIFIED DIFF below. Strip the two leading line-number columns and the +/-/space marker - quote only the code itself. A finding whose evidence does not appear in the diff will be discarded${fileContext ? ', and the full-file context below is NOT the diff -- a line quoted from it counts as no evidence at all' : ''}.`, + INTENT_CHECK_INSTRUCTION, 'Prioritize correctness, security, and production-impacting bugs. Raise anything you can ground in a quoted line; avoid subjective style feedback.', '', `## Output JSON Schema (STRICTLY REQUIRED)`, @@ -313,6 +324,7 @@ export function buildFileReviewPrompts(input: { '', 'Unified diff:', renderFileDiff(input.file), + ...(fileContext ? ['', fileContext] : []), ].join('\n'); return { systemPrompt, userPrompt }; @@ -326,6 +338,7 @@ export function buildBatchReviewPrompts(input: { files: readonly FileDiff[]; prTitle: string | null; prDescription: string | null; + changelogExcerpt?: string | null; config: RepoConfig['review']; rejectedExemplars?: readonly RejectedExemplar[]; }) { @@ -336,7 +349,7 @@ export function buildBatchReviewPrompts(input: { const systemPrompt = buildFileReviewSystemPrompt(input.config, uniformLanguage?.persona, { multiFile: true }); - const prContext = renderPrContext(input.prDescription); + const intentBlock = renderIntentBlock(input); const exemplars = renderExemplars(input.rejectedExemplars); const pathList = files.map((file) => `- ${file.path}`).join('\n'); @@ -349,8 +362,7 @@ export function buildBatchReviewPrompts(input: { ]); const userPrompt = [ - `PR title: ${input.prTitle ?? 'Untitled PR'}`, - ...(prContext ? [prContext] : []), + intentBlock, ...(exemplars ? [exemplars] : []), `You are reviewing ${files.length} files in ONE response. Return exactly ${files.length} entries in "files", one per path, in this order:\n${pathList}`, ...(uniformLanguage ? [renderLanguageGuidelines(files[0].path)] : []), @@ -359,6 +371,7 @@ export function buildBatchReviewPrompts(input: { 'File scoping: each finding belongs to exactly ONE file. Put it inside that file\'s entry, set that file\'s path in `absolute_file_path`, and quote evidence from that file\'s diff only. Never report a finding about one file inside another file\'s entry, and never quote a line from a different file.', 'Line numbers: every diff line below is prefixed with two columns - the OLD file line number, then the NEW file line number. Always report `line` (and `line_range`) using the NEW (second, right-hand) number, and only ever cite a line that appears in that file\'s diff. For a removed line, cite the nearest NEW line number shown next to it.', 'Evidence: every finding must carry an `evidence` string containing the exact code of the line it is about, copied character-for-character from its own file\'s diff below. Strip the two leading line-number columns and the +/-/space marker - quote only the code itself. A finding whose evidence does not appear in that file\'s diff will be discarded.', + INTENT_CHECK_INSTRUCTION, 'Prioritize correctness, security, and production-impacting bugs. Raise anything you can ground in a quoted line; avoid subjective style feedback.', '', `## Output JSON Schema (STRICTLY REQUIRED)`, diff --git a/packages/core/src/prompts/languages.ts b/packages/core/src/prompts/languages.ts index bb6e15d6..d4ae8ddb 100644 --- a/packages/core/src/prompts/languages.ts +++ b/packages/core/src/prompts/languages.ts @@ -86,3 +86,11 @@ export function getLanguageForFile(path: string): LanguageGuideline | undefined return matches[0]; } + +// Matched by filename convention, not extension; kept narrow to avoid false positives. +export function isChangelogPath(path: string): boolean { + const name = path.split('/').pop()?.toLowerCase() ?? ''; + const stem = name.replace(/\.(md|mdx|markdown|rst|txt)$/, ''); + return stem === 'changelog' || stem === 'changes' || stem === 'history' || stem === 'news' + || stem === 'release-notes' || stem === 'release_notes' || stem === 'releasenotes'; +} diff --git a/packages/core/src/prompts/review-context.ts b/packages/core/src/prompts/review-context.ts new file mode 100644 index 00000000..fda6bd82 --- /dev/null +++ b/packages/core/src/prompts/review-context.ts @@ -0,0 +1,114 @@ +import type { FileDiff } from '../diff'; +import { isChangelogPath } from './languages'; +import { + CHANGELOG_EXCERPT_CHARS, + FILE_CONTEXT_CHAR_BUDGET, + FILE_CONTEXT_WINDOW_LINES, + FRAGMENTED_HUNK_THRESHOLD, + FRAGMENTED_MIN_LINES, + PACKABLE_MAX_DIFF_LINES, + PR_DESCRIPTION_CHARS, +} from '../constants'; + +// Prompt blocks that surround a diff: what the change is for, and what the rest of the file looks like. + + + + +function clip(text: string, limit: number): string { + return text.length > limit ? `${text.slice(0, limit)}…` : text; +} + +// Must avoid the `===== FILE ` delimiter and lines starting `Language: `: the batch prompt splits on those. +export function renderIntentBlock(input: { + prTitle: string | null; + prDescription: string | null; + changelogExcerpt?: string | null; +}): string { + const description = input.prDescription?.trim(); + const changelog = input.changelogExcerpt?.trim(); + + return [ + '## PR INTENT (what the author set out to do)', + `Title: ${input.prTitle ?? 'Untitled PR'}`, + ...(description ? ['Description:', clip(description, PR_DESCRIPTION_CHARS)] : []), + ...(changelog ? ['Changelog lines added by this PR:', clip(changelog, CHANGELOG_EXCERPT_CHARS)] : []), + 'Behaviour that serves this stated intent is deliberate. Do not report it as an accident, an oversight, or a regression.', + ].join('\n'); +} + +export function changelogExcerptFromDiff(files: readonly FileDiff[]): string | null { + const added: string[] = []; + let used = 0; + + for (const file of files) { + if (!isChangelogPath(file.path)) continue; + for (const hunk of file.hunks) { + for (const line of hunk.lines) { + if (line.kind !== 'add') continue; + const text = line.content.trim(); + if (!text) continue; + if (used + text.length > CHANGELOG_EXCERPT_CHARS) { + return added.length > 0 ? added.join('\n') : null; + } + added.push(text); + used += text.length + 1; + } + } + } + + return added.length > 0 ? added.join('\n') : null; +} + +export function wantsFileContext( + file: Pick & { hunks?: FileDiff['hunks'] }, + fullFileContext: boolean, + gate: { compactPrompt?: boolean } = {}, +): boolean { + if (!fullFileContext) return false; + if (gate.compactPrompt) return false; + if (file.isNew || file.isDeleted || file.isBinary) return false; + if (file.lineCount > PACKABLE_MAX_DIFF_LINES) return true; + + // Must stay in step with `planReviewUnits`: a promoted file given no context wastes a model call. + return (file.hunks?.length ?? 0) >= FRAGMENTED_HUNK_THRESHOLD && file.lineCount >= FRAGMENTED_MIN_LINES; +} + +// Windowed per chunk's own hunks so a chunked file doesn't repeat the whole block MAX_CHUNKS times. +export function renderFileContext(file: FileDiff, content: string): string | null { + const lines = content.split('\n'); + + let lowest = Number.POSITIVE_INFINITY; + let highest = 0; + for (const hunk of file.hunks) { + for (const line of hunk.lines) { + if (typeof line.newLineNumber !== 'number') continue; + lowest = Math.min(lowest, line.newLineNumber); + highest = Math.max(highest, line.newLineNumber); + } + } + if (!Number.isFinite(lowest)) return null; + + const start = Math.max(1, lowest - FILE_CONTEXT_WINDOW_LINES); + const end = Math.min(lines.length, highest + FILE_CONTEXT_WINDOW_LINES); + + const numbered: string[] = []; + let used = 0; + for (let n = start; n <= end; n++) { + const rendered = `${n}\t${lines[n - 1] ?? ''}`; + if (used + rendered.length > FILE_CONTEXT_CHAR_BUDGET) break; + numbered.push(rendered); + used += rendered.length + 1; + } + if (numbered.length === 0) return null; + + const last = start + numbered.length - 1; + const partial = start > 1 || last < lines.length; + return [ + `Full file after the change, lines ${start}-${last}${partial ? ` of ${lines.length}` : ''} (CONTEXT ONLY, not reviewable):`, + ...numbered, + ].join('\n'); +} + +export const INTENT_CHECK_INSTRUCTION = + 'Intent check: every finding must survive a comparison with the PR INTENT above. If what you are about to flag IS the stated intent, it is not a finding - drop it. Otherwise open the `body` with one line saying how the problem differs from what the author set out to do.'; diff --git a/packages/core/src/review/bin-runner.ts b/packages/core/src/review/bin-runner.ts index 098530cb..4b6530b4 100644 --- a/packages/core/src/review/bin-runner.ts +++ b/packages/core/src/review/bin-runner.ts @@ -1,5 +1,5 @@ import { logger } from '../logger'; -import type { RepoConfig } from '@codra/schema'; +import type { RepoConfig } from '@codraoss/schema'; import type { FileDiff } from '../diff'; import { renderFileDiff, type RejectedExemplar } from '../prompts/file-review'; import type { BulkFileReviewInput, PullRequestRecord, ReviewModel, ReviewRuntime } from '../ports'; @@ -36,6 +36,7 @@ export async function reviewAndPersistBin( model: ReviewModel, resolveFailureModelProvider: () => Promise, rejectedExemplars: readonly RejectedExemplar[] = [], + changelogExcerpt: string | null = null, ): Promise { const startedAt = env.clock.now(); @@ -66,6 +67,7 @@ export async function reviewAndPersistBin( files, prTitle: pr.title ?? null, prDescription: pr.body ?? null, + changelogExcerpt, config, totalLineCount, rejectedExemplars, @@ -101,7 +103,11 @@ export async function reviewAndPersistBin( + (parsed.evidenceStats?.absent ?? 0) + (parsed.evidenceStats?.weak ?? 0), claimDenied: Object.values(parsed.deniedClaimCounts ?? {}).reduce((sum, n) => sum + n, 0), + contextOnly: parsed.evidenceStats?.contextOnly ?? 0, + absenceRefuted: parsed.absenceCheckStats?.refuted ?? 0, }, + // The whole bin shared one call, so every member inherits its degradation. + degraded: response.degraded ?? null, batchSize: files.length, }; }); diff --git a/packages/core/src/review/budget.ts b/packages/core/src/review/budget.ts index f9249a05..f8e01364 100644 --- a/packages/core/src/review/budget.ts +++ b/packages/core/src/review/budget.ts @@ -5,12 +5,27 @@ export function budgetAwareFileLimit( remainingSafeBudget: number, configuredChunkFileLimit: number, modelChainLength = 1, + fetchesFileContent = false, + runsSecondaryReviewer = false, ) { - const budgetLimit = Math.floor(remainingSafeBudget / estimatedSubrequestsPerFile(modelChainLength)); - return Math.min(configuredChunkFileLimit, budgetLimit); + const budgetLimit = Math.floor( + remainingSafeBudget / estimatedSubrequestsPerFile(modelChainLength, fetchesFileContent, runsSecondaryReviewer), + ); + // Never zero while any budget remains: a file limit of 0 defers every file forever, and a job that + // can afford one file at a time should make progress one file at a time. + return Math.max(remainingSafeBudget > 0 ? 1 : 0, Math.min(configuredChunkFileLimit, budgetLimit)); } -export function estimatedSubrequestsPerFile(modelChainLength: number) { +export function estimatedSubrequestsPerFile( + modelChainLength: number, + fetchesFileContent = false, + runsSecondaryReviewer = false, +) { const modelAttempts = Math.max(1, Math.min(modelChainLength, MAX_MODEL_ATTEMPTS_ESTIMATE)); - return FILE_FIXED_SUBREQUESTS + modelAttempts; + // A second reviewer walks its own chain, so it doubles the model half of the estimate but not the + // fixed per-file cost. Roughly halving files per invocation is the correct answer, not a problem: + // the continuation loop already carries the rest of the job into the next invocation. + return FILE_FIXED_SUBREQUESTS + + modelAttempts * (runsSecondaryReviewer ? 2 : 1) + + (fetchesFileContent ? 1 : 0); } diff --git a/packages/core/src/review/diff-cache.ts b/packages/core/src/review/diff-cache.ts index 98fd5021..9c996d25 100644 --- a/packages/core/src/review/diff-cache.ts +++ b/packages/core/src/review/diff-cache.ts @@ -1,4 +1,4 @@ -import { reviewMaxFilesRange, type RepoConfig } from '@codra/schema'; +import { reviewMaxFilesRange, type RepoConfig } from '@codraoss/schema'; import { filterReviewableFiles, parseUnifiedDiff, type FileDiff } from '../diff'; import type { ReviewGitProvider, ReviewRuntime } from '../ports'; import { logger } from '../logger'; diff --git a/packages/core/src/review/file-context.ts b/packages/core/src/review/file-context.ts new file mode 100644 index 00000000..b189eb00 --- /dev/null +++ b/packages/core/src/review/file-context.ts @@ -0,0 +1,55 @@ +import type { FileDiff } from '../diff'; +import type { ReviewGitProvider } from '../ports'; +import { logger } from '../logger'; + +const VALIDATION_SAMPLES = 5; + +export function contentMatchesDiff(file: FileDiff, content: string): boolean { + const lines = content.split('\n'); + const samples: Array<{ newLineNumber: number; content: string }> = []; + + for (const hunk of file.hunks) { + for (const line of hunk.lines) { + if (line.kind === 'del' || typeof line.newLineNumber !== 'number') continue; + if (!line.content.trim()) continue; + samples.push({ newLineNumber: line.newLineNumber, content: line.content }); + } + } + if (samples.length === 0) return false; + + const step = Math.max(1, Math.floor(samples.length / VALIDATION_SAMPLES)); + for (let i = 0; i < samples.length; i += step) { + const sample = samples[i]; + if (lines[sample.newLineNumber - 1] !== sample.content) return false; + } + return true; +} + +export async function loadFileContext( + github: Pick, + job: { owner: string; repo: string; commitSha: string }, + file: FileDiff, + onFetch?: () => void, +): Promise { + if (!github.getRepoFile) return null; + + let content: string | null; + try { + onFetch?.(); + content = await github.getRepoFile(job.owner, job.repo, file.path, job.commitSha); + } catch (error) { + logger.warn(`Could not fetch file content for ${file.path}; reviewing the diff alone`, { + error: error instanceof Error ? error.message : String(error), + }); + return null; + } + + if (!content) return null; + if (!contentMatchesDiff(file, content)) { + logger.warn(`Fetched content for ${file.path} does not line up with the diff; reviewing the diff alone`, { + commitSha: job.commitSha, + }); + return null; + } + return content; +} diff --git a/packages/core/src/review/file-runner.ts b/packages/core/src/review/file-runner.ts index caa6a04a..0753610f 100644 --- a/packages/core/src/review/file-runner.ts +++ b/packages/core/src/review/file-runner.ts @@ -1,5 +1,5 @@ import { logger } from '../logger'; -import { type ParsedReviewComment, type RepoConfig } from '@codra/schema'; +import { type ParsedReviewComment, type RepoConfig } from '@codraoss/schema'; import { parseUnifiedDiff, type FileDiff } from '../diff'; import { ruleHitsToComments, scanFileForRuleHits, type RuleScanStats } from '../rules/detect'; import type { RejectedExemplar } from '../prompts/file-review'; @@ -105,6 +105,44 @@ export function scanRuleChannel( } } +/** + * Marks who found what, for display only. + * + * Explicitly NOT for scoring. In the measured corpus, claims found by seven configurations were right + * 7% of the time against 20% for claims found by one -- so the fact that both reviewers found + * something is not a reason to trust it more, and this field must never become a weight. + */ +function tagReviewer(comments: ParsedReviewComment[], reviewerModel: string): ParsedReviewComment[] { + return comments.map((comment) => ({ ...comment, reviewerModel })); +} + +/** Never throws: the primary review already succeeded, and a second opinion is not worth losing it. */ +async function runSecondaryReview( + model: ReviewModel, + params: Parameters[0], + secondary: { model: string; fallbacks: string[] }, + path: string, +) { + try { + // `selectModel` reads `config.model`, so swapping it is the whole mechanism -- no second runner, + // no second chain type. `size_overrides` are deliberately not carried: the secondary is one + // deliberate choice, not a size ladder. + return await model.reviewFile({ + ...params, + config: { + ...params.config, + model: { ...params.config.model, main: secondary.model, fallbacks: secondary.fallbacks }, + }, + }); + } catch (error) { + logger.warn(`Secondary reviewer failed for ${path}; keeping the primary review`, { + model: secondary.model, + error: error instanceof Error ? error.message : String(error), + }); + return null; + } +} + export async function reviewAndPersistFile( env: ReviewRuntime, job: PersistedReviewJob, @@ -116,6 +154,8 @@ export async function reviewAndPersistFile( resolveFailureModelProvider: () => Promise, previousReview?: { transient_error_count: number }, rejectedExemplars: readonly RejectedExemplar[] = [], + changelogExcerpt: string | null = null, + fileContext: string | null = null, ) { const startedAt = env.clock.now(); const compactPrompt = (previousReview?.transient_error_count ?? 0) > 0; @@ -123,15 +163,35 @@ export async function reviewAndPersistFile( const ruleScan = scanRuleChannel(file, config); try { - const response = await model.reviewFile({ + const reviewParams = { file, + fileContext, prTitle: pr.title ?? null, prDescription: pr.body ?? null, + changelogExcerpt, config, totalLineCount, compactPrompt, rejectedExemplars, - }); + }; + + const response = await model.reviewFile(reviewParams); + + // A second, independent reviewer over the same file. Its findings are UNIONED with the primary's: + // the measured gain from two reviewers is entirely coverage, and nothing here counts agreement. + // + // Best-effort by construction. The primary's result already exists, so a failing secondary must + // never cost the file -- it logs and the review stands on the primary alone. Skipped when + // `compactPrompt` is set, because that flag means the last attempt was already too much. + const secondary = config.model?.secondary ?? null; + const secondaryReview = secondary && !compactPrompt + ? await runSecondaryReview(model, reviewParams, secondary, file.path) + : null; + + const llmComments = [ + ...tagReviewer(response.parsed.comments, response.modelUsed), + ...(secondaryReview ? tagReviewer(secondaryReview.parsed.comments, secondaryReview.modelUsed) : []), + ]; await env.fileReviews.upsertFileReview(job.id, { filePath: file.path, @@ -141,9 +201,11 @@ export async function reviewAndPersistFile( diffLineCount: file.lineCount, diffInput: null, rawAiOutput: response.rawText, - parsedComments: [...response.parsed.comments, ...ruleScan.comments], - inputTokens: response.inputTokens, - outputTokens: response.outputTokens, + // One row per file, always: `file_reviews` is unique on (job_id, file_path), and review + // inheritance, resume and finalize all assume that. The two reviewers merge into it. + parsedComments: [...llmComments, ...ruleScan.comments], + inputTokens: response.inputTokens + (secondaryReview?.inputTokens ?? 0), + outputTokens: response.outputTokens + (secondaryReview?.outputTokens ?? 0), durationMs: env.clock.now() - startedAt, verdict: response.parsed.verdict, fileSummary: response.parsed.fileSummary, @@ -155,7 +217,16 @@ export async function reviewAndPersistFile( + (response.parsed.evidenceStats?.absent ?? 0) + (response.parsed.evidenceStats?.weak ?? 0), claimDenied: Object.values(response.parsed.deniedClaimCounts ?? {}).reduce((sum, n) => sum + n, 0), + // Findings about code the diff never touched. Counted apart from the evidence gate: those are + // findings whose quote could not be found at all, these are ones that were found in the wrong + // place, and only the second number says anything about how the reviewer is misreading a PR. + contextOnly: response.parsed.evidenceStats?.contextOnly ?? 0, + // "X is missing", answered by finding X. Counted so the gate's real hit rate is visible. + absenceRefuted: response.parsed.absenceCheckStats?.refuted ?? 0, }, + // Only logged until now, which made "how often did a review run unconstrained or truncated?" + // unanswerable without reading the logs of every job one at a time. + degraded: response.degraded ?? null, }); logger.info(`File review parsed: ${file.path}`, { diff --git a/packages/core/src/review/finalize.ts b/packages/core/src/review/finalize.ts index a742cfec..cd2712d1 100644 --- a/packages/core/src/review/finalize.ts +++ b/packages/core/src/review/finalize.ts @@ -1,5 +1,5 @@ import { logger } from '../logger'; -import { defaultRepoConfig, type ParsedReviewComment, type RepoConfig } from '@codra/schema'; +import { defaultRepoConfig, type ParsedReviewComment, type RepoConfig } from '@codraoss/schema'; import { shadowEvaluate } from '../finding-gates'; import { getDiffFiles } from './diff-cache'; import type { ReviewFormatter, ReviewGitProvider, ReviewModel, ReviewRuntime } from '../ports'; @@ -83,10 +83,14 @@ export async function runFinalizePhase( const hasFailures = fileSummaries.some((file) => file.verdict === 'failed'); const failedFileCount = fileSummaries.filter((file) => file.verdict === 'failed').length; + + await env.jobs.updateJobStep(job.id, 'Verifying Findings', { status: 'running' }); + const { finalComments, dispositions, verifyReasons, + verificationSkipped, suppressedComments, droppedBySuppression, beforeVerifyList, @@ -104,6 +108,7 @@ export async function runFinalizePhase( logger.info('Finding pipeline outcome', { jobId: job.id, parsed: reviewedComments.length, + verificationSkipped, droppedByFilters, droppedBySuppression, droppedByVerification, @@ -130,6 +135,11 @@ export async function runFinalizePhase( ...shadowEvaluate(beforeVerifyList, finalComments), }); + // Failed on every skip reason: each one means findings were posted unverified. + await env.jobs.updateJobStep(job.id, 'Verifying Findings', verificationSkipped + ? { status: 'failed', error: `Verification did not run (${verificationSkipped}); findings were posted unverified.` } + : { status: 'done' }); + const rawVerdict = formatter.summarizeVerdict([...finalComments, ...suppressedComments], hasFailures); const everythingWithheld = finalComments.length === 0 @@ -141,10 +151,23 @@ export async function runFinalizePhase( await env.jobs.updateJobStep(job.id, 'Generating Summary', { status: 'done' }); await heartbeatAndCheckSuperseded(env, job.id, leaseOwner); - let formattedSummary = formatter.formatReviewOverview(pr.head.sha, env.botUsername); + const formattedSummary = formatter.formatReviewOverview({ + commitSha: pr.head.sha, + postedFindings: finalComments.length, + filesReviewed: files.length, + linesReviewed: files.reduce((sum, file) => sum + file.lineCount, 0), + withheldFindings: withheldByParser + droppedByFilters + droppedByVerification, + filesFailed: failedFileCount, + }); + // Skipped-file counts are dashboard information, not PR content: skips have more than one cause. if (filesOverCap > 0) { - formattedSummary += `\n\n> [!WARNING]\n> **${filesOverCap} file${filesOverCap === 1 ? ' was' : 's were'} not reviewed.** This pull request has ${files.length + filesOverCap} reviewable files and the limit is ${reviewSettings.maxFiles}. Raise it in Settings to cover the whole diff.`; + logger.info('Some reviewable files were skipped by the file or diff-size limits', { + jobId: job.id, + filesOverCap, + reviewed: files.length, + maxFiles: reviewSettings.maxFiles, + }); } const finalizeRetriedPastPost = job.steps.some( @@ -174,6 +197,21 @@ export async function runFinalizePhase( await env.fileReviews.markCommentsPosted(job.id, postedFingerprints); } + // A clean pass also gets a thumbs-up on the pull request's opening post, so the author sees the + // outcome without opening the review. Best-effort: reacting is decoration, and losing it must never + // fail a job that already posted its review. GitHub returns the existing reaction on a repeat, so a + // retried finalize does not duplicate it. + if (finalComments.length === 0 && github.addIssueReaction) { + try { + await github.addIssueReaction(job.owner, job.repo, job.prNumber, '+1'); + } catch (error) { + logger.warn('Could not react to the pull request', { + jobId: job.id, + error: error instanceof Error ? error.message : String(error), + }); + } + } + try { const withReasons = new Map(); for (const fingerprint of new Set([...dispositions.keys(), ...verifyReasons.keys()])) { diff --git a/packages/core/src/review/gate-pipeline.ts b/packages/core/src/review/gate-pipeline.ts index 09fc7058..d6b49a36 100644 --- a/packages/core/src/review/gate-pipeline.ts +++ b/packages/core/src/review/gate-pipeline.ts @@ -1,10 +1,12 @@ import { dedupeFindings } from '../model-output'; import { verifyFindings } from '../finding-gates'; -import type { FindingDisposition, ParsedReviewComment, RepoConfig } from '@codra/schema'; +import type { FindingDisposition, ParsedReviewComment, RepoConfig } from '@codraoss/schema'; import type { FileDiff } from '../diff'; import type { PersistedReviewJob } from './phase-control'; import type { ReviewModel, ReviewRuntime } from '../ports'; import { loadSuppressedFingerprints } from './telemetry'; +import { reviewBreadth } from '../prompts/file-review'; +import { getLanguageForFile } from '../prompts/languages'; export async function applyFindingGates(params: { env: Pick; @@ -14,7 +16,7 @@ export async function applyFindingGates(params: { model: Pick; effectiveMaxComments: number; reviewedComments: ParsedReviewComment[]; - reviews: Array<{ withheld_counts?: { evidence?: number; claimDenied?: number } | null }>; + reviews: Array<{ withheld_counts?: { evidence?: number; claimDenied?: number; contextOnly?: number; absenceRefuted?: number } | null }>; }) { const { env, job, config, files, model, effectiveMaxComments, reviewedComments, reviews } = params; @@ -22,6 +24,24 @@ export async function applyFindingGates(params: { const minRank = severityRanks[config.review.min_severity] ?? 4; const minConfidence = config.review.min_confidence ?? 0; + // Precision varies 5.8x by language in the measured corpus, and until now every gate was global. + // Resolved per finding from its own path, so a mixed-language pull request is judged per file rather + // than by whatever the repo is mostly written in. + const languageGates = config.review.language_gates ?? {}; + const gatesByLanguage = new Map( + Object.entries(languageGates).map(([language, gate]) => [language.toLowerCase(), gate]), + ); + const thresholdsFor = (path: string) => { + if (gatesByLanguage.size === 0) return { minRank, minConfidence }; + const language = getLanguageForFile(path)?.language; + const override = language ? gatesByLanguage.get(language.toLowerCase()) : undefined; + if (!override) return { minRank, minConfidence }; + return { + minRank: override.min_severity ? (severityRanks[override.min_severity] ?? 4) : minRank, + minConfidence: override.min_confidence ?? minConfidence, + }; + }; + const dispositions = new Map(); const verifyReasons = new Map(); const recordDisposition = (comments: ParsedReviewComment[], stage: FindingDisposition) => { @@ -33,11 +53,12 @@ export async function applyFindingGates(params: { }; let finalComments = reviewedComments.filter((c) => { - if ((severityRanks[c.severity] ?? 4) > minRank) { + const thresholds = thresholdsFor(c.path); + if ((severityRanks[c.severity] ?? 4) > thresholds.minRank) { recordDisposition([c], 'severity'); return false; } - if (typeof c.confidenceScore === 'number' && c.confidenceScore < minConfidence) { + if (typeof c.confidenceScore === 'number' && c.confidenceScore < thresholds.minConfidence) { recordDisposition([c], 'confidence'); return false; } @@ -79,7 +100,7 @@ export async function applyFindingGates(params: { }); const beforeVerifyList = finalComments; - const verify = await verifyFindings({ job, config, files, comments: finalComments, model, maxCandidates: effectiveMaxComments }); + const verify = await verifyFindings({ job, config, files, comments: finalComments, model, maxCandidates: reviewBreadth(config.review) }); finalComments = verify.comments; const droppedByVerification = verify.dropped.length; for (const drop of verify.dropped) recordDisposition([drop.comment], drop.disposition); @@ -98,7 +119,13 @@ export async function applyFindingGates(params: { const droppedByFilters = omittedCount - droppedBySuppression - droppedByVerification - droppedByCap; const withheldByParser = reviews.reduce( - (sum, review) => sum + (review.withheld_counts?.evidence ?? 0) + (review.withheld_counts?.claimDenied ?? 0), + (sum, review) => sum + + (review.withheld_counts?.evidence ?? 0) + + (review.withheld_counts?.claimDenied ?? 0) + // Counted here too, or a file whose findings were ALL about untouched code looks like a file + // with nothing to say, and `everythingWithheld` lets the PR be approved silently. + + (review.withheld_counts?.contextOnly ?? 0) + + (review.withheld_counts?.absenceRefuted ?? 0), 0, ); @@ -117,6 +144,9 @@ export async function applyFindingGates(params: { finalComments, dispositions, verifyReasons, + // Non-null means these findings were never checked. The caller records it on the job, so a review + // that skipped verification stops looking identical to one that passed it. + verificationSkipped: verify.skipped, suppressedComments, droppedBySuppression, beforeVerifyList, diff --git a/packages/core/src/review/index.ts b/packages/core/src/review/index.ts index b889061a..3ec08bf6 100644 --- a/packages/core/src/review/index.ts +++ b/packages/core/src/review/index.ts @@ -1,6 +1,6 @@ import { logger } from '../logger'; -import { type WebhookPayload, type ChangeRequestWebhookPayload } from '@codra/schema/webhook'; -import { REVIEW_CONCURRENCY_LIMITS, type ReviewJobMessage } from '@codra/schema'; +import { type WebhookPayload, type ChangeRequestWebhookPayload } from '@codraoss/schema/webhook'; +import { REVIEW_CONCURRENCY_LIMITS, type ReviewJobMessage } from '@codraoss/schema'; import type { ReviewGitProvider, ReviewRuntime } from '../ports'; import { extractReviewRequest } from './request'; diff --git a/packages/core/src/review/pack.ts b/packages/core/src/review/pack.ts index 2f34c9d4..01aad445 100644 --- a/packages/core/src/review/pack.ts +++ b/packages/core/src/review/pack.ts @@ -5,6 +5,8 @@ import { BIN_TARGET_DIFF_LINES, BIN_MAX_FILES, BIN_DIFF_CHAR_BUDGET, + FRAGMENTED_HUNK_THRESHOLD, + FRAGMENTED_MIN_LINES, } from '../constants'; export type ReviewUnit = @@ -19,6 +21,13 @@ export function unitFiles(unit: ReviewUnit): FileDiff[] { const measure = (file: FileDiff) => renderFileDiff(file).length; +/** Changes scattered thinly across a file rather than concentrated in one place. */ +export function isFragmented(file: FileDiff): boolean { + return file.hunks.length >= FRAGMENTED_HUNK_THRESHOLD + && file.lineCount >= FRAGMENTED_MIN_LINES + && !file.isNew; +} + const asBin = (files: FileDiff[]): ReviewUnit => (files.length === 1 ? { kind: 'single', file: files[0] } : { @@ -28,7 +37,10 @@ const asBin = (files: FileDiff[]): ReviewUnit => (files.length === 1 diffChars: files.reduce((sum, f) => sum + measure(f), 0), }); -export function planReviewUnits(files: readonly FileDiff[], opts: { enabled: boolean }): ReviewUnit[] { +export function planReviewUnits( + files: readonly FileDiff[], + opts: { enabled: boolean; fullFileContext?: boolean }, +): ReviewUnit[] { if (!opts.enabled) return files.map((file) => ({ kind: 'single', file })); const units: ReviewUnit[] = []; @@ -45,7 +57,8 @@ export function planReviewUnits(files: readonly FileDiff[], opts: { enabled: boo for (const file of files) { const fileChars = measure(file); - if (file.lineCount > PACKABLE_MAX_DIFF_LINES || fileChars > BIN_DIFF_CHAR_BUDGET) { + const promoteForContext = opts.fullFileContext === true && isFragmented(file); + if (promoteForContext || file.lineCount > PACKABLE_MAX_DIFF_LINES || fileChars > BIN_DIFF_CHAR_BUDGET) { close(); units.push({ kind: 'single', file }); continue; diff --git a/packages/core/src/review/phase.ts b/packages/core/src/review/phase.ts index 05075f19..d69041e3 100644 --- a/packages/core/src/review/phase.ts +++ b/packages/core/src/review/phase.ts @@ -1,9 +1,11 @@ import { logger } from '../logger'; -import { defaultRepoConfig, REVIEW_CONCURRENCY_LIMITS, type ParsedReviewComment, type RepoConfig } from '@codra/schema'; +import { defaultRepoConfig, REVIEW_CONCURRENCY_LIMITS, type ParsedReviewComment, type RepoConfig } from '@codraoss/schema'; import { budgetAwareFileLimit } from './budget'; import { narrowUnit, planReviewUnits } from './pack'; import { reviewAndPersistBin } from './bin-runner'; import { getDiffFiles } from './diff-cache'; +import { changelogExcerptFromDiff, wantsFileContext } from '../prompts/file-review'; +import { loadFileContext } from './file-context'; import type { ReviewGitProvider, ReviewModel, ReviewRuntime } from '../ports'; import { TokenTracker } from '../token-tracker'; import { @@ -58,12 +60,16 @@ export async function runReviewPhase( const { concurrencyLevel, maxFiles } = await env.settings.getReviewSettings(); const { files } = await getDiffFiles(env, job, github, config, maxFiles); const totalLineCount = files.reduce((sum, file) => sum + file.lineCount, 0); + const changelogExcerpt = changelogExcerptFromDiff(files); const configuredChunkFileLimit = REVIEW_CONCURRENCY_LIMITS[concurrencyLevel]; const modelChainLength = 1 + (config.model.fallbacks?.length ?? 0); const reviewChunkFileLimit = budgetAwareFileLimit( tracker.remainingSafeBudget(), configuredChunkFileLimit, modelChainLength, + config.review.full_file_context, + // A second reviewer walks its own chain per file, so fewer files fit in one invocation. + Boolean(config.model?.secondary), ); if (reviewChunkFileLimit <= 0) { throw new Error('Subrequest budget for this invocation was exhausted before starting the next review chunk.'); @@ -121,7 +127,7 @@ export async function runReviewPhase( }]; })); - const units = planReviewUnits(files, { enabled: true }).flatMap((unit) => narrowUnit(unit, ledger)); + const units = planReviewUnits(files, { enabled: true, fullFileContext: config.review.full_file_context }).flatMap((unit) => narrowUnit(unit, ledger)); const plannedBins = units.filter((unit) => unit.kind === 'bin'); let binsDispatched = 0; let filesDispatchedInBins = 0; @@ -133,7 +139,7 @@ export async function runReviewPhase( const binFiles = unit.kind === 'bin' ? unit.files : []; binFiles.forEach((file) => binnedPaths.add(file.path)); reviewTasks.push((async () => { - const terminal = await reviewAndPersistBin(env, job, binFiles, pr, config, totalLineCount, model, resolveFailureModelProvider, rejectedExemplars); + const terminal = await reviewAndPersistBin(env, job, binFiles, pr, config, totalLineCount, model, resolveFailureModelProvider, rejectedExemplars, changelogExcerpt); terminalProgress += terminal; })()); processedThisChunk += 1; @@ -166,6 +172,16 @@ export async function runReviewPhase( } const inherited = parentReviews.get(file.path); + let fileContextPromise: Promise | null = null; + const fileContextFor = () => { + if (!wantsFileContext(file, config.review.full_file_context, { + compactPrompt: (existingReview?.transient_error_count ?? 0) > 0, + })) { + return Promise.resolve(null); + } + fileContextPromise ??= loadFileContext(github, job, file, () => tracker.incrementSubrequests(1)); + return fileContextPromise; + }; const reviewTask = async () => { if (awaitingReview) { const poll = await model.pollReviewBatch({ @@ -182,7 +198,7 @@ export async function runReviewPhase( logger.warn(`Async batch poll failed for ${file.path}; falling back to synchronous review`, { error: poll.error instanceof Error ? poll.error.message : String(poll.error), }); - await reviewAndPersistFile(env, job, file, pr, config, totalLineCount, model, resolveFailureModelProvider, existingReview, rejectedExemplars); + await reviewAndPersistFile(env, job, file, pr, config, totalLineCount, model, resolveFailureModelProvider, existingReview, rejectedExemplars, changelogExcerpt, await fileContextFor()); terminalProgress += 1; return; } @@ -194,8 +210,10 @@ export async function runReviewPhase( if (!inherited) { const submitted = await model.submitReviewBatch({ file, + fileContext: await fileContextFor(), prTitle: pr.title ?? null, prDescription: pr.body ?? null, + changelogExcerpt, config, totalLineCount, compactPrompt: (existingReview?.transient_error_count ?? 0) > 0, @@ -224,14 +242,14 @@ export async function runReviewPhase( awaitingAsync += 1; return; } - await reviewAndPersistFile(env, job, file, pr, config, totalLineCount, model, resolveFailureModelProvider, existingReview, rejectedExemplars); + await reviewAndPersistFile(env, job, file, pr, config, totalLineCount, model, resolveFailureModelProvider, existingReview, rejectedExemplars, changelogExcerpt, await fileContextFor()); terminalProgress += 1; return; } if (!canInheritParentFileReview(config, inherited)) { logger.info(`Ignoring inherited review for ${file.path}; parent model ${inherited.model_used} is not in the current model strategy`); - await reviewAndPersistFile(env, job, file, pr, config, totalLineCount, model, resolveFailureModelProvider, existingReview, rejectedExemplars); + await reviewAndPersistFile(env, job, file, pr, config, totalLineCount, model, resolveFailureModelProvider, existingReview, rejectedExemplars, changelogExcerpt, await fileContextFor()); terminalProgress += 1; } else { await env.fileReviews.upsertFileReview(job.id, { diff --git a/packages/core/src/review/prepare.ts b/packages/core/src/review/prepare.ts index 89fc933f..c357ec48 100644 --- a/packages/core/src/review/prepare.ts +++ b/packages/core/src/review/prepare.ts @@ -1,5 +1,5 @@ import { logger } from '../logger'; -import { defaultRepoConfig, type RepoConfig } from '@codra/schema'; +import { defaultRepoConfig, type RepoConfig } from '@codraoss/schema'; import type { ReviewGitProvider, ReviewRuntime } from '../ports'; import { getDiffFiles } from './diff-cache'; import type { RejectedExemplar } from '../prompts/file-review'; diff --git a/packages/core/src/review/request.ts b/packages/core/src/review/request.ts index a2a3aaca..275aa24b 100644 --- a/packages/core/src/review/request.ts +++ b/packages/core/src/review/request.ts @@ -3,8 +3,8 @@ import type { WebhookPayload, CommentWebhookPayload, ChangeRequestWebhookPayload, -} from '@codra/schema/webhook'; -import type { RepoConfig } from '@codra/schema'; +} from '@codraoss/schema/webhook'; +import type { RepoConfig } from '@codraoss/schema'; function shouldTriggerFromChangeRequest(action: ChangeRequestWebhookPayload['action'], config: RepoConfig['review']) { return (config.on as string[]).includes(action); diff --git a/packages/core/src/review/retry-policy.ts b/packages/core/src/review/retry-policy.ts index e82d9492..866e69a2 100644 --- a/packages/core/src/review/retry-policy.ts +++ b/packages/core/src/review/retry-policy.ts @@ -1,6 +1,6 @@ import { logger } from '../logger'; -import { normalizeModelId, type RepoConfig } from '@codra/schema'; -import { isSubrequestBudgetMessage, isTimeoutMessage, matchesAnyTransientSubstring } from '@codra/schema/transient-errors'; +import { normalizeModelId, type RepoConfig } from '@codraoss/schema'; +import { isSubrequestBudgetMessage, isTimeoutMessage, matchesAnyTransientSubstring } from '@codraoss/schema/transient-errors'; import type { ReviewRuntime } from '../ports'; import { RETRYABLE_MODEL_FAILURE_RETRY_DELAYS_SECONDS } from '../constants'; diff --git a/packages/core/src/rules/detect.ts b/packages/core/src/rules/detect.ts index d9c525b6..25bedf93 100644 --- a/packages/core/src/rules/detect.ts +++ b/packages/core/src/rules/detect.ts @@ -1,8 +1,8 @@ -import type { ClaimType, ParsedReviewComment } from '@codra/schema'; +import type { ClaimType, ParsedReviewComment } from '@codraoss/schema'; import type { DiffLine, FileDiff } from '../diff'; import { commentSyntaxFor, stripCommentsAndStrings } from '../claim-checks'; import { buildAnchorHash, buildFindingFingerprint, buildFindingFingerprintV2, normalizeDiffText } from '../fingerprint'; -import { CLAIM_TYPE_CATEGORY } from '@codra/schema'; +import { CLAIM_TYPE_CATEGORY } from '@codraoss/schema'; import { RULES, type Rule } from './table'; import { MAX_RULE_SCAN_ADDED_LINES } from '../constants'; diff --git a/packages/core/src/rules/table.ts b/packages/core/src/rules/table.ts index 45ba7b37..52c770ab 100644 --- a/packages/core/src/rules/table.ts +++ b/packages/core/src/rules/table.ts @@ -1,4 +1,4 @@ -import type { ClaimType, reviewSeverities } from '@codra/schema'; +import type { ClaimType, reviewSeverities } from '@codraoss/schema'; type ReviewSeverity = typeof reviewSeverities[number]; diff --git a/packages/core/src/verify.ts b/packages/core/src/verify.ts index 4a97e869..1351f673 100644 --- a/packages/core/src/verify.ts +++ b/packages/core/src/verify.ts @@ -1,4 +1,4 @@ -import { hexToBytes } from '@codra/schema/hex'; +import { hexToBytes } from '@codraoss/schema/hex'; const encoder = new TextEncoder(); diff --git a/packages/core/test/in-memory.ts b/packages/core/test/in-memory.ts index 12a20e4b..0902108a 100644 --- a/packages/core/test/in-memory.ts +++ b/packages/core/test/in-memory.ts @@ -1,5 +1,5 @@ -import { defaultRepoConfig, reviewSettingsSchema, type ParsedReviewComment, type RepoConfig, type ReviewSettings } from '@codra/schema'; +import { defaultRepoConfig, reviewSettingsSchema, type ParsedReviewComment, type RepoConfig, type ReviewSettings } from '@codraoss/schema'; import type { BulkFileReviewInput, FileReviewRow, @@ -392,7 +392,8 @@ export function createInMemoryRuntime( warnings: comments.length, }), formatInlineComment: (comment) => `**${comment.title}**\n\n${comment.body}`, - formatReviewOverview: (commitSha, botUsername) => `Reviewed ${commitSha.slice(0, 7)} by ${botUsername}`, + formatReviewOverview: ({ commitSha, postedFindings }) => + `### Codra Review\nReviewed ${commitSha.slice(0, 7)}: ${postedFindings} posted`, }), githubClients: { forInstallation: () => { throw new Error('webhook resolution is not exercised by these tests'); } }, diff --git a/packages/core/test/review-in-memory.spec.ts b/packages/core/test/review-in-memory.spec.ts index aafc6a81..0817b158 100644 --- a/packages/core/test/review-in-memory.spec.ts +++ b/packages/core/test/review-in-memory.spec.ts @@ -51,7 +51,8 @@ describe('runReview end to end on in-memory ports', () => { expect(recorded.postedReviews[0].comments).toEqual([ { path: 'src/retry.ts', body: expect.stringContaining('Hard-coded delay') }, ]); - expect(recorded.postedReviews[0].body).toContain('codra-bot'); + // The posted body is the formatter's overview, carrying the head sha and the count actually posted. + expect(recorded.postedReviews[0].body).toContain(`Reviewed ${'a'.repeat(7)}: 1 posted`); expect(recorded.checkRuns[0].title).toBe('Review queued'); expect(recorded.checkRuns.at(-1)).toMatchObject({ status: 'completed' }); diff --git a/packages/core/tsconfig.json b/packages/core/tsconfig.json index 608f0e1c..afff666a 100644 --- a/packages/core/tsconfig.json +++ b/packages/core/tsconfig.json @@ -9,7 +9,7 @@ "types": ["node"], // No emit, and therefore no project reference to ../schema. Packages here have no build step: - // @codra/schema is consumed as raw TS source through its `exports` map, exactly as the root + // @codraoss/schema is consumed as raw TS source through its `exports` map, exactly as the root // program consumes it. Keeping the base's composite/declaration settings would instead demand // that schema be built to dist/ first (TS6305) -- a build nothing in this repo performs. // This project exists to typecheck the package against a NARROWER lib/types than the root diff --git a/packages/core/tsup.config.json b/packages/core/tsup.config.json new file mode 100644 index 00000000..c5d47f10 --- /dev/null +++ b/packages/core/tsup.config.json @@ -0,0 +1,26 @@ +{ + "entry": [ + "src/index.ts", + "src/ports/index.ts", + "src/logger.ts", + "src/diff/index.ts", + "src/model-output/index.ts", + "src/rules/detect.ts", + "src/rules/table.ts", + "src/claim-checks.ts", + "src/verify.ts", + "src/fingerprint.ts", + "src/timeout.ts", + "src/token-tracker.ts", + "src/prompts/file-review.ts", + "src/prompts/languages.ts", + "src/prompts/summary.ts", + "src/prompts/verify.ts" + ], + "format": "esm", + "dts": true, + "splitting": true, + "sourcemap": true, + "clean": true, + "outDir": "dist" +} diff --git a/packages/db/LICENSE b/packages/db/LICENSE new file mode 100644 index 00000000..024299ea --- /dev/null +++ b/packages/db/LICENSE @@ -0,0 +1,625 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 2007 Free Software Foundation, Inc. <>_ + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### 0. Definitions +“This License” refers to version 3 of the GNU Affero General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based +on the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that **(1)** displays an appropriate copyright notice, and **(2)** +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code +The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other +than the work as a whole, that **(a)** is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and **(b)** serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +### 2. Basic Permissions +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +### 4. Conveying Verbatim Copies +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified +it, and giving a relevant date. +* **b)** The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. +This requirement modifies the requirement in section 4 to +“keep intact all notices”. +* **c)** You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. +* **d)** If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either **(1)** a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or **(2)** access to copy the +Corresponding Source from a network server at no charge. +* **c)** Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. +* **d)** Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A “User Product” is either **(1)** a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or **(2)** anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install + +### 6. Conveying Non-Source Forms +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or +authors of the material; or +* **e)** Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +### 8. Termination +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated **(a)** +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and **(b)** permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either **(1)** cause the Corresponding Source to be so +available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license **(a)** in connection with copies of the covered work +conveyed by you (or copies made from those copies), or **(b)** primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found. + + Codra: Open source PR review infrastructure for Cloudflare Workers. + Copyright (C) 2026 Devarshi Shimpi + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source” link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<>. diff --git a/packages/db/README.md b/packages/db/README.md new file mode 100644 index 00000000..d567e446 --- /dev/null +++ b/packages/db/README.md @@ -0,0 +1,15 @@ +# @codraoss/db + +Codra's Postgres persistence layer behind repository interfaces. + +Part of [Codra](https://codra.run), an open-source code review engine. See the [monorepo](https://github.com/devarshishimpi/codra) for development, and [CONTRIBUTING](https://github.com/devarshishimpi/codra/blob/main/CONTRIBUTING.md) for the dual-licensing / CLA details. + +## Install + +```bash +npm install @codraoss/db +``` + +## License + +[AGPL-3.0-only](./LICENSE) © Devarshi Shimpi diff --git a/packages/db/migrations/004_observability.sql b/packages/db/migrations/004_observability.sql new file mode 100644 index 00000000..c20135f7 --- /dev/null +++ b/packages/db/migrations/004_observability.sql @@ -0,0 +1,27 @@ +-- Observability for reviews that ran in a degraded mode but reported success. +-- +-- `degraded` was computed by the model adapters and then only logged, so the two questions an operator +-- most needs answered after a bad review -- "did this run without a response grammar?" and "was the +-- answer truncated?" -- could not be asked of the database at all. +-- +-- Values: +-- 'schema-dropped' the model refused the response grammar and answered unconstrained +-- 'schema-dropped-catchall' same, but matched only the broad 400 heuristic, so it may not have been +-- a grammar rejection at all -- kept separate so the heuristic's real +-- hit rate is measurable rather than assumed +-- 'truncated' the last model in the chain ran out of output room and its partial +-- answer was salvaged; findings may be missing +-- NULL clean +ALTER TABLE file_reviews ADD COLUMN IF NOT EXISTS degraded TEXT; + +-- Partial: the overwhelming majority of rows are NULL, and every query against this column asks for +-- the ones that are not. +CREATE INDEX IF NOT EXISTS file_reviews_degraded_idx + ON file_reviews (degraded) + WHERE degraded IS NOT NULL; + +-- Which reviewer produced a comment. NULL means the repo's primary model, which is every row today; +-- it only becomes meaningful once a secondary reviewer is configured. Recorded for display and +-- attribution only -- never for scoring, since agreement between models is anti-correlated with +-- correctness in the measured corpus. +ALTER TABLE review_comments ADD COLUMN IF NOT EXISTS reviewer_model TEXT; diff --git a/packages/db/package.json b/packages/db/package.json index e9eefc46..eb08cff8 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,27 +1,76 @@ { - "name": "@codra/db", + "name": "@codraoss/db", "version": "0.9.4", - "private": true, + "description": "Codra's Postgres persistence layer behind repository interfaces.", + "author": "Devarshi Shimpi", + "license": "AGPL-3.0-only", + "homepage": "https://codra.run", + "repository": { + "type": "git", + "url": "git+https://github.com/devarshishimpi/codra.git", + "directory": "packages/db" + }, + "bugs": { + "url": "https://github.com/devarshishimpi/codra/issues" + }, "type": "module", + "sideEffects": false, "exports": { - ".": "./src/index.ts", "./client": "./src/client.ts", "./repositories": "./src/repositories/index.ts", "./env": "./src/env.ts", "./jobs": "./src/jobs.ts", "./file-reviews": "./src/file-reviews.ts", - "./test/fakes": "./test/fakes/index.ts", "./*": "./src/*.ts" }, + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "exports": { + "./client": { + "types": "./dist/client.d.ts", + "import": "./dist/client.js" + }, + "./repositories": { + "types": "./dist/repositories/index.d.ts", + "import": "./dist/repositories/index.js" + }, + "./env": { + "types": "./dist/env.d.ts", + "import": "./dist/env.js" + }, + "./jobs": { + "types": "./dist/jobs.d.ts", + "import": "./dist/jobs.js" + }, + "./file-reviews": { + "types": "./dist/file-reviews.d.ts", + "import": "./dist/file-reviews.js" + }, + "./*": { + "types": "./dist/*.d.ts", + "import": "./dist/*.js" + } + } + }, "scripts": { + "build": "tsup", "typecheck": "tsc -p tsconfig.json", - "test": "vitest run" + "test": "vitest run", + "prepack": "node ../../scripts/swap-publish-exports.mjs promote", + "postpack": "node ../../scripts/swap-publish-exports.mjs restore" }, "dependencies": { - "@codra/schema": "*", + "@codraoss/schema": "^0.9.4", + "@codraoss/core": "^0.9.4", "postgres": "^3.4.9" }, "devDependencies": { - "@types/node": "^22.0.0" + "@types/node": "^22.0.0", + "tsup": "^8.0.0" } } diff --git a/packages/db/src/app-settings.ts b/packages/db/src/app-settings.ts index 7ba51096..eda495ea 100644 --- a/packages/db/src/app-settings.ts +++ b/packages/db/src/app-settings.ts @@ -7,7 +7,7 @@ import { CONCURRENCY_LEVELS, MAX_COMMENTS_OPTIONS } from './constants'; -import { reviewMaxFilesRange, reviewSettingsSchema, type ReviewSettings } from '@codra/schema'; +import { reviewMaxFilesRange, reviewSettingsSchema, type ReviewSettings } from '@codraoss/schema'; diff --git a/packages/db/src/constants.ts b/packages/db/src/constants.ts index 7e048ba5..34454b76 100644 --- a/packages/db/src/constants.ts +++ b/packages/db/src/constants.ts @@ -1,4 +1,4 @@ -import { reviewConcurrencyLevels, reviewMaxCommentsOptions } from '@codra/schema'; +import { reviewConcurrencyLevels, reviewMaxCommentsOptions } from '@codraoss/schema'; // accounts.ts export const ACCOUNT_COLUMNS = 'id, github_user_id, github_username, account_name, account_email, timezone'; diff --git a/packages/db/src/file-reviews-bulk.ts b/packages/db/src/file-reviews-bulk.ts index a6e8c0a1..10f69ee8 100644 --- a/packages/db/src/file-reviews-bulk.ts +++ b/packages/db/src/file-reviews-bulk.ts @@ -1,5 +1,5 @@ import type { DbEnv } from './env'; -import type { BulkFileReviewInput } from '@codra/core/ports'; +import type { BulkFileReviewInput } from '@codraoss/core/ports'; import { queryRows, queryTransaction } from './client'; import { REVIEW_COMMENT_INSERT_CASTS, @@ -24,12 +24,14 @@ export async function bulkInheritFileReviews( -- Carried, not defaulted: an inheriting job reading 0 here approves the PR silently. withheld_counts, -- Carried too, or every inherited row looks pre-batching. - batch_size + batch_size, + -- An inherited row is the SAME review; hiding that it ran degraded would be a fresh lie. + degraded ) SELECT $1::uuid, file_path, file_status, model_used, diff_line_count, diff_input, raw_ai_output, input_tokens, output_tokens, duration_ms, verdict, file_summary, overall_correctness, confidence_score, error_msg, model_provider, - withheld_counts, batch_size + withheld_counts, batch_size, degraded FROM file_reviews WHERE job_id = $2::uuid AND file_status = 'done' AND file_path = ANY($3::text[]) ON CONFLICT (job_id, file_path) DO NOTHING @@ -45,12 +47,12 @@ export async function bulkInheritFileReviews( INSERT INTO review_comments ( file_review_id, path, line, position, severity, category, title, body, code_suggestion, confidence_score, evidence, fingerprint, anchor_hash, posted, claim_type, context_snippet, disposition, fingerprint_v2, - source, rule_id + source, rule_id, reviewer_model ) SELECT nw.new_id, rc.path, rc.line, rc.position, rc.severity, rc.category, rc.title, rc.body, rc.code_suggestion, rc.confidence_score, rc.evidence, rc.fingerprint, rc.anchor_hash, FALSE, rc.claim_type, rc.context_snippet, NULL, rc.fingerprint_v2, -- Carried, or a retried job's rule findings become LLM findings. - rc.source, rc.rule_id + rc.source, rc.rule_id, rc.reviewer_model FROM UNNEST($1::uuid[], $2::text[]) AS nw(new_id, file_path) JOIN file_reviews pf ON pf.job_id = $3::uuid AND pf.file_path = nw.file_path JOIN review_comments rc ON rc.file_review_id = pf.id @@ -63,7 +65,7 @@ export async function bulkInheritFileReviews( }); } -export type { BulkFileReviewInput } from '@codra/core/ports'; +export type { BulkFileReviewInput } from '@codraoss/core/ports'; // One transaction: per-file upserts would spend the saved model calls back on DB subrequests. `diff_input` is not written (migration 003 nulls it). export async function bulkUpsertFileReviews( @@ -80,20 +82,20 @@ export async function bulkUpsertFileReviews( job_id, file_path, file_status, model_used, diff_line_count, diff_input, raw_ai_output, input_tokens, output_tokens, duration_ms, verdict, file_summary, overall_correctness, confidence_score, error_msg, model_provider, - withheld_counts, batch_size + withheld_counts, degraded, batch_size ) SELECT $1::uuid, u.file_path, u.file_status, u.model_used, u.diff_line_count, NULL, u.raw_ai_output, u.input_tokens, u.output_tokens, u.duration_ms, u.verdict, u.file_summary, u.overall_correctness, u.confidence_score, u.error_msg, u.model_provider, -- Matches upsertFileReview's '::text::jsonb' idiom; mixing idioms is how the string-scalar bug spread across five columns. - u.withheld_counts::jsonb, u.batch_size + u.withheld_counts::jsonb, u.degraded, u.batch_size FROM UNNEST( $2::text[], $3::text[], $4::text[], $5::int[], $6::text[], $7::int[], $8::int[], $9::int[], - $10::text[], $11::text[], $12::text[], $13::real[], $14::text[], $15::text[], $16::text[], $17::int[] + $10::text[], $11::text[], $12::text[], $13::real[], $14::text[], $15::text[], $16::text[], $17::text[], $18::int[] ) AS u( file_path, file_status, model_used, diff_line_count, raw_ai_output, input_tokens, output_tokens, duration_ms, verdict, file_summary, overall_correctness, confidence_score, - error_msg, model_provider, withheld_counts, batch_size + error_msg, model_provider, withheld_counts, degraded, batch_size ) ON CONFLICT (job_id, file_path) DO UPDATE SET file_status = EXCLUDED.file_status, @@ -111,6 +113,7 @@ export async function bulkUpsertFileReviews( error_msg = EXCLUDED.error_msg, model_provider = EXCLUDED.model_provider, withheld_counts = EXCLUDED.withheld_counts, + degraded = EXCLUDED.degraded, batch_size = EXCLUDED.batch_size, -- A terminal review supersedes any in-flight async submission, matching upsertFileReview. async_request_id = NULL, @@ -119,12 +122,12 @@ export async function bulkUpsertFileReviews( RETURNING id, file_path `, (() => { - const res: any[] = [jobId, [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []]; + const res: any[] = [jobId, [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], [], []]; for (const i of inputs) { res[1].push(i.filePath); res[2].push(i.fileStatus); res[3].push(i.modelUsed); res[4].push(i.diffLineCount); res[5].push(i.rawAiOutput); res[6].push(i.inputTokens); res[7].push(i.outputTokens); res[8].push(i.durationMs); res[9].push(i.verdict); res[10].push(i.fileSummary); res[11].push(i.overallCorrectness ?? null); res[12].push(i.confidenceScore ?? null); - res[13].push(i.errorMessage); res[14].push(i.modelProvider ?? null); res[15].push(i.withheldCounts ? JSON.stringify(i.withheldCounts) : null); res[16].push(i.batchSize); + res[13].push(i.errorMessage); res[14].push(i.modelProvider ?? null); res[15].push(i.withheldCounts ? JSON.stringify(i.withheldCounts) : null); res[16].push(i.degraded ?? null); res[17].push(i.batchSize); } return res; })(), @@ -186,6 +189,7 @@ export async function bulkRecordRetryableFileReviewFailures( confidence_score = NULL, -- Also cleared, unlike the single-file version: gate-pipeline sums this unfiltered. withheld_counts = NULL, + degraded = NULL, error_msg = EXCLUDED.error_msg, transient_error_count = file_reviews.transient_error_count + $6::int RETURNING id, file_path, transient_error_count diff --git a/packages/db/src/file-reviews-findings.ts b/packages/db/src/file-reviews-findings.ts index b7743387..1cec7149 100644 --- a/packages/db/src/file-reviews-findings.ts +++ b/packages/db/src/file-reviews-findings.ts @@ -1,8 +1,8 @@ import type { DbEnv } from './env'; -import type { SuppressedFinding } from '@codra/core/ports'; +import type { SuppressedFinding } from '@codraoss/core/ports'; import { queryRows } from './client'; -export type { SuppressedFinding } from '@codra/core/ports'; +export type { SuppressedFinding } from '@codraoss/core/ports'; // Findings already posted on an EARLIER commit with the anchored line unchanged, or rejected by a human anywhere in this repository. // `j.commit_sha <> me.commit_sha` is load-bearing: retries and mention-triggered re-reviews reuse the SAME head commit. diff --git a/packages/db/src/file-reviews.ts b/packages/db/src/file-reviews.ts index b61c4b77..effd1811 100644 --- a/packages/db/src/file-reviews.ts +++ b/packages/db/src/file-reviews.ts @@ -1,5 +1,5 @@ -import type { DbEnv } from './env'; -import type { ParsedReviewComment } from '@codra/schema'; +import type { DbEnv } from './env'; +import type { ParsedReviewComment } from '@codraoss/schema'; import { parseJsonColumn, queryRows, queryTransaction } from './client'; import { @@ -61,7 +61,10 @@ export async function upsertFileReview( confidenceScore?: number | null; errorMessage: string | null; // Findings dropped in the PARSER have no review_comments row to carry a disposition; without this, "everything was withheld" is indistinguishable from clean. - withheldCounts?: { evidence: number; claimDenied: number } | null; + withheldCounts?: { evidence: number; claimDenied: number; contextOnly?: number; absenceRefuted?: number } | null; + // The call answered, but not cleanly: it ran without a response grammar, or its output was cut off + // and salvaged. Persisted rather than logged so "how often did this happen" is a query. + degraded?: string | null; // Async batch bookkeeping: set on submit to the Workers AI queue, cleared once the batch completes. asyncRequestId?: string | null; asyncModel?: string | null; @@ -90,9 +93,10 @@ export async function upsertFileReview( async_request_id, async_model, withheld_counts, + degraded, batch_size ) - VALUES ($1::uuid, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19::text::jsonb, 1) + VALUES ($1::uuid, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19::text::jsonb, $20, 1) ON CONFLICT (job_id, file_path) DO UPDATE SET file_status = EXCLUDED.file_status, model_used = EXCLUDED.model_used, @@ -111,6 +115,7 @@ export async function upsertFileReview( async_request_id = EXCLUDED.async_request_id, async_model = EXCLUDED.async_model, withheld_counts = EXCLUDED.withheld_counts, + degraded = EXCLUDED.degraded, batch_size = EXCLUDED.batch_size, transient_error_count = 0 RETURNING id @@ -136,6 +141,7 @@ export async function upsertFileReview( input.asyncModel ?? null, // JSON text to ::text::jsonb placeholder prevents string-scalar bugs. input.withheldCounts ? JSON.stringify(input.withheldCounts) : null, + input.degraded ?? null, ], ); diff --git a/packages/db/src/jobs-mapping.ts b/packages/db/src/jobs-mapping.ts index dc16b28d..5b8f9399 100644 --- a/packages/db/src/jobs-mapping.ts +++ b/packages/db/src/jobs-mapping.ts @@ -1,5 +1,5 @@ import { parseJsonColumn } from './client'; -import { defaultRepoConfig, jobSummarySchema, repoConfigSchema, type RepoConfig } from '@codra/schema'; +import { defaultRepoConfig, jobSummarySchema, repoConfigSchema, type RepoConfig } from '@codraoss/schema'; // Import from db/jobs.ts, not from here: eight specs vi.mock the '@server/db/jobs' specifier, and a direct sibling import silently bypasses them. // Deliberately imports NONE of the other jobs-* siblings, so it stays the leaf they can all depend on. diff --git a/packages/db/src/jobs.ts b/packages/db/src/jobs.ts index 0c540e3f..2261cd8b 100644 --- a/packages/db/src/jobs.ts +++ b/packages/db/src/jobs.ts @@ -1,7 +1,7 @@ import type { DbEnv } from './env'; -import { hexToBytes } from '@codra/schema/hex'; +import { hexToBytes } from '@codraoss/schema/hex'; import { parseJsonColumn, queryRows } from './client'; -import { defaultRepoConfig, jobDetailSchema, repoConfigSchema, type RepoConfig } from '@codra/schema'; +import { defaultRepoConfig, jobDetailSchema, repoConfigSchema, type RepoConfig } from '@codraoss/schema'; import { getOrCreateRepository } from './repositories'; import { reviewCommentJsonObject } from './review-comment-sql'; import { type JobRow, bytesToHex, mapJob } from './jobs-mapping'; @@ -248,6 +248,7 @@ export async function getJobDetail(env: DbEnv, jobId: string) { -- What the gates dropped. Without it the logs cannot tell "found nothing" apart -- from "found things and withheld every one of them". 'withheldCounts', fr.withheld_counts, + 'degraded', fr.degraded, 'parsedComments', COALESCE( ( SELECT JSON_AGG( diff --git a/packages/db/src/learning.ts b/packages/db/src/learning.ts index ebd346cd..f314985e 100644 --- a/packages/db/src/learning.ts +++ b/packages/db/src/learning.ts @@ -1,6 +1,6 @@ import type { DbEnv } from './env'; import { queryRows } from './client'; -import type { ClaimType } from '@codra/schema'; +import type { ClaimType } from '@codraoss/schema'; // Reads over findings a human has already judged: report only over the LABELLED subset, always with n. The absence of a label is not a signal. diff --git a/packages/db/src/model-configs.ts b/packages/db/src/model-configs.ts index 918d0fc1..05ecfaae 100644 --- a/packages/db/src/model-configs.ts +++ b/packages/db/src/model-configs.ts @@ -11,7 +11,7 @@ import { type ModelConfig, type ResolvedModelConfig, type LlmProviderSecret, -} from '@codra/schema'; +} from '@codraoss/schema'; export type { ResolvedModelConfig, LlmProviderSecret }; diff --git a/packages/db/src/repo-configs.ts b/packages/db/src/repo-configs.ts index 7764a54b..a6ce3291 100644 --- a/packages/db/src/repo-configs.ts +++ b/packages/db/src/repo-configs.ts @@ -1,7 +1,7 @@ import type { DbEnv } from './env'; import { parseJsonColumn, queryRows } from './client'; -import { defaultRepoConfig, normalizeRepoConfig, repoConfigRecordSchema, repoConfigSchema, type RepoConfig } from '@codra/schema'; +import { defaultRepoConfig, normalizeRepoConfig, repoConfigRecordSchema, repoConfigSchema, type RepoConfig } from '@codraoss/schema'; import { getOrCreateRepository } from './repositories'; type RepoConfigRow = { diff --git a/packages/db/src/repositories/file-review-repository.ts b/packages/db/src/repositories/file-review-repository.ts index d3c78dbd..79052088 100644 --- a/packages/db/src/repositories/file-review-repository.ts +++ b/packages/db/src/repositories/file-review-repository.ts @@ -1,4 +1,4 @@ -import type { FileReviewStore } from '@codra/core/ports'; +import type { FileReviewStore } from '@codraoss/core/ports'; import type { DbEnv } from '../env'; import { bulkInheritFileReviews, diff --git a/packages/db/src/repositories/instance-id-repository.ts b/packages/db/src/repositories/instance-id-repository.ts index 9460ff21..d2cd8e1b 100644 --- a/packages/db/src/repositories/instance-id-repository.ts +++ b/packages/db/src/repositories/instance-id-repository.ts @@ -1,4 +1,4 @@ -import type { InstanceIdStore } from '@codra/core/ports'; +import type { InstanceIdStore } from '@codraoss/core/ports'; import type { DbEnv } from '../env'; import { queryRows } from '../client'; import { INSTANCE_ID_KEY } from '../constants'; diff --git a/packages/db/src/repositories/jobs-repository.ts b/packages/db/src/repositories/jobs-repository.ts index bb50f4b5..e18e91b1 100644 --- a/packages/db/src/repositories/jobs-repository.ts +++ b/packages/db/src/repositories/jobs-repository.ts @@ -1,4 +1,4 @@ -import type { JobLeaseClaim as CoreJobLeaseClaim, JobRow as CoreJobRow, JobStore, PersistedReviewJob } from '@codra/core/ports'; +import type { JobLeaseClaim as CoreJobLeaseClaim, JobRow as CoreJobRow, JobStore, PersistedReviewJob } from '@codraoss/core/ports'; import type { DbEnv } from '../env'; import { claimJobLease, diff --git a/packages/db/src/repositories/repo-config-repository.ts b/packages/db/src/repositories/repo-config-repository.ts index 73adc242..2fb0e3d0 100644 --- a/packages/db/src/repositories/repo-config-repository.ts +++ b/packages/db/src/repositories/repo-config-repository.ts @@ -1,4 +1,4 @@ -import type { RepoConfigStore } from '@codra/core/ports'; +import type { RepoConfigStore } from '@codraoss/core/ports'; import type { DbEnv } from '../env'; import { getRepoConfigRecord, syncRepoConfig } from '../repo-configs'; diff --git a/packages/db/src/repositories/settings-repository.ts b/packages/db/src/repositories/settings-repository.ts index 2e5e29c2..9d908aec 100644 --- a/packages/db/src/repositories/settings-repository.ts +++ b/packages/db/src/repositories/settings-repository.ts @@ -1,4 +1,4 @@ -import type { LearningStore, ModelConfigReader, ReviewSettingsReader, WebhookDeliveryReader } from '@codra/core/ports'; +import type { LearningStore, ModelConfigReader, ReviewSettingsReader, WebhookDeliveryReader } from '@codraoss/core/ports'; import type { DbEnv } from '../env'; import { getReviewSettings } from '../app-settings'; import { getResolvedModelConfig } from '../model-configs'; diff --git a/packages/db/src/review-comment-sql.ts b/packages/db/src/review-comment-sql.ts index 270c701c..f23d4a7d 100644 --- a/packages/db/src/review-comment-sql.ts +++ b/packages/db/src/review-comment-sql.ts @@ -1,4 +1,4 @@ -import type { ParsedReviewComment } from '@codra/schema'; +import type { ParsedReviewComment } from '@codraoss/schema'; // Shared review_comments field list. Update bulkInheritFileReviews if changed. @@ -6,7 +6,7 @@ import type { ParsedReviewComment } from '@codra/schema'; export const REVIEW_COMMENT_INSERT_COLUMNS = [ 'path', 'line', 'position', 'severity', 'category', 'title', 'body', 'code_suggestion', 'confidence_score', 'evidence', 'fingerprint', 'anchor_hash', 'claim_type', 'context_snippet', - 'disposition', 'fingerprint_v2', 'source', 'rule_id', + 'disposition', 'fingerprint_v2', 'source', 'rule_id', 'reviewer_model', ] as const; // Generated rather than written out so the cast count can never fall out of step with the column list. @@ -40,6 +40,7 @@ export function reviewCommentInsertValues(comments: ParsedReviewComment[]) { comments.map((c) => c.fingerprintV2 ?? null), comments.map((c) => c.source ?? 'llm'), comments.map((c) => c.ruleId ?? null), + comments.map((c) => c.reviewerModel ?? null), ]; } @@ -68,6 +69,7 @@ export function reviewCommentJsonObject(extraFields = '') { `'verifyReason', rc.verify_reason`, `'source', rc.source`, `'ruleId', rc.rule_id`, + `'reviewerModel', rc.reviewer_model`, ].join(',\n '); return `JSON_BUILD_OBJECT(\n ${fields}${extraFields ? `,\n ${extraFields}` : ''}\n )`; diff --git a/packages/db/src/stats.ts b/packages/db/src/stats.ts index 25423c69..33795224 100644 --- a/packages/db/src/stats.ts +++ b/packages/db/src/stats.ts @@ -1,7 +1,7 @@ import type { DbEnv } from './env'; -import { isSupportedTimeZone } from '@codra/schema/timezone'; +import { isSupportedTimeZone } from '@codraoss/schema/timezone'; import { queryRows } from './client'; -import { statsSchema, jobStatuses, reviewTriggers, reviewSeverities, reviewCategories } from '@codra/schema'; +import { statsSchema, jobStatuses, reviewTriggers, reviewSeverities, reviewCategories } from '@codraoss/schema'; import { getModelUsageStats } from './file-reviews'; // Guard the zone before it reaches SQL, so an unknown name can't error the query. diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index f30b5c17..2dfe7a61 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -3,10 +3,12 @@ "compilerOptions": { "module": "ESNext", "moduleResolution": "Bundler", - "outDir": "./dist", - "rootDir": "./src", "target": "ES2022", - "types": ["node"] + "types": ["node"], + "composite": false, + "declaration": false, + "emitDeclarationOnly": false, + "noEmit": true }, "include": [ "src/**/*", diff --git a/packages/db/tsup.config.json b/packages/db/tsup.config.json new file mode 100644 index 00000000..e0c19c65 --- /dev/null +++ b/packages/db/tsup.config.json @@ -0,0 +1,9 @@ +{ + "entry": ["src/**/*.ts"], + "format": "esm", + "dts": true, + "splitting": true, + "sourcemap": true, + "clean": true, + "outDir": "dist" +} diff --git a/packages/models/LICENSE b/packages/models/LICENSE new file mode 100644 index 00000000..024299ea --- /dev/null +++ b/packages/models/LICENSE @@ -0,0 +1,625 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 2007 Free Software Foundation, Inc. <>_ + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### 0. Definitions +“This License” refers to version 3 of the GNU Affero General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based +on the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that **(1)** displays an appropriate copyright notice, and **(2)** +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code +The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other +than the work as a whole, that **(a)** is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and **(b)** serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +### 2. Basic Permissions +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +### 4. Conveying Verbatim Copies +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified +it, and giving a relevant date. +* **b)** The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. +This requirement modifies the requirement in section 4 to +“keep intact all notices”. +* **c)** You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. +* **d)** If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either **(1)** a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or **(2)** access to copy the +Corresponding Source from a network server at no charge. +* **c)** Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. +* **d)** Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A “User Product” is either **(1)** a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or **(2)** anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install + +### 6. Conveying Non-Source Forms +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or +authors of the material; or +* **e)** Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +### 8. Termination +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated **(a)** +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and **(b)** permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either **(1)** cause the Corresponding Source to be so +available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license **(a)** in connection with copies of the covered work +conveyed by you (or copies made from those copies), or **(b)** primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found. + + Codra: Open source PR review infrastructure for Cloudflare Workers. + Copyright (C) 2026 Devarshi Shimpi + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source” link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<>. diff --git a/packages/models/README.md b/packages/models/README.md new file mode 100644 index 00000000..de57fe41 --- /dev/null +++ b/packages/models/README.md @@ -0,0 +1,15 @@ +# @codraoss/models + +LLM provider adapters and model catalog for the Codra review engine. + +Part of [Codra](https://codra.run), an open-source code review engine. See the [monorepo](https://github.com/devarshishimpi/codra) for development, and [CONTRIBUTING](https://github.com/devarshishimpi/codra/blob/main/CONTRIBUTING.md) for the dual-licensing / CLA details. + +## Install + +```bash +npm install @codraoss/models +``` + +## License + +[AGPL-3.0-only](./LICENSE) © Devarshi Shimpi diff --git a/packages/models/package.json b/packages/models/package.json index 7c6baed9..750d6279 100644 --- a/packages/models/package.json +++ b/packages/models/package.json @@ -1,8 +1,20 @@ { - "name": "@codra/models", + "name": "@codraoss/models", "version": "0.9.4", - "private": true, + "description": "LLM provider adapters and model catalog for the Codra review engine.", + "author": "Devarshi Shimpi", + "license": "AGPL-3.0-only", + "homepage": "https://codra.run", + "repository": { + "type": "git", + "url": "git+https://github.com/devarshishimpi/codra.git", + "directory": "packages/models" + }, + "bugs": { + "url": "https://github.com/devarshishimpi/codra/issues" + }, "type": "module", + "sideEffects": false, "exports": { ".": "./src/index.ts", "./types": "./src/types.ts", @@ -13,14 +25,63 @@ "./anthropic": "./src/providers/anthropic.ts", "./openai": "./src/providers/openai.ts" }, + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./types": { + "types": "./dist/types.d.ts", + "import": "./dist/types.js" + }, + "./runner": { + "types": "./dist/runner.d.ts", + "import": "./dist/runner.js" + }, + "./cloudflare": { + "types": "./dist/providers/cloudflare.d.ts", + "import": "./dist/providers/cloudflare.js" + }, + "./google": { + "types": "./dist/providers/google.d.ts", + "import": "./dist/providers/google.js" + }, + "./vertex": { + "types": "./dist/providers/vertex.d.ts", + "import": "./dist/providers/vertex.js" + }, + "./anthropic": { + "types": "./dist/providers/anthropic.d.ts", + "import": "./dist/providers/anthropic.js" + }, + "./openai": { + "types": "./dist/providers/openai.d.ts", + "import": "./dist/providers/openai.js" + } + } + }, "scripts": { + "build": "tsup", "typecheck": "tsc -p tsconfig.json", - "test": "vitest run" + "test": "vitest run", + "prepack": "node ../../scripts/swap-publish-exports.mjs promote", + "postpack": "node ../../scripts/swap-publish-exports.mjs restore" }, "dependencies": { - "@codra/schema": "*", - "@codra/core": "*" + "@codraoss/schema": "^0.9.4", + "@codraoss/core": "^0.9.4" }, "devDependencies": { + "tsup": "^8.0.0" } } diff --git a/packages/models/src/catalog.ts b/packages/models/src/catalog.ts index 38e12874..459f852e 100644 --- a/packages/models/src/catalog.ts +++ b/packages/models/src/catalog.ts @@ -1,5 +1,5 @@ -import type { LlmApiFormat } from '@codra/schema'; -import { withTimeout } from '@codra/core/timeout'; +import type { LlmApiFormat } from '@codraoss/schema'; +import { withTimeout } from '@codraoss/core/timeout'; import { assertPublicBaseUrl } from './url-guard'; const MODEL_LIST_TIMEOUT_MS = 8_000; diff --git a/packages/models/src/internal/model-chain-progress.ts b/packages/models/src/internal/model-chain-progress.ts index 94cd62ae..e534337c 100644 --- a/packages/models/src/internal/model-chain-progress.ts +++ b/packages/models/src/internal/model-chain-progress.ts @@ -1,21 +1,18 @@ -import { logger } from '@codra/core/logger'; -import type { KvStore } from '@codra/core/ports'; -import type { TokenTracker } from '@codra/core/token-tracker'; +import { logger } from '@codraoss/core/logger'; +import type { KvStore } from '@codraoss/core/ports'; +import type { TokenTracker } from '@codraoss/core/token-tracker'; import { isPlausibleTokenBucket } from './model-support'; -// Stores where each label got to in its model chain so deferred reviews resume properly. -// Stored as a single KV value per job to minimize subrequest budget overhead. - -// Outlives job continuations; key is job-scoped and dies with the job. +// One KV value per job to limit subrequests. const CHAIN_PROGRESS_TTL_SECONDS = 24 * 60 * 60; -// Allowed timeouts per model before dropping it from the job (3 = one full wave). +// 3 = one full wave. const MODEL_TIMEOUT_STRIKES = 3; -// Strikes before dropping the LAST chain candidate. Finite (6) to avoid infinite retries on dead models. +// Finite to avoid infinite retries on dead models. const LAST_CANDIDATE_TIMEOUT_STRIKES = 6; -// Max persisted cool-off (5 mins) protects against mis-parsed long delays. +// Caps mis-parsed long delays. const MAX_PERSISTED_COOLDOWN_MS = 5 * 60 * 1000; export interface ModelCooldown { @@ -39,7 +36,7 @@ function positiveInts(source: Record | undefined): Map | undefined): Map { const kept = new Map(); if (!source || typeof source !== 'object') return kept; @@ -48,7 +45,7 @@ function parseCooldowns(source: Record | undefined): Map for (const [model, value] of Object.entries(source)) { if (!value || typeof value !== 'object') continue; const until = typeof value.until === 'number' && Number.isFinite(value.until) ? value.until : 0; - // Drops implausible limit sizes to prevent indefinite model suppression. + // Drop implausible limits; avoids indefinite suppression. const limitTokens = typeof value.limitTokens === 'number' && isPlausibleTokenBucket(value.limitTokens) ? value.limitTokens @@ -61,26 +58,21 @@ function parseCooldowns(source: Record | undefined): Map function mergeCooldown(a: ModelCooldown | undefined, b: ModelCooldown): ModelCooldown { return { - // Idempotent max() merge for advancing deadlines. cooldownUntil: Math.max(a?.cooldownUntil ?? 0, b.cooldownUntil), - // Sticky limit retention on subsequent 429s. + // Sticky across repeated 429s. limitTokens: a?.limitTokens ?? b.limitTokens, }; } export class ModelChainProgressStore { private loaded: Promise> | null = null; - - // Per-model timeouts stored alongside chain progress to save subrequests. private timeouts = new Map(); - // Cleared strikes in this invocation, preventing max() merges from resurrecting them. + // Prevents max() merge from resurrecting cleared strikes. private clearedTimeouts = new Set(); - - // Persisted rate-limits to avoid re-paying 429 prompts across invocations. private cooldowns = new Map(); - // Single-flight writer prevents overlapping KV puts from dropping concurrent updates. + // Single-flight write; concurrent KV puts must not drop updates. private inFlightWrite: Promise | null = null; private dirty = false; @@ -94,7 +86,6 @@ export class ModelChainProgressStore { return this.jobId ? `jobs:${this.jobId}:chain-progress` : null; } - // Job-less reviews return 0 (nothing to resume). private load(): Promise> { if (this.loaded) return this.loaded; @@ -107,12 +98,11 @@ export class ModelChainProgressStore { const raw = rawString ? JSON.parse(rawString) : null; if (!raw || typeof raw !== 'object') return new Map(); - // Legacy support: reads bare label->index maps as files for smooth deploys. + // Legacy: bare label->index maps read as files. const stored = raw as StoredShape; const isNewShape = stored.files !== undefined || stored.timeouts !== undefined || stored.cooldowns !== undefined; this.timeouts = positiveInts(isNewShape ? stored.timeouts : undefined); - // Merge sync noteRateLimit calls. for (const [model, value] of parseCooldowns(isNewShape ? stored.cooldowns : undefined)) { this.cooldowns.set(model, mergeCooldown(this.cooldowns.get(model), value)); } @@ -137,7 +127,6 @@ export class ModelChainProgressStore { if (!this.key || nextIndex <= 0) return; const progress = await this.load(); - // Monotonic advance only. if ((progress.get(label) ?? 0) >= nextIndex) return; progress.set(label, nextIndex); this.dirty = true; @@ -145,14 +134,11 @@ export class ModelChainProgressStore { return this.flush(); } - // Ensure progress durability before deferring. private flush(): Promise { - // Join in-flight writes. if (this.inFlightWrite) return this.inFlightWrite; this.inFlightWrite = (async () => { try { - // Process mid-put advances. while (this.dirty) { this.dirty = false; await this.writeOnce(); @@ -171,7 +157,7 @@ export class ModelChainProgressStore { const progress = await this.load(); try { - // Merge against remote KV state using max() to prevent concurrent invocations from dropping labels. + // Merge via max() so concurrent invocations don't drop labels. this.tracker?.incrementSubrequests(1); const rawString = await this.kv.get(key); const raw = rawString ? JSON.parse(rawString) : null; @@ -215,13 +201,11 @@ export class ModelChainProgressStore { } } - // Clears terminal labels for clean retries. async clear(label: string): Promise { const progress = await this.load(); progress.delete(label); } - // Persist timeouts so subsequent waves can skip failing models. async noteTimeout(modelId: string): Promise { if (!this.key) return; await this.load(); @@ -230,11 +214,9 @@ export class ModelChainProgressStore { return this.flush(); } - // Reset tallies on success to avoid false permanent bans. async noteSuccess(modelId: string): Promise { if (!this.key) return; await this.load(); - // No-op for healthy models to save subrequests. if (!this.timeouts.has(modelId)) return; this.timeouts.delete(modelId); this.clearedTimeouts.add(modelId); @@ -247,26 +229,24 @@ export class ModelChainProgressStore { return (this.timeouts.get(modelId) ?? 0) >= MODEL_TIMEOUT_STRIKES; } - // For chain tails with no fallback. + // Chain tails have no fallback. async isTimingOutTerminally(modelId: string): Promise { await this.load(); return (this.timeouts.get(modelId) ?? 0) >= LAST_CANDIDATE_TIMEOUT_STRIKES; } - // Share load() promise to save KV reads. async loadCooldowns(): Promise> { await this.load(); return new Map(this.cooldowns); } - // Sync/non-flushing. Deferrals call flushPending() to coalesce writes. + // Sync; flushPending() coalesces writes. noteRateLimit(modelId: string, entry: ModelCooldown): void { if (!this.key) return; this.cooldowns.set(modelId, mergeCooldown(this.cooldowns.get(modelId), entry)); this.dirty = true; } - // Flush mutations without advancing progress (e.g. quota deferrals). flushPending(): Promise { if (!this.key || !this.dirty) return Promise.resolve(); return this.flush(); diff --git a/packages/models/src/internal/model-chain-runner.ts b/packages/models/src/internal/model-chain-runner.ts index f16ffa73..707cbc6b 100644 --- a/packages/models/src/internal/model-chain-runner.ts +++ b/packages/models/src/internal/model-chain-runner.ts @@ -1,10 +1,17 @@ -import { buildSummaryPrompt, SUMMARY_SYSTEM_PROMPT } from '@codra/core/prompts/summary'; -import { buildVerifyPrompt, VERIFY_RESPONSE_SCHEMA, VERIFY_SYSTEM_PROMPT, type VerifyCandidate } from '@codra/core/prompts/verify'; -import { adaptiveModelTimeoutMs, clampTimeoutToChainBudget, MODEL_FALLBACK_CHAIN_BUDGET_MS } from '../limits'; +import { buildSummaryPrompt, SUMMARY_SYSTEM_PROMPT } from '@codraoss/core/prompts/summary'; +import { buildVerifyPrompt, VERIFY_RESPONSE_SCHEMA, VERIFY_SYSTEM_PROMPT, type VerifyCandidate } from '@codraoss/core/prompts/verify'; +import { + adaptiveModelTimeoutMs, + chainAttemptTimeoutMs, + clampTimeoutToChainBudget, + MODEL_FALLBACK_CHAIN_BUDGET_MS, + MODEL_MIN_VIABLE_ATTEMPT_MS, + verifyTimeoutMs, +} from '../limits'; import { isCloudflareAllocationError, isTransientModelFailure, RetryableModelError } from './model-support'; -import { logger } from '@codra/core/logger'; -import type { RepoConfig, ResolvedModelConfig } from '@codra/schema'; -import type { TokenTracker } from '@codra/core/token-tracker'; +import { logger } from '@codraoss/core/logger'; +import type { RepoConfig, ResolvedModelConfig } from '@codraoss/schema'; +import type { TokenTracker } from '@codraoss/core/token-tracker'; import type { ModelInput, ModelResponse } from '../types'; // Import from services/model.ts, not here -- four specs vi.mock that specifier. @@ -99,9 +106,10 @@ export async function verifyFindings(ctx: ModelChainContext, params: { candidate userPrompt: buildVerifyPrompt(params.candidates), // Must be the verify grammar, not the file-review one -- that schema makes strict decoding unsatisfiable and the pass a silent no-op. responseSchema: VERIFY_RESPONSE_SCHEMA as unknown as ModelInput['responseSchema'], + // A missing verdict is not a pass; a truncated list would silently withhold findings. + truncationIntolerant: true, }; - // Scale the timeout with the number of findings under review (capped inside adaptiveModelTimeoutMs). - const timeoutMs = clampTimeoutToChainBudget(adaptiveModelTimeoutMs(params.candidates.length * 8)); + const requestedTimeoutMs = clampTimeoutToChainBudget(verifyTimeoutMs(params.candidates.length)); let lastError: unknown; const chainStartedAt = Date.now(); @@ -116,12 +124,17 @@ export async function verifyFindings(ctx: ModelChainContext, params: { candidate }); break; } - // Prospective: see the matching check in runModelChain. - if (modelIndex > 0 && Date.now() - chainStartedAt - gateWaitMs + timeoutMs > MODEL_FALLBACK_CHAIN_BUDGET_MS) { + const attemptTimeoutMs = chainAttemptTimeoutMs({ + requestedMs: requestedTimeoutMs, + remainingChainMs: MODEL_FALLBACK_CHAIN_BUDGET_MS - (Date.now() - chainStartedAt - gateWaitMs), + hasAnotherModel: modelIndex < modelsToTry.length - 1, + }); + + if (modelIndex > 0 && attemptTimeoutMs === 0) { logger.warn('Stopping the verification chain; no room in the per-invocation time budget for another model', { elapsedMs: Date.now() - chainStartedAt, gateWaitMs, - timeoutMs, + requestedTimeoutMs, skippedModels: modelsToTry.slice(modelIndex), }); break; @@ -140,7 +153,12 @@ export async function verifyFindings(ctx: ModelChainContext, params: { candidate } try { - const response = await ctx.callResolvedModel(resolved, input, timeoutMs, recordGateWait); + const response = await ctx.callResolvedModel( + resolved, + input, + Math.max(attemptTimeoutMs, MODEL_MIN_VIABLE_ATTEMPT_MS), + recordGateWait, + ); if (ctx.tracker) { ctx.tracker.record(response.modelUsed, response.inputTokens, response.outputTokens); } @@ -150,7 +168,11 @@ export async function verifyFindings(ctx: ModelChainContext, params: { candidate if (resolved.apiFormat === 'cloudflare-workers-ai' && isCloudflareAllocationError(error)) { await ctx.markProviderUnavailable(resolved.providerId, error instanceof Error ? error.message : String(error)); } - logger.warn(`Verification model ${currentModel} failed`, { error: error instanceof Error ? error.message : String(error) }); + logger.warn(`Verification model ${currentModel} failed`, { + error: error instanceof Error ? error.message : String(error), + attemptTimeoutMs, + candidates: params.candidates.length, + }); } } diff --git a/packages/models/src/internal/model-rate-limits.ts b/packages/models/src/internal/model-rate-limits.ts index ac1c7735..16a5c473 100644 --- a/packages/models/src/internal/model-rate-limits.ts +++ b/packages/models/src/internal/model-rate-limits.ts @@ -1,33 +1,26 @@ -import { logger } from '@codra/core/logger'; +import { logger } from '@codraoss/core/logger'; import { ModelCallGate } from '../limits'; -import type { ResolvedModelConfig } from '@codra/schema'; +import type { ResolvedModelConfig } from '@codraoss/schema'; import { MAX_METERED_QUEUE_DEPTH, PROMPT_FIT_SAFETY_FACTOR, parseRateLimitFromError } from './model-support'; -// Narrow port onto whatever survives an invocation (today: the job's chain-progress KV value), so -// this class stays unit-testable without an env and the barrel surface is unchanged. export interface RateLimitPersistence { loadCooldowns(): Promise>; noteRateLimit(modelId: string, entry: { cooldownUntil: number; limitTokens?: number }): void; } -// Import from the services/model barrel, not here (four specs vi.mock it). export class ModelRateLimitBook { - // Workers allows 6 simultaneous connections per invocation; gating starts the client timeout once a slot is held, instead of while queued. + // Slot held before client timeout starts (Workers: 6 concurrent conns/invocation). private readonly callGate = new ModelCallGate(); - // Google's 429 body carries both bucket and cool-off, so parsing it keeps this adaptive with no baked-in numbers. private readonly modelRateLimits = new Map(); - // Keyed by model, not provider: keying by provider serialized every call in an all-Google chain, dropping concurrency and throughput. + // Keyed by model, not provider, so an all-Google chain doesn't serialize. private readonly tokenMeteredModels = new Map(); - // Memoized so the KV read is shared, not repeated per model per file. private hydrated: Promise | null = null; constructor(private readonly persistence?: RateLimitPersistence) {} - // Without this the book is invocation-scoped: every job continuation re-paid a full-prompt 429 to - // re-learn a cool-off the previous invocation had already been told about. private hydrate(): Promise { this.hydrated ??= (async () => { if (!this.persistence) return; @@ -38,8 +31,7 @@ export class ModelRateLimitBook { cooldownUntil: Math.max(existing?.cooldownUntil ?? 0, entry.cooldownUntil), }); - // A model with a known bucket is token-metered, so serialize it from the FIRST call rather - // than after this invocation re-earns its own 429. + // Serialize from the first call, not after re-earning our own 429. if (!this.tokenMeteredModels.has(modelName)) { this.tokenMeteredModels.set(modelName, new ModelCallGate(1)); } @@ -48,15 +40,12 @@ export class ModelRateLimitBook { return this.hydrated; } - // Deliberately does NOT wait out the cool-off: the file falls through to the next model immediately, trading share of files for wall-clock. note(resolved: ResolvedModelConfig, error: unknown) { const { limitTokens, retryAfterMs } = parseRateLimitFromError(error); const existing = this.modelRateLimits.get(resolved.modelName); const entry = { - // Sticky: a later 429 that omits the number must not erase it. - limitTokens: limitTokens ?? existing?.limitTokens, - // Default to a minute when the provider didn't say -- these buckets are per-minute. + limitTokens: limitTokens ?? existing?.limitTokens, // sticky: a later 429 without a number can't erase it cooldownUntil: Date.now() + (retryAfterMs ?? 60_000), }; this.modelRateLimits.set(resolved.modelName, entry); @@ -71,12 +60,9 @@ export class ModelRateLimitBook { } } - // Avoids re-probing a model that already said "retry in Ns" once per file, which is what produced "Too many subrequests". async skipReason(modelName: string, estimatedPromptTokens: number): Promise { - // Cool-offs learned by an earlier invocation of this job count too. await this.hydrate(); - // Never queue deeply behind a serialized model; a shallow queue sends overflow to a free model instead. const gate = this.tokenMeteredModels.get(modelName); if (gate && gate.queueDepth >= MAX_METERED_QUEUE_DEPTH) { return `${gate.queueDepth} calls already queued on it`; @@ -89,7 +75,6 @@ export class ModelRateLimitBook { return `cooling off for another ${Math.ceil((known.cooldownUntil - Date.now()) / 1000)}s`; } - // A prompt larger than the whole per-minute bucket can never succeed, however long we wait. if (known.limitTokens && estimatedPromptTokens > known.limitTokens * PROMPT_FIT_SAFETY_FACTOR) { return `prompt ~${estimatedPromptTokens} tokens exceeds its ${known.limitTokens}-token bucket`; } @@ -97,12 +82,11 @@ export class ModelRateLimitBook { return null; } - // 6-connection cap only, no per-model gate: async-batch submit/poll isn't the token-metered path. async runShared(fn: () => Promise): Promise { return this.callGate.run(fn); } - // Model gate then shared gate, in that order, to rule out deadlock. + // Model gate before shared gate, in that order, to rule out deadlock. async runGated( resolved: ResolvedModelConfig, onGateWait: ((waitedMs: number) => void) | undefined, diff --git a/packages/models/src/internal/model-review-batch.ts b/packages/models/src/internal/model-review-batch.ts index 3a92d7dd..6ea7dabc 100644 --- a/packages/models/src/internal/model-review-batch.ts +++ b/packages/models/src/internal/model-review-batch.ts @@ -1,9 +1,9 @@ import { submitCloudflareBatch, pollCloudflareBatch } from '../providers/cloudflare'; -import { buildFileReviewPrompts, buildReviewResponseSchema } from '@codra/core/prompts/file-review'; -import { parseFileReviewResponse } from '@codra/core/model-output'; -import { truncateFileDiff } from '@codra/core/diff'; -import { logger } from '@codra/core/logger'; -import type { RepoConfig, ResolvedModelConfig } from '@codra/schema'; +import { buildFileReviewPrompts, buildReviewResponseSchema, reviewBreadth } from '@codraoss/core/prompts/file-review'; +import { parseFileReviewResponse } from '@codraoss/core/model-output'; +import { truncateFileDiff } from '@codraoss/core/diff'; +import { logger } from '@codraoss/core/logger'; +import type { RepoConfig, ResolvedModelConfig } from '@codraoss/schema'; import type { ModelResponse } from '../types'; import { COMPACT_REVIEW_PROMPT_LINE_CAP, type ModelReviewContext } from './model-review-file'; @@ -13,8 +13,10 @@ import { COMPACT_REVIEW_PROMPT_LINE_CAP, type ModelReviewContext } from './model // Returns null when unusable for the primary model, in which case the caller falls back to synchronous reviewFile. export async function submitReviewBatch(ctx: ModelReviewContext, params: { file: any; + fileContext?: string | null; prTitle: string | null; prDescription: string | null; + changelogExcerpt?: string | null; config: RepoConfig; totalLineCount: number; compactPrompt?: boolean; @@ -50,7 +52,7 @@ export async function submitReviewBatch(ctx: ModelReviewContext, params: { submitCloudflareBatch( ctx.aiBinding!, resolved.modelName, - { systemPrompt, userPrompt, responseSchema: buildReviewResponseSchema(params.config.review.max_comments) }, + { systemPrompt, userPrompt, responseSchema: buildReviewResponseSchema(reviewBreadth(params.config.review)) }, ctx.tracker, ), ); diff --git a/packages/models/src/internal/model-review-chain.ts b/packages/models/src/internal/model-review-chain.ts index 01733be0..cc3d6ee7 100644 --- a/packages/models/src/internal/model-review-chain.ts +++ b/packages/models/src/internal/model-review-chain.ts @@ -1,9 +1,9 @@ -import { logger } from '@codra/core/logger'; -import { isSubrequestBudgetMessage, isTimeoutMessage } from '@codra/schema/transient-errors'; -import type { RepoConfig, ResolvedModelConfig } from '@codra/schema'; +import { logger } from '@codraoss/core/logger'; +import { isSubrequestBudgetMessage, isTimeoutMessage } from '@codraoss/schema/transient-errors'; +import type { RepoConfig, ResolvedModelConfig } from '@codraoss/schema'; import type { CloudflareAiBinding } from '../providers/cloudflare'; -import type { ModelResponseSchema } from '../types'; -import { clampTimeoutToChainBudget, MODEL_FALLBACK_CHAIN_BUDGET_MS, SUBREQUEST_HEADROOM_FOR_MODEL_CALL } from '../limits'; +import { partialResponseOf, type ModelResponseSchema } from '../types'; +import { chainAttemptTimeoutMs, clampTimeoutToChainBudget, MODEL_MIN_VIABLE_ATTEMPT_MS, MODEL_FALLBACK_CHAIN_BUDGET_MS, SUBREQUEST_HEADROOM_FOR_MODEL_CALL } from '../limits'; import { estimatePromptTokens, isCloudflareAllocationError, @@ -15,39 +15,32 @@ import type { ModelChainContext } from './model-chain-runner'; import type { ModelRateLimitBook } from './model-rate-limits'; import type { ModelChainProgressStore } from './model-chain-progress'; -// Fallback chain for single/batched reviews. Import from services/model barrel. - // Past two quota failures per file burns subrequests for nothing. const MAX_QUOTA_FAILURES_PER_FILE = 2; -// Internal per-invocation chain state. export type ModelReviewContext = ModelChainContext & { aiBinding?: CloudflareAiBinding; rateLimits: ModelRateLimitBook; - // Models lacking async batching; routes subsequent files directly to synchronous. asyncUnsupportedModels: Set; - // Per-job memo of chain progress for each label. chainProgress: ModelChainProgressStore; }; -// Walks chain returning first success. `parse` errors count as model failures. export async function runModelChain(ctx: ModelReviewContext, params: { systemPrompt: string; userPrompt: string; - // Accepts both single-file and batched response schemas. responseSchema: ModelResponseSchema; timeoutMs: number; label: string; totalLineCount: number; config: RepoConfig; - // Needed output tokens; adapters clamp it, omission defaults to provider ceiling. outputBudgetTokens?: number; - // `isLastModel` allows parsers to reject weak answers and try better models, accepting them only as a last resort. + truncationIntolerant?: boolean; + // isLastModel: lets parsers reject weak answers except as a last resort. parse: (rawText: string, ctx: { isLastModel: boolean }) => T; - // Keys for resume memo. Bins pass member paths so progress survives mid-batch success/de-escalation. + // Bins pass member paths so progress survives mid-batch success/de-escalation. progressLabels?: readonly string[]; }) { - const { systemPrompt, userPrompt, responseSchema, label, outputBudgetTokens } = params; + const { systemPrompt, userPrompt, responseSchema, label, outputBudgetTokens, truncationIntolerant } = params; const progressLabels = params.progressLabels?.length ? params.progressLabels : [label]; const { primary, fallbacks } = ctx.selectModel({ @@ -56,8 +49,7 @@ export async function runModelChain(ctx: ModelReviewContext, params: { }); const wholeChain = [primary, ...fallbacks]; - // Resumes from invocation memo. Clamped to prevent empty chains on mid-job config changes. - // Takes the MINIMUM across members so no file skips a model untried due to bin-mates. + // Resume index is the min across bin members, clamped so it can't empty the chain. const recorded = await Promise.all(progressLabels.map((key) => ctx.chainProgress.startIndexFor(key))); const startIndex = Math.min(Math.min(...recorded), Math.max(wholeChain.length - 1, 0)); const modelsToTry = wholeChain.slice(startIndex); @@ -68,7 +60,6 @@ export async function runModelChain(ctx: ModelReviewContext, params: { }); } - // Guards chain head; see clampTimeoutToChainBudget. const timeoutMs = clampTimeoutToChainBudget(params.timeoutMs); const estimatedPromptTokens = estimatePromptTokens(systemPrompt, userPrompt); @@ -77,39 +68,39 @@ export async function runModelChain(ctx: ModelReviewContext, params: { let lastTransientError: unknown; let sawTransientFailure = false; let quotaFailures = 0; - // Prevents undefined lastError from failing files permanently. let attemptedAnyModel = false; - // Distinguishes rate-limit skips from timeouts for job logs. let skippedForTimeouts = false; - // Advances memo past failed models only; skips/429s never rule out the current model. let attemptedFailedThrough = 0; const chainStartedAt = Date.now(); - // Excluded from call timeout so busy gates don't manifest as slow models. let gateWaitMs = 0; const recordGateWait = (waitedMs: number) => { gateWaitMs += waitedMs; }; for (const [modelIndex, currentModel] of modelsToTry.entries()) { - // Primary guaranteed; fallbacks near 50-subrequest cap defer. + // Subrequest cap: primary always runs, fallbacks defer once near the limit. if (modelIndex > 0 && ctx.tracker?.isNearLimit()) { logger.warn(`Skipping remaining fallback models for ${label}; subrequest budget for this invocation is nearly exhausted`, { skippedModels: modelsToTry.slice(modelIndex), }); - // If no transient failures, let permanent error propagate. Avoid 'subrequest' keyword to ensure write. + // Avoid the word 'subrequest' here so the message isn't misread as a runtime refusal. if (sawTransientFailure) { lastTransientError = lastTransientError ?? lastError ?? new Error('Per-invocation request budget was nearly exhausted before trying all configured fallback models'); } break; } - // Prospective ~120s limit check to avoid doomed calls and CPU faults. Defers gracefully to fresh invocations. - if (modelIndex > 0 && Date.now() - chainStartedAt - gateWaitMs + timeoutMs > MODEL_FALLBACK_CHAIN_BUDGET_MS) { + const attemptTimeoutMs = chainAttemptTimeoutMs({ + requestedMs: timeoutMs, + remainingChainMs: MODEL_FALLBACK_CHAIN_BUDGET_MS - (Date.now() - chainStartedAt - gateWaitMs), + hasAnotherModel: modelIndex < modelsToTry.length - 1, + }); + + if (modelIndex > 0 && attemptTimeoutMs === 0) { logger.warn(`Deferring ${label}: no room in the per-invocation time budget for another model`, { elapsedMs: Date.now() - chainStartedAt, gateWaitMs, timeoutMs, skippedModels: modelsToTry.slice(modelIndex), }); - // Defer to retry on a fresh budget. sawTransientFailure = true; lastTransientError = lastTransientError ?? lastError ?? new Error(`Model fallback chain for ${label} exceeded its time budget; deferring for retry.`); break; @@ -131,7 +122,6 @@ export async function runModelChain(ctx: ModelReviewContext, params: { continue; } - // Skip models timing out consistently. Last candidates use a higher threshold to avoid "no model attempted" errors, but still cut off eventually. const isLastCandidate = modelIndex === modelsToTry.length - 1; const timingOut = isLastCandidate ? await ctx.chainProgress.isTimingOutTerminally(currentModel) @@ -144,7 +134,7 @@ export async function runModelChain(ctx: ModelReviewContext, params: { continue; } - // Hard subrequest floor for ALL models (including primary) prevents prompt transmissions into depleted invocations. + // Floor applies even to the primary model, not just fallbacks. if (ctx.tracker && !ctx.tracker.hasRemainingSubrequests(SUBREQUEST_HEADROOM_FOR_MODEL_CALL)) { logger.warn(`Deferring ${label}: not enough subrequest budget left to commit a prompt`, { subrequests: ctx.tracker.getSubrequestCount(), @@ -152,13 +142,11 @@ export async function runModelChain(ctx: ModelReviewContext, params: { skippedModels: modelsToTry.slice(modelIndex), }); sawTransientFailure = true; - // Avoid 'subrequest' keyword so error isn't mistaken for runtime refusal. lastTransientError = lastTransientError ?? lastError ?? new Error(`Per-invocation request budget was too low to attempt a model for ${label}; deferring for retry.`); break; } - // Pre-flight check against known limits. const skipReason = await ctx.rateLimits.skipReason(resolved.modelName, estimatedPromptTokens); if (skipReason) { logger.info(`Skipping ${currentModel} for ${label}: ${skipReason}`); @@ -166,13 +154,12 @@ export async function runModelChain(ctx: ModelReviewContext, params: { continue; } - // No intra-model retries here; outages defer the file or fall to next model. try { attemptedAnyModel = true; const response = await ctx.callResolvedModel( resolved, - { systemPrompt, userPrompt, responseSchema, outputBudgetTokens }, - timeoutMs, + { systemPrompt, userPrompt, responseSchema, outputBudgetTokens, truncationIntolerant }, + Math.max(attemptTimeoutMs, MODEL_MIN_VIABLE_ATTEMPT_MS), recordGateWait, ); @@ -180,20 +167,37 @@ export async function runModelChain(ctx: ModelReviewContext, params: { ctx.tracker.record(response.modelUsed, response.inputTokens, response.outputTokens); } - // Parse in try-block. `isLastModel` uses absolute chain length, protecting resumed jobs from premature acceptance. + // isLastModel uses the absolute chain index so resumed jobs don't accept prematurely. const parsed = params.parse(response.rawText, { isLastModel: startIndex + modelIndex >= wholeChain.length - 1, }); - // Clear strikes. No-op on healthy paths to avoid KV writes. await ctx.chainProgress.noteSuccess(currentModel); - // Terminal success; clear memo for clean future retries. await Promise.all(progressLabels.map((key) => ctx.chainProgress.clear(key))); - // Flush 429 cool-offs from earlier models in this chain to prevent re-paying them on next invocation. await ctx.chainProgress.flushPending(); return { ...response, userPrompt, parsed }; } catch (error) { + const isLastModel = startIndex + modelIndex >= wholeChain.length - 1; + // Salvage a truncated partial from the final model rather than fail outright. + const partial = isLastModel ? partialResponseOf(error) : null; + if (partial) { + try { + const parsed = params.parse(partial.rawText, { isLastModel: true }); + logger.warn(`Salvaged a truncated response from the last model in the chain for ${label}`, { + model: partial.modelUsed, + responseChars: partial.rawText.length, + }); + if (ctx.tracker) { + ctx.tracker.record(partial.modelUsed, partial.inputTokens, partial.outputTokens); + } + await Promise.all(progressLabels.map((key) => ctx.chainProgress.clear(key))); + await ctx.chainProgress.flushPending(); + return { ...partial, userPrompt, parsed, degraded: 'truncated' as const }; + } catch { + // intentional no-op + } + } + lastError = error; - // Record failed wire transmission. ctx.tracker?.recordFailedAttempt( resolved.modelName, estimatedPromptTokens, @@ -207,7 +211,7 @@ export async function runModelChain(ctx: ModelReviewContext, params: { await ctx.markProviderUnavailable(resolved.providerId, error instanceof Error ? error.message : String(error)); } - // Runtime refusal (out of subrequests). Aborts immediately to prevent cascading instant failures. Not recorded as chain progress so healthy models aren't skipped on retry. + // Out of subrequests: abort now (not recorded as progress, so healthy models aren't skipped on retry). if (isSubrequestBudgetMessage(error)) { logger.warn(`Aborting the model chain for ${label}; this invocation is out of subrequests`, { skippedModels: modelsToTry.slice(modelIndex + 1), @@ -217,7 +221,6 @@ export async function runModelChain(ctx: ModelReviewContext, params: { break; } - // Persist timeouts so subsequent waves avoid doomed models. if (isTimeoutMessage(String(error instanceof Error ? error.message : error).toLowerCase())) { await ctx.chainProgress.noteTimeout(currentModel); } @@ -226,18 +229,16 @@ export async function runModelChain(ctx: ModelReviewContext, params: { if (!rateLimited) attemptedFailedThrough = startIndex + modelIndex + 1; if (rateLimited) { quotaFailures += 1; - // Extract rate limit data to protect subsequent calls. ctx.rateLimits.note(resolved, error); } - // 429 defers rather than trying fallbacks to preserve subrequests. const outOfQuotaBudget = quotaFailures >= MAX_QUOTA_FAILURES_PER_FILE; logger.warn(`Model ${currentModel} failed for ${label}`, { error: error instanceof Error ? error.message : String(error), rateLimited, quotaFailures, - // `estimatedWastedInput` over `Tokens` since logger redacts 'token'. + // Named "Input" not "Tokens": the logger redacts fields containing "token". estimatedWastedInput: estimatedPromptTokens, willTryFallback: !outOfQuotaBudget && modelIndex < modelsToTry.length - 1, }); @@ -258,21 +259,18 @@ export async function runModelChain(ctx: ModelReviewContext, params: { retryCause, ); - // Advance progress past failed models for the retry. Skipped at chain's end so file retries start clean. + // Skipped once at chain's end, so a fresh retry starts clean rather than looping. if (attemptedFailedThrough > 0 && attemptedFailedThrough < wholeChain.length) { - // Coalesced writes to minimize KV get+puts from subrequest budget. await Promise.all(progressLabels.map((key) => ctx.chainProgress.advance(key, attemptedFailedThrough))); Object.defineProperty(error, 'nextChainIndex', { value: attemptedFailedThrough, configurable: true }); } else { - // Flush cool-offs learned before the quota deferral. await ctx.chainProgress.flushPending(); } throw error; } - // No model was called. Two very different reasons land here. if (!attemptedAnyModel) { - // Throw permanent operator errors (transient wrappers obscure root cause). + // Unwrap so a permanent operator error isn't hidden behind a transient wrapper. if (lastError !== undefined) { if (isTransientModelFailure(lastError)) { throw new RetryableModelError(`Every model for ${label} failed to resolve; retrying later.`, lastError); @@ -280,7 +278,6 @@ export async function runModelChain(ctx: ModelReviewContext, params: { throw lastError; } - // All models skipped via cool-downs, timeouts, or unavailability. throw new RetryableModelError( `No configured review model was attempted for ${label} (all skipped: ${ skippedForTimeouts ? 'repeated timeouts on this job' : 'rate-limit cooldown or provider unavailable' diff --git a/packages/models/src/internal/model-review-file.ts b/packages/models/src/internal/model-review-file.ts index 690f5540..8f73b830 100644 --- a/packages/models/src/internal/model-review-file.ts +++ b/packages/models/src/internal/model-review-file.ts @@ -4,29 +4,31 @@ import { buildFileReviewPrompts, buildReviewResponseSchema, type RejectedExemplar, -} from '@codra/core/prompts/file-review'; -import { isNonAnswerReview, parseBatchReviewResponse, parseFileReviewResponse, type BatchReviewResult } from '@codra/core/model-output'; +} from '@codraoss/core/prompts/file-review'; +import { isNonAnswerReview, parseBatchReviewResponse, parseFileReviewResponse, type BatchReviewResult } from '@codraoss/core/model-output'; import { UnparseableModelResponseError } from '../types'; -import { chunkFileDiff, type FileDiff } from '@codra/core/diff'; +import { chunkFileDiff, type FileDiff } from '@codraoss/core/diff'; import { adaptiveModelTimeoutMs, reviewOutputBudgetTokens } from '../limits'; -import { generatorFindingCap } from '@codra/core/prompts/file-review'; +import { reviewBreadth } from '@codraoss/core/prompts/file-review'; import { mergeCounts } from './model-support'; import { type ModelReviewContext, runModelChain } from './model-review-chain'; -import { logger } from '@codra/core/logger'; -import type { RepoConfig } from '@codra/schema'; +import { logger } from '@codraoss/core/logger'; +import type { RepoConfig } from '@codraoss/schema'; import type { ModelResponse } from '../types'; -// Import from the services/model barrel, not here (four specs vi.mock it). +// vi.mock targets services/model barrel; import model from there, not here. export const COMPACT_REVIEW_PROMPT_LINE_CAP = 400; -// Budget required before a chunk past BASE_CHUNKS runs, so a tail chunk only spends while another whole file still fits. +// Reserve so tail chunks only run if budget still fits another whole file. const EXTRA_CHUNK_BUDGET_RESERVE = 8; export type { ModelReviewContext }; export async function reviewFile(ctx: ModelReviewContext, params: { file: any; + fileContext?: string | null; prTitle: string | null; prDescription: string | null; + changelogExcerpt?: string | null; config: RepoConfig; totalLineCount: number; compactPrompt?: boolean; @@ -38,10 +40,8 @@ export async function reviewFile(ctx: ModelReviewContext, params: { : configuredLineCap; let chunks = chunkFileDiff(params.file, modelLineCap); - // Pre-cap count, so wasPromptTruncated doesn't re-run chunkFileDiff. const totalChunkCount = chunks.length; - // Past BASE_CHUNKS is opportunistic, only on spare budget. const BASE_CHUNKS = 4; const MAX_CHUNKS = 8; if (chunks.length > MAX_CHUNKS) { @@ -56,13 +56,13 @@ export async function reviewFile(ctx: ModelReviewContext, params: { const { path: filePath } = params.file; for (const [chunkIndex, chunk] of chunks.entries()) { - // No new chunk when close to the 50-subrequest limit. + // isNearLimit guards the ~50-subrequest cap. if (results.length > 0 && ctx.tracker?.isNearLimit()) { logger.warn(`Stopping chunk processing for ${filePath} early due to subrequest budget limits.`); break; } - // Needs spare budget, not merely remaining: isNearLimit() only fires once in-flight files are already starved. + // Needs spare budget, not just remaining: isNearLimit only trips once already starved. if (chunkIndex >= BASE_CHUNKS) { const remaining = ctx.tracker?.remainingSafeBudget() ?? Number.POSITIVE_INFINITY; if (remaining < EXTRA_CHUNK_BUDGET_RESERVE) { @@ -81,7 +81,7 @@ export async function reviewFile(ctx: ModelReviewContext, params: { results.push(res as any); } catch (error) { if (results.length === 0) { - throw error; // First chunk failed, let it defer/fail properly + throw error; } logger.warn(`Chunk review failed for ${filePath}, returning partial results to avoid stalling the job.`, { error: error instanceof Error ? error.message : String(error) }); break; @@ -89,7 +89,7 @@ export async function reviewFile(ctx: ModelReviewContext, params: { } const combinedFindings = results.flatMap(r => r.parsed.comments); - // Most serious chunk's verdict, not the last: a clean final chunk would mask earlier findings. + // Most serious verdict, not last: a clean final chunk would mask earlier findings. const primaryResult = results.find(r => r.parsed.verdict === 'comment') ?? results[results.length - 1]; return { @@ -99,14 +99,14 @@ export async function reviewFile(ctx: ModelReviewContext, params: { parsed: { ...primaryResult.parsed, comments: combinedFindings, - // Summed across chunks, or a truncated file under-reports the "N claims withheld" note. evidenceStats: results.reduce((acc, r) => ({ total: acc.total + (r.parsed.evidenceStats?.total ?? 0), matched: acc.matched + (r.parsed.evidenceStats?.matched ?? 0), unmatched: acc.unmatched + (r.parsed.evidenceStats?.unmatched ?? 0), weak: acc.weak + (r.parsed.evidenceStats?.weak ?? 0), absent: acc.absent + (r.parsed.evidenceStats?.absent ?? 0), - }), { total: 0, matched: 0, unmatched: 0, weak: 0, absent: 0 }), + contextOnly: acc.contextOnly + (r.parsed.evidenceStats?.contextOnly ?? 0), + }), { total: 0, matched: 0, unmatched: 0, weak: 0, absent: 0, contextOnly: 0 }), claimTypeCounts: mergeCounts(results.map((r) => r.parsed.claimTypeCounts)), deniedClaimCounts: mergeCounts(results.map((r) => r.parsed.deniedClaimCounts)), absenceCheckStats: results.reduce((acc, r) => ({ @@ -117,15 +117,17 @@ export async function reviewFile(ctx: ModelReviewContext, params: { }, reviewedLineCount: results.reduce((sum, r) => sum + r.reviewedLineCount, 0), wasPromptTruncated: chunks.length < totalChunkCount || results.length < chunks.length, + degraded: results.find((r) => r.degraded)?.degraded, }; } -// Internal to this module: reviewFile fans out to it per chunk. async function reviewFileChunk(ctx: ModelReviewContext, params: { file: any; + fileContext?: string | null; prTitle: string | null; prDescription: string | null; + changelogExcerpt?: string | null; config: RepoConfig; totalLineCount: number; compactPrompt?: boolean; @@ -138,31 +140,29 @@ async function reviewFileChunk(ctx: ModelReviewContext, params: { rejectedExemplars: params.rejectedExemplars, }); - // One figure drives three things: the room the answer gets, and now the time it gets to write it. const outputBudgetTokens = reviewOutputBudgetTokens({ - findingCap: generatorFindingCap(params.config.review.max_comments), + findingCap: reviewBreadth(params.config.review), fileCount: 1, }); const response = await runModelChain(ctx, { systemPrompt, userPrompt, - responseSchema: buildReviewResponseSchema(params.config.review.max_comments), - // Scales with the diff the model sees AND the answer it was asked for: small files fail over fast. + responseSchema: buildReviewResponseSchema(reviewBreadth(params.config.review)), timeoutMs: adaptiveModelTimeoutMs(params.file.lineCount, outputBudgetTokens), outputBudgetTokens, + // Partial output reads as "no findings left", not truncation; treat as untrusted. + truncationIntolerant: true, label: params.file.path, totalLineCount: params.totalLineCount, config: params.config, parse: (rawText, { isLastModel }) => { const parsed = parseFileReviewResponse(rawText, params.file, { deniedClaimTypes: params.config.review.deny_claim_types, + fileContent: params.fileContext, }); - // A substantive diff waved through in one sentence is not a clean verdict, it is a model declining - // to review. Thrown as UnparseableModelResponseError so the chain treats it exactly like any other - // useless response and tries the next entry -- and never on the last one, where the alternative to - // an unearned "clean" is failing the file, which is worse. + // A one-sentence reply to a substantive diff means the model declined; escalate except on the last model, where failing beats an unearned clean. if (!isLastModel && isNonAnswerReview({ rawText, file: params.file, @@ -195,11 +195,12 @@ export type BatchReviewOutcome = ModelResponse & { userPrompt: string; }; -// Caller fans the result out to per-file rows and must not record `batch.missing` as reviewed. +// Caller must not record batch.missing as reviewed when fanning out to file rows. export async function reviewFiles(ctx: ModelReviewContext, params: { files: readonly FileDiff[]; prTitle: string | null; prDescription: string | null; + changelogExcerpt?: string | null; config: RepoConfig; totalLineCount: number; rejectedExemplars?: readonly RejectedExemplar[]; @@ -208,36 +209,34 @@ export async function reviewFiles(ctx: ModelReviewContext, params: { files: params.files, prTitle: params.prTitle, prDescription: params.prDescription, + changelogExcerpt: params.changelogExcerpt, config: params.config.review, rejectedExemplars: params.rejectedExemplars, }); - // The bin's total: a 400-line bin on a small-file timeout dies mid-call and takes all of it down. + // Bin's total lines; a small-file timeout on a 400-line bin kills the whole call. const binLineCount = params.files.reduce((sum, file) => sum + file.lineCount, 0); - // The bin's whole response, not one file's: every entry shares one `maxOutputTokens`, and a bin that - // overruns it comes back as a repaired prefix with its tail files looking clean. A packed bin is also - // the slowest call the system makes, and its diff line count badly under-predicts that, so the same - // figure sizes the timeout. + // Whole-bin response shares one maxOutputTokens (overrun leaves tail files looking falsely clean); same figure sizes the timeout since a packed bin is the slowest call. const outputBudgetTokens = reviewOutputBudgetTokens({ - findingCap: generatorFindingCap(params.config.review.max_comments), + findingCap: reviewBreadth(params.config.review), fileCount: params.files.length, }); const response = await runModelChain(ctx, { systemPrompt, userPrompt, - responseSchema: buildBatchReviewResponseSchema(params.config.review.max_comments, params.files.length), + responseSchema: buildBatchReviewResponseSchema(reviewBreadth(params.config.review), params.files.length), timeoutMs: adaptiveModelTimeoutMs(binLineCount, outputBudgetTokens), outputBudgetTokens, + truncationIntolerant: true, label: `${params.files.length} files (${params.files[0]?.path ?? 'unknown'} …)`, - // Per file, so progress survives the bin narrowing or exploding into singles. progressLabels: params.files.map((file) => file.path), totalLineCount: params.totalLineCount, config: params.config, parse: (rawText) => parseBatchReviewResponse(rawText, params.files, { deniedClaimTypes: params.config.review.deny_claim_types, - maxCommentsPerFile: params.config.review.max_comments, + maxCommentsPerFile: reviewBreadth(params.config.review), }), }); diff --git a/packages/models/src/internal/model-support.ts b/packages/models/src/internal/model-support.ts index 9ab9e7b4..d767cbc9 100644 --- a/packages/models/src/internal/model-support.ts +++ b/packages/models/src/internal/model-support.ts @@ -1,10 +1,8 @@ -import { normalizeModelId } from '@codra/schema'; -import { isTimeoutMessage, matchesAnyTransientSubstring } from '@codra/schema/transient-errors'; +import { normalizeModelId } from '@codraoss/schema'; +import { isTimeoutMessage, matchesAnyTransientSubstring } from '@codraoss/schema/transient-errors'; import { UnparseableModelResponseError } from '../types'; -// Model service pure helpers: aliases, prompt sizes, rate limits, errors. - -// Legacy ID rewrites (applied before resolution). Hook for future aliases. +// Hook for future legacy ID rewrites, applied before resolution. const MODEL_ALIASES: Record = {}; export function mergeCounts(sources: Array | undefined>): Record { @@ -17,52 +15,45 @@ export function mergeCounts(sources: Array | undefined>): return merged; } -// Rough 4-chars/token estimate to preempt doomed calls. Overestimating safely routes onward. +// 4 chars/token estimate; overestimating is safe. export function estimatePromptTokens(systemPrompt: string, userPrompt: string): number { return Math.ceil((systemPrompt.length + userPrompt.length) / 4); } -// Headroom required before committing prompts to metered models. export const PROMPT_FIT_SAFETY_FACTOR = 0.8; -// Minimum plausible token bucket. Rejects misparsed small numbers (like request quotas) to prevent jobs from permanently blocking valid prompts. +// Floor to reject misparsed request quotas, not real token buckets. export const MIN_PLAUSIBLE_TOKEN_BUCKET = 1_000; export function isPlausibleTokenBucket(limitTokens: number | undefined): boolean { return typeof limitTokens === 'number' && limitTokens >= MIN_PLAUSIBLE_TOKEN_BUCKET; } -// Extracted nextChainIndex from deferrals. Lets callers distinguish "progress made" from "same failures". Lives here to avoid vi.mock TypeError in specs. +// Lives here (not with callers) to avoid vi.mock TypeError in specs. export function nextChainIndexOf(error: unknown): number | null { const value = (error as { nextChainIndex?: unknown } | null)?.nextChainIndex; return typeof value === 'number' && Number.isInteger(value) && value > 0 ? value : null; } -// Detects if Gemini adapter dropped grammar before throwing. Lets callers latch the schema-dropped state. export function isSchemaDroppedError(error: unknown): boolean { return (error as { schemaDropped?: unknown } | null)?.schemaDropped === true; } -// Max serialized queue depth before routing elsewhere, avoiding budget exhaustion while waiting. export const MAX_METERED_QUEUE_DEPTH = 2; -// Extracts metric/limit pairs from 429 bodies (may contain multiple). const QUOTA_VIOLATION_PATTERN = /metric:\s*(\S+?),\s*limit:\s*(\d[\d_,]*)/gi; -// Token metrics only. Prevents parsing request quotas (e.g. limit: 15) as token buckets, which would permanently disable the model. +// Excludes request-count quotas, which would otherwise disable the model. const TOKEN_QUOTA_METRIC = /(?:input_token|output_token|token_count|_tokens)/i; -// Extracts limit/retry from 429 bodies ("limit: 16000... retry in 26.9s"). export function parseRateLimitFromError(error: unknown): { limitTokens?: number; retryAfterMs?: number } { const message = error instanceof Error ? error.message : String(error ?? ''); - // Avoid bare limits; first stated limit might be request count. let limitTokens: number | undefined; for (const [, metric, limit] of message.matchAll(QUOTA_VIOLATION_PATTERN)) { if (!TOKEN_QUOTA_METRIC.test(metric)) continue; const parsed = Number(limit.replace(/[_,]/g, '')); if (!Number.isFinite(parsed) || !isPlausibleTokenBucket(parsed)) continue; - // Smallest valid token bucket wins. if (limitTokens === undefined || parsed < limitTokens) limitTokens = parsed; } @@ -121,7 +112,7 @@ export function isGoogleRateLimitError(error: unknown) { export function isTransientModelFailure(error: unknown) { if (isRetryableModelError(error)) return true; - // Deterministic unparseable output (reasoning-only/truncated) is non-retryable. + // Unparseable output is deterministic, not transient. if (error instanceof UnparseableModelResponseError) return false; if (isCloudflareAllocationError(error)) return false; const message = error instanceof Error ? error.message : String(error); @@ -137,7 +128,6 @@ export function isTransientModelFailure(error: unknown) { lower.includes('fetch failed') || lower.includes('network') || lower.includes('temporar') || - // Upstream 5xx is transient; defer rather than failing. /\b50[0-9]\b/.test(lower) || lower.includes('internal error') ); diff --git a/packages/models/src/limits.ts b/packages/models/src/limits.ts index 0bf8b144..2f31c352 100644 --- a/packages/models/src/limits.ts +++ b/packages/models/src/limits.ts @@ -1,20 +1,14 @@ -// Free-plan constraints: 6 concurrent connections max, 50 subrequests per invocation. - -// Base timeout for small diffs (~1-5s response expected). export const MODEL_TIMEOUT_BASE_MS = 20_000; const MODEL_TIMEOUT_PER_LINE_MS = 100; const MODEL_TIMEOUT_FREE_LINES = 100; -// Hard ceiling (max 50s) avoids 120s `exceededCpu` runtime limits, allowing failovers. +// Ceiling stays under 120s exceededCpu limit, leaving room to fail over. export const MODEL_TIMEOUT_MAX_MS = 50_000; -// File's total fallback chain budget. Exceeding defers the file to a fresh invocation. -// Sized slightly above MODEL_TIMEOUT_MAX_MS to give big diffs the full ceiling on a single model. +// Slightly above MODEL_TIMEOUT_MAX_MS so a large diff can use the full ceiling before deferring. export const MODEL_FALLBACK_CHAIN_BUDGET_MS = 55_000; -// Scaled timeout buffer based on requested output tokens (1200ms per 1k tokens) to accommodate model generation time. -const MODEL_TIMEOUT_PER_1K_OUTPUT_MS = 1_200; +export const MODEL_TIMEOUT_PER_1K_OUTPUT_MS = 1_200; -// Per-call timeout, scaled by diff size and expected output budget. export function adaptiveModelTimeoutMs( diffLineCount: number | null | undefined, outputBudgetTokens?: number | null, @@ -25,28 +19,50 @@ export function adaptiveModelTimeoutMs( const budget = typeof outputBudgetTokens === 'number' && Number.isFinite(outputBudgetTokens) ? Math.max(0, outputBudgetTokens) : 0; - // Only the room ABOVE the floor earns extra time: every caller asks for at least the floor. const answerAllowance = Math.max(0, budget - OUTPUT_TOKENS_FLOOR) / 1_000 * MODEL_TIMEOUT_PER_1K_OUTPUT_MS; return Math.min(MODEL_TIMEOUT_MAX_MS, scaled + answerAllowance); } -// Clamps timeout so single calls never exceed the chain budget and loop endlessly without running. +// Per-candidate, not the old `candidates * 8` diff-line proxy: 12 findings fell under the 100-line free allowance and collapsed to the 20s base, so verification timed out and later chain rungs were skipped. +export const VERIFY_TIMEOUT_FLOOR_MS = 30_000; +const VERIFY_TIMEOUT_FREE_CANDIDATES = 10; +const VERIFY_TIMEOUT_PER_CANDIDATE_MS = 1_200; + +export function verifyTimeoutMs(candidateCount: number): number { + const extra = Math.max(0, candidateCount - VERIFY_TIMEOUT_FREE_CANDIDATES); + return Math.min(MODEL_TIMEOUT_MAX_MS, VERIFY_TIMEOUT_FLOOR_MS + extra * VERIFY_TIMEOUT_PER_CANDIDATE_MS); +} + export function clampTimeoutToChainBudget(timeoutMs: number): number { return Math.min(timeoutMs, MODEL_FALLBACK_CHAIN_BUDGET_MS); } -// Max 3 calls limits connection pool (out of 6 max) to leave slots for KV/GitHub. +export const MODEL_MIN_VIABLE_ATTEMPT_MS = 8_000; + +export const MODEL_FALLBACK_RESERVE_MS = 20_000; + +// Returns 0 to defer the file to a fresh invocation. +export function chainAttemptTimeoutMs(input: { + requestedMs: number; + remainingChainMs: number; + hasAnotherModel: boolean; +}): number { + const { requestedMs, remainingChainMs, hasAnotherModel } = input; + if (remainingChainMs < MODEL_MIN_VIABLE_ATTEMPT_MS) return 0; + if (!hasAnotherModel) return Math.min(requestedMs, remainingChainMs); + + const withReserve = remainingChainMs - MODEL_FALLBACK_RESERVE_MS; + return Math.min(requestedMs, withReserve >= MODEL_MIN_VIABLE_ATTEMPT_MS ? withReserve : remainingChainMs); +} + +// 3 of the 6 pool connections reserved for KV/GitHub. export const MAX_CONCURRENT_MODEL_CALLS = 3; -// Token cost of a single finding (JSON structure). Generous to avoid silent truncations. const OUTPUT_TOKENS_PER_FINDING = 340; -// Tokens per file entry in batch response. const OUTPUT_TOKENS_PER_FILE_ENTRY = 160; -// Enough for the verify/summary paths and any caller that states no budget. export const OUTPUT_TOKENS_FLOOR = 8_192; -// Output budget sizing (excludes reasoning tokens). Driven by requested capacity (findings * files). export function reviewOutputBudgetTokens(input: { findingCap: number; fileCount: number }): number { const files = Math.max(1, input.fileCount); const findings = Math.max(1, input.findingCap) * files; @@ -56,7 +72,6 @@ export function reviewOutputBudgetTokens(input: { findingCap: number; fileCount: ); } -// Clamps output budget to provider maximums. export function resolveOutputTokenCeiling( requested: number | undefined, providerMax: number, @@ -68,25 +83,22 @@ export function resolveOutputTokenCeiling( return Math.min(providerMax, Math.max(providerDefault, Math.ceil(requested))); } -// Gemini 2.5 reasoning budget (1024-8192 bounds). Must be explicitly limited since it counts against maxOutputTokens. +// Gemini thinking budget counts against maxOutputTokens, so it must stay bounded to 1024-8192. export function geminiThinkingBudgetTokens(answerBudgetTokens: number): number { return Math.min(8_192, Math.max(1_024, Math.floor(answerBudgetTokens / 4))); } -// Actual subrequests cost per attempt (allows for adapter retries/fallback). const SUBREQUESTS_PER_MODEL_ATTEMPT = 3; -// Subrequest headroom before calling models. Multiplied by concurrency cap to ensure pool can execute. export const SUBREQUEST_HEADROOM_FOR_MODEL_CALL = SUBREQUESTS_PER_MODEL_ATTEMPT * MAX_CONCURRENT_MODEL_CALLS; -// FIFO semaphore: waits here don't eat into the caller's timeout budget. +// Queue wait time is excluded from the caller's timeout budget. export class ModelCallGate { private active = 0; private readonly waiters: Array<() => void> = []; constructor(private readonly limit = MAX_CONCURRENT_MODEL_CALLS) {} - // onAcquired tracks wait time, preventing busy queues from skewing model latency metrics. async run(fn: () => Promise, onAcquired?: (waitedMs: number) => void): Promise { const startedWaiting = Date.now(); await this.acquire(); @@ -111,7 +123,6 @@ export class ModelCallGate { } private release() { - // Fair release to next waiter. const next = this.waiters.shift(); if (next) { next(); diff --git a/packages/models/src/llm-crypto.ts b/packages/models/src/llm-crypto.ts index ca01e8ed..131fc32b 100644 --- a/packages/models/src/llm-crypto.ts +++ b/packages/models/src/llm-crypto.ts @@ -1,4 +1,4 @@ -import type { SecretStore } from '@codra/core/ports'; +import type { SecretStore } from '@codraoss/core/ports'; const KEY_VERSION = 'v1'; const encoder = new TextEncoder(); diff --git a/packages/models/src/providers/anthropic.ts b/packages/models/src/providers/anthropic.ts index 679a8c70..e4fb8159 100644 --- a/packages/models/src/providers/anthropic.ts +++ b/packages/models/src/providers/anthropic.ts @@ -1,5 +1,5 @@ -import { logger } from '@codra/core/logger'; -import { withTimeout } from '@codra/core/timeout'; +import { logger } from '@codraoss/core/logger'; +import { withTimeout } from '@codraoss/core/timeout'; import { ProviderRequestError, providerErrorMessage, jsonOnlyPrompts, type ModelResponse } from '../types'; import { assertPublicBaseUrl } from '../url-guard'; import { MODEL_TIMEOUT_MAX_MS, resolveOutputTokenCeiling } from '../limits'; diff --git a/packages/models/src/providers/cloudflare.ts b/packages/models/src/providers/cloudflare.ts index f91884f2..8381267b 100644 --- a/packages/models/src/providers/cloudflare.ts +++ b/packages/models/src/providers/cloudflare.ts @@ -1,6 +1,6 @@ -import { logger } from '@codra/core/logger'; +import { logger } from '@codraoss/core/logger'; -import { TimeoutError } from '@codra/core/timeout'; +import { TimeoutError } from '@codraoss/core/timeout'; import { ProviderRequestError, UnparseableModelResponseError, jsonOnlyPrompts, type ModelInput, type ModelResponse } from '../types'; import { MODEL_TIMEOUT_MAX_MS, OUTPUT_TOKENS_FLOOR, resolveOutputTokenCeiling } from '../limits'; diff --git a/packages/models/src/providers/google.ts b/packages/models/src/providers/google.ts index a59cc9b2..f2857256 100644 --- a/packages/models/src/providers/google.ts +++ b/packages/models/src/providers/google.ts @@ -1,33 +1,30 @@ -import { logger } from '@codra/core/logger'; -import { withTimeout } from '@codra/core/timeout'; -import { ProviderRequestError, UnparseableModelResponseError, providerErrorMessage, jsonOnlyPrompts, type ModelInput, type ModelResponse } from '../types'; +import { logger } from '@codraoss/core/logger'; +import { withTimeout } from '@codraoss/core/timeout'; +import { ProviderRequestError, UnparseableModelResponseError, providerErrorMessage, jsonOnlyPrompts, isThinkingRejection, attachPartialResponse, type ModelInput, type ModelResponse } from '../types'; import { toGeminiResponseJsonSchema } from '../gemini-schema'; import { assertPublicBaseUrl } from '../url-guard'; import { MODEL_TIMEOUT_MAX_MS, + MODEL_TIMEOUT_PER_1K_OUTPUT_MS, OUTPUT_TOKENS_FLOOR, geminiThinkingBudgetTokens, resolveOutputTokenCeiling, } from '../limits'; -/** Fallback timeout if caller omits budget. */ const GEMINI_TIMEOUT_MS = MODEL_TIMEOUT_MAX_MS; const GEMINI_MAX_RETRIES = 2; -// Output floor for low-budget tasks (verify, summary). const GEMINI_DEFAULT_OUTPUT_TOKENS = OUTPUT_TOKENS_FLOOR; -// Max output claims. 65k allows room for thinking tokens and dense multi-file bins. +// 65k leaves room for thinking tokens plus dense multi-file output. const GEMINI_MAX_OUTPUT_TOKENS = 65_536; -// Cap on retry sleeps; longer cool-offs defer files to free up gates. const GEMINI_MAX_RETRY_DELAY_MS = 5_000; const DEFAULT_GEMINI_BASE_URL = 'https://generativelanguage.googleapis.com/v1beta'; -// 429 handled separately (only retryable if cool-off is stated). +// 429 handled separately; only retryable if a cool-off is stated. function isRetryableGeminiStatus(status: number) { return status === 408 || status === 500 || status === 502 || status === 503 || status === 504 || status === 524; } function defaultRetryDelayMs(attempt: number) { - // Exponential backoff for transient 5xx errors (clears quickly). return Math.pow(2, attempt) * 800 + Math.random() * 400; } @@ -46,7 +43,6 @@ function retryAfterDelayMs(value: string | null) { return null; } -// Extract body cool-off ("Please retry in Xs.") to avoid indefinite 429 retries. function requestedRetryDelayFromBody(message: string): number | null { const match = /retry in ([\d.]+)s/i.exec(message); if (!match) return null; @@ -54,33 +50,34 @@ function requestedRetryDelayFromBody(message: string): number | null { return Number.isFinite(seconds) ? seconds * 1000 : null; } -// Broad matcher (false positives cost 1 subrequest; false negatives break chains). -function isSchemaRejection(status: number, message: string) { - if (status !== 400) return false; +export function classifySchemaRejection(status: number, message: string): 'confident' | 'catchall' | null { + if (status !== 400) return null; const lower = message.toLowerCase(); - return ( + + const namesTheGrammar = lower.includes('responsejsonschema') || lower.includes('response_json_schema') || lower.includes('responseschema') || lower.includes('response_schema') || lower.includes('invalid json payload') || lower.includes('unknown name') || - lower.includes('schema') || - // Bare 400 catch-all to prevent permanent schema failures. Worst case: one extra failed schema-less attempt. - lower.includes('invalid argument') - ); -} + lower.includes('schema'); + if (namesTheGrammar) return 'confident'; -// Narrow matcher probed BEFORE isSchemaRejection to prevent misidentifying thinking-config refusals as schema drops. -function isThinkingRejection(status: number, message: string) { - if (status !== 400) return false; - const lower = message.toLowerCase(); - return lower.includes('thinking') || lower.includes('thought'); + const grammarAdjacent = + lower.includes('generation_config') || + lower.includes('generationconfig') || + lower.includes('json') || + lower.includes('constrained') || + lower.includes('too many states'); + if (lower.includes('invalid argument') && grammarAdjacent) return 'catchall'; + + return null; } function isRetryableTransportError(error: unknown) { if (!(error instanceof Error)) return false; - // Don't retry timeouts (caller grants up to 2m); defer to fallback chains. + // Skip retrying timeouts (caller already grants up to 2m); defer to fallback chain. if (error.name === 'TimeoutError' || error.message.toLowerCase().includes('timed out')) return false; if (error.message.includes('fetch failed')) return true; return error instanceof TypeError; @@ -100,21 +97,21 @@ export async function reviewWithGoogle( const responseJsonSchema = input.responseSchema ? toGeminiResponseJsonSchema(input.responseSchema.schema) : null; - // Latches to disable features on subsequent attempts if rejected. let schemaRejected = false; + let schemaRejectionBranch: 'confident' | 'catchall' = 'confident'; let thinkingRejected = false; - // Summing JSON and thinking token budgets prevents truncated prefixes. const answerBudget = resolveOutputTokenCeiling( input.outputBudgetTokens, GEMINI_MAX_OUTPUT_TOKENS, GEMINI_DEFAULT_OUTPUT_TOKENS, ); const thinkingBudget = geminiThinkingBudgetTokens(answerBudget); - const outputCeiling = Math.min(GEMINI_MAX_OUTPUT_TOKENS, answerBudget + thinkingBudget); - // Mark error so caller latches schema-dropped state even on subsequent failure. + let currentCeiling = Math.min(GEMINI_MAX_OUTPUT_TOKENS, answerBudget + thinkingBudget); + let ceilingRaised = false; const fail = (error: unknown): never => { - if (schemaRejected && typeof error === 'object' && error !== null) { + // Confident rejections only: a probe that failed anyway proves nothing, and latching would strip the schema from every later call in the job. A successful probe latches via `degraded` instead. + if (schemaRejected && schemaRejectionBranch === 'confident' && typeof error === 'object' && error !== null) { Object.defineProperty(error, 'schemaDropped', { value: true, configurable: true }); } throw error; @@ -156,14 +153,11 @@ export async function reviewWithGoogle( { role: 'user', parts: [{ text: prompts.user }] }, ], generationConfig: { - // Required for schemas and summary path. responseMimeType: 'application/json', - // See gemini-schema.ts. ...(responseJsonSchema && !schemaRejected ? { responseJsonSchema } : {}), - maxOutputTokens: outputCeiling, - // Bounded thinking budget so it doesn't consume the output ceiling. + maxOutputTokens: currentCeiling, ...(thinkingRejected ? {} : { thinkingConfig: { thinkingBudget } }), - // 0.9 on Gemini's 0-2 scale. + // Gemini's temperature scale is 0-2, not 0-1. temperature: 0.9, }, }), @@ -182,7 +176,7 @@ export async function reviewWithGoogle( const errorText = await response.text(); const message = providerErrorMessage(errorText); - // Check thinking first; isSchemaRejection is broad. + // Check thinking rejection first; isSchemaRejection below is broad. if (!thinkingRejected && isThinkingRejection(response.status, message)) { thinkingRejected = true; logger.warn('Gemini rejected thinkingConfig; retrying without an explicit thinking budget', { @@ -190,28 +184,56 @@ export async function reviewWithGoogle( error: message, }); lastError = new ProviderRequestError(config.providerName ?? 'Google', response.status, message); - // Refund attempt (no sleep); latched. attempt--; continue; } - if (responseJsonSchema && !schemaRejected && isSchemaRejection(response.status, message)) { + const schemaRejection = responseJsonSchema && !schemaRejected + ? classifySchemaRejection(response.status, message) + : null; + if (schemaRejection) { schemaRejected = true; - // Inferred schema rejection; real cause thrown below if 400 recurs. + schemaRejectionBranch = schemaRejection; + // Inferred from message; real cause surfaces below if 400 recurs. logger.warn('Gemini returned a 400 that looks like a response-grammar rejection; retrying without constrained decoding', { model, + branch: schemaRejection, error: message, }); lastError = new ProviderRequestError(config.providerName ?? 'Google', response.status, message); - // Refund attempt (no sleep); latched. attempt--; continue; } + // Unexplained invalid-argument 400: strip optional features one at a time -- grammar, then thinking budget -- refunding the attempt each time. The latches bound this ladder to two extra probes. + if (response.status === 400 && /invalid argument/i.test(message)) { + if (responseJsonSchema && !schemaRejected) { + schemaRejected = true; + schemaRejectionBranch = 'catchall'; + logger.warn('Gemini returned an unexplained 400; probing without constrained decoding', { + model, + error: message, + }); + lastError = new ProviderRequestError(config.providerName ?? 'Google', response.status, message); + attempt--; + continue; + } + if (!thinkingRejected) { + thinkingRejected = true; + logger.warn('Gemini returned an unexplained 400 with the grammar already off; probing without an explicit thinking budget', { + model, + error: message, + }); + lastError = new ProviderRequestError(config.providerName ?? 'Google', response.status, message); + attempt--; + continue; + } + } + const requestedDelayMs = response.status === 429 ? retryAfterDelayMs(response.headers.get('retry-after')) ?? requestedRetryDelayFromBody(message) : null; - // Unstated 429s back-off for ~60s, making them unretryable here. Retry only on short, stated cool-offs. + // Unstated 429s back off ~60s, making them unretryable here; retry only short, stated cool-offs. const isRetryable = response.status === 429 ? requestedDelayMs !== null && requestedDelayMs <= GEMINI_MAX_RETRY_DELAY_MS : isRetryableGeminiStatus(response.status); @@ -226,7 +248,6 @@ export async function reviewWithGoogle( willRetry: isRetryable && attempt < maxRetries, requestedDelayMs: requestedDelayMs ?? undefined, retryDelayMs: isRetryable && attempt < maxRetries ? retryDelayMs : undefined, - // Log bounded raw body for terminal 4xx to debug unactionable "invalid argument" errors. rawBody: response.status >= 400 && response.status < 500 && !(isRetryable && attempt < maxRetries) ? errorText.slice(0, 2_000) : undefined, @@ -250,32 +271,64 @@ export async function reviewWithGoogle( usageMetadata?: { promptTokenCount?: number; candidatesTokenCount?: number; - // Billed against `maxOutputTokens`. + // Billed against maxOutputTokens. thoughtsTokenCount?: number; }; }; const candidate = data.candidates?.[0]; const rawText = candidate?.content?.parts?.map((part) => part.text ?? '').join('')?.trim(); + const finishReason = candidate?.finishReason; + const truncated = finishReason === 'MAX_TOKENS'; + + if (finishReason && finishReason !== 'STOP') { + logger.warn(`Gemini response for ${model} ended with finishReason=${finishReason}; output is likely incomplete`, { + // Avoid a `Tokens` key name; the logger redacts it. + outputSpend: (data.usageMetadata?.candidatesTokenCount ?? 0) + (data.usageMetadata?.thoughtsTokenCount ?? 0), + thoughtSpend: data.usageMetadata?.thoughtsTokenCount ?? 0, + outputCeiling: currentCeiling, + thinkingBudget: thinkingRejected ? undefined : thinkingBudget, + schemaDropped: schemaRejected, + }); + } + + if (truncated && input.truncationIntolerant && !ceilingRaised) { + const elapsed = Date.now() - startTime; + const raisedCeiling = Math.min(GEMINI_MAX_OUTPUT_TOKENS, 2 * answerBudget + thinkingBudget); + const extraMs = ((raisedCeiling - currentCeiling) / 1_000) * MODEL_TIMEOUT_PER_1K_OUTPUT_MS; + + if (elapsed + extraMs < timeoutMs) { + ceilingRaised = true; + currentCeiling = raisedCeiling; + logger.warn(`Gemini ran out of output room on ${model}; resending once with a larger ceiling`, { + outputCeiling: raisedCeiling, + thoughtSpend: data.usageMetadata?.thoughtsTokenCount ?? 0, + hadPartialText: Boolean(rawText), + }); + attempt--; + continue; + } + } + if (!rawText) { - const finishReason = candidate?.finishReason; - // Deterministic non-STOP (budget burn, safety) fails permanently; empty STOP is transient. + // Non-STOP finish fails permanently; empty STOP is transient. if (finishReason && finishReason !== 'STOP') { return fail(new UnparseableModelResponseError(model, `finishReason=${finishReason}`)); } return fail(new Error('Gemini returned an empty response.')); } - // Log non-STOP prefix truncations. - if (candidate?.finishReason && candidate.finishReason !== 'STOP') { - logger.warn(`Gemini response for ${model} ended with finishReason=${candidate.finishReason}; output is likely incomplete`, { - // Avoid `Tokens` key name to bypass logger redaction. Sum thinking + output spend. - outputSpend: (data.usageMetadata?.candidatesTokenCount ?? 0) + (data.usageMetadata?.thoughtsTokenCount ?? 0), - thoughtSpend: data.usageMetadata?.thoughtsTokenCount ?? 0, - outputCeiling, - thinkingBudget: thinkingRejected ? undefined : thinkingBudget, - schemaDropped: schemaRejected, + // Attach partial text so a later fallback model can salvage it. + if (truncated && input.truncationIntolerant) { + const error = new UnparseableModelResponseError(model, 'finishReason=MAX_TOKENS'); + attachPartialResponse(error, { + rawText, + inputTokens: data.usageMetadata?.promptTokenCount ?? 0, + outputTokens: data.usageMetadata?.candidatesTokenCount ?? 0, + modelUsed: model, + provider: config.providerName ?? 'Google', }); + return fail(error); } return { @@ -284,7 +337,11 @@ export async function reviewWithGoogle( outputTokens: data.usageMetadata?.candidatesTokenCount ?? 0, modelUsed: model, provider: config.providerName ?? 'Google', - ...(schemaRejected ? { degraded: 'schema-dropped' as const } : {}), + ...(schemaRejected + ? { degraded: schemaRejectionBranch === 'catchall' + ? ('schema-dropped-catchall' as const) + : ('schema-dropped' as const) } + : {}), }; } diff --git a/packages/models/src/providers/openai.ts b/packages/models/src/providers/openai.ts index 866c386e..4c5e1a85 100644 --- a/packages/models/src/providers/openai.ts +++ b/packages/models/src/providers/openai.ts @@ -1,5 +1,5 @@ -import { logger } from '@codra/core/logger'; -import { withTimeout } from '@codra/core/timeout'; +import { logger } from '@codraoss/core/logger'; +import { withTimeout } from '@codraoss/core/timeout'; import { ProviderRequestError, providerErrorMessage, jsonOnlyPrompts, type ModelResponse } from '../types'; import { assertPublicBaseUrl } from '../url-guard'; import { MODEL_TIMEOUT_MAX_MS, resolveOutputTokenCeiling } from '../limits'; diff --git a/packages/models/src/providers/vertex.ts b/packages/models/src/providers/vertex.ts index 8a9fb201..3dd77bcb 100644 --- a/packages/models/src/providers/vertex.ts +++ b/packages/models/src/providers/vertex.ts @@ -1,14 +1,20 @@ -import { logger } from '@codra/core/logger'; -import { withTimeout } from '@codra/core/timeout'; -import { ProviderRequestError, UnparseableModelResponseError, providerErrorMessage, jsonOnlyPrompts, type ModelResponse } from '../types'; +import { logger } from '@codraoss/core/logger'; +import { withTimeout } from '@codraoss/core/timeout'; +import { ProviderRequestError, UnparseableModelResponseError, providerErrorMessage, jsonOnlyPrompts, isThinkingRejection, attachPartialResponse, type ModelResponse } from '../types'; import { assertPublicBaseUrl } from '../url-guard'; -import { MODEL_TIMEOUT_MAX_MS, OUTPUT_TOKENS_FLOOR, resolveOutputTokenCeiling } from '../limits'; +import { + MODEL_TIMEOUT_MAX_MS, + MODEL_TIMEOUT_PER_1K_OUTPUT_MS, + OUTPUT_TOKENS_FLOOR, + geminiThinkingBudgetTokens, + resolveOutputTokenCeiling, +} from '../limits'; // Vertex's REST API rejects plain API keys and requires an OAuth2 token via RFC 7523 JWT-bearer grant, so `apiKey` here holds the full service-account JSON key, not a short API key string. const VERTEX_TIMEOUT_MS = MODEL_TIMEOUT_MAX_MS; const VERTEX_DEFAULT_OUTPUT_TOKENS = OUTPUT_TOKENS_FLOOR; -// Same Gemini models as the Google adapter, so the same ceiling. No `thinkingConfig` here though: this -// adapter makes ONE attempt and has no latch, so a model that refused the field would fail the file. +// Same Gemini models as the Google adapter, so the same ceiling and thinkingConfig (unbounded reasoning +// would otherwise consume the whole ceiling and leave a truncated answer). const VERTEX_MAX_OUTPUT_TOKENS = 65_536; // Retries for a 429 only, and only while the caller's own timeout still has room. See the loop below // for why resending an unchanged request is the correct response to this particular refusal. @@ -22,6 +28,15 @@ const ACCESS_TOKEN_LIFETIME_S = 3600; // Refresh before real expiry so an in-flight review never starts a call with a token that expires mid-request. const TOKEN_REFRESH_MARGIN_MS = 60_000; +interface VertexGenerateResponse { + candidates?: Array<{ content?: { parts?: Array<{ text?: string }> }; finishReason?: string }>; + usageMetadata?: { + promptTokenCount?: number; + candidatesTokenCount?: number; + thoughtsTokenCount?: number; // billed against maxOutputTokens + }; +} + interface ServiceAccountKey { client_email: string; private_key: string; @@ -127,16 +142,19 @@ async function getAccessToken( export async function reviewWithVertex( config: { apiKey: string; baseUrl?: string | null; providerName?: string; timeoutMs?: number }, model: string, - input: { systemPrompt: string; userPrompt: string; outputBudgetTokens?: number }, + input: { systemPrompt: string; userPrompt: string; outputBudgetTokens?: number; truncationIntolerant?: boolean }, tracker?: { incrementSubrequests(count?: number): void }, ): Promise { const providerName = config.providerName ?? 'Google Vertex AI'; const timeoutMs = config.timeoutMs ?? VERTEX_TIMEOUT_MS; - const outputCeiling = resolveOutputTokenCeiling( + const answerBudget = resolveOutputTokenCeiling( input.outputBudgetTokens, VERTEX_MAX_OUTPUT_TOKENS, VERTEX_DEFAULT_OUTPUT_TOKENS, ); + const thinkingBudget = geminiThinkingBudgetTokens(answerBudget); + let currentCeiling = Math.min(VERTEX_MAX_OUTPUT_TOKENS, answerBudget + thinkingBudget); + let ceilingRaised = false; logger.info(`Calling Vertex AI model: ${model}`); assertPublicBaseUrl(config.baseUrl, providerName); @@ -159,7 +177,7 @@ export async function reviewWithVertex( } const url = `${baseUrl}/publishers/google/models/${encodeURIComponent(model)}:generateContent`; - const body = JSON.stringify({ + const buildBody = (includeThinking: boolean, ceiling: number) => JSON.stringify({ systemInstruction: { role: 'system', parts: [{ text: prompts.system }], @@ -170,13 +188,14 @@ export async function reviewWithVertex( generationConfig: { responseMimeType: 'application/json', // No `responseJsonSchema`: this adapter cannot drop a schema mid-flight, so a rejection would fail the file outright. - maxOutputTokens: outputCeiling, + maxOutputTokens: ceiling, + ...(includeThinking ? { thinkingConfig: { thinkingBudget } } : {}), // Same models as the Google adapter, so the same value keeps the two paths comparable. temperature: 0.9, }, }); - const attempt = () => + const attempt = (body: string) => withTimeout('Vertex AI', timeoutMs, (signal) => fetch(url, { method: 'POST', @@ -189,54 +208,101 @@ export async function reviewWithVertex( }), ); - if (tracker) tracker.incrementSubrequests(1); - let response = await attempt(); - - // A Vertex 429 here is queueing, not a bucket the caller can pace around. Measured over ~900 calls on - // one project: roughly three in four refused, and the refusal was uncorrelated with the requested - // output ceiling, with the endpoint, and with whether the previous call succeeded -- resending the - // IDENTICAL request works. The adapter used to make one attempt and turn every one of those into a - // failed file, which is the one case where the single-attempt rule above does not apply: there is no - // schema to re-probe and nothing about the request to change. - // - // Bounded by the caller's own timeout, not by a retry count alone: `timeoutMs` is already clamped to - // the fallback-chain budget, so a slow rung must not spend the whole invocation sitting in backoff. - for (let retry = 0; retry < VERTEX_QUOTA_RETRIES && response.status === 429; retry++) { - const waitMs = VERTEX_QUOTA_BACKOFF_MS * (retry + 1); - if (Date.now() - startTime + waitMs + VERTEX_MIN_ATTEMPT_MS > timeoutMs) break; - - logger.warn(`Vertex AI refused with 429; resending unchanged in ${waitMs}ms`, { model, retry: retry + 1 }); - await new Promise((resolve) => setTimeout(resolve, waitMs)); + let thinkingRejected = false; + let response: Response; + let data: VertexGenerateResponse; + let rawText: string | undefined; + let finishReason: string | undefined; + + for (;;) { + const body = buildBody(!thinkingRejected, currentCeiling); + if (tracker) tracker.incrementSubrequests(1); - response = await attempt(); - } + response = await attempt(body); + + // A Vertex 429 here is queueing, not a rate bucket: resending the identical request works (~3/4 of + // ~900 sampled calls). Bounded by the caller's timeout, already clamped to the fallback-chain budget. + for (let retry = 0; retry < VERTEX_QUOTA_RETRIES && response.status === 429; retry++) { + const waitMs = VERTEX_QUOTA_BACKOFF_MS * (retry + 1); + if (Date.now() - startTime + waitMs + VERTEX_MIN_ATTEMPT_MS > timeoutMs) break; + + logger.warn(`Vertex AI refused with 429; resending unchanged in ${waitMs}ms`, { model, retry: retry + 1 }); + await new Promise((resolve) => setTimeout(resolve, waitMs)); + if (tracker) tracker.incrementSubrequests(1); + response = await attempt(body); + } + + if (response.ok) { + data = (await response.json()) as VertexGenerateResponse; + const candidate = data.candidates?.[0]; + rawText = candidate?.content?.parts?.map((part) => part.text ?? '').join('')?.trim(); + finishReason = candidate?.finishReason; + + if (finishReason && finishReason !== 'STOP') { + logger.warn(`Vertex AI response for ${model} ended with finishReason=${finishReason}; output is likely incomplete`, { + outputSpend: (data.usageMetadata?.candidatesTokenCount ?? 0) + (data.usageMetadata?.thoughtsTokenCount ?? 0), + thoughtSpend: data.usageMetadata?.thoughtsTokenCount ?? 0, + outputCeiling: currentCeiling, + thinkingBudget: thinkingRejected ? undefined : thinkingBudget, + }); + } + + // thinkingConfig stays on here: dropping it switches to unbounded dynamic thinking, the opposite of the fix. + if (finishReason === 'MAX_TOKENS' && input.truncationIntolerant && !ceilingRaised) { + const raisedCeiling = Math.min(VERTEX_MAX_OUTPUT_TOKENS, 2 * answerBudget + thinkingBudget); + const extraMs = ((raisedCeiling - currentCeiling) / 1_000) * MODEL_TIMEOUT_PER_1K_OUTPUT_MS; + if (Date.now() - startTime + extraMs < timeoutMs) { + ceilingRaised = true; + currentCeiling = raisedCeiling; + logger.warn(`Vertex AI ran out of output room on ${model}; resending once with a larger ceiling`, { + outputCeiling: raisedCeiling, + thoughtSpend: data.usageMetadata?.thoughtsTokenCount ?? 0, + hadPartialText: Boolean(rawText), + }); + continue; + } + } + + break; + } - if (!response.ok) { const message = providerErrorMessage(await response.text()); + + if (!thinkingRejected && isThinkingRejection(response.status, message)) { + thinkingRejected = true; + logger.warn('Vertex AI rejected thinkingConfig; resending without an explicit thinking budget', { + model, + error: message, + }); + continue; + } + throw new ProviderRequestError(providerName, response.status, message); } const durationMs = Date.now() - startTime; logger.info(`AI model ${model} responded in ${durationMs}ms`); - const data = (await response.json()) as { - candidates?: Array<{ content?: { parts?: Array<{ text?: string }> }; finishReason?: string }>; - usageMetadata?: { - promptTokenCount?: number; - candidatesTokenCount?: number; - }; - }; - - const candidate = data.candidates?.[0]; - const rawText = candidate?.content?.parts?.map((part) => part.text ?? '').join('')?.trim(); if (!rawText) { - const finishReason = candidate?.finishReason; if (finishReason && finishReason !== 'STOP') { throw new UnparseableModelResponseError(model, `finishReason=${finishReason}`); } throw new Error('Vertex AI returned an empty response.'); } + // Still truncated after the re-probe; fail but attach the partial text so the chain's last model can salvage it. + if (finishReason === 'MAX_TOKENS' && input.truncationIntolerant) { + const error = new UnparseableModelResponseError(model, 'finishReason=MAX_TOKENS'); + attachPartialResponse(error, { + rawText, + inputTokens: data.usageMetadata?.promptTokenCount ?? 0, + outputTokens: data.usageMetadata?.candidatesTokenCount ?? 0, + modelUsed: model, + provider: providerName, + }); + throw error; + } + return { rawText, inputTokens: data.usageMetadata?.promptTokenCount ?? 0, diff --git a/packages/models/src/runner.ts b/packages/models/src/runner.ts index d5d25ab4..a20723d3 100644 --- a/packages/models/src/runner.ts +++ b/packages/models/src/runner.ts @@ -1,15 +1,15 @@ -import type { KvStore, SecretStore } from '@codra/core/ports'; +import type { KvStore, SecretStore } from '@codraoss/core/ports'; import type { CloudflareAiBinding } from './providers/cloudflare'; import { reviewWithGoogle } from './providers/google'; import { reviewWithVertex } from './providers/vertex'; import { reviewWithCloudflare } from './providers/cloudflare'; import { reviewWithOpenAI } from './providers/openai'; import { reviewWithAnthropic } from './providers/anthropic'; -import type { VerifyCandidate } from '@codra/core/prompts/verify'; -import type { RepoConfig, ResolvedModelConfig } from '@codra/schema'; -import type { TokenTracker } from '@codra/core/token-tracker'; +import type { VerifyCandidate } from '@codraoss/core/prompts/verify'; +import type { RepoConfig, ResolvedModelConfig } from '@codraoss/schema'; +import type { TokenTracker } from '@codraoss/core/token-tracker'; import type { ModelInput, ModelResponse } from './types'; -import { logger } from '@codra/core/logger'; +import { logger } from '@codraoss/core/logger'; import { decryptLlmApiKey } from './llm-crypto'; import { isSchemaDroppedError, @@ -20,40 +20,36 @@ import { ModelRateLimitBook } from './internal/model-rate-limits'; import { ModelChainProgressStore } from './internal/model-chain-progress'; import { type ModelChainContext, generateSummary, verifyFindings } from './internal/model-chain-runner'; import { type ModelReviewContext, reviewFile, reviewFiles } from './internal/model-review-file'; -// Re-exported so test doubles can be typed against the real batched-review shape. +// Re-exported so test doubles can be typed against the real shape. export type { BatchReviewOutcome } from './internal/model-review-file'; import { pollReviewBatch, submitReviewBatch } from './internal/model-review-batch'; -// Re-exported: core/review.ts and two specs import these from '@codra/models'. +// Re-exported: core/review.ts and two specs import these. export { RetryableModelError, isRetryableModelError, nextChainIndexOf } from './internal/model-support'; -// Re-exported so the batch-prompt budget test asserts against these constants, not a copy. export { PROMPT_FIT_SAFETY_FACTOR, estimatePromptTokens } from './internal/model-support'; -// Re-exported so its unit spec can reach it without a sibling import (no-restricted-imports). +// Re-exported to avoid a sibling import (no-restricted-imports). export { ModelChainProgressStore } from './internal/model-chain-progress'; -// Same reason: the 429-parsing spec asserts against the real implementation, not a copy. export { isPlausibleTokenBucket, parseRateLimitFromError } from './internal/model-support'; const PROVIDER_UNAVAILABLE_TTL_SECONDS = 24 * 60 * 60; export class ModelRunner { - // Caches the in-flight PROMISE (not just the result), so concurrent calls for a model await one request. + // Caches the in-flight promise so concurrent calls for a model share one request. private readonly resolvedModelCache = new Map>(); - // Rate-limit learning plus the connection/token gates, keyed by MODEL, not provider. - // Backed by chainProgress so learned cool-offs outlive the invocation; assigned in the constructor - // because it depends on it. + // Keyed by model, not provider; backed by chainProgress so cool-offs persist across invocations. private readonly rateLimits: ModelRateLimitBook; - // Provider-unavailable markers live in KV and can't flip set-to-unset within one invocation, so cache them per instance. + // KV can't flip unavailable to available within one invocation, so cache per instance. private readonly providerUnavailableCache = new Map>(); - // Models proven this invocation not to support async batching, so later files skip the probe. + // Confirmed unsupported for async batching this invocation; skip re-probing. private readonly asyncUnsupportedModels = new Set(); - // Same idea for constrained decoding: `(provider, model, grammar)` triples that were refused, keyed by grammar so one oversized bin doesn't disable the single-file grammar too. + // Same, for constrained decoding, keyed by grammar so one refusal doesn't disable others. private readonly schemaUnsupportedModels = new Set(); - // How far down the chain each file already got, so a deferral resumes instead of replaying. + // Per-file progress so a deferral resumes instead of replaying. private readonly chainProgress: ModelChainProgressStore; constructor( @@ -100,7 +96,6 @@ export class ModelRunner { const key = this.providerUnavailableKey(providerId); if (!key) return; - // Keep the in-invocation cache consistent with what we just wrote. this.providerUnavailableCache.set(providerId, Promise.resolve(true)); try { @@ -154,10 +149,10 @@ export class ModelRunner { const normalized = normalizeModel(model); let pending = this.resolvedModelCache.get(normalized); if (!pending) { - // Cache the DB answer, including a null "not configured", so it isn't re-queried per file. + // Cache the null "not configured" result too, so it isn't requeried. pending = this.deps.getConfig(normalized); this.resolvedModelCache.set(normalized, pending); - // Don't let a transient DB error poison the cache; drop it so the next call retries. + // Drop cache entry on error so the next call retries. pending.catch(() => this.resolvedModelCache.delete(normalized)); } const resolved = await pending; @@ -183,10 +178,10 @@ export class ModelRunner { config: ResolvedModelConfig, input: ModelInput, timeoutMs?: number, - // Reports queue time so a caller budgeting wall clock can exclude it. + // Excludes queue wait from the caller's timing budget. onGateWait?: (waitedMs: number) => void, ): Promise { - // Resolve credentials BEFORE taking a gate slot, so slow KV/crypto work never occupies one. + // Resolve credentials before the gate slot so slow work doesn't hold one. if (config.apiFormat === 'cloudflare-workers-ai') { if (!this.deps.aiBinding) { throw new Error(`Provider ${config.providerName} requires a Cloudflare AI binding, but none was provided.`); @@ -202,7 +197,7 @@ export class ModelRunner { let response: ModelResponse; try { response = await this.rateLimits.runGated(config, onGateWait, () => { - // Read inside the gate: hoisted, the opening wave would all see "not yet known" and probe. + // Read inside the gate: a hoisted read would race the opening wave. const gatedInput = this.schemaUnsupportedModels.has(schemaKey) ? { ...input, responseSchema: undefined } : input; @@ -214,12 +209,11 @@ export class ModelRunner { ); }); } catch (error) { - // Latch on failure too: the probe already proved the grammar is refused, and without this a - // schema-dropped attempt that then 429s re-pays the 400 plus a full prompt on the next call. + // Latch failure too, so a schema-dropped retry doesn't repay the 400 and full prompt. if (isSchemaDroppedError(error)) this.schemaUnsupportedModels.add(schemaKey); throw error; } - if (response.degraded === 'schema-dropped') { + if (response.degraded === 'schema-dropped' || response.degraded === 'schema-dropped-catchall') { this.schemaUnsupportedModels.add(schemaKey); } return response; @@ -269,7 +263,6 @@ export class ModelRunner { return this.callResolvedModel(await this.resolveModel(model), input, timeoutMs); } - // chainCtx() plus the review flow's extra per-invocation state. private reviewCtx(): ModelReviewContext { return { ...this.chainCtx(), @@ -284,7 +277,7 @@ export class ModelRunner { return reviewFile(this.reviewCtx(), params); } - // Several small files in one call; `batch.missing` files must not be recorded as reviewed. + // batch.missing files must not be recorded as reviewed. async reviewFiles(params: Parameters[1]) { return reviewFiles(this.reviewCtx(), params); } @@ -297,7 +290,6 @@ export class ModelRunner { return pollReviewBatch(this.reviewCtx(), params); } - // Hands the extracted flows the private model-chain surface. Built per call; holds no state. private chainCtx(): ModelChainContext { return { selectModel: (params) => this.selectModel(params), diff --git a/packages/models/src/types.ts b/packages/models/src/types.ts index 12da12ac..646622c9 100644 --- a/packages/models/src/types.ts +++ b/packages/models/src/types.ts @@ -1,18 +1,15 @@ -// Both live in @codra/core/ports now: prompts/file-review.ts builds a ModelResponseSchema, and it is -// the only reason a pure prompt module ever imported from models/. Re-exported here so the ~20 -// existing `@codra/models/types` importers are unaffected, and so there is exactly one definition. -import type { ModelResponseSchema } from '@codra/core/ports'; -export type { ModelResponse, ModelResponseSchema } from '@codra/core/ports'; +// Re-exported from @codraoss/core/ports so existing @codraoss/models/types imports keep working. +import type { ModelResponse as ModelResponseShape, ModelResponseSchema } from '@codraoss/core/ports'; +export type { ModelResponse, ModelResponseSchema } from '@codraoss/core/ports'; -// `responseSchema` is per-call on purpose: file review, verification, and summary each need a different output shape. export type ModelInput = { systemPrompt: string; userPrompt: string; responseSchema?: ModelResponseSchema; - // Output tokens this call needs to answer in full, from `reviewOutputBudgetTokens`. Advisory: each - // adapter clamps it to its own provider maximum and never goes BELOW its own default, so a caller - // that omits it is unaffected. Omitting it on a large batched review is what truncates the response. + // Advisory: adapters clamp to their max and never go below their own default. outputBudgetTokens?: number; + // Only for callers needing a whole answer; adapters retry once with more room on MAX_TOKENS. + truncationIntolerant?: boolean; }; export class ProviderRequestError extends Error { @@ -26,7 +23,7 @@ export class ProviderRequestError extends Error { } } -// Thrown instead of synthesizing a fake "inconclusive" pass, so the fallback chain tries the next model. Treated as PERMANENT (not transient): the outcome is deterministic, so retrying just burns quota. +// Deliberately PERMANENT (not transient): outcome is deterministic, so retry just burns quota. export class UnparseableModelResponseError extends Error { constructor(public readonly model: string, public readonly reason: string) { super(`Model ${model} produced no reviewable output (${reason}); the file review failed.`); @@ -34,7 +31,18 @@ export class UnparseableModelResponseError extends Error { } } -// `details[].description` and `details[].fieldViolations[].description`, flattened and deduped. +export function attachPartialResponse(error: object, response: ModelResponseShape) { + Object.defineProperty(error, 'partialResponse', { value: response, configurable: true }); +} + +export function partialResponseOf(error: unknown): ModelResponseShape | null { + if (typeof error !== 'object' || error === null) return null; + const partial = (error as { partialResponse?: unknown }).partialResponse; + if (typeof partial !== 'object' || partial === null) return null; + const { rawText } = partial as { rawText?: unknown }; + return typeof rawText === 'string' && rawText.trim() ? (partial as ModelResponseShape) : null; +} + function errorDetailText(error: unknown): string { const details = (error as Record | null)?.details; if (!Array.isArray(details)) return ''; @@ -65,21 +73,24 @@ export function providerErrorMessage(errorText: string) { } if (typeof message === 'string' && message.trim()) { - // Gemini puts the actionable reason in `error.details`, leaving `message` as the useless - // "Request contains an invalid argument." -- which made isSchemaRejection miss a grammar - // rejection and lose the whole model instead of retrying without the schema. + // Gemini's top-level message is useless; the real reason is in error.details. const detail = errorDetailText(obj.error); return detail ? `${message.trim()} ${detail}` : message.trim(); } } } catch { - // Fall back to the provider body below. + // Not JSON: fall through to the raw provider body below. } return errorText.trim() || 'The provider returned an error.'; } -// Temperature deliberately not zero: a little randomness reviews better than greedy decoding. Each adapter sits at the same relative point on its own scale (Google/Vertex/OpenAI 0-2 at 0.9; Anthropic 0-1 and Cloudflare 0-5 at 0.6); watch `droppedByVerdict` if these move. +export function isThinkingRejection(status: number, message: string) { + if (status !== 400) return false; + const lower = message.toLowerCase(); + return lower.includes('thinking') || lower.includes('thought'); +} + export function jsonOnlyPrompts(input: ModelInput) { return { system: `${input.systemPrompt}\n\nReturn only the JSON object. Do not include chain-of-thought, analysis, markdown, code fences, or explanatory prose.`, diff --git a/packages/models/test/model/batch-routing.spec.ts b/packages/models/test/model/batch-routing.spec.ts index 0617fcb1..259809cd 100644 --- a/packages/models/test/model/batch-routing.spec.ts +++ b/packages/models/test/model/batch-routing.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest'; -import { parseBatchReviewResponse } from '@codra/core/model-output'; -import type { FileDiff } from '@codra/core/diff'; +import { parseBatchReviewResponse } from '@codraoss/core/model-output'; +import type { FileDiff } from '@codraoss/core/diff'; function file(path: string, contents: string[], previousPath: string | null = null): FileDiff { return { @@ -57,13 +57,12 @@ describe('parseBatchReviewResponse', () => { expect(result.reviews.get('src/a.ts')!.comments[0].path).toBe('src/a.ts'); expect(result.reviews.get('src/b.ts')!.comments[0].path).toBe('src/b.ts'); expect(result.reviews.get('src/a.ts')!.fileSummary).toContain('Summary for src/a.ts'); - // Never silently approved: an omitted file has no entry and must surface for re-queueing. + // Omitted file must surface for re-queueing, not silently approved. expect(result.missing).toEqual(['src/c.ts']); expect(result.reviews.has('src/c.ts')).toBe(false); }); - // Routing tolerates loose paths, but only when unambiguous. Renames matter because renderFileDiff - // shows the old path on the header line. + // Renames matter: renderFileDiff shows the old path on the header line. it('tolerates path noise and renames, but refuses to guess', () => { for (const reported of ['./src/a.ts', 'a/src/a.ts', 'b/src/a.ts', '/src/a.ts', 'a.ts']) { const result = parseBatchReviewResponse( @@ -80,13 +79,12 @@ describe('parseBatchReviewResponse', () => { ); expect(renamed.reviews.get('src/new.ts')!.comments).toHaveLength(1); - // Two files share a basename: guessing would file findings against code they were never about. + // Shared basename: guessing would misattribute findings. const siblings = [file('src/a/index.ts', ['const alpha = 1;']), file('src/b/index.ts', ['const bravo = 2;'])]; const ambiguous = parseBatchReviewResponse(raw([entry('index.ts', 'const alpha = 1;')]), siblings); expect(ambiguous.stats.unroutableEntries).toBe(1); expect(ambiguous.reviews.size).toBe(0); - // A duplicate entry is discarded, never re-homed onto a sibling. const duplicated = parseBatchReviewResponse( raw([entry('src/a/index.ts', 'const alpha = 1;'), entry('src/a/index.ts', 'const alpha = 1;', 'Duplicate')]), siblings, @@ -96,7 +94,7 @@ describe('parseBatchReviewResponse', () => { expect(duplicated.missing).toEqual(['src/b/index.ts']); }); - // What per-file indexes miss: a misfiled finding whose quote exists in the wrong file too. + // Per-file indexes miss quotes shared across files. it('withholds only when a shared quote AND a path disagreement coincide', () => { const shared = '} catch (error) {'; const files = [file('src/a.ts', [shared, 'const uniqueToAlpha = 1;']), file('src/b.ts', [shared, 'const bravo = 2;'])]; @@ -118,19 +116,17 @@ describe('parseBatchReviewResponse', () => { expect(withheld.stats.ambiguousAcrossBin).toBe(1); expect(withheld.reviews.get('src/a.ts')!.comments).toHaveLength(0); - // Shared quote, agreeing path: ordinary, keep it. const agreeing = parseBatchReviewResponse(raw([entry('src/a.ts', shared, 'Swallowed error')]), files); expect(agreeing.stats.ambiguousAcrossBin).toBe(0); expect(agreeing.reviews.get('src/a.ts')!.comments).toHaveLength(1); - // Unique quote, disagreeing path: the enclosing entry wins, which is the point of nesting. + // Unique quote + wrong path: enclosing entry still wins. const mismatch = parseBatchReviewResponse(misfiled('const uniqueToAlpha = 1;', 'src/b.ts'), files); expect(mismatch.stats.pathMismatchFindings).toBe(1); expect(mismatch.reviews.get('src/a.ts')!.comments[0].path).toBe('src/a.ts'); }); - // Per file, not a shared pool: a shared ceiling lets one noisy file keep everything while - // its bin-mates are trimmed to nothing. + // Cap is per-file: a noisy file keeps its own cap while others are untouched. it('trims over-cap findings per file and accounts for the drop', () => { const lines = Array.from({ length: 30 }, (_, i) => `const value${i} = ${i};`); const files = [file('src/a.ts', lines), file('src/b.ts', ['const bravo = 2;'])]; @@ -159,12 +155,10 @@ describe('parseBatchReviewResponse', () => { expect(result.reviews.get('src/a.ts')!.comments).toHaveLength(10); expect(result.stats.overCap).toBe(20); expect(result.reviews.get('src/a.ts')!.fileSummary).toContain('over-cap'); - // The quiet file keeps everything -- it never competed for a shared budget. expect(result.reviews.get('src/b.ts')!.comments).toHaveLength(1); }); - // Assembly can reject one finding; under batching an uncontained throw would discard - // every other file packed alongside it. + // One bad finding must not sink the rest of the batch. it('drops an unassemblable finding without losing the rest of the bin', () => { const files = [file('src/a.ts', ['const alpha = 1;']), file('src/b.ts', ['const bravo = 2;'])]; diff --git a/packages/models/test/model/chain-progress-store.spec.ts b/packages/models/test/model/chain-progress-store.spec.ts index 40edd4b8..bc2fba64 100644 --- a/packages/models/test/model/chain-progress-store.spec.ts +++ b/packages/models/test/model/chain-progress-store.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; -import { ModelChainProgressStore } from '@codra/models'; +import { ModelChainProgressStore } from '@codraoss/models'; -// KV double tracks overlapping puts (KV lacks ordering; late puts with less state can revert progress). +// KV has no ordering; a late put with less state could revert progress. function makeKV() { let value: string | null = null; let inFlight = 0; @@ -46,7 +46,6 @@ describe('ModelChainProgressStore', () => { await Promise.all([store.advance('src/a.ts', 2), store.advance('src/b.ts', 3)]); - // Order irrelevance: non-overlapping puts ensure the last one is the most complete. expect(kv.maxInFlight).toBe(1); expect(kv.stored?.files).toEqual({ 'src/a.ts': 2, 'src/b.ts': 3 }); expect(await store.startIndexFor('src/a.ts')).toBe(2); @@ -60,7 +59,6 @@ describe('ModelChainProgressStore', () => { await Promise.all([1, 2, 3, 4, 5, 6].map((n) => store.advance(`src/f${n}.ts`, n))); expect(kv.maxInFlight).toBe(1); - // Six advances but fewer writes; conserves subrequests. expect(kv.writes.length).toBeLessThan(6); expect(Object.keys(kv.stored?.files ?? {})).toHaveLength(6); }); @@ -88,7 +86,6 @@ describe('ModelChainProgressStore', () => { expect(await store.startIndexFor('src/a.ts')).toBe(3); }); - // Persisted tally lets the next concurrent wave avoid models the first wave timed out on. it('drops a model after a full wave of timeouts, and remembers across invocations', async () => { const kv = makeKV(); const store = new ModelChainProgressStore(kv.kv, 'job-slow'); @@ -101,33 +98,29 @@ describe('ModelChainProgressStore', () => { await store.noteTimeout('vertex-ai:gemini-2.5-pro'); expect(await store.isTimingOut('vertex-ai:gemini-2.5-pro')).toBe(true); - // Fresh store mimics next invocation. const next = new ModelChainProgressStore(kv.kv, 'job-slow'); expect(await next.isTimingOut('vertex-ai:gemini-2.5-pro')).toBe(true); - // Scoped to the failing model. expect(await next.isTimingOut('vertex-ai:gemini-2.5-flash')).toBe(false); }); - // Tail candidates use a higher strike threshold rather than exemption, to prevent infinite looping. + // Tail gets higher strike threshold, not exemption, to avoid infinite looping. it('holds the last candidate to a higher strike count before dropping it too', async () => { const kv = makeKV(); const store = new ModelChainProgressStore(kv.kv, 'job-tail'); for (let i = 0; i < 3; i += 1) await store.noteTimeout('cf:glm-4.7-flash'); - // Drops mid-chain, but preserves the tail. expect(await store.isTimingOut('cf:glm-4.7-flash')).toBe(true); expect(await store.isTimingOutTerminally('cf:glm-4.7-flash')).toBe(false); for (let i = 0; i < 3; i += 1) await store.noteTimeout('cf:glm-4.7-flash'); expect(await store.isTimingOutTerminally('cf:glm-4.7-flash')).toBe(true); - // Durable across invocations. const next = new ModelChainProgressStore(kv.kv, 'job-tail'); expect(await next.isTimingOutTerminally('cf:glm-4.7-flash')).toBe(true); }); describe('noteSuccess', () => { - // Resets prevent cumulative tallies from condemning a model for the job's entire 24h life. + // Reset prevents lifetime tally from condemning a model for the whole job. it('restarts the tally, so a slow patch cannot condemn a working model', async () => { const kv = makeKV(); const store = new ModelChainProgressStore(kv.kv, 'job-recovered'); @@ -139,7 +132,7 @@ describe('ModelChainProgressStore', () => { expect(await store.isTimingOut('vertex-ai:gemini-2.5-pro')).toBe(false); }); - // writeOnce's max() merge must not resurrect pre-success counts from KV. + // max() merge must not resurrect pre-success counts from KV. it('survives the merge against what another invocation stored', async () => { const kv = makeKV(); await kv.kv.put('k', JSON.stringify({ timeouts: { 'vertex-ai:gemini-2.5-pro': 5 } })); @@ -158,7 +151,6 @@ describe('ModelChainProgressStore', () => { await store.noteSuccess('vertex-ai:gemini-2.5-pro'); - // Healthy paths don't incur KV writes to save subrequests. expect(kv.writes).toHaveLength(0); }); }); @@ -187,7 +179,6 @@ describe('ModelChainProgressStore', () => { expect(await store.isTimingOut('anything')).toBe(false); }); - // Persisted rate-limits prevent continuation jobs from re-paying for known cool-offs. describe('rate-limit cool-offs', () => { it('carries a learned cool-off and bucket size to the next invocation', async () => { const kv = makeKV(); @@ -200,7 +191,6 @@ describe('ModelChainProgressStore', () => { const next = new ModelChainProgressStore(kv.kv, 'job-cooldown'); const loaded = await next.loadCooldowns(); expect(loaded.get('google:gemini-2.5-flash')).toEqual({ cooldownUntil: until, limitTokens: 16000 }); - // Cool-offs scope per-model bucket. expect(loaded.has('google:gemini-2.5-flash-lite')).toBe(false); }); @@ -211,7 +201,6 @@ describe('ModelChainProgressStore', () => { store.noteRateLimit('google:gemini-2.5-flash', { cooldownUntil: Date.now() + 30_000 }); expect(kv.writes).toHaveLength(0); - // Made durable by the subsequent deferral. await store.flushPending(); expect(kv.writes.length).toBeGreaterThan(0); }); @@ -230,7 +219,7 @@ describe('ModelChainProgressStore', () => { expect(kv.stored?.cooldowns?.['google:m']).toEqual({ until: later, limitTokens: 16000 }); }); - // Protects against sticky, misparsed request counts crippling the model for 24h. + // Guards against misparsed counts crippling the model for 24h. it('discards a stored bucket too small to be a token quota', async () => { const kv = makeKV(); const until = Date.now() + 30_000; @@ -239,7 +228,6 @@ describe('ModelChainProgressStore', () => { const store = new ModelChainProgressStore(kv.kv, 'job-poisoned-bucket'); const entry = (await store.loadCooldowns()).get('google:m'); - // Retains valid cool-off while discarding nonsense bucket size. expect(entry?.cooldownUntil).toBe(until); expect(entry?.limitTokens).toBeUndefined(); }); diff --git a/packages/models/test/model/chain-resume.spec.ts b/packages/models/test/model/chain-resume.spec.ts index 20ea01d2..3b049cbd 100644 --- a/packages/models/test/model/chain-resume.spec.ts +++ b/packages/models/test/model/chain-resume.spec.ts @@ -1,12 +1,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { nextChainIndexOf, ModelRunner } from '@codra/models'; -import { defaultRepoConfig } from '@codra/schema'; -import { TokenTracker } from '@codra/core/token-tracker'; +import { nextChainIndexOf, ModelRunner } from '@codraoss/models'; +import { defaultRepoConfig } from '@codraoss/schema'; +import { TokenTracker } from '@codraoss/core/token-tracker'; import { createTestEnv, saveTestProviderApiKey, createTestModelRunner } from '../../../../test/helpers'; -// One invocation only affords ~55s of model calls, so a chain whose head is slow never reaches its -// tail. These pin that a deferral records where it got to and the next attempt resumes there -- -// without which entries past the first two are unreachable no matter how often a job retries. +// ~55s per invocation: a slow head never reaches the tail, so resume must pick up where it left off. describe('model chain resume', () => { afterEach(() => { vi.restoreAllMocks(); @@ -26,8 +24,7 @@ describe('model chain resume', () => { ...defaultRepoConfig, model: { main: 'gemini-3.1-pro-preview', - // Three entries, all configured in the test env: the memo only records progress while the - // chain still has somewhere to go, so a two-entry chain would never write one. + // 3 entries: the memo only records progress when there's still somewhere left to go. fallbacks: ['gemini-2.5-pro', 'gemini-3.1-flash-lite'], size_overrides: [], }, @@ -40,7 +37,6 @@ describe('model chain resume', () => { candidates: [{ content: { parts: [{ text: '{"findings":[],"overall_correctness":"patch is correct","overall_explanation":"ok","overall_confidence_score":0.9}' }] } }], usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1 }, }); - // 503, not 500: a transient failure is what produces a deferral rather than a hard failure. const unavailable = () => gemini(503, { error: { code: 503, message: 'The model is overloaded.', status: 'UNAVAILABLE' } }); const rateLimited = () => gemini(429, { error: { code: 429, message: 'Resource exhausted. limit: 16000, model: gemini. Please retry in 30s.', status: 'RESOURCE_EXHAUSTED' } }); @@ -51,16 +47,13 @@ describe('model chain resume', () => { prDescription: null, config: chainConfig, totalLineCount: 1, - // Every model gets one shot, so the deferral arrives without a long inline retry ladder. } as Parameters[0]); } it('resumes at the model after the ones that already failed, instead of replaying them', async () => { const env = createTestEnv(); await saveTestProviderApiKey(env); - // Same jobId across both services: the memo is job-scoped KV, exactly as across invocations. - // Near the subrequest cap, so the chain stops after the primary -- the real shape of the - // problem, where a breaker ends the walk with models still untried. + // Near the subrequest cap, so the breaker ends the chain after the primary, matching prod. const tracker = new TokenTracker(); tracker.incrementSubrequests(40); const first = createTestModelRunner(env, tracker, { jobId: 'job-chain-resume' }); @@ -71,12 +64,10 @@ describe('model chain resume', () => { expect(walked.every((url) => url.includes('gemini-3.1-pro-preview'))).toBe(true); vi.restoreAllMocks(); - // A fresh service stands in for the next invocation; it reads the memo back out of KV. const second = createTestModelRunner(env, undefined, { jobId: 'job-chain-resume' }); const secondFetch = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => ok()); await review(second); - // The head of the chain is not retried: it was already ruled out for this file. const retried = secondFetch.mock.calls.map((call) => String(call[0])); expect(retried.every((url) => !url.includes('gemini-3.1-pro-preview'))).toBe(true); expect(retried.length).toBeGreaterThan(0); @@ -87,15 +78,13 @@ describe('model chain resume', () => { await saveTestProviderApiKey(env); const service = createTestModelRunner(env, undefined, { jobId: 'job-chain-429' }); - // A 429 means "same model, later" -- advancing past it would skip a healthy model for good. + // 429 means "same model, later"; advancing past it would skip a healthy model for good. vi.spyOn(globalThis, 'fetch').mockImplementation(async () => rateLimited()); const failure = await review(service).catch((error) => error); expect(nextChainIndexOf(failure)).toBeNull(); }); - // Observed in production: pro timed out, the invocation ran out of subrequests, and the chain then - // walked all 8 remaining entries for 2 files -- 16 refusals -- before failing the chunk outright. it('stops the chain the moment the invocation runs out of subrequests', async () => { const env = createTestEnv(); await saveTestProviderApiKey(env); @@ -106,12 +95,8 @@ describe('model chain resume', () => { ); const failure = await review(service).catch((error) => error); - // One attempt, not one per configured model: the runtime refused the call, so nothing about the - // next model could make it succeed. expect(fetchMock).toHaveBeenCalledTimes(1); expect(String(failure?.message)).toMatch(/retrying later/); - // And no progress recorded: those models never ran, so marking them tried would make the resume - // memo skip healthy models for the rest of the job. expect(nextChainIndexOf(failure)).toBeNull(); }); diff --git a/packages/models/test/model/chain-salvage.spec.ts b/packages/models/test/model/chain-salvage.spec.ts new file mode 100644 index 00000000..c5be4c9d --- /dev/null +++ b/packages/models/test/model/chain-salvage.spec.ts @@ -0,0 +1,231 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { defaultRepoConfig, type ResolvedModelConfig } from '@codraoss/schema'; +import { TokenTracker } from '@codraoss/core/token-tracker'; + +import { runModelChain, type ModelReviewContext } from '../../src/internal/model-review-chain'; +import { ModelChainProgressStore } from '../../src/internal/model-chain-progress'; +import { ModelRateLimitBook } from '../../src/internal/model-rate-limits'; +import { attachPartialResponse, UnparseableModelResponseError } from '../../src/types'; +import { MODEL_FALLBACK_CHAIN_BUDGET_MS } from '../../src/limits'; + +function fakeKv() { + const store = new Map(); + return { + async get(key: string) { return store.get(key) ?? null; }, + async put(key: string, value: string) { store.set(key, value); }, + }; +} + +function resolved(modelName: string): ResolvedModelConfig { + return { + modelId: modelName, + providerId: '00000000-0000-4000-8000-000000000000', + providerName: 'Google', + apiFormat: 'gemini', + modelName, + updatedAt: new Date().toISOString(), + providerEnabled: true, + baseUrl: null, + encryptedApiKey: 'key', + }; +} + +const ANSWER = '{"findings":[]}'; + +type Attempt = { model: string; timeoutMs: number | undefined }; + +function makeContext(input: { + chain: string[]; + call: (model: string, attempt: number) => Promise<{ rawText: string; inputTokens: number; outputTokens: number; modelUsed: string; provider: string }>; + attempts: Attempt[]; + tracker?: TokenTracker; +}): ModelReviewContext { + let callCount = 0; + return { + selectModel: () => ({ primary: input.chain[0], fallbacks: input.chain.slice(1) }), + resolveModel: async (model: string) => resolved(model), + isProviderUnavailable: async () => false, + markProviderUnavailable: async () => {}, + callResolvedModel: async (model: ResolvedModelConfig, _modelInput: unknown, timeoutMs?: number) => { + input.attempts.push({ model: model.modelName, timeoutMs }); + return input.call(model.modelName, callCount++); + }, + tracker: input.tracker, + jobId: 'job-1', + aiBinding: undefined, + rateLimits: new ModelRateLimitBook(), + asyncUnsupportedModels: new Set(), + chainProgress: new ModelChainProgressStore(fakeKv(), 'job-1'), + } as unknown as ModelReviewContext; +} + +function truncatedError(model: string, rawText: string) { + const error = new UnparseableModelResponseError(model, 'finishReason=MAX_TOKENS'); + attachPartialResponse(error, { + rawText, + inputTokens: 1_200, + outputTokens: 8_000, + modelUsed: model, + provider: 'Google', + }); + return error; +} + +function chainParams(overrides: Partial[1]> = {}) { + return { + systemPrompt: 'system', + userPrompt: 'user', + responseSchema: { name: 'x', schema: {} } as never, + timeoutMs: 50_000, + label: 'src/app.ts', + totalLineCount: 400, + config: defaultRepoConfig, + parse: (rawText: string) => JSON.parse(rawText) as unknown, + ...overrides, + } as Parameters[1]; +} + +describe('runModelChain: salvaging a truncated last answer', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it('parses the partial answer when the last model runs out of room', async () => { + const attempts: Attempt[] = []; + const tracker = new TokenTracker(); + const ctx = makeContext({ + chain: ['gemini-2.5-flash'], + attempts, + tracker, + call: async (model) => { throw truncatedError(model, ANSWER); }, + }); + + const result = await runModelChain(ctx, chainParams()); + + expect(result.parsed).toEqual({ findings: [] }); + expect(result.modelUsed).toBe('gemini-2.5-flash'); + expect(result.degraded).toBe('truncated'); + expect(tracker.getTotalUsage()).toMatchObject({ input: 1_200, output: 8_000 }); + expect(attempts).toHaveLength(1); + }); + + it('does not salvage while a better model is still available', async () => { + const attempts: Attempt[] = []; + const ctx = makeContext({ + chain: ['gemini-2.5-flash', 'gemini-2.5-pro'], + attempts, + call: async (model) => { + if (model === 'gemini-2.5-flash') throw truncatedError(model, ANSWER); + return { rawText: '{"findings":["real"]}', inputTokens: 10, outputTokens: 20, modelUsed: model, provider: 'Google' }; + }, + }); + + const result = await runModelChain(ctx, chainParams()); + + expect(attempts.map((a) => a.model)).toEqual(['gemini-2.5-flash', 'gemini-2.5-pro']); + expect(result.parsed).toEqual({ findings: ['real'] }); + expect(result.degraded).toBeUndefined(); + }); + + it('fails normally when the partial answer cannot be parsed', async () => { + const attempts: Attempt[] = []; + const ctx = makeContext({ + chain: ['gemini-2.5-flash'], + attempts, + call: async (model) => { throw truncatedError(model, '{"findings":[{"path":'); }, + }); + + await expect(runModelChain(ctx, chainParams())).rejects.toThrow(/no reviewable output/i); + }); + + it('leaves an ordinary failure on the last model alone', async () => { + const attempts: Attempt[] = []; + const ctx = makeContext({ + chain: ['gemini-2.5-flash'], + attempts, + call: async () => { throw new UnparseableModelResponseError('gemini-2.5-flash', 'empty'); }, + }); + + await expect(runModelChain(ctx, chainParams())).rejects.toThrow(/empty/); + }); +}); + +describe('runModelChain: sharing the invocation time budget across rungs', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + function withClock() { + let now = 1_000_000; + vi.spyOn(Date, 'now').mockImplementation(() => now); + return { spend: (ms: number) => { now += ms; } }; + } + + it('gives a single-model chain everything it asked for', async () => { + withClock(); + const attempts: Attempt[] = []; + const ctx = makeContext({ + chain: ['gemini-2.5-flash'], + attempts, + call: async (model) => ({ rawText: ANSWER, inputTokens: 1, outputTokens: 1, modelUsed: model, provider: 'Google' }), + }); + + await runModelChain(ctx, chainParams({ timeoutMs: 50_000 })); + + expect(attempts[0].timeoutMs).toBe(50_000); + }); + + it('holds back room so a fallback still runs in the same invocation', async () => { + const clock = withClock(); + const attempts: Attempt[] = []; + const ctx = makeContext({ + chain: ['gemini-2.5-flash', 'gemini-2.5-pro', 'gemini-3.1-flash-lite'], + attempts, + call: async (model, attempt) => { + clock.spend(attempts[attempt].timeoutMs ?? 0); + throw new UnparseableModelResponseError(model, 'no answer'); + }, + }); + + await expect(runModelChain(ctx, chainParams({ timeoutMs: 50_000 }))).rejects.toThrow(); + + expect(attempts.map((a) => a.timeoutMs)).toEqual([35_000, 20_000]); + expect(attempts.map((a) => a.model)).toEqual(['gemini-2.5-flash', 'gemini-2.5-pro']); + }); + + it('never grants more than the chain budget in total', async () => { + const clock = withClock(); + const attempts: Attempt[] = []; + const ctx = makeContext({ + chain: ['a', 'b', 'c', 'd'], + attempts, + call: async (model, attempt) => { + clock.spend(attempts[attempt].timeoutMs ?? 0); + throw new UnparseableModelResponseError(model, 'no answer'); + }, + }); + + await expect(runModelChain(ctx, chainParams({ timeoutMs: 50_000 }))).rejects.toThrow(); + + const granted = attempts.reduce((sum, a) => sum + (a.timeoutMs ?? 0), 0); + expect(granted).toBeLessThanOrEqual(MODEL_FALLBACK_CHAIN_BUDGET_MS); + }); + + it('never grants a rung more than it asked for', async () => { + const clock = withClock(); + const attempts: Attempt[] = []; + const ctx = makeContext({ + chain: ['a', 'b', 'c'], + attempts, + call: async (model, attempt) => { + clock.spend(attempts[attempt].timeoutMs ?? 0); + throw new UnparseableModelResponseError(model, 'no answer'); + }, + }); + + await expect(runModelChain(ctx, chainParams({ timeoutMs: 12_000 }))).rejects.toThrow(); + + expect(attempts.every((a) => (a.timeoutMs ?? 0) <= 12_000)).toBe(true); + expect(attempts).toHaveLength(3); + }); +}); diff --git a/packages/models/test/model/cloudflare.spec.ts b/packages/models/test/model/cloudflare.spec.ts index ecb39aa5..21db037e 100644 --- a/packages/models/test/model/cloudflare.spec.ts +++ b/packages/models/test/model/cloudflare.spec.ts @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { reviewWithCloudflare, submitCloudflareBatch, pollCloudflareBatch } from '@codra/models/cloudflare'; +import { reviewWithCloudflare, submitCloudflareBatch, pollCloudflareBatch } from '@codraoss/models/cloudflare'; // Regression: some Workers AI models (e.g. @cf/qwen/qwen2.5-coder-32b-instruct honoring // response_format) return `response` as an already-parsed JSON object/array rather than a string. diff --git a/packages/models/test/model/config-cache.spec.ts b/packages/models/test/model/config-cache.spec.ts index 04060050..51f1d4ba 100644 --- a/packages/models/test/model/config-cache.spec.ts +++ b/packages/models/test/model/config-cache.spec.ts @@ -1,11 +1,11 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createTestEnv, createTestModelRunner } from '../../../../test/helpers'; -// Isolated in its own file: mocking @codra/db/model-configs module-wide would break the +// Isolated in its own file: mocking @codraoss/db/model-configs module-wide would break the // other model-service tests that resolve configs against the real test DB. const getResolvedModelConfigMock = vi.hoisted(() => vi.fn()); -vi.mock('@codra/db/model-configs', async (importOriginal) => { +vi.mock('@codraoss/db/model-configs', async (importOriginal) => { const mod = await importOriginal(); return { ...mod, getResolvedModelConfig: getResolvedModelConfigMock }; }); diff --git a/packages/models/test/model/gemini-schema.spec.ts b/packages/models/test/model/gemini-schema.spec.ts index 9bafdc9e..b6a6d693 100644 --- a/packages/models/test/model/gemini-schema.spec.ts +++ b/packages/models/test/model/gemini-schema.spec.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest'; import { toGeminiResponseJsonSchema } from '../../src/gemini-schema'; -import { buildBatchReviewResponseSchema, buildReviewResponseSchema } from '@codra/core/prompts/file-review'; -import { VERIFY_RESPONSE_SCHEMA } from '@codra/core/prompts/verify'; +import { buildBatchReviewResponseSchema, buildReviewResponseSchema } from '@codraoss/core/prompts/file-review'; +import { VERIFY_RESPONSE_SCHEMA } from '@codraoss/core/prompts/verify'; // Transformations asserted on the pure function; the adapter specs only check a grammar reaches the // wire. Every failure mode here is silent -- a mangled grammar still returns 200. diff --git a/packages/models/test/model/limits.spec.ts b/packages/models/test/model/limits.spec.ts index 3b3a860d..27353a7f 100644 --- a/packages/models/test/model/limits.spec.ts +++ b/packages/models/test/model/limits.spec.ts @@ -11,10 +11,9 @@ import { resolveOutputTokenCeiling, reviewOutputBudgetTokens, } from '../../src/limits'; -import { generatorFindingCap } from '@codra/core/prompts/file-review'; +import { generatorFindingCap } from '@codraoss/core/prompts/file-review'; -// The whole point of these: a bin that overruns `maxOutputTokens` comes back as a repaired JSON prefix -// with its tail files silently empty, which is indistinguishable from "those files are clean". +// Overrun output repairs to a JSON prefix, silently emptying tail files: indistinguishable from clean. describe('reviewOutputBudgetTokens', () => { it('never asks for less than the floor', () => { expect(reviewOutputBudgetTokens({ findingCap: 1, fileCount: 1 })).toBe(OUTPUT_TOKENS_FLOOR); @@ -23,13 +22,12 @@ describe('reviewOutputBudgetTokens', () => { it('grows with the number of findings the prompt asked for', () => { const one = reviewOutputBudgetTokens({ findingCap: generatorFindingCap(10), fileCount: 1 }); const bin = reviewOutputBudgetTokens({ findingCap: generatorFindingCap(10), fileCount: 6 }); - // Six files at the same per-file cap need more room than one. expect(bin).toBeGreaterThan(one); expect(bin).toBeGreaterThan(OUTPUT_TOKENS_FLOOR); }); it('covers the bin ask that the old flat ceiling could not', () => { - // The regression: 6 files x 20 findings each, requested inside a flat 8192. + // Regression: 6 files x 20 findings exceeded the old flat 8192 ceiling. expect(reviewOutputBudgetTokens({ findingCap: 20, fileCount: 6 })).toBeGreaterThan(8_192); }); }); @@ -37,7 +35,6 @@ describe('reviewOutputBudgetTokens', () => { describe('resolveOutputTokenCeiling', () => { it('falls back to the provider default when no budget is stated', () => { expect(resolveOutputTokenCeiling(undefined, 65_536, 8_192)).toBe(8_192); - // A caller that omits it must be unaffected by a raised provider max. expect(resolveOutputTokenCeiling(0, 65_536, 8_192)).toBe(8_192); expect(resolveOutputTokenCeiling(Number.NaN, 65_536, 8_192)).toBe(8_192); }); @@ -46,29 +43,26 @@ describe('resolveOutputTokenCeiling', () => { expect(resolveOutputTokenCeiling(1_000, 65_536, 8_192)).toBe(8_192); expect(resolveOutputTokenCeiling(20_000, 65_536, 8_192)).toBe(20_000); expect(resolveOutputTokenCeiling(999_999, 65_536, 8_192)).toBe(65_536); - // A provider whose max is below the shared default still gets a request it accepts. expect(resolveOutputTokenCeiling(20_000, 4_096, 8_192)).toBe(4_096); }); }); describe('geminiThinkingBudgetTokens', () => { - // Thinking bills against the SAME maxOutputTokens the JSON must fit in, so raising the ceiling has to - // buy answer rather than more thinking. + // Thinking shares maxOutputTokens with JSON output, so a higher ceiling must mostly buy answer room. it('stays a minority of the ceiling', () => { expect(geminiThinkingBudgetTokens(32_768)).toBeLessThan(32_768 / 3); expect(geminiThinkingBudgetTokens(8_192)).toBeLessThan(8_192 / 3); }); it('stays inside the band every Gemini 2.5 model accepts', () => { - // Never 0 (the Pro models refuse it outright) and never above 8192 (Flash's own ceiling is lower). + // Above 0 (Pro rejects it) and under Flash's 8192 ceiling. expect(geminiThinkingBudgetTokens(1_024)).toBeGreaterThanOrEqual(1_024); expect(geminiThinkingBudgetTokens(65_536)).toBeLessThanOrEqual(8_192); }); }); describe('generatorFindingCap', () => { - // Bin size deliberately does NOT divide this; see the note on generatorFindingCap. Measured output was - // ~3% of the ceiling, so the cap has never been the limit and lowering it only removes headroom. + // Bin size intentionally doesn't divide this cap; measured output stays ~3% of ceiling. it('is 2x max_comments regardless of how many files share the call', () => { expect(generatorFindingCap(10)).toBe(20); expect(generatorFindingCap(1)).toBe(2); @@ -84,7 +78,6 @@ describe('adaptiveModelTimeoutMs', () => { }); it('scales with diff size beyond the free-line allowance', () => { - // Use line counts that stay below the MAX cap so the linear scaling is observable. expect(adaptiveModelTimeoutMs(200)).toBe(MODEL_TIMEOUT_BASE_MS + 100 * 100); expect(adaptiveModelTimeoutMs(250)).toBeGreaterThan(adaptiveModelTimeoutMs(150)); }); @@ -96,13 +89,11 @@ describe('adaptiveModelTimeoutMs', () => { describe('clampTimeoutToChainBudget', () => { it('leaves every budget the adaptive ceiling can produce untouched', () => { - // A big bin is meant to spend a whole invocation on one model and get the full ceiling. expect(clampTimeoutToChainBudget(MODEL_TIMEOUT_MAX_MS)).toBe(MODEL_TIMEOUT_MAX_MS); expect(clampTimeoutToChainBudget(MODEL_TIMEOUT_BASE_MS)).toBe(MODEL_TIMEOUT_BASE_MS); }); - // The invariant it exists to hold: the head of a chain is exempt from the budget check, so a per-call - // budget above the chain budget would let a call start that can never finish inside it. + // Chain head is exempt from the budget check, so ceiling must stay <= chain budget. it('holds the ceiling under the chain budget', () => { expect(MODEL_TIMEOUT_MAX_MS).toBeLessThanOrEqual(MODEL_FALLBACK_CHAIN_BUDGET_MS); expect(clampTimeoutToChainBudget(MODEL_FALLBACK_CHAIN_BUDGET_MS + 10_000)).toBe(MODEL_FALLBACK_CHAIN_BUDGET_MS); @@ -120,7 +111,6 @@ describe('ModelCallGate', () => { gate.run(async () => { active++; peak = Math.max(peak, active); - // Yield a couple of microtasks so tasks genuinely overlap. await Promise.resolve(); await Promise.resolve(); active--; @@ -140,7 +130,6 @@ describe('ModelCallGate', () => { throw new Error('boom'); })).rejects.toThrow('boom'); - // The slot must be free again for the next caller. const result = await gate.run(async () => 'ok'); expect(result).toBe('ok'); }); diff --git a/packages/models/test/model/output-batch.spec.ts b/packages/models/test/model/output-batch.spec.ts index 524d92e1..88d79cd4 100644 --- a/packages/models/test/model/output-batch.spec.ts +++ b/packages/models/test/model/output-batch.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { parseRawBatchPayload } from '@codra/core/model-output'; +import { parseRawBatchPayload } from '@codraoss/core/model-output'; function nested(paths: string[]) { return { diff --git a/packages/models/test/model/output.spec.ts b/packages/models/test/model/output.spec.ts index f0687391..cbf67d24 100644 --- a/packages/models/test/model/output.spec.ts +++ b/packages/models/test/model/output.spec.ts @@ -1,6 +1,6 @@ -import { parseFileReviewResponse, dedupeFindings } from '@codra/core/model-output'; -import type { FileDiff } from '@codra/core/diff'; -import type { ParsedReviewComment } from '@codra/schema'; +import { parseFileReviewResponse, dedupeFindings } from '@codraoss/core/model-output'; +import type { FileDiff } from '@codraoss/core/diff'; +import type { ParsedReviewComment } from '@codraoss/schema'; describe('Model Output Parsing Deep Dive', () => { const mockFile: FileDiff = { @@ -204,16 +204,28 @@ describe('dedupeFindings', () => { ...over, }); - it('collapses same-titled findings across files, keeping the strongest', () => { - const input = [ + // This used to assert the opposite, and the opposite was a bug: the key was the normalized title + // alone, so "Use of any" in three files became one comment and two real findings were dropped. + // Dedupe is a union over locations, not a merge of everything that happens to share a name. + it('keeps same-titled findings that are in different files', () => { + const result = dedupeFindings([ make({ path: 'a.ts', severity: 'P3', confidenceScore: 0.4 }), make({ path: 'b.ts', severity: 'P1', confidenceScore: 0.5 }), make({ path: 'c.ts', severity: 'P3', confidenceScore: 0.9 }), - ]; - const result = dedupeFindings(input); + ]); + + expect(result.map((c) => c.path)).toEqual(['a.ts', 'b.ts', 'c.ts']); + }); + + it('collapses the same finding at the same place, keeping the strongest', () => { + const result = dedupeFindings([ + make({ severity: 'P3', confidenceScore: 0.4, anchorHash: 'aaaa' }), + make({ severity: 'P1', confidenceScore: 0.5, anchorHash: 'aaaa' }), + make({ severity: 'P3', confidenceScore: 0.9, anchorHash: 'aaaa' }), + ]); + expect(result).toHaveLength(1); expect(result[0].severity).toBe('P1'); - expect(result[0].path).toBe('b.ts'); }); }); diff --git a/packages/models/test/model/rate-limit-parse.spec.ts b/packages/models/test/model/rate-limit-parse.spec.ts index ad184af2..7fe41613 100644 --- a/packages/models/test/model/rate-limit-parse.spec.ts +++ b/packages/models/test/model/rate-limit-parse.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { isPlausibleTokenBucket, parseRateLimitFromError } from '@codra/models'; +import { isPlausibleTokenBucket, parseRateLimitFromError } from '@codraoss/models'; // Verbatim from production: a free-tier 429 whose only stated quota counts REQUESTS, not tokens. const REQUESTS_QUOTA_429 = [ diff --git a/packages/models/test/model/schema-rejection.spec.ts b/packages/models/test/model/schema-rejection.spec.ts new file mode 100644 index 00000000..a2568099 --- /dev/null +++ b/packages/models/test/model/schema-rejection.spec.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from 'vitest'; +import { classifySchemaRejection } from '@codraoss/models/google'; + +// A hit here latches the model into `schemaUnsupportedModels` for the rest of the job -- every later +// call runs unconstrained. It used to fire on a bare "Request contains an invalid argument.", which is +// also what an unrelated bad request looks like, so roughly two in five calls could be running without +// a response grammar with no way to tell. These pin both halves: what must still match, and what the +// heuristic branch now requires before it will claim a grammar rejection. +describe('classifySchemaRejection', () => { + it('is confident when the error names the grammar', () => { + const named = [ + 'Invalid value at responseJsonSchema', + 'Unknown name "response_json_schema"', + 'responseSchema is not supported for this model', + 'Invalid JSON payload received.', + 'Unknown name "foo" at generation_config', + 'The schema is too deeply nested', + ]; + for (const message of named) { + expect(classifySchemaRejection(400, message)).toBe('confident'); + } + }); + + it('accepts a bare invalid-argument only alongside a grammar-adjacent term', () => { + expect(classifySchemaRejection(400, 'Request contains an invalid argument. generation_config is malformed')) + .toBe('catchall'); + // Gemini's way of saying the grammar was too complex to compile. + expect(classifySchemaRejection(400, 'Request contains an invalid argument. too many states for serving')) + .toBe('catchall'); + expect(classifySchemaRejection(400, 'Request contains an invalid argument. constrained decoding failed')) + .toBe('catchall'); + }); + + // The regression this guards: one unrelated 400 used to cost the model its grammar for the whole job. + it('refuses a bare invalid-argument with nothing grammar-shaped about it', () => { + expect(classifySchemaRejection(400, 'Request contains an invalid argument.')).toBeNull(); + expect(classifySchemaRejection(400, 'Request contains an invalid argument. The prompt is too long.')).toBeNull(); + expect(classifySchemaRejection(400, 'API key not valid. Please pass a valid API key.')).toBeNull(); + }); + + it('only ever classifies a 400', () => { + expect(classifySchemaRejection(429, 'responseJsonSchema is invalid')).toBeNull(); + expect(classifySchemaRejection(500, 'Invalid JSON payload received.')).toBeNull(); + expect(classifySchemaRejection(200, 'schema')).toBeNull(); + }); +}); diff --git a/packages/models/test/model/service-chunking.spec.ts b/packages/models/test/model/service-chunking.spec.ts index 4631afc6..d55b0bc4 100644 --- a/packages/models/test/model/service-chunking.spec.ts +++ b/packages/models/test/model/service-chunking.spec.ts @@ -1,15 +1,15 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { ModelRunner } from '@codra/models'; +import { ModelRunner } from '@codraoss/models'; import { createTestEnv, saveTestProviderApiKey, createTestModelRunner } from '../../../../test/helpers'; -import { defaultRepoConfig } from '@codra/schema'; -import { TokenTracker } from '@codra/core/token-tracker'; +import { defaultRepoConfig } from '@codraoss/schema'; +import { TokenTracker } from '@codraoss/core/token-tracker'; import { geminiThinkingBudgetTokens, reviewOutputBudgetTokens } from '../../src/limits'; -import { generatorFindingCap } from '@codra/core/prompts/file-review'; +import { reviewBreadth } from '@codraoss/core/prompts/file-review'; describe('ModelRunner: diff chunking', () => { afterEach(() => { @@ -69,7 +69,7 @@ describe('ModelRunner: diff chunking', () => { // 900 lines at the 800-line cap: two chunks, each its own model call. expect(fetchMock).toHaveBeenCalledTimes(2); const answerBudget = reviewOutputBudgetTokens({ - findingCap: generatorFindingCap(defaultRepoConfig.review.max_comments), + findingCap: reviewBreadth(defaultRepoConfig.review), fileCount: 1, }); for (const body of requestBodies) { diff --git a/packages/models/test/model/service-fallbacks.spec.ts b/packages/models/test/model/service-fallbacks.spec.ts index c365c189..2f79d41d 100644 --- a/packages/models/test/model/service-fallbacks.spec.ts +++ b/packages/models/test/model/service-fallbacks.spec.ts @@ -1,13 +1,12 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { isRetryableModelError } from '@codra/models'; +import { isRetryableModelError } from '@codraoss/models'; import { createTestEnv, saveTestProviderApiKey, createTestModelRunner } from '../../../../test/helpers'; -import { defaultRepoConfig } from '@codra/schema'; -import { TokenTracker } from '@codra/core/token-tracker'; +import { defaultRepoConfig } from '@codraoss/schema'; +import { TokenTracker } from '@codraoss/core/token-tracker'; -// Walking the model chain: fallback, the two subrequest-budget breakers, and marking a provider -// unavailable. The inline retry ladder lives in service-retries.spec.ts. +// Chain fallback, budget breakers, provider availability. Inline retry ladder: service-retries.spec.ts. describe('ModelRunner: chain fallback, budget breakers and provider availability', () => { afterEach(() => { vi.restoreAllMocks(); @@ -20,7 +19,7 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability JSON.stringify({ error: { code: 500, message: 'Internal error encountered.', status: 'INTERNAL' } }), { status: 500, headers: { 'content-type': 'application/json' } }, ); - // The primary makes 3 attempts before failing over; the fallback succeeds on the 4th call. + // Primary fails 3x, fallback succeeds on the 4th call. const fetchMock = vi.spyOn(globalThis, 'fetch') .mockResolvedValueOnce(gemini500()) .mockResolvedValueOnce(gemini500()) @@ -85,7 +84,7 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability expect(response.modelUsed).toBe('gemini-2.5-pro'); }); - // Parse lives inside the per-model try: an unreadable 200 is that model's failure. + // Unparseable 200 counts as that model's own failure (parse is inside the per-model try). it('falls through to the next model when the primary returns an unparseable body', async () => { const geminiText = (text: string) => new Response( JSON.stringify({ @@ -117,8 +116,7 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability expect(response.modelUsed).toBe('gemini-2.5-pro'); }); - // Three `continue` paths can leave the loop with `lastError` undefined, which matches no retry - // predicate and fails the file permanently. + // Three `continue` paths can leave `lastError` undefined, matching no retry predicate. it('defers rather than throwing undefined when every model is skipped', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch'); const env = createTestEnv(); @@ -143,15 +141,13 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability expect(fetchMock).not.toHaveBeenCalled(); }); - // Regression: the tail of the chain used to be exempt from the timeout breaker entirely, so a model - // that had never once answered on a job still cost every unit a full per-call budget -- 20 batches - // and 15 minutes of wall clock in production, all of it spent to re-learn the tally's verdict. + // Regression: the tail used to be exempt from the timeout breaker, wasting a full budget per unit. it('drops even the last candidate once it has never answered on this job', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch'); const env = createTestEnv(); await saveTestProviderApiKey(env); - // Six strikes: past the tail's higher bar, which a merely-slow model does not reach. + // Six strikes exceeds the tail's higher bar. await env.APP_KV.put( 'jobs:job-tail-drop:chain-progress', JSON.stringify({ timeouts: { 'gemini-3.1-pro-preview': 6, 'gemini-2.5-pro': 6 } }), @@ -169,15 +165,12 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability totalLineCount: 1, }); - // Deferred, and the message says which of the two skip reasons applied. await expect(promise).rejects.toThrow(/No configured review model was attempted.*repeated timeouts/); await promise.catch((error) => expect(isRetryableModelError(error)).toBe(true)); - // The whole point: not one call was paid for. expect(fetchMock).not.toHaveBeenCalled(); }); - // The other side of the same rule: a merely-slow tail still gets its shot, because deferring with no - // model attempted is the worse outcome when the model does sometimes answer. + // Same rule, other side: a merely-slow tail still gets its shot since deferring untried is worse. it('still tries the last candidate when it is only mid-chain slow', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce( new Response( @@ -209,7 +202,6 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability totalLineCount: 1, }); - // The struck primary is skipped, the tail is attempted anyway, and it answers. expect(fetchMock).toHaveBeenCalledTimes(1); expect(String(fetchMock.mock.calls[0][0])).toContain('/models/gemini-2.5-pro:generateContent'); expect(response.modelUsed).toBe('gemini-2.5-pro'); @@ -248,7 +240,7 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability const env = createTestEnv(); await saveTestProviderApiKey(env); const tracker = new TokenTracker(); - tracker.incrementSubrequests(40); // above the near-limit threshold (MAX_SUBREQUESTS 50 - SAFE_MARGIN 25) + tracker.incrementSubrequests(40); // above near-limit threshold (50 - 25 margin) const service = createTestModelRunner(env, tracker); const response = await service.reviewFile({ @@ -278,9 +270,7 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability expect(response.modelUsed).toBe('gemini-3.1-pro-preview'); }); - // The counterpart to the test above: the primary gets its shot at a merely-tight budget, but not at - // one that cannot cover the call. Previously it transmitted the prompt regardless and the runtime - // refused it, losing the unit AND the prompt -- three files' worth in one observed invocation. + // Counterpart: primary is skipped only when budget truly can't cover the call, not merely tight. it('will not commit a prompt when the budget cannot cover the call', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch'); const env = createTestEnv(); @@ -304,13 +294,11 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability // Deferred, not failed: a fresh invocation has a fresh budget. await expect(promise).rejects.toThrow(/retrying later/); await promise.catch((error) => expect(isRetryableModelError(error)).toBe(true)); - // The whole point -- nothing went over the wire. expect(fetchMock).not.toHaveBeenCalled(); }); it('skips remaining fallback models (instead of spending more of the shared budget) once near the subrequest limit', async () => { - // The primary retries internally, so return a fresh Response per call (a body reads once). - // 503, not 500: only a genuinely transient failure produces a retryable deferral. + // Fresh Response per call (body reads once); 503 not 500 keeps the failure retryable. const fetchMock = vi.spyOn(globalThis, 'fetch').mockImplementation(async () => new Response( JSON.stringify({ error: { code: 503, message: 'The model is overloaded and currently unavailable.', status: 'UNAVAILABLE' } }), @@ -320,7 +308,7 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability const env = createTestEnv(); await saveTestProviderApiKey(env); const tracker = new TokenTracker(); - tracker.incrementSubrequests(40); // above the near-limit threshold (MAX_SUBREQUESTS 50 - SAFE_MARGIN 25) + tracker.incrementSubrequests(40); // above near-limit threshold (50 - 25 margin) const service = createTestModelRunner(env, tracker); await expect( @@ -348,8 +336,6 @@ describe('ModelRunner: chain fallback, budget breakers and provider availability }), ).rejects.toSatisfy(isRetryableModelError); - // Only the primary model was attempted; the fallback was skipped rather than risking tipping - // the shared invocation over Cloudflare's subrequest cap, deferring the file for a later retry. expect(fetchMock.mock.calls.length).toBeGreaterThan(0); for (const call of fetchMock.mock.calls) { expect(String(call[0])).toContain('/models/gemini-3.1-pro-preview:generateContent'); diff --git a/packages/models/test/model/service-grammar-rejection.spec.ts b/packages/models/test/model/service-grammar-rejection.spec.ts index 3113fd08..1490ffea 100644 --- a/packages/models/test/model/service-grammar-rejection.spec.ts +++ b/packages/models/test/model/service-grammar-rejection.spec.ts @@ -1,13 +1,11 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { reviewWithGoogle } from '@codra/models/google'; -import { buildReviewResponseSchema } from '@codra/core/prompts/file-review'; +import { reviewWithGoogle } from '@codraoss/models/google'; +import { buildReviewResponseSchema } from '@codraoss/core/prompts/file-review'; import { createTestEnv, saveTestProviderApiKey, createTestModelRunner } from '../../../../test/helpers'; -import { defaultRepoConfig } from '@codra/schema'; +import { defaultRepoConfig } from '@codraoss/schema'; -// Split out of service-retries.spec.ts: a 400 matches no transient pattern, so grammar rejection is -// its own ladder rung -- drop responseJsonSchema, retry once, latch it off -- not part of the -// transient-failure ladder those specs cover. +// Split out of service-retries.spec.ts: a non-transient 400 gets its own ladder rung here. describe('ModelRunner: response-grammar rejection', () => { afterEach(() => { vi.restoreAllMocks(); @@ -32,46 +30,101 @@ describe('ModelRunner: response-grammar rejection', () => { const withGrammar = { systemPrompt: 'system', userPrompt: 'user', responseSchema: buildReviewResponseSchema(10) }; - // Google sometimes 400s with nothing but "Request contains an invalid argument." and no - // `error.details`, so none of the specific schema markers can fire. That used to fail the file on its - // first 400 -- permanently, since a 400 is not transient -- with no grammar probe and no fallback. - it('drops the response grammar and retries on a 400 that explains nothing', async () => { - const bareInvalidArgument = () => - new Response( - JSON.stringify({ error: { code: 400, message: 'Request contains an invalid argument.' } }), - { status: 400, headers: { 'content-type': 'application/json' } }, - ); + // A bare "invalid argument" with no details is Google's ACTUAL wording for some feature rejections + // -- observed in production on gemini-3.x-lite, where the identical prompt succeeds once the grammar + // is stripped. So an unexplained 400 gets a bounded probe ladder: retry without the grammar, then + // without the thinking budget, then fail for real. Refusing to probe (one earlier iteration of this + // code) burnt both lite models on every such file; probing on ANY 400 (the iteration before that) + // let one unrelated 400 latch the model into unconstrained mode for the whole job. + it('probes an unexplained 400 by stripping the grammar, then the thinking budget', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch') - .mockResolvedValueOnce(bareInvalidArgument()) - .mockResolvedValueOnce( - new Response( - JSON.stringify({ - candidates: [{ content: { parts: [{ text: '{"findings":[],"overall_correctness":"patch is correct","overall_explanation":"ok","overall_confidence_score":0.9}' }] } }], - usageMetadata: { promptTokenCount: 1, candidatesTokenCount: 1 }, - }), - { status: 200, headers: { 'content-type': 'application/json' } }, - ), - ); - - const response = await reviewWithGoogle( - { apiKey: 'test-key' }, - 'gemini-3.1-flash-lite', - { - systemPrompt: 'system', - userPrompt: 'user', - responseSchema: buildReviewResponseSchema(5), - }, + .mockResolvedValueOnce(gemini400('Request contains an invalid argument.')) + .mockResolvedValueOnce(gemini400('Request contains an invalid argument.')) + .mockResolvedValueOnce(geminiOk()); + + const response = await reviewWithGoogle({ apiKey: 'test-key' }, 'gemini-3.1-flash-lite', withGrammar); + + const bodies = fetchMock.mock.calls.map((call) => JSON.parse(String((call[1] as RequestInit).body))); + expect(bodies).toHaveLength(3); + expect(bodies[0].generationConfig.responseJsonSchema).toBeDefined(); + expect(bodies[0].generationConfig.thinkingConfig).toBeDefined(); + // First probe: grammar off, thinking still on. + expect(bodies[1].generationConfig.responseJsonSchema).toBeUndefined(); + expect(bodies[1].generationConfig.thinkingConfig).toBeDefined(); + // Second probe: both off. + expect(bodies[2].generationConfig.responseJsonSchema).toBeUndefined(); + expect(bodies[2].generationConfig.thinkingConfig).toBeUndefined(); + // Heuristic, so marked apart from a confident rejection. + expect(response.degraded).toBe('schema-dropped-catchall'); + }); + + it('fails without latching when the probes do not help', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockImplementation(async () => gemini400('Request contains an invalid argument.')); + + const error = await reviewWithGoogle({ apiKey: 'test-key' }, 'gemini-3.1-flash-lite', withGrammar) + .catch((e: unknown) => e); + + expect((error as Error).message).toMatch(/400/); + // Full attempt + two probes, then done -- the ladder is bounded by its own latches. + expect(fetchMock.mock.calls.length).toBe(3); + // NOT marked schema-dropped: the probe failed too, so it proved nothing about the grammar, and + // this flag is what latches the model into unconstrained mode for the rest of the job. + expect((error as { schemaDropped?: boolean }).schemaDropped).toBeUndefined(); + }); + + it('leaves a 400 that is not invalid-argument-shaped alone', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(gemini400('API key not valid. Please pass a valid API key.')); + + await expect( + reviewWithGoogle({ apiKey: 'test-key' }, 'gemini-3.1-flash-lite', withGrammar), + ).rejects.toThrow(/400/); + expect(fetchMock.mock.calls.length).toBe(1); + }); + + // The realistic shape of a grammar rejection: the message is the useless generic one, and the + // actionable text arrives via error.details. + it('drops the grammar when the flattened detail names the response format', async () => { + const withDetails = () => new Response( + JSON.stringify({ + error: { + code: 400, + status: 'INVALID_ARGUMENT', + message: 'Request contains an invalid argument.', + details: [{ description: 'Invalid value at generation_config.response_json_schema' }], + }, + }), + { status: 400, headers: { 'content-type': 'application/json' } }, ); + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(withDetails()) + .mockResolvedValueOnce(geminiOk()); + + const response = await reviewWithGoogle({ apiKey: 'test-key' }, 'gemini-3.1-flash-lite', withGrammar); expect(fetchMock).toHaveBeenCalledTimes(2); - // The first attempt carried the grammar and the retry did not. - const firstBody = JSON.parse(String((fetchMock.mock.calls[0][1] as RequestInit).body)); const retryBody = JSON.parse(String((fetchMock.mock.calls[1][1] as RequestInit).body)); - expect(firstBody.generationConfig.responseJsonSchema).toBeDefined(); expect(retryBody.generationConfig.responseJsonSchema).toBeUndefined(); - // Still asks for JSON, or the schema-less attempt returns prose. expect(retryBody.generationConfig.responseMimeType).toBe('application/json'); expect(response.rawText).toContain('"findings"'); + // Named confidently, so the marker is the plain one. + expect(response.degraded).toBe('schema-dropped'); + }); + + // Kept for the ambiguous middle: enough to act on, not enough to be sure. The distinct marker is what + // makes the heuristic's real hit rate answerable from `file_reviews.degraded` instead of guessed at. + it('marks a heuristic grammar drop apart from a confident one', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(gemini400('Request contains an invalid argument. too many states for serving')) + .mockResolvedValueOnce(geminiOk()); + + const response = await reviewWithGoogle({ apiKey: 'test-key' }, 'gemini-3.1-flash-lite', withGrammar); + + expect(fetchMock).toHaveBeenCalledTimes(2); + const retryBody = JSON.parse(String((fetchMock.mock.calls[1][1] as RequestInit).body)); + expect(retryBody.generationConfig.responseJsonSchema).toBeUndefined(); + expect(response.degraded).toBe('schema-dropped-catchall'); }); it('drops the grammar and retries once when Gemini rejects responseJsonSchema', async () => { @@ -85,10 +138,10 @@ describe('ModelRunner: response-grammar rejection', () => { expect(JSON.parse(String(fetchMock.mock.calls[0]?.[1]?.body)).generationConfig.responseJsonSchema).toBeDefined(); expect(JSON.parse(String(fetchMock.mock.calls[1]?.[1]?.body)).generationConfig.responseJsonSchema).toBeUndefined(); expect(response.rawText).toContain('"findings"'); - // Surfaced so the "Test connection" preflight cannot call a grammar-incapable endpoint working. + // Preflight ("Test connection") depends on this surfacing. expect(response.degraded).toBe('schema-dropped'); - // The probe is not spent on an unrelated 400, nor when there was no grammar to drop. + // Probe must not fire on an unrelated 400, nor when there was no grammar to drop. for (const [message, input] of [ ['API key not valid. Please pass a valid API key.', withGrammar], ['Invalid value at generation_config.schema.', { systemPrompt: 'system', userPrompt: 'user' }], @@ -102,13 +155,10 @@ describe('ModelRunner: response-grammar rejection', () => { } }); - // The latch used to be set only when the schema-less retry SUCCEEDED. If that retry then 429'd, - // the next call re-probed with the grammar -- a wasted 400 plus a second full prompt, every call. + // Latch used to set only when the schema-less retry succeeded; a later 429 would re-probe every call. it('latches the grammar off even when the schema-less retry itself fails', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch') - // Grammar rejected, then the schema-less probe fails for an unrelated reason. Deliberately - // not a 429: that would cool the model off and the second review would skip it entirely, - // masking whether the latch held. + // Not a 429 deliberately: that would cool the model and skip call 2, hiding whether the latch held. .mockResolvedValueOnce(gemini400('Unknown name "responseJsonSchema" at \'generation_config\'.')) .mockResolvedValueOnce(gemini400('API key not valid. Please pass a valid API key.')) .mockResolvedValue(geminiOk()); @@ -138,9 +188,7 @@ describe('ModelRunner: response-grammar rejection', () => { expect(fetchMock.mock.calls.length).toBe(callsAfterFirstReview + 1); }); - // Observed in production: Gemini 3.x sends a generic top-level message and puts the real reason - // in `details`. Without reading it the grammar rejection looked like an unrelated 400 and the model - // was dropped from the chain entirely. + // Gemini 3.x puts the real reason in error.details; without reading it this looked like an unrelated 400. it('reads the rejection reason out of error.details, not just the message', async () => { const fetchMock = vi.spyOn(globalThis, 'fetch') .mockResolvedValueOnce(new Response( @@ -169,8 +217,7 @@ describe('ModelRunner: response-grammar rejection', () => { }); it('gives the attempt back for the probe, but only once', async () => { - // The probe isn't a transient rung: without the give-back, a ladder spent on 5xx could never - // drop the schema. The latch stops it looping. + // Without the give-back, a ladder spent on 5xx could never drop the schema. const gemini500 = () => new Response(JSON.stringify({ error: { code: 500, message: 'Internal error encountered.' } }), { status: 500, headers: { 'content-type': 'application/json' } }); const ladderSpent = vi.spyOn(globalThis, 'fetch') .mockResolvedValueOnce(gemini500()) @@ -184,7 +231,7 @@ describe('ModelRunner: response-grammar rejection', () => { expect(JSON.parse(String(ladderSpent.mock.calls[3]?.[1]?.body)).generationConfig.responseJsonSchema).toBeUndefined(); expect(response.degraded).toBe('schema-dropped'); - // mockImplementation, not mockResolvedValue: a retried call cannot re-read one Response body. + // mockImplementation: a retried call can't reread one Response body. vi.restoreAllMocks(); const persistent = vi.spyOn(globalThis, 'fetch') .mockImplementation(async () => gemini400('Invalid JSON payload received. Unknown name "responseJsonSchema".')); @@ -193,7 +240,6 @@ describe('ModelRunner: response-grammar rejection', () => { reviewWithGoogle({ apiKey: 'test-key' }, 'gemini-3.1-pro-preview', withGrammar), ).rejects.toThrow(/400/); - // Two, not three and not unbounded: one with the grammar, one without, then throw. expect(persistent).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/models/test/model/service-requests.spec.ts b/packages/models/test/model/service-requests.spec.ts index b8728877..d18ed514 100644 --- a/packages/models/test/model/service-requests.spec.ts +++ b/packages/models/test/model/service-requests.spec.ts @@ -1,10 +1,10 @@ import { afterEach, describe, expect, it } from 'vitest'; -import { reviewWithCloudflare } from '@codra/models/cloudflare'; -import { reviewWithGoogle } from '@codra/models/google'; +import { reviewWithCloudflare } from '@codraoss/models/cloudflare'; +import { reviewWithGoogle } from '@codraoss/models/google'; -import { buildBatchReviewResponseSchema, buildReviewResponseSchema } from '@codra/core/prompts/file-review'; -import { VERIFY_RESPONSE_SCHEMA } from '@codra/core/prompts/verify'; +import { buildBatchReviewResponseSchema, buildReviewResponseSchema } from '@codraoss/core/prompts/file-review'; +import { VERIFY_RESPONSE_SCHEMA } from '@codraoss/core/prompts/verify'; import { createTestEnv, saveTestProviderApiKey, createTestModelRunner } from '../../../../test/helpers'; @@ -115,7 +115,7 @@ describe('ModelRunner: request shape and response handling', () => { it('sends the caller\'s grammar as responseJsonSchema, or none at all', async () => { const review = await captureGeminiBody({ systemPrompt: 'system', userPrompt: 'user', responseSchema: buildReviewResponseSchema(10) }); expect(schemaKeys(review)).toEqual(['responseJsonSchema']); - expect(review.generationConfig.responseJsonSchema.properties.findings.maxItems).toBe(20); + expect(review.generationConfig.responseJsonSchema.properties.findings.maxItems).toBe(10); expect(review.generationConfig.responseMimeType).toBe('application/json'); // No `outputBudgetTokens` on this input, so the adapter's own default answer budget applies -- and // the bounded thinking budget is added ON TOP of it, never carved out of it. diff --git a/packages/models/test/model/service-retries.spec.ts b/packages/models/test/model/service-retries.spec.ts index 64acd4e3..d01f2f07 100644 --- a/packages/models/test/model/service-retries.spec.ts +++ b/packages/models/test/model/service-retries.spec.ts @@ -1,10 +1,10 @@ import { afterEach, describe, expect, it, vi } from 'vitest'; -import { isRetryableModelError } from '@codra/models'; -import { reviewWithCloudflare } from '@codra/models/cloudflare'; -import { reviewWithGoogle } from '@codra/models/google'; +import { isRetryableModelError } from '@codraoss/models'; +import { reviewWithCloudflare } from '@codraoss/models/cloudflare'; +import { reviewWithGoogle } from '@codraoss/models/google'; import { MODEL_TIMEOUT_MAX_MS } from '../../src/limits'; import { createTestEnv, saveTestProviderApiKey, createTestModelRunner } from '../../../../test/helpers'; -import { defaultRepoConfig } from '@codra/schema'; +import { defaultRepoConfig } from '@codraoss/schema'; // The retry ladder: inline retries, Retry-After, and which exhausted runs report as retryable. describe('ModelRunner: transient failures and the retry ladder', () => { diff --git a/packages/models/test/model/truncation.spec.ts b/packages/models/test/model/truncation.spec.ts new file mode 100644 index 00000000..e6cebd55 --- /dev/null +++ b/packages/models/test/model/truncation.spec.ts @@ -0,0 +1,173 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { generateKeyPairSync } from 'node:crypto'; + +import { reviewWithGoogle } from '@codraoss/models/google'; +import { reviewWithVertex } from '@codraoss/models/vertex'; +import { geminiThinkingBudgetTokens, OUTPUT_TOKENS_FLOOR } from '../../src/limits'; + +const ANSWER = '{"findings":[],"overall_correctness":"patch is correct","overall_explanation":"ok","overall_confidence_score":0.9}'; +const THINKING = geminiThinkingBudgetTokens(OUTPUT_TOKENS_FLOOR); +const FIRST_CEILING = OUTPUT_TOKENS_FLOOR + THINKING; +const RAISED_CEILING = 2 * OUTPUT_TOKENS_FLOOR + THINKING; + +function geminiResponse(finishReason: string, text: string) { + return new Response( + JSON.stringify({ + candidates: [{ content: { parts: text ? [{ text }] : [] }, finishReason }], + usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 400, thoughtsTokenCount: 9_826 }, + }), + { status: 200, headers: { 'content-type': 'application/json' } }, + ); +} + +const PARTIAL = '{"findings":[{"path":"a.ts","line":1,"severity":"P1","claim_type":"bug","title":"x"'; + +function bodiesOf(fetchMock: { mock: { calls: unknown[][] } }) { + return fetchMock.mock.calls.map((call) => JSON.parse(String((call[1] as RequestInit).body))); +} + +describe('Gemini truncation handling', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + const config = { apiKey: 'key', providerName: 'Google' }; + const input = { systemPrompt: 'system', userPrompt: 'user', truncationIntolerant: true }; + + it('re-probes once with a larger ceiling when the answer is cut off', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(geminiResponse('MAX_TOKENS', PARTIAL)) + .mockResolvedValueOnce(geminiResponse('STOP', ANSWER)); + + const result = await reviewWithGoogle(config, 'gemini-2.5-flash', input); + + const sent = bodiesOf(fetchMock); + expect(sent).toHaveLength(2); + expect(sent[0].generationConfig.maxOutputTokens).toBe(FIRST_CEILING); + expect(sent[1].generationConfig.maxOutputTokens).toBe(RAISED_CEILING); + expect(sent[1].generationConfig.thinkingConfig).toEqual({ thinkingBudget: THINKING }); + expect(result.rawText).toContain('patch is correct'); + }); + + it('re-probes at most once, then fails with the partial text attached', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockImplementation(async () => geminiResponse('MAX_TOKENS', PARTIAL)); + + const error = await reviewWithGoogle(config, 'gemini-2.5-flash', input).catch((e: unknown) => e); + + expect(error).toBeInstanceOf(Error); + expect((error as Error).message).toMatch(/no reviewable output.*MAX_TOKENS/i); + expect((error as { partialResponse?: { rawText: string; outputTokens: number } }).partialResponse) + .toMatchObject({ rawText: PARTIAL, outputTokens: 400, modelUsed: 'gemini-2.5-flash' }); + expect(bodiesOf(fetchMock)).toHaveLength(2); + }); + + it('leaves a truncated answer alone for callers that can use a partial one', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(geminiResponse('MAX_TOKENS', PARTIAL)); + + const result = await reviewWithGoogle(config, 'gemini-2.5-flash', { systemPrompt: 's', userPrompt: 'u' }); + + expect(result.rawText).toBe(PARTIAL); + expect(bodiesOf(fetchMock)).toHaveLength(1); + }); + + it('does not re-probe a non-MAX_TOKENS stop reason', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(geminiResponse('SAFETY', '')); + + await expect(reviewWithGoogle(config, 'gemini-2.5-flash', input)).rejects.toThrow(/finishReason=SAFETY/); + expect(bodiesOf(fetchMock)).toHaveLength(1); + }); + + it('skips the re-probe when the remaining time cannot deliver the extra tokens', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(geminiResponse('MAX_TOKENS', PARTIAL)); + + await expect( + reviewWithGoogle({ ...config, timeoutMs: 1_000 }, 'gemini-2.5-flash', input), + ).rejects.toThrow(/no reviewable output/i); + expect(bodiesOf(fetchMock)).toHaveLength(1); + }); +}); + +describe('Vertex truncation handling', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + const { privateKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + }); + + // Tokens are cached per client_email for the isolate's life; unique accounts avoid cross-test reuse. + let account = 0; + function freshConfig() { + account += 1; + return { + apiKey: JSON.stringify({ + client_email: 'codra-trunc-' + account + '@example.iam.gserviceaccount.com', + private_key: privateKey, + }), + baseUrl: 'https://us-central1-aiplatform.googleapis.com/v1/projects/p/locations/us-central1', + providerName: 'Google Vertex AI', + }; + } + + function tokenOk() { + return new Response(JSON.stringify({ access_token: 'token', expires_in: 3600 }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); + } + + function generateBodies(fetchMock: { mock: { calls: unknown[][] } }) { + return fetchMock.mock.calls + .filter((call) => String(call[0]).includes(':generateContent')) + .map((call) => JSON.parse(String((call[1] as RequestInit).body))); + } + + const input = { systemPrompt: 'system', userPrompt: 'user', truncationIntolerant: true }; + + it('re-probes once with a larger ceiling and keeps the thinking budget', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(geminiResponse('MAX_TOKENS', PARTIAL)) + .mockResolvedValueOnce(geminiResponse('STOP', ANSWER)); + + const result = await reviewWithVertex(freshConfig(), 'gemini-2.5-flash', input); + + const sent = generateBodies(fetchMock); + expect(sent).toHaveLength(2); + expect(sent[0].generationConfig.maxOutputTokens).toBe(FIRST_CEILING); + expect(sent[1].generationConfig.maxOutputTokens).toBe(RAISED_CEILING); + expect(sent[1].generationConfig.thinkingConfig).toEqual({ thinkingBudget: THINKING }); + expect(result.rawText).toContain('patch is correct'); + }); + + it('fails with the partial text attached when the re-probe is still cut off', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockImplementation(async () => geminiResponse('MAX_TOKENS', PARTIAL)); + + const error = await reviewWithVertex(freshConfig(), 'gemini-2.5-flash', input).catch((e: unknown) => e); + + expect((error as Error).message).toMatch(/no reviewable output.*MAX_TOKENS/i); + expect((error as { partialResponse?: { rawText: string } }).partialResponse) + .toMatchObject({ rawText: PARTIAL, provider: 'Google Vertex AI' }); + expect(generateBodies(fetchMock)).toHaveLength(2); + }); + + it('leaves a truncated answer alone for callers that can use a partial one', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(geminiResponse('MAX_TOKENS', PARTIAL)); + + const result = await reviewWithVertex(freshConfig(), 'gemini-2.5-flash', { systemPrompt: 's', userPrompt: 'u' }); + + expect(result.rawText).toBe(PARTIAL); + expect(generateBodies(fetchMock)).toHaveLength(1); + }); +}); diff --git a/packages/models/test/model/verify-budget.spec.ts b/packages/models/test/model/verify-budget.spec.ts new file mode 100644 index 00000000..ff52ea92 --- /dev/null +++ b/packages/models/test/model/verify-budget.spec.ts @@ -0,0 +1,94 @@ +import { describe, expect, it } from 'vitest'; +import { + MODEL_FALLBACK_CHAIN_BUDGET_MS, + MODEL_TIMEOUT_BASE_MS, + MODEL_TIMEOUT_MAX_MS, + VERIFY_TIMEOUT_FLOOR_MS, + adaptiveModelTimeoutMs, + chainAttemptTimeoutMs, + verifyTimeoutMs, +} from '../../src/limits'; + +// Verification silently skipped in production, and the arithmetic is why: it borrowed +// `adaptiveModelTimeoutMs` with `candidates * 8` standing in for diff lines, which for any realistic +// finding count sat under the 100-line free allowance and collapsed to the 20s BASE. Two rungs timed +// out at 20s each, the fixed-timeout chain check then judged that no third full attempt would fit, and +// two configured models were never tried. + +describe('verifyTimeoutMs', () => { + // The regression that mattered: the old proxy gave the same 20s to 1 finding and to 12. + it('does not collapse to the base timeout for a normal finding count', () => { + for (const candidates of [1, 5, 10, 12]) { + const old = adaptiveModelTimeoutMs(candidates * 8); + expect(old).toBe(MODEL_TIMEOUT_BASE_MS); + expect(verifyTimeoutMs(candidates)).toBeGreaterThan(old); + } + }); + + it('floors at a workable amount and grows with the candidate count', () => { + expect(verifyTimeoutMs(0)).toBe(VERIFY_TIMEOUT_FLOOR_MS); + expect(verifyTimeoutMs(10)).toBe(VERIFY_TIMEOUT_FLOOR_MS); + expect(verifyTimeoutMs(20)).toBeGreaterThan(verifyTimeoutMs(10)); + expect(verifyTimeoutMs(40)).toBeGreaterThan(verifyTimeoutMs(20)); + }); + + it('never exceeds the per-call ceiling', () => { + // 40 is verifyCandidateLimit's maximum; the ceiling exists to leave room for a failover. + expect(verifyTimeoutMs(40)).toBeLessThanOrEqual(MODEL_TIMEOUT_MAX_MS); + expect(verifyTimeoutMs(10_000)).toBe(MODEL_TIMEOUT_MAX_MS); + }); +}); + +describe('the verification chain fits two real attempts', () => { + /** Replays how the chain grants time, rung by rung, with each rung spending its whole grant. */ + function walk(candidates: number, models: number) { + const requested = verifyTimeoutMs(candidates); + const grants: number[] = []; + let elapsed = 0; + + for (let i = 0; i < models; i++) { + const grant = chainAttemptTimeoutMs({ + requestedMs: requested, + remainingChainMs: MODEL_FALLBACK_CHAIN_BUDGET_MS - elapsed, + hasAnotherModel: i < models - 1, + }); + // The head is guaranteed an attempt; a fallback with no room stops the chain. + if (grant === 0 && i > 0) break; + grants.push(Math.max(grant, 8_000)); + elapsed += grants[grants.length - 1]; + } + return { requested, grants, elapsed }; + } + + // The production shape: 4 configured models, a dozen findings, every attempt timing out. + it('tries at least two models even when every attempt burns its full grant', () => { + const { grants } = walk(12, 4); + + expect(grants.length).toBeGreaterThanOrEqual(2); + // And the head gets more than the 20s that was timing out. + expect(grants[0]).toBeGreaterThan(MODEL_TIMEOUT_BASE_MS); + }); + + it('keeps the whole chain inside the invocation budget', () => { + for (const candidates of [1, 12, 25, 40]) { + const { elapsed } = walk(candidates, 4); + expect(elapsed).toBeLessThanOrEqual(MODEL_FALLBACK_CHAIN_BUDGET_MS); + } + }); + + it('gives a single-model chain its full request', () => { + const { requested, grants } = walk(12, 1); + + expect(grants).toEqual([requested]); + }); + + // A rung must never be handed a slice too small to answer in -- that spends a subrequest to + // guarantee another timeout. + it('never grants a viable rung less than the minimum attempt', () => { + for (const candidates of [1, 12, 40]) { + for (const grant of walk(candidates, 4).grants) { + expect(grant).toBeGreaterThanOrEqual(8_000); + } + } + }); +}); diff --git a/packages/models/test/model/vertex.spec.ts b/packages/models/test/model/vertex.spec.ts new file mode 100644 index 00000000..01541b0a --- /dev/null +++ b/packages/models/test/model/vertex.spec.ts @@ -0,0 +1,192 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { generateKeyPairSync } from 'node:crypto'; + +import { reviewWithVertex } from '@codraoss/models/vertex'; +import { geminiThinkingBudgetTokens, OUTPUT_TOKENS_FLOOR } from '../../src/limits'; + +describe('reviewWithVertex: thinking budget and the rejection latch', () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + // Vertex signs a JWT with the key, so it must be a genuine PKCS8 key, not a placeholder string. + const { privateKey } = generateKeyPairSync('rsa', { + modulusLength: 2048, + privateKeyEncoding: { type: 'pkcs8', format: 'pem' }, + publicKeyEncoding: { type: 'spki', format: 'pem' }, + }); + + // Tokens are cached per client_email for the isolate's life; unique accounts avoid cross-test reuse. + let account = 0; + function freshConfig() { + account += 1; + return { + apiKey: JSON.stringify({ + client_email: `codra-${account}@example.iam.gserviceaccount.com`, + private_key: privateKey, + }), + baseUrl: 'https://us-central1-aiplatform.googleapis.com/v1/projects/p/locations/us-central1', + providerName: 'Google Vertex AI', + }; + } + + const input = { systemPrompt: 'system', userPrompt: 'user' }; + + function tokenOk() { + return new Response(JSON.stringify({ access_token: 'token', expires_in: 3600 }), { + status: 200, + headers: { 'content-type': 'application/json' }, + }); + } + + function vertexOk(text = '{"findings":[],"overall_correctness":"patch is correct","overall_explanation":"ok","overall_confidence_score":0.9}') { + return new Response( + JSON.stringify({ + candidates: [{ content: { parts: [{ text }] }, finishReason: 'STOP' }], + usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 20, thoughtsTokenCount: 5 }, + }), + { status: 200, headers: { 'content-type': 'application/json' } }, + ); + } + + function vertex400(message: string) { + return new Response( + JSON.stringify({ error: { code: 400, status: 'INVALID_ARGUMENT', message } }), + { status: 400, headers: { 'content-type': 'application/json' } }, + ); + } + + function vertex429() { + return new Response( + JSON.stringify({ error: { code: 429, message: 'Resource exhausted. Please try again later.' } }), + { status: 429, headers: { 'content-type': 'application/json' } }, + ); + } + + function generateBodies(fetchMock: { mock: { calls: unknown[][] } }) { + return fetchMock.mock.calls + .filter((call) => String(call[0]).includes(':generateContent')) + .map((call) => JSON.parse(String((call[1] as RequestInit).body))); + } + + it('sends a bounded thinking budget and reserves room for it in maxOutputTokens', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(vertexOk()); + + await reviewWithVertex(freshConfig(), 'gemini-2.5-flash', input); + + const [body] = generateBodies(fetchMock); + const expectedThinking = geminiThinkingBudgetTokens(OUTPUT_TOKENS_FLOOR); + + expect(body.generationConfig.thinkingConfig).toEqual({ thinkingBudget: expectedThinking }); + expect(body.generationConfig.maxOutputTokens).toBe(OUTPUT_TOKENS_FLOOR + expectedThinking); + }); + + it('scales the budget with the requested output and keeps thinking a minority of the ceiling', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(vertexOk()); + + await reviewWithVertex(freshConfig(), 'gemini-2.5-pro', { ...input, outputBudgetTokens: 32_768 }); + + const [body] = generateBodies(fetchMock); + const thinking = body.generationConfig.thinkingConfig.thinkingBudget; + + expect(thinking).toBe(geminiThinkingBudgetTokens(32_768)); + expect(thinking).toBeGreaterThanOrEqual(1_024); + expect(thinking).toBeLessThan(body.generationConfig.maxOutputTokens / 3); + expect(body.generationConfig.maxOutputTokens).toBe(32_768 + thinking); + }); + + it('drops thinkingConfig and resends once when the model refuses it', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(vertex400('Unable to submit request because thinking is not supported by this model.')) + .mockResolvedValueOnce(vertexOk()); + + const result = await reviewWithVertex(freshConfig(), 'gemini-2.5-flash', input); + + const bodies = generateBodies(fetchMock); + expect(bodies).toHaveLength(2); + expect(bodies[0].generationConfig.thinkingConfig).toBeDefined(); + expect(bodies[1].generationConfig.thinkingConfig).toBeUndefined(); + expect(bodies[1].generationConfig.maxOutputTokens).toBe(bodies[0].generationConfig.maxOutputTokens); + expect(result.rawText).toContain('patch is correct'); + }); + + it('latches the refusal so a second failure is not retried again', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(vertex400('thinking is not supported')) + .mockResolvedValueOnce(vertex400('thinking is not supported')); + + await expect(reviewWithVertex(freshConfig(), 'gemini-2.5-flash', input)).rejects.toThrow(/400/); + expect(generateBodies(fetchMock)).toHaveLength(2); + }); + + it('does not treat an unrelated 400 as a thinking rejection', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(vertex400('Request payload size exceeds the limit.')); + + await expect(reviewWithVertex(freshConfig(), 'gemini-2.5-flash', input)).rejects.toThrow(/payload size/); + expect(generateBodies(fetchMock)).toHaveLength(1); + }); + + it('still resends on a 429 after the thinking latch has fired', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(vertex400('thinking is not supported')) + .mockResolvedValueOnce(vertex429()) + .mockResolvedValueOnce(vertexOk()); + + const result = await reviewWithVertex(freshConfig(), 'gemini-2.5-flash', input); + + const bodies = generateBodies(fetchMock); + expect(bodies).toHaveLength(3); + expect(bodies[1].generationConfig.thinkingConfig).toBeUndefined(); + expect(bodies[2].generationConfig.thinkingConfig).toBeUndefined(); + expect(result.rawText).toContain('patch is correct'); + }, 20_000); + + it('resends an unchanged request on a plain 429', async () => { + const fetchMock = vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(vertex429()) + .mockResolvedValueOnce(vertexOk()); + + await reviewWithVertex(freshConfig(), 'gemini-2.5-flash', input); + + const bodies = generateBodies(fetchMock); + expect(bodies).toHaveLength(2); + expect(bodies[0]).toEqual(bodies[1]); + }, 20_000); + + it('fails rather than returning nothing when the budget is spent before any answer', async () => { + vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(new Response( + JSON.stringify({ + candidates: [{ content: { parts: [] }, finishReason: 'MAX_TOKENS' }], + usageMetadata: { promptTokenCount: 10, candidatesTokenCount: 0, thoughtsTokenCount: 9_826 }, + }), + { status: 200, headers: { 'content-type': 'application/json' } }, + )); + + await expect(reviewWithVertex(freshConfig(), 'gemini-2.5-flash', input)) + .rejects.toThrow(/no reviewable output.*MAX_TOKENS/i); + }); + + it('reports reasoning spend in usage so a truncation can be attributed', async () => { + vi.spyOn(globalThis, 'fetch') + .mockResolvedValueOnce(tokenOk()) + .mockResolvedValueOnce(vertexOk()); + + const result = await reviewWithVertex(freshConfig(), 'gemini-2.5-flash', input); + + expect(result.inputTokens).toBe(10); + expect(result.outputTokens).toBe(20); + expect(result.modelUsed).toBe('gemini-2.5-flash'); + }); +}); diff --git a/packages/models/test/url-guard.spec.ts b/packages/models/test/url-guard.spec.ts index e369c411..d5ec2e06 100644 --- a/packages/models/test/url-guard.spec.ts +++ b/packages/models/test/url-guard.spec.ts @@ -1,13 +1,8 @@ import { describe, expect, it } from 'vitest'; import { assertPublicBaseUrl, isPrivateHost, isValidPublicUrl } from '../src/url-guard'; -import { ProviderRequestError } from '@codra/models/types'; +import { ProviderRequestError } from '../src/types'; -// A provider's base URL comes from the dashboard and is then fetched server-side, so an unguarded -// adapter turns that form into an SSRF primitive. -// -// The guard existed in the Google and OpenAI adapters and was simply missing from Anthropic, which -// fetched `config.baseUrl` unchecked - the copy-paste is why nobody noticed. These tests cover the -// shared module so all three are protected by the same assertions. +// Guards SSRF via user-supplied provider base URLs (was missing from Anthropic adapter). describe('provider base URL guard', () => { it('rejects loopback, link-local and RFC1918 hosts', () => { const blocked = [ @@ -17,17 +12,14 @@ describe('provider base URL guard', () => { 'http://192.168.1.1/v1', 'http://172.16.0.1/v1', 'http://172.31.255.255/v1', - // 169.254.0.0/16 is where the AWS/Azure metadata service lives. - 'http://169.254.169.254/latest/meta-data', + 'http://169.254.169.254/latest/meta-data', // cloud metadata range ]; for (const url of blocked) { expect(isValidPublicUrl(url), url).toBe(false); } }); - // The original guard carried `/^::1$/`, which never matched: `URL.hostname` returns an IPv6 - // literal with its brackets ("[::1]"). IPv6 loopback and the unique-local/link-local ranges were - // therefore reachable in both adapters that had a guard at all. + // hostname includes brackets, e.g. "[::1]"; a bare /^::1$/ regex would miss it it('rejects IPv6 private ranges, brackets and all', () => { const blocked = [ 'http://[::1]/v1', @@ -40,12 +32,9 @@ describe('provider base URL guard', () => { for (const url of blocked) { expect(isValidPublicUrl(url), url).toBe(false); } - // A genuinely public IPv6 address must still pass. expect(isValidPublicUrl('http://[2606:4700::1111]/v1')).toBe(true); }); - // Public-looking names that resolve only from inside a cloud instance, so the range checks alone - // would let them through. it('rejects cloud metadata endpoints by name', () => { expect(isValidPublicUrl('http://metadata.google.internal/computeMetadata/v1')).toBe(false); expect(isValidPublicUrl('http://100.100.100.200/latest/meta-data')).toBe(false); @@ -62,8 +51,7 @@ describe('provider base URL guard', () => { expect(isValidPublicUrl('https://api.anthropic.com/v1')).toBe(true); expect(isValidPublicUrl('https://generativelanguage.googleapis.com/v1beta')).toBe(true); expect(isValidPublicUrl('https://api.openai.com/v1')).toBe(true); - // 172.32 is outside the private 172.16-172.31 block, so the range regex must not over-match. - expect(isValidPublicUrl('http://172.32.0.1/v1')).toBe(true); + expect(isValidPublicUrl('http://172.32.0.1/v1')).toBe(true); // outside 172.16-172.31 block }); it('classifies hosts without needing a full URL', () => { @@ -83,8 +71,6 @@ describe('provider base URL guard', () => { } }); - // Every adapter defaults to its own vendor URL when none is configured, so an absent base URL - // must pass rather than throw. it('accepts an absent base URL', () => { expect(() => assertPublicBaseUrl(null, 'Anthropic')).not.toThrow(); expect(() => assertPublicBaseUrl(undefined, 'Google')).not.toThrow(); diff --git a/packages/models/tsconfig.json b/packages/models/tsconfig.json index 6e47b8c8..284e4ff4 100644 --- a/packages/models/tsconfig.json +++ b/packages/models/tsconfig.json @@ -9,5 +9,24 @@ "emitDeclarationOnly": false, "noEmit": true }, - "include": ["src/**/*", "test/**/*"] + "include": ["src/**/*", "test/**/*"], + + // These specs drive the runner against the app-level harness (`test/helpers.ts`), which reaches into + // `apps/worker` and therefore into Cloudflare globals like ExecutionContext and KVNamespace. This + // project deliberately omits @cloudflare/workers-types -- the same guardrail packages/core relies on, + // so a stray KVNamespace in package SOURCE cannot resolve -- so those globals are unresolvable here + // and the project failed on files it was never meant to own. + // + // Nothing is left unchecked: the ROOT program's include covers `packages/*/test/**` and does pull in + // the worker type declarations, so these files are typechecked there. Adding workers-types here + // instead would typecheck them twice and quietly retire the guardrail. + "exclude": [ + "test/model/chain-resume.spec.ts", + "test/model/config-cache.spec.ts", + "test/model/service-chunking.spec.ts", + "test/model/service-fallbacks.spec.ts", + "test/model/service-grammar-rejection.spec.ts", + "test/model/service-requests.spec.ts", + "test/model/service-retries.spec.ts" + ] } diff --git a/packages/models/tsup.config.json b/packages/models/tsup.config.json new file mode 100644 index 00000000..67531a67 --- /dev/null +++ b/packages/models/tsup.config.json @@ -0,0 +1,18 @@ +{ + "entry": [ + "src/index.ts", + "src/types.ts", + "src/runner.ts", + "src/providers/cloudflare.ts", + "src/providers/google.ts", + "src/providers/vertex.ts", + "src/providers/anthropic.ts", + "src/providers/openai.ts" + ], + "format": "esm", + "dts": true, + "splitting": true, + "sourcemap": true, + "clean": true, + "outDir": "dist" +} diff --git a/packages/provider-github/LICENSE b/packages/provider-github/LICENSE new file mode 100644 index 00000000..024299ea --- /dev/null +++ b/packages/provider-github/LICENSE @@ -0,0 +1,625 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 2007 Free Software Foundation, Inc. <>_ + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### 0. Definitions +“This License” refers to version 3 of the GNU Affero General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based +on the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that **(1)** displays an appropriate copyright notice, and **(2)** +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code +The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other +than the work as a whole, that **(a)** is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and **(b)** serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +### 2. Basic Permissions +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +### 4. Conveying Verbatim Copies +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified +it, and giving a relevant date. +* **b)** The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. +This requirement modifies the requirement in section 4 to +“keep intact all notices”. +* **c)** You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. +* **d)** If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either **(1)** a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or **(2)** access to copy the +Corresponding Source from a network server at no charge. +* **c)** Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. +* **d)** Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A “User Product” is either **(1)** a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or **(2)** anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install + +### 6. Conveying Non-Source Forms +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or +authors of the material; or +* **e)** Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +### 8. Termination +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated **(a)** +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and **(b)** permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either **(1)** cause the Corresponding Source to be so +available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license **(a)** in connection with copies of the covered work +conveyed by you (or copies made from those copies), or **(b)** primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found. + + Codra: Open source PR review infrastructure for Cloudflare Workers. + Copyright (C) 2026 Devarshi Shimpi + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source” link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<>. diff --git a/packages/provider-github/README.md b/packages/provider-github/README.md new file mode 100644 index 00000000..66c75fc9 --- /dev/null +++ b/packages/provider-github/README.md @@ -0,0 +1,15 @@ +# @codraoss/provider-github + +GitHub git-provider adapter for Codra: App auth, webhooks, OAuth, and review posting. + +Part of [Codra](https://codra.run), an open-source code review engine. See the [monorepo](https://github.com/devarshishimpi/codra) for development, and [CONTRIBUTING](https://github.com/devarshishimpi/codra/blob/main/CONTRIBUTING.md) for the dual-licensing / CLA details. + +## Install + +```bash +npm install @codraoss/provider-github +``` + +## License + +[AGPL-3.0-only](./LICENSE) © Devarshi Shimpi diff --git a/packages/provider-github/package.json b/packages/provider-github/package.json index 831124fb..460e0b2a 100644 --- a/packages/provider-github/package.json +++ b/packages/provider-github/package.json @@ -1,19 +1,62 @@ { - "name": "@codra/provider-github", + "name": "@codraoss/provider-github", "version": "0.9.4", - "private": true, + "description": "GitHub git-provider adapter for Codra: App auth, webhooks, OAuth, and review posting.", + "author": "Devarshi Shimpi", + "license": "AGPL-3.0-only", + "homepage": "https://codra.run", + "repository": { + "type": "git", + "url": "git+https://github.com/devarshishimpi/codra.git", + "directory": "packages/provider-github" + }, + "bugs": { + "url": "https://github.com/devarshishimpi/codra/issues" + }, "type": "module", + "sideEffects": false, "exports": { ".": "./src/index.ts", "./oauth": "./src/oauth.ts", "./webhook": "./src/webhook.ts" }, + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./oauth": { + "types": "./dist/oauth.d.ts", + "import": "./dist/oauth.js" + }, + "./webhook": { + "types": "./dist/webhook.d.ts", + "import": "./dist/webhook.js" + } + } + }, "scripts": { + "build": "tsup", "typecheck": "tsc -p tsconfig.json", - "test": "vitest run" + "test": "vitest run", + "prepack": "node ../../scripts/swap-publish-exports.mjs promote", + "postpack": "node ../../scripts/swap-publish-exports.mjs restore" }, "dependencies": { - "@codra/core": "*", - "@codra/schema": "*" + "@codraoss/core": "^0.9.4", + "@codraoss/schema": "^0.9.4" + }, + "devDependencies": { + "tsup": "^8.0.0" } } diff --git a/packages/provider-github/src/app-auth.ts b/packages/provider-github/src/app-auth.ts index 046fa2e7..afa78e49 100644 --- a/packages/provider-github/src/app-auth.ts +++ b/packages/provider-github/src/app-auth.ts @@ -1,5 +1,5 @@ import type { AppBindingsConfig } from './service'; -import { withTimeout } from '@codra/core/timeout'; +import { withTimeout } from '@codraoss/core/timeout'; import { assertResponseOk, installationCacheKey, withRetry } from './http'; import type { GitHubAppRecord, GitHubInstallation, InstallationTokenCacheRecord } from './types'; import { GITHUB_TIMEOUT_MS, GITHUB_APP_INSTALL_URL_CACHE_KEY } from './constants'; diff --git a/packages/provider-github/src/client.ts b/packages/provider-github/src/client.ts index 27d0dce8..ab4902b7 100644 --- a/packages/provider-github/src/client.ts +++ b/packages/provider-github/src/client.ts @@ -1,5 +1,5 @@ import type { AppBindingsConfig } from './service'; -import { withTimeout } from '@codra/core/timeout'; +import { withTimeout } from '@codraoss/core/timeout'; import { GitHubError, assertResponseOk, @@ -19,6 +19,7 @@ import { import { fetchCompareDiff, fetchPullRequestDiff } from './diff-fetch'; import { findBotReviewForCommit, postReview } from './review-post'; import { addIssueLabels, ensureLabel, listIssueLabels, removeIssueLabel } from './labels'; +import { addIssueReaction } from './reactions'; import type { GitHubInstallation, GitHubRepository, @@ -183,9 +184,12 @@ export class GitHubClient { return fetchCompareDiff(this.ctx(), owner, repo, base, head); } - async getRepoFileOrNull(owner: string, repo: string, path: string) { - return withRetry(`getRepoFileOrNull ${owner}/${repo}/${path}`, async () => { - const response = await this.request(`${repoApiPath(owner, repo)}/contents/${encodeGitHubContentPath(path)}`); + // File content at a specific commit; without `ref`, GitHub answers from the default branch instead. + // Never retried: a secondary rate limit here would sleep 60-120s, worse than just skipping the context. + async getRepoFile(owner: string, repo: string, path: string, ref?: string) { + return withRetry(`getRepoFile ${owner}/${repo}/${path}`, async () => { + const query = ref ? `?ref=${encodeURIComponent(ref)}` : ''; + const response = await this.request(`${repoApiPath(owner, repo)}/contents/${encodeGitHubContentPath(path)}${query}`); if (response.status === 404) return null; await assertResponseOk(response, path, 'GitHub repo file fetch'); @@ -193,9 +197,13 @@ export class GitHubClient { if (!data.content) { return null; } + if (data.encoding !== 'base64') return data.content; - return data.encoding === 'base64' ? atob(data.content.replace(/\n/g, '')) : data.content; - }); + // atob alone yields one code unit per byte, mangling non-ASCII source files; decode as UTF-8 instead. + const binary = atob(data.content.replace(/\n/g, '')); + const bytes = Uint8Array.from(binary, (char) => char.charCodeAt(0)); + return new TextDecoder().decode(bytes); + }, 0); } async createCheckRun( @@ -287,6 +295,10 @@ export class GitHubClient { return addIssueLabels(this.ctx(), owner, repo, issueNumber, labels); } + async addIssueReaction(owner: string, repo: string, issueNumber: number, content: '+1') { + return addIssueReaction(this.ctx(), owner, repo, issueNumber, content); + } + async listIssueLabels(owner: string, repo: string, issueNumber: number) { return listIssueLabels(this.ctx(), owner, repo, issueNumber); } diff --git a/packages/provider-github/src/diff-fetch.ts b/packages/provider-github/src/diff-fetch.ts index d8806c47..bf66502f 100644 --- a/packages/provider-github/src/diff-fetch.ts +++ b/packages/provider-github/src/diff-fetch.ts @@ -1,5 +1,5 @@ -import { logger } from '@codra/core/logger'; -import { buildUnifiedDiffFromFiles, type DiffFileEntry } from '@codra/core/diff'; +import { logger } from '@codraoss/core/logger'; +import { buildUnifiedDiffFromFiles, type DiffFileEntry } from '@codraoss/core/diff'; import { type GitHubRequestContext, isDiffTooLargeError, repoApiPath, withRetry } from './http'; import { DIFF_FILES_PER_PAGE, MAX_DIFF_FILE_PAGES } from './constants'; diff --git a/packages/provider-github/src/http.ts b/packages/provider-github/src/http.ts index a31f185d..91d39d2d 100644 --- a/packages/provider-github/src/http.ts +++ b/packages/provider-github/src/http.ts @@ -1,4 +1,4 @@ -import { logger } from '@codra/core/logger'; +import { logger } from '@codraoss/core/logger'; export class GitHubError extends Error { constructor( diff --git a/packages/provider-github/src/oauth.ts b/packages/provider-github/src/oauth.ts index 3bab3808..81eef86d 100644 --- a/packages/provider-github/src/oauth.ts +++ b/packages/provider-github/src/oauth.ts @@ -1,4 +1,4 @@ -import type { DashboardSessionUser, IdentityProvider, AuthorizationResult } from '@codra/core'; +import type { DashboardSessionUser, IdentityProvider, AuthorizationResult } from '@codraoss/core'; import type { AppBindingsConfig } from './service'; export type GitHubOAuthProfile = { diff --git a/packages/provider-github/src/reactions.ts b/packages/provider-github/src/reactions.ts new file mode 100644 index 00000000..2101caf3 --- /dev/null +++ b/packages/provider-github/src/reactions.ts @@ -0,0 +1,29 @@ +import { assertResponseOk, type GitHubRequestContext, repoApiPath, withRetry } from './http'; + +/** + * React to the pull request's opening post -- the author's own comment, which is what a reader sees + * first. `/issues/{n}/reactions` is the right endpoint: a pull request IS an issue for this purpose, + * and `/pulls/{n}` has no reactions collection. + * + * Idempotent by design at GitHub's end: re-reacting with the same content as the same user returns the + * existing reaction rather than duplicating it, so a retried finalize is safe. + */ +export async function addIssueReaction( + ctx: GitHubRequestContext, + owner: string, + repo: string, + issueNumber: number, + content: '+1' | '-1' | 'eyes' | 'rocket' | 'heart', +) { + return withRetry(`addIssueReaction ${owner}/${repo}#${issueNumber} ${content}`, async () => { + const response = await ctx.request(`${repoApiPath(owner, repo)}/issues/${issueNumber}/reactions`, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ content }), + }); + + // 200 = the reaction already existed, 201 = created. Both are success. + if (response.status === 200 || response.status === 201) return; + await assertResponseOk(response, String(issueNumber), 'GitHub reaction'); + }); +} diff --git a/packages/provider-github/src/review-post.ts b/packages/provider-github/src/review-post.ts index 929b0b2a..edf2757a 100644 --- a/packages/provider-github/src/review-post.ts +++ b/packages/provider-github/src/review-post.ts @@ -1,4 +1,4 @@ -import { logger } from '@codra/core/logger'; +import { logger } from '@codraoss/core/logger'; import { assertResponseOk, type GitHubRequestContext, repoApiPath, withRetry } from './http'; import type { ReviewComment } from './types'; diff --git a/packages/provider-github/src/service.ts b/packages/provider-github/src/service.ts index 63ba9f22..15967cc9 100644 --- a/packages/provider-github/src/service.ts +++ b/packages/provider-github/src/service.ts @@ -31,6 +31,10 @@ export class GitHubService { return this.client.getCompareDiff(owner, repo, base, head); } + async getRepoFile(owner: string, repo: string, path: string, ref?: string) { + return this.client.getRepoFile(owner, repo, path, ref); + } + async createCheckRun(owner: string, repo: string, params: { headSha: string; title: string; summary: string }) { return this.client.createCheckRun(owner, repo, params); } @@ -55,6 +59,10 @@ export class GitHubService { return this.client.addIssueLabels(owner, repo, prNumber, labels); } + async addIssueReaction(owner: string, repo: string, prNumber: number, content: '+1') { + return this.client.addIssueReaction(owner, repo, prNumber, content); + } + async removeIssueLabelsIfPresent(owner: string, repo: string, prNumber: number, labels: string[]) { return this.client.removeIssueLabelsIfPresent(owner, repo, prNumber, labels); } diff --git a/packages/provider-github/src/types.ts b/packages/provider-github/src/types.ts index fbcc3b01..e770bed0 100644 --- a/packages/provider-github/src/types.ts +++ b/packages/provider-github/src/types.ts @@ -1,6 +1,6 @@ -// Both of these are part of the git-provider PORT contract, so @codra/core/ports owns them and +// Both of these are part of the git-provider PORT contract, so @codraoss/core/ports owns them and // this module re-exports: one definition, and the engine does not depend on this file. -export type { ReviewComment, PullRequestRecord } from '@codra/core/ports'; +export type { ReviewComment, PullRequestRecord } from '@codraoss/core/ports'; // Response shapes from the GitHub REST API, narrowed to the fields this app reads. // Import these from @server/core/github, not from here: specs mock that barrel by replacing the whole GitHubClient class. diff --git a/packages/provider-github/src/webhook.ts b/packages/provider-github/src/webhook.ts index ead51a08..c4163c3b 100644 --- a/packages/provider-github/src/webhook.ts +++ b/packages/provider-github/src/webhook.ts @@ -1,5 +1,5 @@ -import type { WebhookPayload, WebhookEventName } from '@codra/schema/webhook'; -import type { PullRequestWebhookPayload, IssueCommentWebhookPayload } from '@codra/schema/github'; +import type { WebhookPayload, WebhookEventName } from '@codraoss/schema/webhook'; +import type { PullRequestWebhookPayload, IssueCommentWebhookPayload } from '@codraoss/schema/github'; export function normalizeGitHubWebhook( eventName: string, diff --git a/packages/provider-github/tsup.config.json b/packages/provider-github/tsup.config.json new file mode 100644 index 00000000..3c001c21 --- /dev/null +++ b/packages/provider-github/tsup.config.json @@ -0,0 +1,9 @@ +{ + "entry": ["src/index.ts", "src/oauth.ts", "src/webhook.ts"], + "format": "esm", + "dts": true, + "splitting": true, + "sourcemap": true, + "clean": true, + "outDir": "dist" +} diff --git a/packages/schema/LICENSE b/packages/schema/LICENSE new file mode 100644 index 00000000..024299ea --- /dev/null +++ b/packages/schema/LICENSE @@ -0,0 +1,625 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 2007 Free Software Foundation, Inc. <>_ + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### 0. Definitions +“This License” refers to version 3 of the GNU Affero General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based +on the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that **(1)** displays an appropriate copyright notice, and **(2)** +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code +The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other +than the work as a whole, that **(a)** is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and **(b)** serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +### 2. Basic Permissions +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +### 4. Conveying Verbatim Copies +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified +it, and giving a relevant date. +* **b)** The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. +This requirement modifies the requirement in section 4 to +“keep intact all notices”. +* **c)** You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. +* **d)** If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either **(1)** a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or **(2)** access to copy the +Corresponding Source from a network server at no charge. +* **c)** Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. +* **d)** Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A “User Product” is either **(1)** a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or **(2)** anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install + +### 6. Conveying Non-Source Forms +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or +authors of the material; or +* **e)** Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +### 8. Termination +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated **(a)** +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and **(b)** permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either **(1)** cause the Corresponding Source to be so +available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license **(a)** in connection with copies of the covered work +conveyed by you (or copies made from those copies), or **(b)** primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found. + + Codra: Open source PR review infrastructure for Cloudflare Workers. + Copyright (C) 2026 Devarshi Shimpi + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source” link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<>. diff --git a/packages/schema/README.md b/packages/schema/README.md new file mode 100644 index 00000000..bbe8e484 --- /dev/null +++ b/packages/schema/README.md @@ -0,0 +1,15 @@ +# @codraoss/schema + +Shared types and Zod contracts for Codra, the open-source code review engine. + +Part of [Codra](https://codra.run), an open-source code review engine. See the [monorepo](https://github.com/devarshishimpi/codra) for development, and [CONTRIBUTING](https://github.com/devarshishimpi/codra/blob/main/CONTRIBUTING.md) for the dual-licensing / CLA details. + +## Install + +```bash +npm install @codraoss/schema +``` + +## License + +[AGPL-3.0-only](./LICENSE) © Devarshi Shimpi diff --git a/packages/schema/package.json b/packages/schema/package.json index 48364079..423cc147 100644 --- a/packages/schema/package.json +++ b/packages/schema/package.json @@ -1,12 +1,23 @@ { - "name": "@codra/schema", + "name": "@codraoss/schema", "version": "0.9.4", - "private": true, + "description": "Shared types and Zod contracts for Codra, the open-source code review engine.", + "author": "Devarshi Shimpi", + "license": "AGPL-3.0-only", + "homepage": "https://codra.run", + "repository": { + "type": "git", + "url": "git+https://github.com/devarshishimpi/codra.git", + "directory": "packages/schema" + }, + "bugs": { + "url": "https://github.com/devarshishimpi/codra/issues" + }, "type": "module", + "sideEffects": false, "exports": { ".": "./src/index.ts", "./api": "./src/api.ts", - "./config": "./src/config.ts", "./github": "./src/github.ts", "./hex": "./src/hex.ts", "./review-limits": "./src/review-limits.ts", @@ -14,7 +25,61 @@ "./transient-errors": "./src/transient-errors.ts", "./webhook": "./src/webhook.ts" }, + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./api": { + "types": "./dist/api.d.ts", + "import": "./dist/api.js" + }, + "./github": { + "types": "./dist/github.d.ts", + "import": "./dist/github.js" + }, + "./hex": { + "types": "./dist/hex.d.ts", + "import": "./dist/hex.js" + }, + "./review-limits": { + "types": "./dist/review-limits.d.ts", + "import": "./dist/review-limits.js" + }, + "./timezone": { + "types": "./dist/timezone.d.ts", + "import": "./dist/timezone.js" + }, + "./transient-errors": { + "types": "./dist/transient-errors.d.ts", + "import": "./dist/transient-errors.js" + }, + "./webhook": { + "types": "./dist/webhook.d.ts", + "import": "./dist/webhook.js" + } + } + }, + "scripts": { + "build": "tsup", + "typecheck": "tsc -p tsconfig.json", + "prepack": "node ../../scripts/swap-publish-exports.mjs promote", + "postpack": "node ../../scripts/swap-publish-exports.mjs restore" + }, "dependencies": { "zod": "^4.3.6" + }, + "devDependencies": { + "tsup": "^8.0.0" } } diff --git a/packages/schema/src/schema-repo-config.ts b/packages/schema/src/schema-repo-config.ts index ad103812..0ef60b69 100644 --- a/packages/schema/src/schema-repo-config.ts +++ b/packages/schema/src/schema-repo-config.ts @@ -32,8 +32,25 @@ export const reviewConfigSchema = z.object({ max_diff_lines_per_file: z.number().int().min(1).max(5_000).default(800), batch_small_files: z.boolean().default(true), max_total_diff_chars: z.number().int().min(1).max(500_000).default(150_000), + // Presentation cap: comments actually posted to the PR. Findings past this are still recorded + // (disposition 'cap') and shown on the dashboard; nothing upstream of posting should read it. max_comments: z.number().int().min(1).max(150).default(10), + // How many findings the pipeline works with (generator, verifier, dashboard); independent of + // max_comments so tightening the posted cap no longer quietly shrinks the review itself. + review_breadth: z.number().int().min(1).max(150).default(25), + // Off by default: adds a large single file's post-change content as read-only context, costing + // one extra GitHub subrequest per qualifying file (see FILE_CONTEXT_CHAR_BUDGET). + full_file_context: z.boolean().default(false), min_severity: z.enum(reviewSeverities).default('P3'), + language_gates: z + .record( + z.string(), + z.object({ + min_severity: z.enum(reviewSeverities).optional(), + min_confidence: z.number().min(0).max(1).optional(), + }), + ) + .default({}), min_confidence: z.number().min(0).max(1).default(0), focus: z.array(z.enum(reviewCategories)).default([...reviewCategories]), deny_claim_types: z.array(z.enum(claimTypes)).default([...DEFAULT_DENIED_CLAIM_TYPES]), @@ -64,7 +81,7 @@ export const reviewConfigSchema = z.object({ }); export const DEFAULT_REVIEW_CONFIG = reviewConfigSchema.parse({}); -export const DEFAULT_MODEL_CONFIG = { main: null, fallbacks: [], size_overrides: [] }; +export const DEFAULT_MODEL_CONFIG = { main: null, fallbacks: [], size_overrides: [], secondary: null }; export const repoConfigSchema = z.object({ review: reviewConfigSchema.default(DEFAULT_REVIEW_CONFIG), @@ -82,6 +99,14 @@ export const repoConfigSchema = z.object({ ) .nullable() .optional(), + // Findings are UNIONED with the primary's, never voted on (F1 0.200 vs 0.149 best-single; agreement is not evidence). Never pair across a capability gap: strong + much weaker measured BELOW strong alone. + secondary: z + .object({ + model: z.string(), + fallbacks: z.array(z.string()).default([]), + }) + .nullable() + .optional(), }) .default(DEFAULT_MODEL_CONFIG), }); @@ -108,6 +133,14 @@ export function normalizeRepoModelConfig(model: RepoConfig['model']): RepoConfig model: normalizeModelId(tier.model), fallbacks: tier.fallbacks?.map(normalizeModelId), })), + ...(model.secondary + ? { + secondary: { + model: normalizeModelId(model.secondary.model), + fallbacks: (model.secondary.fallbacks ?? []).map(normalizeModelId), + }, + } + : {}), }; } diff --git a/packages/schema/src/schema.ts b/packages/schema/src/schema.ts index 5b6b1a22..4388f9f1 100644 --- a/packages/schema/src/schema.ts +++ b/packages/schema/src/schema.ts @@ -125,6 +125,10 @@ export const parsedReviewCommentSchema = z.object({ fingerprintV2: z.string().min(1).nullable().optional(), // Absent means 'llm'. Always test `=== 'rule'` positively. source: z.enum(['llm', 'rule']).nullable().optional(), + // Which reviewer produced this, when a secondary reviewer is configured. Attribution for a human + // reading the dashboard -- never an input to any gate, and never a weight: agreement between models + // is anti-correlated with correctness in the measured corpus. + reviewerModel: z.string().nullable().optional(), // Retirement signal when source is 'rule'. ruleId: z.string().min(1).nullable().optional(), }); @@ -268,10 +272,18 @@ const fileReviewRecordSchema = z.object({ confidenceScore: z.number().nullable().optional(), batchSize: z.number().int().nullable().optional(), withheldCounts: z - .object({ evidence: z.number().int(), claimDenied: z.number().int() }) + .object({ + evidence: z.number().int(), + claimDenied: z.number().int(), + contextOnly: z.number().int(), + absenceRefuted: z.number().int(), + }) .partial() .nullable() .optional(), + // The review answered, but not cleanly. Free-form rather than an enum so a database written by a + // newer worker never fails to parse in an older dashboard. + degraded: z.string().nullable().optional(), errorMessage: z.string().nullable(), createdAt: dateStringSchema, }); diff --git a/packages/schema/tsup.config.json b/packages/schema/tsup.config.json new file mode 100644 index 00000000..2c1aab49 --- /dev/null +++ b/packages/schema/tsup.config.json @@ -0,0 +1,18 @@ +{ + "entry": [ + "src/index.ts", + "src/api.ts", + "src/github.ts", + "src/hex.ts", + "src/review-limits.ts", + "src/timezone.ts", + "src/transient-errors.ts", + "src/webhook.ts" + ], + "format": "esm", + "dts": true, + "splitting": true, + "sourcemap": true, + "clean": true, + "outDir": "dist" +} diff --git a/packages/ui/LICENSE b/packages/ui/LICENSE new file mode 100644 index 00000000..024299ea --- /dev/null +++ b/packages/ui/LICENSE @@ -0,0 +1,625 @@ +GNU Affero General Public License +================================= + +_Version 3, 19 November 2007_ +_Copyright © 2007 Free Software Foundation, Inc. <>_ + +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + +The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + +The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + +When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + +Developers that use our General Public Licenses protect your rights +with two steps: **(1)** assert copyright on the software, and **(2)** offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + +A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + +The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + +An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + +The precise terms and conditions for copying, distribution and +modification follow. + +### 0. Definitions +“This License” refers to version 3 of the GNU Affero General Public License. + +“Copyright” also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + +“The Program” refers to any copyrightable work licensed under this +License. Each licensee is addressed as “you”. “Licensees” and +“recipients” may be individuals or organizations. + +To “modify” a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a “modified version” of the +earlier work or a work “based on” the earlier work. + +A “covered work” means either the unmodified Program or a work based +on the Program. + +To “propagate” a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + +To “convey” a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + +An interactive user interface displays “Appropriate Legal Notices” +to the extent that it includes a convenient and prominently visible +feature that **(1)** displays an appropriate copyright notice, and **(2)** +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + +### 1. Source Code +The “source code” for a work means the preferred form of the work +for making modifications to it. “Object code” means any non-source +form of a work. + +A “Standard Interface” means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + +The “System Libraries” of an executable work include anything, other +than the work as a whole, that **(a)** is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and **(b)** serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +“Major Component”, in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + +The “Corresponding Source” for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + +The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + +The Corresponding Source for a work in source code form is that +same work. + +### 2. Basic Permissions +All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + +You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + +Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + +### 3. Protecting Users' Legal Rights From Anti-Circumvention Law +No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + +When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + +### 4. Conveying Verbatim Copies +You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + +You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + +You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + +* **a)** The work must carry prominent notices stating that you modified +it, and giving a relevant date. +* **b)** The work must carry prominent notices stating that it is +released under this License and any conditions added under section 7. +This requirement modifies the requirement in section 4 to +“keep intact all notices”. +* **c)** You must license the entire work, as a whole, under this +License to anyone who comes into possession of a copy. This +License will therefore apply, along with any applicable section 7 +additional terms, to the whole of the work, and all its parts, +regardless of how they are packaged. This License gives no +permission to license the work in any other way, but it does not +invalidate such permission if you have separately received it. +* **d)** If the work has interactive user interfaces, each must display +Appropriate Legal Notices; however, if the Program has interactive +interfaces that do not display Appropriate Legal Notices, your +work need not make them do so. + +A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +“aggregate” if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + +You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + +* **a)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by the +Corresponding Source fixed on a durable physical medium +customarily used for software interchange. +* **b)** Convey the object code in, or embodied in, a physical product +(including a physical distribution medium), accompanied by a +written offer, valid for at least three years and valid for as +long as you offer spare parts or customer support for that product +model, to give anyone who possesses the object code either **(1)** a +copy of the Corresponding Source for all the software in the +product that is covered by this License, on a durable physical +medium customarily used for software interchange, for a price no +more than your reasonable cost of physically performing this +conveying of source, or **(2)** access to copy the +Corresponding Source from a network server at no charge. +* **c)** Convey individual copies of the object code with a copy of the +written offer to provide the Corresponding Source. This +alternative is allowed only occasionally and noncommercially, and +only if you received the object code with such an offer, in accord +with subsection 6b. +* **d)** Convey the object code by offering access from a designated +place (gratis or for a charge), and offer equivalent access to the +Corresponding Source in the same way through the same place at no +further charge. You need not require recipients to copy the +Corresponding Source along with the object code. If the place to +copy the object code is a network server, the Corresponding Source +may be on a different server (operated by you or a third party) +that supports equivalent copying facilities, provided you maintain +clear directions next to the object code saying where to find the +Corresponding Source. Regardless of what server hosts the +Corresponding Source, you remain obligated to ensure that it is +available for as long as needed to satisfy these requirements. +* **e)** Convey the object code using peer-to-peer transmission, provided +you inform other peers where the object code and Corresponding +Source of the work are being offered to the general public at no +charge under subsection 6d. + +A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + +A “User Product” is either **(1)** a “consumer product”, which means any +tangible personal property which is normally used for personal, family, +or household purposes, or **(2)** anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, “normally used” refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + +“Installation Information” for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + +If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install + +### 6. Conveying Non-Source Forms +modified object code on the User Product (for example, the work has +been installed in ROM). + +The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + +Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + +### 7. Additional Terms +“Additional permissions” are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + +When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + +Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + +* **a)** Disclaiming warranty or limiting liability differently from the +terms of sections 15 and 16 of this License; or +* **b)** Requiring preservation of specified reasonable legal notices or +author attributions in that material or in the Appropriate Legal +Notices displayed by works containing it; or +* **c)** Prohibiting misrepresentation of the origin of that material, or +requiring that modified versions of such material be marked in +reasonable ways as different from the original version; or +* **d)** Limiting the use for publicity purposes of names of licensors or +authors of the material; or +* **e)** Declining to grant rights under trademark law for use of some +trade names, trademarks, or service marks; or +* **f)** Requiring indemnification of licensors and authors of that +material by anyone who conveys the material (or modified versions of +it) with contractual assumptions of liability to the recipient, for +any liability that these contractual assumptions directly impose on +those licensors and authors. + +All other non-permissive additional terms are considered “further +restrictions” within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + +If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + +Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + +### 8. Termination +You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + +However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated **(a)** +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and **(b)** permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + +Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + +Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + +### 9. Acceptance Not Required for Having Copies +You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + +### 10. Automatic Licensing of Downstream Recipients +Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + +An “entity transaction” is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + +You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + +A “contributor” is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's “contributor version”. + +A contributor's “essential patent claims” are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, “control” includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + +Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + +In the following three paragraphs, a “patent license” is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To “grant” such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + +If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either **(1)** cause the Corresponding Source to be so +available, or **(2)** arrange to deprive yourself of the benefit of the +patent license for this particular work, or **(3)** arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. “Knowingly relying” means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + +If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + +A patent license is “discriminatory” if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license **(a)** in connection with copies of the covered work +conveyed by you (or copies made from those copies), or **(b)** primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + +Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + +### 12. No Surrender of Others' Freedom +If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + +### 13. Remote Network Interaction; Use with the GNU General Public License +Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + +Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + +### 14. Revised Versions of this License +The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License “or any later version” applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + +If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + +Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + +### 15. Disclaimer of Warranty +THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM “AS IS” WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + +### 16. Limitation of Liability +IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + +If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + +_END OF TERMS AND CONDITIONS_ + +If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + +To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the “copyright” line and a pointer to where the full notice is found. + + Codra: Open source PR review infrastructure for Cloudflare Workers. + Copyright (C) 2026 Devarshi Shimpi + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + +If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a “Source” link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + +You should also get your employer (if you work as a programmer) or school, +if any, to sign a “copyright disclaimer” for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +<>. diff --git a/packages/ui/README.md b/packages/ui/README.md new file mode 100644 index 00000000..86fd257e --- /dev/null +++ b/packages/ui/README.md @@ -0,0 +1,15 @@ +# @codraoss/ui + +Codra's reusable React design-system primitives and hooks. + +Part of [Codra](https://codra.run), an open-source code review engine. See the [monorepo](https://github.com/devarshishimpi/codra) for development, and [CONTRIBUTING](https://github.com/devarshishimpi/codra/blob/main/CONTRIBUTING.md) for the dual-licensing / CLA details. + +## Install + +```bash +npm install @codraoss/ui +``` + +## License + +[AGPL-3.0-only](./LICENSE) © Devarshi Shimpi diff --git a/packages/ui/package.json b/packages/ui/package.json index 83765435..eb111254 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,8 +1,20 @@ { - "name": "@codra/ui", + "name": "@codraoss/ui", "version": "0.9.4", - "private": true, + "description": "Codra's reusable React design-system primitives and hooks.", + "author": "Devarshi Shimpi", + "license": "AGPL-3.0-only", + "homepage": "https://codra.run", + "repository": { + "type": "git", + "url": "git+https://github.com/devarshishimpi/codra.git", + "directory": "packages/ui" + }, + "bugs": { + "url": "https://github.com/devarshishimpi/codra/issues" + }, "type": "module", + "sideEffects": false, "exports": { ".": "./src/index.ts", "./theme": "./src/lib/theme.tsx", @@ -14,8 +26,70 @@ "./prompt-diff": "./src/lib/prompt-diff.ts", "./markdown-plugins": "./src/lib/markdown-plugins.ts", "./motion": "./src/components/motion/index.ts", - "./hooks": "./src/hooks/index.ts", - "./styles": "./src/styles/tokens.css" + "./hooks": "./src/hooks/index.ts" + }, + "files": [ + "dist", + "LICENSE", + "README.md" + ], + "publishConfig": { + "access": "public", + "main": "./dist/index.js", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + }, + "./theme": { + "types": "./dist/lib/theme.d.ts", + "import": "./dist/lib/theme.js" + }, + "./utils": { + "types": "./dist/lib/utils.d.ts", + "import": "./dist/lib/utils.js" + }, + "./ease": { + "types": "./dist/lib/ease.d.ts", + "import": "./dist/lib/ease.js" + }, + "./highlight": { + "types": "./dist/lib/highlight.d.ts", + "import": "./dist/lib/highlight.js" + }, + "./selection": { + "types": "./dist/lib/selection.d.ts", + "import": "./dist/lib/selection.js" + }, + "./file-tree": { + "types": "./dist/lib/file-tree.d.ts", + "import": "./dist/lib/file-tree.js" + }, + "./prompt-diff": { + "types": "./dist/lib/prompt-diff.d.ts", + "import": "./dist/lib/prompt-diff.js" + }, + "./markdown-plugins": { + "types": "./dist/lib/markdown-plugins.d.ts", + "import": "./dist/lib/markdown-plugins.js" + }, + "./motion": { + "types": "./dist/components/motion/index.d.ts", + "import": "./dist/components/motion/index.js" + }, + "./hooks": { + "types": "./dist/hooks/index.d.ts", + "import": "./dist/hooks/index.js" + } + } + }, + "scripts": { + "build": "tsup", + "typecheck": "tsc -p tsconfig.json", + "prepack": "node ../../scripts/swap-publish-exports.mjs promote", + "postpack": "node ../../scripts/swap-publish-exports.mjs restore" }, "peerDependencies": { "react": "^19.0.0", @@ -27,10 +101,14 @@ "sonner": ">=2.0.0" }, "dependencies": { + "@codraoss/schema": "^0.9.4", "@base-ui/react": "^1.6.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.5.0", "sugar-high": "^2.0.0" + }, + "devDependencies": { + "tsup": "^8.0.0" } -} \ No newline at end of file +} diff --git a/packages/ui/src/components/motion/smooth-scroll.tsx b/packages/ui/src/components/motion/smooth-scroll.tsx index b13a03a8..052eda8b 100644 --- a/packages/ui/src/components/motion/smooth-scroll.tsx +++ b/packages/ui/src/components/motion/smooth-scroll.tsx @@ -11,8 +11,7 @@ import { useRef, } from 'react'; -// Lenis' own expo-out curve, kept as a named fn (not a lib/ease token): tokens are bezier control -// points for the motion lib, while Lenis needs a (t) => number easing fn. +// Named fn, not a lib/ease token: Lenis needs a (t) => number easing fn, not bezier points. const EASE_SCROLL = (t: number) => Math.min(1, 1.001 - 2 ** (-10 * t)); export type ScrollTarget = number | string | HTMLElement; @@ -24,15 +23,13 @@ export type ScrollToOptions = { }; export type SmoothScrollApi = { - /** Underlying Lenis instance, or null on the reduced-motion / native path. */ + /** Null on the reduced-motion / native path. */ lenis: Lenis | null; - /** Current scroll offset in px. */ scrollY: MotionValue; - /** Scroll position as 0..1 of the scrollable height. */ progress: MotionValue; - /** Signed scroll velocity (px/frame). */ + /** px/frame. */ velocity: MotionValue; - /** Programmatic smooth scroll. Respects reduced motion (jumps instantly). */ + /** Jumps instantly under reduced motion. */ scrollTo: (target: ScrollTarget, options?: ScrollToOptions) => void; }; @@ -40,15 +37,14 @@ const SmoothScrollContext = createContext(null); export interface SmoothScrollProps { children: ReactNode; - /** Drive the page (window) when true, or a contained scroll area when false. */ + /** True drives window scroll; false scrolls a contained area. */ root?: boolean; - /** Smoothing factor; lower is smoother and heavier. */ + /** Lower = smoother, heavier. */ lerp?: number; - /** Wheel / programmatic ease duration in seconds. */ duration?: number; orientation?: 'vertical' | 'horizontal'; wheelMultiplier?: number; - /** Off by default - native touch momentum is already good on mobile. */ + /** Off by default: native touch momentum is already good on mobile. */ touch?: boolean; className?: string; } @@ -87,7 +83,6 @@ function resolveTop( return el.offsetTop + offset; } -/** Pushes Lenis' live scroll state into the shared motion values. */ function LenisBridge({ scrollY, progress, @@ -113,7 +108,6 @@ function LenisBridge({ return null; } -/** Native scroll listener for the reduced-motion path and the no-provider fallback. */ function useNativeScrollSync( enabled: boolean, getTarget: () => ScrollSource | null, @@ -184,7 +178,6 @@ export function SmoothScroll({ [reduce, nativeSource], ); - // Reduced motion drives the native listener; otherwise LenisBridge feeds the values instead. useNativeScrollSync(!!reduce, nativeSource, scrollY, progress, velocity); const api = useMemo( @@ -215,8 +208,7 @@ export function SmoothScroll({ smoothWheel: true, syncTouch: touch, easing: EASE_SCROLL, - // Without this, Lenis preventDefault()s every wheel event, blocking nested scrollable - // elements (log panes, drawers, option lists). Escape hatch: `data-lenis-prevent`. + // Else Lenis preventDefault()s every wheel event, blocking nested scroll areas. allowNestedScroll: true, }} > diff --git a/packages/ui/src/lib/file-tree.ts b/packages/ui/src/lib/file-tree.ts index 9f6bd8b8..35ca8cea 100644 --- a/packages/ui/src/lib/file-tree.ts +++ b/packages/ui/src/lib/file-tree.ts @@ -1,4 +1,4 @@ -import type { FileReviewRecord } from '@codra/schema'; +import type { FileReviewRecord } from '@codraoss/schema'; /** Builds the collapsed directory tree the diff viewer's file list renders. */ diff --git a/packages/ui/src/lib/prompt-diff.ts b/packages/ui/src/lib/prompt-diff.ts index 7702d95b..6a0bfd61 100644 --- a/packages/ui/src/lib/prompt-diff.ts +++ b/packages/ui/src/lib/prompt-diff.ts @@ -1,14 +1,4 @@ -/** - * Parsing for the rendered review prompt shown in the diff viewer. - * - * NOT `parseUnifiedDiff` from `@server/core/diff`: that parses real git output for the review - * pipeline; this reads the padded, gutter-prefixed form the prompt renders for the model. - */ - -// Codra renders each file's diff body as 4-wide padded number columns: -// " " e.g. " 615 615 const x = 1" -// We read those embedded line numbers directly and fall back to standard git-diff lines for -// anything else. Only content inside a hunk is parsed, so the prompt preamble is ignored. +// Parses the prompt's padded gutter diff ("NNNN MMMM Pcontent"), not raw git output (see parseUnifiedDiff in @server/core/diff). export interface DiffRow { kind: 'add' | 'del' | 'ctx' | 'hunk'; @@ -19,7 +9,6 @@ export interface DiffRow { const HUNK_RE = /^@@ -(\d+)(?:,\d+)? \+(\d+)(?:,\d+)? @@/; -/** Parse a padded body line ("NNNN MMMM Pcontent"); null if it isn't one. */ function parsePaddedLine(line: string) { if (line.length < 11 || line[4] !== ' ' || line[9] !== ' ') return null; const prefix = line[10]; @@ -33,7 +22,7 @@ function parsePaddedLine(line: string) { export function parsePromptDiff(diff: string): DiffRow[] { const rows: DiffRow[] = []; - let started = false; // inside a hunk + let started = false; let oldNo = 0; let newNo = 0; @@ -46,10 +35,10 @@ export function parsePromptDiff(diff: string): DiffRow[] { rows.push({ kind: 'hunk', oldNo: null, newNo: null, text: line }); continue; } - if (!started) continue; // skip prompt preamble before the first hunk + if (!started) continue; // preamble before first hunk if (line.startsWith('diff --git')) { started = false; continue; } - if (line.startsWith('\\')) continue; // "\ No newline at end of file" - if (line.startsWith('[NOTE')) continue; // truncation note + if (line.startsWith('\\')) continue; // no-newline marker + if (line.startsWith('[NOTE')) continue; const padded = parsePaddedLine(line); if (padded) { @@ -63,21 +52,20 @@ export function parsePromptDiff(diff: string): DiffRow[] { continue; } - // Standard git-diff fallback. const p = line[0]; if (p === '+') rows.push({ kind: 'add', oldNo: null, newNo: newNo++, text: line.slice(1) }); else if (p === '-') rows.push({ kind: 'del', oldNo: oldNo++, newNo: null, text: line.slice(1) }); else if (p === ' ') rows.push({ kind: 'ctx', oldNo: oldNo++, newNo: newNo++, text: line.slice(1) }); } - // Drop a single trailing blank context row left behind by the final newline. + // drop trailing blank row from final newline const last = rows[rows.length - 1]; if (last && last.kind === 'ctx' && last.text === '') rows.pop(); return rows; } -/** Cheap line scan (no row objects) so collapsed panels never pay for a full parse. */ +// line-only scan; avoids full parse for collapsed panels export function diffStats(diff: string | null) { if (!diff) return { adds: 0, dels: 0, total: 0 }; let adds = 0; diff --git a/packages/ui/tsup.config.json b/packages/ui/tsup.config.json new file mode 100644 index 00000000..af385a76 --- /dev/null +++ b/packages/ui/tsup.config.json @@ -0,0 +1,21 @@ +{ + "entry": [ + "src/index.ts", + "src/lib/theme.tsx", + "src/lib/utils.ts", + "src/lib/ease.ts", + "src/lib/highlight.tsx", + "src/lib/selection.ts", + "src/lib/file-tree.ts", + "src/lib/prompt-diff.ts", + "src/lib/markdown-plugins.ts", + "src/components/motion/index.ts", + "src/hooks/index.ts" + ], + "format": "esm", + "dts": true, + "splitting": true, + "sourcemap": true, + "clean": true, + "outDir": "dist" +} diff --git a/scripts/check-package-exports.mjs b/scripts/check-package-exports.mjs new file mode 100644 index 00000000..d8c39edd --- /dev/null +++ b/scripts/check-package-exports.mjs @@ -0,0 +1,69 @@ +import { readFileSync, existsSync, readdirSync } from 'node:fs'; +import { join, dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '..'); +const packagesDir = join(root, 'packages'); + +const errors = []; + +for (const name of readdirSync(packagesDir)) { + const pkgDir = join(packagesDir, name); + const pkgJsonPath = join(pkgDir, 'package.json'); + if (!existsSync(pkgJsonPath)) continue; + + const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf8')); + const id = pkg.name ?? name; + if (pkg.private) continue; + + const exp = pkg.exports; + const pubExp = pkg.publishConfig?.exports; + + if (!exp) { + errors.push(`${id}: missing top-level "exports".`); + continue; + } + if (!pubExp) { + errors.push(`${id}: missing "publishConfig.exports" (the compiled/dist surface).`); + continue; + } + + const srcKeys = Object.keys(exp).sort(); + const distKeys = Object.keys(pubExp).sort(); + if (JSON.stringify(srcKeys) !== JSON.stringify(distKeys)) { + const onlySrc = srcKeys.filter((k) => !distKeys.includes(k)); + const onlyDist = distKeys.filter((k) => !srcKeys.includes(k)); + errors.push( + `${id}: exports keys drift between source and publishConfig.` + + (onlySrc.length ? ` Only in exports: ${onlySrc.join(', ')}.` : '') + + (onlyDist.length ? ` Only in publishConfig.exports: ${onlyDist.join(', ')}.` : ''), + ); + } + + for (const [key, target] of Object.entries(exp)) { + if (typeof target !== 'string') { + errors.push(`${id}: source export "${key}" must be a string path to a .ts/.tsx source file.`); + continue; + } + if (target.includes('*')) continue; + if (!existsSync(join(pkgDir, target))) { + errors.push(`${id}: export "${key}" points at "${target}" which does not exist.`); + } + } + + if (!Array.isArray(pkg.files) || !pkg.files.includes('dist')) { + errors.push(`${id}: "files" must include "dist" so the compiled output is published.`); + } + if (pkg.publishConfig?.access !== 'public') { + errors.push(`${id}: "publishConfig.access" must be "public".`); + } +} + +if (errors.length) { + console.error('Package export check failed:\n'); + for (const e of errors) console.error(' - ' + e); + console.error('\nFix the exports maps in the offending packages/*/package.json.'); + process.exit(1); +} + +console.log('Package export check passed: all publishable @codraoss/* packages have consistent, resolvable exports.'); diff --git a/scripts/outdated-rate.ts b/scripts/outdated-rate.ts index 1d9eaeb4..c7549f36 100644 --- a/scripts/outdated-rate.ts +++ b/scripts/outdated-rate.ts @@ -1,17 +1,5 @@ -/** - * Outdated Rate: the share of flagged lines a developer actually modified afterwards. - * - * npx vite-node scripts/outdated-rate.ts -- --repo devarshishimpi/codra - * - * Chosen over precision because it needs ZERO human annotation. It is also the retirement signal: a - * rule that is right but never acted on is a rule worth deleting. - * - * For a job A that posted findings, find the next job B on the same PR at a DIFFERENT commit, diff - * the two heads, and check whether a changed line hashes to one of A's anchor hashes. - * - * Deliberately NOT the pure-SQL alternative ("did B re-derive the same fingerprint_v2?"): that - * cannot separate "the line was fixed" from "the model was flaky". - */ +/** Outdated Rate: share of flagged lines later modified. No annotation needed; a retirement + * signal for unacted rules. npx vite-node scripts/outdated-rate.ts -- --repo devarshishimpi/codra */ import { readFileSync } from 'node:fs'; import postgres from 'postgres'; import { buildUnifiedDiffFromFiles, parseUnifiedDiff } from '@server/core/diff'; @@ -35,8 +23,8 @@ type Candidate = { rule_ids: (string | null)[]; }; -/** Unauthenticated compare, same as the fixture recorder. Rebuilt from the JSON file list because - * the unified-diff media type 406s past 20,000 lines. */ +// Diffs job A vs next job B on same PR (not pure SQL: can't tell fixed from flaky). Rebuilt from +// JSON file list since unified-diff media type 406s past 20,000 lines. async function changedLineHashes(base: string, head: string): Promise> { const res = await fetch(`https://api.github.com/repos/${owner}/${repo}/compare/${base}...${head}`, { headers: { accept: 'application/vnd.github+json' }, @@ -50,7 +38,6 @@ async function changedLineHashes(base: string, head: string): Promise'); + process.exit(1); +} diff --git a/src/client/app.css b/src/client/app.css index 5a734f86..ef0a2a65 100644 --- a/src/client/app.css +++ b/src/client/app.css @@ -2,17 +2,10 @@ @import "tailwindcss"; -/* Make `dark:` utilities follow the app's `.dark` class (theme toggle) - instead of the OS `prefers-color-scheme` default. */ +/* dark: utilities follow .dark class, not OS prefers-color-scheme. */ @custom-variant dark (&:where(.dark, .dark *)); -/* ───────────────────────────────────────────────────── - Surface palette (ui-* tokens). - The app's neutral surface scale, defined locally so there's no runtime - design-system dependency. Raw values live in :root/.dark (below); the - @theme inline block maps them to Tailwind so `bg/text/border/ring/ - divide-ui-*` utilities generate and flip with the `.dark` class. Brand - tracks Codra's lime `--primary`. */ +/* --ui-* : local neutral surface scale (no runtime design-system dep). */ :root { --ui-base: #ffffff; --ui-canvas: oklch(98.75% 0 0); @@ -32,66 +25,47 @@ --ui-strong: oklch(98.5% 0 0); } -/* ───────────────────────────────────────────────────── - Easing tokens -───────────────────────────────────────────────────── */ :root { --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); --ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1); } -/* ───────────────────────────────────────────────────── - LIGHT MODE (:root default) -───────────────────────────────────────────────────── */ +/* LIGHT MODE (:root default) */ :root { - /* Surfaces - Pure & Crisp */ - /* Surfaces - High-Contrast */ - --background: oklch(100% 0 0); /* #ffffff */ + --background: oklch(100% 0 0); --foreground: oklch(12% 0.02 115); - --card: oklch(100% 0 0); /* #ffffff */ + --card: oklch(100% 0 0); --card-foreground: oklch(12% 0.02 115); --popover: oklch(100% 0 0); --popover-foreground: oklch(12% 0.02 115); - /* Signature lime - darkened in light mode for AA accessibility on white */ + /* Lime darkened for AA contrast on white; .dark restores full brightness. */ --primary: oklch(64% 0.24 115); - --primary-foreground: oklch(100% 0 0); /* White text on the deeper green */ - /* Brand lime. `--btn-primary-bg` is the solid accent (progress bars, tab - underlines, viewed checkboxes) - deepened here to the same AA-accessible - tone as `--primary` so it doesn't read as a neon slash on white; .dark - overrides it back to the bright lime. Primary BUTTONS use a lime-tinted - surface + lime border. Light mode: pale lime fill with near-black text for - max readability; .dark overrides below (bright lime text there instead). */ + --primary-foreground: oklch(100% 0 0); --btn-primary-bg: oklch(64% 0.24 115); --btn-primary-fg: oklch(20% 0.02 118); --btn-primary-border: oklch(72% 0.17 118); --btn-primary-surface: oklch(95% 0.09 118); --btn-primary-hover: oklch(90% 0.13 118); - /* Secondary / muted - Zinc */ --secondary: oklch(96.3% 0.003 286.3); - --secondary-foreground:oklch(27.4% 0.006 286.3); /* #27272a */ + --secondary-foreground:oklch(27.4% 0.006 286.3); --muted: oklch(96.3% 0.003 286.3); - --muted-foreground: oklch(55.1% 0.011 286.3); /* #71717a */ + --muted-foreground: oklch(55.1% 0.011 286.3); - /* Accent - slightly darker zinc for visible hover on white popovers */ - --accent: oklch(90.9% 0.004 286.3); /* #e4e4e7 */ - --accent-foreground: oklch(20.5% 0.005 286.3); /* #18181b */ + --accent: oklch(90.9% 0.004 286.3); + --accent-foreground: oklch(20.5% 0.005 286.3); - /* Destructive */ --destructive: oklch(55% 0.22 25); --destructive-foreground: oklch(100% 0 0); - /* Border / input / ring */ --border: oklch(90.9% 0.004 286.3); --input: oklch(90.9% 0.004 286.3); --ring: oklch(72% 0.22 115); - /* Radius */ --radius: 0.75rem; --sidebar-width: 240px; - /* Semantic palette */ --success: oklch(64% 0.24 115); --success-bg: oklch(98% 0.04 115); --success-border: oklch(85% 0.15 115); @@ -105,30 +79,23 @@ --info-bg: oklch(98% 0.04 250); --info-border: oklch(88% 0.12 250); - /* Premium Shadows */ --shadow-sm: 0 1px 2px oklch(0% 0 0 / 0.02); --shadow-md: 0 1px 4px oklch(0% 0 0 / 0.03), 0 1px 2px oklch(0% 0 0 / 0.02); --shadow-lg: 0 4px 16px -4px oklch(0% 0 0 / 0.04), 0 1px 6px -2px oklch(0% 0 0 / 0.03); - /* Code Blocks (Zinc) */ --code-bg: oklch(96.3% 0.003 286.3); --code-fg: oklch(27.4% 0.006 286.3); --code-border: oklch(90.9% 0.004 286.3); - /* Diff add/del - a true green/red (not the brand lime), tuned per theme so - rows are clearly distinguishable and +/- counts read correctly. Light. */ + /* True green/red, not brand lime, so diff rows/counts stay distinguishable. */ --diff-add-bg: oklch(95% 0.06 150); --diff-add-fg: oklch(48% 0.13 150); --diff-del-bg: oklch(95% 0.05 27); --diff-del-fg: oklch(52% 0.16 27); } -/* ───────────────────────────────────────────────────── - DARK MODE (.dark class on ) -───────────────────────────────────────────────────── */ +/* DARK MODE (.dark class on ) */ .dark { - /* Surfaces - Deep & Layered */ - /* Surfaces - Ultra-Dark */ --background: #000000; --foreground: oklch(98% 0.005 115); --card: #09090b; @@ -136,11 +103,9 @@ --popover: #09090b; --popover-foreground: oklch(98% 0.005 115); - --primary: oklch(94% 0.23 115); /* #E0FE56 */ + --primary: oklch(94% 0.23 115); --primary-foreground: oklch(12% 0.04 115); - /* Primary buttons: bright lime text/border on a faint lime fill. Solid - accent (tab underlines, progress bars) goes back to the bright neon here. */ --btn-primary-bg: #CCE800; --btn-primary-fg: #CCE800; --btn-primary-border: color-mix(in oklab, #CCE800 50%, transparent); @@ -158,7 +123,7 @@ --destructive: oklch(60% 0.220 25); --destructive-foreground: oklch(10% 0.015 115); - --border: oklch(22% 0.02 115); /* #1A1A1A */ + --border: oklch(22% 0.02 115); --input: oklch(22% 0.02 115); --ring: oklch(94% 0.23 115); @@ -179,28 +144,22 @@ --shadow-md: 0 4px 12px oklch(0% 0 0 / 0.45), 0 1px 4px oklch(0% 0 0 / 0.25); --shadow-lg: 0 12px 24px -4px oklch(0% 0 0 / 0.5), 0 4px 12px -2px oklch(0% 0 0 / 0.3); - /* Code Blocks (Zinc) */ --code-bg: oklch(20.5% 0.005 286.3); --code-fg: oklch(86.5% 0.005 286.3); --code-border: oklch(27.4% 0.006 286.3); - /* Diff add/del - green/red bands on the near-black diff panel. Dark. */ --diff-add-bg: oklch(30% 0.06 150); --diff-add-fg: oklch(82% 0.15 150); --diff-del-bg: oklch(31% 0.08 27); --diff-del-fg: oklch(80% 0.16 27); } -/* ───────────────────────────────────────────────────── - Tailwind v4 theme tokens (@theme inline = dynamic) -───────────────────────────────────────────────────── */ +/* Tailwind v4 theme tokens (@theme inline = dynamic) */ @theme inline { --font-sans: 'IBM Plex Sans', 'Segoe UI', system-ui, sans-serif; --font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace; - /* Surface palette → Tailwind utilities. Reference the raw --ui-* vars - (defined per-theme in :root/.dark) so the utilities flip with `.dark`. - Brand tracks Codra's lime `--primary`. */ + /* References raw --ui-* vars so utilities flip with .dark. */ --color-ui-base: var(--ui-base); --color-ui-canvas: var(--ui-canvas); --color-ui-line: var(--ui-line); @@ -246,16 +205,13 @@ --color-info-bg: var(--info-bg); --color-info-border: var(--info-border); - /* Common radius scale - one system across the app. Surfaces (rounded-lg / - rounded-xl / `.surface`) match the dashboard stat cards; controls - (rounded-md: inputs, buttons, dropdown items) sit a touch tighter. */ - --radius-sm: 0.3125rem; /* 5px */ - --radius-md: 0.4375rem; /* 7px - controls */ - --radius-lg: 0.6875rem; /* 11px - cards, panels, dropdown menus */ - --radius-xl: 0.6875rem; /* 11px - large surfaces (`.surface`) */ - --radius-2xl: 0.875rem; /* 14px */ + /* radius-lg == radius-xl intentionally: cards and .surface share one size. */ + --radius-sm: 0.3125rem; + --radius-md: 0.4375rem; + --radius-lg: 0.6875rem; + --radius-xl: 0.6875rem; + --radius-2xl: 0.875rem; - /* Fluid Typography Scale (Ratio: 1.25) */ --text-xs: 0.75rem; --text-sm: 0.875rem; --text-base: 1rem; @@ -266,7 +222,6 @@ --text-4xl: clamp(2.5rem, 10vw, 6rem); --text-display: clamp(3rem, 12vw, 9rem); - /* Spacing Scale (Fluid) */ --space-xs: clamp(0.5rem, 1vw, 0.75rem); --space-sm: clamp(1rem, 2vw, 1.5rem); --space-md: clamp(1.5rem, 4vw, 3rem); @@ -274,9 +229,6 @@ --space-xl: clamp(6rem, 12vw, 10rem); } -/* ───────────────────────────────────────────────────── - Base resets -───────────────────────────────────────────────────── */ @layer base { *, *::before, *::after { box-sizing: border-box; } @@ -305,7 +257,6 @@ color 0.3s var(--ease-out-expo); } - /* High-Contrast Background Glows */ body::before { content: ''; pointer-events: none; @@ -316,7 +267,6 @@ transition: opacity 0.4s; } - /* Dark mode: high contrast green glow */ .dark body::before { background: radial-gradient(ellipse 60% 45% at 0% 0%, oklch(20% 0.15 115 / 0.15), transparent 60%), @@ -328,9 +278,6 @@ pre, code { font-family: var(--font-mono); } } -/* ───────────────────────────────────────────────────── - Keyframes -───────────────────────────────────────────────────── */ @keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } @@ -390,7 +337,6 @@ to { transform: rotate(360deg); } } -/* Radix & UI Animations */ @utility animate-in { animation-duration: 200ms; animation-timing-function: cubic-bezier(0.4, 0, 0.2, 1); @@ -485,9 +431,7 @@ } } -/* ───────────────────────────────────────────────────── - Scroll-reveal utility (toggled by JS IntersectionObserver) -───────────────────────────────────────────────────── */ +/* Toggled by JS IntersectionObserver. */ @utility reveal-on-scroll { opacity: 0; transform: translateY(24px); @@ -507,30 +451,17 @@ } } -/* Stagger helpers used on siblings */ @utility reveal-delay-1 { transition-delay: 80ms !important; } @utility reveal-delay-2 { transition-delay: 160ms !important; } @utility reveal-delay-3 { transition-delay: 240ms !important; } @utility reveal-delay-4 { transition-delay: 320ms !important; } -/* ───────────────────────────────────────────────────── - Scrollbars - ONE treatment everywhere. - • Neutral (default grey), not the brand colour. - • Auto-hiding: the thumb is invisible until the container is actively - scrolling. A global scroll listener (app-shell.tsx) sets `data-scrolling` - on whatever just scrolled and clears it ~700ms after scrolling stops. - • Applied via `*` (Firefox doesn't inherit scrollbar-* ) and the global - WebKit pseudo-elements, so every scroll container in the app matches - without any per-element opt-in class. - Track space is always reserved while an element is scrollable, so revealing - the thumb never shifts layout. -───────────────────────────────────────────────────── */ +/* Scrollbars: neutral grey, auto-hidden via app-shell.tsx toggling data-scrolling. */ * { scrollbar-width: thin; scrollbar-color: transparent transparent; } -/* Firefox: reveal a neutral thumb only while scrolling. */ [data-scrolling] { scrollbar-color: oklch(0% 0 0 / 0.32) transparent; } @@ -538,8 +469,7 @@ scrollbar-color: oklch(100% 0 0 / 0.3) transparent; } -/* WebKit (Chrome, Safari, Edge). Transparent border + padding-box clip insets - the visible thumb so it reads as a slim rounded bar. */ +/* Transparent border + padding-box clip insets the thumb into a slim bar. */ ::-webkit-scrollbar { width: 10px; height: 10px; @@ -567,9 +497,6 @@ background-color: oklch(100% 0 0 / 0.45); } -/* ───────────────────────────────────────────────────── - Reduced Motion: global safety net -───────────────────────────────────────────────────── */ @media (prefers-reduced-motion: reduce) { *, *::before, @@ -581,9 +508,6 @@ } } -/* ───────────────────────────────────────────────────── - Surface & Utilities -───────────────────────────────────────────────────── */ @utility surface { @apply bg-card border border-border rounded-xl; box-shadow: var(--shadow-md); @@ -631,8 +555,7 @@ @apply animate-[shimmer_1.8s_linear_infinite] rounded-sm; } -/* Unlayered `.skeleton` shimmer, kept at this specificity so it can't be - beaten by any later utility. */ +/* Unlayered, kept at this specificity so no later utility can beat it. */ .skeleton { background: linear-gradient( @@ -645,8 +568,7 @@ animation: shimmer 1.8s linear infinite !important; } -/* Recolour a indicator to the info/blue token (model-call bars), - overriding the default lime brand fill. */ +/* Model-call bars: override default lime fill with info/blue. */ .meter-indicator-info { background-image: none !important; background-color: var(--info) !important; @@ -707,9 +629,6 @@ @apply w-[7px] h-[7px] rounded-full bg-info inline-block; } -/* ───────────────────────────────────────────────────── - Recharts overrides -───────────────────────────────────────────────────── */ .recharts-default-tooltip { background: var(--card) !important; border: 1px solid var(--border) !important; @@ -721,23 +640,18 @@ box-shadow: 0 8px 32px oklch(0% 0 0 / 0.5) !important; } -/* Sidebar is flat against the page canvas; its chrome (nav rows, controls, - text) is styled with the app's ui-* / --primary tokens via Tailwind classes - in app-shell.tsx, so it reads as the same system as the rest of the UI. */ - .app-shell-content { - --background: oklch(97.8% 0.002 286.3); /* lighter zinc - between zinc-100 and white */ - --card: oklch(100% 0 0); /* #ffffff */ - --muted: oklch(90.9% 0.004 286.3); /* #e4e4e7 */ + --background: oklch(97.8% 0.002 286.3); + --card: oklch(100% 0 0); + --muted: oklch(90.9% 0.004 286.3); --popover: oklch(100% 0 0); - --secondary: oklch(88.5% 0.004 286.3); /* #e2e2e6 */ - --border: oklch(90.9% 0.004 286.3); /* #e4e4e7 */ - --input: oklch(90.9% 0.004 286.3); /* #e4e4e7 */ + --secondary: oklch(88.5% 0.004 286.3); + --border: oklch(90.9% 0.004 286.3); + --input: oklch(90.9% 0.004 286.3); } .dark .app-shell-content { - /* Zinc, matching the light block's hue: a cool neutral (hue 286.3, low - chroma) rather than the warm olive cast that hue 115 gave the card. */ + /* Cool neutral hue 286.3, not hue 115 which gave the card a warm olive cast. */ --background: oklch(18% 0.006 286.3); --card: oklch(18% 0.006 286.3); --muted: oklch(22% 0.006 286.3); @@ -747,17 +661,14 @@ --input: oklch(22% 0.006 286.3); } -/* Sidebar rows: the SharedLayoutBg pill is the sole hover affordance - the - row itself never transforms on hover/focus/active. Active rows paint their - own neutral fill (Tailwind classes in app-shell.tsx). */ +/* SharedLayoutBg pill is the sole hover affordance; row itself never transforms. */ .dashboard-sidebar-action:hover, .dashboard-sidebar-action:focus-visible, .dashboard-sidebar-action:active { transform: none !important; } -/* Skimmer shine on the selected row: a light beam parked off-screen that - sweeps across once when the active row is hovered/focused. */ +/* Light beam parked off-screen, sweeps across once on hover/focus. */ .dashboard-sidebar-shine { transform: skew(-13deg) translateX(-130%); transition: transform 0ms linear; @@ -791,9 +702,7 @@ @apply text-2xl md:text-3xl lg:text-[2.25rem] font-bold tracking-[-0.04em] leading-none text-foreground tabular-nums; } -/* Fonts for the KPI-card surface. Geist for the sans (same family as - Geist Mono), scoped here because the global @theme sets --font-sans / - --font-mono to the app's default families. */ +/* Geist, scoped locally since global @theme sets --font-sans/mono to app defaults. */ .ui-font-sans { font-family: 'Geist', ui-sans-serif, system-ui, sans-serif; } @@ -802,10 +711,7 @@ font-feature-settings: 'tnum' 1; } -/* Shared panel chrome matching the dashboard stat cards: white/black surface, - ui-line border (deeper in dark), Geist, 11px radius. Use for table cards and - section panels so everything reads as one system. `.ui-well` is the matching - recessed strip (table header rows, footers) - the stat cards' inner panel. */ +/* Matches dashboard stat-card chrome; .ui-well is its recessed inner panel. */ .ui-panel { font-family: 'Geist', ui-sans-serif, system-ui, sans-serif; border-radius: var(--radius-lg); @@ -817,17 +723,14 @@ border-color: oklch(0.27 0 0); } .ui-well { - /* Slightly lighter + warmer than pure gray so recessed panels (table - headers, stat-card inner wells) read as a soft tint, not a flat slab. */ background: oklch(97.8% 0.002 286.3); } .dark .ui-well { background: oklch(19% 0 0); } -/* Diff-viewer syntax tokens (sugar-high, see src/client/lib/highlight.tsx). Muted, - theme-aware hues so highlighted code stays readable on the add/del tints. - sugar-high emits `color: var(--sh-)` per token. */ +/* Syntax tokens for sugar-high (src/client/lib/highlight.tsx); it emits + color: var(--sh-) per token. */ :root { --sh-keyword: oklch(48% 0.19 305); --sh-string: oklch(46% 0.12 150); @@ -851,13 +754,11 @@ } .sh__token--comment { font-style: italic; } -/* Diff row tints + add/del number colors (theme vars set above). */ .diff-add { background-color: var(--diff-add-bg); } .diff-del { background-color: var(--diff-del-bg); } .diff-add-fg { color: var(--diff-add-fg); } .diff-del-fg { color: var(--diff-del-fg); } -/* Late-arriving header/meta content eases in instead of popping. */ @keyframes ui-fade-in { from { opacity: 0; transform: translateY(2px); } to { opacity: 1; transform: translateY(0); } @@ -866,15 +767,12 @@ animation: ui-fade-in 0.25s ease-out both; } -/* Diff-viewer file tree: indent guide lines + connector ticks on nested - levels, and an animated expand/collapse (grid-rows 0fr ↔ 1fr). */ .diff-tree ul { list-style: none; margin: 0; padding: 0; } .diff-tree ul ul { - /* Tight per-level indent (18px) so deeply nested paths keep usable width. */ margin-left: 10px; padding-left: 8px; border-left: 1px solid var(--ui-line); @@ -894,8 +792,7 @@ } .diff-tree-children { display: grid; - /* The implicit column would size to content (auto), leaving nested rows narrow; - pin it to full width so file rows stretch edge-to-edge like the top level. */ + /* Implicit column would size to content (auto); pin full width so rows stretch edge-to-edge. */ grid-template-columns: minmax(0, 1fr); grid-template-rows: 1fr; transition: grid-template-rows 0.25s ease-in-out; @@ -908,14 +805,9 @@ min-width: 0; } -/* `.thin-scroll` and `.auto-hide-scroll` are retained as no-op aliases: the - neutral, auto-hiding treatment is now global (see the Scrollbars block - above), so these classes need no rules of their own. Existing markup that - references them keeps working and matches everything else. */ +/* .thin-scroll / .auto-hide-scroll kept as no-op aliases: the treatment is + now global (Scrollbars block above); existing markup referencing them still works. */ -/* The diff-tree scroller only needs its functional traits; colour + auto-hide - come from the global rules. `scrollbar-gutter: stable` keeps it clear of the - panel's rounded corners. */ .diff-tree-scroll { overscroll-behavior: contain; scrollbar-gutter: stable; @@ -959,18 +851,14 @@ & hr { @apply border-border my-[1.5em]; } } -/* ───────────────────────────────────────────────────── - Sonner Toast - Premium overrides -───────────────────────────────────────────────────── */ +/* Sonner toast overrides */ -/* ── Outer list / viewport ───────────────────────── */ [data-sonner-toaster] { --offset: 1.25rem !important; --width: min(22rem, calc(100vw - 2rem)) !important; font-family: var(--font-sans) !important; } -/* ── Base toast shell ─────────────────────────────── */ .codra-toast { display: flex !important; align-items: flex-start !important; @@ -986,11 +874,9 @@ 0 1px 4px oklch(0% 0 0 / 0.06), inset 0 1px 0 oklch(100% 0 0 / 0.05) !important; - /* light defaults (overridden per-variant below) */ background: oklch(99.5% 0.004 115) !important; color: oklch(15% 0.02 115) !important; - /* smooth entrance */ animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1) !important; } @@ -1003,7 +889,6 @@ inset 0 1px 0 oklch(100% 0 0 / 0.04) !important; } -/* ── Title ────────────────────────────────────────── */ .codra-toast-title { font-size: 0.8125rem !important; font-weight: 600 !important; @@ -1011,7 +896,6 @@ line-height: 1.35 !important; } -/* ── Description ──────────────────────────────────── */ .codra-toast-description { font-size: 0.74rem !important; font-weight: 400 !important; @@ -1020,13 +904,11 @@ line-height: 1.5 !important; } -/* ── Icon wrapper ─────────────────────────────────── */ .codra-toast-icon { margin-top: 0.05rem !important; flex-shrink: 0 !important; } -/* ── Close button ─────────────────────────────────── */ .codra-toast-close { top: 0.55rem !important; right: 0.55rem !important; @@ -1054,16 +936,11 @@ background: oklch(28% 0.022 115) !important; } -/* ── SUCCESS / ERROR / LOADING ───────────────────── - Use the default toast text color instead of a - status tint (icon color already conveys status). */ - -/* spinner inherits accent color */ +/* Status color comes from the icon; text stays the default toast color. */ .codra-toast-loader svg { color: var(--primary) !important; } -/* ── WARNING ─────────────────────────────────────── */ .codra-toast-warning { color: oklch(35% 0.12 65) !important; } @@ -1072,7 +949,6 @@ color: oklch(82% 0.14 65) !important; } -/* ── INFO ────────────────────────────────────────── */ .codra-toast-info { color: oklch(30% 0.12 250) !important; } diff --git a/src/client/components/features/account/detail-rows.tsx b/src/client/components/features/account/detail-rows.tsx index 5baf372c..b8976a00 100644 --- a/src/client/components/features/account/detail-rows.tsx +++ b/src/client/components/features/account/detail-rows.tsx @@ -1,6 +1,6 @@ -import { LayerCard, Skeleton, Text } from '@codra/ui'; +import { LayerCard, Skeleton, Text } from '@codraoss/ui'; import { useState } from 'react'; -import { cn } from '@codra/ui/utils'; +import { cn } from '@codraoss/ui/utils'; export function DetailGroup({ caption, children }: { caption: string; children: React.ReactNode }) { return ( diff --git a/src/client/components/features/account/details-section.tsx b/src/client/components/features/account/details-section.tsx index df3edce1..2acde290 100644 --- a/src/client/components/features/account/details-section.tsx +++ b/src/client/components/features/account/details-section.tsx @@ -1,4 +1,4 @@ -import { SectionCard, Select, Skeleton, Text } from '@codra/ui'; +import { SectionCard, Select, Skeleton, Text } from '@codraoss/ui'; import { useMemo } from 'react'; import { Mail } from 'lucide-react'; import { @@ -9,7 +9,7 @@ import { resolvedTimeZone, timeZoneOffsetLabel, } from '@client/lib/timezone'; -import type { AccountSettings, AuthSessionUser } from '@codra/schema/api'; +import type { AccountSettings, AuthSessionUser } from '@codraoss/schema/api'; import { DetailGroup, RevealOnClick, DetailRow } from './detail-rows'; diff --git a/src/client/components/features/account/profile-card.tsx b/src/client/components/features/account/profile-card.tsx index 606808e0..80692241 100644 --- a/src/client/components/features/account/profile-card.tsx +++ b/src/client/components/features/account/profile-card.tsx @@ -1,9 +1,9 @@ -import { Badge, Button, GithubMark, Input, LinkButton, Skeleton } from '@codra/ui'; +import { Badge, Button, GithubMark, Input, LinkButton, Skeleton } from '@codraoss/ui'; import { useState } from 'react'; import { toast } from 'sonner'; import { api } from '@client/lib/api'; import { ExternalLink, Pencil, Check, X } from 'lucide-react'; -import type { AccountSettings, AuthSessionUser } from '@codra/schema/api'; +import type { AccountSettings, AuthSessionUser } from '@codraoss/schema/api'; export function ProfileCard({ user, diff --git a/src/client/components/features/dashboard/updates-email-prompt.tsx b/src/client/components/features/dashboard/updates-email-prompt.tsx index 4fd03128..73195dce 100644 --- a/src/client/components/features/dashboard/updates-email-prompt.tsx +++ b/src/client/components/features/dashboard/updates-email-prompt.tsx @@ -1,9 +1,9 @@ -import { Button, Input } from '@codra/ui'; +import { Button, Input } from '@codraoss/ui'; import { useEffect, useState, type FormEvent } from 'react'; import { toast } from 'sonner'; import { Check, Mail } from 'lucide-react'; import { api } from '@client/lib/api'; -import type { UpdatesEmailResponse } from '@codra/schema/api'; +import type { UpdatesEmailResponse } from '@codraoss/schema/api'; export function UpdatesEmailPrompt() { const [status, setStatus] = useState(null); diff --git a/src/client/components/features/job-detail/comment-card.tsx b/src/client/components/features/job-detail/comment-card.tsx index c168e076..2f9d2bf2 100644 --- a/src/client/components/features/job-detail/comment-card.tsx +++ b/src/client/components/features/job-detail/comment-card.tsx @@ -1,16 +1,16 @@ -import { CopyButton } from '@codra/ui'; +import { CopyButton } from '@codraoss/ui'; import { useState, type ComponentPropsWithoutRef } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { FileText, ThumbsDown, ThumbsUp } from 'lucide-react'; -import { cn } from '@codra/ui/utils'; +import { cn } from '@codraoss/ui/utils'; import { api } from '@client/lib/api'; -import { preventToggleOnTextSelection } from '@codra/ui/selection'; -import type { ParsedReviewComment } from '@codra/schema'; +import { preventToggleOnTextSelection } from '@codraoss/ui/selection'; +import type { ParsedReviewComment } from '@codraoss/schema'; import { severityConfig } from './constants'; import { ContextSnippet } from './context-snippet'; -import { safeRehypePlugins } from '@codra/ui/markdown-plugins'; +import { safeRehypePlugins } from '@codraoss/ui/markdown-plugins'; /** Plain-English reason a finding never reached the pull request. */ const DISPOSITION_LABEL: Record = { severity: 'Below the severity threshold for this repository', @@ -103,6 +103,16 @@ export function CommentCard({ comment, filePath, jobId }: CommentCardProps) { {comment.claimType.replace(/_/g, ' ')} )} + {/* Only when a secondary reviewer is configured, and only as attribution: which reviewer found + this says nothing about whether it is right, and must never be read as a confidence signal. */} + {comment.reviewerModel && ( + + {comment.reviewerModel.split('/').pop()} + + )} {/* Say which stage stopped it rather than leaving a filtered finding looking identical to a posted one. */} {comment.posted === false && comment.disposition && comment.disposition !== 'posted' && ( ; } -/** Verdict pill: the border stays neutral so only the leading icon carries colour. */ +// Border stays neutral; only the icon carries colour. export function VerdictPill({ verdict }: { verdict: NonNullable }) { const approved = verdict === 'approve'; const Icon = approved ? CheckCircle2 : MessageSquare; @@ -70,7 +65,6 @@ export function VerdictPill({ verdict }: { verdict: NonNullable.png - * redirect hop can fail, and falls back to an initial. No `loading="lazy"`: intersection - * detection is unreliable inside the app's scroll containers. - */ +// Hits avatars.githubusercontent.com directly: the github.com/.png redirect can fail. +// No loading="lazy": intersection detection is unreliable in this app's scroll containers. export function AuthorAvatar({ login, size = 20 }: { login: string | null; size?: number }) { const [failed, setFailed] = useState(false); const box = { width: size, height: size }; @@ -166,7 +157,6 @@ export function EmptyValue() { return -; } -/** File path in the table's mono style: the directory recedes, the basename carries the weight. */ export function MonoPath({ path, className }: { path: string; className?: string }) { const slash = path.lastIndexOf('/'); const dir = slash === -1 ? '' : path.slice(0, slash + 1); diff --git a/src/client/components/features/job-detail/job-diffs.tsx b/src/client/components/features/job-detail/job-diffs.tsx index 7f65324f..0543780c 100644 --- a/src/client/components/features/job-detail/job-diffs.tsx +++ b/src/client/components/features/job-detail/job-diffs.tsx @@ -9,10 +9,10 @@ import { Info, } from 'lucide-react'; import { api } from '@client/lib/api'; -import { buildTree } from '@codra/ui/file-tree'; -import { diffStats } from '@codra/ui/prompt-diff'; +import { buildTree } from '@codraoss/ui/file-tree'; +import { diffStats } from '@codraoss/ui/prompt-diff'; import { readDiffsCache, writeDiffsCache } from '@client/lib/diffs-cache'; -import type { FileReviewRecord, JobDetail } from '@codra/schema'; +import type { FileReviewRecord, JobDetail } from '@codraoss/schema'; import { FileDiff } from './diff-file-panel'; import { panelCvStyle, fileAnchorId } from './diff-file-panel-utils'; diff --git a/src/client/components/features/job-detail/job-findings-list.tsx b/src/client/components/features/job-detail/job-findings-list.tsx index 38256d3e..0191abb0 100644 --- a/src/client/components/features/job-detail/job-findings-list.tsx +++ b/src/client/components/features/job-detail/job-findings-list.tsx @@ -1,8 +1,8 @@ import { useState, type ReactNode } from 'react'; import { FileText } from 'lucide-react'; -import type { JobDetail } from '@codra/schema'; -import { reviewSeverities } from '@codra/schema/review-limits'; -import { Tabs, TabsList, TabsTrigger } from '@codra/ui/motion'; +import type { JobDetail } from '@codraoss/schema'; +import { reviewSeverities } from '@codraoss/schema/review-limits'; +import { Tabs, TabsList, TabsTrigger } from '@codraoss/ui/motion'; import { FileFinding } from './file-finding'; import { CommentCard } from './comment-card'; import { severityConfig } from './constants'; diff --git a/src/client/components/features/job-detail/job-header.tsx b/src/client/components/features/job-detail/job-header.tsx index 2cbafbb4..081fddf3 100644 --- a/src/client/components/features/job-detail/job-header.tsx +++ b/src/client/components/features/job-detail/job-header.tsx @@ -1,4 +1,4 @@ -import { Button, ConfirmDialog } from '@codra/ui'; +import { Button, ConfirmDialog } from '@codraoss/ui'; import { useState } from 'react'; import type { ComponentType } from 'react'; import { Link } from 'react-router-dom'; @@ -14,11 +14,11 @@ import { Terminal, Trash2, } from 'lucide-react'; -import type { ButtonProps } from '@codra/ui'; +import type { ButtonProps } from '@codraoss/ui'; import { UpdatesEmailPrompt } from '@client/components/features/dashboard/updates-email-prompt'; import { AuthorChip, JobStatusLine, MetaChip, VerdictPill } from './job-chips'; import { formatAbsoluteDate, formatRelativeDate } from './job-chip-utils'; -import type { JobDetail } from '@codra/schema'; +import type { JobDetail } from '@codraoss/schema'; // Lucide's CircleStop strokes the inner square too, which reads as a blob at 14px; filling it // instead keeps the stop symbol legible. diff --git a/src/client/components/features/job-detail/job-meta-cards.tsx b/src/client/components/features/job-detail/job-meta-cards.tsx index e6e4d6e9..1ca022e9 100644 --- a/src/client/components/features/job-detail/job-meta-cards.tsx +++ b/src/client/components/features/job-detail/job-meta-cards.tsx @@ -1,8 +1,8 @@ import type { ReactNode } from 'react'; import { AtSign, ExternalLink, Info, ListChecks, RotateCcw, Zap } from 'lucide-react'; import { Link } from 'react-router-dom'; -import { cn, formatPreciseDuration } from '@codra/ui/utils'; -import type { JobDetail, JobStep } from '@codra/schema'; +import { cn, formatPreciseDuration } from '@codraoss/ui/utils'; +import type { JobDetail, JobStep } from '@codraoss/schema'; import { EmptyValue, JobStatusLine, diff --git a/src/client/components/features/job-detail/job-progress.tsx b/src/client/components/features/job-detail/job-progress.tsx index dca35f20..97e42fb5 100644 --- a/src/client/components/features/job-detail/job-progress.tsx +++ b/src/client/components/features/job-detail/job-progress.tsx @@ -1,5 +1,5 @@ import { FileCode2, Hourglass } from 'lucide-react'; -import type { JobDetail } from '@codra/schema'; +import type { JobDetail } from '@codraoss/schema'; interface JobProgressProps { job: JobDetail; diff --git a/src/client/components/features/job-detail/job-review-overview.tsx b/src/client/components/features/job-detail/job-review-overview.tsx index 435f8ce9..d3e875e1 100644 --- a/src/client/components/features/job-detail/job-review-overview.tsx +++ b/src/client/components/features/job-detail/job-review-overview.tsx @@ -1,11 +1,11 @@ import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { CheckCircle2, ClipboardList, TriangleAlert } from 'lucide-react'; -import type { JobDetail } from '@codra/schema'; -import { reviewSeverities } from '@codra/schema/review-limits'; +import type { JobDetail } from '@codraoss/schema'; +import { reviewSeverities } from '@codraoss/schema/review-limits'; import { OutlinePill } from './job-chips'; -import { safeRehypePlugins } from '@codra/ui/markdown-plugins'; +import { safeRehypePlugins } from '@codraoss/ui/markdown-plugins'; interface JobReviewOverviewProps { job: JobDetail; } diff --git a/src/client/components/features/job-detail/job-skeleton.tsx b/src/client/components/features/job-detail/job-skeleton.tsx index d54412b6..03538968 100644 --- a/src/client/components/features/job-detail/job-skeleton.tsx +++ b/src/client/components/features/job-detail/job-skeleton.tsx @@ -1,4 +1,4 @@ -import { LoadError, Skeleton } from '@codra/ui'; +import { LoadError, Skeleton } from '@codraoss/ui'; import { Link } from 'react-router-dom'; import { ChevronRight, ClipboardList, FileDiff, Info, ListChecks } from 'lucide-react'; import { DETAIL_LABEL, DETAIL_ROW } from './job-chip-utils'; diff --git a/src/client/components/features/job-detail/status-badge.tsx b/src/client/components/features/job-detail/status-badge.tsx index f53f4c7b..d63f7eaa 100644 --- a/src/client/components/features/job-detail/status-badge.tsx +++ b/src/client/components/features/job-detail/status-badge.tsx @@ -1,5 +1,5 @@ -import { Badge } from '@codra/ui'; -import type { JobSummary } from '@codra/schema'; +import { Badge } from '@codraoss/ui'; +import type { JobSummary } from '@codraoss/schema'; import { LiveReviewStepper } from '@client/components/features/reviews/live-review-stepper'; type BadgeVariant = 'success' | 'info' | 'warning' | 'danger' | 'neutral'; diff --git a/src/client/components/features/models/model-chain.tsx b/src/client/components/features/models/model-chain.tsx index d5b7156c..8e6ffbb5 100644 --- a/src/client/components/features/models/model-chain.tsx +++ b/src/client/components/features/models/model-chain.tsx @@ -1,6 +1,6 @@ -import { Button, Select } from '@codra/ui'; +import { Button, Select } from '@codraoss/ui'; import { useId, useMemo, useState } from 'react'; -import { cn } from '@codra/ui/utils'; +import { cn } from '@codraoss/ui/utils'; import { Trash2, ListPlus } from 'lucide-react'; import type { diff --git a/src/client/components/features/repos/repo-model-modal.tsx b/src/client/components/features/repos/repo-model-modal.tsx index 4d3015af..9961e1ea 100644 --- a/src/client/components/features/repos/repo-model-modal.tsx +++ b/src/client/components/features/repos/repo-model-modal.tsx @@ -1,10 +1,10 @@ -import { Alert, Button } from '@codra/ui'; +import { Alert, Button } from '@codraoss/ui'; import { useMemo, useState } from 'react'; import { Dialog } from '@base-ui/react/dialog'; import { toast } from 'sonner'; import { api } from '@client/lib/api'; import { Save, RotateCcw, X } from 'lucide-react'; -import type { RepoConfigRecord } from '@codra/schema'; +import type { RepoConfigRecord } from '@codraoss/schema'; import { ModelRouteEditor } from '@client/components/features/models/model-chain'; import { EMPTY_MODEL_ROUTE, diff --git a/src/client/components/features/repos/repo-route.ts b/src/client/components/features/repos/repo-route.ts index c3e5ccff..214e7997 100644 --- a/src/client/components/features/repos/repo-route.ts +++ b/src/client/components/features/repos/repo-route.ts @@ -1,5 +1,5 @@ import { formatDateTime } from '@client/lib/timezone'; -import type { RepoConfig, RepoConfigRecord } from '@codra/schema'; +import type { RepoConfig, RepoConfigRecord } from '@codraoss/schema'; import { EMPTY_MODEL_ROUTE, normalizeModelRoute, routesEqual, type ModelRouteConfig } from '@client/components/features/models/model-route'; // Shared by the repos page, its rows and the strategy dialog, so it can't live in any single one. diff --git a/src/client/components/features/repos/repo-row.tsx b/src/client/components/features/repos/repo-row.tsx index 4c7f50af..6584afe5 100644 --- a/src/client/components/features/repos/repo-row.tsx +++ b/src/client/components/features/repos/repo-row.tsx @@ -1,6 +1,6 @@ -import { Badge, Button, Switch } from '@codra/ui'; +import { Badge, Button, Switch } from '@codraoss/ui'; import { Settings2 } from 'lucide-react'; -import type { RepoConfigRecord } from '@codra/schema'; +import type { RepoConfigRecord } from '@codraoss/schema'; import { describeModelRoute, type ModelOption, type ModelRouteConfig } from '@client/components/features/models/model-route'; import { getRepoRoute, hasMeaningfulCustomStrategy, formatLastActivity, type GlobalModelConfig } from './repo-route'; diff --git a/src/client/components/features/reviews/live-review-stepper.tsx b/src/client/components/features/reviews/live-review-stepper.tsx index 6c1f05b3..37409c8c 100644 --- a/src/client/components/features/reviews/live-review-stepper.tsx +++ b/src/client/components/features/reviews/live-review-stepper.tsx @@ -1,4 +1,4 @@ -import type { JobSummary } from '@codra/schema'; +import type { JobSummary } from '@codraoss/schema'; interface LiveReviewStepperProps { job: JobSummary; diff --git a/src/client/components/features/settings/about-section.tsx b/src/client/components/features/settings/about-section.tsx index d875566e..9e9e32d0 100644 --- a/src/client/components/features/settings/about-section.tsx +++ b/src/client/components/features/settings/about-section.tsx @@ -1,4 +1,4 @@ -import { Badge, LayerCard, SectionCard, Text } from '@codra/ui'; +import { Badge, LayerCard, SectionCard, Text } from '@codraoss/ui'; import pkg from '../../../../../package.json'; import { ExternalLink } from 'lucide-react'; diff --git a/src/client/components/features/settings/default-models-section.tsx b/src/client/components/features/settings/default-models-section.tsx index edbcc2f4..952a3258 100644 --- a/src/client/components/features/settings/default-models-section.tsx +++ b/src/client/components/features/settings/default-models-section.tsx @@ -1,6 +1,6 @@ -import { Skeleton } from '@codra/ui'; +import { Skeleton } from '@codraoss/ui'; import { useMemo } from 'react'; -import type { ModelConfig } from '@codra/schema'; +import type { ModelConfig } from '@codraoss/schema'; import { ModelRouteEditor } from '@client/components/features/models/model-chain'; import type { ModelOption, diff --git a/src/client/components/features/settings/new-provider-form.tsx b/src/client/components/features/settings/new-provider-form.tsx index 5cdc6ad2..2c212a56 100644 --- a/src/client/components/features/settings/new-provider-form.tsx +++ b/src/client/components/features/settings/new-provider-form.tsx @@ -1,4 +1,4 @@ -import { Button, Input, Select } from '@codra/ui'; +import { Button, Input, Select } from '@codraoss/ui'; import type { Dispatch, SetStateAction } from 'react'; import { Plus } from 'lucide-react'; import { FieldLabel } from './field-label'; diff --git a/src/client/components/features/settings/provider-list.tsx b/src/client/components/features/settings/provider-list.tsx index fe04bf94..c797d33f 100644 --- a/src/client/components/features/settings/provider-list.tsx +++ b/src/client/components/features/settings/provider-list.tsx @@ -1,6 +1,6 @@ -import { Skeleton } from '@codra/ui'; +import { Skeleton } from '@codraoss/ui'; import { toast } from 'sonner'; -import type { LlmProvider } from '@codra/schema'; +import type { LlmProvider } from '@codraoss/schema'; import { ProviderRow } from './provider-row'; import type { ProviderDraft } from './settings-support'; diff --git a/src/client/components/features/settings/provider-row.tsx b/src/client/components/features/settings/provider-row.tsx index 29ef4810..191127f0 100644 --- a/src/client/components/features/settings/provider-row.tsx +++ b/src/client/components/features/settings/provider-row.tsx @@ -1,7 +1,7 @@ -import { Badge, Button, Input, Select, Switch } from '@codra/ui'; +import { Badge, Button, Input, Select, Switch } from '@codraoss/ui'; import { ChevronRight, Save, Trash2 } from 'lucide-react'; -import { cn } from '@codra/ui/utils'; -import type { LlmApiFormat, LlmProvider } from '@codra/schema'; +import { cn } from '@codraoss/ui/utils'; +import type { LlmApiFormat, LlmProvider } from '@codraoss/schema'; import { FieldLabel } from './field-label'; import { API_FORMAT_OPTIONS, diff --git a/src/client/components/features/settings/review-section.tsx b/src/client/components/features/settings/review-section.tsx index 5cc20dab..1a5418d8 100644 --- a/src/client/components/features/settings/review-section.tsx +++ b/src/client/components/features/settings/review-section.tsx @@ -1,7 +1,7 @@ -import { ConfirmDialog, Input, SectionCard, Skeleton } from '@codra/ui'; -import { SteppedSlider } from '@codra/ui/motion'; -import type { ReviewSettings } from '@codra/schema'; -import { REVIEW_CONCURRENCY_LIMITS, reviewMaxFilesRange } from '@codra/schema/review-limits'; +import { ConfirmDialog, Input, SectionCard, Skeleton } from '@codraoss/ui'; +import { SteppedSlider } from '@codraoss/ui/motion'; +import type { ReviewSettings } from '@codraoss/schema'; +import { REVIEW_CONCURRENCY_LIMITS, reviewMaxFilesRange } from '@codraoss/schema/review-limits'; import { FieldLabel } from './field-label'; import { CONCURRENCY_LEVEL_LABEL, diff --git a/src/client/components/features/settings/settings-support.ts b/src/client/components/features/settings/settings-support.ts index 27c3f9c1..6be96600 100644 --- a/src/client/components/features/settings/settings-support.ts +++ b/src/client/components/features/settings/settings-support.ts @@ -1,5 +1,5 @@ -import type { LlmApiFormat, LlmProvider } from '@codra/schema'; -import { REVIEW_CONCURRENCY_LIMITS, reviewMaxCommentsOptions, type ReviewConcurrencyLevel } from '@codra/schema/review-limits'; +import type { LlmApiFormat, LlmProvider } from '@codraoss/schema'; +import { REVIEW_CONCURRENCY_LIMITS, reviewMaxCommentsOptions, type ReviewConcurrencyLevel } from '@codraoss/schema/review-limits'; // Pure and render-free, so the settings page and its sections can all depend on it without depending on each other. diff --git a/src/client/components/features/stats/chart-primitives.tsx b/src/client/components/features/stats/chart-primitives.tsx index fd8e1be0..718d50c8 100644 --- a/src/client/components/features/stats/chart-primitives.tsx +++ b/src/client/components/features/stats/chart-primitives.tsx @@ -1,4 +1,4 @@ -import { Skeleton, GraphShell } from '@codra/ui'; +import { Skeleton, GraphShell } from '@codraoss/ui'; import type { ReactNode } from 'react'; import { Activity, Boxes, Coins, FolderGit2, ShieldCheck } from 'lucide-react'; import { formatCompact, formatDayRange } from './chart-support'; diff --git a/src/client/components/features/stats/chart-support.ts b/src/client/components/features/stats/chart-support.ts index 764571ec..6235c777 100644 --- a/src/client/components/features/stats/chart-support.ts +++ b/src/client/components/features/stats/chart-support.ts @@ -1,4 +1,4 @@ -import { fmtNumber } from '@codra/ui/utils'; +import { fmtNumber } from '@codraoss/ui/utils'; import { formatDayLabel } from '@client/lib/timezone'; // Pure and render-free, so the chart components and the grid can share it without Fast Refresh diff --git a/src/client/components/features/stats/metrics-grid-charts.tsx b/src/client/components/features/stats/metrics-grid-charts.tsx index c9ec35ec..1fbea113 100644 --- a/src/client/components/features/stats/metrics-grid-charts.tsx +++ b/src/client/components/features/stats/metrics-grid-charts.tsx @@ -13,7 +13,7 @@ import { YAxis, } from 'recharts'; import { Activity, Boxes, Coins, FolderGit2, ShieldCheck } from 'lucide-react'; -import type { StatsPayload } from '@codra/schema'; +import type { StatsPayload } from '@codraoss/schema'; import { ChartTooltip } from './chart-primitives'; @@ -23,7 +23,7 @@ import { ChartDefs, MeterList, TickMeter -} from '@codra/ui'; +} from '@codraoss/ui'; import { CHART, MONO_STACK, diff --git a/src/client/components/features/stats/metrics-grid.tsx b/src/client/components/features/stats/metrics-grid.tsx index 40ec05d0..36b8548b 100644 --- a/src/client/components/features/stats/metrics-grid.tsx +++ b/src/client/components/features/stats/metrics-grid.tsx @@ -1,5 +1,5 @@ import React, { Suspense } from 'react'; -import type { StatsPayload } from '@codra/schema'; +import type { StatsPayload } from '@codraoss/schema'; import { MetricsGridSkeleton } from './chart-primitives'; // Recharts is only needed once stats have loaded, so it stays out of the initial bundle and the diff --git a/src/client/components/features/stats/overview-stats.tsx b/src/client/components/features/stats/overview-stats.tsx index 0dcb3619..39b3203e 100644 --- a/src/client/components/features/stats/overview-stats.tsx +++ b/src/client/components/features/stats/overview-stats.tsx @@ -1,9 +1,9 @@ import { useMemo } from 'react'; import { Activity, ArrowUpRight, Cpu, MessageSquare } from 'lucide-react'; import { StatsGrid, type StatDelta } from './stats-grid'; -import { fmtStat } from '@codra/ui/utils'; -import { useIsDarkMode } from '@codra/ui/hooks'; -import type { StatsPayload } from '@codra/schema'; +import { fmtStat } from '@codraoss/ui/utils'; +import { useIsDarkMode } from '@codraoss/ui/hooks'; +import type { StatsPayload } from '@codraoss/schema'; interface OverviewStatsProps { stats: StatsPayload | null; diff --git a/src/client/components/features/stats/stats-grid.tsx b/src/client/components/features/stats/stats-grid.tsx index c210add7..8b8db681 100644 --- a/src/client/components/features/stats/stats-grid.tsx +++ b/src/client/components/features/stats/stats-grid.tsx @@ -1,6 +1,6 @@ -import { BarSparkline, Skeleton } from '@codra/ui'; +import { BarSparkline, Skeleton } from '@codraoss/ui'; import * as React from 'react'; -import { cn } from '@codra/ui/utils'; +import { cn } from '@codraoss/ui/utils'; import type { LucideIcon } from 'lucide-react'; export interface StatDelta { diff --git a/src/client/components/features/stats/time-range-select.tsx b/src/client/components/features/stats/time-range-select.tsx index 9ea6a94e..f6dd9d09 100644 --- a/src/client/components/features/stats/time-range-select.tsx +++ b/src/client/components/features/stats/time-range-select.tsx @@ -1,8 +1,8 @@ -import { Select } from '@codra/ui'; +import { Select } from '@codraoss/ui'; import type { CSSProperties } from 'react'; import { Clock } from 'lucide-react'; import { DEFAULT_STATS_DAYS } from '@client/hooks/use-stats-range'; -import { cn } from '@codra/ui/utils'; +import { cn } from '@codraoss/ui/utils'; interface TimeRangeSelectProps { value: number; diff --git a/src/client/components/layout/account-menu.tsx b/src/client/components/layout/account-menu.tsx index 612d6b04..4c939422 100644 --- a/src/client/components/layout/account-menu.tsx +++ b/src/client/components/layout/account-menu.tsx @@ -1,10 +1,10 @@ -import { GithubMark } from '@codra/ui'; +import { GithubMark } from '@codraoss/ui'; import { Link } from 'react-router-dom'; import { useEffect, useRef, useState } from 'react'; import { api } from '@client/lib/api'; import { LogOut, ChevronsUpDown, UserRound } from 'lucide-react'; -import { cn } from '@codra/ui/utils'; -import type { AuthSessionUser } from '@codra/schema/api'; +import { cn } from '@codraoss/ui/utils'; +import type { AuthSessionUser } from '@codraoss/schema/api'; /** * Built from scratch (no shared dropdown primitive): a local popover anchored diff --git a/src/client/components/layout/app-shell.tsx b/src/client/components/layout/app-shell.tsx index 325adb2e..73b52e32 100644 --- a/src/client/components/layout/app-shell.tsx +++ b/src/client/components/layout/app-shell.tsx @@ -1,13 +1,13 @@ import { Outlet, Link } from 'react-router-dom'; import { useEffect, useState } from 'react'; -import { SharedLayoutBg } from '@codra/ui/motion'; +import { SharedLayoutBg } from '@codraoss/ui/motion'; import { api } from '@client/lib/api'; import { LayoutDashboard, AlignLeft, GitBranch, BarChart2, Sun, Moon, Activity, Settings, Star, X, ArrowUpRight } from 'lucide-react'; -import { cn } from '@codra/ui/utils'; -import { useTheme } from '@codra/ui/theme'; +import { cn } from '@codraoss/ui/utils'; +import { useTheme } from '@codraoss/ui/theme'; import codraDark from '@/assets/codra-fullicon-dark.svg'; import codraLight from '@/assets/codra-fullicon-light.svg'; -import type { AuthSessionUser } from '@codra/schema/api'; +import type { AuthSessionUser } from '@codraoss/schema/api'; import { SidebarNavItem } from '@client/components/layout/sidebar-nav-item'; import { AccountMenu } from '@client/components/layout/account-menu'; @@ -33,7 +33,7 @@ export function AppShell() { return () => { cancelled = true; }; }, []); - // Scroll events don't bubble, so listen in the capture phase at the document level and flag whatever scrolled with `data-scrolling` (global CSS keys off it), clearing it ~700ms after scrolling stops. + // Scroll doesn't bubble: listen in capture phase, flag scrolled el with data-scrolling for CSS, clear after 700ms idle. useEffect(() => { const timers = new WeakMap(); const onScroll = (e: Event) => { @@ -54,9 +54,7 @@ export function AppShell() {
{mobileMenuOpen && ( - /* A button so the tap target is a real control, but hidden from the keyboard and the - a11y tree: the drawer already has a focusable "Close menu" X, and a full-viewport - scrim as a tab stop would be an invisible focus target announced twice. */ + /* Hidden from a11y tree: drawer's X is the real focusable close; scrim as a tab stop would double-announce. */